Skip to content

Commit

Permalink
Add createHttpTransportForSolanaRpc function
Browse files Browse the repository at this point in the history
  • Loading branch information
lorisleiva committed Sep 3, 2024
1 parent cdf7109 commit 674b13b
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/forty-ravens-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solana/rpc-transport-http': patch
---

Add new `createHttpTransportForSolanaRpc` function that creates a new HTTP transport specific to the Solana RPC API. This transport uses custom JSON parsing and stringifying strategies on both the request and response of Solana RPC API requests in order to prevents loss of precision for large integers.
8 changes: 8 additions & 0 deletions packages/rpc-transport-http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ An optional function that takes the request payload and converts it to a JSON st

A string representing the target endpoint. In Node, it must be an absolute URL using the `http` or `https` protocol.

### `createHttpTransportForSolanaRpc()`

Creates an `RpcTransport` that uses JSON HTTP requests — much like the `createHttpTransport` function — except that it also uses custom `toJson` and `fromJson` functions in order to allow `bigint` values to be serialized and deserialized correctly over the wire.

Since this is something specific to the Solana RPC API, these custom JSON functions are only triggered when the request is recognized as a Solana RPC request. Normal RPC APIs should aim to wrap their `bigint` values — e.g. `u64` or `i64` — in special value objects that represent the number as a string to avoid numerical values going above `Number.MAX_SAFE_INTEGER`.

It has the same configuration options as `createHttpTransport`, but without the `fromJson` and `toJson` options.

## Augmenting Transports

Using this core transport, you can implement specialized functionality for leveraging multiple transports, attempting/handling retries, and more.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { RpcTransport } from '@solana/rpc-spec';

const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
const MAX_SAFE_INTEGER_PLUS_ONE = BigInt(Number.MAX_SAFE_INTEGER) + 1n;

describe('createHttpTransportForSolanaRpc', () => {
let fetchSpy: jest.SpyInstance;
let makeHttpRequest: RpcTransport;
beforeEach(async () => {
await jest.isolateModulesAsync(async () => {
fetchSpy = jest.spyOn(globalThis, 'fetch');
const { createHttpTransportForSolanaRpc } =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
await import('../http-transport-for-solana-rpc');
makeHttpRequest = createHttpTransportForSolanaRpc({ url: 'http://localhost' });
});
});
describe('when the request is from the Solana RPC API', () => {
it('passes all bigints as large numerical values in the request body', async () => {
expect.assertions(1);
fetchSpy.mockResolvedValue({ ok: true, text: () => `{"ok":true}` });
await makeHttpRequest({
methodName: 'getBalance',
params: {
numbersInString: 'He said: "1, 2, 3, Soleil!"',
safeNumber: MAX_SAFE_INTEGER,
unsafeNumber: MAX_SAFE_INTEGER_PLUS_ONE,
},
});
expect(fetchSpy).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
body: expect.stringContaining(
`"params":{` +
`"numbersInString":"He said: \\"1, 2, 3, Soleil!\\"",` +
`"safeNumber":${MAX_SAFE_INTEGER},` +
`"unsafeNumber":${MAX_SAFE_INTEGER_PLUS_ONE}}`,
),
}),
);
});
it('gets all integers as bigints within the response', async () => {
expect.assertions(1);
fetchSpy.mockResolvedValue({
ok: true,
text: () =>
`{"safeNumber": ${MAX_SAFE_INTEGER}, ` +
`"unsafeNumber": ${MAX_SAFE_INTEGER_PLUS_ONE}, ` +
`"numbersInString": "He said: \\"1, 2, 3, Soleil!\\""}`,
});
const requestPromise = makeHttpRequest({ methodName: 'getBalance', params: ['1234..5678'] });
await expect(requestPromise).resolves.toStrictEqual({
numbersInString: 'He said: "1, 2, 3, Soleil!"',
safeNumber: MAX_SAFE_INTEGER,
unsafeNumber: MAX_SAFE_INTEGER_PLUS_ONE,
});
});
});
describe('when the request is not from the Solana RPC API', () => {
it('fails to stringify bigints in requests', async () => {
expect.assertions(1);
const promise = makeHttpRequest({
methodName: 'getAssetsByOwner',
params: [MAX_SAFE_INTEGER_PLUS_ONE],
});
await expect(promise).rejects.toThrow(new TypeError('Do not know how to serialize a BigInt'));
});
it('downcasts bigints to numbers in responses', async () => {
expect.assertions(1);
fetchSpy.mockResolvedValue({
ok: true,
text: () =>
`{"safeNumber": ${MAX_SAFE_INTEGER}, ` +
`"unsafeNumber": ${MAX_SAFE_INTEGER_PLUS_ONE}, ` +
`"numbersInString": "He said: \\"1, 2, 3, Soleil!\\""}`,
});
const requestPromise = makeHttpRequest({ methodName: 'getAssetsByOwner', params: ['1234..5678'] });
await expect(requestPromise).resolves.toStrictEqual({
numbersInString: 'He said: "1, 2, 3, Soleil!"',
safeNumber: Number(MAX_SAFE_INTEGER),
unsafeNumber: Number(MAX_SAFE_INTEGER_PLUS_ONE),
});
});
});
});
24 changes: 24 additions & 0 deletions packages/rpc-transport-http/src/http-transport-for-solana-rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { RpcRequest, RpcTransport } from '@solana/rpc-spec';
import type Dispatcher from 'undici-types/dispatcher';

import { createHttpTransport } from './http-transport';
import { AllowedHttpRequestHeaders } from './http-transport-headers';
import { isSolanaRequest } from './is-solana-request';
import { parseJsonWithBigInts } from './parse-json-with-bigints';
import { stringifyJsonWithBigints } from './stringify-json-with-bigints';

type Config = Readonly<{
dispatcher_NODE_ONLY?: Dispatcher;
headers?: AllowedHttpRequestHeaders;
url: string;
}>;

export function createHttpTransportForSolanaRpc(config: Config): RpcTransport {
return createHttpTransport({
...config,
fromJson: (rawResponse: string, request: RpcRequest) =>
isSolanaRequest(request) ? parseJsonWithBigInts(rawResponse) : JSON.parse(rawResponse),
toJson: (payload: unknown, request: RpcRequest) =>
isSolanaRequest(request) ? stringifyJsonWithBigints(payload) : JSON.stringify(payload),
});
}
1 change: 1 addition & 0 deletions packages/rpc-transport-http/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './http-transport';
export * from './http-transport-for-solana-rpc';

0 comments on commit 674b13b

Please sign in to comment.