diff --git a/.changeset/metal-bees-lick.md b/.changeset/metal-bees-lick.md new file mode 100644 index 00000000000..40b158dbaf4 --- /dev/null +++ b/.changeset/metal-bees-lick.md @@ -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 diff --git a/packages/rpc-spec/package.json b/packages/rpc-spec/package.json index bb32653eba3..298606321a0 100644 --- a/packages/rpc-spec/package.json +++ b/packages/rpc-spec/package.json @@ -63,7 +63,6 @@ "maintained node versions" ], "dependencies": { - "@solana/errors": "workspace:*", "@solana/rpc-spec-types": "workspace:*" }, "peerDependencies": { diff --git a/packages/rpc-spec/src/__tests__/rpc-test.ts b/packages/rpc-spec/src/__tests__/rpc-test.ts index 37f60459afc..c37b09e5be5 100644 --- a/packages/rpc-spec/src/__tests__/rpc-test.ts +++ b/packages/rpc-spec/src/__tests__/rpc-test.ts @@ -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'; @@ -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; @@ -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'); }); diff --git a/packages/rpc-spec/src/rpc-api.ts b/packages/rpc-spec/src/rpc-api.ts index 4da9c41c87b..f0ed256a00c 100644 --- a/packages/rpc-spec/src/rpc-api.ts +++ b/packages/rpc-spec/src/rpc-api.ts @@ -4,7 +4,7 @@ import { RpcRequest } from './rpc-request'; export type RpcApiConfig = Readonly<{ parametersTransformer?: (params: T, methodName: string) => unknown; - responseTransformer?: (response: unknown, methodName: string) => T; + responseTransformer?: (response: unknown, methodName?: string) => T; }>; export type RpcApi = { diff --git a/packages/rpc-spec/src/rpc.ts b/packages/rpc-spec/src/rpc.ts index 3d44e86ff0d..dec275b7f66 100644 --- a/packages/rpc-spec/src/rpc.ts +++ b/packages/rpc-spec/src/rpc.ts @@ -1,4 +1,3 @@ -import { getSolanaErrorFromJsonRpcError } from '@solana/errors'; import { Callable, createRpcMessage, @@ -74,12 +73,7 @@ function createPendingRpcRequest { .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`', @@ -324,7 +324,7 @@ describe('JSON-RPC 2.0 Subscriptions', () => { let responseTransformer: jest.Mock; let rpc: RpcSubscriptions; beforeEach(() => { - responseTransformer = jest.fn(response => `${response} processed response`); + responseTransformer = jest.fn(response => `${response.result} processed response`); rpc = createSubscriptionRpc({ api: { thingNotifications(...params: unknown[]): RpcSubscriptionsRequest { @@ -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); diff --git a/packages/rpc-subscriptions-spec/src/rpc-subscriptions.ts b/packages/rpc-subscriptions-spec/src/rpc-subscriptions.ts index 96948c0474a..694fe0df939 100644 --- a/packages/rpc-subscriptions-spec/src/rpc-subscriptions.ts +++ b/packages/rpc-subscriptions-spec/src/rpc-subscriptions.ts @@ -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; diff --git a/packages/rpc-transformers/package.json b/packages/rpc-transformers/package.json index bc77be72643..00bb8702faf 100644 --- a/packages/rpc-transformers/package.json +++ b/packages/rpc-transformers/package.json @@ -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" diff --git a/packages/rpc-transformers/src/__tests__/response-transformer-test.ts b/packages/rpc-transformers/src/__tests__/response-transformer-test.ts index e454ecdceff..0e0db01d49b 100644 --- a/packages/rpc-transformers/src/__tests__/response-transformer-test.ts +++ b/packages/rpc-transformers/src/__tests__/response-transformer-test.ts @@ -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], @@ -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) }, @@ -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' }), + ); + }); + }); }); diff --git a/packages/rpc-transformers/src/response-transformer.ts b/packages/rpc-transformers/src/response-transformer.ts index f4b961ab7d3..1911c02581e 100644 --- a/packages/rpc-transformers/src/response-transformer.ts +++ b/packages/rpc-transformers/src/response-transformer.ts @@ -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'; @@ -6,14 +9,21 @@ export type ResponseTransformerConfig = Readonly<{ allowedNumericKeyPaths?: AllowedNumericKeypaths; }>; -export function getDefaultResponseTransformerForSolanaRpc(config?: ResponseTransformerConfig) { - return (rawResponse: unknown, methodName?: keyof TApi): T => { +type JsonRpcResponse = { error: Parameters[0] } | { result: unknown }; + +export function getDefaultResponseTransformerForSolanaRpc( + config?: ResponseTransformerConfig, +): NonNullable { + return ((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; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31bc1e208ba..c2b47266a05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -912,9 +912,6 @@ importers: packages/rpc-spec: dependencies: - '@solana/errors': - specifier: workspace:* - version: link:../errors '@solana/rpc-spec-types': specifier: workspace:* version: link:../rpc-spec-types @@ -1025,6 +1022,9 @@ importers: packages/rpc-transformers: dependencies: + '@solana/errors': + specifier: workspace:* + version: link:../errors '@solana/functional': specifier: workspace:* version: link:../functional