Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more request transformers #3161

Merged
merged 1 commit into from
Sep 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
},
};