diff --git a/web3.js/src/connection.js b/web3.js/src/connection.js index b3d8bc730f..7ab618c746 100644 --- a/web3.js/src/connection.js +++ b/web3.js/src/connection.js @@ -1224,59 +1224,63 @@ export class Connection { transaction: Transaction, ...signers: Array ): Promise { - for (;;) { - // Attempt to use a recent blockhash for up to 30 seconds - const seconds = new Date().getSeconds(); - if ( - this._blockhashInfo.recentBlockhash != null && - this._blockhashInfo.seconds < seconds + 30 - ) { - transaction.recentBlockhash = this._blockhashInfo.recentBlockhash; - transaction.sign(...signers); - if (!transaction.signature) { - throw new Error('!signature'); // should never happen - } - - // If the signature of this transaction has not been seen before with the - // current recentBlockhash, all done. - const signature = transaction.signature.toString(); - if (!this._blockhashInfo.transactionSignatures.includes(signature)) { - this._blockhashInfo.transactionSignatures.push(signature); - if (this._disableBlockhashCaching) { - this._blockhashInfo.seconds = -1; - } - break; - } - } - - // Fetch a new blockhash - let attempts = 0; - const startTime = Date.now(); + if (transaction.nonceInfo) { + transaction.sign(...signers); + } else { for (;;) { - const [ - recentBlockhash, - //feeCalculator, - ] = await this.getRecentBlockhash(); + // Attempt to use a recent blockhash for up to 30 seconds + const seconds = new Date().getSeconds(); + if ( + this._blockhashInfo.recentBlockhash != null && + this._blockhashInfo.seconds < seconds + 30 + ) { + transaction.recentBlockhash = this._blockhashInfo.recentBlockhash; + transaction.sign(...signers); + if (!transaction.signature) { + throw new Error('!signature'); // should never happen + } - if (this._blockhashInfo.recentBlockhash != recentBlockhash) { - this._blockhashInfo = { + // If the signature of this transaction has not been seen before with the + // current recentBlockhash, all done. + const signature = transaction.signature.toString(); + if (!this._blockhashInfo.transactionSignatures.includes(signature)) { + this._blockhashInfo.transactionSignatures.push(signature); + if (this._disableBlockhashCaching) { + this._blockhashInfo.seconds = -1; + } + break; + } + } + + // Fetch a new blockhash + let attempts = 0; + const startTime = Date.now(); + for (;;) { + const [ recentBlockhash, - seconds: new Date().getSeconds(), - transactionSignatures: [], - }; - break; - } - if (attempts === 50) { - throw new Error( - `Unable to obtain a new blockhash after ${Date.now() - - startTime}ms`, - ); - } + //feeCalculator, + ] = await this.getRecentBlockhash(); - // Sleep for approximately half a slot - await sleep((500 * DEFAULT_TICKS_PER_SLOT) / NUM_TICKS_PER_SECOND); + if (this._blockhashInfo.recentBlockhash != recentBlockhash) { + this._blockhashInfo = { + recentBlockhash, + seconds: new Date().getSeconds(), + transactionSignatures: [], + }; + break; + } + if (attempts === 50) { + throw new Error( + `Unable to obtain a new blockhash after ${Date.now() - + startTime}ms`, + ); + } - ++attempts; + // Sleep for approximately half a slot + await sleep((500 * DEFAULT_TICKS_PER_SLOT) / NUM_TICKS_PER_SECOND); + + ++attempts; + } } } diff --git a/web3.js/src/nonce-account.js b/web3.js/src/nonce-account.js index 074c7a521c..2f03e91110 100644 --- a/web3.js/src/nonce-account.js +++ b/web3.js/src/nonce-account.js @@ -13,7 +13,7 @@ import {PublicKey} from './publickey'; const NonceAccountLayout = BufferLayout.struct([ BufferLayout.u32('state'), Layout.publicKey('authorizedPubkey'), - Layout.publicKey('hash'), + Layout.publicKey('nonce'), ]); /** diff --git a/web3.js/src/transaction.js b/web3.js/src/transaction.js index 4610c5fbf3..9cd96d1f90 100644 --- a/web3.js/src/transaction.js +++ b/web3.js/src/transaction.js @@ -184,7 +184,7 @@ export class Transaction { */ _getSignData(): Buffer { const {nonceInfo} = this; - if (nonceInfo) { + if (nonceInfo && this.instructions[0] != nonceInfo.nonceInstruction) { this.recentBlockhash = nonceInfo.nonce; this.instructions.unshift(nonceInfo.nonceInstruction); } @@ -203,25 +203,10 @@ export class Transaction { const programIds = []; + const allKeys = []; this.instructions.forEach(instruction => { instruction.keys.forEach(keySignerPair => { - const keyStr = keySignerPair.pubkey.toString(); - if (!keys.includes(keyStr)) { - if (keySignerPair.isSigner) { - this.signatures.push({ - signature: null, - publicKey: keySignerPair.pubkey, - }); - if (!keySignerPair.isWritable) { - numReadonlySignedAccounts += 1; - } - } else { - if (!keySignerPair.isWritable) { - numReadonlyUnsignedAccounts += 1; - } - } - keys.push(keyStr); - } + allKeys.push(keySignerPair); }); const programId = instruction.programId.toString(); @@ -230,6 +215,32 @@ export class Transaction { } }); + allKeys.sort(function(x, y) { + const checkSigner = x.isSigner === y.isSigner ? 0 : x.isSigner ? -1 : 1; + const checkWritable = x.isWritable === y.isWritable ? 0 : x.isWritable ? -1 : 1; + return checkSigner || checkWritable; + }); + + allKeys.forEach(keySignerPair => { + const keyStr = keySignerPair.pubkey.toString(); + if (!keys.includes(keyStr)) { + if (keySignerPair.isSigner) { + this.signatures.push({ + signature: null, + publicKey: keySignerPair.pubkey, + }); + if (!keySignerPair.isWritable) { + numReadonlySignedAccounts += 1; + } + } else { + if (!keySignerPair.isWritable) { + numReadonlyUnsignedAccounts += 1; + } + } + keys.push(keyStr); + } + }); + programIds.forEach(programId => { if (!keys.includes(programId)) { keys.push(programId); diff --git a/web3.js/test/system-program.test.js b/web3.js/test/system-program.test.js index 32cd1a17de..c1bd8c7183 100644 --- a/web3.js/test/system-program.test.js +++ b/web3.js/test/system-program.test.js @@ -3,10 +3,21 @@ import { Account, BudgetProgram, + Connection, SystemInstruction, SystemProgram, Transaction, + sendAndConfirmRecentTransaction, + LAMPORTS_PER_SOL, } from '../src'; +import {mockRpcEnabled} from './__mocks__/node-fetch'; +import {sleep} from '../src/util/sleep'; +import {url} from './url'; + +if (!mockRpcEnabled) { + // Testing max commitment level takes around 20s to complete + jest.setTimeout(30000); +} test('createAccount', () => { const from = new Account(); @@ -85,7 +96,9 @@ test('createNonceAccount', () => { expect(transaction.instructions[0].programId).toEqual( SystemProgram.programId, ); - expect(transaction.instructions[1].programId).toEqual(SystemProgram.programId); + expect(transaction.instructions[1].programId).toEqual( + SystemProgram.programId, + ); // TODO: Validate transaction contents more }); @@ -259,3 +272,72 @@ test('non-SystemInstruction error', () => { SystemInstruction.from(transaction.instructions[0]); }).toThrow(); }); + +test('live Nonce actions', async () => { + if (mockRpcEnabled) { + console.log('non-live test skipped'); + return; + } + + const connection = new Connection(url, 'recent'); + const nonceAccount = new Account(); + const from = new Account(); + const to = new Account(); + const authority = new Account(); + await connection.requestAirdrop(from.publicKey, 2 * LAMPORTS_PER_SOL); + await connection.requestAirdrop(authority.publicKey, LAMPORTS_PER_SOL); + + const minimumAmount = await connection.getMinimumBalanceForRentExemption( + SystemProgram.nonceSpace, + 'recent', + ); + + let createNonceAccount = SystemProgram.createNonceAccount( + from.publicKey, + nonceAccount.publicKey, + from.publicKey, + minimumAmount, + ); + await sendAndConfirmRecentTransaction( + connection, + createNonceAccount, + from, + nonceAccount, + ); + const nonceBalance = await connection.getBalance(nonceAccount.publicKey); + expect(nonceBalance).toEqual(minimumAmount); + + const nonceQuery1 = await connection.getNonce(nonceAccount.publicKey); + const nonceQuery2 = await connection.getNonce(nonceAccount.publicKey); + expect(nonceQuery1.nonce).toEqual(nonceQuery2.nonce); + + await sleep(500); + + const advanceNonce = new Transaction().add( + SystemProgram.nonceAdvance(nonceAccount.publicKey, from.publicKey), + ); + await sendAndConfirmRecentTransaction(connection, advanceNonce, from); + + const nonceQuery3 = await connection.getNonce(nonceAccount.publicKey); + expect(nonceQuery1.nonce).not.toEqual(nonceQuery3.nonce); + const nonce = nonceQuery3.nonce; + + await sleep(500); + + let transfer = SystemProgram.transfer( + from.publicKey, + to.publicKey, + minimumAmount, + ); + transfer.nonceInfo = { + nonce, + nonceInstruction: SystemProgram.nonceAdvance( + nonceAccount.publicKey, + from.publicKey, + ), + }; + + await sendAndConfirmRecentTransaction(connection, transfer, from); + const toBalance = await connection.getBalance(to.publicKey); + expect(toBalance).toEqual(minimumAmount); +});