Skip to content
This repository was archived by the owner on Jan 22, 2025. It is now read-only.

Commit 18f642a

Browse files
committed
refactor(experimental): add getBlockProduction API
## Summary - Add the `getBlockProduction` API + unit tests - Add jest-extended which provides some extra jest matchers ## Test Plan ``` pnpm turbo test:unit:node test:unit:browser ```
1 parent b69daf2 commit 18f642a

File tree

8 files changed

+136
-2
lines changed

8 files changed

+136
-2
lines changed

packages/rpc-core/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"eslint-plugin-sort-keys-fix": "^1.1.2",
8080
"jest": "^29.5.0",
8181
"jest-environment-jsdom": "^29.5.0",
82+
"jest-extended": "^3.2.4",
8283
"jest-fetch-mock-fork": "^3.0.4",
8384
"jest-runner-eslint": "^2.0.0",
8485
"jest-runner-prettier": "^1.0.0",

packages/rpc-core/src/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { GetBlockHeightApi } from './rpc-methods/getBlockHeight';
66
import { GetBlocksApi } from './rpc-methods/getBlocks';
77
import { GetInflationRewardApi } from './rpc-methods/getInflationReward';
88
import { GetBalanceApi } from './rpc-methods/getBalance';
9+
import { GetBlockProductionApi } from './rpc-methods/getBlockProduction';
910

