diff --git a/web3.js/module.d.ts b/web3.js/module.d.ts index 009a4928e2..9b65764924 100644 --- a/web3.js/module.d.ts +++ b/web3.js/module.d.ts @@ -41,12 +41,26 @@ declare module '@solana/web3.js' { slot: number; }; + export type SendOptions = { + skipPreflight: boolean; + }; + + export type ConfirmOptions = { + confirmations: number; + skipPreflight: boolean; + }; + export type RpcResponseAndContext = { context: Context; value: T; }; - export type Commitment = 'max' | 'recent' | 'root' | 'single' | 'singleGossip'; + export type Commitment = + | 'max' + | 'recent' + | 'root' + | 'single' + | 'singleGossip'; export type LargestAccountsFilter = 'circulating' | 'nonCirculating'; @@ -269,12 +283,15 @@ declare module '@solana/web3.js' { sendTransaction( transaction: Transaction, signers: Array, + options?: SendOptions, ): Promise; sendEncodedTransaction( encodedTransaction: string, + options?: SendOptions, ): Promise; sendRawTransaction( wireTransaction: Buffer | Uint8Array | Array, + options?: SendOptions, ): Promise; onAccountChange( publickey: PublicKey, @@ -794,14 +811,14 @@ declare module '@solana/web3.js' { connection: Connection, transaction: Transaction, signers: Array, - confirmations?: number, + options?: ConfirmOptions, ): Promise; // === src/util/send-and-confirm-raw-transaction.js === export function sendAndConfirmRawTransaction( connection: Connection, wireTransaction: Buffer, - confirmations?: number, + options?: ConfirmOptions, ): Promise; // === src/util/cluster.js === diff --git a/web3.js/module.flow.js b/web3.js/module.flow.js index 4cc09d1732..c3c99d9c07 100644 --- a/web3.js/module.flow.js +++ b/web3.js/module.flow.js @@ -54,12 +54,26 @@ declare module '@solana/web3.js' { slot: number, }; + declare export type SendOptions = { + skipPreflight: boolean; + }; + + declare export type ConfirmOptions = { + confirmations: number; + skipPreflight: boolean; + }; + declare export type RpcResponseAndContext = { context: Context, value: T, }; - declare export type Commitment = 'max' | 'recent' | 'root' | 'single' | 'singleGossip'; + declare export type Commitment = + | 'max' + | 'recent' + | 'root' + | 'single' + | 'singleGossip'; declare export type LargestAccountsFilter = 'circulating' | 'nonCirculating'; @@ -282,12 +296,15 @@ declare module '@solana/web3.js' { sendTransaction( transaction: Transaction, signers: Array, + options?: SendOptions, ): Promise; sendEncodedTransaction( encodedTransaction: string, + options?: SendOptions, ): Promise; sendRawTransaction( wireTransaction: Buffer | Uint8Array | Array, + options?: SendOptions, ): Promise; onAccountChange( publickey: PublicKey, @@ -809,14 +826,14 @@ declare module '@solana/web3.js' { connection: Connection, transaction: Transaction, signers: Array, - confirmations: ?number, + options: ?ConfirmOptions, ): Promise; // === src/util/send-and-confirm-raw-transaction.js === declare export function sendAndConfirmRawTransaction( connection: Connection, wireTransaction: Buffer, - confirmations: ?number, + options: ?ConfirmOptions, ): Promise; // === src/util/cluster.js === diff --git a/web3.js/src/connection.js b/web3.js/src/connection.js index 3f48d71c1c..e160485704 100644 --- a/web3.js/src/connection.js +++ b/web3.js/src/connection.js @@ -31,6 +31,28 @@ type Context = { slot: number, }; +/** + * Options for sending transactions + * + * @typedef {Object} SendOptions + * @property {boolean} skipPreflight disable transaction verification step + */ +export type SendOptions = { + skipPreflight: boolean, +}; + +/** + * Options for confirming transactions + * + * @typedef {Object} ConfirmOptions + * @property {boolean} skipPreflight disable transaction verification step + * @property {number} confirmations desired number of cluster confirmations + */ +export type ConfirmOptions = { + confirmations: number, + skipPreflight: boolean, +}; + /** * RPC Response with extra contextual information * @@ -1663,6 +1685,7 @@ export class Connection { async sendTransaction( transaction: Transaction, signers: Array, + options?: SendOptions, ): Promise { if (transaction.nonceInfo) { transaction.sign(...signers); @@ -1696,7 +1719,7 @@ export class Connection { let attempts = 0; const startTime = Date.now(); for (;;) { - const {blockhash} = await this.getRecentBlockhash(); + const {blockhash} = await this.getRecentBlockhash('max'); if (this._blockhashInfo.recentBlockhash != blockhash) { this._blockhashInfo = { @@ -1723,7 +1746,7 @@ export class Connection { } const wireTransaction = transaction.serialize(); - return await this.sendRawTransaction(wireTransaction); + return await this.sendRawTransaction(wireTransaction, options); } /** @@ -1745,9 +1768,13 @@ export class Connection { */ async sendRawTransaction( rawTransaction: Buffer | Uint8Array | Array, + options: ?SendOptions, ): Promise { const encodedTransaction = bs58.encode(toBuffer(rawTransaction)); - const result = await this.sendEncodedTransaction(encodedTransaction); + const result = await this.sendEncodedTransaction( + encodedTransaction, + options, + ); return result; } @@ -1757,10 +1784,12 @@ export class Connection { */ async sendEncodedTransaction( encodedTransaction: string, + options: ?SendOptions, ): Promise { - const unsafeRes = await this._rpcRequest('sendTransaction', [ - encodedTransaction, - ]); + const args = [encodedTransaction]; + const skipPreflight = options && options.skipPreflight; + if (skipPreflight) args.push({skipPreflight}); + const unsafeRes = await this._rpcRequest('sendTransaction', args); const res = SendTransactionRpcResult(unsafeRes); if (res.error) { throw new Error('failed to send transaction: ' + res.error.message); diff --git a/web3.js/src/loader.js b/web3.js/src/loader.js index 85dc35d2c1..213e4fa294 100644 --- a/web3.js/src/loader.js +++ b/web3.js/src/loader.js @@ -69,7 +69,10 @@ export class Loader { connection, transaction, [payer, program], - 1, + { + confirmations: 1, + skipPreflight: true, + }, ); } @@ -107,7 +110,10 @@ export class Loader { data, }); transactions.push( - sendAndConfirmTransaction(connection, transaction, [payer, program], 1), + sendAndConfirmTransaction(connection, transaction, [payer, program], { + confirmations: 1, + skipPreflight: true, + }), ); // Delay ~1 tick between write transactions in an attempt to reduce AccountInUse errors @@ -152,7 +158,10 @@ export class Loader { connection, transaction, [payer, program], - 1, + { + confirmations: 1, + skipPreflight: true, + }, ); } } diff --git a/web3.js/src/util/send-and-confirm-raw-transaction.js b/web3.js/src/util/send-and-confirm-raw-transaction.js index 84349f2759..d19273486f 100644 --- a/web3.js/src/util/send-and-confirm-raw-transaction.js +++ b/web3.js/src/util/send-and-confirm-raw-transaction.js @@ -2,6 +2,7 @@ import {Connection} from '../connection'; import type {TransactionSignature} from '../transaction'; +import type {ConfirmOptions} from '../connection'; /** * Send and confirm a raw transaction @@ -9,12 +10,19 @@ import type {TransactionSignature} from '../transaction'; export async function sendAndConfirmRawTransaction( connection: Connection, rawTransaction: Buffer, - confirmations: ?number, + options?: ConfirmOptions, ): Promise { const start = Date.now(); - const signature = await connection.sendRawTransaction(rawTransaction); - const status = (await connection.confirmTransaction(signature, confirmations)) - .value; + const signature = await connection.sendRawTransaction( + rawTransaction, + options, + ); + const status = ( + await connection.confirmTransaction( + signature, + options && options.confirmations, + ) + ).value; if (status) { if (status.err) { diff --git a/web3.js/src/util/send-and-confirm-transaction.js b/web3.js/src/util/send-and-confirm-transaction.js index 9e5516640e..62cd77600c 100644 --- a/web3.js/src/util/send-and-confirm-transaction.js +++ b/web3.js/src/util/send-and-confirm-transaction.js @@ -4,6 +4,7 @@ import {Connection} from '../connection'; import {Transaction} from '../transaction'; import {sleep} from './sleep'; import type {Account} from '../account'; +import type {ConfirmOptions} from '../connection'; import type {TransactionSignature} from '../transaction'; const NUM_SEND_RETRIES = 10; @@ -17,15 +18,22 @@ export async function sendAndConfirmTransaction( connection: Connection, transaction: Transaction, signers: Array, - confirmations: ?number, + options?: ConfirmOptions, ): Promise { const start = Date.now(); let sendRetries = NUM_SEND_RETRIES; for (;;) { - const signature = await connection.sendTransaction(transaction, signers); + const signature = await connection.sendTransaction( + transaction, + signers, + options, + ); const status = ( - await connection.confirmTransaction(signature, confirmations) + await connection.confirmTransaction( + signature, + options && options.confirmations, + ) ).value; if (status) { diff --git a/web3.js/test/bpf-loader.test.js b/web3.js/test/bpf-loader.test.js index ba5380823d..31c084c4b6 100644 --- a/web3.js/test/bpf-loader.test.js +++ b/web3.js/test/bpf-loader.test.js @@ -44,7 +44,10 @@ test('load BPF C program', async () => { keys: [{pubkey: from.publicKey, isSigner: true, isWritable: true}], programId: program.publicKey, }); - await sendAndConfirmTransaction(connection, transaction, [from], 1); + await sendAndConfirmTransaction(connection, transaction, [from], { + confirmations: 1, + skipPreflight: true, + }); }); test('load BPF Rust program', async () => { @@ -73,5 +76,8 @@ test('load BPF Rust program', async () => { keys: [{pubkey: from.publicKey, isSigner: true, isWritable: true}], programId: program.publicKey, }); - await sendAndConfirmTransaction(connection, transaction, [from], 1); + await sendAndConfirmTransaction(connection, transaction, [from], { + confirmations: 1, + skipPreflight: true, + }); }); diff --git a/web3.js/test/connection.test.js b/web3.js/test/connection.test.js index 4370768429..141fc36b31 100644 --- a/web3.js/test/connection.test.js +++ b/web3.js/test/connection.test.js @@ -117,7 +117,7 @@ test('get program accounts', async () => { await connection.requestAirdrop(account0.publicKey, LAMPORTS_PER_SOL); await connection.requestAirdrop(account1.publicKey, 0.5 * LAMPORTS_PER_SOL); - mockGetRecentBlockhash('recent'); + mockGetRecentBlockhash('max'); mockRpc.push([ url, { @@ -160,7 +160,10 @@ test('get program accounts', async () => { accountPubkey: account0.publicKey, programId: programId.publicKey, }); - await sendAndConfirmTransaction(connection, transaction, [account0], 1); + await sendAndConfirmTransaction(connection, transaction, [account0], { + confirmations: 1, + skipPreflight: true, + }); mockRpc.push([ url, @@ -205,7 +208,10 @@ test('get program accounts', async () => { programId: programId.publicKey, }); - await sendAndConfirmTransaction(connection, transaction, [account1], 1); + await sendAndConfirmTransaction(connection, transaction, [account1], { + confirmations: 1, + skipPreflight: true, + }); mockGetRecentBlockhash('recent'); const {feeCalculator} = await connection.getRecentBlockhash(); @@ -1056,7 +1062,13 @@ test('get confirmed block', async () => { test('get recent blockhash', async () => { const connection = new Connection(url); - for (const commitment of ['max', 'recent', 'root', 'single', 'singleGossip']) { + for (const commitment of [ + 'max', + 'recent', + 'root', + 'single', + 'singleGossip', + ]) { mockGetRecentBlockhash(commitment); const {blockhash, feeCalculator} = await connection.getRecentBlockhash( @@ -1471,7 +1483,7 @@ test('transaction failure', async () => { minimumAmount + 100010, ); - mockGetRecentBlockhash('recent'); + mockGetRecentBlockhash('max'); mockRpc.push([ url, { @@ -1484,14 +1496,66 @@ test('transaction failure', async () => { }, ]); - const transaction = SystemProgram.transfer({ - fromPubkey: account.publicKey, - toPubkey: account.publicKey, - lamports: 10, - }); - const signature = await connection.sendTransaction(transaction, [account]); + mockRpc.push([ + url, + { + method: 'getSignatureStatuses', + params: [ + [ + '3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', + ], + ], + }, + { + error: null, + result: { + context: { + slot: 11, + }, + value: [ + { + slot: 0, + confirmations: 1, + err: null, + }, + ], + }, + }, + ]); - const expectedErr = {InstructionError: [0, 'AccountBorrowFailed']}; + const newAccount = new Account(); + let transaction = SystemProgram.createAccount({ + fromPubkey: account.publicKey, + newAccountPubkey: newAccount.publicKey, + lamports: 1000, + space: 0, + programId: SystemProgram.programId + }); + await sendAndConfirmTransaction(connection, transaction, [account, newAccount], {confirmations: 1, skipPreflight: true}); + + mockRpc.push([ + url, + { + method: 'sendTransaction', + }, + { + error: null, + result: + '3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', + }, + ]); + + // This should fail because the account is already created + transaction = SystemProgram.createAccount({ + fromPubkey: account.publicKey, + newAccountPubkey: newAccount.publicKey, + lamports: 10, + space: 0, + programId: SystemProgram.programId + }); + const signature = await connection.sendTransaction(transaction, [account, newAccount], {skipPreflight: true}); + + const expectedErr = {InstructionError: [0, {Custom: 0}]}; mockRpc.push([ url, { @@ -1656,7 +1720,7 @@ test('transaction', async () => { minimumAmount + 21, ); - mockGetRecentBlockhash('recent'); + mockGetRecentBlockhash('max'); mockRpc.push([ url, { @@ -1676,7 +1740,7 @@ test('transaction', async () => { }); const signature = await connection.sendTransaction(transaction, [ accountFrom, - ]); + ], {skipPreflight: true}); mockRpc.push([ url, @@ -1881,7 +1945,7 @@ test('multi-instruction transaction', async () => { const signature = await connection.sendTransaction(transaction, [ accountFrom, accountTo, - ]); + ], {skipPreflight: true}); await connection.confirmTransaction(signature, 1); @@ -1934,7 +1998,10 @@ test('account change notification', async () => { toPubkey: programAccount.publicKey, lamports: balanceNeeded, }); - await sendAndConfirmTransaction(connection, transaction, [owner], 1); + await sendAndConfirmTransaction(connection, transaction, [owner], { + confirmations: 1, + skipPreflight: true, + }); } catch (err) { await connection.removeAccountChangeListener(subscriptionId); throw err; @@ -1998,7 +2065,10 @@ test('program account change notification', async () => { toPubkey: programAccount.publicKey, lamports: balanceNeeded, }); - await sendAndConfirmTransaction(connection, transaction, [owner], 1); + await sendAndConfirmTransaction(connection, transaction, [owner], { + confirmations: 1, + skipPreflight: true, + }); } catch (err) { await connection.removeProgramAccountChangeListener(subscriptionId); throw err; diff --git a/web3.js/test/nonce.test.js b/web3.js/test/nonce.test.js index 8c8ecf5ab2..45ba17c2f0 100644 --- a/web3.js/test/nonce.test.js +++ b/web3.js/test/nonce.test.js @@ -84,7 +84,7 @@ test('create and query nonce account', async () => { const balance = await connection.getBalance(from.publicKey); expect(balance).toBe(minimumAmount * 2); - mockGetRecentBlockhash('recent'); + mockGetRecentBlockhash('max'); mockRpc.push([ url, { @@ -103,7 +103,7 @@ test('create and query nonce account', async () => { authorizedPubkey: from.publicKey, lamports: minimumAmount, }); - await connection.sendTransaction(transaction, [from, nonceAccount]); + await connection.sendTransaction(transaction, [from, nonceAccount], {skipPreflight: true}); mockRpc.push([ url, @@ -201,7 +201,7 @@ test('create and query nonce account with seed', async () => { const balance = await connection.getBalance(from.publicKey); expect(balance).toBe(minimumAmount * 2); - mockGetRecentBlockhash('recent'); + mockGetRecentBlockhash('max'); mockRpc.push([ url, { @@ -222,7 +222,7 @@ test('create and query nonce account with seed', async () => { authorizedPubkey: from.publicKey, lamports: minimumAmount, }); - await connection.sendTransaction(transaction, [from]); + await connection.sendTransaction(transaction, [from], {skipPreflight: true}); mockRpc.push([ url, diff --git a/web3.js/test/stake-program.test.js b/web3.js/test/stake-program.test.js index 4b5774b4e9..8dae9489a8 100644 --- a/web3.js/test/stake-program.test.js +++ b/web3.js/test/stake-program.test.js @@ -272,7 +272,7 @@ test('live staking actions', async () => { connection, createAndInitialize, [from, newStakeAccount], - 0, + {confirmations: 0, skipPreflight: true}, ); expect(await connection.getBalance(newStakeAccount.publicKey)).toEqual( minimumAmount + 42, @@ -283,7 +283,10 @@ test('live staking actions', async () => { authorizedPubkey: authorized.publicKey, votePubkey, }); - await sendAndConfirmTransaction(connection, delegation, [authorized], 0); + await sendAndConfirmTransaction(connection, delegation, [authorized], { + confirmations: 0, + skipPreflight: true, + }); } // Create Stake account with seed @@ -308,7 +311,7 @@ test('live staking actions', async () => { connection, createAndInitializeWithSeed, [from], - 0, + {confirmations: 0, skipPreflight: true}, ); let originalStakeBalance = await connection.getBalance(newAccountPubkey); expect(originalStakeBalance).toEqual(3 * minimumAmount + 42); @@ -318,7 +321,10 @@ test('live staking actions', async () => { authorizedPubkey: authorized.publicKey, votePubkey, }); - await sendAndConfirmTransaction(connection, delegation, [authorized], 0); + await sendAndConfirmTransaction(connection, delegation, [authorized], { + confirmations: 0, + skipPreflight: true, + }); // Test that withdraw fails before deactivation const recipient = new Account(); @@ -329,7 +335,10 @@ test('live staking actions', async () => { lamports: 1000, }); await expect( - sendAndConfirmTransaction(connection, withdraw, [authorized], 0), + sendAndConfirmTransaction(connection, withdraw, [authorized], { + confirmations: 0, + skipPreflight: true, + }), ).rejects.toThrow(); // Split stake @@ -340,7 +349,10 @@ test('live staking actions', async () => { splitStakePubkey: newStake.publicKey, lamports: minimumAmount + 20, }); - await sendAndConfirmTransaction(connection, split, [authorized, newStake], 0); + await sendAndConfirmTransaction(connection, split, [authorized, newStake], { + confirmations: 0, + skipPreflight: true, + }); // Authorize to new account const newAuthorized = new Account(); @@ -352,14 +364,20 @@ test('live staking actions', async () => { newAuthorizedPubkey: newAuthorized.publicKey, stakeAuthorizationType: StakeAuthorizationLayout.Withdrawer, }); - await sendAndConfirmTransaction(connection, authorize, [authorized], 0); + await sendAndConfirmTransaction(connection, authorize, [authorized], { + confirmations: 0, + skipPreflight: true, + }); authorize = StakeProgram.authorize({ stakePubkey: newAccountPubkey, authorizedPubkey: authorized.publicKey, newAuthorizedPubkey: newAuthorized.publicKey, stakeAuthorizationType: StakeAuthorizationLayout.Staker, }); - await sendAndConfirmTransaction(connection, authorize, [authorized], 0); + await sendAndConfirmTransaction(connection, authorize, [authorized], { + confirmations: 0, + skipPreflight: true, + }); // Test old authorized can't deactivate let deactivateNotAuthorized = StakeProgram.deactivate({ @@ -371,7 +389,7 @@ test('live staking actions', async () => { connection, deactivateNotAuthorized, [authorized], - 0, + {confirmations: 0, skipPreflight: true}, ), ).rejects.toThrow(); @@ -380,7 +398,10 @@ test('live staking actions', async () => { stakePubkey: newAccountPubkey, authorizedPubkey: newAuthorized.publicKey, }); - await sendAndConfirmTransaction(connection, deactivate, [newAuthorized], 0); + await sendAndConfirmTransaction(connection, deactivate, [newAuthorized], { + confirmations: 0, + skipPreflight: true, + }); // Test that withdraw succeeds after deactivation withdraw = StakeProgram.withdraw({ @@ -389,7 +410,10 @@ test('live staking actions', async () => { toPubkey: recipient.publicKey, lamports: minimumAmount + 20, }); - await sendAndConfirmTransaction(connection, withdraw, [newAuthorized], 0); + await sendAndConfirmTransaction(connection, withdraw, [newAuthorized], { + confirmations: 0, + skipPreflight: true, + }); const balance = await connection.getBalance(newAccountPubkey); expect(balance).toEqual(minimumAmount + 2); const recipientBalance = await connection.getBalance(recipient.publicKey); diff --git a/web3.js/test/system-program.test.js b/web3.js/test/system-program.test.js index 1d4c09ac81..995b322b34 100644 --- a/web3.js/test/system-program.test.js +++ b/web3.js/test/system-program.test.js @@ -297,7 +297,7 @@ test('live Nonce actions', async () => { connection, createNonceAccount, [from, nonceAccount], - 0, + {confirmations: 0, skipPreflight: true}, ); const nonceBalance = await connection.getBalance(nonceAccount.publicKey); expect(nonceBalance).toEqual(minimumAmount); @@ -325,7 +325,10 @@ test('live Nonce actions', async () => { authorizedPubkey: from.publicKey, }), ); - await sendAndConfirmTransaction(connection, advanceNonce, [from], 0); + await sendAndConfirmTransaction(connection, advanceNonce, [from], { + confirmations: 0, + skipPreflight: true, + }); const nonceQuery3 = await connection.getNonce(nonceAccount.publicKey); if (nonceQuery3 === null) { expect(nonceQuery3).not.toBeNull(); @@ -344,7 +347,10 @@ test('live Nonce actions', async () => { newAuthorizedPubkey: newAuthority.publicKey, }), ); - await sendAndConfirmTransaction(connection, authorizeNonce, [from], 0); + await sendAndConfirmTransaction(connection, authorizeNonce, [from], { + confirmations: 0, + skipPreflight: true, + }); let transfer = SystemProgram.transfer({ fromPubkey: from.publicKey, @@ -359,12 +365,10 @@ test('live Nonce actions', async () => { }), }; - await sendAndConfirmTransaction( - connection, - transfer, - [from, newAuthority], - 0, - ); + await sendAndConfirmTransaction(connection, transfer, [from, newAuthority], { + confirmations: 0, + skipPreflight: true, + }); const toBalance = await connection.getBalance(to.publicKey); expect(toBalance).toEqual(minimumAmount); @@ -380,7 +384,10 @@ test('live Nonce actions', async () => { toPubkey: withdrawAccount.publicKey, }), ); - await sendAndConfirmTransaction(connection, withdrawNonce, [newAuthority], 0); + await sendAndConfirmTransaction(connection, withdrawNonce, [newAuthority], { + confirmations: 0, + skipPreflight: true, + }); expect(await connection.getBalance(nonceAccount.publicKey)).toEqual(0); const withdrawBalance = await connection.getBalance( withdrawAccount.publicKey, diff --git a/web3.js/test/transaction-payer.test.js b/web3.js/test/transaction-payer.test.js index b7a7939a29..60473a78e0 100644 --- a/web3.js/test/transaction-payer.test.js +++ b/web3.js/test/transaction-payer.test.js @@ -86,7 +86,7 @@ test('transaction-payer', async () => { ]); await connection.requestAirdrop(accountTo.publicKey, minimumAmount + 21); - mockGetRecentBlockhash('recent'); + mockGetRecentBlockhash('max'); mockRpc.push([ url, { @@ -108,7 +108,7 @@ test('transaction-payer', async () => { const signature = await connection.sendTransaction(transaction, [ accountPayer, accountFrom, - ]); + ], {skipPreflight: true}); mockRpc.push([ url,