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

Commit

Permalink
refactor(experimental): add generic createJsonRpcApi function for c…
Browse files Browse the repository at this point in the history
…ustom 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.
  • Loading branch information
buffalojoec authored Dec 12, 2023
1 parent 9f2b2fd commit 1e2106f
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 35 deletions.
45 changes: 13 additions & 32 deletions packages/rpc-core/src/rpc-methods/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { IRpcApi, RpcRequest } from '@solana/rpc-transport';
import { createJsonRpcApi } from '@solana/rpc-transport';
import { IRpcApi } from '@solana/rpc-transport';

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

export function createSolanaRpcApi(config?: Config): IRpcApi<SolanaRpcMethods> {
return new Proxy({} as IRpcApi<SolanaRpcMethods>, {
defineProperty() {
return false;
},
deleteProperty() {
return false;
},
get<TMethodName extends keyof IRpcApi<SolanaRpcMethods>>(
...args: Parameters<NonNullable<ProxyHandler<IRpcApi<SolanaRpcMethods>>['get']>>
) {
const [_, p] = args;
const methodName = p.toString() as keyof SolanaRpcMethods as string;
return function (
...rawParams: Parameters<
SolanaRpcMethods[TMethodName] extends CallableFunction ? SolanaRpcMethods[TMethodName] : never
>
): RpcRequest<ReturnType<SolanaRpcMethods[TMethodName]>> {
const handleIntegerOverflow = config?.onIntegerOverflow;
const params = patchParamsForSolanaLabsRpc(
rawParams,
handleIntegerOverflow
? (keyPath, value) => handleIntegerOverflow(methodName, keyPath, value)
: undefined,
);
return {
methodName,
params,
responseTransformer: rawResponse => patchResponseForSolanaLabsRpc(rawResponse, methodName),
};
};
},
const handleIntegerOverflow = config?.onIntegerOverflow;
return createJsonRpcApi<SolanaRpcMethods>({
parametersTransformer: <T>(rawParams: T, methodName: string) =>
patchParamsForSolanaLabsRpc(
rawParams,
handleIntegerOverflow
? (keyPath, value) => handleIntegerOverflow(methodName, keyPath, value)
: undefined,
) as unknown[],
responseTransformer: <T>(rawResponse: unknown, methodName: string): T =>
patchResponseForSolanaLabsRpc(rawResponse, methodName as keyof SolanaRpcMethods),
});
}

Expand Down
2 changes: 1 addition & 1 deletion packages/rpc-transport/src/__tests__/json-rpc-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe('JSON-RPC 2.0', () => {
expect.assertions(1);
(makeHttpRequest as jest.Mock).mockResolvedValueOnce({ result: 123 });
await rpc.someMethod().send();
expect(responseTransformer).toHaveBeenCalledWith(123);
expect(responseTransformer).toHaveBeenCalledWith(123, 'someMethod');
});
it('returns the processed response', async () => {
expect.assertions(1);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { IRpcApi } from '../../json-rpc-types';
import { IRpcApiMethods } from '../api-types';
import { createJsonRpcApi } from '../methods/methods-api';

type NftCollectionDetailsApiResponse = Readonly<{
address: string;
circulatingSupply: number;
description: string;
erc721: boolean;
erc1155: boolean;
genesisBlock: string;
genesisTransaction: string;
name: string;
totalSupply: number;
}>;

interface NftCollectionDetailsApi extends IRpcApiMethods {
qn_fetchNFTCollectionDetails(args: { contracts: string[] }): NftCollectionDetailsApiResponse;
}

type QuickNodeRpcMethods = NftCollectionDetailsApi;

createJsonRpcApi<QuickNodeRpcMethods>() satisfies IRpcApi<QuickNodeRpcMethods>;
36 changes: 36 additions & 0 deletions packages/rpc-transport/src/apis/methods/methods-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { IRpcApi, RpcRequest } from '../../json-rpc-types';
import { IRpcApiMethods, RpcApiConfig } from '../api-types';

export function createJsonRpcApi<TRpcMethods extends IRpcApiMethods>(config?: RpcApiConfig): IRpcApi<TRpcMethods> {
return new Proxy({} as IRpcApi<TRpcMethods>, {
defineProperty() {
return false;
},
deleteProperty() {
return false;
},
get<TMethodName extends keyof IRpcApi<TRpcMethods>>(
...args: Parameters<NonNullable<ProxyHandler<IRpcApi<TRpcMethods>>['get']>>
) {
const [_, p] = args;
const methodName = p.toString() as keyof TRpcMethods as string;
return function (
...rawParams: Parameters<
TRpcMethods[TMethodName] extends CallableFunction ? TRpcMethods[TMethodName] : never
>
): RpcRequest<ReturnType<TRpcMethods[TMethodName]>> {
const params = config?.parametersTransformer
? config?.parametersTransformer(rawParams, methodName)
: rawParams;
const responseTransformer = config?.responseTransformer
? config?.responseTransformer<ReturnType<TRpcMethods[TMethodName]>>
: (rawResponse: unknown) => rawResponse as ReturnType<TRpcMethods[TMethodName]>;
return {
methodName,
params,
responseTransformer,
};
};
},
});
}
1 change: 1 addition & 0 deletions packages/rpc-transport/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './apis/api-types';
export * from './apis/methods/methods-api';
export * from './json-rpc';
export type { SolanaJsonRpcErrorCode } from './json-rpc-errors';
export * from './json-rpc-subscription';
Expand Down
2 changes: 1 addition & 1 deletion packages/rpc-transport/src/json-rpc-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export type RpcSubscriptionConfig<TRpcMethods> = Readonly<{
export type RpcRequest<TResponse> = {
methodName: string;
params: unknown[];
responseTransformer?: (response: unknown) => TResponse;
responseTransformer?: (response: unknown, methodName: string) => TResponse;
};
export type RpcSubscription<TResponse> = {
params: unknown[];
Expand Down
4 changes: 3 additions & 1 deletion packages/rpc-transport/src/json-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ function createPendingRpcRequest<TRpcMethods, TResponse>(
if ('error' in response) {
throw new SolanaJsonRpcError(response.error);
} else {
return (responseTransformer ? responseTransformer(response.result) : response.result) as TResponse;
return (
responseTransformer ? responseTransformer(response.result, methodName) : response.result
) as TResponse;
}
},
};
Expand Down

0 comments on commit 1e2106f

Please sign in to comment.