@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 +8 -7
- package/src/index.ts +16 -0
- package/src/request-transformer-bigint-downcast-internal.ts +10 -0
- package/src/request-transformer-bigint-downcast.ts +17 -0
- package/src/request-transformer-default-commitment-internal.ts +58 -0
- package/src/request-transformer-default-commitment.ts +51 -0
- package/src/request-transformer-integer-overflow-internal.ts +12 -0
- package/src/request-transformer-integer-overflow.ts +29 -0
- package/src/request-transformer-options-object-position-config.ts +41 -0
- package/src/request-transformer.ts +59 -0
- package/src/response-transformer-allowed-numeric-values.ts +55 -0
- package/src/response-transformer-bigint-upcast-internal.ts +32 -0
- package/src/response-transformer-bigint-upcast.ts +25 -0
- package/src/response-transformer-result.ts +19 -0
- package/src/response-transformer-throw-solana-error.ts +63 -0
- package/src/response-transformer.ts +74 -0
- package/src/tree-traversal.ts +79 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solana/rpc-transformers",
|
|
3
|
-
"version": "6.3.
|
|
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.
|
|
59
|
-
"@solana/
|
|
60
|
-
"@solana/
|
|
61
|
-
"@solana/rpc-
|
|
62
|
-
"@solana/rpc-types": "6.3.
|
|
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
|
+
}
|