feat: add getBlock and getTransaction apis (#17449)

This commit is contained in:
Justin Starry 2021-05-25 10:12:47 -07:00 committed by GitHub
parent 3ae4806dae
commit 0dbe926efe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 413 additions and 34 deletions

View File

@ -510,6 +510,25 @@ export type ConfirmedTransactionMeta = {
err: TransactionError | null;
};
/**
* A processed transaction from the RPC API
*/
export type TransactionResponse = {
/** The slot during which the transaction was processed */
slot: number;
/** The transaction */
transaction: {
/** The transaction message */
message: Message;
/** The transaction signatures */
signatures: string[];
};
/** Metadata produced from the transaction */
meta: ConfirmedTransactionMeta | null;
/** The unix timestamp of when the transaction was processed */
blockTime?: number | null;
};
/**
* A confirmed transaction on the ledger
*/
@ -596,6 +615,43 @@ export type ParsedConfirmedTransaction = {
blockTime?: number | null;
};
/**
* A processed block fetched from the RPC API
*/
export type BlockResponse = {
/** Blockhash of this block */
blockhash: Blockhash;
/** Blockhash of this block's parent */
previousBlockhash: Blockhash;
/** Slot index of this block's parent */
parentSlot: number;
/** Vector of transactions with status meta and original message */
transactions: Array<{
/** The transaction */
transaction: {
/** The transaction message */
message: Message;
/** The transaction signatures */
signatures: string[];
};
/** Metadata produced from the transaction */
meta: ConfirmedTransactionMeta | null;
}>;
/** Vector of block rewards */
rewards?: Array<{
/** Public key of reward recipient */
pubkey: string;
/** Reward value in lamports */
lamports: number;
/** Account balance after reward is applied */
postBalance: number | null;
/** Type of reward received */
rewardType: string | null;
}>;
/** The unix timestamp of when the block was processed */
blockTime: number | null;
};
/**
* A ConfirmedBlock on the ledger
*/
@ -1234,9 +1290,6 @@ const GetSignatureStatusesRpcResult = jsonRpcResultAndContext(
*/
const GetMinimumBalanceForRentExemptionRpcResult = jsonRpcResult(number());
/**
* @internal
*/
const ConfirmedTransactionResult = pick({
signatures: array(string()),
message: pick({
@ -1257,15 +1310,6 @@ const ConfirmedTransactionResult = pick({
}),
});
const TransactionFromConfirmed = coerce(
instance(Transaction),
ConfirmedTransactionResult,
result => {
const {message, signatures} = result;
return Transaction.populate(new Message(message), signatures);
},
);
const ParsedInstructionResult = pick({
parsed: unknown(),
program: string(),
@ -1395,7 +1439,7 @@ const GetConfirmedBlockRpcResult = jsonRpcResult(
parentSlot: number(),
transactions: array(
pick({
transaction: TransactionFromConfirmed,
transaction: ConfirmedTransactionResult,
meta: nullable(ConfirmedTransactionMetaResult),
}),
),
@ -1436,9 +1480,9 @@ const GetConfirmedTransactionRpcResult = jsonRpcResult(
nullable(
pick({
slot: number(),
transaction: TransactionFromConfirmed,
meta: ConfirmedTransactionMetaResult,
blockTime: optional(nullable(number())),
transaction: ConfirmedTransactionResult,
}),
),
);
@ -2668,7 +2712,8 @@ export class Connection {
/**
* Fetch the current total currency supply of the cluster in lamports
* @deprecated Deprecated since v1.2.8. Use `Connection.getSupply()` instead.
*
* @deprecated Deprecated since v1.2.8. Please use {@link getSupply} instead.
*/
async getTotalSupply(commitment?: Commitment): Promise<number> {
const args = this._buildArgs([], commitment);
@ -2869,25 +2914,100 @@ export class Connection {
return res.result;
}
/**
* Fetch a processed block from the cluster.
*/
async getBlock(
slot: number,
opts?: {commitment?: Finality},
): Promise<BlockResponse | null> {
const args = this._buildArgsAtLeastConfirmed(
[slot],
opts && opts.commitment,
);
const unsafeRes = await this._rpcRequest('getConfirmedBlock', args);
const res = create(unsafeRes, GetConfirmedBlockRpcResult);
if ('error' in res) {
throw new Error('failed to get confirmed block: ' + res.error.message);
}
const result = res.result;
if (!result) return result;
return {
...result,
transactions: result.transactions.map(({transaction, meta}) => {
const message = new Message(transaction.message);
return {
meta,
transaction: {
...transaction,
message,
},
};
}),
};
}
/**
* Fetch a processed transaction from the cluster.
*/
async getTransaction(
signature: string,
opts?: {commitment?: Finality},
): Promise<TransactionResponse | null> {
const args = this._buildArgsAtLeastConfirmed(
[signature],
opts && opts.commitment,
);
const unsafeRes = await this._rpcRequest('getConfirmedTransaction', args);
const res = create(unsafeRes, GetConfirmedTransactionRpcResult);
if ('error' in res) {
throw new Error(
'failed to get confirmed transaction: ' + res.error.message,
);
}
const result = res.result;
if (!result) return result;
return {
...result,
transaction: {
...result.transaction,
message: new Message(result.transaction.message),
},
};
}
/**
* Fetch a list of Transactions and transaction statuses from the cluster
* for a confirmed block
* for a confirmed block.
*
* @deprecated Deprecated since v1.13.0. Please use {@link getBlock} instead.
*/
async getConfirmedBlock(
slot: number,
commitment?: Finality,
): Promise<ConfirmedBlock> {
const args = this._buildArgsAtLeastConfirmed([slot], commitment);
const unsafeRes = await this._rpcRequest('getConfirmedBlock', args);
const res = create(unsafeRes, GetConfirmedBlockRpcResult);
if ('error' in res) {
throw new Error('failed to get confirmed block: ' + res.error.message);
}
const result = res.result;
const result = await this.getBlock(slot, {commitment});
if (!result) {
throw new Error('Confirmed block ' + slot + ' not found');
}
return result;
return {
...result,
transactions: result.transactions.map(({transaction, meta}) => {
return {
meta,
transaction: Transaction.populate(
transaction.message,
transaction.signatures,
),
};
}),
};
}
/**
@ -2925,15 +3045,13 @@ export class Connection {
signature: TransactionSignature,
commitment?: Finality,
): Promise<ConfirmedTransaction | null> {
const args = this._buildArgsAtLeastConfirmed([signature], commitment);
const unsafeRes = await this._rpcRequest('getConfirmedTransaction', args);
const res = create(unsafeRes, GetConfirmedTransactionRpcResult);
if ('error' in res) {
throw new Error(
'failed to get confirmed transaction: ' + res.error.message,
);
}
return res.result;
const result = await this.getTransaction(signature, {commitment});
if (!result) return result;
const {message, signatures} = result.transaction;
return {
...result,
transaction: Transaction.populate(message, signatures),
};
}
/**
@ -2994,7 +3112,8 @@ export class Connection {
/**
* 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.
* @deprecated Deprecated since v1.3. Use `Connection.getConfirmedSignaturesForAddress2()` instead.
*
* @deprecated Deprecated since v1.3. Please use {@link getConfirmedSignaturesForAddress2} instead.
*
* @param address queried address
* @param startSlot start slot, inclusive

View File

@ -1283,6 +1283,151 @@ describe('Connection', () => {
expect(result).to.be.empty;
});
it('get transaction', 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.getBlock(slot);
if (block && block.transactions.length > 0) {
confirmedTransaction = block.transactions[0].transaction.signatures[0];
}
}
await mockRpcResponse({
method: 'getConfirmedTransaction',
params: [confirmedTransaction],
value: {
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.getTransaction(confirmedTransaction);
if (!result) {
expect(result).to.be.ok;
return;
}
const resultSignature = result.transaction.signatures[0];
expect(resultSignature).to.eq(confirmedTransaction);
const newAddress = Keypair.generate().publicKey;
const recentSignature = await helpers.airdrop({
connection,
address: newAddress,
amount: 1,
});
await mockRpcResponse({
method: 'getConfirmedTransaction',
params: [recentSignature],
value: null,
});
// Signature hasn't been finalized yet
const nullResponse = await connection.getTransaction(recentSignature);
expect(nullResponse).to.be.null;
});
it('get confirmed transaction', async () => {
await mockRpcResponse({
method: 'getSlot',
@ -1535,6 +1680,121 @@ describe('Connection', () => {
});
}
it('get block', async () => {
await mockRpcResponse({
method: 'getSlot',
params: [],
value: 1,
});
while ((await connection.getSlot()) <= 0) {
continue;
}
await mockRpcResponse({
method: 'getConfirmedBlock',
params: [0],
value: {
blockTime: 1614281964,
blockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo',
parentSlot: 0,
transactions: [],
},
});
// Block 0 never has any transactions in test validator
const block0 = await connection.getBlock(0);
if (!block0) {
expect(block0).not.to.be.null;
return;
}
const blockhash0 = block0.blockhash;
expect(block0.transactions).to.have.length(0);
expect(blockhash0).not.to.be.null;
expect(block0.previousBlockhash).not.to.be.null;
expect(block0.parentSlot).to.eq(0);
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 x = 1;
while (x < 10) {
const block1 = await connection.getBlock(x);
if (block1 && block1.transactions.length >= 1) {
expect(block1.previousBlockhash).to.eq(blockhash0);
expect(block1.blockhash).not.to.be.null;
expect(block1.parentSlot).to.eq(0);
expect(block1.transactions[0].transaction).not.to.be.null;
break;
}
x++;
}
await mockRpcResponse({
method: 'getConfirmedBlock',
params: [Number.MAX_SAFE_INTEGER],
error: {
message: `Block not available for slot ${Number.MAX_SAFE_INTEGER}`,
},
});
await expect(
connection.getBlock(Number.MAX_SAFE_INTEGER),
).to.be.rejectedWith(
`Block not available for slot ${Number.MAX_SAFE_INTEGER}`,
);
});
it('get confirmed block', async () => {
await mockRpcResponse({
method: 'getSlot',