diff --git a/web3.js/module.d.ts b/web3.js/module.d.ts index 437ec38cf9a6a4..a99afdd2250f0f 100644 --- a/web3.js/module.d.ts +++ b/web3.js/module.d.ts @@ -148,6 +148,39 @@ declare module '@solana/web3.js' { meta: ConfirmedTransactionMeta | null; }; + export type ParsedMessageAccount = { + pubkey: PublicKey; + signer: boolean; + writable: boolean; + }; + + export type ParsedInstruction = { + programId: PublicKey; + program: string; + parsed: string; + }; + + export type PartiallyDecodedInstruction = { + programId: PublicKey; + accounts: Array; + data: string; + }; + + export type ParsedTransaction = { + signatures: Array; + message: { + accountKeys: ParsedMessageAccount[]; + instructions: (ParsedInstruction | PartiallyDecodedInstruction)[]; + recentBlockhash: string; + }; + }; + + export type ParsedConfirmedTransaction = { + slot: number; + transaction: ParsedTransaction; + meta: ConfirmedTransactionMeta | null; + }; + export type KeyedAccountInfo = { accountId: PublicKey; accountInfo: AccountInfo; @@ -288,6 +321,9 @@ declare module '@solana/web3.js' { getConfirmedTransaction( signature: TransactionSignature, ): Promise; + getParsedConfirmedTransaction( + signature: TransactionSignature, + ): Promise; getConfirmedSignaturesForAddress( address: PublicKey, startSlot: number, diff --git a/web3.js/module.flow.js b/web3.js/module.flow.js index 40e7b5dd2f1e4c..7b7745ed0c564b 100644 --- a/web3.js/module.flow.js +++ b/web3.js/module.flow.js @@ -169,6 +169,39 @@ declare module '@solana/web3.js' { meta: ConfirmedTransactionMeta | null, }; + declare export type ParsedMessageAccount = { + pubkey: PublicKey, + signer: boolean, + writable: boolean, + }; + + declare export type ParsedInstruction = {| + programId: PublicKey, + program: string, + parsed: string, + |}; + + declare export type PartiallyDecodedInstruction = {| + programId: PublicKey, + accounts: Array, + data: string, + |}; + + declare export type ParsedTransaction = { + signatures: Array, + message: { + accountKeys: ParsedMessageAccount[], + instructions: (ParsedInstruction | PartiallyDecodedInstruction)[], + recentBlockhash: string, + }, + }; + + declare export type ParsedConfirmedTransaction = { + slot: number, + transaction: ParsedTransaction, + meta: ConfirmedTransactionMeta | null, + }; + declare export type KeyedAccountInfo = { accountId: PublicKey, accountInfo: AccountInfo, @@ -309,6 +342,9 @@ declare module '@solana/web3.js' { getConfirmedTransaction( signature: TransactionSignature, ): Promise; + getParsedConfirmedTransaction( + signature: TransactionSignature, + ): Promise; getConfirmedSignaturesForAddress( address: PublicKey, startSlot: number, diff --git a/web3.js/package-lock.json b/web3.js/package-lock.json index 6dca94777a6528..576bb36070af5a 100644 --- a/web3.js/package-lock.json +++ b/web3.js/package-lock.json @@ -4743,24 +4743,23 @@ } }, "@solana/spl-token": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.0.4.tgz", - "integrity": "sha512-zYoZ6iYMKxGYbouunEkWdf6vWRJyEPOkAjvlNVjww9oPKMkIeM9VzgGtjZ/kKMelao1QEohH4JN9qXO4+LwfRA==", + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.0.5.tgz", + "integrity": "sha512-OXW/zHzMQqVGcSNrNt8sRaHlKT5vjdcUcmUHi8d4ssG8ChbZVA2lkJK10XDXlcnMIiSTindpEjiFmooYc9K3uQ==", "dev": true, "requires": { "@babel/runtime": "^7.10.5", - "@solana/web3.js": "^0.63.2", + "@solana/web3.js": "^0.64.0", "bn.js": "^5.0.0", "buffer-layout": "^1.2.0", "dotenv": "8.2.0", - "json-to-pretty-yaml": "^1.2.2", "mkdirp-promise": "^5.0.1" } }, "@solana/web3.js": { - "version": "0.63.2", - "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.63.2.tgz", - "integrity": "sha512-4jd8U1U/eFTEemr+jCzQCDepKnkttV4dxWsjMloifb82x1d6KgCzP+Jd6D9kr8f1MFj2i/AnG++97tlHAGTOkA==", + "version": "0.64.2", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.64.2.tgz", + "integrity": "sha512-aGRG1rn8fLerE4NscRL6rq0nSyYAK9K+TGRZxb6ue7Ontufa6wO1kxum4zJs17+xT0zVf8wABUtCMgP4W7FxpA==", "dev": true, "requires": { "@babel/runtime": "^7.3.1", @@ -14214,16 +14213,6 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, - "json-to-pretty-yaml": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/json-to-pretty-yaml/-/json-to-pretty-yaml-1.2.2.tgz", - "integrity": "sha1-9M0L0KXo/h3yWq9boRiwmf2ZLVs=", - "dev": true, - "requires": { - "remedial": "^1.0.7", - "remove-trailing-spaces": "^1.0.6" - } - }, "json5": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", @@ -20159,24 +20148,12 @@ } } }, - "remedial": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/remedial/-/remedial-1.0.8.tgz", - "integrity": "sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==", - "dev": true - }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", "dev": true }, - "remove-trailing-spaces": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/remove-trailing-spaces/-/remove-trailing-spaces-1.0.7.tgz", - "integrity": "sha512-wjM17CJ2kk0SgoGyJ7ZMzRRCuTq+V8YhMwpZ5XEWX0uaked2OUq6utvHXGNBQrfkUzUUABFMyxlKn+85hMv4dg==", - "dev": true - }, "repeat-element": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", diff --git a/web3.js/package.json b/web3.js/package.json index 3508e66ce818e8..e7f79aab383b80 100644 --- a/web3.js/package.json +++ b/web3.js/package.json @@ -97,7 +97,7 @@ "@babel/preset-flow": "^7.0.0", "@commitlint/config-conventional": "^9.0.1", "@commitlint/travis-cli": "^9.0.1", - "@solana/spl-token": "^0.0.4", + "@solana/spl-token": "^0.0.5", "@typescript-eslint/eslint-plugin": "^2.18.0", "@typescript-eslint/parser": "^2.18.0", "acorn": "^7.0.0", diff --git a/web3.js/src/connection.js b/web3.js/src/connection.js index b35f6d0c38d506..7ced11ef60ff0f 100644 --- a/web3.js/src/connection.js +++ b/web3.js/src/connection.js @@ -49,7 +49,7 @@ type Context = { * @property {boolean | undefined} skipPreflight disable transaction verification step */ export type SendOptions = { - skipPreflight: ?boolean, + skipPreflight?: boolean, }; /** @@ -60,8 +60,8 @@ export type SendOptions = { * @property {number | undefined} confirmations desired number of cluster confirmations */ export type ConfirmOptions = { - skipPreflight: ?boolean, - confirmations: ?number, + skipPreflight?: boolean, + confirmations?: number, }; /** @@ -378,6 +378,88 @@ type ConfirmedTransaction = { meta: ConfirmedTransactionMeta | null, }; +/** + * A partially decoded transaction instruction + * + * @typedef {Object} ParsedMessageAccount + * @property {PublicKey} pubkey Public key of the account + * @property {PublicKey} accounts Indicates if the account signed the transaction + * @property {string} data Raw base-58 instruction data + */ +type PartiallyDecodedInstruction = {| + programId: PublicKey, + accounts: Array, + data: string, +|}; + +/** + * A parsed transaction message account + * + * @typedef {Object} ParsedMessageAccount + * @property {PublicKey} pubkey Public key of the account + * @property {boolean} signer Indicates if the account signed the transaction + * @property {boolean} writable Indicates if the account is writable for this transaction + */ +type ParsedMessageAccount = { + pubkey: PublicKey, + signer: boolean, + writable: boolean, +}; + +/** + * A parsed transaction instruction + * + * @typedef {Object} ParsedInstruction + * @property {string} program Name of the program for this instruction + * @property {PublicKey} programId ID of the program for this instruction + * @property {any} parsed Parsed instruction info + */ +type ParsedInstruction = {| + program: string, + programId: PublicKey, + parsed: any, +|}; + +/** + * A parsed transaction message + * + * @typedef {Object} ParsedMessage + * @property {Array} accountKeys Accounts used in the instructions + * @property {Array} instructions The atomically executed instructions for the transaction + * @property {string} recentBlockhash Recent blockhash + */ +type ParsedMessage = { + accountKeys: ParsedMessageAccount[], + instructions: (ParsedInstruction | PartiallyDecodedInstruction)[], + recentBlockhash: string, +}; + +/** + * A parsed transaction + * + * @typedef {Object} ParsedTransaction + * @property {Array} signatures Signatures for the transaction + * @property {ParsedMessage} message Message of the transaction + */ +type ParsedTransaction = { + signatures: Array, + message: ParsedMessage, +}; + +/** + * A parsed and confirmed transaction on the ledger + * + * @typedef {Object} ParsedConfirmedTransaction + * @property {number} slot The slot during which the transaction was processed + * @property {ParsedTransaction} transaction The details of the transaction + * @property {ConfirmedTransactionMeta|null} meta Metadata produced from the transaction + */ +type ParsedConfirmedTransaction = { + slot: number, + transaction: ParsedTransaction, + meta: ConfirmedTransactionMeta | null, +}; + /** * A ConfirmedBlock on the ledger * @@ -807,13 +889,41 @@ const ConfirmedTransactionResult = struct({ numReadonlySignedAccounts: 'number', numReadonlyUnsignedAccounts: 'number', }), + instructions: struct.array([ + struct({ + accounts: struct.array(['number']), + data: 'string', + programIdIndex: 'number', + }), + ]), + recentBlockhash: 'string', + }), +}); + +/** + * @private + */ +const ParsedConfirmedTransactionResult = struct({ + signatures: struct.array(['string']), + message: struct({ + accountKeys: struct.array([ + struct({ + pubkey: 'string', + signer: 'boolean', + writable: 'boolean', + }), + ]), instructions: struct.array([ struct.union([ - struct.array(['number']), struct({ - accounts: struct.array(['number']), + accounts: struct.array(['string']), data: 'string', - programIdIndex: 'number', + programId: 'string', + }), + struct({ + parsed: 'any', + program: 'string', + programId: 'string', }), ]), ]), @@ -877,6 +987,20 @@ const GetConfirmedTransactionRpcResult = jsonRpcResult( ]), ); +/** + * Expected JSON RPC response for the "getConfirmedTransaction" message + */ +const GetParsedConfirmedTransactionRpcResult = jsonRpcResult( + struct.union([ + 'null', + struct.pick({ + slot: 'number', + transaction: ParsedConfirmedTransactionResult, + meta: ConfirmedTransactionMetaResult, + }), + ]), +); + /** * Expected JSON RPC response for the "getRecentBlockhash" message */ @@ -1853,6 +1977,56 @@ export class Connection { }; } + /** + * Fetch parsed transaction details for a confirmed transaction + */ + async getParsedConfirmedTransaction( + signature: TransactionSignature, + ): Promise { + const unsafeRes = await this._rpcRequest('getConfirmedTransaction', [ + signature, + 'jsonParsed', + ]); + const {result, error} = GetParsedConfirmedTransactionRpcResult(unsafeRes); + if (error) { + throw new Error('failed to get confirmed transaction: ' + error.message); + } + assert(typeof result !== 'undefined'); + if (result === null) return result; + + const { + accountKeys, + instructions, + recentBlockhash, + } = result.transaction.message; + return { + slot: result.slot, + meta: result.meta, + transaction: { + signatures: result.transaction.signatures, + message: { + accountKeys: accountKeys.map(accountKey => ({ + pubkey: new PublicKey(accountKey.pubkey), + signer: accountKey.signer, + writable: accountKey.writable, + })), + instructions: instructions.map(ix => { + let mapped: any = {programId: new PublicKey(ix.programId)}; + if ('accounts' in ix) { + mapped.accounts = ix.accounts.map(key => new PublicKey(key)); + } + + return { + ...ix, + ...mapped, + }; + }), + recentBlockhash, + }, + }, + }; + } + /** * Fetch a list of all the confirmed signatures for transactions involving an address * within a specified slot range. Max range allowed is 10,000 slots. diff --git a/web3.js/test/bpf-loader.test.js b/web3.js/test/bpf-loader.test.js index 31c084c4b69609..60997b7ec2bc4b 100644 --- a/web3.js/test/bpf-loader.test.js +++ b/web3.js/test/bpf-loader.test.js @@ -1,5 +1,6 @@ // @flow +import bs58 from 'bs58'; import fs from 'mz/fs'; import { @@ -77,7 +78,29 @@ test('load BPF Rust program', async () => { programId: program.publicKey, }); await sendAndConfirmTransaction(connection, transaction, [from], { - confirmations: 1, skipPreflight: true, }); + + if (transaction.signature === null) { + expect(transaction.signature).not.toBeNull(); + return; + } + + const confirmedSignature = bs58.encode(transaction.signature); + const parsedTx = await connection.getParsedConfirmedTransaction( + confirmedSignature, + ); + if (parsedTx === null) { + expect(parsedTx).not.toBeNull(); + return; + } + const {signatures, message} = parsedTx.transaction; + expect(signatures[0]).toEqual(confirmedSignature); + const ix = message.instructions[0]; + if (ix.parsed) { + expect('parsed' in ix).toBe(false); + } else { + expect(ix.programId.equals(program.publicKey)).toBe(true); + expect(ix.data).toEqual(''); + } }); diff --git a/web3.js/test/connection.test.js b/web3.js/test/connection.test.js index dc4854d8804761..d66baa449385b2 100644 --- a/web3.js/test/connection.test.js +++ b/web3.js/test/connection.test.js @@ -16,6 +16,7 @@ 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 {TransactionSignature} from '../src/transaction'; import type {SignatureStatus, TransactionError} from '../src/connection'; import {mockConfirmTransaction} from './mockrpc/confirm-transaction'; @@ -1299,116 +1300,150 @@ const TOKEN_PROGRAM_ID = new PublicKey( 'TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o', ); -test('token methods', async () => { +describe('token methods', () => { if (mockRpcEnabled) { console.log('non-live test skipped'); return; } const connection = new Connection(url); - const payerAccount = new Account(); - await connection.confirmTransaction( - await connection.requestAirdrop(payerAccount.publicKey, 100000000000), - 0, - ); + const newAccount = new Account().publicKey; + + let testToken: Token; + let testTokenAccount: PublicKey; + let testSignature: TransactionSignature; + let testOwner: Account; + + // Setup token mints and accounts for token tests + beforeAll(async () => { + const payerAccount = new Account(); + await connection.confirmTransaction( + await connection.requestAirdrop(payerAccount.publicKey, 100000000000), + 0, + ); - const mintOwner = new Account(); - const accountOwner = new Account(); - const [token, tokenAccount] = await Token.createMint( - connection, - payerAccount, - mintOwner.publicKey, - accountOwner.publicKey, - new u64(11111), - 2, - TOKEN_PROGRAM_ID, - false, - ); + const mintOwner = new Account(); + const accountOwner = new Account(); + const [token, tokenAccount] = await Token.createMint( + connection, + payerAccount, + mintOwner.publicKey, + accountOwner.publicKey, + new u64(11111), + 2, + TOKEN_PROGRAM_ID, + false, + ); - await Token.createMint( - connection, - payerAccount, - mintOwner.publicKey, - accountOwner.publicKey, - new u64(10000), - 2, - TOKEN_PROGRAM_ID, - false, - ); + await Token.createMint( + connection, + payerAccount, + mintOwner.publicKey, + accountOwner.publicKey, + new u64(10000), + 2, + TOKEN_PROGRAM_ID, + false, + ); - const tokenAccountDest = await token.createAccount(accountOwner.publicKey); - await token.transfer( - tokenAccount, - tokenAccountDest, - accountOwner, - [], - new u64(1), - ); + const tokenAccountDest = await token.createAccount(accountOwner.publicKey); + testSignature = await token.transfer( + tokenAccount, + tokenAccountDest, + accountOwner, + [], + new u64(1), + ); - const supply = (await connection.getTokenSupply(token.publicKey, 'recent')) - .value; - expect(supply.uiAmount).toEqual(111.11); - expect(supply.decimals).toEqual(2); - expect(supply.amount).toEqual('11111'); + await connection.confirmTransaction(testSignature); - const newAccount = new Account(); - await expect( - connection.getTokenSupply(newAccount.publicKey, 'recent'), - ).rejects.toThrow(); + testOwner = accountOwner; + testToken = token; + testTokenAccount = tokenAccount; + }); - const balance = ( - await connection.getTokenAccountBalance(tokenAccount, 'recent') - ).value; - expect(balance.amount).toEqual('11110'); - expect(balance.decimals).toEqual(2); - expect(balance.uiAmount).toEqual(111.1); + test('get token supply', async () => { + const supply = (await connection.getTokenSupply(testToken.publicKey)).value; + expect(supply.uiAmount).toEqual(111.11); + expect(supply.decimals).toEqual(2); + expect(supply.amount).toEqual('11111'); - await expect( - connection.getTokenAccountBalance(newAccount.publicKey, 'recent'), - ).rejects.toThrow(); + await expect(connection.getTokenSupply(newAccount)).rejects.toThrow(); + }); - const accountsWithMintFilter = ( - await connection.getTokenAccountsByOwner( - accountOwner.publicKey, - {mint: token.publicKey}, - 'recent', - ) - ).value; - expect(accountsWithMintFilter.length).toEqual(2); + test('get confirmed token transaction', async () => { + const parsedTx = await connection.getParsedConfirmedTransaction( + testSignature, + ); + if (parsedTx === null) { + expect(parsedTx).not.toBeNull(); + return; + } + const {signatures, message} = parsedTx.transaction; + expect(signatures[0]).toEqual(testSignature); + const ix = message.instructions[0]; + if (ix.parsed) { + expect(ix.program).toEqual('spl-token'); + expect(ix.programId.equals(TOKEN_PROGRAM_ID)).toBe(true); + } else { + expect('parsed' in ix).toBe(true); + } - const accountsWithProgramFilter = ( - await connection.getTokenAccountsByOwner( - accountOwner.publicKey, - {programId: TOKEN_PROGRAM_ID}, - 'recent', - ) - ).value; - expect(accountsWithProgramFilter.length).toEqual(3); - - const noAccounts = ( - await connection.getTokenAccountsByOwner( - newAccount.publicKey, - {mint: token.publicKey}, - 'recent', - ) - ).value; - expect(noAccounts.length).toEqual(0); + const missingSignature = + '45pGoC4Rr3fJ1TKrsiRkhHRbdUeX7633XAGVec6XzVdpRbzQgHhe6ZC6Uq164MPWtiqMg7wCkC6Wy3jy2BqsDEKf'; + const nullResponse = await connection.getParsedConfirmedTransaction( + missingSignature, + ); - await expect( - connection.getTokenAccountsByOwner( - accountOwner.publicKey, - {mint: newAccount.publicKey}, - 'recent', - ), - ).rejects.toThrow(); + expect(nullResponse).toBeNull(); + }); - await expect( - connection.getTokenAccountsByOwner( - accountOwner.publicKey, - {programId: newAccount.publicKey}, - 'recent', - ), - ).rejects.toThrow(); + test('get token account balance', async () => { + const balance = (await connection.getTokenAccountBalance(testTokenAccount)) + .value; + expect(balance.amount).toEqual('11110'); + expect(balance.decimals).toEqual(2); + expect(balance.uiAmount).toEqual(111.1); + + await expect( + connection.getTokenAccountBalance(newAccount), + ).rejects.toThrow(); + }); + + test('get token accounts by owner', async () => { + const accountsWithMintFilter = ( + await connection.getTokenAccountsByOwner(testOwner.publicKey, { + mint: testToken.publicKey, + }) + ).value; + expect(accountsWithMintFilter.length).toEqual(2); + + const accountsWithProgramFilter = ( + await connection.getTokenAccountsByOwner(testOwner.publicKey, { + programId: TOKEN_PROGRAM_ID, + }) + ).value; + expect(accountsWithProgramFilter.length).toEqual(3); + + const noAccounts = ( + await connection.getTokenAccountsByOwner(newAccount, { + mint: testToken.publicKey, + }) + ).value; + expect(noAccounts.length).toEqual(0); + + await expect( + connection.getTokenAccountsByOwner(testOwner.publicKey, { + mint: newAccount, + }), + ).rejects.toThrow(); + + await expect( + connection.getTokenAccountsByOwner(testOwner.publicKey, { + programId: newAccount, + }), + ).rejects.toThrow(); + }); }); test('get largest accounts', async () => {