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;
};
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 = {
accountId: PublicKey;
accountInfo: AccountInfo;
@ -288,6 +321,9 @@ declare module '@solana/web3.js' {
getConfirmedTransaction(
signature: TransactionSignature,
): Promise<ConfirmedTransaction | null>;
getParsedConfirmedTransaction(
signature: TransactionSignature,
): Promise<ParsedConfirmedTransaction | null>;
getConfirmedSignaturesForAddress(
address: PublicKey,
startSlot: number,

View File

@ -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<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 = {
accountId: PublicKey,
accountInfo: AccountInfo,
@ -309,6 +342,9 @@ declare module '@solana/web3.js' {
getConfirmedTransaction(
signature: TransactionSignature,
): Promise<ConfirmedTransaction | null>;
getParsedConfirmedTransaction(
signature: TransactionSignature,
): Promise<ParsedConfirmedTransaction | null>;
getConfirmedSignaturesForAddress(
address: PublicKey,
startSlot: number,

View File

@ -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",

View File

@ -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",

View File

@ -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<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
*
@ -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<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
* within a specified slot range. Max range allowed is 10,000 slots.

View File

@ -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('');
}
});

View File

@ -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;
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,
);
let testToken: Token;
let testTokenAccount: PublicKey;
let testSignature: TransactionSignature;
let testOwner: Account;
await Token.createMint(
connection,
payerAccount,
mintOwner.publicKey,
accountOwner.publicKey,
new u64(10000),
2,
TOKEN_PROGRAM_ID,
false,
);
// 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 tokenAccountDest = await token.createAccount(accountOwner.publicKey);
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');
const newAccount = new Account();
await expect(
connection.getTokenSupply(newAccount.publicKey, 'recent'),
).rejects.toThrow();
const balance = (
await connection.getTokenAccountBalance(tokenAccount, 'recent')
).value;
expect(balance.amount).toEqual('11110');
expect(balance.decimals).toEqual(2);
expect(balance.uiAmount).toEqual(111.1);
await expect(
connection.getTokenAccountBalance(newAccount.publicKey, 'recent'),
).rejects.toThrow();
const accountsWithMintFilter = (
await connection.getTokenAccountsByOwner(
const mintOwner = new Account();
const accountOwner = new Account();
const [token, tokenAccount] = await Token.createMint(
connection,
payerAccount,
mintOwner.publicKey,
accountOwner.publicKey,
{mint: token.publicKey},
'recent',
)
).value;
expect(accountsWithMintFilter.length).toEqual(2);
new u64(11111),
2,
TOKEN_PROGRAM_ID,
false,
);
const accountsWithProgramFilter = (
await connection.getTokenAccountsByOwner(
await Token.createMint(
connection,
payerAccount,
mintOwner.publicKey,
accountOwner.publicKey,
{programId: TOKEN_PROGRAM_ID},
'recent',
)
).value;
expect(accountsWithProgramFilter.length).toEqual(3);
new u64(10000),
2,
TOKEN_PROGRAM_ID,
false,
);
const noAccounts = (
await connection.getTokenAccountsByOwner(
newAccount.publicKey,
{mint: token.publicKey},
'recent',
)
).value;
expect(noAccounts.length).toEqual(0);
const tokenAccountDest = await token.createAccount(accountOwner.publicKey);
testSignature = await token.transfer(
tokenAccount,
tokenAccountDest,
accountOwner,
[],
new u64(1),
);
await expect(
connection.getTokenAccountsByOwner(
accountOwner.publicKey,
{mint: newAccount.publicKey},
'recent',
),
).rejects.toThrow();
await connection.confirmTransaction(testSignature);
await expect(
connection.getTokenAccountsByOwner(
accountOwner.publicKey,
{programId: newAccount.publicKey},
'recent',
),
).rejects.toThrow();
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.decimals).toEqual(2);
expect(supply.amount).toEqual('11111');
await expect(connection.getTokenSupply(newAccount)).rejects.toThrow();
});
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 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.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 () => {