diff --git a/packages/rpc-spec/README.md b/packages/rpc-spec/README.md index 9599aad5309e..affac1bd3dc9 100644 --- a/packages/rpc-spec/README.md +++ b/packages/rpc-spec/README.md @@ -41,6 +41,32 @@ Calling the `send(options)` method on a `PendingRpcRequest` will trigger the req An object that exposes all of the functions described by `TRpcMethods`, and fulfils them using `TRpcTransport`. Calling each method returns a `PendingRpcRequest` where `TResponse` is that method's response type. +### `RpcRequest` + +An object that describes the elements of a JSON RPC request. It consists of the following properties: + +- `methodName`: The name of the JSON RPC method to be called. +- `params`: The parameters to be passed to the JSON RPC method. + +### `RpcRequestTransformer` + +A function that accepts an `RpcRequest` and returns another `RpcRequest`. This allows the `RpcApi` to transform the request before it is sent to the JSON RPC server. + +### `RpcResponse` + +An object that represents the response from a JSON RPC server. It contains two asynchronous methods that can be used to access the response data: + +- `await response.json()`: Returns the data as a JSON object. +- `await response.text()`: Returns the data, unparsed, as a JSON string. + +This allows the `RpcApi` to decide whether they want the parsed JSON object or the raw JSON string. Ultimately, the `json` method will be used by the `Rpc` to provide the final response to the caller. + +### `RpcResponseTransformer` + +A function that accepts an `RpcResponse` and returns another `RpcResponse`. This allows the `RpcApi` to transform the response before it is returned to the caller. + +Note that a `RpcResponseTransformerFor` generic function type is also available to ensure the response data returned by the transformer matches the expected type `T`. + ### `RpcApi` For each of `TRpcMethods` this object exposes a method with the same name that maps between its input arguments and a `RpcApiRequestPlan` that describes how to prepare a JSON RPC request to fetch `TResponse`. @@ -51,7 +77,7 @@ This is a marker interface that all RPC method definitions must extend to be acc ### `RpcApiRequestPlan` -This type describes how a particular request should be issued to the JSON RPC server. Given a function that was called on a `Rpc`, this object gives you the opportunity to: +This type allows an `RpcApi` to describe how a particular request should be issued to the JSON RPC server. Given a function that was called on a `Rpc`, this object gives you the opportunity to: - customize the JSON RPC method name in the case that it's different than the name of that function - define the shape of the JSON RPC params in case they are different than the arguments provided to that function @@ -116,3 +142,17 @@ A config object with the following properties: - `parametersTransformer(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(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`. + +### `createJsonRpcResponseTransformer(jsonTransformer)` + +Creates an `RpcResponseTransformerFor` function from a function that transforms any JSON value to a value of type `T` by wrapping it in a `json` async function. + +```ts +const getResultTransformer = createJsonRpcResponseTransformer((json: unknown) => { + return (json as { result: TResponse }).result; +}); +``` + +#### Arguments + +- `jsonTransformer: (json: unknown, request: RpcRequest) => T`: A function that transforms an unknown JSON value to a value of type `T`. diff --git a/packages/rpc-spec/src/__tests__/rpc-shared-test.ts b/packages/rpc-spec/src/__tests__/rpc-shared-test.ts new file mode 100644 index 000000000000..cdb580cc00da --- /dev/null +++ b/packages/rpc-spec/src/__tests__/rpc-shared-test.ts @@ -0,0 +1,56 @@ +import '@solana/test-matchers/toBeFrozenObject'; + +import { createJsonRpcResponseTransformer, RpcRequest, RpcResponse } from '../rpc-shared'; + +describe('createJsonRpcResponseTransformer', () => { + it('can alter the value of the json Promise', async () => { + expect.assertions(1); + + // Given a request and a response that returns a number. + const request = { methodName: 'someMethod', params: [123] }; + const response = { + json: () => Promise.resolve(123), + text: () => Promise.resolve('123'), + }; + + // When we create a JSON transformer that doubles the number. + const transformer = createJsonRpcResponseTransformer((json: unknown) => (json as number) * 2); + + // Then the transformed response should return the doubled number. + const transformedResponse = transformer(response, request); + transformedResponse satisfies RpcResponse; + await expect(transformedResponse.json()).resolves.toBe(246); + }); + + it('does not alter the value of the text Promise', async () => { + expect.assertions(1); + + // Given a request and a response that returns a number. + const request = { methodName: 'someMethod', params: [123] }; + const response = { + json: () => Promise.resolve(123), + text: () => Promise.resolve('123'), + }; + + // When we create a JSON transformer that doubles the number. + const transformer = createJsonRpcResponseTransformer((json: unknown) => (json as number) * 2); + + // Then the text should function should return the original string. + const transformedResponse = transformer(response, request); + await expect(transformedResponse.text()).resolves.toBe('123'); + }); + + it('returns a frozen object as the Reponse', () => { + // Given any response. + const response = { + json: () => Promise.resolve(123), + text: () => Promise.resolve('123'), + }; + + // When we pass it through a JSON transformer. + const transformedResponse = createJsonRpcResponseTransformer(x => x)(response, {} as RpcRequest); + + // Then we expect the transformed response to be frozen. + expect(transformedResponse).toBeFrozenObject(); + }); +}); diff --git a/packages/rpc-spec/src/index.ts b/packages/rpc-spec/src/index.ts index 17e751b30e52..ebf563096ad0 100644 --- a/packages/rpc-spec/src/index.ts +++ b/packages/rpc-spec/src/index.ts @@ -1,4 +1,5 @@ export * from './rpc'; export * from './rpc-api'; export * from './rpc-request'; +export * from './rpc-shared'; export * from './rpc-transport'; diff --git a/packages/rpc-spec/src/rpc-shared.ts b/packages/rpc-spec/src/rpc-shared.ts new file mode 100644 index 000000000000..96a9b8447fdf --- /dev/null +++ b/packages/rpc-spec/src/rpc-shared.ts @@ -0,0 +1,35 @@ +export type RpcRequest = { + readonly methodName: string; + readonly params: TParams; +}; + +export type RpcResponse = { + readonly json: () => Promise; + readonly text: () => Promise; +}; + +export type RpcRequestTransformer = { + (request: RpcRequest): RpcRequest; +}; + +export type RpcResponseTransformer = { + (response: RpcResponse, request: RpcRequest): RpcResponse; +}; + +export type RpcResponseTransformerFor = { + (response: RpcResponse, request: RpcRequest): RpcResponse; +}; + +export function createJsonRpcResponseTransformer( + jsonTransformer: (json: unknown, request: RpcRequest) => TResponse, +): RpcResponseTransformerFor { + return function (response: RpcResponse, request: RpcRequest): RpcResponse { + return Object.freeze({ + ...response, + json: async () => { + const json = await response.json(); + return jsonTransformer(json, request); + }, + }); + }; +}