Skip to content

Commit

Permalink
refactor(experimental): add getBlockProduction API (#1263)
Browse files Browse the repository at this point in the history
- Add the `getBlockProduction` API + unit tests
- Add jest-extended which provides some extra jest matchers

```
pnpm turbo test:unit:node test:unit:browser
```

* fixup! refactor(experimental): add `getBlockProduction` API

* nit: do without jest-extended for now, formatting nits

---------

Co-authored-by: Callum McIntyre <[email protected]>
Co-authored-by: steveluscher <[email protected]>
  • Loading branch information
3 people authored Apr 25, 2023
1 parent 3244e3e commit edfcbdc
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 1 deletion.
4 changes: 3 additions & 1 deletion packages/rpc-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,6 +15,7 @@ type Config = Readonly<{
export type SolanaRpcMethods = GetAccountInfoApi &
GetBalanceApi &
GetBlockHeightApi &
GetBlockProductionApi &
GetBlocksApi &
GetInflationRewardApi;

Expand Down
Original file line number Diff line number Diff line change
@@ -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<SolanaRpcMethods>;
beforeEach(() => {
fetchMock.resetMocks();
fetchMock.dontMock();
rpc = createJsonRpc<SolanaRpcMethods>({
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',
});
});
});
});
46 changes: 46 additions & 0 deletions packages/rpc-core/src/rpc-methods/getBlockProduction.ts
Original file line number Diff line number Diff line change
@@ -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<Base58EncodedAddress, [NumberOfLeaderSlots, NumberOfBlocksProduced]>;
}>;
}>;

type GetBlockProductionApiResponseWithSingleIdentity<TIdentity extends string> = 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<TIdentity extends Base58EncodedAddress>(
config: GetBlockProductionApiConfigBase &
Readonly<{
identity: TIdentity;
}>
): GetBlockProductionApiResponseBase & GetBlockProductionApiResponseWithSingleIdentity<TIdentity>;
getBlockProduction(
config?: GetBlockProductionApiConfigBase
): GetBlockProductionApiResponseBase & GetBlockProductionApiResponseWithAllIdentities;
}
1 change: 1 addition & 0 deletions packages/rpc-transport/src/json-rpc-errors.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down

0 comments on commit edfcbdc

Please sign in to comment.