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

Commit

Permalink
refactor(experimental): add getBlockProduction API
Browse files Browse the repository at this point in the history
## 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
```
  • Loading branch information
mcintyre94 committed Apr 18, 2023
1 parent 3290268 commit ab6148f
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/rpc-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion packages/rpc-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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;
Expand All @@ -18,7 +19,8 @@ export type SolanaRpcMethods = GetAccountInfoApi &
GetBalanceApi &
GetBlockHeightApi &
GetBlocksApi &
GetInflationRewardApi;
GetInflationRewardApi &
GetBlockProductionApi;

export function createSolanaRpcApi(config?: Config): IRpcApi<SolanaRpcMethods> {
return new Proxy({} as IRpcApi<SolanaRpcMethods>, {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { createJsonRpcTransport } from '@solana/rpc-transport';
import type { SolanaJsonRpcErrorCode } from '@solana/rpc-transport/dist/types/json-rpc-transport/json-rpc-errors';
import type { Transport } from '@solana/rpc-transport/dist/types/json-rpc-transport/json-rpc-transport-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 transport: Transport<SolanaRpcMethods>;
beforeEach(() => {
fetchMock.resetMocks();
fetchMock.dontMock();
transport = createJsonRpcTransport({
api: createSolanaRpcApi(),
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 = transport.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 = transport.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 = transport.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 = transport
.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',
});
});
});
});
30 changes: 30 additions & 0 deletions packages/rpc-core/src/rpc-methods/getBlockProduction.ts
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 2 additions & 1 deletion packages/rpc-core/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/test-config/jest-unit.config.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import path from 'path';
const config: Partial<Config.InitialProjectOptions> = {
restoreMocks: true,
roots: ['<rootDir>/src/'],
setupFilesAfterEnv: [path.resolve(__dirname, 'setup-dev-mode.ts'), path.resolve(__dirname, 'setup-fetch-mock.ts')],
setupFilesAfterEnv: [path.resolve(__dirname, 'setup-dev-mode.ts'), path.resolve(__dirname, 'setup-fetch-mock.ts'), 'jest-extended/all'],
transform: {
'^.+\\.(ts|js)$': [
'@swc/jest',
Expand Down
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit ab6148f

Please sign in to comment.