web3.js: add support for batch getParsedConfirmedTransactions (#16001)

* feat: add support for batch requests

* feat: get confirmed transactions batch

* feat: test get parsed confirmed transactions

* fix: run prettier

* fix: test uses one signature

* fix: fix docs and return type on ParsedConfirmedTransactions

* fix: null values in test
This commit is contained in:
Josh 2021-03-22 10:22:59 -07:00 committed by GitHub
parent a2dae8e8d4
commit 63d0c78b20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 367 additions and 4 deletions

View File

@ -63,6 +63,16 @@ export const BLOCKHASH_CACHE_TIMEOUT_MS = 30 * 1000;
type RpcRequest = (methodName: string, args: Array<any>) => any;
type RpcBatchRequest = (requests: RpcParams[]) => any;
/**
* @internal
*/
export type RpcParams = {
methodName: string;
args: Array<any>;
};
export type TokenAccountsFilter =
| {
mint: PublicKey;
@ -642,7 +652,7 @@ export type PerfSample = {
samplePeriodSecs: number;
};
function createRpcRequest(url: string, useHttps: boolean): RpcRequest {
function createRpcClient(url: string, useHttps: boolean): RpcClient {
let agentManager: AgentManager | undefined;
if (!process.env.BROWSER) {
agentManager = new AgentManager(useHttps);
@ -692,9 +702,31 @@ function createRpcRequest(url: string, useHttps: boolean): RpcRequest {
}
}, {});
return clientBrowser;
}
function createRpcRequest(client: RpcClient): RpcRequest {
return (method, args) => {
return new Promise((resolve, reject) => {
clientBrowser.request(method, args, (err: any, response: any) => {
client.request(method, args, (err: any, response: any) => {
if (err) {
reject(err);
return;
}
resolve(response);
});
});
};
}
function createRpcBatchRequest(client: RpcClient): RpcBatchRequest {
return (requests: RpcParams[]) => {
return new Promise((resolve, reject) => {
const batch = requests.map((params: RpcParams) => {
return client.request(params.methodName, params.args);
});
client.request(batch, (err: any, response: any) => {
if (err) {
reject(err);
return;
@ -1591,7 +1623,9 @@ export type ConfirmedSignatureInfo = {
export class Connection {
/** @internal */ _commitment?: Commitment;
/** @internal */ _rpcEndpoint: string;
/** @internal */ _rpcClient: RpcClient;
/** @internal */ _rpcRequest: RpcRequest;
/** @internal */ _rpcBatchRequest: RpcBatchRequest;
/** @internal */ _rpcWebSocket: RpcWebSocketClient;
/** @internal */ _rpcWebSocketConnected: boolean = false;
/** @internal */ _rpcWebSocketHeartbeat: ReturnType<
@ -1647,7 +1681,9 @@ export class Connection {
let url = urlParse(endpoint);
const useHttps = url.protocol === 'https:';
this._rpcRequest = createRpcRequest(url.href, useHttps);
this._rpcClient = createRpcClient(url.href, useHttps);
this._rpcRequest = createRpcRequest(this._rpcClient);
this._rpcBatchRequest = createRpcBatchRequest(this._rpcClient);
this._commitment = commitment;
this._blockhashInfo = {
recentBlockhash: null,
@ -2503,6 +2539,33 @@ export class Connection {
return res.result;
}
/**
* Fetch parsed transaction details for a batch of confirmed transactions
*/
async getParsedConfirmedTransactions(
signatures: TransactionSignature[],
): Promise<(ParsedConfirmedTransaction | null)[]> {
const batch = signatures.map(signature => {
return {
methodName: 'getConfirmedTransaction',
args: [signature, 'jsonParsed'],
};
});
const unsafeRes = await this._rpcBatchRequest(batch);
const res = unsafeRes.map((unsafeRes: any) => {
const res = create(unsafeRes, GetParsedConfirmedTransactionRpcResult);
if ('error' in res) {
throw new Error(
'failed to get confirmed transactions: ' + res.error.message,
);
}
return res.result;
});
return res;
}
/**
* 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

@ -32,6 +32,7 @@ import {
helpers,
mockErrorMessage,
mockErrorResponse,
mockRpcBatchResponse,
mockRpcResponse,
mockServer,
} from './mocks/rpc-http';
@ -649,6 +650,271 @@ describe('Connection', () => {
}
});
it('get parsed confirmed transactions', async () => {
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 confirmedTransaction: string | undefined;
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);
}
}
}
await mockRpcBatchResponse({
batch: [
{
methodName: 'getConfirmedTransaction',
args: [],
},
],
result: [
{
blockTime: 1616102519,
meta: {
err: null,
fee: 5000,
innerInstructions: [],
logMessages: [
'Program Vote111111111111111111111111111111111111111 invoke [1]',
'Program Vote111111111111111111111111111111111111111 success',
],
postBalances: [499999995000, 26858640, 1, 1, 1],
postTokenBalances: [],
preBalances: [500000000000, 26858640, 1, 1, 1],
preTokenBalances: [],
status: {
Ok: null,
},
},
slot: 2,
transaction: {
message: {
accountKeys: [
{
pubkey: 'jcU4R7JccGEvDpe1i6bahvHpe47XahMXacG73EzE198',
signer: true,
writable: true,
},
{
pubkey: 'GfBcnCAU7kWfAYqKRCNyWEHjdEJZmzRZvEcX5bbzEQqt',
signer: false,
writable: true,
},
{
pubkey: 'SysvarS1otHashes111111111111111111111111111',
signer: false,
writable: false,
},
{
pubkey: 'SysvarC1ock11111111111111111111111111111111',
signer: false,
writable: false,
},
{
pubkey: 'Vote111111111111111111111111111111111111111',
signer: false,
writable: false,
},
],
instructions: [
{
parsed: {
info: {
clockSysvar:
'SysvarC1ock11111111111111111111111111111111',
slotHashesSysvar:
'SysvarS1otHashes111111111111111111111111111',
vote: {
hash: 'GuCya3AAGxn1qhoqxqy3WEdZdZUkXKpa9pthQ3tqvbpx',
slots: [1],
timestamp: 1616102669,
},
voteAccount:
'GfBcnCAU7kWfAYqKRCNyWEHjdEJZmzRZvEcX5bbzEQqt',
voteAuthority:
'jcU4R7JccGEvDpe1i6bahvHpe47XahMXacG73EzE198',
},
type: 'vote',
},
program: 'vote',
programId: 'Vote111111111111111111111111111111111111111',
},
],
recentBlockhash: 'G9ywjV5CVgMtLXruXtrE7af4QgFKYNXgDTw4jp7SWcSo',
},
signatures: [
'4G4rTqnUdzrmBHsdKJSiMtonpQLWSw1avJ8YxWQ95jE6iFFHFsEkBnoYycxnkBS9xHWRc6EarDsrFG9USFBbjfjx',
],
},
},
{
blockTime: 1616102519,
meta: {
err: null,
fee: 5000,
innerInstructions: [],
logMessages: [
'Program Vote111111111111111111111111111111111111111 invoke [1]',
'Program Vote111111111111111111111111111111111111111 success',
],
postBalances: [499999995000, 26858640, 1, 1, 1],
postTokenBalances: [],
preBalances: [500000000000, 26858640, 1, 1, 1],
preTokenBalances: [],
status: {
Ok: null,
},
},
slot: 2,
transaction: {
message: {
accountKeys: [
{
pubkey: 'jcU4R7JccGEvDpe1i6bahvHpe47XahMXacG73EzE198',
signer: true,
writable: true,
},
{
pubkey: 'GfBcnCAU7kWfAYqKRCNyWEHjdEJZmzRZvEcX5bbzEQqt',
signer: false,
writable: true,
},
{
pubkey: 'SysvarS1otHashes111111111111111111111111111',
signer: false,
writable: false,
},
{
pubkey: 'SysvarC1ock11111111111111111111111111111111',
signer: false,
writable: false,
},
{
pubkey: 'Vote111111111111111111111111111111111111111',
signer: false,
writable: false,
},
],
instructions: [
{
parsed: {
info: {
clockSysvar:
'SysvarC1ock11111111111111111111111111111111',
slotHashesSysvar:
'SysvarS1otHashes111111111111111111111111111',
vote: {
hash: 'GuCya3AAGxn1qhoqxqy3WEdZdZUkXKpa9pthQ3tqvbpx',
slots: [1],
timestamp: 1616102669,
},
voteAccount:
'GfBcnCAU7kWfAYqKRCNyWEHjdEJZmzRZvEcX5bbzEQqt',
voteAuthority:
'jcU4R7JccGEvDpe1i6bahvHpe47XahMXacG73EzE198',
},
type: 'vote',
},
program: 'vote',
programId: 'Vote111111111111111111111111111111111111111',
},
],
recentBlockhash: 'G9ywjV5CVgMtLXruXtrE7af4QgFKYNXgDTw4jp7SWcSo',
},
signatures: [
'4G4rTqnUdzrmBHsdKJSiMtonpQLWSw1avJ8YxWQ95jE6iFFHFsEkBnoYycxnkBS9xHWRc6EarDsrFG9USFBbjfjx',
],
},
},
],
});
const result = await connection.getParsedConfirmedTransactions([
confirmedTransaction,
confirmedTransaction,
]);
if (!result) {
expect(result).to.be.ok;
return;
}
expect(result).to.be.length(2);
expect(result[0]).to.not.be.null;
expect(result[1]).to.not.be.null;
if (result[0] !== null) {
expect(result[0].transaction.signatures).not.to.be.null;
}
if (result[1] !== null) {
expect(result[1].transaction.signatures).not.to.be.null;
}
});
it('get confirmed transaction', async () => {
await mockRpcResponse({
method: 'getSlot',

View File

@ -5,7 +5,7 @@ import * as mockttp from 'mockttp';
import {mockRpcMessage} from './rpc-websockets';
import {Account, Connection, PublicKey, Transaction} from '../../src';
import type {Commitment} from '../../src/connection';
import type {Commitment, RpcParams} from '../../src/connection';
export const mockServer: mockttp.Mockttp | undefined =
process.env.TEST_LIVE === undefined ? mockttp.getLocal() : undefined;
@ -24,6 +24,40 @@ export const mockErrorResponse = {
message: mockErrorMessage,
};
export const mockRpcBatchResponse = async ({
batch,
result,
error,
}: {
batch: RpcParams[];
result: any[];
error?: string;
}) => {
if (!mockServer) return;
const request = batch.map((batch: RpcParams) => {
return {
jsonrpc: '2.0',
method: batch.methodName,
params: batch.args,
};
});
const response = result.map((result: any) => {
return {
jsonrpc: '2.0',
id: '',
result,
error,
};
});
await mockServer
.post('/')
.withJsonBodyIncluding(request)
.thenReply(200, JSON.stringify(response));
};
export const mockRpcResponse = async ({
method,
params,