Skip to content

Commit 1e2106f

Browse files
authored
refactor(experimental): add generic createJsonRpcApi function for custom APIs
This PR is a concept for now. It serves to potentially address the discussion raised in #1740. The goal of the collective `@solana/rpc-*` packages has been to allow complete customization, as long as their API adheres to the [official JSON RPC spec](https://www.jsonrpc.org/specification). However, I couldn't find the best way someone would _actually_ go about doing this with our current implementation. Maybe I'm missing something. In the actual `library` package, we're providing what's been dubbed the "default" Solana API. The function `createSolanaRpc(..)` allows you to provide your own transport, but it automatically uses the Solana RPC API, defined by `@solana/rpc-core` for the client API. https://github.com/solana-labs/solana-web3.js/blob/589c379aa0ab495f50dac873f06573f52cdc9f98/packages/library/src/rpc.ts#L19-L24 If one wants to create their own RPC client manually, they can use the following code, comprised of `@solana/rpc-transport` and `@solana/rpc-core`, _not_ the main library. ```typescript const api = createSolanaRpcApi(); // ^ IRpcApi<SolanaRpcMethods> const transport = createHttpTransport({ url: 'http://127.0.0.1:8899' }); const rpc = createJsonRpc<SolanaRpcMethods>({ api, transport }); // ^ RpcMethods<SolanaRpcMethods> ``` You can see you can choose to define your API and provide it as a parameter to `createJsonRpc(..)`, however unless I'm missing something in our code, there's no generic API-creator. This PR attempts to roll that generic API-creator as `createJsonRpcApi(..)`. I envision this function being extremely useful for projects who wish to define their own `rpc-core` type-spec via interfaces, as we have with `SolanaRpcMethods`, and simply create it like so: ```typescript // Define the method's response payload type NftCollectionDetailsApiResponse = Readonly<{ address: string; circulatingSupply: number; description: string; erc721: boolean; erc1155: boolean; genesisBlock: string; genesisTransaction: string; name: string; totalSupply: number; }>; // Set up an interface for the request method interface NftCollectionDetailsApi { // Define the method's name, parameters and response type qn_fetchNFTCollectionDetails(args: { contracts: string[] }): NftCollectionDetailsApiResponse; } // Export the type spec for downstream users export type QuickNodeRpcMethods = NftCollectionDetailsApi; // Create the custom API const api = createJsonRpcApi<QuickNodeRpcMethods>(); // Set up an HTTP transport const transport = createHttpTransport({ url: 'http://127.0.0.1:8899' }); // Create the RPC client const quickNodeRpc = createJsonRpc<QuickNodeRpcMethods>({ api, transport }); // ^ RpcMethods<QuickNodeRpcMethods> ``` Of course you could _also_ combine your type spec with Solana's: ```typescript export type QuickNodeSolanaRpcMethods = SolanaRpcMethods & NftCollectionDetailsApi; ``` Let me know any thoughts.
1 parent 9f2b2fd commit 1e2106f

File tree

7 files changed

+78
-35
lines changed

7 files changed

+78
-35
lines changed

packages/rpc-core/src/rpc-methods/index.ts

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { IRpcApi, RpcRequest } from '@solana/rpc-transport';
1+
import { createJsonRpcApi } from '@solana/rpc-transport';
2+
import { IRpcApi } from '@solana/rpc-transport';
23

34
import { patchParamsForSolanaLabsRpc } from '../params-patcher';
45
import { patchResponseForSolanaLabsRpc } from '../response-patcher';
@@ -115,37 +116,17 @@ export type SolanaRpcMethods = GetAccountInfoApi &
115116
SimulateTransactionApi;
116117

