diff --git a/web3.js/src/connection.js b/web3.js/src/connection.js index c52802f525..3788e2ce0b 100644 --- a/web3.js/src/connection.js +++ b/web3.js/src/connection.js @@ -19,6 +19,8 @@ import type {FeeCalculator} from './fee-calculator'; import type {Account} from './account'; import type {TransactionSignature} from './transaction'; +export const BLOCKHASH_CACHE_TIMEOUT_MS = 30 * 1000; + type RpcRequest = (methodName: string, args: Array) => any; /** @@ -975,7 +977,7 @@ export class Connection { _commitment: ?Commitment; _blockhashInfo: { recentBlockhash: Blockhash | null, - seconds: number, + lastFetch: Date, transactionSignatures: Array, }; _disableBlockhashCaching: boolean = false; @@ -1011,7 +1013,7 @@ export class Connection { this._commitment = commitment; this._blockhashInfo = { recentBlockhash: null, - seconds: -1, + lastFetch: new Date(0), transactionSignatures: [], }; @@ -1731,10 +1733,10 @@ export class Connection { } else { 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 + Date.now() - this._blockhashInfo.lastFetch < + BLOCKHASH_CACHE_TIMEOUT_MS ) { transaction.recentBlockhash = this._blockhashInfo.recentBlockhash; transaction.sign(...signers); @@ -1748,7 +1750,7 @@ export class Connection { if (!this._blockhashInfo.transactionSignatures.includes(signature)) { this._blockhashInfo.transactionSignatures.push(signature); if (this._disableBlockhashCaching) { - this._blockhashInfo.seconds = -1; + this._blockhashInfo.lastFetch = new Date(0); } break; } @@ -1763,7 +1765,7 @@ export class Connection { if (this._blockhashInfo.recentBlockhash != blockhash) { this._blockhashInfo = { recentBlockhash: blockhash, - seconds: new Date().getSeconds(), + lastFetch: new Date(), transactionSignatures: [], }; break; diff --git a/web3.js/test/connection.test.js b/web3.js/test/connection.test.js index 79274797e5..2613fea710 100644 --- a/web3.js/test/connection.test.js +++ b/web3.js/test/connection.test.js @@ -14,12 +14,11 @@ import {mockRpc, mockRpcEnabled} from './__mocks__/node-fetch'; import {mockGetRecentBlockhash} from './mockrpc/get-recent-blockhash'; import {url} from './url'; import {sleep} from '../src/util/sleep'; +import {BLOCKHASH_CACHE_TIMEOUT_MS} from '../src/connection'; import type {SignatureStatus, TransactionError} from '../src/connection'; -if (!mockRpcEnabled) { - // Testing max commitment level takes around 20s to complete - jest.setTimeout(30000); -} +// Testing blockhash cache takes around 30s to complete +jest.setTimeout(40000); const errorMessage = 'Invalid'; const errorResponse = { @@ -46,7 +45,7 @@ const verifySignatureStatus = ( const confirmations = status.confirmations; if (typeof confirmations === 'number') { - expect(confirmations).toBeGreaterThan(0); + expect(confirmations).toBeGreaterThanOrEqual(0); } else { expect(confirmations).toBeNull(); } @@ -1630,6 +1629,33 @@ test('transaction', async () => { '0WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', }, ]); + mockRpc.push([ + url, + { + method: 'getSignatureStatuses', + params: [ + [ + '0WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', + ], + ], + }, + { + error: null, + result: { + context: { + slot: 11, + }, + value: [ + { + slot: 0, + confirmations: 0, + status: {Ok: null}, + err: null, + }, + ], + }, + }, + ]); mockRpc.push([ url, { @@ -1646,10 +1672,11 @@ test('transaction', async () => { }, }, ]); - await connection.requestAirdrop( + const airdropFromSig = await connection.requestAirdrop( accountFrom.publicKey, minimumAmount + 100010, ); + await connection.confirmTransaction(airdropFromSig, 0); expect(await connection.getBalance(accountFrom.publicKey)).toBe( minimumAmount + 100010, ); @@ -1658,7 +1685,7 @@ test('transaction', async () => { url, { method: 'requestAirdrop', - params: [accountTo.publicKey.toBase58(), minimumAmount + 21], + params: [accountTo.publicKey.toBase58(), minimumAmount], }, { error: null, @@ -1666,6 +1693,33 @@ test('transaction', async () => { '8WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', }, ]); + mockRpc.push([ + url, + { + method: 'getSignatureStatuses', + params: [ + [ + '8WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', + ], + ], + }, + { + error: null, + result: { + context: { + slot: 11, + }, + value: [ + { + slot: 0, + confirmations: 0, + status: {Ok: null}, + err: null, + }, + ], + }, + }, + ]); mockRpc.push([ url, { @@ -1678,14 +1732,16 @@ test('transaction', async () => { context: { slot: 11, }, - value: minimumAmount + 21, + value: minimumAmount, }, }, ]); - await connection.requestAirdrop(accountTo.publicKey, minimumAmount + 21); - expect(await connection.getBalance(accountTo.publicKey)).toBe( - minimumAmount + 21, + const airdropToSig = await connection.requestAirdrop( + accountTo.publicKey, + minimumAmount, ); + await connection.confirmTransaction(airdropToSig, 0); + expect(await connection.getBalance(accountTo.publicKey)).toBe(minimumAmount); mockGetRecentBlockhash('max'); mockRpc.push([ @@ -1696,7 +1752,7 @@ test('transaction', async () => { { error: null, result: - '3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', + '1WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', }, ]); @@ -1717,7 +1773,7 @@ test('transaction', async () => { method: 'getSignatureStatuses', params: [ [ - '3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', + '1WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', ], ], }, @@ -1730,7 +1786,7 @@ test('transaction', async () => { value: [ { slot: 0, - confirmations: 1, + confirmations: 0, status: {Ok: null}, err: null, }, @@ -1739,96 +1795,91 @@ test('transaction', async () => { }, ]); - // Wait for one confirmation - const confirmResult = (await connection.confirmTransaction(signature, 1)) - .value; + let confirmResult = (await connection.confirmTransaction(signature, 0)).value; verifySignatureStatus(confirmResult); + mockGetRecentBlockhash('max'); mockRpc.push([ url, { - method: 'getSignatureStatuses', - params: [ - [ - '3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', - ], - ], + method: 'sendTransaction', }, { error: null, - result: { - context: { - slot: 11, - }, - value: [ - { - slot: 0, - confirmations: 11, - status: {Ok: null}, - err: null, - }, - ], - }, + result: + '2WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', }, ]); - const response = verifySignatureStatus( - (await connection.getSignatureStatus(signature)).value, + // Send again and ensure that new blockhash is used + const lastFetch = Date.now(); + const transaction2 = SystemProgram.transfer({ + fromPubkey: accountFrom.publicKey, + toPubkey: accountTo.publicKey, + lamports: 10, + }); + const signature2 = await connection.sendTransaction( + transaction2, + [accountFrom], + {skipPreflight: true}, + ); + expect(signature).not.toEqual(signature2); + expect(transaction.recentBlockhash).not.toEqual(transaction2.recentBlockhash); + + mockRpc.push([ + url, + { + method: 'sendTransaction', + }, + { + error: null, + result: + '3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', + }, + ]); + + // Send new transaction and ensure that same blockhash is used + const transaction3 = SystemProgram.transfer({ + fromPubkey: accountFrom.publicKey, + toPubkey: accountTo.publicKey, + lamports: 9, + }); + await connection.sendTransaction(transaction3, [accountFrom], { + skipPreflight: true, + }); + expect(transaction2.recentBlockhash).toEqual(transaction3.recentBlockhash); + + // Sleep until blockhash cache times out + await sleep( + Math.max(0, 1000 + BLOCKHASH_CACHE_TIMEOUT_MS - (Date.now() - lastFetch)), ); - const unprocessedSignature = - '8WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk'; + mockGetRecentBlockhash('max'); mockRpc.push([ url, { - method: 'getSignatureStatuses', - params: [ - [ - '3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', - unprocessedSignature, - ], - ], + method: 'sendTransaction', }, { error: null, - result: { - context: { - slot: 11, - }, - value: [ - { - slot: 0, - confirmations: 11, - status: {Ok: null}, - err: null, - }, - null, - ], - }, + result: + '3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', }, ]); - const responses = ( - await connection.getSignatureStatuses([signature, unprocessedSignature]) - ).value; - expect(responses.length).toEqual(2); - expect(responses[1]).toBeNull(); + const transaction4 = SystemProgram.transfer({ + fromPubkey: accountFrom.publicKey, + toPubkey: accountTo.publicKey, + lamports: 13, + }); - const firstResponse = verifySignatureStatus(responses[0]); - expect(firstResponse.slot).toBeGreaterThanOrEqual(response.slot); - expect(firstResponse.err).toEqual(response.err); + await connection.sendTransaction(transaction4, [accountFrom], { + skipPreflight: true, + }); - const responseConfirmations = response.confirmations; - if ( - typeof responseConfirmations === 'number' && - typeof firstResponse.confirmations === 'number' - ) { - expect(firstResponse.confirmations).toBeGreaterThanOrEqual( - responseConfirmations, - ); - } else { - expect(firstResponse.confirmations).toBeNull(); - } + expect(transaction4.recentBlockhash).not.toEqual( + transaction3.recentBlockhash, + ); mockRpc.push([ url, @@ -1864,12 +1915,12 @@ test('transaction', async () => { context: { slot: 11, }, - value: minimumAmount + 31, + value: minimumAmount + 42, }, }, ]); expect(await connection.getBalance(accountTo.publicKey)).toBe( - minimumAmount + 31, + minimumAmount + 42, ); }); @@ -1883,7 +1934,11 @@ test('multi-instruction transaction', async () => { const accountTo = new Account(); const connection = new Connection(url, 'recent'); - await connection.requestAirdrop(accountFrom.publicKey, LAMPORTS_PER_SOL); + let signature = await connection.requestAirdrop( + accountFrom.publicKey, + LAMPORTS_PER_SOL, + ); + await connection.confirmTransaction(signature, 0); expect(await connection.getBalance(accountFrom.publicKey)).toBe( LAMPORTS_PER_SOL, ); @@ -1893,7 +1948,11 @@ test('multi-instruction transaction', async () => { 'recent', ); - await connection.requestAirdrop(accountTo.publicKey, minimumAmount + 21); + signature = await connection.requestAirdrop( + accountTo.publicKey, + minimumAmount + 21, + ); + await connection.confirmTransaction(signature, 0); expect(await connection.getBalance(accountTo.publicKey)).toBe( minimumAmount + 21, ); @@ -1911,7 +1970,7 @@ test('multi-instruction transaction', async () => { lamports: 100, }), ); - const signature = await connection.sendTransaction( + signature = await connection.sendTransaction( transaction, [accountFrom, accountTo], {skipPreflight: true},