feat: add getParsedConfirmedTransaction API

This commit is contained in:
Justin Starry
2020-08-06 19:16:01 +08:00
committed by Justin Starry
parent 5a63c9d535
commit b36e60738e
7 changed files with 414 additions and 133 deletions

36
web3.js/module.d.ts vendored
View File

@ -148,6 +148,39 @@ declare module '@solana/web3.js' {
meta: ConfirmedTransactionMeta | null; 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<PublicKey>;
data: string;
};
export type ParsedTransaction = {
signatures: Array<string>;
message: {
accountKeys: ParsedMessageAccount[];
instructions: (ParsedInstruction | PartiallyDecodedInstruction)[];
recentBlockhash: string;
};
};
export type ParsedConfirmedTransaction = {
slot: number;
transaction: ParsedTransaction;
meta: ConfirmedTransactionMeta | null;
};
export type KeyedAccountInfo = { export type KeyedAccountInfo = {
accountId: PublicKey; accountId: PublicKey;
accountInfo: AccountInfo; accountInfo: AccountInfo;
@ -288,6 +321,9 @@ declare module '@solana/web3.js' {
getConfirmedTransaction( getConfirmedTransaction(
signature: TransactionSignature, signature: TransactionSignature,
): Promise<ConfirmedTransaction | null>; ): Promise<ConfirmedTransaction | null>;
getParsedConfirmedTransaction(
signature: TransactionSignature,
): Promise<ParsedConfirmedTransaction | null>;
getConfirmedSignaturesForAddress( getConfirmedSignaturesForAddress(
address: PublicKey, address: PublicKey,
startSlot: number, startSlot: number,

View File

@ -169,6 +169,39 @@ declare module '@solana/web3.js' {
meta: ConfirmedTransactionMeta | null, 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<PublicKey>,
data: string,
|};
declare export type ParsedTransaction = {
signatures: Array<string>,
message: {
accountKeys: ParsedMessageAccount[],
instructions: (ParsedInstruction | PartiallyDecodedInstruction)[],
recentBlockhash: string,
},
};
declare export type ParsedConfirmedTransaction = {
slot: number,
transaction: ParsedTransaction,
meta: ConfirmedTransactionMeta | null,
};
declare export type KeyedAccountInfo = { declare export type KeyedAccountInfo = {
accountId: PublicKey, accountId: PublicKey,
accountInfo: AccountInfo, accountInfo: AccountInfo,
@ -309,6 +342,9 @@ declare module '@solana/web3.js' {
getConfirmedTransaction( getConfirmedTransaction(
signature: TransactionSignature, signature: TransactionSignature,
): Promise<ConfirmedTransaction | null>; ): Promise<ConfirmedTransaction | null>;
getParsedConfirmedTransaction(
signature: TransactionSignature,
): Promise<ParsedConfirmedTransaction | null>;
getConfirmedSignaturesForAddress( getConfirmedSignaturesForAddress(
address: PublicKey, address: PublicKey,
startSlot: number, startSlot: number,

View File

@ -4743,24 +4743,23 @@
} }
}, },
"@solana/spl-token": { "@solana/spl-token": {
"version": "0.0.4", "version": "0.0.5",
"resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.0.4.tgz", "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.0.5.tgz",
"integrity": "sha512-zYoZ6iYMKxGYbouunEkWdf6vWRJyEPOkAjvlNVjww9oPKMkIeM9VzgGtjZ/kKMelao1QEohH4JN9qXO4+LwfRA==", "integrity": "sha512-OXW/zHzMQqVGcSNrNt8sRaHlKT5vjdcUcmUHi8d4ssG8ChbZVA2lkJK10XDXlcnMIiSTindpEjiFmooYc9K3uQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/runtime": "^7.10.5", "@babel/runtime": "^7.10.5",
"@solana/web3.js": "^0.63.2", "@solana/web3.js": "^0.64.0",
"bn.js": "^5.0.0", "bn.js": "^5.0.0",
"buffer-layout": "^1.2.0", "buffer-layout": "^1.2.0",
"dotenv": "8.2.0", "dotenv": "8.2.0",
"json-to-pretty-yaml": "^1.2.2",
"mkdirp-promise": "^5.0.1" "mkdirp-promise": "^5.0.1"
} }
}, },
"@solana/web3.js": { "@solana/web3.js": {
"version": "0.63.2", "version": "0.64.2",
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.63.2.tgz", "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.64.2.tgz",
"integrity": "sha512-4jd8U1U/eFTEemr+jCzQCDepKnkttV4dxWsjMloifb82x1d6KgCzP+Jd6D9kr8f1MFj2i/AnG++97tlHAGTOkA==", "integrity": "sha512-aGRG1rn8fLerE4NscRL6rq0nSyYAK9K+TGRZxb6ue7Ontufa6wO1kxum4zJs17+xT0zVf8wABUtCMgP4W7FxpA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/runtime": "^7.3.1", "@babel/runtime": "^7.3.1",
@ -14214,16 +14213,6 @@
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" "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": { "json5": {
"version": "0.5.1", "version": "0.5.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "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": { "remove-trailing-separator": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
"integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
"dev": true "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": { "repeat-element": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",

View File

@ -97,7 +97,7 @@
"@babel/preset-flow": "^7.0.0", "@babel/preset-flow": "^7.0.0",
"@commitlint/config-conventional": "^9.0.1", "@commitlint/config-conventional": "^9.0.1",
"@commitlint/travis-cli": "^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/eslint-plugin": "^2.18.0",
"@typescript-eslint/parser": "^2.18.0", "@typescript-eslint/parser": "^2.18.0",
"acorn": "^7.0.0", "acorn": "^7.0.0",

View File

@ -49,7 +49,7 @@ type Context = {
* @property {boolean | undefined} skipPreflight disable transaction verification step * @property {boolean | undefined} skipPreflight disable transaction verification step
*/ */
export type SendOptions = { export type SendOptions = {
skipPreflight: ?boolean, skipPreflight?: boolean,
}; };
/** /**
@ -60,8 +60,8 @@ export type SendOptions = {
* @property {number | undefined} confirmations desired number of cluster confirmations * @property {number | undefined} confirmations desired number of cluster confirmations
*/ */
export type ConfirmOptions = { export type ConfirmOptions = {
skipPreflight: ?boolean, skipPreflight?: boolean,
confirmations: ?number, confirmations?: number,
}; };
/** /**
@ -378,6 +378,88 @@ type ConfirmedTransaction = {
meta: ConfirmedTransactionMeta | null, 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<PublicKey>,
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<ParsedMessageAccount>} accountKeys Accounts used in the instructions
* @property {Array<ParsedInstruction | PartiallyDecodedInstruction>} 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<string>} signatures Signatures for the transaction
* @property {ParsedMessage} message Message of the transaction
*/
type ParsedTransaction = {
signatures: Array<string>,
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 * A ConfirmedBlock on the ledger
* *
@ -808,14 +890,42 @@ const ConfirmedTransactionResult = struct({
numReadonlyUnsignedAccounts: 'number', numReadonlyUnsignedAccounts: 'number',
}), }),
instructions: struct.array([ instructions: struct.array([
struct.union([
struct.array(['number']),
struct({ struct({
accounts: struct.array(['number']), accounts: struct.array(['number']),
data: 'string', data: 'string',
programIdIndex: 'number', 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({
accounts: struct.array(['string']),
data: 'string',
programId: 'string',
}),
struct({
parsed: 'any',
program: 'string',
programId: 'string',
}),
]),
]), ]),
recentBlockhash: 'string', recentBlockhash: '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 * 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<ParsedConfirmedTransaction | null> {
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 * 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. * within a specified slot range. Max range allowed is 10,000 slots.

View File

@ -1,5 +1,6 @@
// @flow // @flow
import bs58 from 'bs58';
import fs from 'mz/fs'; import fs from 'mz/fs';
import { import {
@ -77,7 +78,29 @@ test('load BPF Rust program', async () => {
programId: program.publicKey, programId: program.publicKey,
}); });
await sendAndConfirmTransaction(connection, transaction, [from], { await sendAndConfirmTransaction(connection, transaction, [from], {
confirmations: 1,
skipPreflight: true, 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('');
}
}); });

View File

@ -16,6 +16,7 @@ import {mockGetRecentBlockhash} from './mockrpc/get-recent-blockhash';
import {url} from './url'; import {url} from './url';
import {sleep} from '../src/util/sleep'; import {sleep} from '../src/util/sleep';
import {BLOCKHASH_CACHE_TIMEOUT_MS} from '../src/connection'; import {BLOCKHASH_CACHE_TIMEOUT_MS} from '../src/connection';
import type {TransactionSignature} from '../src/transaction';
import type {SignatureStatus, TransactionError} from '../src/connection'; import type {SignatureStatus, TransactionError} from '../src/connection';
import {mockConfirmTransaction} from './mockrpc/confirm-transaction'; import {mockConfirmTransaction} from './mockrpc/confirm-transaction';
@ -1299,13 +1300,22 @@ const TOKEN_PROGRAM_ID = new PublicKey(
'TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o', 'TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o',
); );
test('token methods', async () => { describe('token methods', () => {
if (mockRpcEnabled) { if (mockRpcEnabled) {
console.log('non-live test skipped'); console.log('non-live test skipped');
return; return;
} }
const connection = new Connection(url); const connection = new Connection(url);
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(); const payerAccount = new Account();
await connection.confirmTransaction( await connection.confirmTransaction(
await connection.requestAirdrop(payerAccount.publicKey, 100000000000), await connection.requestAirdrop(payerAccount.publicKey, 100000000000),
@ -1337,7 +1347,7 @@ test('token methods', async () => {
); );
const tokenAccountDest = await token.createAccount(accountOwner.publicKey); const tokenAccountDest = await token.createAccount(accountOwner.publicKey);
await token.transfer( testSignature = await token.transfer(
tokenAccount, tokenAccount,
tokenAccountDest, tokenAccountDest,
accountOwner, accountOwner,
@ -1345,70 +1355,95 @@ test('token methods', async () => {
new u64(1), new u64(1),
); );
const supply = (await connection.getTokenSupply(token.publicKey, 'recent')) await connection.confirmTransaction(testSignature);
.value;
testOwner = accountOwner;
testToken = token;
testTokenAccount = tokenAccount;
});
test('get token supply', async () => {
const supply = (await connection.getTokenSupply(testToken.publicKey)).value;
expect(supply.uiAmount).toEqual(111.11); expect(supply.uiAmount).toEqual(111.11);
expect(supply.decimals).toEqual(2); expect(supply.decimals).toEqual(2);
expect(supply.amount).toEqual('11111'); expect(supply.amount).toEqual('11111');
const newAccount = new Account(); await expect(connection.getTokenSupply(newAccount)).rejects.toThrow();
await expect( });
connection.getTokenSupply(newAccount.publicKey, 'recent'),
).rejects.toThrow();
const balance = ( test('get confirmed token transaction', async () => {
await connection.getTokenAccountBalance(tokenAccount, 'recent') const parsedTx = await connection.getParsedConfirmedTransaction(
).value; 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 missingSignature =
'45pGoC4Rr3fJ1TKrsiRkhHRbdUeX7633XAGVec6XzVdpRbzQgHhe6ZC6Uq164MPWtiqMg7wCkC6Wy3jy2BqsDEKf';
const nullResponse = await connection.getParsedConfirmedTransaction(
missingSignature,
);
expect(nullResponse).toBeNull();
});
test('get token account balance', async () => {
const balance = (await connection.getTokenAccountBalance(testTokenAccount))
.value;
expect(balance.amount).toEqual('11110'); expect(balance.amount).toEqual('11110');
expect(balance.decimals).toEqual(2); expect(balance.decimals).toEqual(2);
expect(balance.uiAmount).toEqual(111.1); expect(balance.uiAmount).toEqual(111.1);
await expect( await expect(
connection.getTokenAccountBalance(newAccount.publicKey, 'recent'), connection.getTokenAccountBalance(newAccount),
).rejects.toThrow(); ).rejects.toThrow();
});
test('get token accounts by owner', async () => {
const accountsWithMintFilter = ( const accountsWithMintFilter = (
await connection.getTokenAccountsByOwner( await connection.getTokenAccountsByOwner(testOwner.publicKey, {
accountOwner.publicKey, mint: testToken.publicKey,
{mint: token.publicKey}, })
'recent',
)
).value; ).value;
expect(accountsWithMintFilter.length).toEqual(2); expect(accountsWithMintFilter.length).toEqual(2);
const accountsWithProgramFilter = ( const accountsWithProgramFilter = (
await connection.getTokenAccountsByOwner( await connection.getTokenAccountsByOwner(testOwner.publicKey, {
accountOwner.publicKey, programId: TOKEN_PROGRAM_ID,
{programId: TOKEN_PROGRAM_ID}, })
'recent',
)
).value; ).value;
expect(accountsWithProgramFilter.length).toEqual(3); expect(accountsWithProgramFilter.length).toEqual(3);
const noAccounts = ( const noAccounts = (
await connection.getTokenAccountsByOwner( await connection.getTokenAccountsByOwner(newAccount, {
newAccount.publicKey, mint: testToken.publicKey,
{mint: token.publicKey}, })
'recent',
)
).value; ).value;
expect(noAccounts.length).toEqual(0); expect(noAccounts.length).toEqual(0);
await expect( await expect(
connection.getTokenAccountsByOwner( connection.getTokenAccountsByOwner(testOwner.publicKey, {
accountOwner.publicKey, mint: newAccount,
{mint: newAccount.publicKey}, }),
'recent',
),
).rejects.toThrow(); ).rejects.toThrow();
await expect( await expect(
connection.getTokenAccountsByOwner( connection.getTokenAccountsByOwner(testOwner.publicKey, {
accountOwner.publicKey, programId: newAccount,
{programId: newAccount.publicKey}, }),
'recent',
),
).rejects.toThrow(); ).rejects.toThrow();
});
}); });
test('get largest accounts', async () => { test('get largest accounts', async () => {