diff --git a/web3.js/src/connection.ts b/web3.js/src/connection.ts index 42e6d14242..7499f80879 100644 --- a/web3.js/src/connection.ts +++ b/web3.js/src/connection.ts @@ -127,6 +127,21 @@ export type ConfirmedSignaturesForAddress2Options = { limit?: number; }; +/** + * Options for getSignaturesForAddress + */ +export type SignaturesForAddressOptions = { + /** + * Start searching backwards from this transaction signature. + * @remark If not provided the search starts from the highest max confirmed block. + */ + before?: TransactionSignature; + /** Search until this transaction signature is reached, if found before `limit`. */ + until?: TransactionSignature; + /** Maximum transaction signatures to return (between 1 and 1,000, default: 1,000). */ + limit?: number; +}; + /** * RPC Response with extra contextual information */ @@ -1060,6 +1075,21 @@ const GetConfirmedSignaturesForAddress2RpcResult = jsonRpcResult( ), ); +/** + * Expected JSON RPC response for the "getSignaturesForAddress" message + */ +const GetSignaturesForAddressRpcResult = jsonRpcResult( + array( + pick({ + signature: string(), + slot: number(), + err: TransactionErrorResult, + memo: nullable(string()), + blockTime: optional(nullable(number())), + }), + ), +); + /*** * Expected JSON RPC response for the "accountNotification" message */ @@ -3205,6 +3235,35 @@ export class Connection { return res.result; } + /** + * Returns confirmed signatures for transactions involving an + * address backwards in time from the provided signature or most recent confirmed block + * + * + * @param address queried address + * @param options + */ + async getSignaturesForAddress( + address: PublicKey, + options?: SignaturesForAddressOptions, + commitment?: Finality, + ): Promise> { + const args = this._buildArgsAtLeastConfirmed( + [address.toBase58()], + commitment, + undefined, + options, + ); + const unsafeRes = await this._rpcRequest('getSignaturesForAddress', args); + const res = create(unsafeRes, GetSignaturesForAddressRpcResult); + if ('error' in res) { + throw new Error( + 'failed to get signatures for address: ' + res.error.message, + ); + } + return res.result; + } + /** * Fetch the contents of a Nonce account from the cluster, return with context */ diff --git a/web3.js/test/connection.test.ts b/web3.js/test/connection.test.ts index eb09e40c8b..96a6422e5f 100644 --- a/web3.js/test/connection.test.ts +++ b/web3.js/test/connection.test.ts @@ -1011,6 +1011,112 @@ describe('Connection', () => { } }); + it('get signatures for address', async () => { + const connection = new Connection(url); + + await mockRpcResponse({ + method: 'getSlot', + params: [], + value: 1, + }); + + while ((await connection.getSlot()) <= 0) { + continue; + } + + await mockRpcResponse({ + method: 'getConfirmedBlock', + params: [1], + value: { + blockTime: 1614281964, + blockhash: '57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy', + previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo', + parentSlot: 0, + transactions: [ + { + meta: { + fee: 10000, + postBalances: [499260347380, 15298080, 1, 1, 1], + preBalances: [499260357380, 15298080, 1, 1, 1], + status: {Ok: null}, + err: null, + }, + transaction: { + message: { + accountKeys: [ + 'va12u4o9DipLEB2z4fuoHszroq1U9NcAB9aooFDPJSf', + '57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy', + 'SysvarS1otHashes111111111111111111111111111', + 'SysvarC1ock11111111111111111111111111111111', + 'Vote111111111111111111111111111111111111111', + ], + header: { + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 3, + numRequiredSignatures: 2, + }, + instructions: [ + { + accounts: [1, 2, 3], + data: '37u9WtQpcm6ULa3VtWDFAWoQc1hUvybPrA3dtx99tgHvvcE7pKRZjuGmn7VX2tC3JmYDYGG7', + programIdIndex: 4, + }, + ], + recentBlockhash: 'GeyAFFRY3WGpmam2hbgrKw4rbU2RKzfVLm5QLSeZwTZE', + }, + signatures: [ + 'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt', + '4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG', + ], + }, + }, + ], + }, + }); + + // Find a block that has a transaction, usually Block 1 + let slot = 0; + let address: PublicKey | undefined; + let expectedSignature: string | undefined; + while (!address || !expectedSignature) { + slot++; + const block = await connection.getConfirmedBlock(slot); + if (block.transactions.length > 0) { + const {signature, publicKey} = + block.transactions[0].transaction.signatures[0]; + if (signature) { + address = publicKey; + expectedSignature = bs58.encode(signature); + } + } + } + + // getSignaturesForAddress tests... + await mockRpcResponse({ + method: 'getSignaturesForAddress', + params: [address.toBase58(), {limit: 1}], + value: [ + { + signature: expectedSignature, + slot, + err: null, + memo: null, + }, + ], + }); + + const signatures = await connection.getSignaturesForAddress(address, { + limit: 1, + }); + expect(signatures).to.have.length(1); + if (mockServer) { + expect(signatures[0].signature).to.eq(expectedSignature); + expect(signatures[0].slot).to.eq(slot); + expect(signatures[0].err).to.be.null; + expect(signatures[0].memo).to.be.null; + } + }); + it('get parsed confirmed transactions', async () => { await mockRpcResponse({ method: 'getSlot',