feat: use pubsub to confirm transactions (#12095)

This commit is contained in:
Justin Starry
2020-09-08 13:12:47 +08:00
committed by GitHub
parent 9940870c89
commit 11b199cccf
16 changed files with 276 additions and 367 deletions

6
web3.js/module.d.ts vendored
View File

@ -51,7 +51,7 @@ declare module '@solana/web3.js' {
}; };
export type ConfirmOptions = { export type ConfirmOptions = {
confirmations?: number; commitment?: Commitment;
skipPreflight?: boolean; skipPreflight?: boolean;
}; };
@ -384,8 +384,8 @@ declare module '@solana/web3.js' {
getVoteAccounts(commitment?: Commitment): Promise<VoteAccountStatus>; getVoteAccounts(commitment?: Commitment): Promise<VoteAccountStatus>;
confirmTransaction( confirmTransaction(
signature: TransactionSignature, signature: TransactionSignature,
confirmations?: number, commitment?: Commitment,
): Promise<RpcResponseAndContext<SignatureStatus | null>>; ): Promise<RpcResponseAndContext<SignatureResult>>;
getSlot(commitment?: Commitment): Promise<number>; getSlot(commitment?: Commitment): Promise<number>;
getSlotLeader(commitment?: Commitment): Promise<string>; getSlotLeader(commitment?: Commitment): Promise<string>;
getSignatureStatus( getSignatureStatus(

View File

@ -64,7 +64,7 @@ declare module '@solana/web3.js' {
}; };
declare export type ConfirmOptions = { declare export type ConfirmOptions = {
confirmations: ?number, commitment: ?Commitment,
skipPreflight: ?boolean, skipPreflight: ?boolean,
}; };
@ -388,8 +388,8 @@ declare module '@solana/web3.js' {
getVoteAccounts(commitment: ?Commitment): Promise<VoteAccountStatus>; getVoteAccounts(commitment: ?Commitment): Promise<VoteAccountStatus>;
confirmTransaction( confirmTransaction(
signature: TransactionSignature, signature: TransactionSignature,
confirmations: ?number, commitment: ?Commitment,
): Promise<RpcResponseAndContext<SignatureStatus | null>>; ): Promise<RpcResponseAndContext<SignatureResult>>;
getSlot(commitment: ?Commitment): Promise<number>; getSlot(commitment: ?Commitment): Promise<number>;
getSlotLeader(commitment: ?Commitment): Promise<string>; getSlotLeader(commitment: ?Commitment): Promise<string>;
getSignatureStatus( getSignatureStatus(

View File

@ -14,6 +14,7 @@ import {MS_PER_SLOT} from './timing';
import {Transaction} from './transaction'; import {Transaction} from './transaction';
import {Message} from './message'; import {Message} from './message';
import {sleep} from './util/sleep'; import {sleep} from './util/sleep';
import {promiseTimeout} from './util/promise-timeout';
import {toBuffer} from './util/to-buffer'; import {toBuffer} from './util/to-buffer';
import type {Blockhash} from './blockhash'; import type {Blockhash} from './blockhash';
import type {FeeCalculator} from './fee-calculator'; import type {FeeCalculator} from './fee-calculator';
@ -57,11 +58,11 @@ export type SendOptions = {
* *
* @typedef {Object} ConfirmOptions * @typedef {Object} ConfirmOptions
* @property {boolean | undefined} skipPreflight disable transaction verification step * @property {boolean | undefined} skipPreflight disable transaction verification step
* @property {number | undefined} confirmations desired number of cluster confirmations * @property {Commitment | undefined} commitment desired commitment level
*/ */
export type ConfirmOptions = { export type ConfirmOptions = {
skipPreflight?: boolean, skipPreflight?: boolean,
confirmations?: number, commitment?: Commitment,
}; };
/** /**
@ -1951,39 +1952,74 @@ export class Connection {
/** /**
* Confirm the transaction identified by the specified signature. * Confirm the transaction identified by the specified signature.
* *
* If `confirmations` count is not specified, wait for transaction to be finalized. * If `commitment` is not specified, default to 'max'.
*
*/ */
async confirmTransaction( async confirmTransaction(
signature: TransactionSignature, signature: TransactionSignature,
confirmations: ?number, commitment: ?Commitment,
): Promise<RpcResponseAndContext<SignatureStatus | null>> { ): Promise<RpcResponseAndContext<SignatureResult>> {
let decodedSignature;
try {
decodedSignature = bs58.decode(signature);
} catch (err) {
throw new Error('signature must be base58 encoded: ' + signature);
}
assert(decodedSignature.length === 64, 'signature has invalid length');
const start = Date.now(); const start = Date.now();
const WAIT_TIMEOUT_MS = 60 * 1000; const subscriptionCommitment: Commitment = commitment || 'max';
let statusResponse = await this.getSignatureStatus(signature); let subscriptionId;
for (;;) { let response: RpcResponseAndContext<SignatureResult> | null = null;
const status = statusResponse.value; const confirmPromise = new Promise((resolve, reject) => {
if (status) { try {
// 'status.confirmations === null' implies that the tx has been finalized subscriptionId = this.onSignature(
if ( signature,
status.err || (result, context) => {
status.confirmations === null || subscriptionId = undefined;
(typeof confirmations === 'number' && response = {
status.confirmations >= confirmations) context,
) { value: result,
};
resolve();
},
subscriptionCommitment,
);
} catch (err) {
reject(err);
}
});
let timeoutMs = 60 * 1000;
switch (subscriptionCommitment) {
case 'recent':
case 'single':
case 'singleGossip': {
timeoutMs = 10 * 1000;
break; break;
} }
} else if (Date.now() - start >= WAIT_TIMEOUT_MS) { // exhaust enums to ensure full coverage
break; case 'max':
case 'root':
} }
// Sleep for approximately one slot try {
await sleep(MS_PER_SLOT); await promiseTimeout(confirmPromise, timeoutMs);
statusResponse = await this.getSignatureStatus(signature); } finally {
if (subscriptionId) {
this.removeSignatureListener(subscriptionId);
}
} }
return statusResponse; if (response === null) {
const duration = (Date.now() - start) / 1000;
throw new Error(
`Transaction was not confirmed in ${duration.toFixed(2)} seconds`,
);
}
return response;
} }
/** /**

View File

@ -4,7 +4,6 @@ import * as BufferLayout from 'buffer-layout';
import {Account} from './account'; import {Account} from './account';
import {PublicKey} from './publickey'; import {PublicKey} from './publickey';
import {NUM_TICKS_PER_SECOND} from './timing';
import {Transaction, PACKET_DATA_SIZE} from './transaction'; import {Transaction, PACKET_DATA_SIZE} from './transaction';
import {SYSVAR_RENT_PUBKEY} from './sysvar'; import {SYSVAR_RENT_PUBKEY} from './sysvar';
import {sendAndConfirmTransaction} from './util/send-and-confirm-transaction'; import {sendAndConfirmTransaction} from './util/send-and-confirm-transaction';
@ -70,7 +69,7 @@ export class Loader {
transaction, transaction,
[payer, program], [payer, program],
{ {
confirmations: 1, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}, },
); );
@ -111,17 +110,17 @@ export class Loader {
}); });
transactions.push( transactions.push(
sendAndConfirmTransaction(connection, transaction, [payer, program], { sendAndConfirmTransaction(connection, transaction, [payer, program], {
confirmations: 1, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}), }),
); );
// Delay ~1 tick between write transactions in an attempt to reduce AccountInUse errors // Delay between sends in an attempt to reduce rate limit errors
// since all the write transactions modify the same program account const REQUESTS_PER_SECOND = 4;
await sleep(1000 / NUM_TICKS_PER_SECOND); await sleep(1000 / REQUESTS_PER_SECOND);
// Run up to 8 Loads in parallel to prevent too many parallel transactions from // Run up to 8 Loads in parallel to prevent too many parallel transactions from
// getting rejected with AccountInUse. // getting retried due to AccountInUse errors.
// //
// TODO: 8 was selected empirically and should probably be revisited // TODO: 8 was selected empirically and should probably be revisited
if (transactions.length === 8) { if (transactions.length === 8) {
@ -159,7 +158,7 @@ export class Loader {
transaction, transaction,
[payer, program], [payer, program],
{ {
confirmations: 1, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}, },
); );

View File

@ -0,0 +1,16 @@
// @flow
export function promiseTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
): Promise<T | null> {
let timeoutId: TimeoutID;
const timeoutPromise = new Promise(resolve => {
timeoutId = setTimeout(() => resolve(null), timeoutMs);
});
return Promise.race([promise, timeoutPromise]).then(result => {
clearTimeout(timeoutId);
return result;
});
}

View File

@ -7,7 +7,7 @@ import type {ConfirmOptions} from '../connection';
/** /**
* Send and confirm a raw transaction * Send and confirm a raw transaction
* *
* If `confirmations` count is not specified, wait for transaction to be finalized. * If `commitment` option is not specified, defaults to 'max' commitment.
* *
* @param {Connection} connection * @param {Connection} connection
* @param {Buffer} rawTransaction * @param {Buffer} rawTransaction
@ -19,31 +19,23 @@ export async function sendAndConfirmRawTransaction(
rawTransaction: Buffer, rawTransaction: Buffer,
options?: ConfirmOptions, options?: ConfirmOptions,
): Promise<TransactionSignature> { ): Promise<TransactionSignature> {
const start = Date.now();
const signature = await connection.sendRawTransaction( const signature = await connection.sendRawTransaction(
rawTransaction, rawTransaction,
options, options,
); );
const status = ( const status = (
await connection.confirmTransaction( await connection.confirmTransaction(
signature, signature,
options && options.confirmations, options && options.commitment,
) )
).value; ).value;
if (status) {
if (status.err) { if (status.err) {
throw new Error( throw new Error(
`Raw transaction ${signature} failed (${JSON.stringify(status)})`, `Raw transaction ${signature} failed (${JSON.stringify(status)})`,
); );
} }
return signature;
}
const duration = (Date.now() - start) / 1000; return signature;
throw new Error(
`Raw transaction '${signature}' was not confirmed in ${duration.toFixed(
2,
)} seconds`,
);
} }

View File

@ -9,7 +9,7 @@ import type {TransactionSignature} from '../transaction';
/** /**
* Sign, send and confirm a transaction. * Sign, send and confirm a transaction.
* *
* If `confirmations` count is not specified, wait for transaction to be finalized. * If `commitment` option is not specified, defaults to 'max' commitment.
* *
* @param {Connection} connection * @param {Connection} connection
* @param {Transaction} transaction * @param {Transaction} transaction
@ -23,32 +23,24 @@ export async function sendAndConfirmTransaction(
signers: Array<Account>, signers: Array<Account>,
options?: ConfirmOptions, options?: ConfirmOptions,
): Promise<TransactionSignature> { ): Promise<TransactionSignature> {
const start = Date.now();
const signature = await connection.sendTransaction( const signature = await connection.sendTransaction(
transaction, transaction,
signers, signers,
options, options,
); );
const status = ( const status = (
await connection.confirmTransaction( await connection.confirmTransaction(
signature, signature,
options && options.confirmations, options && options.commitment,
) )
).value; ).value;
if (status) {
if (status.err) { if (status.err) {
throw new Error( throw new Error(
`Transaction ${signature} failed (${JSON.stringify(status)})`, `Transaction ${signature} failed (${JSON.stringify(status)})`,
); );
} }
return signature;
}
const duration = (Date.now() - start) / 1000; return signature;
throw new Error(
`Transaction was not confirmed in ${duration.toFixed(
2,
)} seconds (${JSON.stringify(status)})`,
);
} }

View File

@ -1,29 +1,75 @@
// @flow // @flow
import {Client as LiveClient} from 'rpc-websockets'; import {Client as LiveClient} from 'rpc-websockets';
import EventEmitter from 'events';
type RpcRequest = {
method: string,
params?: Array<any>,
};
type RpcResponse = {
context: {
slot: number,
},
value: any,
};
// Define TEST_LIVE in the environment to test against the real full node // Define TEST_LIVE in the environment to test against the real full node
// identified by `url` instead of using the mock // identified by `url` instead of using the mock
export const mockRpcEnabled = !process.env.TEST_LIVE; export const mockRpcEnabled = !process.env.TEST_LIVE;
let mockNotice = true; export const mockRpcSocket: Array<[RpcRequest, RpcResponse]> = [];
class MockClient { class MockClient extends EventEmitter {
constructor(url: string) { mockOpen = false;
if (mockNotice) { subscriptionCounter = 0;
console.log(
'Note: rpc-websockets mock is disabled, testing live against', constructor() {
url, super();
); }
mockNotice = false;
connect() {
if (!this.mockOpen) {
this.mockOpen = true;
this.emit('open');
} }
} }
connect() {} close() {
close() {} if (this.mockOpen) {
on() {} this.mockOpen = false;
call(): Promise<Object> { this.emit('close');
throw new Error('call unsupported'); }
}
notify(): Promise<any> {
return Promise.resolve();
}
on(event: string, callback: Function): this {
return super.on(event, callback);
}
call(method: string, params: Array<any>): Promise<Object> {
expect(mockRpcSocket.length).toBeGreaterThanOrEqual(1);
const [mockRequest, mockResponse] = mockRpcSocket.shift();
expect(method).toBe(mockRequest.method);
expect(params).toMatchObject(mockRequest.params);
let id = this.subscriptionCounter++;
const response = {
subscription: id,
result: mockResponse,
};
setImmediate(() => {
const eventName = method.replace('Subscribe', 'Notification');
this.emit(eventName, response);
});
return Promise.resolve(id);
} }
} }

View File

@ -1,6 +1,5 @@
// @flow // @flow
import bs58 from 'bs58';
import fs from 'mz/fs'; import fs from 'mz/fs';
import { import {
@ -47,7 +46,7 @@ test('load BPF C program', async () => {
programId: program.publicKey, programId: program.publicKey,
}); });
await sendAndConfirmTransaction(connection, transaction, [from], { await sendAndConfirmTransaction(connection, transaction, [from], {
confirmations: 1, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}); });
}); });
@ -90,22 +89,22 @@ describe('load BPF Rust program', () => {
data, data,
BPF_LOADER_PROGRAM_ID, BPF_LOADER_PROGRAM_ID,
); );
const transaction = new Transaction().add({ const transaction = new Transaction().add({
keys: [ keys: [
{pubkey: payerAccount.publicKey, isSigner: true, isWritable: true}, {pubkey: payerAccount.publicKey, isSigner: true, isWritable: true},
], ],
programId: program.publicKey, programId: program.publicKey,
}); });
await sendAndConfirmTransaction(connection, transaction, [payerAccount], {
signature = await sendAndConfirmTransaction(
connection,
transaction,
[payerAccount],
{
skipPreflight: true, skipPreflight: true,
}); },
);
if (transaction.signature === null) {
expect(transaction.signature).not.toBeNull();
return;
}
signature = bs58.encode(transaction.signature);
}); });
test('get confirmed transaction', async () => { test('get confirmed transaction', async () => {

View File

@ -19,8 +19,9 @@ import {BLOCKHASH_CACHE_TIMEOUT_MS} from '../src/connection';
import type {TransactionSignature} from '../src/transaction'; import type {TransactionSignature} from '../src/transaction';
import type {SignatureStatus, TransactionError} from '../src/connection'; import type {SignatureStatus, TransactionError} from '../src/connection';
import {mockConfirmTransaction} from './mockrpc/confirm-transaction'; import {mockConfirmTransaction} from './mockrpc/confirm-transaction';
import {mockRpcSocket} from './__mocks__/rpc-websockets';
// Testing blockhash cache takes around 30s to complete // Testing tokens and blockhash cache each take around 30s to complete
jest.setTimeout(40000); jest.setTimeout(40000);
const errorMessage = 'Invalid'; const errorMessage = 'Invalid';
@ -99,7 +100,7 @@ test('get program accounts', async () => {
{ {
error: null, error: null,
result: result:
'0WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', '2WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
}, },
]); ]);
mockRpc.push([ mockRpc.push([
@ -111,7 +112,7 @@ test('get program accounts', async () => {
{ {
error: null, error: null,
result: result:
'0WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', '2WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
}, },
]); ]);
await connection.requestAirdrop(account0.publicKey, LAMPORTS_PER_SOL); await connection.requestAirdrop(account0.publicKey, LAMPORTS_PER_SOL);
@ -129,39 +130,17 @@ test('get program accounts', async () => {
'3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', '3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
}, },
]); ]);
mockRpc.push([
url,
{
method: 'getSignatureStatuses',
params: [
[
'3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
],
],
},
{
error: null,
result: {
context: {
slot: 11,
},
value: [
{
slot: 0,
confirmations: 11,
status: {Ok: null},
err: null,
},
],
},
},
]);
let transaction = SystemProgram.assign({ let transaction = SystemProgram.assign({
accountPubkey: account0.publicKey, accountPubkey: account0.publicKey,
programId: programId.publicKey, programId: programId.publicKey,
}); });
mockConfirmTransaction(
'3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
);
await sendAndConfirmTransaction(connection, transaction, [account0], { await sendAndConfirmTransaction(connection, transaction, [account0], {
confirmations: 1, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}); });
@ -176,41 +155,17 @@ test('get program accounts', async () => {
'3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', '3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
}, },
]); ]);
mockRpc.push([
url,
{
method: 'getSignatureStatuses',
params: [
[
'3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
],
],
},
{
error: null,
result: {
context: {
slot: 11,
},
value: [
{
slot: 0,
confirmations: 11,
status: {Ok: null},
err: null,
},
],
},
},
]);
transaction = SystemProgram.assign({ transaction = SystemProgram.assign({
accountPubkey: account1.publicKey, accountPubkey: account1.publicKey,
programId: programId.publicKey, programId: programId.publicKey,
}); });
mockConfirmTransaction(
'3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
);
await sendAndConfirmTransaction(connection, transaction, [account1], { await sendAndConfirmTransaction(connection, transaction, [account1], {
confirmations: 1, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}); });
@ -607,18 +562,9 @@ test('confirm transaction - error', async () => {
const badTransactionSignature = 'bad transaction signature'; const badTransactionSignature = 'bad transaction signature';
mockRpc.push([
url,
{
method: 'getSignatureStatuses',
params: [[badTransactionSignature]],
},
errorResponse,
]);
await expect( await expect(
connection.confirmTransaction(badTransactionSignature), connection.confirmTransaction(badTransactionSignature),
).rejects.toThrow(errorMessage); ).rejects.toThrow('signature must be base58 encoded');
mockRpc.push([ mockRpc.push([
url, url,
@ -1348,7 +1294,7 @@ describe('token methods', () => {
const payerAccount = new Account(); const payerAccount = new Account();
await connection.confirmTransaction( await connection.confirmTransaction(
await connection.requestAirdrop(payerAccount.publicKey, 100000000000), await connection.requestAirdrop(payerAccount.publicKey, 100000000000),
0, 'single',
); );
const mintOwner = new Account(); const mintOwner = new Account();
@ -1628,35 +1574,8 @@ test('request airdrop', async () => {
minimumAmount + 42, minimumAmount + 42,
); );
mockRpc.push([ mockConfirmTransaction(signature);
url, await connection.confirmTransaction(signature, 'single');
{
method: 'getSignatureStatuses',
params: [
[
'1WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
],
],
},
{
error: null,
result: {
context: {
slot: 11,
},
value: [
{
slot: 0,
confirmations: null,
status: {Ok: null},
err: null,
},
],
},
},
]);
await connection.confirmTransaction(signature, 0);
mockRpc.push([ mockRpc.push([
url, url,
@ -1782,7 +1701,7 @@ test('transaction failure', async () => {
{ {
error: null, error: null,
result: result:
'0WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', '2WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
}, },
]); ]);
const airdropSignature = await connection.requestAirdrop( const airdropSignature = await connection.requestAirdrop(
@ -1791,7 +1710,7 @@ test('transaction failure', async () => {
); );
mockConfirmTransaction(airdropSignature); mockConfirmTransaction(airdropSignature);
await connection.confirmTransaction(airdropSignature, 0); await connection.confirmTransaction(airdropSignature, 'single');
mockRpc.push([ mockRpc.push([
url, url,
@ -1826,33 +1745,6 @@ test('transaction failure', async () => {
}, },
]); ]);
mockRpc.push([
url,
{
method: 'getSignatureStatuses',
params: [
[
'3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
],
],
},
{
error: null,
result: {
context: {
slot: 11,
},
value: [
{
slot: 0,
confirmations: 1,
err: null,
},
],
},
},
]);
const newAccount = new Account(); const newAccount = new Account();
let transaction = SystemProgram.createAccount({ let transaction = SystemProgram.createAccount({
fromPubkey: account.publicKey, fromPubkey: account.publicKey,
@ -1861,11 +1753,15 @@ test('transaction failure', async () => {
space: 0, space: 0,
programId: SystemProgram.programId, programId: SystemProgram.programId,
}); });
mockConfirmTransaction(
'3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
);
await sendAndConfirmTransaction( await sendAndConfirmTransaction(
connection, connection,
transaction, transaction,
[account, newAccount], [account, newAccount],
{confirmations: 1, skipPreflight: true}, {commitment: 'single', skipPreflight: true},
); );
mockRpc.push([ mockRpc.push([
@ -1895,38 +1791,24 @@ test('transaction failure', async () => {
); );
const expectedErr = {InstructionError: [0, {Custom: 0}]}; const expectedErr = {InstructionError: [0, {Custom: 0}]};
mockRpc.push([ mockRpcSocket.push([
url,
{ {
method: 'getSignatureStatuses', method: 'signatureSubscribe',
params: [ params: [signature, {commitment: 'single'}],
[
'3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
],
],
}, },
{ {
error: null,
result: {
context: { context: {
slot: 11, slot: 11,
}, },
value: [ value: {err: expectedErr},
{
slot: 0,
confirmations: 1,
status: {Err: expectedErr},
err: expectedErr,
},
],
},
}, },
]); ]);
// Wait for one confirmation // Wait for one confirmation
const confirmResult = (await connection.confirmTransaction(signature, 1)) const confirmResult = (
.value; await connection.confirmTransaction(signature, 'single')
verifySignatureStatus(confirmResult, expectedErr); ).value;
expect(confirmResult.err).toEqual(expectedErr);
mockRpc.push([ mockRpc.push([
url, url,
@ -1991,15 +1873,16 @@ test('transaction', async () => {
{ {
error: null, error: null,
result: result:
'0WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', '2WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
}, },
]); ]);
const airdropFromSig = await connection.requestAirdrop( const airdropFromSig = await connection.requestAirdrop(
accountFrom.publicKey, accountFrom.publicKey,
minimumAmount + 100010, minimumAmount + 100010,
); );
mockConfirmTransaction(airdropFromSig); mockConfirmTransaction(airdropFromSig);
await connection.confirmTransaction(airdropFromSig, 0); await connection.confirmTransaction(airdropFromSig, 'single');
mockRpc.push([ mockRpc.push([
url, url,
@ -2033,33 +1916,15 @@ test('transaction', async () => {
'8WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', '8WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
}, },
]); ]);
mockRpc.push([
url, const airdropToSig = await connection.requestAirdrop(
{ accountTo.publicKey,
method: 'getSignatureStatuses', minimumAmount,
params: [ );
[
'8WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', mockConfirmTransaction(airdropToSig);
], await connection.confirmTransaction(airdropToSig, 'single');
],
},
{
error: null,
result: {
context: {
slot: 11,
},
value: [
{
slot: 0,
confirmations: 0,
status: {Ok: null},
err: null,
},
],
},
},
]);
mockRpc.push([ mockRpc.push([
url, url,
{ {
@ -2076,11 +1941,7 @@ test('transaction', async () => {
}, },
}, },
]); ]);
const airdropToSig = await connection.requestAirdrop(
accountTo.publicKey,
minimumAmount,
);
await connection.confirmTransaction(airdropToSig, 0);
expect(await connection.getBalance(accountTo.publicKey)).toBe(minimumAmount); expect(await connection.getBalance(accountTo.publicKey)).toBe(minimumAmount);
mockGetRecentBlockhash('max'); mockGetRecentBlockhash('max');
@ -2108,8 +1969,9 @@ test('transaction', async () => {
); );
mockConfirmTransaction(signature); mockConfirmTransaction(signature);
let confirmResult = (await connection.confirmTransaction(signature, 0)).value; let confirmResult = (await connection.confirmTransaction(signature, 'single'))
verifySignatureStatus(confirmResult); .value;
expect(confirmResult.err).toBeNull();
mockGetRecentBlockhash('max'); mockGetRecentBlockhash('max');
mockRpc.push([ mockRpc.push([
@ -2140,7 +2002,7 @@ test('transaction', async () => {
expect(transaction.recentBlockhash).not.toEqual(transaction2.recentBlockhash); expect(transaction.recentBlockhash).not.toEqual(transaction2.recentBlockhash);
mockConfirmTransaction(signature2); mockConfirmTransaction(signature2);
await connection.confirmTransaction(signature2, 0); await connection.confirmTransaction(signature2, 'single');
mockRpc.push([ mockRpc.push([
url, url,
@ -2170,7 +2032,7 @@ test('transaction', async () => {
expect(transaction2.recentBlockhash).toEqual(transaction3.recentBlockhash); expect(transaction2.recentBlockhash).toEqual(transaction3.recentBlockhash);
mockConfirmTransaction(signature3); mockConfirmTransaction(signature3);
await connection.confirmTransaction(signature3, 0); await connection.confirmTransaction(signature3, 'single');
// Sleep until blockhash cache times out // Sleep until blockhash cache times out
await sleep( await sleep(
@ -2204,7 +2066,7 @@ test('transaction', async () => {
}, },
); );
mockConfirmTransaction(signature4); mockConfirmTransaction(signature4);
await connection.confirmTransaction(signature4, 0); await connection.confirmTransaction(signature4, 'single');
expect(transaction4.recentBlockhash).not.toEqual( expect(transaction4.recentBlockhash).not.toEqual(
transaction3.recentBlockhash, transaction3.recentBlockhash,
@ -2267,7 +2129,7 @@ test('multi-instruction transaction', async () => {
accountFrom.publicKey, accountFrom.publicKey,
LAMPORTS_PER_SOL, LAMPORTS_PER_SOL,
); );
await connection.confirmTransaction(signature, 0); await connection.confirmTransaction(signature, 'single');
expect(await connection.getBalance(accountFrom.publicKey)).toBe( expect(await connection.getBalance(accountFrom.publicKey)).toBe(
LAMPORTS_PER_SOL, LAMPORTS_PER_SOL,
); );
@ -2281,7 +2143,7 @@ test('multi-instruction transaction', async () => {
accountTo.publicKey, accountTo.publicKey,
minimumAmount + 21, minimumAmount + 21,
); );
await connection.confirmTransaction(signature, 0); await connection.confirmTransaction(signature, 'single');
expect(await connection.getBalance(accountTo.publicKey)).toBe( expect(await connection.getBalance(accountTo.publicKey)).toBe(
minimumAmount + 21, minimumAmount + 21,
); );
@ -2305,7 +2167,7 @@ test('multi-instruction transaction', async () => {
{skipPreflight: true}, {skipPreflight: true},
); );
await connection.confirmTransaction(signature, 1); await connection.confirmTransaction(signature, 'single');
const response = (await connection.getSignatureStatus(signature)).value; const response = (await connection.getSignatureStatus(signature)).value;
if (response !== null) { if (response !== null) {
@ -2357,7 +2219,7 @@ test('account change notification', async () => {
lamports: balanceNeeded, lamports: balanceNeeded,
}); });
await sendAndConfirmTransaction(connection, transaction, [owner], { await sendAndConfirmTransaction(connection, transaction, [owner], {
confirmations: 1, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}); });
} catch (err) { } catch (err) {
@ -2424,7 +2286,7 @@ test('program account change notification', async () => {
lamports: balanceNeeded, lamports: balanceNeeded,
}); });
await sendAndConfirmTransaction(connection, transaction, [owner], { await sendAndConfirmTransaction(connection, transaction, [owner], {
confirmations: 1, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}); });
} catch (err) { } catch (err) {

View File

@ -1,31 +1,19 @@
// @flow // @flow
import type {TransactionSignature} from '../../src/transaction'; import type {TransactionSignature} from '../../src/transaction';
import {url} from '../url'; import {mockRpcSocket} from '../__mocks__/rpc-websockets';
import {mockRpc} from '../__mocks__/node-fetch';
export function mockConfirmTransaction(signature: TransactionSignature) { export function mockConfirmTransaction(signature: TransactionSignature) {
mockRpc.push([ mockRpcSocket.push([
url,
{ {
method: 'getSignatureStatuses', method: 'signatureSubscribe',
params: [[signature]], params: [signature, {commitment: 'single'}],
}, },
{ {
error: null,
result: {
context: { context: {
slot: 11, slot: 11,
}, },
value: [ value: {err: null},
{
slot: 0,
confirmations: null,
status: {Ok: null},
err: null,
},
],
},
}, },
]); ]);
} }

View File

@ -26,6 +26,11 @@ export async function newAccountWithLamports(
]); ]);
} }
await connection.requestAirdrop(account.publicKey, lamports); const signature = await connection.requestAirdrop(
account.publicKey,
lamports,
);
await connection.confirmTransaction(signature, 'single');
return account; return account;
} }

View File

@ -64,7 +64,7 @@ test('create and query nonce account', async () => {
minimumAmount * 2, minimumAmount * 2,
); );
mockConfirmTransaction(signature); mockConfirmTransaction(signature);
await connection.confirmTransaction(signature, 0); await connection.confirmTransaction(signature, 'single');
mockRpc.push([ mockRpc.push([
url, url,
@ -113,7 +113,7 @@ test('create and query nonce account', async () => {
}, },
); );
mockConfirmTransaction(nonceSignature); mockConfirmTransaction(nonceSignature);
await connection.confirmTransaction(nonceSignature, 0); await connection.confirmTransaction(nonceSignature, 'single');
mockRpc.push([ mockRpc.push([
url, url,
@ -193,7 +193,7 @@ test('create and query nonce account with seed', async () => {
minimumAmount * 2, minimumAmount * 2,
); );
mockConfirmTransaction(signature); mockConfirmTransaction(signature);
await connection.confirmTransaction(signature, 0); await connection.confirmTransaction(signature, 'single');
mockRpc.push([ mockRpc.push([
url, url,
@ -240,7 +240,7 @@ test('create and query nonce account with seed', async () => {
skipPreflight: true, skipPreflight: true,
}); });
mockConfirmTransaction(nonceSignature); mockConfirmTransaction(nonceSignature);
await connection.confirmTransaction(nonceSignature, 0); await connection.confirmTransaction(nonceSignature, 'single');
mockRpc.push([ mockRpc.push([
url, url,

View File

@ -295,7 +295,7 @@ test('live staking actions', async () => {
connection, connection,
createAndInitialize, createAndInitialize,
[from, newStakeAccount], [from, newStakeAccount],
{confirmations: 0, skipPreflight: true}, {commitment: 'single', skipPreflight: true},
); );
expect(await connection.getBalance(newStakeAccount.publicKey)).toEqual( expect(await connection.getBalance(newStakeAccount.publicKey)).toEqual(
minimumAmount + 42, minimumAmount + 42,
@ -307,7 +307,7 @@ test('live staking actions', async () => {
votePubkey, votePubkey,
}); });
await sendAndConfirmTransaction(connection, delegation, [authorized], { await sendAndConfirmTransaction(connection, delegation, [authorized], {
confirmations: 0, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}); });
} }
@ -334,7 +334,7 @@ test('live staking actions', async () => {
connection, connection,
createAndInitializeWithSeed, createAndInitializeWithSeed,
[from], [from],
{confirmations: 0, skipPreflight: true}, {commitment: 'single', skipPreflight: true},
); );
let originalStakeBalance = await connection.getBalance(newAccountPubkey); let originalStakeBalance = await connection.getBalance(newAccountPubkey);
expect(originalStakeBalance).toEqual(3 * minimumAmount + 42); expect(originalStakeBalance).toEqual(3 * minimumAmount + 42);
@ -345,7 +345,7 @@ test('live staking actions', async () => {
votePubkey, votePubkey,
}); });
await sendAndConfirmTransaction(connection, delegation, [authorized], { await sendAndConfirmTransaction(connection, delegation, [authorized], {
confirmations: 0, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}); });
@ -359,7 +359,7 @@ test('live staking actions', async () => {
}); });
await expect( await expect(
sendAndConfirmTransaction(connection, withdraw, [authorized], { sendAndConfirmTransaction(connection, withdraw, [authorized], {
confirmations: 0, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}), }),
).rejects.toThrow(); ).rejects.toThrow();
@ -373,7 +373,7 @@ test('live staking actions', async () => {
lamports: minimumAmount + 20, lamports: minimumAmount + 20,
}); });
await sendAndConfirmTransaction(connection, split, [authorized, newStake], { await sendAndConfirmTransaction(connection, split, [authorized, newStake], {
confirmations: 0, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}); });
@ -388,7 +388,7 @@ test('live staking actions', async () => {
stakeAuthorizationType: StakeAuthorizationLayout.Withdrawer, stakeAuthorizationType: StakeAuthorizationLayout.Withdrawer,
}); });
await sendAndConfirmTransaction(connection, authorize, [authorized], { await sendAndConfirmTransaction(connection, authorize, [authorized], {
confirmations: 0, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}); });
authorize = StakeProgram.authorize({ authorize = StakeProgram.authorize({
@ -398,7 +398,7 @@ test('live staking actions', async () => {
stakeAuthorizationType: StakeAuthorizationLayout.Staker, stakeAuthorizationType: StakeAuthorizationLayout.Staker,
}); });
await sendAndConfirmTransaction(connection, authorize, [authorized], { await sendAndConfirmTransaction(connection, authorize, [authorized], {
confirmations: 0, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}); });
@ -412,7 +412,7 @@ test('live staking actions', async () => {
connection, connection,
deactivateNotAuthorized, deactivateNotAuthorized,
[authorized], [authorized],
{confirmations: 0, skipPreflight: true}, {commitment: 'single', skipPreflight: true},
), ),
).rejects.toThrow(); ).rejects.toThrow();
@ -422,7 +422,7 @@ test('live staking actions', async () => {
authorizedPubkey: newAuthorized.publicKey, authorizedPubkey: newAuthorized.publicKey,
}); });
await sendAndConfirmTransaction(connection, deactivate, [newAuthorized], { await sendAndConfirmTransaction(connection, deactivate, [newAuthorized], {
confirmations: 0, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}); });
@ -434,7 +434,7 @@ test('live staking actions', async () => {
lamports: minimumAmount + 20, lamports: minimumAmount + 20,
}); });
await sendAndConfirmTransaction(connection, withdraw, [newAuthorized], { await sendAndConfirmTransaction(connection, withdraw, [newAuthorized], {
confirmations: 0, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}); });
const balance = await connection.getBalance(newAccountPubkey); const balance = await connection.getBalance(newAccountPubkey);

View File

@ -290,7 +290,7 @@ test('live Nonce actions', async () => {
connection, connection,
createNonceAccount, createNonceAccount,
[from, nonceAccount], [from, nonceAccount],
{confirmations: 0, skipPreflight: true}, {commitment: 'single', skipPreflight: true},
); );
const nonceBalance = await connection.getBalance(nonceAccount.publicKey); const nonceBalance = await connection.getBalance(nonceAccount.publicKey);
expect(nonceBalance).toEqual(minimumAmount); expect(nonceBalance).toEqual(minimumAmount);
@ -319,7 +319,7 @@ test('live Nonce actions', async () => {
}), }),
); );
await sendAndConfirmTransaction(connection, advanceNonce, [from], { await sendAndConfirmTransaction(connection, advanceNonce, [from], {
confirmations: 0, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}); });
const nonceQuery3 = await connection.getNonce(nonceAccount.publicKey); const nonceQuery3 = await connection.getNonce(nonceAccount.publicKey);
@ -341,7 +341,7 @@ test('live Nonce actions', async () => {
}), }),
); );
await sendAndConfirmTransaction(connection, authorizeNonce, [from], { await sendAndConfirmTransaction(connection, authorizeNonce, [from], {
confirmations: 0, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}); });
@ -359,7 +359,7 @@ test('live Nonce actions', async () => {
}; };
await sendAndConfirmTransaction(connection, transfer, [from, newAuthority], { await sendAndConfirmTransaction(connection, transfer, [from, newAuthority], {
confirmations: 0, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}); });
const toBalance = await connection.getBalance(to.publicKey); const toBalance = await connection.getBalance(to.publicKey);
@ -378,7 +378,7 @@ test('live Nonce actions', async () => {
}), }),
); );
await sendAndConfirmTransaction(connection, withdrawNonce, [newAuthority], { await sendAndConfirmTransaction(connection, withdrawNonce, [newAuthority], {
confirmations: 0, commitment: 'single',
skipPreflight: true, skipPreflight: true,
}); });
expect(await connection.getBalance(nonceAccount.publicKey)).toEqual(0); expect(await connection.getBalance(nonceAccount.publicKey)).toEqual(0);

View File

@ -3,6 +3,7 @@ import {Account, Connection, SystemProgram, LAMPORTS_PER_SOL} from '../src';
import {mockRpc, mockRpcEnabled} from './__mocks__/node-fetch'; import {mockRpc, mockRpcEnabled} from './__mocks__/node-fetch';
import {mockGetRecentBlockhash} from './mockrpc/get-recent-blockhash'; import {mockGetRecentBlockhash} from './mockrpc/get-recent-blockhash';
import {url} from './url'; import {url} from './url';
import {mockConfirmTransaction} from './mockrpc/confirm-transaction';
if (!mockRpcEnabled) { if (!mockRpcEnabled) {
// The default of 5 seconds is too slow for live testing sometimes // The default of 5 seconds is too slow for live testing sometimes
@ -99,35 +100,8 @@ test('transaction-payer', async () => {
{skipPreflight: true}, {skipPreflight: true},
); );
mockRpc.push([ mockConfirmTransaction(signature);
url, await connection.confirmTransaction(signature, 'single');
{
method: 'getSignatureStatuses',
params: [
[
'3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
],
],
},
{
error: null,
result: {
context: {
slot: 11,
},
value: [
{
slot: 0,
confirmations: 1,
status: {Ok: null},
err: null,
},
],
},
},
]);
await connection.confirmTransaction(signature, 1);
mockRpc.push([ mockRpc.push([
url, url,