diff --git a/.changeset/giant-coins-complain.md b/.changeset/giant-coins-complain.md new file mode 100644 index 00000000000..08edcee4e44 --- /dev/null +++ b/.changeset/giant-coins-complain.md @@ -0,0 +1,7 @@ +--- +'@solana/rpc': patch +'@solana/rpc-subscriptions': patch +'@solana/rpc-transformers': patch +--- + +Change first argument of `onIntegerOverflow` handler from `methodName: string` to `request: RpcRequest` diff --git a/.changeset/popular-buttons-repair.md b/.changeset/popular-buttons-repair.md new file mode 100644 index 00000000000..6089841bb49 --- /dev/null +++ b/.changeset/popular-buttons-repair.md @@ -0,0 +1,6 @@ +--- +'@solana/rpc-transformers': patch +'@solana/rpc-api': patch +--- + +Add `getIntegerOverflowRequestTransformer`, `getBigIntDowncastRequestTransformer` and `getTreeWalkerRequestTransformer` helpers diff --git a/packages/rpc-api/README.md b/packages/rpc-api/README.md index f73f55c1e1d..c8d160193a1 100644 --- a/packages/rpc-api/README.md +++ b/packages/rpc-api/README.md @@ -56,4 +56,4 @@ The default behaviours include: A config object with the following properties: - `defaultCommitment`: An optional default `Commitment` value. Given an RPC method that takes `commitment` as a parameter, this value will be used when the caller does not supply one. -- `onIntegerOverflow(methodName, keyPath, value): void`: An optional function that will be called whenever a `bigint` input exceeds that which can be expressed using JavaScript numbers. This is used in the default `SolanaRpcApi` to throw an exception rather than to allow truncated values to propagate through a program. +- `onIntegerOverflow(request, keyPath, value): void`: An optional function that will be called whenever a `bigint` input exceeds that which can be expressed using JavaScript numbers. This is used in the default `SolanaRpcApi` to throw an exception rather than to allow truncated values to propagate through a program. diff --git a/packages/rpc-subscriptions/src/rpc-default-config.ts b/packages/rpc-subscriptions/src/rpc-default-config.ts index 490889e8e68..7cf5759eb51 100644 --- a/packages/rpc-subscriptions/src/rpc-default-config.ts +++ b/packages/rpc-subscriptions/src/rpc-default-config.ts @@ -6,7 +6,7 @@ export const DEFAULT_RPC_SUBSCRIPTIONS_CONFIG: Partial< NonNullable[0]> > = { defaultCommitment: 'confirmed', - onIntegerOverflow(methodName, keyPath, value) { - throw createSolanaJsonRpcIntegerOverflowError(methodName, keyPath, value); + onIntegerOverflow(request, keyPath, value) { + throw createSolanaJsonRpcIntegerOverflowError(request.methodName, keyPath, value); }, }; diff --git a/packages/rpc-transformers/README.md b/packages/rpc-transformers/README.md index a44dc7b9078..3830627a87e 100644 --- a/packages/rpc-transformers/README.md +++ b/packages/rpc-transformers/README.md @@ -14,7 +14,22 @@ # @solana/rpc-transformers -## Functions +## Request Transformers + +### `getDefaultRequestTransformerForSolanaRpc(config)` + +Returns the default request transformer for the Solana RPC API. Under the hood, this function composes multiple `RpcRequestTransformer` together such as the `getDefaultCommitmentTransformer`, the `getIntegerOverflowRequestTransformer` and the `getBigIntDowncastRequestTransformer`. + +```ts +import { getDefaultRequestTransformerForSolanaRpc } from '@solana/rpc-transformers'; + +const requestTransformer = getDefaultRequestTransformerForSolanaRpc({ + defaultCommitment: 'confirmed', + onIntegerOverflow: (request, keyPath, value) => { + throw new Error(`Integer overflow at ${keyPath.join('.')}: ${value}`); + }, +}); +``` ### `getDefaultCommitmentTransformer(config)` @@ -28,3 +43,43 @@ const requestTransformer = getDefaultCommitmentTransformer({ optionsObjectPositionByMethod: OPTIONS_OBJECT_POSITION_BY_METHOD, }); ``` + +### `getIntegerOverflowRequestTransformer(handler)` + +Creates a transformer that traverses the request parameters and executes the provided handler when an integer overflow is detected. + +```ts +import { getIntegerOverflowRequestTransformer } from '@solana/rpc-transformers'; + +const requestTransformer = getIntegerOverflowRequestTransformer((request, keyPath, value) => { + throw new Error(`Integer overflow at ${keyPath.join('.')}: ${value}`); +}); +``` + +### `getBigIntDowncastRequestTransformer()` + +Creates a transformer that downcasts all `BigInt` values to `Number`. + +```ts +import { getBigIntDowncastRequestTransformer } from '@solana/rpc-transformers'; + +const requestTransformer = getBigIntDowncastRequestTransformer(); +``` + +### `getTreeWalkerRequestTransformer(visitors, initialState)` + +Creates a transformer that traverses the request parameters and executes the provided visitors at each node. A custom initial state can be provided but must at least provide `{ keyPath: [] }`. + +```ts +import { getTreeWalkerRequestTransformer } from '@solana/rpc-transformers'; + +const requestTransformer = getTreeWalkerRequestTransformer( + [ + // Replaces foo.bar with "baz". + (node, state) => (state.keyPath === ['foo', 'bar'] ? 'baz' : node), + // Increments all numbers by 1. + node => (typeof node === number ? node + 1 : node), + ], + { keyPath: [] }, +); +``` diff --git a/packages/rpc-transformers/src/__tests__/params-transformer-bigint-downcast-test.ts b/packages/rpc-transformers/src/__tests__/request-transformer-bigint-downcast-test.ts similarity index 90% rename from packages/rpc-transformers/src/__tests__/params-transformer-bigint-downcast-test.ts rename to packages/rpc-transformers/src/__tests__/request-transformer-bigint-downcast-test.ts index 5a07984545b..b36c3c73ef2 100644 --- a/packages/rpc-transformers/src/__tests__/params-transformer-bigint-downcast-test.ts +++ b/packages/rpc-transformers/src/__tests__/request-transformer-bigint-downcast-test.ts @@ -1,4 +1,4 @@ -import { downcastNodeToNumberIfBigint } from '../params-transformer-bigint-downcast'; +import { downcastNodeToNumberIfBigint } from '../request-transformer-bigint-downcast'; describe('bigint downcast visitor', () => { it.each([10, '10', null, undefined, Symbol()])('returns the value `%p` as-is', value => { diff --git a/packages/rpc-transformers/src/__tests__/params-transformer-integer-overflow-test.ts b/packages/rpc-transformers/src/__tests__/request-transformer-integer-overflow-test.ts similarity index 93% rename from packages/rpc-transformers/src/__tests__/params-transformer-integer-overflow-test.ts rename to packages/rpc-transformers/src/__tests__/request-transformer-integer-overflow-test.ts index de8d905f06c..791caa1c901 100644 --- a/packages/rpc-transformers/src/__tests__/params-transformer-integer-overflow-test.ts +++ b/packages/rpc-transformers/src/__tests__/request-transformer-integer-overflow-test.ts @@ -1,4 +1,4 @@ -import { getIntegerOverflowNodeVisitor } from '../params-transformer-integer-overflow'; +import { getIntegerOverflowNodeVisitor } from '../request-transformer-integer-overflow'; import { TraversalState } from '../tree-traversal'; const MOCK_TRAVERSAL_STATE = { diff --git a/packages/rpc-transformers/src/__tests__/request-transformer-test.ts b/packages/rpc-transformers/src/__tests__/request-transformer-test.ts index 47bd7fa51c8..35e18cf5b46 100644 --- a/packages/rpc-transformers/src/__tests__/request-transformer-test.ts +++ b/packages/rpc-transformers/src/__tests__/request-transformer-test.ts @@ -6,15 +6,17 @@ import { OPTIONS_OBJECT_POSITION_BY_METHOD } from '../request-transformer-option describe('getDefaultRequestTransformerForSolanaRpc', () => { describe('given no config', () => { - let paramsTransformer: (params: unknown) => unknown; + let createRequest: (params: unknown) => { methodName: 'getFoo'; params: unknown }; + let requestTransformer: RpcRequestTransformer; beforeEach(() => { - const requestTransformer = getDefaultRequestTransformerForSolanaRpc(); - paramsTransformer = params => requestTransformer({ methodName: 'getFoo', params }).params; + createRequest = params => ({ methodName: 'getFoo', params }); + requestTransformer = getDefaultRequestTransformerForSolanaRpc(); }); describe('given an array as input', () => { const input = [10n, 10, '10', ['10', [10, 10n], 10n]] as const; it('casts the bigints in the array to a `number`, recursively', () => { - expect(paramsTransformer(input)).toStrictEqual([ + const request = createRequest(input); + expect(requestTransformer(request).params).toStrictEqual([ Number(input[0]), input[1], input[2], @@ -25,7 +27,8 @@ describe('getDefaultRequestTransformerForSolanaRpc', () => { describe('given an object as input', () => { const input = { a: 10n, b: 10, c: { c1: '10', c2: 10n } } as const; it('casts the bigints in the array to a `number`, recursively', () => { - expect(paramsTransformer(input)).toStrictEqual({ + const request = createRequest(input); + expect(requestTransformer(request).params).toStrictEqual({ a: Number(input.a), b: input.b, c: { c1: input.c.c1, c2: Number(input.c.c2) }, @@ -241,42 +244,47 @@ describe('getDefaultRequestTransformerForSolanaRpc', () => { ); describe('given an integer overflow handler', () => { let onIntegerOverflow: jest.Mock; - let paramsTransformer: (value: unknown) => unknown; + let requestTransformer: RpcRequestTransformer; + let createRequest = (params: unknown) => ({ methodName: 'getFoo', params }); beforeEach(() => { onIntegerOverflow = jest.fn(); - const requestTransformer = getDefaultRequestTransformerForSolanaRpc({ onIntegerOverflow }); - paramsTransformer = params => requestTransformer({ methodName: 'getFoo', params }).params; + requestTransformer = getDefaultRequestTransformerForSolanaRpc({ onIntegerOverflow }); + createRequest = params => ({ methodName: 'getFoo', params }); }); Object.entries({ 'value above `Number.MAX_SAFE_INTEGER`': BigInt(Number.MAX_SAFE_INTEGER) + 1n, 'value below `Number.MAX_SAFE_INTEGER`': -BigInt(Number.MAX_SAFE_INTEGER) - 1n, }).forEach(([description, value]) => { it('calls `onIntegerOverflow` when passed a value ' + description, () => { - paramsTransformer(value); + const request = createRequest(value); + requestTransformer(request); expect(onIntegerOverflow).toHaveBeenCalledWith( - 'getFoo', + request, [], // Equivalent to `params` value, ); }); it('calls `onIntegerOverflow` when passed a nested array having a value ' + description, () => { - paramsTransformer([1, 2, [3, value]]); + const request = createRequest([1, 2, [3, value]]); + requestTransformer(request); expect(onIntegerOverflow).toHaveBeenCalledWith( - 'getFoo', + request, [2, 1], // Equivalent to `params[2][1]`. value, ); }); it('calls `onIntegerOverflow` when passed a nested object having a value ' + description, () => { - paramsTransformer({ a: 1, b: { b1: 2, b2: value } }); + const request = createRequest({ a: 1, b: { b1: 2, b2: value } }); + requestTransformer(request); expect(onIntegerOverflow).toHaveBeenCalledWith( - 'getFoo', + request, ['b', 'b2'], // Equivalent to `params.b.b2`. value, ); }); it('does not call `onIntegerOverflow` when passed `Number.MAX_SAFE_INTEGER`', () => { - paramsTransformer(BigInt(Number.MAX_SAFE_INTEGER)); + const request = createRequest(BigInt(Number.MAX_SAFE_INTEGER)); + requestTransformer(request); expect(onIntegerOverflow).not.toHaveBeenCalled(); }); }); diff --git a/packages/rpc-transformers/src/index.ts b/packages/rpc-transformers/src/index.ts index 5d09c8c8b71..ace9f990872 100644 --- a/packages/rpc-transformers/src/index.ts +++ b/packages/rpc-transformers/src/index.ts @@ -1,7 +1,7 @@ export * from './request-transformer'; -export * from './params-transformer-bigint-downcast'; -export * from './params-transformer-integer-overflow'; +export * from './request-transformer-bigint-downcast'; export * from './request-transformer-default-commitment'; +export * from './request-transformer-integer-overflow'; export * from './request-transformer-options-object-position-config'; export * from './response-transformer'; export * from './response-transformer-allowed-numeric-values'; diff --git a/packages/rpc-transformers/src/params-transformer-integer-overflow.ts b/packages/rpc-transformers/src/params-transformer-integer-overflow.ts deleted file mode 100644 index 787f6a555ab..00000000000 --- a/packages/rpc-transformers/src/params-transformer-integer-overflow.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { KeyPath, TraversalState } from './tree-traversal'; - -type IntegerOverflowHandler = (keyPath: KeyPath, value: bigint) => void; - -export function getIntegerOverflowNodeVisitor(onIntegerOverflow: IntegerOverflowHandler) { - return (value: T, { keyPath }: TraversalState): T => { - if (typeof value === 'bigint') { - if (onIntegerOverflow && (value > Number.MAX_SAFE_INTEGER || value < -Number.MAX_SAFE_INTEGER)) { - onIntegerOverflow(keyPath as (number | string)[], value); - } - } - return value; - }; -} diff --git a/packages/rpc-transformers/src/params-transformer-bigint-downcast.ts b/packages/rpc-transformers/src/request-transformer-bigint-downcast.ts similarity index 70% rename from packages/rpc-transformers/src/params-transformer-bigint-downcast.ts rename to packages/rpc-transformers/src/request-transformer-bigint-downcast.ts index f9786f4df41..16dad589957 100644 --- a/packages/rpc-transformers/src/params-transformer-bigint-downcast.ts +++ b/packages/rpc-transformers/src/request-transformer-bigint-downcast.ts @@ -1,3 +1,9 @@ +import { getTreeWalkerRequestTransformer } from './tree-traversal'; + +export function getBigIntDowncastRequestTransformer() { + return getTreeWalkerRequestTransformer([downcastNodeToNumberIfBigint], { keyPath: [] }); +} + export function downcastNodeToNumberIfBigint(value: bigint): number; export function downcastNodeToNumberIfBigint(value: T): T; export function downcastNodeToNumberIfBigint(value: unknown): unknown { diff --git a/packages/rpc-transformers/src/request-transformer-integer-overflow.ts b/packages/rpc-transformers/src/request-transformer-integer-overflow.ts new file mode 100644 index 00000000000..dd2aeda186b --- /dev/null +++ b/packages/rpc-transformers/src/request-transformer-integer-overflow.ts @@ -0,0 +1,26 @@ +import { RpcRequest } from '@solana/rpc-spec'; + +import { getTreeWalkerRequestTransformer, KeyPath, TraversalState } from './tree-traversal'; + +export type IntegerOverflowHandler = (request: RpcRequest, keyPath: KeyPath, value: bigint) => void; + +export function getIntegerOverflowRequestTransformer(onIntegerOverflow: IntegerOverflowHandler) { + return (request: RpcRequest): RpcRequest => { + const transformer = getTreeWalkerRequestTransformer( + [getIntegerOverflowNodeVisitor((...args) => onIntegerOverflow(request, ...args))], + { keyPath: [] }, + ); + return transformer(request); + }; +} + +export function getIntegerOverflowNodeVisitor(onIntegerOverflow: (keyPath: KeyPath, value: bigint) => void) { + return (value: T, { keyPath }: TraversalState): T => { + if (typeof value === 'bigint') { + if (onIntegerOverflow && (value > Number.MAX_SAFE_INTEGER || value < -Number.MAX_SAFE_INTEGER)) { + onIntegerOverflow(keyPath as (number | string)[], value); + } + } + return value; + }; +} diff --git a/packages/rpc-transformers/src/request-transformer.ts b/packages/rpc-transformers/src/request-transformer.ts index ee9819c16ee..6e4dc679797 100644 --- a/packages/rpc-transformers/src/request-transformer.ts +++ b/packages/rpc-transformers/src/request-transformer.ts @@ -2,36 +2,25 @@ import { pipe } from '@solana/functional'; import { RpcRequest, RpcRequestTransformer } from '@solana/rpc-spec'; import { Commitment } from '@solana/rpc-types'; -import { downcastNodeToNumberIfBigint } from './params-transformer-bigint-downcast'; -import { getIntegerOverflowNodeVisitor } from './params-transformer-integer-overflow'; +import { getBigIntDowncastRequestTransformer } from './request-transformer-bigint-downcast'; import { getDefaultCommitmentRequestTransformer } from './request-transformer-default-commitment'; +import { getIntegerOverflowRequestTransformer, IntegerOverflowHandler } from './request-transformer-integer-overflow'; import { OPTIONS_OBJECT_POSITION_BY_METHOD } from './request-transformer-options-object-position-config'; -import { getTreeWalker, KeyPath } from './tree-traversal'; export type RequestTransformerConfig = Readonly<{ defaultCommitment?: Commitment; - onIntegerOverflow?: (methodName: string, keyPath: KeyPath, value: bigint) => void; + onIntegerOverflow?: IntegerOverflowHandler; }>; export function getDefaultRequestTransformerForSolanaRpc(config?: RequestTransformerConfig): RpcRequestTransformer { - const defaultCommitment = config?.defaultCommitment; const handleIntegerOverflow = config?.onIntegerOverflow; return (request: RpcRequest): RpcRequest => { - const { params: rawParams, methodName } = request; - const traverse = getTreeWalker([ - ...(handleIntegerOverflow - ? [getIntegerOverflowNodeVisitor((...args) => handleIntegerOverflow(methodName, ...args))] - : []), - downcastNodeToNumberIfBigint, - ]); - const initialState = { - keyPath: [], - }; - const patchedRequest = { methodName, params: traverse(rawParams, initialState) }; return pipe( - patchedRequest, + request, + handleIntegerOverflow ? getIntegerOverflowRequestTransformer(handleIntegerOverflow) : r => r, + getBigIntDowncastRequestTransformer(), getDefaultCommitmentRequestTransformer({ - defaultCommitment, + defaultCommitment: config?.defaultCommitment, optionsObjectPositionByMethod: OPTIONS_OBJECT_POSITION_BY_METHOD, }), // FIXME Remove when https://github.com/anza-xyz/agave/pull/483 is deployed. diff --git a/packages/rpc-transformers/src/tree-traversal.ts b/packages/rpc-transformers/src/tree-traversal.ts index b24612d6c57..51c9a0d354a 100644 --- a/packages/rpc-transformers/src/tree-traversal.ts +++ b/packages/rpc-transformers/src/tree-traversal.ts @@ -1,3 +1,5 @@ +import { RpcRequest, RpcRequestTransformer } from '@solana/rpc-spec'; + export type KeyPathWildcard = { readonly __brand: unique symbol }; export type KeyPath = ReadonlyArray; @@ -36,3 +38,16 @@ export function getTreeWalker(visitors: NodeVisitor[]) { } }; } + +export function getTreeWalkerRequestTransformer( + visitors: NodeVisitor[], + initialState: TState, +): RpcRequestTransformer { + return (request: RpcRequest): RpcRequest => { + const traverse = getTreeWalker(visitors); + return Object.freeze({ + ...request, + params: traverse(request.params, initialState), + }); + }; +} diff --git a/packages/rpc/src/rpc-default-config.ts b/packages/rpc/src/rpc-default-config.ts index dc8810e2c01..efd42cfa03e 100644 --- a/packages/rpc/src/rpc-default-config.ts +++ b/packages/rpc/src/rpc-default-config.ts @@ -4,7 +4,7 @@ import { createSolanaJsonRpcIntegerOverflowError } from './rpc-integer-overflow- export const DEFAULT_RPC_CONFIG: Partial[0]>> = { defaultCommitment: 'confirmed', - onIntegerOverflow(methodName, keyPath, value) { - throw createSolanaJsonRpcIntegerOverflowError(methodName, keyPath, value); + onIntegerOverflow(request, keyPath, value) { + throw createSolanaJsonRpcIntegerOverflowError(request.methodName, keyPath, value); }, };