117118
export function createSolanaRpcApi(config?: Config): IRpcApi<SolanaRpcMethods> {
118-
return new Proxy({} as IRpcApi<SolanaRpcMethods>, {
119-
defineProperty() {
120-
return false;
121-
},
122-
deleteProperty() {
123-
return false;
124-
},
125-
get<TMethodName extends keyof IRpcApi<SolanaRpcMethods>>(
126-
...args: Parameters<NonNullable<ProxyHandler<IRpcApi<SolanaRpcMethods>>['get']>>
127-
) {
128-
const [_, p] = args;
129-
const methodName = p.toString() as keyof SolanaRpcMethods as string;
130-
return function (
131-
...rawParams: Parameters<
132-
SolanaRpcMethods[TMethodName] extends CallableFunction ? SolanaRpcMethods[TMethodName] : never
133-
>
134-
): RpcRequest<ReturnType<SolanaRpcMethods[TMethodName]>> {
135-
const handleIntegerOverflow = config?.onIntegerOverflow;
136-
const params = patchParamsForSolanaLabsRpc(
137-
rawParams,
138-
handleIntegerOverflow
139-
? (keyPath, value) => handleIntegerOverflow(methodName, keyPath, value)
140-
: undefined,
141-
);
142-
return {
143-
methodName,
144-
params,
145-
responseTransformer: rawResponse => patchResponseForSolanaLabsRpc(rawResponse, methodName),
146-
};
147-
};
148-
},
119+
const handleIntegerOverflow = config?.onIntegerOverflow;
120+
return createJsonRpcApi<SolanaRpcMethods>({
121+
parametersTransformer: <T>(rawParams: T, methodName: string) =>
122+
patchParamsForSolanaLabsRpc(
123+
rawParams,
124+
handleIntegerOverflow
125+
? (keyPath, value) => handleIntegerOverflow(methodName, keyPath, value)
126+
: undefined,
127+
) as unknown[],
128+
responseTransformer: <T>(rawResponse: unknown, methodName: string): T =>
129+
patchResponseForSolanaLabsRpc(rawResponse, methodName as keyof SolanaRpcMethods),
149130
});
150131
}
151132

packages/rpc-transport/src/__tests__/json-rpc-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ describe('JSON-RPC 2.0', () => {
9797
expect.assertions(1);
9898
(makeHttpRequest as jest.Mock).mockResolvedValueOnce({ result: 123 });
9999
await rpc.someMethod().send();
100-
expect(responseTransformer).toHaveBeenCalledWith(123);
100+
expect(responseTransformer).toHaveBeenCalledWith(123, 'someMethod');
101101
});
102102
it('returns the processed response', async () => {
103103
expect.assertions(1);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { IRpcApi } from '../../json-rpc-types';
2+
import { IRpcApiMethods } from '../api-types';
3+
import { createJsonRpcApi } from '../methods/methods-api';
4+
5+
type NftCollectionDetailsApiResponse = Readonly<{
6+
address: string;
7+
circulatingSupply: number;
8+
description: string;
9+
erc721: boolean;
10+
erc1155: boolean;
11+
genesisBlock: string;
12+
genesisTransaction: string;
13+
name: string;
14+
totalSupply: number;
15+
}>;
16+
17+
interface NftCollectionDetailsApi extends IRpcApiMethods {
18+
qn_fetchNFTCollectionDetails(args: { contracts: string[] }): NftCollectionDetailsApiResponse;
19+
}
20+
21+
type QuickNodeRpcMethods = NftCollectionDetailsApi;
22+
23+
createJsonRpcApi<QuickNodeRpcMethods>() satisfies IRpcApi<QuickNodeRpcMethods>;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { IRpcApi, RpcRequest } from '../../json-rpc-types';
2+
import { IRpcApiMethods, RpcApiConfig } from '../api-types';
3+
4+
export function createJsonRpcApi<TRpcMethods extends IRpcApiMethods>(config?: RpcApiConfig): IRpcApi<TRpcMethods> {
5+
return new Proxy({} as IRpcApi<TRpcMethods>, {
6+
defineProperty() {
7+
return false;
8+
},
9+
deleteProperty() {
10+
return false;
11+
},
12+
get<TMethodName extends keyof IRpcApi<TRpcMethods>>(
13+
...args: Parameters<NonNullable<ProxyHandler<IRpcApi<TRpcMethods>>['get']>>
14+
) {
15+
const [_, p] = args;
16+
const methodName = p.toString() as keyof TRpcMethods as string;
17+
return function (
18+
...rawParams: Parameters<
19+
TRpcMethods[TMethodName] extends CallableFunction ? TRpcMethods[TMethodName] : never
20+
>
21+
): RpcRequest<ReturnType<TRpcMethods[TMethodName]>> {
22+
const params = config?.parametersTransformer
23+
? config?.parametersTransformer(rawParams, methodName)
24+
: rawParams;
25+
const responseTransformer = config?.responseTransformer
26+
? config?.responseTransformer<ReturnType<TRpcMethods[TMethodName]>>
27+
: (rawResponse: unknown) => rawResponse as ReturnType<TRpcMethods[TMethodName]>;
28+
return {
29+
methodName,
30+
params,
31+
responseTransformer,
32+
};
33+
};
34+
},
35+
});
36+
}

packages/rpc-transport/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './apis/api-types';
2+
export * from './apis/methods/methods-api';
23
export * from './json-rpc';
34
export type { SolanaJsonRpcErrorCode } from './json-rpc-errors';
45
export * from './json-rpc-subscription';

packages/rpc-transport/src/json-rpc-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export type RpcSubscriptionConfig<TRpcMethods> = Readonly<{
2626
export type RpcRequest<TResponse> = {
2727
methodName: string;
2828
params: unknown[];
29-
responseTransformer?: (response: unknown) => TResponse;
29+
responseTransformer?: (response: unknown, methodName: string) => TResponse;
3030
};
3131
export type RpcSubscription<TResponse> = {
3232
params: unknown[];

packages/rpc-transport/src/json-rpc.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ function createPendingRpcRequest<TRpcMethods, TResponse>(
2323
if ('error' in response) {
2424
throw new SolanaJsonRpcError(response.error);
2525
} else {
26-
return (responseTransformer ? responseTransformer(response.result) : response.result) as TResponse;
26+
return (
27+
responseTransformer ? responseTransformer(response.result, methodName) : response.result
28+
) as TResponse;
2729
}
2830
},
2931
};

0 commit comments

Comments
 (0)