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

Commit

Permalink
Add more request transformers (#3161)
Browse files Browse the repository at this point in the history
This PR finishes the refactoring of the `getDefaultRequestTransformerForSolanaRpc` function by creating three new request transformers and delegating to them:

- `getIntegerOverflowRequestTransformer`: Wraps the integer overflow visitor in a request transformer.
- `getBigIntDowncastRequestTransformer`: Wraps the bigint downcast visitor in a request transformer.
- `getTreeWalkerRequestTransformer`: Helper that creates request transformers from visitors.

Additionally, this PR changes the definition of the `onIntegerOverflow` slightly by using `request: RpcRequest` as its first argument instead of `methodName: string` to be more consistent with the rest of the RPC API.
  • Loading branch information
lorisleiva authored Sep 1, 2024
1 parent fcfaec9 commit 9dfca45
Show file tree
Hide file tree
Showing 15 changed files with 155 additions and 57 deletions.
7 changes: 7 additions & 0 deletions .changeset/giant-coins-complain.md
Original file line number Diff line number Diff line change
@@ -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`
6 changes: 6 additions & 0 deletions .changeset/popular-buttons-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@solana/rpc-transformers': patch
'@solana/rpc-api': patch
---

Add `getIntegerOverflowRequestTransformer`, `getBigIntDowncastRequestTransformer` and `getTreeWalkerRequestTransformer` helpers
2 changes: 1 addition & 1 deletion packages/rpc-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 2 additions & 2 deletions packages/rpc-subscriptions/src/rpc-default-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const DEFAULT_RPC_SUBSCRIPTIONS_CONFIG: Partial<
NonNullable<Parameters<typeof createSolanaRpcSubscriptionsApi>[0]>
> = {
defaultCommitment: 'confirmed',
onIntegerOverflow(methodName, keyPath, value) {
throw createSolanaJsonRpcIntegerOverflowError(methodName, keyPath, value);
onIntegerOverflow(request, keyPath, value) {
throw createSolanaJsonRpcIntegerOverflowError(request.methodName, keyPath, value);
},
};
57 changes: 56 additions & 1 deletion packages/rpc-transformers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`

Expand All @@ -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: [] },
);
```
Original file line number Diff line number Diff line change
@@ -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 => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down
38 changes: 23 additions & 15 deletions packages/rpc-transformers/src/__tests__/request-transformer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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) },
Expand Down Expand Up @@ -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();
});
});
Expand Down
4 changes: 2 additions & 2 deletions packages/rpc-transformers/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<T>(value: T): T;
export function downcastNodeToNumberIfBigint(value: unknown): unknown {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <TParams>(request: RpcRequest<TParams>): RpcRequest => {
const transformer = getTreeWalkerRequestTransformer(
[getIntegerOverflowNodeVisitor((...args) => onIntegerOverflow(request, ...args))],
{ keyPath: [] },
);
return transformer(request);
};
}

export function getIntegerOverflowNodeVisitor(onIntegerOverflow: (keyPath: KeyPath, value: bigint) => void) {
return <T>(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;
};
}
25 changes: 7 additions & 18 deletions packages/rpc-transformers/src/request-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions packages/rpc-transformers/src/tree-traversal.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { RpcRequest, RpcRequestTransformer } from '@solana/rpc-spec';

export type KeyPathWildcard = { readonly __brand: unique symbol };
export type KeyPath = ReadonlyArray<KeyPath | KeyPathWildcard | number | string>;

Expand Down Expand Up @@ -36,3 +38,16 @@ export function getTreeWalker(visitors: NodeVisitor[]) {
}
};
}

export function getTreeWalkerRequestTransformer<TState extends TraversalState>(
visitors: NodeVisitor[],
initialState: TState,
): RpcRequestTransformer {
return <TParams>(request: RpcRequest<TParams>): RpcRequest => {
const traverse = getTreeWalker(visitors);
return Object.freeze({
...request,
params: traverse(request.params, initialState),
});
};
}
4 changes: 2 additions & 2 deletions packages/rpc/src/rpc-default-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createSolanaJsonRpcIntegerOverflowError } from './rpc-integer-overflow-

export const DEFAULT_RPC_CONFIG: Partial<NonNullable<Parameters<typeof createSolanaRpcApi>[0]>> = {
defaultCommitment: 'confirmed',
onIntegerOverflow(methodName, keyPath, value) {
throw createSolanaJsonRpcIntegerOverflowError(methodName, keyPath, value);
onIntegerOverflow(request, keyPath, value) {
throw createSolanaJsonRpcIntegerOverflowError(request.methodName, keyPath, value);
},
};

0 comments on commit 9dfca45

Please sign in to comment.