@solana/rpc-transformers 6.3.1 → 6.3.2-canary-20260313112147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solana/rpc-transformers",
3
- "version": "6.3.1",
3
+ "version": "6.3.2-canary-20260313112147",
4
4
  "description": "Reusable transformers for patching RPC inputs and outputs",
5
5
  "homepage": "https://www.solanakit.com/api#solanarpc-transformers",
6
6
  "exports": {
@@ -33,7 +33,8 @@
33
33
  "types": "./dist/types/index.d.ts",
34
34
  "type": "commonjs",
35
35
  "files": [
36
- "./dist/"
36
+ "./dist/",
37
+ "./src/"
37
38
  ],
38
39
  "sideEffects": false,
39
40
  "keywords": [
@@ -55,11 +56,11 @@
55
56
  "maintained node versions"
56
57
  ],
57
58
  "dependencies": {
58
- "@solana/errors": "6.3.1",
59
- "@solana/functional": "6.3.1",
60
- "@solana/nominal-types": "6.3.1",
61
- "@solana/rpc-spec-types": "6.3.1",
62
- "@solana/rpc-types": "6.3.1"
59
+ "@solana/errors": "6.3.2-canary-20260313112147",
60
+ "@solana/nominal-types": "6.3.2-canary-20260313112147",
61
+ "@solana/functional": "6.3.2-canary-20260313112147",
62
+ "@solana/rpc-types": "6.3.2-canary-20260313112147",
63
+ "@solana/rpc-spec-types": "6.3.2-canary-20260313112147"
63
64
  },
64
65
  "peerDependencies": {
65
66
  "typescript": "^5.0.0"
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * This package contains helpers for transforming Solana JSON RPC and RPC Subscriptions requests,
3
+ * responses, and notifications in various ways appropriate for use in a JavaScript application.
4
+ *
5
+ * @packageDocumentation
6
+ */
7
+ export * from './request-transformer';
8
+ export * from './request-transformer-bigint-downcast';
9
+ export * from './request-transformer-default-commitment';
10
+ export * from './request-transformer-integer-overflow';
11
+ export * from './response-transformer';
12
+ export * from './response-transformer-allowed-numeric-values';
13
+ export * from './response-transformer-bigint-upcast';
14
+ export * from './response-transformer-result';
15
+ export * from './response-transformer-throw-solana-error';
16
+ export * from './tree-traversal';
@@ -0,0 +1,10 @@
1
+ export function downcastNodeToNumberIfBigint(value: bigint): number;
2
+ export function downcastNodeToNumberIfBigint<T>(value: T): T;
3
+ export function downcastNodeToNumberIfBigint(value: unknown): unknown {
4
+ return typeof value === 'bigint'
5
+ ? // FIXME(solana-labs/solana/issues/30341) Create a data type to represent u64 in the Solana
6
+ // JSON RPC implementation so that we can throw away this entire patcher instead of unsafely
7
+ // downcasting `bigints` to `numbers`.
8
+ Number(value)
9
+ : value;
10
+ }
@@ -0,0 +1,17 @@
1
+ import { downcastNodeToNumberIfBigint } from './request-transformer-bigint-downcast-internal';
2
+ import { getTreeWalkerRequestTransformer } from './tree-traversal';
3
+
4
+ /**
5
+ * Creates a transformer that downcasts all `BigInt` values to `Number`.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { getBigIntDowncastRequestTransformer } from '@solana/rpc-transformers';
10
+ *
11
+ * const requestTransformer = getBigIntDowncastRequestTransformer();
12
+ * ```
13
+ *
14
+ */
15
+ export function getBigIntDowncastRequestTransformer() {
16
+ return getTreeWalkerRequestTransformer([downcastNodeToNumberIfBigint], { keyPath: [] });
17
+ }
@@ -0,0 +1,58 @@
1
+ import { Commitment } from '@solana/rpc-types';
2
+
3
+ export function applyDefaultCommitment({
4
+ commitmentPropertyName,
5
+ params,
6
+ optionsObjectPositionInParams,
7
+ overrideCommitment,
8
+ }: Readonly<{
9
+ commitmentPropertyName: string;
10
+ optionsObjectPositionInParams: number;
11
+ overrideCommitment?: Commitment;
12
+ params: unknown[];
13
+ }>) {
14
+ const paramInTargetPosition = params[optionsObjectPositionInParams];
15
+ if (
16
+ // There's no config.
17
+ paramInTargetPosition === undefined ||
18
+ // There is a config object.
19
+ (paramInTargetPosition && typeof paramInTargetPosition === 'object' && !Array.isArray(paramInTargetPosition))
20
+ ) {
21
+ if (
22
+ // The config object already has a commitment set.
23
+ paramInTargetPosition &&
24
+ commitmentPropertyName in paramInTargetPosition
25
+ ) {
26
+ if (
27
+ !paramInTargetPosition[commitmentPropertyName as keyof typeof paramInTargetPosition] ||
28
+ paramInTargetPosition[commitmentPropertyName as keyof typeof paramInTargetPosition] === 'finalized'
29
+ ) {
30
+ // Delete the commitment property; `finalized` is already the server default.
31
+ const nextParams = [...params];
32
+ const {
33
+ [commitmentPropertyName as keyof typeof paramInTargetPosition]: _, // eslint-disable-line @typescript-eslint/no-unused-vars
34
+ ...rest
35
+ } = paramInTargetPosition;
36
+ if (Object.keys(rest).length > 0) {
37
+ nextParams[optionsObjectPositionInParams] = rest;
38
+ } else {
39
+ if (optionsObjectPositionInParams === nextParams.length - 1) {
40
+ nextParams.length--;
41
+ } else {
42
+ nextParams[optionsObjectPositionInParams] = undefined;
43
+ }
44
+ }
45
+ return nextParams;
46
+ }
47
+ } else if (overrideCommitment !== 'finalized') {
48
+ // Apply the default commitment.
49
+ const nextParams = [...params];
50
+ nextParams[optionsObjectPositionInParams] = {
51
+ ...paramInTargetPosition,
52
+ [commitmentPropertyName]: overrideCommitment,
53
+ };
54
+ return nextParams;
55
+ }
56
+ }
57
+ return params;
58
+ }
@@ -0,0 +1,51 @@
1
+ import type { RpcRequest, RpcRequestTransformer } from '@solana/rpc-spec-types';
2
+ import type { Commitment } from '@solana/rpc-types';
3
+
4
+ import { applyDefaultCommitment } from './request-transformer-default-commitment-internal';
5
+
6
+ /**
7
+ * Creates a transformer that adds the provided default commitment to the configuration object of the request when applicable.
8
+ *
9
+ * @param config
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { getDefaultCommitmentRequestTransformer, OPTIONS_OBJECT_POSITION_BY_METHOD } from '@solana/rpc-transformers';
14
+ *
15
+ * const requestTransformer = getDefaultCommitmentRequestTransformer({
16
+ * defaultCommitment: 'confirmed',
17
+ * optionsObjectPositionByMethod: OPTIONS_OBJECT_POSITION_BY_METHOD,
18
+ * });
19
+ */
20
+ export function getDefaultCommitmentRequestTransformer({
21
+ defaultCommitment,
22
+ optionsObjectPositionByMethod,
23
+ }: Readonly<{
24
+ defaultCommitment?: Commitment;
25
+ optionsObjectPositionByMethod: Record<string, number>;
26
+ }>): RpcRequestTransformer {
27
+ return <TParams>(request: RpcRequest<TParams>): RpcRequest => {
28
+ const { params, methodName } = request;
29
+
30
+ // We only apply default commitment to array parameters.
31
+ if (!Array.isArray(params)) {
32
+ return request;
33
+ }
34
+
35
+ // Find the position of the options object in the parameters and abort if not found.
36
+ const optionsObjectPositionInParams = optionsObjectPositionByMethod[methodName];
37
+ if (optionsObjectPositionInParams == null) {
38
+ return request;
39
+ }
40
+
41
+ return Object.freeze({
42
+ methodName,
43
+ params: applyDefaultCommitment({
44
+ commitmentPropertyName: methodName === 'sendTransaction' ? 'preflightCommitment' : 'commitment',
45
+ optionsObjectPositionInParams,
46
+ overrideCommitment: defaultCommitment,
47
+ params,
48
+ }),
49
+ });
50
+ };
51
+ }
@@ -0,0 +1,12 @@
1
+ import { KeyPath, TraversalState } from './tree-traversal';
2
+
3
+ export function getIntegerOverflowNodeVisitor(onIntegerOverflow: (keyPath: KeyPath, value: bigint) => void) {
4
+ return <T>(value: T, { keyPath }: TraversalState): T => {
5
+ if (typeof value === 'bigint') {
6
+ if (onIntegerOverflow && (value > Number.MAX_SAFE_INTEGER || value < -Number.MAX_SAFE_INTEGER)) {
7
+ onIntegerOverflow(keyPath as (number | string)[], value);
8
+ }
9
+ }
10
+ return value;
11
+ };
12
+ }
@@ -0,0 +1,29 @@
1
+ import { RpcRequest } from '@solana/rpc-spec-types';
2
+
3
+ import { getIntegerOverflowNodeVisitor } from './request-transformer-integer-overflow-internal';
4
+ import { getTreeWalkerRequestTransformer, KeyPath } from './tree-traversal';
5
+
6
+ export type IntegerOverflowHandler = (request: RpcRequest, keyPath: KeyPath, value: bigint) => void;
7
+
8
+ /**
9
+ * Creates a transformer that traverses the request parameters and executes the provided handler
10
+ * when an integer overflow is detected.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { getIntegerOverflowRequestTransformer } from '@solana/rpc-transformers';
15
+ *
16
+ * const requestTransformer = getIntegerOverflowRequestTransformer((request, keyPath, value) => {
17
+ * throw new Error(`Integer overflow at ${keyPath.join('.')}: ${value}`);
18
+ * });
19
+ * ```
20
+ */
21
+ export function getIntegerOverflowRequestTransformer(onIntegerOverflow: IntegerOverflowHandler) {
22
+ return <TParams>(request: RpcRequest<TParams>): RpcRequest => {
23
+ const transformer = getTreeWalkerRequestTransformer(
24
+ [getIntegerOverflowNodeVisitor((...args) => onIntegerOverflow(request, ...args))],
25
+ { keyPath: [] },
26
+ );
27
+ return transformer(request);
28
+ };
29
+ }
@@ -0,0 +1,41 @@
1
+ export const OPTIONS_OBJECT_POSITION_BY_METHOD: Record<string, number> = {
2
+ accountNotifications: 1,
3
+ blockNotifications: 1,
4
+ getAccountInfo: 1,
5
+ getBalance: 1,
6
+ getBlock: 1,
7
+ getBlockHeight: 0,
8
+ getBlockProduction: 0,
9
+ getBlocks: 2,
10
+ getBlocksWithLimit: 2,
11
+ getEpochInfo: 0,
12
+ getFeeForMessage: 1,
13
+ getInflationGovernor: 0,
14
+ getInflationReward: 1,
15
+ getLargestAccounts: 0,
16
+ getLatestBlockhash: 0,
17
+ getLeaderSchedule: 1,
18
+ getMinimumBalanceForRentExemption: 1,
19
+ getMultipleAccounts: 1,
20
+ getProgramAccounts: 1,
21
+ getSignaturesForAddress: 1,
22
+ getSlot: 0,
23
+ getSlotLeader: 0,
24
+ getStakeMinimumDelegation: 0,
25
+ getSupply: 0,
26
+ getTokenAccountBalance: 1,
27
+ getTokenAccountsByDelegate: 2,
28
+ getTokenAccountsByOwner: 2,
29
+ getTokenLargestAccounts: 1,
30
+ getTokenSupply: 1,
31
+ getTransaction: 1,
32
+ getTransactionCount: 0,
33
+ getVoteAccounts: 0,
34
+ isBlockhashValid: 1,
35
+ logsNotifications: 1,
36
+ programNotifications: 1,
37
+ requestAirdrop: 2,
38
+ sendTransaction: 1,
39
+ signatureNotifications: 1,
40
+ simulateTransaction: 1,
41
+ };
@@ -0,0 +1,59 @@
1
+ import { pipe } from '@solana/functional';
2
+ import { RpcRequest, RpcRequestTransformer } from '@solana/rpc-spec-types';
3
+ import { Commitment } from '@solana/rpc-types';
4
+
5
+ import { getBigIntDowncastRequestTransformer } from './request-transformer-bigint-downcast';
6
+ import { getDefaultCommitmentRequestTransformer } from './request-transformer-default-commitment';
7
+ import { getIntegerOverflowRequestTransformer, IntegerOverflowHandler } from './request-transformer-integer-overflow';
8
+ import { OPTIONS_OBJECT_POSITION_BY_METHOD } from './request-transformer-options-object-position-config';
9
+
10
+ export type RequestTransformerConfig = Readonly<{
11
+ /**
12
+ * An optional {@link Commitment} value to use as the default when none is supplied by the
13
+ * caller.
14
+ */
15
+ defaultCommitment?: Commitment;
16
+ /**
17
+ * An optional function that will be called whenever a `bigint` input exceeds that which can be
18
+ * expressed using JavaScript numbers.
19
+ *
20
+ * This is used in the default {@link SolanaRpcSubscriptionsApi} to throw an exception rather
21
+ * than to allow truncated values to propagate through a program.
22
+ */
23
+ onIntegerOverflow?: IntegerOverflowHandler;
24
+ }>;
25
+
26
+ /**
27
+ * Returns the default request transformer for the Solana RPC API.
28
+ *
29
+ * Under the hood, this function composes multiple
30
+ * {@link RpcRequestTransformer | RpcRequestTransformers} together such as the
31
+ * {@link getDefaultCommitmentTransformer}, the {@link getIntegerOverflowRequestTransformer} and the
32
+ * {@link getBigIntDowncastRequestTransformer}.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { getDefaultRequestTransformerForSolanaRpc } from '@solana/rpc-transformers';
37
+ *
38
+ * const requestTransformer = getDefaultRequestTransformerForSolanaRpc({
39
+ * defaultCommitment: 'confirmed',
40
+ * onIntegerOverflow: (request, keyPath, value) => {
41
+ * throw new Error(`Integer overflow at ${keyPath.join('.')}: ${value}`);
42
+ * },
43
+ * });
44
+ * ```
45
+ */
46
+ export function getDefaultRequestTransformerForSolanaRpc(config?: RequestTransformerConfig): RpcRequestTransformer {
47
+ const handleIntegerOverflow = config?.onIntegerOverflow;
48
+ return (request: RpcRequest): RpcRequest => {
49
+ return pipe(
50
+ request,
51
+ handleIntegerOverflow ? getIntegerOverflowRequestTransformer(handleIntegerOverflow) : r => r,
52
+ getBigIntDowncastRequestTransformer(),
53
+ getDefaultCommitmentRequestTransformer({
54
+ defaultCommitment: config?.defaultCommitment,
55
+ optionsObjectPositionByMethod: OPTIONS_OBJECT_POSITION_BY_METHOD,
56
+ }),
57
+ );
58
+ };
59
+ }
@@ -0,0 +1,55 @@
1
+ import { KeyPath, KEYPATH_WILDCARD } from './tree-traversal';
2
+
3
+ export type AllowedNumericKeypaths<TApi> = Partial<Record<keyof TApi, readonly KeyPath[]>>;
4
+
5
+ // Numeric values nested in `jsonParsed` accounts
6
+ export const jsonParsedTokenAccountsConfigs = [
7
+ // parsed Token/Token22 token account
8
+ ['data', 'parsed', 'info', 'tokenAmount', 'decimals'],
9
+ ['data', 'parsed', 'info', 'tokenAmount', 'uiAmount'],
10
+ ['data', 'parsed', 'info', 'rentExemptReserve', 'decimals'],
11
+ ['data', 'parsed', 'info', 'rentExemptReserve', 'uiAmount'],
12
+ ['data', 'parsed', 'info', 'delegatedAmount', 'decimals'],
13
+ ['data', 'parsed', 'info', 'delegatedAmount', 'uiAmount'],
14
+ ['data', 'parsed', 'info', 'extensions', KEYPATH_WILDCARD, 'state', 'olderTransferFee', 'transferFeeBasisPoints'],
15
+ ['data', 'parsed', 'info', 'extensions', KEYPATH_WILDCARD, 'state', 'newerTransferFee', 'transferFeeBasisPoints'],
16
+ ['data', 'parsed', 'info', 'extensions', KEYPATH_WILDCARD, 'state', 'preUpdateAverageRate'],
17
+ ['data', 'parsed', 'info', 'extensions', KEYPATH_WILDCARD, 'state', 'currentRate'],
18
+ ];
19
+ export const jsonParsedAccountsConfigs = [
20
+ ...jsonParsedTokenAccountsConfigs,
21
+ // parsed AddressTableLookup account
22
+ ['data', 'parsed', 'info', 'lastExtendedSlotStartIndex'],
23
+ // parsed Config account
24
+ ['data', 'parsed', 'info', 'slashPenalty'],
25
+ ['data', 'parsed', 'info', 'warmupCooldownRate'],
26
+ // parsed Token/Token22 mint account
27
+ ['data', 'parsed', 'info', 'decimals'],
28
+ // parsed Token/Token22 multisig account
29
+ ['data', 'parsed', 'info', 'numRequiredSigners'],
30
+ ['data', 'parsed', 'info', 'numValidSigners'],
31
+ // parsed Stake account
32
+ ['data', 'parsed', 'info', 'stake', 'delegation', 'warmupCooldownRate'],
33
+ // parsed Sysvar rent account
34
+ ['data', 'parsed', 'info', 'exemptionThreshold'],
35
+ ['data', 'parsed', 'info', 'burnPercent'],
36
+ // parsed Vote account
37
+ ['data', 'parsed', 'info', 'commission'],
38
+ ['data', 'parsed', 'info', 'votes', KEYPATH_WILDCARD, 'confirmationCount'],
39
+ ];
40
+ export const innerInstructionsConfigs = [
41
+ ['index'],
42
+ ['instructions', KEYPATH_WILDCARD, 'accounts', KEYPATH_WILDCARD],
43
+ ['instructions', KEYPATH_WILDCARD, 'programIdIndex'],
44
+ ['instructions', KEYPATH_WILDCARD, 'stackHeight'],
45
+ ];
46
+ export const messageConfig = [
47
+ ['addressTableLookups', KEYPATH_WILDCARD, 'writableIndexes', KEYPATH_WILDCARD],
48
+ ['addressTableLookups', KEYPATH_WILDCARD, 'readonlyIndexes', KEYPATH_WILDCARD],
49
+ ['header', 'numReadonlySignedAccounts'],
50
+ ['header', 'numReadonlyUnsignedAccounts'],
51
+ ['header', 'numRequiredSignatures'],
52
+ ['instructions', KEYPATH_WILDCARD, 'accounts', KEYPATH_WILDCARD],
53
+ ['instructions', KEYPATH_WILDCARD, 'programIdIndex'],
54
+ ['instructions', KEYPATH_WILDCARD, 'stackHeight'],
55
+ ] as const;
@@ -0,0 +1,32 @@
1
+ import { KeyPath, KEYPATH_WILDCARD, TraversalState } from './tree-traversal';
2
+
3
+ export function getBigIntUpcastVisitor(allowedNumericKeyPaths: readonly KeyPath[]) {
4
+ return function upcastNodeToBigIntIfNumber(value: unknown, { keyPath }: TraversalState) {
5
+ const isInteger = (typeof value === 'number' && Number.isInteger(value)) || typeof value === 'bigint';
6
+ if (!isInteger) return value;
7
+ if (keyPathIsAllowedToBeNumeric(keyPath, allowedNumericKeyPaths)) {
8
+ return Number(value);
9
+ } else {
10
+ return BigInt(value);
11
+ }
12
+ };
13
+ }
14
+
15
+ function keyPathIsAllowedToBeNumeric(keyPath: KeyPath, allowedNumericKeyPaths: readonly KeyPath[]) {
16
+ return allowedNumericKeyPaths.some(prohibitedKeyPath => {
17
+ if (prohibitedKeyPath.length !== keyPath.length) {
18
+ return false;
19
+ }
20
+ for (let ii = keyPath.length - 1; ii >= 0; ii--) {
21
+ const keyPathPart = keyPath[ii];
22
+ const prohibitedKeyPathPart = prohibitedKeyPath[ii];
23
+ if (
24
+ prohibitedKeyPathPart !== keyPathPart &&
25
+ (prohibitedKeyPathPart !== KEYPATH_WILDCARD || typeof keyPathPart !== 'number')
26
+ ) {
27
+ return false;
28
+ }
29
+ }
30
+ return true;
31
+ });
32
+ }
@@ -0,0 +1,25 @@
1
+ import { getBigIntUpcastVisitor } from './response-transformer-bigint-upcast-internal';
2
+ import { getTreeWalkerResponseTransformer, KeyPath } from './tree-traversal';
3
+
4
+ /**
5
+ * Returns a transformer that upcasts all `Number` values to `BigInts` unless they match within the
6
+ * provided {@link KeyPath | KeyPaths}. In other words, the provided {@link KeyPath | KeyPaths} will
7
+ * remain as `Number` values, any other numeric value will be upcasted to a `BigInt`.
8
+ *
9
+ * Note that you can use {@link KEYPATH_WILDCARD} to match any key within a {@link KeyPath}.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { getBigIntUpcastResponseTransformer } from '@solana/rpc-transformers';
14
+ *
15
+ * const responseTransformer = getBigIntUpcastResponseTransformer([
16
+ * ['index'],
17
+ * ['instructions', KEYPATH_WILDCARD, 'accounts', KEYPATH_WILDCARD],
18
+ * ['instructions', KEYPATH_WILDCARD, 'programIdIndex'],
19
+ * ['instructions', KEYPATH_WILDCARD, 'stackHeight'],
20
+ * ]);
21
+ * ```
22
+ */
23
+ export function getBigIntUpcastResponseTransformer(allowedNumericKeyPaths: readonly KeyPath[]) {
24
+ return getTreeWalkerResponseTransformer([getBigIntUpcastVisitor(allowedNumericKeyPaths)], { keyPath: [] });
25
+ }
@@ -0,0 +1,19 @@
1
+ import { RpcResponseTransformer } from '@solana/rpc-spec-types';
2
+
3
+ type JsonRpcResponse = { result: unknown };
4
+
5
+ /**
6
+ * Returns a transformer that extracts the `result` field from the body of the RPC response.
7
+ *
8
+ * For instance, we go from `{ jsonrpc: '2.0', result: 'foo', id: 1 }` to `'foo'`.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { getResultResponseTransformer } from '@solana/rpc-transformers';
13
+ *
14
+ * const responseTransformer = getResultResponseTransformer();
15
+ * ```
16
+ */
17
+ export function getResultResponseTransformer(): RpcResponseTransformer {
18
+ return json => (json as JsonRpcResponse).result;
19
+ }
@@ -0,0 +1,63 @@
1
+ import { getSolanaErrorFromJsonRpcError } from '@solana/errors';
2
+ import { RpcResponseTransformer } from '@solana/rpc-spec-types';
3
+
4
+ import { innerInstructionsConfigs, jsonParsedAccountsConfigs } from './response-transformer-allowed-numeric-values';
5
+ import { getBigIntUpcastVisitor } from './response-transformer-bigint-upcast-internal';
6
+ import { getTreeWalkerResponseTransformer, KeyPath, KEYPATH_WILDCARD } from './tree-traversal';
7
+
8
+ type JsonRpcResponse = { error: Parameters<typeof getSolanaErrorFromJsonRpcError>[0] } | { result: unknown };
9
+
10
+ // Keypaths for simulateTransaction result that should remain as Number (not BigInt)
11
+ // Note: These are relative to the error.data root, not result.value like in success responses
12
+ function getSimulateTransactionAllowedNumericKeypaths(): readonly KeyPath[] {
13
+ return [
14
+ ['loadedAccountsDataSize'],
15
+ ...jsonParsedAccountsConfigs.map(c => ['accounts', KEYPATH_WILDCARD, ...c]),
16
+ ...innerInstructionsConfigs.map(c => ['innerInstructions', KEYPATH_WILDCARD, ...c]),
17
+ ];
18
+ }
19
+
20
+ /**
21
+ * Returns a transformer that throws a {@link SolanaError} with the appropriate RPC error code if
22
+ * the body of the RPC response contains an error.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { getThrowSolanaErrorResponseTransformer } from '@solana/rpc-transformers';
27
+ *
28
+ * const responseTransformer = getThrowSolanaErrorResponseTransformer();
29
+ * ```
30
+ */
31
+ export function getThrowSolanaErrorResponseTransformer(): RpcResponseTransformer {
32
+ return (json, request) => {
33
+ const jsonRpcResponse = json as JsonRpcResponse;
34
+ if ('error' in jsonRpcResponse) {
35
+ const { error } = jsonRpcResponse;
36
+
37
+ // Check if this is a sendTransaction preflight failure (error code -32002)
38
+ // These errors contain RpcSimulateTransactionResult in error.data which needs
39
+ // BigInt values downcast to Number for fields that should be numbers
40
+ const isSendTransactionPreflightFailure =
41
+ error &&
42
+ typeof error === 'object' &&
43
+ 'code' in error &&
44
+ (error.code === -32002 || error.code === -32002n);
45
+
46
+ if (isSendTransactionPreflightFailure && 'data' in error && error.data) {
47
+ // Apply BigInt downcast transformation to error.data
48
+ const treeWalker = getTreeWalkerResponseTransformer(
49
+ [getBigIntUpcastVisitor(getSimulateTransactionAllowedNumericKeypaths())],
50
+ { keyPath: [] },
51
+ );
52
+ const transformedData = treeWalker(error.data, request);
53
+
54
+ // Reconstruct error with transformed data
55
+ const transformedError = { ...error, data: transformedData };
56
+ throw getSolanaErrorFromJsonRpcError(transformedError);
57
+ }
58
+
59
+ throw getSolanaErrorFromJsonRpcError(jsonRpcResponse.error);
60
+ }
61
+ return jsonRpcResponse;
62
+ };
63
+ }
@@ -0,0 +1,74 @@
1
+ import { pipe } from '@solana/functional';
2
+ import { RpcRequest, RpcResponse, RpcResponseTransformer } from '@solana/rpc-spec-types';
3
+
4
+ import { AllowedNumericKeypaths } from './response-transformer-allowed-numeric-values';
5
+ import { getBigIntUpcastResponseTransformer } from './response-transformer-bigint-upcast';
6
+ import { getResultResponseTransformer } from './response-transformer-result';
7
+ import { getThrowSolanaErrorResponseTransformer } from './response-transformer-throw-solana-error';
8
+
9
+ export type ResponseTransformerConfig<TApi> = Readonly<{
10
+ /**
11
+ * An optional map from the name of an API method to an array of {@link KeyPath | KeyPaths}
12
+ * pointing to values in the response that should materialize in the application as `Number`
13
+ * instead of `BigInt`.
14
+ */
15
+ allowedNumericKeyPaths?: AllowedNumericKeypaths<TApi>;
16
+ }>;
17
+
18
+ /**
19
+ * Returns the default response transformer for the Solana RPC API.
20
+ *
21
+ * Under the hood, this function composes multiple
22
+ * {@link RpcResponseTransformer | RpcResponseTransformers} together such as the
23
+ * {@link getThrowSolanaErrorResponseTransformer}, the {@link getResultResponseTransformer} and the
24
+ * {@link getBigIntUpcastResponseTransformer}.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * import { getDefaultResponseTransformerForSolanaRpc } from '@solana/rpc-transformers';
29
+ *
30
+ * const responseTransformer = getDefaultResponseTransformerForSolanaRpc({
31
+ * allowedNumericKeyPaths: getAllowedNumericKeypaths(),
32
+ * });
33
+ * ```
34
+ */
35
+ export function getDefaultResponseTransformerForSolanaRpc<TApi>(
36
+ config?: ResponseTransformerConfig<TApi>,
37
+ ): RpcResponseTransformer {
38
+ return (response: RpcResponse, request: RpcRequest): RpcResponse => {
39
+ const methodName = request.methodName as keyof TApi;
40
+ const keyPaths =
41
+ config?.allowedNumericKeyPaths && methodName ? config.allowedNumericKeyPaths[methodName] : undefined;
42
+ return pipe(
43
+ response,
44
+ r => getThrowSolanaErrorResponseTransformer()(r, request),
45
+ r => getResultResponseTransformer()(r, request),
46
+ r => getBigIntUpcastResponseTransformer(keyPaths ?? [])(r, request),
47
+ );
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Returns the default response transformer for the Solana RPC Subscriptions API.
53
+ *
54
+ * Under the hood, this function composes the {@link getBigIntUpcastResponseTransformer}.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import { getDefaultResponseTransformerForSolanaRpcSubscriptions } from '@solana/rpc-transformers';
59
+ *
60
+ * const responseTransformer = getDefaultResponseTransformerForSolanaRpcSubscriptions({
61
+ * allowedNumericKeyPaths: getAllowedNumericKeypaths(),
62
+ * });
63
+ * ```
64
+ */
65
+ export function getDefaultResponseTransformerForSolanaRpcSubscriptions<TApi>(
66
+ config?: ResponseTransformerConfig<TApi>,
67
+ ): RpcResponseTransformer {
68
+ return (response: RpcResponse, request: RpcRequest): RpcResponse => {
69
+ const methodName = request.methodName as keyof TApi;
70
+ const keyPaths =
71
+ config?.allowedNumericKeyPaths && methodName ? config.allowedNumericKeyPaths[methodName] : undefined;
72
+ return pipe(response, r => getBigIntUpcastResponseTransformer(keyPaths ?? [])(r, request));
73
+ };
74
+ }
@@ -0,0 +1,79 @@
1
+ import { RpcRequest, RpcRequestTransformer, RpcResponseTransformer } from '@solana/rpc-spec-types';
2
+
3
+ export type KeyPathWildcard = { readonly ['__keyPathWildcard:@solana/kit']: unique symbol };
4
+ export type KeyPath = ReadonlyArray<KeyPath | KeyPathWildcard | number | string>;
5
+
6
+ export const KEYPATH_WILDCARD = {} as KeyPathWildcard;
7
+
8
+ type NodeVisitor = <TState extends TraversalState>(value: unknown, state: TState) => unknown;
9
+ export type TraversalState = Readonly<{
10
+ keyPath: KeyPath;
11
+ }>;
12
+
13
+ function getTreeWalker(visitors: NodeVisitor[]) {
14
+ return function traverse<TState extends TraversalState>(node: unknown, state: TState): unknown {
15
+ if (Array.isArray(node)) {
16
+ return node.map((element, ii) => {
17
+ const nextState = {
18
+ ...state,
19
+ keyPath: [...state.keyPath, ii],
20
+ };
21
+ return traverse(element, nextState);
22
+ });
23
+ } else if (typeof node === 'object' && node !== null) {
24
+ const out: Record<number | string | symbol, unknown> = {};
25
+ for (const propName in node) {
26
+ if (!Object.prototype.hasOwnProperty.call(node, propName)) {
27
+ continue;
28
+ }
29
+ const nextState = {
30
+ ...state,
31
+ keyPath: [...state.keyPath, propName],
32
+ };
33
+ out[propName] = traverse(node[propName as keyof typeof node], nextState);
34
+ }
35
+ return out;
36
+ } else {
37
+ return visitors.reduce((acc, visitNode) => visitNode(acc, state), node);
38
+ }
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Creates a transformer that traverses the request parameters and executes the provided visitors at
44
+ * each node. A custom initial state can be provided but must at least provide `{ keyPath: [] }`.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { getTreeWalkerRequestTransformer } from '@solana/rpc-transformers';
49
+ *
50
+ * const requestTransformer = getTreeWalkerRequestTransformer(
51
+ * [
52
+ * // Replaces foo.bar with "baz".
53
+ * (node, state) => (state.keyPath === ['foo', 'bar'] ? 'baz' : node),
54
+ * // Increments all numbers by 1.
55
+ * node => (typeof node === number ? node + 1 : node),
56
+ * ],
57
+ * { keyPath: [] },
58
+ * );
59
+ * ```
60
+ */
61
+ export function getTreeWalkerRequestTransformer<TState extends TraversalState>(
62
+ visitors: NodeVisitor[],
63
+ initialState: TState,
64
+ ): RpcRequestTransformer {
65
+ return <TParams>(request: RpcRequest<TParams>): RpcRequest => {
66
+ const traverse = getTreeWalker(visitors);
67
+ return Object.freeze({
68
+ ...request,
69
+ params: traverse(request.params, initialState),
70
+ });
71
+ };
72
+ }
73
+
74
+ export function getTreeWalkerResponseTransformer<TState extends TraversalState>(
75
+ visitors: NodeVisitor[],
76
+ initialState: TState,
77
+ ): RpcResponseTransformer {
78
+ return json => getTreeWalker(visitors)(json, initialState);
79
+ }