diff --git a/packages/rpc-core/src/index.ts b/packages/rpc-core/src/index.ts index 3b20081d706d..d04cde6de460 100644 --- a/packages/rpc-core/src/index.ts +++ b/packages/rpc-core/src/index.ts @@ -2,10 +2,11 @@ import { IRpcApi, RpcRequest } from '@solana/rpc-transport/dist/types/json-rpc-t import { patchParamsForSolanaLabsRpc } from './params-patcher'; import { patchResponseForSolanaLabsRpc } from './response-patcher'; import { GetAccountInfoApi } from './rpc-methods/getAccountInfo'; +import { GetBalanceApi } from './rpc-methods/getBalance'; import { GetBlockHeightApi } from './rpc-methods/getBlockHeight'; +import { GetBlockProductionApi } from './rpc-methods/getBlockProduction'; import { GetBlocksApi } from './rpc-methods/getBlocks'; import { GetInflationRewardApi } from './rpc-methods/getInflationReward'; -import { GetBalanceApi } from './rpc-methods/getBalance'; type Config = Readonly<{ onIntegerOverflow?: (methodName: string, keyPath: (number | string)[], value: bigint) => void; @@ -14,6 +15,7 @@ type Config = Readonly<{ export type SolanaRpcMethods = GetAccountInfoApi & GetBalanceApi & GetBlockHeightApi & + GetBlockProductionApi & GetBlocksApi & GetInflationRewardApi; diff --git a/packages/rpc-core/src/rpc-methods/__tests__/get-block-production-test.ts b/packages/rpc-core/src/rpc-methods/__tests__/get-block-production-test.ts new file mode 100644 index 000000000000..b28bd1723334 --- /dev/null +++ b/packages/rpc-core/src/rpc-methods/__tests__/get-block-production-test.ts @@ -0,0 +1,79 @@ +import { createHttpTransport, createJsonRpc } from '@solana/rpc-transport'; +import type { SolanaJsonRpcErrorCode } from '@solana/rpc-transport/dist/types/json-rpc-errors'; +import type { Rpc } from '@solana/rpc-transport/dist/types/json-rpc-types'; +import fetchMock from 'jest-fetch-mock-fork'; +import { createSolanaRpcApi, SolanaRpcMethods } from '../../index'; +import { Commitment } from '../common'; +import { Base58EncodedAddress } from '@solana/keys'; + +describe('getBlockProduction', () => { + let rpc: Rpc; + beforeEach(() => { + fetchMock.resetMocks(); + fetchMock.dontMock(); + rpc = createJsonRpc({ + api: createSolanaRpcApi(), + transport: createHttpTransport({ url: 'http://127.0.0.1:8899' }), + }); + }); + + (['confirmed', 'finalized', 'processed'] as Commitment[]).forEach(commitment => { + describe(`when called with \`${commitment}\` commitment`, () => { + it('returns block production data', async () => { + expect.assertions(1); + const blockProductionPromise = rpc.getBlockProduction({ commitment }).send(); + await expect(blockProductionPromise).resolves.toMatchObject({ + value: expect.objectContaining({ + byIdentity: expect.any(Object), + range: expect.objectContaining({ + firstSlot: expect.any(BigInt), + lastSlot: expect.any(BigInt), + }), + }), + }); + }); + + it('has the latest context slot as the last slot', async () => { + expect.assertions(1); + const blockProduction = await rpc.getBlockProduction({ commitment }).send(); + expect(blockProduction.value.range.lastSlot).toBe(blockProduction.context.slot); + }); + }); + }); + + describe('when called with a single identity', () => { + // Currently this call always returns just one identity in tests, so no way to meaningfully test this + it.todo('returns data for just that identity'); + + it('returns an empty byIdentity if the identity is not a block producer', async () => { + expect.assertions(1); + // Randomly generated address, assumed not to be a block producer + const identity = '9NmqDDZa7mH1DBM4zeq9cm7VcRn2un1i2TwuMvjBoVhU' as Base58EncodedAddress; + const blockProductionPromise = rpc.getBlockProduction({ identity }).send(); + await expect(blockProductionPromise).resolves.toMatchObject({ + value: expect.objectContaining({ + byIdentity: {}, + }), + }); + }); + }); + + describe('when called with a `lastSlot` higher than the highest slot available', () => { + it('throws an error', async () => { + expect.assertions(1); + const blockProductionPromise = rpc + .getBlockProduction({ + range: { + firstSlot: 0n, + lastSlot: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high. + }, + }) + .send(); + await expect(blockProductionPromise).rejects.toMatchObject({ + code: -32602 satisfies (typeof SolanaJsonRpcErrorCode)['JSON_RPC_INVALID_PARAMS'], + message: expect.any(String), + name: 'SolanaJsonRpcError', + }); + }); + }); +}); diff --git a/packages/rpc-core/src/rpc-methods/getBlockProduction.ts b/packages/rpc-core/src/rpc-methods/getBlockProduction.ts new file mode 100644 index 000000000000..e76b94a326fc --- /dev/null +++ b/packages/rpc-core/src/rpc-methods/getBlockProduction.ts @@ -0,0 +1,46 @@ +import { Base58EncodedAddress } from '@solana/keys'; +import { Commitment, RpcResponse, U64UnsafeBeyond2Pow53Minus1 } from './common'; + +type NumberOfLeaderSlots = U64UnsafeBeyond2Pow53Minus1; +type NumberOfBlocksProduced = U64UnsafeBeyond2Pow53Minus1; + +type SlotRange = Readonly<{ + firstSlot: U64UnsafeBeyond2Pow53Minus1; + lastSlot: U64UnsafeBeyond2Pow53Minus1; +}>; + +type GetBlockProductionApiConfigBase = Readonly<{ + commitment?: Commitment; + range?: SlotRange; +}>; + +type GetBlockProductionApiResponseBase = RpcResponse<{ + range: SlotRange; +}>; + +type GetBlockProductionApiResponseWithAllIdentities = Readonly<{ + value: Readonly<{ + byIdentity: Record; + }>; +}>; + +type GetBlockProductionApiResponseWithSingleIdentity = Readonly<{ + value: Readonly<{ + byIdentity: Readonly<{ [TAddress in TIdentity]?: [NumberOfLeaderSlots, NumberOfBlocksProduced] }>; + }>; +}>; + +export interface GetBlockProductionApi { + /** + * Returns recent block production information from the current or previous epoch. + */ + getBlockProduction( + config: GetBlockProductionApiConfigBase & + Readonly<{ + identity: TIdentity; + }> + ): GetBlockProductionApiResponseBase & GetBlockProductionApiResponseWithSingleIdentity; + getBlockProduction( + config?: GetBlockProductionApiConfigBase + ): GetBlockProductionApiResponseBase & GetBlockProductionApiResponseWithAllIdentities; +} diff --git a/packages/rpc-transport/src/json-rpc-errors.ts b/packages/rpc-transport/src/json-rpc-errors.ts index 119b8d7f5b14..1d5c57df9a71 100644 --- a/packages/rpc-transport/src/json-rpc-errors.ts +++ b/packages/rpc-transport/src/json-rpc-errors.ts @@ -1,6 +1,7 @@ // Keep in sync with https://github.com/solana-labs/solana/blob/master/rpc-client-api/src/custom_error.rs // Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/ export const SolanaJsonRpcErrorCode = { + JSON_RPC_INVALID_PARAMS: -32602, JSON_RPC_SCAN_ERROR: -32012, JSON_RPC_SERVER_ERROR_BLOCK_CLEANED_UP: -32001, JSON_RPC_SERVER_ERROR_BLOCK_NOT_AVAILABLE: -32004,