feat: add getConfirmedTransaction and getConfirmedSignaturesForAddress

This commit is contained in:
Justin Starry 2020-04-21 15:40:44 +08:00 committed by Michael Vines
parent 7f182d22cd
commit ae53742e1a
4 changed files with 500 additions and 31 deletions

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

@ -98,6 +98,17 @@ declare module '@solana/web3.js' {
}>;
};
export type ConfirmedTransaction = {
slot: number;
transaction: Transaction;
meta: {
fee: number;
preBalances: Array<number>;
postBalances: Array<number>;
err: TransactionError | null;
} | null;
};
export type KeyedAccountInfo = {
accountId: PublicKey;
accountInfo: AccountInfo;
@ -184,6 +195,14 @@ declare module '@solana/web3.js' {
getBalance(publicKey: PublicKey, commitment?: Commitment): Promise<number>;
getClusterNodes(): Promise<Array<ContactInfo>>;
getConfirmedBlock(slot: number): Promise<ConfirmedBlock>;
getConfirmedTransaction(
signature: TransactionSignature,
): Promise<ConfirmedTransaction | null>;
getConfirmedSignaturesForAddress(
address: PublicKey,
startSlot: number,
endSlot: number,
): Promise<Array<TransactionSignature>>;
getVoteAccounts(commitment?: Commitment): Promise<VoteAccountStatus>;
confirmTransactionAndContext(
signature: TransactionSignature,

View File

@ -111,6 +111,17 @@ declare module '@solana/web3.js' {
}>,
};
declare export type ConfirmedTransaction = {
slot: number,
transaction: Transaction,
meta: {
fee: number,
preBalances: Array<number>,
postBalances: Array<number>,
err: TransactionError | null,
} | null,
};
declare export type KeyedAccountInfo = {
accountId: PublicKey,
accountInfo: AccountInfo,
@ -197,6 +208,14 @@ declare module '@solana/web3.js' {
getBalance(publicKey: PublicKey, commitment: ?Commitment): Promise<number>;
getClusterNodes(): Promise<Array<ContactInfo>>;
getConfirmedBlock(slot: number): Promise<ConfirmedBlock>;
getConfirmedTransaction(
signature: TransactionSignature,
): Promise<ConfirmedTransaction | null>;
getConfirmedSignaturesForAddress(
address: PublicKey,
startSlot: number,
endSlot: number,
): Promise<Array<TransactionSignature>>;
getVoteAccounts(commitment: ?Commitment): Promise<VoteAccountStatus>;
confirmTransactionAndContext(
signature: TransactionSignature,

View File

@ -233,6 +233,25 @@ const Version = struct({
'solana-core': 'string',
});
/**
* A confirmed transaction on the ledger
*
* @typedef {Object} ConfirmedTransaction
* @property {number} slot The slot during which the transaction was processed
* @property {Transaction} transaction The details of the transaction
* @property {object} meta Slot index of this block's parent
*/
type ConfirmedTransaction = {
slot: number,
transaction: Transaction,
meta: {
fee: number,
err: TransactionError | null,
preBalances: Array<number>,
postBalances: Array<number>,
} | null,
};
/**
* A ConfirmedBlock on the ledger
*
@ -357,6 +376,13 @@ const GetAccountInfoAndContextRpcResult = jsonRpcResultAndContext(
struct.union(['null', AccountInfoResult]),
);
/**
* @private
*/
const GetConfirmedSignaturesForAddressRpcResult = jsonRpcResult(
struct.array(['string']),
);
/***
* Expected JSON RPC response for the "accountNotification" message
*/
@ -519,6 +545,45 @@ const GetTotalSupplyRpcResult = jsonRpcResult('number');
*/
const GetMinimumBalanceForRentExemptionRpcResult = jsonRpcResult('number');
/**
* @private
*/
const ConfirmedTransactionResult = struct({
signatures: struct.array(['string']),
message: struct({
accountKeys: struct.array(['string']),
header: struct({
numRequiredSignatures: 'number',
numReadonlySignedAccounts: 'number',
numReadonlyUnsignedAccounts: 'number',
}),
instructions: struct.array([
struct.union([
struct.array(['number']),
struct({
accounts: struct.array(['number']),
data: 'string',
programIdIndex: 'number',
}),
]),
]),
recentBlockhash: 'string',
}),
});
/**
* @private
*/
const ConfirmedTransactionMetaResult = struct.union([
'null',
struct.pick({
err: TransactionErrorResult,
fee: 'number',
preBalances: struct.array(['number']),
postBalances: struct.array(['number']),
}),
]);
/**
* Expected JSON RPC response for the "getConfirmedBlock" message
*/
@ -531,37 +596,8 @@ export const GetConfirmedBlockRpcResult = jsonRpcResult(
parentSlot: 'number',
transactions: struct.array([
struct({
transaction: struct({
signatures: struct.array(['string']),
message: struct({
accountKeys: struct.array(['string']),
header: struct({
numRequiredSignatures: 'number',
numReadonlySignedAccounts: 'number',
numReadonlyUnsignedAccounts: 'number',
}),
instructions: struct.array([
struct.union([
struct.array(['number']),
struct({
accounts: struct.array(['number']),
data: 'string',
programIdIndex: 'number',
}),
]),
]),
recentBlockhash: 'string',
}),
}),
meta: struct.union([
'null',
struct.pick({
err: TransactionErrorResult,
fee: 'number',
preBalances: struct.array(['number']),
postBalances: struct.array(['number']),
}),
]),
transaction: ConfirmedTransactionResult,
meta: ConfirmedTransactionMetaResult,
}),
]),
rewards: struct.union([
@ -577,6 +613,20 @@ export const GetConfirmedBlockRpcResult = jsonRpcResult(
]),
);
/**
* Expected JSON RPC response for the "getConfirmedTransaction" message
*/
const GetConfirmedTransactionRpcResult = jsonRpcResult(
struct.union([
'null',
struct({
slot: 'number',
transaction: ConfirmedTransactionResult,
meta: ConfirmedTransactionMetaResult,
}),
]),
);
/**
* Expected JSON RPC response for the "getRecentBlockhash" message
*/
@ -1266,6 +1316,59 @@ export class Connection {
};
}
/**
* Fetch a transaction details for a confirmed transaction
*/
async getConfirmedTransaction(
signature: TransactionSignature,
): Promise<ConfirmedTransaction | null> {
const unsafeRes = await this._rpcRequest('getConfirmedTransaction', [
signature,
]);
const {result, error} = GetConfirmedTransactionRpcResult(unsafeRes);
if (error) {
throw new Error('failed to get confirmed transaction: ' + error.message);
}
assert(typeof result !== 'undefined');
if (result === null) {
return result;
}
return {
slot: result.slot,
transaction: Transaction.fromRpcResult(result.transaction),
meta: result.meta,
};
}
/**
* 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.
*
* @param address queried address
* @param startSlot start slot, inclusive
* @param endSlot end slot, inclusive
*/
async getConfirmedSignaturesForAddress(
address: PublicKey,
startSlot: number,
endSlot: number,
): Promise<Array<TransactionSignature>> {
const unsafeRes = await this._rpcRequest(
'getConfirmedSignaturesForAddress',
[address.toBase58(), startSlot, endSlot],
);
const result = GetConfirmedSignaturesForAddressRpcResult(unsafeRes);
if (result.error) {
throw new Error(
'failed to get confirmed signatures for address: ' +
result.error.message,
);
}
assert(typeof result.result !== 'undefined');
return result.result;
}
/**
* Fetch the contents of a Nonce account from the cluster, return with context
*/

View File

@ -1,10 +1,13 @@
// @flow
import bs58 from 'bs58';
import {
Account,
Connection,
SystemProgram,
sendAndConfirmTransaction,
LAMPORTS_PER_SOL,
PublicKey,
} from '../src';
import {DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND} from '../src/timing';
import {mockRpc, mockRpcEnabled} from './__mocks__/node-fetch';
@ -570,6 +573,331 @@ test('get minimum balance for rent exemption', async () => {
expect(count).toBeGreaterThanOrEqual(0);
});
test('get confirmed signatures for address', async () => {
const connection = new Connection(url);
mockRpc.push([
url,
{
method: 'getSlot',
params: [],
},
{
error: null,
result: 1,
},
]);
while ((await connection.getSlot()) <= 0) {
continue;
}
mockRpc.push([
url,
{
method: 'getConfirmedBlock',
params: [1],
},
{
error: null,
result: {
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;
let expectedSignature: ?string;
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);
}
}
}
mockRpc.push([
url,
{
method: 'getConfirmedSignaturesForAddress',
params: [address.toBase58(), slot, slot + 1],
},
{
error: null,
result: [expectedSignature],
},
]);
const confirmedSignatures = await connection.getConfirmedSignaturesForAddress(
address,
slot,
slot + 1,
);
expect(confirmedSignatures.includes(expectedSignature)).toBe(true);
const badSlot = Number.MAX_SAFE_INTEGER - 1;
mockRpc.push([
url,
{
method: 'getConfirmedSignaturesForAddress',
params: [address.toBase58(), badSlot, badSlot + 1],
},
{
error: null,
result: [],
},
]);
const emptySignatures = await connection.getConfirmedSignaturesForAddress(
address,
badSlot,
badSlot + 1,
);
expect(emptySignatures.length).toBe(0);
});
test('get confirmed transaction', async () => {
const connection = new Connection(url);
mockRpc.push([
url,
{
method: 'getSlot',
params: [],
},
{
error: null,
result: 1,
},
]);
while ((await connection.getSlot()) <= 0) {
continue;
}
mockRpc.push([
url,
{
method: 'getConfirmedBlock',
params: [1],
},
{
error: null,
result: {
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 confirmedTransaction: ?string;
while (!confirmedTransaction) {
slot++;
const block = await connection.getConfirmedBlock(slot);
for (const tx of block.transactions) {
if (tx.transaction.signature) {
confirmedTransaction = bs58.encode(tx.transaction.signature);
}
}
}
mockRpc.push([
url,
{
method: 'getConfirmedTransaction',
params: [confirmedTransaction],
},
{
error: null,
result: {
slot,
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',
],
},
meta: {
fee: 10000,
postBalances: [499260347380, 15298080, 1, 1, 1],
preBalances: [499260357380, 15298080, 1, 1, 1],
status: {Ok: null},
err: null,
},
},
},
]);
const result = await connection.getConfirmedTransaction(confirmedTransaction);
if (!result) {
expect(result).toBeDefined();
expect(result).not.toBeNull();
return;
}
if (result.transaction.signature === null) {
expect(result.transaction.signature).not.toBeNull();
return;
}
const resultSignature = bs58.encode(result.transaction.signature);
expect(resultSignature).toEqual(confirmedTransaction);
const newAddress = new Account().publicKey;
mockRpc.push([
url,
{
method: 'requestAirdrop',
params: [newAddress.toBase58(), 1, {commitment: 'recent'}],
},
{
error: null,
result:
'1WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
},
]);
const recentSignature = await connection.requestAirdrop(
newAddress,
1,
'recent',
);
mockRpc.push([
url,
{
method: 'getConfirmedTransaction',
params: [recentSignature],
},
{
error: null,
result: null,
},
]);
const nullResponse = await connection.getConfirmedTransaction(
recentSignature,
);
expect(nullResponse).toBeNull();
});
test('get confirmed block', async () => {
const connection = new Connection(url);