From c08cfafd6c599abbf440fbac1974ac8d754e265f Mon Sep 17 00:00:00 2001 From: stellaw1 Date: Tue, 22 Feb 2022 18:49:27 -0800 Subject: [PATCH] feat: adds getBlockProduction RPC call --- web3.js/src/connection.ts | 87 +++++++++++++++++++++++ web3.js/test/connection.test.ts | 122 ++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+) diff --git a/web3.js/src/connection.ts b/web3.js/src/connection.ts index f9ed8fb4d..f572c9d58 100644 --- a/web3.js/src/connection.ts +++ b/web3.js/src/connection.ts @@ -754,6 +754,48 @@ export type BlockSignatures = { blockTime: number | null; }; +/** + * recent block production information + */ +export type BlockProduction = Readonly<{ + /** a dictionary of validator identities, as base-58 encoded strings. Value is a two element array containing the number of leader slots and the number of blocks produced */ + byIdentity: Readonly>>; + /** Block production slot range */ + range: Readonly<{ + /** first slot of the block production information (inclusive) */ + firstSlot: number; + /** last slot of block production information (inclusive) */ + lastSlot: number; + }>; +}>; + +export type GetBlockProductionConfig = { + /** Optional commitment level */ + commitment?: Commitment; + /** Slot range to return block production for. If parameter not provided, defaults to current epoch. */ + range?: { + /** first slot to return block production information for (inclusive) */ + firstSlot: number; + /** last slot to return block production information for (inclusive). If parameter not provided, defaults to the highest slot */ + lastSlot?: number; + }; + /** Only return results for this validator identity (base-58 encoded) */ + identity?: string; +}; + +/** + * Expected JSON RPC response for the "getBlockProduction" message + */ +const BlockProductionResponseStruct = jsonRpcResultAndContext( + pick({ + byIdentity: record(string(), array(number())), + range: pick({ + firstSlot: number(), + lastSlot: number(), + }), + }), +); + /** * A performance sample */ @@ -3230,6 +3272,51 @@ export class Connection { }; } + /* + * Returns the current block height of the node + */ + async getBlockHeight(commitment?: Commitment): Promise { + const args = this._buildArgs([], commitment); + const unsafeRes = await this._rpcRequest('getBlockHeight', args); + const res = create(unsafeRes, jsonRpcResult(number())); + if ('error' in res) { + throw new Error( + 'failed to get block height information: ' + res.error.message, + ); + } + + return res.result; + } + + /* + * Returns recent block production information from the current or previous epoch + */ + async getBlockProduction( + configOrCommitment?: GetBlockProductionConfig | Commitment, + ): Promise> { + let extra: Omit | undefined; + let commitment: Commitment | undefined; + + if (typeof configOrCommitment === 'string') { + commitment = configOrCommitment; + } else if (configOrCommitment) { + const {commitment: c, ...rest} = configOrCommitment; + commitment = c; + extra = rest; + } + + const args = this._buildArgs([], commitment, 'base64', extra); + const unsafeRes = await this._rpcRequest('getBlockProduction', args); + const res = create(unsafeRes, BlockProductionResponseStruct); + if ('error' in res) { + throw new Error( + 'failed to get block production information: ' + res.error.message, + ); + } + + return res.result; + } + /** * Fetch a confirmed or finalized transaction from the cluster. */ diff --git a/web3.js/test/connection.test.ts b/web3.js/test/connection.test.ts index 0bb7bfe71..29b851f0a 100644 --- a/web3.js/test/connection.test.ts +++ b/web3.js/test/connection.test.ts @@ -1513,6 +1513,128 @@ describe('Connection', function () { expect(result).to.be.empty; }); + it('get block height', async () => { + const commitment: Commitment = 'confirmed'; + + await mockRpcResponse({ + method: 'getBlockHeight', + params: [{commitment: commitment}], + value: 10, + }); + + const blockHeight = await connection.getBlockHeight(commitment); + expect(blockHeight).to.be.a('number'); + }); + + it('get block production', async () => { + const commitment: Commitment = 'processed'; + + // Find slot of the lowest confirmed block + await mockRpcResponse({ + method: 'getFirstAvailableBlock', + params: [], + value: 1, + }); + let firstSlot = await connection.getFirstAvailableBlock(); + + // Find current block height + await mockRpcResponse({ + method: 'getBlockHeight', + params: [{commitment: commitment}], + value: 10, + }); + let lastSlot = await connection.getBlockHeight(commitment); + + const blockProductionConfig = { + commitment: commitment, + range: { + firstSlot, + lastSlot, + }, + }; + + const blockProductionRet = { + byIdentity: { + '85iYT5RuzRTDgjyRa3cP8SYhM2j21fj7NhfJ3peu1DPr': [12, 10], + }, + range: { + firstSlot, + lastSlot, + }, + }; + + //mock RPC call with config specified + await mockRpcResponse({ + method: 'getBlockProduction', + params: [blockProductionConfig], + value: blockProductionRet, + withContext: true, + }); + + //mock RPC call with commitment only + await mockRpcResponse({ + method: 'getBlockProduction', + params: [{commitment: commitment}], + value: blockProductionRet, + withContext: true, + }); + + const result = await connection.getBlockProduction(blockProductionConfig); + + if (!result) { + expect(result).to.be.ok; + return; + } + + expect(result.context).to.be.ok; + expect(result.value).to.be.ok; + + const resultContextSlot = result.context.slot; + expect(resultContextSlot).to.be.a('number'); + + const resultIdentityDictionary = result.value.byIdentity; + expect(resultIdentityDictionary).to.be.a('object'); + + for (var key in resultIdentityDictionary) { + expect(key).to.be.a('string'); + expect(resultIdentityDictionary[key]).to.be.a('array'); + expect(resultIdentityDictionary[key][0]).to.be.a('number'); + expect(resultIdentityDictionary[key][1]).to.be.a('number'); + } + + const resultSlotRange = result.value.range; + expect(resultSlotRange.firstSlot).to.equal(firstSlot); + expect(resultSlotRange.lastSlot).to.equal(lastSlot); + + const resultCommitmentOnly = await connection.getBlockProduction( + commitment, + ); + + if (!resultCommitmentOnly) { + expect(resultCommitmentOnly).to.be.ok; + return; + } + expect(resultCommitmentOnly.context).to.be.ok; + expect(resultCommitmentOnly.value).to.be.ok; + + const resultCOContextSlot = result.context.slot; + expect(resultCOContextSlot).to.be.a('number'); + + const resultCOIdentityDictionary = result.value.byIdentity; + expect(resultCOIdentityDictionary).to.be.a('object'); + + for (var property in resultCOIdentityDictionary) { + expect(property).to.be.a('string'); + expect(resultCOIdentityDictionary[property]).to.be.a('array'); + expect(resultCOIdentityDictionary[property][0]).to.be.a('number'); + expect(resultCOIdentityDictionary[property][1]).to.be.a('number'); + } + + const resultCOSlotRange = result.value.range; + expect(resultCOSlotRange.firstSlot).to.equal(firstSlot); + expect(resultCOSlotRange.lastSlot).to.equal(lastSlot); + }); + it('get transaction', async () => { await mockRpcResponse({ method: 'getSlot',