Skip to content

Commit

Permalink
Update RPC APIs to use new types (#3150)
Browse files Browse the repository at this point in the history
This PR updates the RPC Api layer such that they use the new `RpcRequest`, `RpcResponse`, `RpcRequestTransformer` and `RpcResponseTransformer` types.

Note that I had to duplicate the implementation of `getDefaultResponseTransformerForSolanaRpc` into `getDefaultResponseTransformerForSolanaRpcSubscriptions` in order to leave RPC Subscription packages unaffected by the refactoring. These will have their own dedicated PR stack further down the line.
  • Loading branch information
lorisleiva authored Sep 1, 2024
1 parent 7b9025b commit a705413
Show file tree
Hide file tree
Showing 12 changed files with 271 additions and 113 deletions.
8 changes: 8 additions & 0 deletions .changeset/metal-spoons-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@solana/rpc-subscriptions-api': patch
'@solana/rpc-transformers': patch
'@solana/rpc-spec': patch
'@solana/rpc-api': patch
---

Make `RpcApi` use new `RpcRequestTransformer` and `RpcResponseTransformer`
8 changes: 4 additions & 4 deletions packages/rpc-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { createRpcApi, RpcApi } from '@solana/rpc-spec';
import {
AllowedNumericKeypaths,
getDefaultParamsTransformerForSolanaRpc,
getDefaultRequestTransformerForSolanaRpc,
getDefaultResponseTransformerForSolanaRpc,
innerInstructionsConfigs,
jsonParsedAccountsConfigs,
jsonParsedTokenAccountsConfigs,
KEYPATH_WILDCARD,
messageConfig,
ParamsTransformerConfig,
RequestTransformerConfig,
} from '@solana/rpc-transformers';

import { GetAccountInfoApi } from './getAccountInfo';
Expand Down Expand Up @@ -179,13 +179,13 @@ export type {
SimulateTransactionApi,
};

type Config = ParamsTransformerConfig;
type Config = RequestTransformerConfig;

export function createSolanaRpcApi<
TRpcMethods extends SolanaRpcApi | SolanaRpcApiDevnet | SolanaRpcApiMainnet | SolanaRpcApiTestnet = SolanaRpcApi,
>(config?: Config): RpcApi<TRpcMethods> {
return createRpcApi<TRpcMethods>({
parametersTransformer: getDefaultParamsTransformerForSolanaRpc(config) as (params: unknown[]) => unknown[],
requestTransformer: getDefaultRequestTransformerForSolanaRpc(config),
responseTransformer: getDefaultResponseTransformerForSolanaRpc({
allowedNumericKeyPaths: getAllowedNumericKeypaths(),
}),
Expand Down
12 changes: 6 additions & 6 deletions packages/rpc-spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,14 @@ A config object with the following properties:

Creates a JavaScript proxy that converts _any_ function call called on it to a `RpcApiRequestPlan` by:

- setting `methodName` to the name of the function called
- setting `params` to the arguments supplied to that function, optionally transformed by `config.parametersTransformer`
- setting `responseTransformer` to `config.responseTransformer` or the identity function if no such config exists
- setting `methodName` to the name of the function called, optionally transformed by `config.requestTransformer`.
- setting `params` to the arguments supplied to that function, optionally transformed by `config.requestTransformer`.
- setting `responseTransformer` to `config.responseTransformer`, if provided.

```ts
// For example, given this `RpcApi`:
const rpcApi = createRpcApi({
paramsTransformer: (...rawParams) => rawParams.reverse(),
requestTransformer: (...rawParams) => rawParams.reverse(),
responseTransformer: response => response.result,
});

Expand All @@ -140,8 +140,8 @@ rpcApi.foo('bar', { baz: 'bat' });

A config object with the following properties:

- `parametersTransformer<T>(params: T, methodName): unknown`: An optional function that maps between the shape of the arguments an RPC method was called with and the shape of the params expected by the JSON RPC server.
- `responseTransformer<T>(response, methodName): T`: An optional function that maps between the shape of the JSON RPC server response for a given method and the shape of the response expected by the `RpcApi`.
- `requestTransformer<T>(request: RpcRequest<T>): RpcRequest`: An optional function that transforms the `RpcRequest` before it is sent to the JSON RPC server.
- `responseTransformer<T>(response: RpcResponse, request: RpcRequest): RpcResponse<T>`: An optional function that transforms the `RpcResponse` before it is returned to the caller.

### `createJsonRpcResponseTransformer<T>(jsonTransformer)`

Expand Down
84 changes: 84 additions & 0 deletions packages/rpc-spec/src/__tests__/rpc-api-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import '@solana/test-matchers/toBeFrozenObject';

import { createRpcApi } from '../rpc-api';
import { RpcRequest, RpcResponse } from '../rpc-shared';

type DummyApi = {
someMethod(...args: unknown[]): unknown;
};

describe('createRpcApi', () => {
it('returns a plan containing the method name and parameters provided', () => {
// Given a dummy API.
const api = createRpcApi<DummyApi>();

// When we call a method on the API.
const plan = api.someMethod(1, 'two', { three: [4] });

// Then we expect the plan to contain the method name and the provided parameters.
expect(plan).toEqual({
methodName: 'someMethod',
params: [1, 'two', { three: [4] }],
});
});
it('applies the request transformer to the provided method name', () => {
// Given a dummy API with a request transformer that appends 'Transformed' to the method name.
const api = createRpcApi<DummyApi>({
requestTransformer: <T>(request: RpcRequest<unknown>) =>
({ ...request, methodName: `${request.methodName}Transformed` }) as RpcRequest<T>,
});

// When we call a method on the API.
const plan = api.someMethod();

// Then we expect the plan to contain the transformed method name.
expect(plan.methodName).toBe('someMethodTransformed');
});
it('applies the request transformer to the provided params', () => {
// Given a dummy API with a request transformer that doubles the provided params.
const api = createRpcApi<DummyApi>({
requestTransformer: <T>(request: RpcRequest<unknown>) =>
({ ...request, params: (request.params as number[]).map(x => x * 2) }) as RpcRequest<T>,
});

// When we call a method on the API.
const plan = api.someMethod(1, 2, 3);

// Then we expect the plan to contain the transformed params.
expect(plan.params).toEqual([2, 4, 6]);
});
it('includes the provided response transformer in the plan', () => {
// Given a dummy API with a response transformer.
const responseTransformer = <T>(response: RpcResponse<unknown>) => response as RpcResponse<T>;
const api = createRpcApi<DummyApi>({ responseTransformer });

// When we call a method on the API.
const plan = api.someMethod(1, 2, 3);

// Then we expect the plan to contain the response transformer.
expect(plan.responseTransformer).toBe(responseTransformer);
});
it('returns a frozen object', () => {
// Given a dummy API.
const api = createRpcApi<DummyApi>();

// When we call a method on the API.
const plan = api.someMethod();

// Then we expect the returned plan to be frozen.
expect(plan).toBeFrozenObject();
});
it('also returns a frozen object with a request transformer', () => {
// Given a dummy API with a request transformer.
const api = createRpcApi<DummyApi>({
requestTransformer: <T>(request: RpcRequest<unknown>) =>
({ ...request, methodName: 'transformed' }) as RpcRequest<T>,
});

// When we call a method on the API.
const plan = api.someMethod();

// Then we expect the returned plan to be frozen.
expect(plan).toBeFrozenObject();
});
});
9 changes: 5 additions & 4 deletions packages/rpc-spec/src/__tests__/rpc-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createRpcMessage } from '@solana/rpc-spec-types';

import { createRpc, Rpc } from '../rpc';
import { RpcApi, RpcApiRequestPlan } from '../rpc-api';
import { RpcResponse } from '../rpc-shared';
import { createJsonRpcResponseTransformer, RpcResponse } from '../rpc-shared';
import { RpcTransport } from '../rpc-transport';

interface TestRpcMethods {
Expand Down Expand Up @@ -81,7 +81,7 @@ describe('JSON-RPC 2.0', () => {
let responseTransformer: jest.Mock;
let rpc: Rpc<TestRpcMethods>;
beforeEach(() => {
responseTransformer = jest.fn(response => `${response} processed response`);
responseTransformer = jest.fn(createJsonRpcResponseTransformer(json => `${json} processed response`));
rpc = createRpc({
api: {
someMethod(...params: unknown[]): RpcApiRequestPlan<unknown> {
Expand All @@ -97,9 +97,10 @@ describe('JSON-RPC 2.0', () => {
});
it('calls the response transformer with the response from the JSON-RPC 2.0 endpoint', async () => {
expect.assertions(1);
(makeHttpRequest as jest.Mock).mockResolvedValueOnce(createMockResponse(123));
const rawResponse = createMockResponse(123);
(makeHttpRequest as jest.Mock).mockResolvedValueOnce(rawResponse);
await rpc.someMethod().send();
expect(responseTransformer).toHaveBeenCalledWith(123, 'someMethod');
expect(responseTransformer).toHaveBeenCalledWith(rawResponse, { methodName: 'someMethod', params: [] });
});
it('returns the processed response', async () => {
expect.assertions(1);
Expand Down
33 changes: 16 additions & 17 deletions packages/rpc-spec/src/rpc-api.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Callable } from '@solana/rpc-spec-types';

import { RpcRequest, RpcRequestTransformer, RpcResponseTransformer, RpcResponseTransformerFor } from './rpc-shared';

export type RpcApiConfig = Readonly<{
parametersTransformer?: <T extends unknown[]>(params: T, methodName: string) => unknown;
responseTransformer?: <T>(response: unknown, methodName?: string) => T;
requestTransformer?: RpcRequestTransformer;
responseTransformer?: RpcResponseTransformer;
}>;

export type RpcApiRequestPlan<TResponse> = {
methodName: string;
params: unknown;
responseTransformer?: (response: unknown, methodName: string) => TResponse;
export type RpcApiRequestPlan<TResponse> = RpcRequest & {
responseTransformer?: RpcResponseTransformerFor<TResponse>;
};

export type RpcApi<TRpcMethods> = {
Expand Down Expand Up @@ -43,17 +43,16 @@ export function createRpcApi<TRpcMethods extends RpcApiMethods>(config?: RpcApiC
TRpcMethods[TMethodName] extends CallableFunction ? TRpcMethods[TMethodName] : never
>
): RpcApiRequestPlan<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,
};
const rawRequest = { methodName, params: rawParams };
const request = config?.requestTransformer
? config?.requestTransformer(Object.freeze(rawRequest))
: rawRequest;
return Object.freeze({
...request,
...(config?.responseTransformer
? { responseTransformer: config.responseTransformer<ReturnType<TRpcMethods[TMethodName]>> }
: {}),
});
};
},
});
Expand Down
7 changes: 4 additions & 3 deletions packages/rpc-spec/src/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,13 @@ function createPendingRpcRequest<TRpcMethods, TRpcTransport extends RpcTransport
return {
async send(options?: RpcSendOptions): Promise<TResponse> {
const { methodName, params, responseTransformer } = pendingRequest;
const response = await rpcConfig.transport<TResponse>({
const request = Object.freeze({ methodName, params });
const rawResponse = await rpcConfig.transport<TResponse>({
payload: createRpcMessage(methodName, params),
signal: options?.abortSignal,
});
const responseData = await response.json();
return responseTransformer ? responseTransformer(responseData, methodName) : responseData;
const response = responseTransformer ? responseTransformer(rawResponse, request) : rawResponse;
return await response.json();
},
};
}
16 changes: 10 additions & 6 deletions packages/rpc-subscriptions-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import {
} from '@solana/rpc-subscriptions-spec';
import {
AllowedNumericKeypaths,
getDefaultParamsTransformerForSolanaRpc,
getDefaultResponseTransformerForSolanaRpc,
getDefaultRequestTransformerForSolanaRpc,
getDefaultResponseTransformerForSolanaRpcSubscriptions,
jsonParsedAccountsConfigs,
KEYPATH_WILDCARD,
ParamsTransformerConfig,
RequestTransformerConfig,
} from '@solana/rpc-transformers';

import { AccountNotificationsApi } from './account-notifications';
Expand Down Expand Up @@ -44,14 +44,18 @@ export type {
VoteNotificationsApi,
};

type Config = ParamsTransformerConfig;
type Config = RequestTransformerConfig;

function createSolanaRpcSubscriptionsApi_INTERNAL<TApi extends RpcSubscriptionsApiMethods>(
config?: Config,
): RpcSubscriptionsApi<TApi> {
return createRpcSubscriptionsApi<TApi>({
parametersTransformer: getDefaultParamsTransformerForSolanaRpc(config) as (params: unknown[]) => unknown[],
responseTransformer: getDefaultResponseTransformerForSolanaRpc({
// TODO(loris): Replace with request transformer.
parametersTransformer: <T extends unknown[]>(params: T, notificationName: string) => {
return getDefaultRequestTransformerForSolanaRpc(config)({ methodName: notificationName, params })
.params as unknown[];
},
responseTransformer: getDefaultResponseTransformerForSolanaRpcSubscriptions({
allowedNumericKeyPaths: getAllowedNumericKeypaths(),
}),
subscribeNotificationNameTransformer: (notificationName: string) =>
Expand Down
Loading

0 comments on commit a705413

Please sign in to comment.