diff --git a/.changeset/light-owls-own.md b/.changeset/light-owls-own.md new file mode 100644 index 00000000000..36d4c8ab87b --- /dev/null +++ b/.changeset/light-owls-own.md @@ -0,0 +1,5 @@ +--- +'@solana/rpc-transformers': patch +--- + +Add `getThrowSolanaErrorResponseTransformer`, `getResultResponseTransformer`, `getBigIntUpcastResponseTransformer` and `getTreeWalkerResponseTransformer` helpers diff --git a/packages/rpc-transformers/README.md b/packages/rpc-transformers/README.md index 3830627a87e..142e67f4cc9 100644 --- a/packages/rpc-transformers/README.md +++ b/packages/rpc-transformers/README.md @@ -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'; @@ -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: [] }, +); +``` diff --git a/packages/rpc-transformers/src/index.ts b/packages/rpc-transformers/src/index.ts index ace9f990872..feb820bf26f 100644 --- a/packages/rpc-transformers/src/index.ts +++ b/packages/rpc-transformers/src/index.ts @@ -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'; diff --git a/packages/rpc-transformers/src/response-transformer-bigint-upcast.ts b/packages/rpc-transformers/src/response-transformer-bigint-upcast.ts index 63dffe5bdd4..25819561ab4 100644 --- a/packages/rpc-transformers/src/response-transformer-bigint-upcast.ts +++ b/packages/rpc-transformers/src/response-transformer-bigint-upcast.ts @@ -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[]) { @@ -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; + }); +} diff --git a/packages/rpc-transformers/src/response-transformer-result.ts b/packages/rpc-transformers/src/response-transformer-result.ts new file mode 100644 index 00000000000..074603ad5b8 --- /dev/null +++ b/packages/rpc-transformers/src/response-transformer-result.ts @@ -0,0 +1,7 @@ +import { createJsonRpcResponseTransformer } from '@solana/rpc-spec'; + +type JsonRpcResponse = { result: unknown }; + +export function getResultResponseTransformer() { + return createJsonRpcResponseTransformer(json => (json as JsonRpcResponse).result); +} diff --git a/packages/rpc-transformers/src/response-transformer-throw-solana-error.ts b/packages/rpc-transformers/src/response-transformer-throw-solana-error.ts new file mode 100644 index 00000000000..40e55aebf9f --- /dev/null +++ b/packages/rpc-transformers/src/response-transformer-throw-solana-error.ts @@ -0,0 +1,14 @@ +import { getSolanaErrorFromJsonRpcError } from '@solana/errors'; +import { createJsonRpcResponseTransformer } from '@solana/rpc-spec'; + +type JsonRpcResponse = { error: Parameters[0] } | { result: unknown }; + +export function getThrowSolanaErrorResponseTransformer() { + return createJsonRpcResponseTransformer(json => { + const jsonRpcResponse = json as JsonRpcResponse; + if ('error' in jsonRpcResponse) { + throw getSolanaErrorFromJsonRpcError(jsonRpcResponse.error); + } + return jsonRpcResponse; + }); +} diff --git a/packages/rpc-transformers/src/response-transformer.ts b/packages/rpc-transformers/src/response-transformer.ts index 7c97cca20f0..e24997e64e9 100644 --- a/packages/rpc-transformers/src/response-transformer.ts +++ b/packages/rpc-transformers/src/response-transformer.ts @@ -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 = Readonly<{ allowedNumericKeyPaths?: AllowedNumericKeypaths; }>; -type JsonRpcResponse = { error: Parameters[0] } | { result: unknown }; - export function getDefaultResponseTransformerForSolanaRpc( config?: ResponseTransformerConfig, ): RpcResponseTransformer { - return (rawResponse: RpcResponse, request: RpcRequest): RpcResponse => { - 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[0] } | { result: unknown }; + export function getDefaultResponseTransformerForSolanaRpcSubscriptions( config?: ResponseTransformerConfig, ): (response: unknown, notificationName: string) => T { diff --git a/packages/rpc-transformers/src/tree-traversal.ts b/packages/rpc-transformers/src/tree-traversal.ts index 51c9a0d354a..4988e10d21d 100644 --- a/packages/rpc-transformers/src/tree-traversal.ts +++ b/packages/rpc-transformers/src/tree-traversal.ts @@ -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; @@ -51,3 +56,12 @@ export function getTreeWalkerRequestTransformer( }); }; } + +export function getTreeWalkerResponseTransformer( + visitors: NodeVisitor[], + initialState: TState, +): RpcResponseTransformer { + return createJsonRpcResponseTransformer(json => { + return getTreeWalker(visitors)(json, initialState); + }); +}