1011
type Config = Readonly<{
1112
onIntegerOverflow?: (methodName: string, keyPath: (number | string)[], value: bigint) => void;
@@ -15,7 +16,8 @@ export type SolanaRpcMethods = GetAccountInfoApi &
1516
GetBalanceApi &
1617
GetBlockHeightApi &
1718
GetBlocksApi &
18-
GetInflationRewardApi;
19+
GetInflationRewardApi &
20+
GetBlockProductionApi;
1921

2022
export function createSolanaRpcApi(config?: Config): IRpcApi<SolanaRpcMethods> {
2123
return new Proxy({} as IRpcApi<SolanaRpcMethods>, {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { createJsonRpcTransport } from '@solana/rpc-transport';
2+
import type { SolanaJsonRpcErrorCode } from '@solana/rpc-transport/dist/types/json-rpc-transport/json-rpc-errors';
3+
import type { Transport } from '@solana/rpc-transport/dist/types/json-rpc-transport/json-rpc-transport-types';
4+
import fetchMock from 'jest-fetch-mock-fork';
5+
import { createSolanaRpcApi, SolanaRpcMethods } from '../../index';
6+
import { Commitment } from '../common';
7+
import { Base58EncodedAddress } from '@solana/keys';
8+
9+
describe('getBalance', () => {
10+
let transport: Transport<SolanaRpcMethods>;
11+
beforeEach(() => {
12+
fetchMock.resetMocks();
13+
fetchMock.dontMock();
14+
transport = createJsonRpcTransport({
15+
api: createSolanaRpcApi(),
16+
url: 'http://127.0.0.1:8899',
17+
});
18+
});
19+
20+
(['confirmed', 'finalized', 'processed'] as Commitment[]).forEach(commitment => {
21+
describe(`when called with \`${commitment}\` commitment`, () => {
22+
it('returns block production data', async () => {
23+
expect.assertions(1);
24+
const blockProductionPromise = transport.getBlockProduction({ commitment }).send();
25+
await expect(blockProductionPromise).resolves.toMatchObject({
26+
value: expect.objectContaining({
27+
byIdentity: expect.toBeObject(),
28+
range: expect.objectContaining({
29+
firstSlot: expect.any(BigInt),
30+
lastSlot: expect.any(BigInt),
31+
}),
32+
}),
33+
});
34+
});
35+
36+
it('has the latest context slot as the last slot', async () => {
37+
expect.assertions(1);
38+
const blockProductionPromise = transport.getBlockProduction({ commitment }).send();
39+
await expect(blockProductionPromise).resolves.toSatisfy(
40+
rpcResponse => rpcResponse.context.slot === rpcResponse.value.range.lastSlot
41+
);
42+
});
43+
});
44+
});
45+
46+
describe('when called with a single identity', () => {
47+
// Currently this call always returns just one identity in tests, so no way to meaningfully test this
48+
it.todo('returns data for just that identity');
49+
50+
it('returns an empty byIdentity if the identity is not a block producer', async () => {
51+
expect.assertions(1);
52+
// Randomly generated address, assumed not to be a block producer
53+
const identity = '9NmqDDZa7mH1DBM4zeq9cm7VcRn2un1i2TwuMvjBoVhU' as Base58EncodedAddress;
54+
const blockProductionPromise = transport.getBlockProduction({ identity }).send();
55+
await expect(blockProductionPromise).resolves.toMatchObject({
56+
value: expect.objectContaining({
57+
byIdentity: expect.toBeEmptyObject(),
58+
}),
59+
});
60+
});
61+
});
62+
63+
describe('when called with a `lastSlot` higher than the highest slot available', () => {
64+
it('throws an error', async () => {
65+
expect.assertions(1);
66+
const blockProductionPromise = transport
67+
.getBlockProduction({
68+
range: {
69+
firstSlot: 0n,
70+
lastSlot: 2n ** 63n - 1n, // u64:MAX; safe bet it'll be too high.
71+
},
72+
})
73+
.send();
74+
await expect(blockProductionPromise).rejects.toMatchObject({
75+
code: -32602 satisfies (typeof SolanaJsonRpcErrorCode)['JSON_RPC_SERVER_ERROR_LAST_SLOT_TOO_LARGE'],
76+
message: expect.any(String),
77+
name: 'SolanaJsonRpcError',
78+
});
79+
});
80+
});
81+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Base58EncodedAddress } from '@solana/keys';
2+
import { Commitment, RpcResponse, U64UnsafeBeyond2Pow53Minus1 } from './common';
3+
4+
type NumberOfLeaderSlots = number;
5+
type NumberOfBlocksProduced = number;
6+
7+
type Range = Readonly<{
8+
firstSlot: U64UnsafeBeyond2Pow53Minus1;
9+
lastSlot: U64UnsafeBeyond2Pow53Minus1;
10+
}>;
11+
12+
type GetBlockProductionApiResponse = RpcResponse<{
13+
byIdentity: Readonly<{
14+
[address: string]: [NumberOfLeaderSlots, NumberOfBlocksProduced];
15+
}>;
16+
range: Range;
17+
}>;
18+
19+
export interface GetBlockProductionApi {
20+
/**
21+
* Returns recent block production information from the current or previous epoch.
22+
*/
23+
getBlockProduction(
24+
config?: Readonly<{
25+
commitment?: Commitment;
26+
identity?: Base58EncodedAddress;
27+
range?: Range;
28+
}>
29+
): GetBlockProductionApiResponse;
30+
}

packages/rpc-core/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
},
66
"display": "@solana/rpc-core",
77
"extends": "tsconfig/base.json",
8-
"include": ["src"]
8+
"include": ["src"],
9+
"files": ["node_modules/jest-extended/types/index.d.ts"]
910
}

packages/rpc-transport/src/json-rpc-errors.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const SolanaJsonRpcErrorCode = {
66
JSON_RPC_SERVER_ERROR_BLOCK_NOT_AVAILABLE: -32004,
77
JSON_RPC_SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET: -32014,
88
JSON_RPC_SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX: -32010,
9+
JSON_RPC_SERVER_ERROR_LAST_SLOT_TOO_LARGE: -32602,
910
JSON_RPC_SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED: -32009,
1011
JSON_RPC_SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED: -32016,
1112
JSON_RPC_SERVER_ERROR_NODE_UNHEALTHY: -32005,

packages/test-config/jest-unit.config.common.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const config: Partial<Config.InitialProjectOptions> = {
88
path.resolve(__dirname, 'setup-dev-mode.ts'),
99
path.resolve(__dirname, 'setup-define-version-constant.ts'),
1010
path.resolve(__dirname, 'setup-fetch-mock.ts'),
11+
'jest-extended/all'
1112
],
1213
transform: {
1314
'^.+\\.(ts|js)$': [

pnpm-lock.yaml

+17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)