From ca19b5116d990562054a5ca8b563eff34e7c52c9 Mon Sep 17 00:00:00 2001 From: Callum McIntyre Date: Thu, 13 Apr 2023 18:24:41 +0000 Subject: [PATCH 1/3] refactor(experimental): add `getBlockProduction` API - Add the `getBlockProduction` API + unit tests - Add jest-extended which provides some extra jest matchers ``` pnpm turbo test:unit:node test:unit:browser ``` --- packages/rpc-core/package.json | 1 + packages/rpc-core/src/index.ts | 4 +- .../__tests__/get-block-production-test.ts | 81 +++++++++++++++++++ .../src/rpc-methods/getBlockProduction.ts | 30 +++++++ packages/rpc-core/tsconfig.json | 3 +- packages/rpc-transport/src/json-rpc-errors.ts | 1 + .../test-config/jest-unit.config.common.ts | 1 + pnpm-lock.yaml | 17 ++++ 8 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 packages/rpc-core/src/rpc-methods/__tests__/get-block-production-test.ts create mode 100644 packages/rpc-core/src/rpc-methods/getBlockProduction.ts diff --git a/packages/rpc-core/package.json b/packages/rpc-core/package.json index c8bac3e2272f..ced4b8548f70 100644 --- a/packages/rpc-core/package.json +++ b/packages/rpc-core/package.json @@ -79,6 +79,7 @@ "eslint-plugin-sort-keys-fix": "^1.1.2", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", + "jest-extended": "^3.2.4", "jest-fetch-mock-fork": "^3.0.4", "jest-runner-eslint": "^2.0.0", "jest-runner-prettier": "^1.0.0", diff --git a/packages/rpc-core/src/index.ts b/packages/rpc-core/src/index.ts index 3b20081d706d..1f0fe409720c 100644 --- a/packages/rpc-core/src/index.ts +++ b/packages/rpc-core/src/index.ts @@ -6,6 +6,7 @@ import { GetBlockHeightApi } from './rpc-methods/getBlockHeight'; import { GetBlocksApi } from './rpc-methods/getBlocks'; import { GetInflationRewardApi } from './rpc-methods/getInflationReward'; import { GetBalanceApi } from './rpc-methods/getBalance'; +import { GetBlockProductionApi } from './rpc-methods/getBlockProduction'; type Config = Readonly<{ onIntegerOverflow?: (methodName: string, keyPath: (number | string)[], value: bigint) => void; @@ -15,7 +16,8 @@ export type SolanaRpcMethods = GetAccountInfoApi & GetBalanceApi & GetBlockHeightApi & GetBlocksApi & - GetInflationRewardApi; + GetInflationRewardApi & + GetBlockProductionApi; export function createSolanaRpcApi(config?: Config): IRpcApi { return new Proxy({} as IRpcApi, { 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..073a856ec115 --- /dev/null +++ b/packages/rpc-core/src/rpc-methods/__tests__/get-block-production-test.ts @@ -0,0 +1,81 @@ +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('getBalance', () => { + 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.toBeObject(), + 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 blockProductionPromise = rpc.getBlockProduction({ commitment }).send(); + await expect(blockProductionPromise).resolves.toSatisfy( + rpcResponse => rpcResponse.context.slot === rpcResponse.value.range.lastSlot + ); + }); + }); + }); + + 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: expect.toBeEmptyObject(), + }), + }); + }); + }); + + 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_SERVER_ERROR_LAST_SLOT_TOO_LARGE'], + 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..8216ff69d046 --- /dev/null +++ b/packages/rpc-core/src/rpc-methods/getBlockProduction.ts @@ -0,0 +1,30 @@ +import { Base58EncodedAddress } from '@solana/keys'; +import { Commitment, RpcResponse, U64UnsafeBeyond2Pow53Minus1 } from './common'; + +type NumberOfLeaderSlots = number; +type NumberOfBlocksProduced = number; + +type Range = Readonly<{ + firstSlot: U64UnsafeBeyond2Pow53Minus1; + lastSlot: U64UnsafeBeyond2Pow53Minus1; +}>; + +type GetBlockProductionApiResponse = RpcResponse<{ + byIdentity: Readonly<{ + [address: string]: [NumberOfLeaderSlots, NumberOfBlocksProduced]; + }>; + range: Range; +}>; + +export interface GetBlockProductionApi { + /** + * Returns recent block production information from the current or previous epoch. + */ + getBlockProduction( + config?: Readonly<{ + commitment?: Commitment; + identity?: Base58EncodedAddress; + range?: Range; + }> + ): GetBlockProductionApiResponse; +} diff --git a/packages/rpc-core/tsconfig.json b/packages/rpc-core/tsconfig.json index 11092e143801..4800ef7e1f38 100644 --- a/packages/rpc-core/tsconfig.json +++ b/packages/rpc-core/tsconfig.json @@ -5,5 +5,6 @@ }, "display": "@solana/rpc-core", "extends": "tsconfig/base.json", - "include": ["src"] + "include": ["src"], + "files": ["node_modules/jest-extended/types/index.d.ts"] } diff --git a/packages/rpc-transport/src/json-rpc-errors.ts b/packages/rpc-transport/src/json-rpc-errors.ts index 119b8d7f5b14..9aff6cf70c23 100644 --- a/packages/rpc-transport/src/json-rpc-errors.ts +++ b/packages/rpc-transport/src/json-rpc-errors.ts @@ -6,6 +6,7 @@ export const SolanaJsonRpcErrorCode = { JSON_RPC_SERVER_ERROR_BLOCK_NOT_AVAILABLE: -32004, JSON_RPC_SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET: -32014, JSON_RPC_SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX: -32010, + JSON_RPC_SERVER_ERROR_LAST_SLOT_TOO_LARGE: -32602, JSON_RPC_SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED: -32009, JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED: -32016, JSON_RPC_SERVER_ERROR_NODE_UNHEALTHY: -32005, diff --git a/packages/test-config/jest-unit.config.common.ts b/packages/test-config/jest-unit.config.common.ts index 65f17295306f..b5e88248cc66 100644 --- a/packages/test-config/jest-unit.config.common.ts +++ b/packages/test-config/jest-unit.config.common.ts @@ -8,6 +8,7 @@ const config: Partial = { path.resolve(__dirname, 'setup-dev-mode.ts'), path.resolve(__dirname, 'setup-define-version-constant.ts'), path.resolve(__dirname, 'setup-fetch-mock.ts'), + 'jest-extended/all' ], transform: { '^.+\\.(ts|js)$': [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa1a4028d15e..9d94717e11d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -543,6 +543,9 @@ importers: jest-environment-jsdom: specifier: ^29.5.0 version: 29.5.0 + jest-extended: + specifier: ^3.2.4 + version: 3.2.4(jest@29.5.0) jest-fetch-mock-fork: specifier: ^3.0.4 version: 3.0.4 @@ -7109,6 +7112,20 @@ packages: jest-mock: 29.5.0 jest-util: 29.5.0 + /jest-extended@3.2.4(jest@29.5.0): + resolution: {integrity: sha512-lSEYhSmvXZG/7YXI7KO3LpiUiQ90gi5giwCJNDMMsX5a+/NZhdbQF2G4ALOBN+KcXVT3H6FPVPohAuMXooaLTQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + jest: '>=27.2.5' + peerDependenciesMeta: + jest: + optional: true + dependencies: + jest: 29.5.0(@types/node@18.15.12)(ts-node@10.9.1) + jest-diff: 29.5.0 + jest-get-type: 29.4.3 + dev: true + /jest-fetch-mock-fork@3.0.4: resolution: {integrity: sha512-1VNwBvy8j1JhSKpgY8fp1fM1CTN8p+0tjSzL00Z88y+s68r1tsOywDqDv0PdylEpMtd1h70281lKHMugvsi4tQ==} dependencies: From 5ccf3735182cb49ae05b382925c1a4828f6ddbf8 Mon Sep 17 00:00:00 2001 From: Callum McIntyre Date: Fri, 21 Apr 2023 13:21:41 +0000 Subject: [PATCH 2/3] fixup! refactor(experimental): add `getBlockProduction` API --- .../__tests__/get-block-production-test.ts | 10 ++--- .../src/rpc-methods/getBlockProduction.ts | 42 +++++++++++++------ packages/rpc-transport/src/json-rpc-errors.ts | 2 +- 3 files changed, 34 insertions(+), 20 deletions(-) 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 index 073a856ec115..533e37a83517 100644 --- 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 @@ -6,7 +6,7 @@ import { createSolanaRpcApi, SolanaRpcMethods } from '../../index'; import { Commitment } from '../common'; import { Base58EncodedAddress } from '@solana/keys'; -describe('getBalance', () => { +describe('getBlockProduction', () => { let rpc: Rpc; beforeEach(() => { fetchMock.resetMocks(); @@ -35,10 +35,8 @@ describe('getBalance', () => { it('has the latest context slot as the last slot', async () => { expect.assertions(1); - const blockProductionPromise = rpc.getBlockProduction({ commitment }).send(); - await expect(blockProductionPromise).resolves.toSatisfy( - rpcResponse => rpcResponse.context.slot === rpcResponse.value.range.lastSlot - ); + const blockProduction = await rpc.getBlockProduction({ commitment }).send(); + expect(blockProduction.value.range.lastSlot).toBe(blockProduction.context.slot); }); }); }); @@ -72,7 +70,7 @@ describe('getBalance', () => { }) .send(); await expect(blockProductionPromise).rejects.toMatchObject({ - code: -32602 satisfies (typeof SolanaJsonRpcErrorCode)['JSON_RPC_SERVER_ERROR_LAST_SLOT_TOO_LARGE'], + 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 index 8216ff69d046..e76b94a326fc 100644 --- a/packages/rpc-core/src/rpc-methods/getBlockProduction.ts +++ b/packages/rpc-core/src/rpc-methods/getBlockProduction.ts @@ -1,30 +1,46 @@ import { Base58EncodedAddress } from '@solana/keys'; import { Commitment, RpcResponse, U64UnsafeBeyond2Pow53Minus1 } from './common'; -type NumberOfLeaderSlots = number; -type NumberOfBlocksProduced = number; +type NumberOfLeaderSlots = U64UnsafeBeyond2Pow53Minus1; +type NumberOfBlocksProduced = U64UnsafeBeyond2Pow53Minus1; -type Range = Readonly<{ +type SlotRange = Readonly<{ firstSlot: U64UnsafeBeyond2Pow53Minus1; lastSlot: U64UnsafeBeyond2Pow53Minus1; }>; -type GetBlockProductionApiResponse = RpcResponse<{ - byIdentity: Readonly<{ - [address: string]: [NumberOfLeaderSlots, NumberOfBlocksProduced]; +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] }>; }>; - range: Range; }>; export interface GetBlockProductionApi { /** * Returns recent block production information from the current or previous epoch. */ + getBlockProduction( + config: GetBlockProductionApiConfigBase & + Readonly<{ + identity: TIdentity; + }> + ): GetBlockProductionApiResponseBase & GetBlockProductionApiResponseWithSingleIdentity; getBlockProduction( - config?: Readonly<{ - commitment?: Commitment; - identity?: Base58EncodedAddress; - range?: Range; - }> - ): GetBlockProductionApiResponse; + 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 9aff6cf70c23..1d5c57df9a71 100644 --- a/packages/rpc-transport/src/json-rpc-errors.ts +++ b/packages/rpc-transport/src/json-rpc-errors.ts @@ -1,12 +1,12 @@ // 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, JSON_RPC_SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET: -32014, JSON_RPC_SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX: -32010, - JSON_RPC_SERVER_ERROR_LAST_SLOT_TOO_LARGE: -32602, JSON_RPC_SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED: -32009, JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED: -32016, JSON_RPC_SERVER_ERROR_NODE_UNHEALTHY: -32005, From 1fef36937a68b8d2338c4d6fcde6a8e8157dc556 Mon Sep 17 00:00:00 2001 From: steveluscher Date: Tue, 25 Apr 2023 20:31:24 +0000 Subject: [PATCH 3/3] nit: do without jest-extended for now, formatting nits --- packages/rpc-core/package.json | 1 - packages/rpc-core/src/index.ts | 8 ++++---- .../__tests__/get-block-production-test.ts | 4 ++-- packages/rpc-core/tsconfig.json | 3 +-- packages/test-config/jest-unit.config.common.ts | 1 - pnpm-lock.yaml | 17 ----------------- 6 files changed, 7 insertions(+), 27 deletions(-) diff --git a/packages/rpc-core/package.json b/packages/rpc-core/package.json index ced4b8548f70..c8bac3e2272f 100644 --- a/packages/rpc-core/package.json +++ b/packages/rpc-core/package.json @@ -79,7 +79,6 @@ "eslint-plugin-sort-keys-fix": "^1.1.2", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", - "jest-extended": "^3.2.4", "jest-fetch-mock-fork": "^3.0.4", "jest-runner-eslint": "^2.0.0", "jest-runner-prettier": "^1.0.0", diff --git a/packages/rpc-core/src/index.ts b/packages/rpc-core/src/index.ts index 1f0fe409720c..d04cde6de460 100644 --- a/packages/rpc-core/src/index.ts +++ b/packages/rpc-core/src/index.ts @@ -2,11 +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'; -import { GetBlockProductionApi } from './rpc-methods/getBlockProduction'; type Config = Readonly<{ onIntegerOverflow?: (methodName: string, keyPath: (number | string)[], value: bigint) => void; @@ -15,9 +15,9 @@ type Config = Readonly<{ export type SolanaRpcMethods = GetAccountInfoApi & GetBalanceApi & GetBlockHeightApi & + GetBlockProductionApi & GetBlocksApi & - GetInflationRewardApi & - GetBlockProductionApi; + GetInflationRewardApi; export function createSolanaRpcApi(config?: Config): IRpcApi { return new Proxy({} as IRpcApi, { 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 index 533e37a83517..b28bd1723334 100644 --- 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 @@ -24,7 +24,7 @@ describe('getBlockProduction', () => { const blockProductionPromise = rpc.getBlockProduction({ commitment }).send(); await expect(blockProductionPromise).resolves.toMatchObject({ value: expect.objectContaining({ - byIdentity: expect.toBeObject(), + byIdentity: expect.any(Object), range: expect.objectContaining({ firstSlot: expect.any(BigInt), lastSlot: expect.any(BigInt), @@ -52,7 +52,7 @@ describe('getBlockProduction', () => { const blockProductionPromise = rpc.getBlockProduction({ identity }).send(); await expect(blockProductionPromise).resolves.toMatchObject({ value: expect.objectContaining({ - byIdentity: expect.toBeEmptyObject(), + byIdentity: {}, }), }); }); diff --git a/packages/rpc-core/tsconfig.json b/packages/rpc-core/tsconfig.json index 4800ef7e1f38..11092e143801 100644 --- a/packages/rpc-core/tsconfig.json +++ b/packages/rpc-core/tsconfig.json @@ -5,6 +5,5 @@ }, "display": "@solana/rpc-core", "extends": "tsconfig/base.json", - "include": ["src"], - "files": ["node_modules/jest-extended/types/index.d.ts"] + "include": ["src"] } diff --git a/packages/test-config/jest-unit.config.common.ts b/packages/test-config/jest-unit.config.common.ts index b5e88248cc66..65f17295306f 100644 --- a/packages/test-config/jest-unit.config.common.ts +++ b/packages/test-config/jest-unit.config.common.ts @@ -8,7 +8,6 @@ const config: Partial = { path.resolve(__dirname, 'setup-dev-mode.ts'), path.resolve(__dirname, 'setup-define-version-constant.ts'), path.resolve(__dirname, 'setup-fetch-mock.ts'), - 'jest-extended/all' ], transform: { '^.+\\.(ts|js)$': [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d94717e11d1..fa1a4028d15e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -543,9 +543,6 @@ importers: jest-environment-jsdom: specifier: ^29.5.0 version: 29.5.0 - jest-extended: - specifier: ^3.2.4 - version: 3.2.4(jest@29.5.0) jest-fetch-mock-fork: specifier: ^3.0.4 version: 3.0.4 @@ -7112,20 +7109,6 @@ packages: jest-mock: 29.5.0 jest-util: 29.5.0 - /jest-extended@3.2.4(jest@29.5.0): - resolution: {integrity: sha512-lSEYhSmvXZG/7YXI7KO3LpiUiQ90gi5giwCJNDMMsX5a+/NZhdbQF2G4ALOBN+KcXVT3H6FPVPohAuMXooaLTQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - jest: '>=27.2.5' - peerDependenciesMeta: - jest: - optional: true - dependencies: - jest: 29.5.0(@types/node@18.15.12)(ts-node@10.9.1) - jest-diff: 29.5.0 - jest-get-type: 29.4.3 - dev: true - /jest-fetch-mock-fork@3.0.4: resolution: {integrity: sha512-1VNwBvy8j1JhSKpgY8fp1fM1CTN8p+0tjSzL00Z88y+s68r1tsOywDqDv0PdylEpMtd1h70281lKHMugvsi4tQ==} dependencies: