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:
parent
a2dae8e8d4
commit
63d0c78b20
|
@ -63,6 +63,16 @@ export const BLOCKHASH_CACHE_TIMEOUT_MS = 30 * 1000;
|
||||||
|
|
||||||
type RpcRequest = (methodName: string, args: Array<any>) => any;
|
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 =
|
export type TokenAccountsFilter =
|
||||||
| {
|
| {
|
||||||
mint: PublicKey;
|
mint: PublicKey;
|
||||||
|
@ -642,7 +652,7 @@ export type PerfSample = {
|
||||||
samplePeriodSecs: number;
|
samplePeriodSecs: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function createRpcRequest(url: string, useHttps: boolean): RpcRequest {
|
function createRpcClient(url: string, useHttps: boolean): RpcClient {
|
||||||
let agentManager: AgentManager | undefined;
|
let agentManager: AgentManager | undefined;
|
||||||
if (!process.env.BROWSER) {
|
if (!process.env.BROWSER) {
|
||||||
agentManager = new AgentManager(useHttps);
|
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 (method, args) => {
|
||||||
return new Promise((resolve, reject) => {
|
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) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
return;
|
return;
|
||||||
|
@ -1591,7 +1623,9 @@ export type ConfirmedSignatureInfo = {
|
||||||
export class Connection {
|
export class Connection {
|
||||||
/** @internal */ _commitment?: Commitment;
|
/** @internal */ _commitment?: Commitment;
|
||||||
/** @internal */ _rpcEndpoint: string;
|
/** @internal */ _rpcEndpoint: string;
|
||||||
|
/** @internal */ _rpcClient: RpcClient;
|
||||||
/** @internal */ _rpcRequest: RpcRequest;
|
/** @internal */ _rpcRequest: RpcRequest;
|
||||||
|
/** @internal */ _rpcBatchRequest: RpcBatchRequest;
|
||||||
/** @internal */ _rpcWebSocket: RpcWebSocketClient;
|
/** @internal */ _rpcWebSocket: RpcWebSocketClient;
|
||||||
/** @internal */ _rpcWebSocketConnected: boolean = false;
|
/** @internal */ _rpcWebSocketConnected: boolean = false;
|
||||||
/** @internal */ _rpcWebSocketHeartbeat: ReturnType<
|
/** @internal */ _rpcWebSocketHeartbeat: ReturnType<
|
||||||
|
@ -1647,7 +1681,9 @@ export class Connection {
|
||||||
let url = urlParse(endpoint);
|
let url = urlParse(endpoint);
|
||||||
const useHttps = url.protocol === 'https:';
|
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._commitment = commitment;
|
||||||
this._blockhashInfo = {
|
this._blockhashInfo = {
|
||||||
recentBlockhash: null,
|
recentBlockhash: null,
|
||||||
|
@ -2503,6 +2539,33 @@ export class Connection {
|
||||||
return res.result;
|
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
|
* 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.
|
||||||
|
|
|
@ -32,6 +32,7 @@ import {
|
||||||
helpers,
|
helpers,
|
||||||
mockErrorMessage,
|
mockErrorMessage,
|
||||||
mockErrorResponse,
|
mockErrorResponse,
|
||||||
|
mockRpcBatchResponse,
|
||||||
mockRpcResponse,
|
mockRpcResponse,
|
||||||
mockServer,
|
mockServer,
|
||||||
} from './mocks/rpc-http';
|
} 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 () => {
|
it('get confirmed transaction', async () => {
|
||||||
await mockRpcResponse({
|
await mockRpcResponse({
|
||||||
method: 'getSlot',
|
method: 'getSlot',
|
||||||
|
|
|
@ -5,7 +5,7 @@ import * as mockttp from 'mockttp';
|
||||||
|
|
||||||
import {mockRpcMessage} from './rpc-websockets';
|
import {mockRpcMessage} from './rpc-websockets';
|
||||||
import {Account, Connection, PublicKey, Transaction} from '../../src';
|
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 =
|
export const mockServer: mockttp.Mockttp | undefined =
|
||||||
process.env.TEST_LIVE === undefined ? mockttp.getLocal() : undefined;
|
process.env.TEST_LIVE === undefined ? mockttp.getLocal() : undefined;
|
||||||
|
@ -24,6 +24,40 @@ export const mockErrorResponse = {
|
||||||
message: mockErrorMessage,
|
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 ({
|
export const mockRpcResponse = async ({
|
||||||
method,
|
method,
|
||||||
params,
|
params,
|
||||||
|
|
Loading…
Reference in New Issue