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

Commit

Permalink
RPC: Remove JSON RPC specific behaviour from rpc-spec, move into defa…
Browse files Browse the repository at this point in the history
…ult transformer (#2950)

* Remove JSON RPC specific behaviour from rpc-spec, move into default transformer

- Update the default RPC transformer to unpack the `result` property
- Remove JSON RPC specific result and error fields from rpc-spec
- Add the JSON RPC error handling to the default transformer

* Add changeset
  • Loading branch information
mcintyre94 authored Jul 18, 2024
1 parent 94157af commit 29821df
Show file tree
Hide file tree
Showing 11 changed files with 59 additions and 35 deletions.
6 changes: 6 additions & 0 deletions .changeset/metal-bees-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@solana/rpc-transformers': patch
'@solana/rpc-spec': patch
---

Refactor rpc-spec to remove requirement for transports to implement parts of JSON RPC spec
1 change: 0 additions & 1 deletion packages/rpc-spec/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@
"maintained node versions"
],
"dependencies": {
"@solana/errors": "workspace:*",
"@solana/rpc-spec-types": "workspace:*"
},
"peerDependencies": {
Expand Down
16 changes: 6 additions & 10 deletions packages/rpc-spec/src/__tests__/rpc-test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { SOLANA_ERROR__JSON_RPC__PARSE_ERROR, SolanaError } from '@solana/errors';
import { createRpcMessage } from '@solana/rpc-spec-types';

import { createRpc, Rpc } from '../rpc';
Expand Down Expand Up @@ -35,19 +34,16 @@ describe('JSON-RPC 2.0', () => {
});
it('returns results from the transport', async () => {
expect.assertions(1);
(makeHttpRequest as jest.Mock).mockResolvedValueOnce({ result: 123 });
(makeHttpRequest as jest.Mock).mockResolvedValueOnce(123);
const result = await rpc.someMethod().send();
expect(result).toBe(123);
});
it('throws errors from the transport', async () => {
expect.assertions(1);
(makeHttpRequest as jest.Mock).mockResolvedValueOnce({
error: { code: SOLANA_ERROR__JSON_RPC__PARSE_ERROR, message: 'o no' },
});
const transportError = new Error('o no');
(makeHttpRequest as jest.Mock).mockRejectedValueOnce(transportError);
const sendPromise = rpc.someMethod().send();
await expect(sendPromise).rejects.toThrow(
new SolanaError(SOLANA_ERROR__JSON_RPC__PARSE_ERROR, { __serverMessage: 'o no' }),
);
await expect(sendPromise).rejects.toThrow(transportError);
});
describe('when calling a method having a concrete implementation', () => {
let rpc: Rpc<TestRpcMethods>;
Expand Down Expand Up @@ -94,13 +90,13 @@ 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({ result: 123 });
(makeHttpRequest as jest.Mock).mockResolvedValueOnce(123);
await rpc.someMethod().send();
expect(responseTransformer).toHaveBeenCalledWith(123, 'someMethod');
});
it('returns the processed response', async () => {
expect.assertions(1);
(makeHttpRequest as jest.Mock).mockResolvedValueOnce({ result: 123 });
(makeHttpRequest as jest.Mock).mockResolvedValueOnce(123);
const result = await rpc.someMethod().send();
expect(result).toBe('123 processed response');
});
Expand Down
2 changes: 1 addition & 1 deletion packages/rpc-spec/src/rpc-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { RpcRequest } from './rpc-request';

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

export type RpcApi<TRpcMethods> = {
Expand Down
8 changes: 1 addition & 7 deletions packages/rpc-spec/src/rpc.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { getSolanaErrorFromJsonRpcError } from '@solana/errors';
import {
Callable,
createRpcMessage,
Expand Down Expand Up @@ -74,12 +73,7 @@ function createPendingRpcRequest<TRpcMethods, TRpcTransport extends RpcTransport
payload,
signal: options?.abortSignal,
});
if ('error' in response) {
throw getSolanaErrorFromJsonRpcError(response.error);
}
return (
responseTransformer ? responseTransformer(response.result, methodName) : response.result
) as TResponse;
return (responseTransformer ? responseTransformer(response, methodName) : response) as TResponse;
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ describe('JSON-RPC 2.0 Subscriptions', () => {
.thingNotifications()
.subscribe({ abortSignal: new AbortController().signal });
const iterator = thingNotifications[Symbol.asyncIterator]();
await expect(iterator.next()).resolves.toHaveProperty('value', 456);
await expect(iterator.next()).resolves.toHaveProperty('value', { result: 456, subscription: 42 });
});
it.each([null, undefined])(
'fatals when the subscription id returned from the server is `%s`',
Expand Down Expand Up @@ -324,7 +324,7 @@ describe('JSON-RPC 2.0 Subscriptions', () => {
let responseTransformer: jest.Mock;
let rpc: RpcSubscriptions<TestRpcSubscriptionNotifications>;
beforeEach(() => {
responseTransformer = jest.fn(response => `${response} processed response`);
responseTransformer = jest.fn(response => `${response.result} processed response`);
rpc = createSubscriptionRpc({
api: {
thingNotifications(...params: unknown[]): RpcSubscriptionsRequest<unknown> {
Expand All @@ -349,7 +349,7 @@ describe('JSON-RPC 2.0 Subscriptions', () => {
.thingNotifications()
.subscribe({ abortSignal: new AbortController().signal });
await thingNotifications[Symbol.asyncIterator]().next();
expect(responseTransformer).toHaveBeenCalledWith(123, 'thingSubscribe');
expect(responseTransformer).toHaveBeenCalledWith({ result: 123, subscription: 42 }, 'thingSubscribe');
});
it('returns the processed response', async () => {
expect.assertions(1);
Expand Down
2 changes: 1 addition & 1 deletion packages/rpc-subscriptions-spec/src/rpc-subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ function createPendingRpcSubscription<
if (!('params' in message) || message.params.subscription !== subscriptionId) {
continue;
}
const notification = message.params.result as TNotification;
const notification = message.params as TNotification;
yield responseTransformer
? responseTransformer(notification, subscribeMethodName)
: notification;
Expand Down
5 changes: 3 additions & 2 deletions packages/rpc-transformers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@
"maintained node versions"
],
"dependencies": {
"@solana/errors": "workspace:*",
"@solana/functional": "workspace:*",
"@solana/rpc-types": "workspace:*",
"@solana/rpc-spec": "workspace:*",
"@solana/rpc-subscriptions-spec": "workspace:*"
"@solana/rpc-subscriptions-spec": "workspace:*",
"@solana/rpc-types": "workspace:*"
},
"peerDependencies": {
"typescript": ">=5"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { SOLANA_ERROR__JSON_RPC__PARSE_ERROR, SolanaError } from '@solana/errors';

import { getDefaultResponseTransformerForSolanaRpc } from '../response-transformer';
import { KEYPATH_WILDCARD } from '../tree-traversal';

describe('getDefaultResponseTransformerForSolanaRpc', () => {
describe('given an array as input', () => {
const input = [10, 10n, '10', ['10', [10n, 10], 10]] as const;
const response = { result: input };
it('casts the numbers in the array to a `bigint`, recursively', () => {
const transformer = getDefaultResponseTransformerForSolanaRpc();
expect(transformer(input)).toStrictEqual([
expect(transformer(response)).toStrictEqual([
BigInt(input[0]),
input[1],
input[2],
Expand All @@ -16,9 +19,11 @@ describe('getDefaultResponseTransformerForSolanaRpc', () => {
});
describe('given an object as input', () => {
const input = { a: 10, b: 10n, c: { c1: '10', c2: 10 } } as const;
const response = { result: input };

it('casts the numbers in the object to `bigints`, recursively', () => {
const transformer = getDefaultResponseTransformerForSolanaRpc();
expect(transformer(input)).toStrictEqual({
expect(transformer(response)).toStrictEqual({
a: BigInt(input.a),
b: input.b,
c: { c1: input.c.c1, c2: BigInt(input.c.c2) },
Expand All @@ -39,8 +44,21 @@ describe('getDefaultResponseTransformerForSolanaRpc', () => {
const transformer = getDefaultResponseTransformerForSolanaRpc({
allowedNumericKeyPaths: { getFoo: allowedKeyPaths },
});
expect(transformer(input, 'getFoo')).toStrictEqual(expectation);
const response = { result: input };
expect(transformer(response, 'getFoo')).toStrictEqual(expectation);
},
);
});
describe('given a JSON RPC error as input', () => {
const input = {
error: { code: SOLANA_ERROR__JSON_RPC__PARSE_ERROR, message: 'o no' },
};

it('throws it as a SolanaError', () => {
const transformer = getDefaultResponseTransformerForSolanaRpc();
expect(() => transformer(input)).toThrow(
new SolanaError(SOLANA_ERROR__JSON_RPC__PARSE_ERROR, { __serverMessage: 'o no' }),
);
});
});
});
18 changes: 14 additions & 4 deletions packages/rpc-transformers/src/response-transformer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { getSolanaErrorFromJsonRpcError } from '@solana/errors';
import { RpcApiConfig } from '@solana/rpc-spec';

import { AllowedNumericKeypaths } from './response-transformer-allowed-numeric-values';
import { getBigIntUpcastVisitor } from './response-transformer-bigint-upcast';
import { getTreeWalker } from './tree-traversal';
Expand All @@ -6,14 +9,21 @@ export type ResponseTransformerConfig<TApi> = Readonly<{
allowedNumericKeyPaths?: AllowedNumericKeypaths<TApi>;
}>;

export function getDefaultResponseTransformerForSolanaRpc<TApi>(config?: ResponseTransformerConfig<TApi>) {
return <T>(rawResponse: unknown, methodName?: keyof TApi): T => {
type JsonRpcResponse = { error: Parameters<typeof getSolanaErrorFromJsonRpcError>[0] } | { result: unknown };

export function getDefaultResponseTransformerForSolanaRpc<TApi>(
config?: ResponseTransformerConfig<TApi>,
): NonNullable<RpcApiConfig['responseTransformer']> {
return (<T>(rawResponse: JsonRpcResponse, methodName?: keyof TApi): T => {
if ('error' in rawResponse) {
throw getSolanaErrorFromJsonRpcError(rawResponse.error);
}
const keyPaths =
config?.allowedNumericKeyPaths && methodName ? config.allowedNumericKeyPaths[methodName] : undefined;
const traverse = getTreeWalker([getBigIntUpcastVisitor(keyPaths ?? [])]);
const initialState = {
keyPath: [],
};
return traverse(rawResponse, initialState) as T;
};
return traverse(rawResponse.result, initialState) as T;
}) as NonNullable<RpcApiConfig['responseTransformer']>;
}
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 29821df

Please sign in to comment.