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

Commit

Permalink
Add more response transformers (#3163)
Browse files Browse the repository at this point in the history
This PR refactors the `getDefaultResponseTransformerForSolanaRpc` function by creating four new response transformers and delegating to them:

- `getThrowSolanaErrorResponseTransformer`: Throws a `SolanaError` if the RPC response is an error.
- `getResultResponseTransformer`: Access the `result` attribute of the RPC response.
- `getBigIntUpcastResponseTransformer`: Upcasts all numeric values to bigints unless they are within the provided allowed keypaths.
- `getTreeWalkerResponseTransformer`: Helper that creates response transformers from visitors.
  • Loading branch information
lorisleiva authored Sep 1, 2024
1 parent c312964 commit 29d5113
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 43 deletions.
5 changes: 5 additions & 0 deletions .changeset/light-owls-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solana/rpc-transformers': patch
---

Add `getThrowSolanaErrorResponseTransformer`, `getResultResponseTransformer`, `getBigIntUpcastResponseTransformer` and `getTreeWalkerResponseTransformer` helpers
69 changes: 68 additions & 1 deletion packages/rpc-transformers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

### `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`.
Returns the default request transformer for the Solana RPC API. Under the hood, this function composes multiple `RpcRequestTransformers` together such as the `getDefaultCommitmentTransformer`, the `getIntegerOverflowRequestTransformer` and the `getBigIntDowncastRequestTransformer`.

```ts
import { getDefaultRequestTransformerForSolanaRpc } from '@solana/rpc-transformers';
Expand Down Expand Up @@ -83,3 +83,70 @@ const requestTransformer = getTreeWalkerRequestTransformer(
{ keyPath: [] },
);
```

## Response Transformers

### `getDefaultResponseTransformerForSolanaRpc(config)`

Returns the default response transformer for the Solana RPC API. Under the hood, this function composes multiple `RpcResponseTransformers` together such as the `getThrowSolanaErrorResponseTransformer`, the `getResultResponseTransformer` and the `getBigIntUpcastResponseTransformer`.

```ts
import { getDefaultResponseTransformerForSolanaRpc } from '@solana/rpc-transformers';

const responseTransformer = getDefaultResponseTransformerForSolanaRpc({
allowedNumericKeyPaths: getAllowedNumericKeypaths(),
});
```

### `getThrowSolanaErrorResponseTransformer()`

Returns a transformer that throws a `SolanaError` with the appropriate RPC error code if the body of the RPC response contains an error.

```ts
import { getThrowSolanaErrorResponseTransformer } from '@solana/rpc-transformers';

const responseTransformer = getThrowSolanaErrorResponseTransformer();
```

### `getResultResponseTransformer()`

Returns a transformer that extracts the `result` field from the body of the RPC response. For instance, we go from `{ jsonrpc: '2.0', result: 'foo', id: 1 }` to `'foo'`.

```ts
import { getResultResponseTransformer } from '@solana/rpc-transformers';

const responseTransformer = getResultResponseTransformer();
```

### `getBigIntUpcastResponseTransformer(allowedNumericKeyPaths)`

Returns a transformer that upcasts all `Number` values to `BigInts` unless they match within the provided `KeyPaths`. In other words, the provided `KeyPaths` will remain as `Number` values, any other numeric value will be upcasted to a `BigInt`. Note that you can use `KEYPATH_WILDCARD` to match any key within a `KeyPath`.

```ts
import { getBigIntUpcastResponseTransformer } from '@solana/rpc-transformers';

const responseTransformer = getBigIntUpcastResponseTransformer([
['index'],
['instructions', KEYPATH_WILDCARD, 'accounts', KEYPATH_WILDCARD],
['instructions', KEYPATH_WILDCARD, 'programIdIndex'],
['instructions', KEYPATH_WILDCARD, 'stackHeight'],
]);
```

### `getTreeWalkerResponseTransformer(visitors, initialState)`

Creates a transformer that traverses the json response and executes the provided visitors at each node. A custom initial state can be provided but must at least provide `{ keyPath: [] }`.

```ts
import { getTreeWalkerResponseTransformer } from '@solana/rpc-transformers';

const responseTransformer = getTreeWalkerResponseTransformer(
[
// 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: [] },
);
```
2 changes: 2 additions & 0 deletions packages/rpc-transformers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ export * from './request-transformer-options-object-position-config';
export * from './response-transformer';
export * from './response-transformer-allowed-numeric-values';
export * from './response-transformer-bigint-upcast';
export * from './response-transformer-result';
export * from './response-transformer-throw-solana-error';
export * from './tree-traversal';
40 changes: 22 additions & 18 deletions packages/rpc-transformers/src/response-transformer-bigint-upcast.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,7 @@
import { KeyPath, KEYPATH_WILDCARD, TraversalState } from './tree-traversal';
import { getTreeWalkerResponseTransformer, KeyPath, KEYPATH_WILDCARD, TraversalState } from './tree-traversal';

function keyPathIsAllowedToBeNumeric(keyPath: KeyPath, allowedNumericKeyPaths: readonly KeyPath[]) {
return allowedNumericKeyPaths.some(prohibitedKeyPath => {
if (prohibitedKeyPath.length !== keyPath.length) {
return false;
}
for (let ii = keyPath.length - 1; ii >= 0; ii--) {
const keyPathPart = keyPath[ii];
const prohibitedKeyPathPart = prohibitedKeyPath[ii];
if (
prohibitedKeyPathPart !== keyPathPart &&
(prohibitedKeyPathPart !== KEYPATH_WILDCARD || typeof keyPathPart !== 'number')
) {
return false;
}
}
return true;
});
export function getBigIntUpcastResponseTransformer(allowedNumericKeyPaths: readonly KeyPath[]) {
return getTreeWalkerResponseTransformer([getBigIntUpcastVisitor(allowedNumericKeyPaths)], { keyPath: [] });
}

export function getBigIntUpcastVisitor(allowedNumericKeyPaths: readonly KeyPath[]) {
Expand All @@ -35,3 +20,22 @@ export function getBigIntUpcastVisitor(allowedNumericKeyPaths: readonly KeyPath[
}
};
}

function keyPathIsAllowedToBeNumeric(keyPath: KeyPath, allowedNumericKeyPaths: readonly KeyPath[]) {
return allowedNumericKeyPaths.some(prohibitedKeyPath => {
if (prohibitedKeyPath.length !== keyPath.length) {
return false;
}
for (let ii = keyPath.length - 1; ii >= 0; ii--) {
const keyPathPart = keyPath[ii];
const prohibitedKeyPathPart = prohibitedKeyPath[ii];
if (
prohibitedKeyPathPart !== keyPathPart &&
(prohibitedKeyPathPart !== KEYPATH_WILDCARD || typeof keyPathPart !== 'number')
) {
return false;
}
}
return true;
});
}
7 changes: 7 additions & 0 deletions packages/rpc-transformers/src/response-transformer-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createJsonRpcResponseTransformer } from '@solana/rpc-spec';

type JsonRpcResponse = { result: unknown };

export function getResultResponseTransformer() {
return createJsonRpcResponseTransformer(json => (json as JsonRpcResponse).result);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { getSolanaErrorFromJsonRpcError } from '@solana/errors';
import { createJsonRpcResponseTransformer } from '@solana/rpc-spec';

type JsonRpcResponse = { error: Parameters<typeof getSolanaErrorFromJsonRpcError>[0] } | { result: unknown };

export function getThrowSolanaErrorResponseTransformer() {
return createJsonRpcResponseTransformer(json => {
const jsonRpcResponse = json as JsonRpcResponse;
if ('error' in jsonRpcResponse) {
throw getSolanaErrorFromJsonRpcError(jsonRpcResponse.error);
}
return jsonRpcResponse;
});
}
39 changes: 16 additions & 23 deletions packages/rpc-transformers/src/response-transformer.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,35 @@
import { getSolanaErrorFromJsonRpcError } from '@solana/errors';
import { pipe } from '@solana/functional';
import { RpcRequest, RpcResponse, RpcResponseTransformer } from '@solana/rpc-spec';

import { AllowedNumericKeypaths } from './response-transformer-allowed-numeric-values';
import { getBigIntUpcastVisitor } from './response-transformer-bigint-upcast';
import { getBigIntUpcastResponseTransformer, getBigIntUpcastVisitor } from './response-transformer-bigint-upcast';
import { getResultResponseTransformer } from './response-transformer-result';
import { getThrowSolanaErrorResponseTransformer } from './response-transformer-throw-solana-error';
import { getTreeWalker } from './tree-traversal';

export type ResponseTransformerConfig<TApi> = Readonly<{
allowedNumericKeyPaths?: AllowedNumericKeypaths<TApi>;
}>;

type JsonRpcResponse = { error: Parameters<typeof getSolanaErrorFromJsonRpcError>[0] } | { result: unknown };

export function getDefaultResponseTransformerForSolanaRpc<TApi>(
config?: ResponseTransformerConfig<TApi>,
): RpcResponseTransformer {
return <T>(rawResponse: RpcResponse, request: RpcRequest): RpcResponse<T> => {
return {
...rawResponse,
json: async () => {
const methodName = request.methodName as keyof TApi;
const rawData = (await rawResponse.json()) as JsonRpcResponse;
if ('error' in rawData) {
throw getSolanaErrorFromJsonRpcError(rawData.error);
}
const keyPaths =
config?.allowedNumericKeyPaths && methodName
? config.allowedNumericKeyPaths[methodName]
: undefined;
const traverse = getTreeWalker([getBigIntUpcastVisitor(keyPaths ?? [])]);
const initialState = {
keyPath: [],
};
return traverse(rawData.result, initialState) as T;
},
};
return (response: RpcResponse, request: RpcRequest): RpcResponse => {
const methodName = request.methodName as keyof TApi;
const keyPaths =
config?.allowedNumericKeyPaths && methodName ? config.allowedNumericKeyPaths[methodName] : undefined;
return pipe(
response,
r => getThrowSolanaErrorResponseTransformer()(r, request),
r => getResultResponseTransformer()(r, request),
r => getBigIntUpcastResponseTransformer(keyPaths ?? [])(r, request),
);
};
}

type JsonRpcResponse = { error: Parameters<typeof getSolanaErrorFromJsonRpcError>[0] } | { result: unknown };

export function getDefaultResponseTransformerForSolanaRpcSubscriptions<TApi>(
config?: ResponseTransformerConfig<TApi>,
): <T>(response: unknown, notificationName: string) => T {
Expand Down
16 changes: 15 additions & 1 deletion packages/rpc-transformers/src/tree-traversal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { RpcRequest, RpcRequestTransformer } from '@solana/rpc-spec';
import {
createJsonRpcResponseTransformer,
RpcRequest,
RpcRequestTransformer,
RpcResponseTransformer,
} from '@solana/rpc-spec';

export type KeyPathWildcard = { readonly __brand: unique symbol };
export type KeyPath = ReadonlyArray<KeyPath | KeyPathWildcard | number | string>;
Expand Down Expand Up @@ -51,3 +56,12 @@ export function getTreeWalkerRequestTransformer<TState extends TraversalState>(
});
};
}

export function getTreeWalkerResponseTransformer<TState extends TraversalState>(
visitors: NodeVisitor[],
initialState: TState,
): RpcResponseTransformer {
return createJsonRpcResponseTransformer(json => {
return getTreeWalker(visitors)(json, initialState);
});
}

0 comments on commit 29d5113

Please sign in to comment.