Skip to content

Document @solana/rpc-transport-http with TypeDoc #361

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/rpc-transport-http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

# @solana/rpc-transport-http

This package allows developers to create custom RPC transports. With this library, one can implement highly specialized functionality for leveraging multiple transports, attempting/handling retries, and more.
This package allows developers to create custom RPC transports. Using these primitives, developers can create custom transports that perform transforms on messages sent and received, attempt retries, and implement routing strategies between multiple transports.

## Functions

Expand Down Expand Up @@ -109,7 +109,7 @@ A string representing the target endpoint. In Node, it must be an absolute URL u

### `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.
Creates a `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`.

Expand Down
101 changes: 101 additions & 0 deletions packages/rpc-transport-http/src/http-transport-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { RpcResponse } from '@solana/rpc-spec-types';
import { Dispatcher } from 'undici-types';

import { AllowedHttpRequestHeaders } from './http-transport-headers';

export type HttpTransportConfig = Readonly<{
/**
* In Node environments you can tune how requests are dispatched to the network. Use this config
* parameter to install a
* [`undici.Dispatcher`](https://undici.nodejs.org/#/docs/api/Agent) in your transport.
*
* @example
* ```ts
* import { createHttpTransport } from '@solana/rpc-transport-http';
* import { Agent, BalancedPool } from 'undici';
*
* // Create a dispatcher that, when called with a special URL, creates a round-robin pool of RPCs.
* const dispatcher = new Agent({
* factory(origin, opts) {
* if (origin === 'https://mypool') {
* const upstreams = [
* 'https://api.mainnet-beta.solana.com',
* 'https://mainnet.helius-rpc.com',
* 'https://several-neat-iguana.quiknode.pro',
* ];
* return new BalancedPool(upstreams, {
* ...opts,
* bodyTimeout: 60e3,
* headersTimeout: 5e3,
* keepAliveTimeout: 19e3,
* });
* } else {
* return new Pool(origin, opts);
* }
* },
* });
* const transport = createHttpTransport({
* dispatcher_NODE_ONLY: dispatcher,
* url: 'https://mypool',
* });
* let id = 0;
* const balances = await Promise.allSettled(
* accounts.map(async account => {
* const response = await transport({
* payload: {
* id: ++id,
* jsonrpc: '2.0',
* method: 'getBalance',
* params: [account],
* },
* });
* return await response.json();
* }),
* );
* ```
*/
dispatcher_NODE_ONLY?: Dispatcher;
/**
* An optional function that takes the response as a JSON string and converts it to a JSON
* value.
*
* The request payload is also provided as a second argument.
*
* @defaultValue When not provided, the JSON value will be accessed via the `response.json()`
* method of the fetch API.
*/
fromJson?: (rawResponse: string, payload: unknown) => RpcResponse;
/**
* An object of headers to set on the request.
*
* Avoid
* [forbidden headers](https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name).
* Additionally, the headers `Accept`, `Content-Length`, and `Content-Type` are disallowed.
*
* @example
* ```ts
* import { createHttpTransport } from '@solana/rpc-transport-http';
*
* const transport = createHttpTransport({
* headers: {
* // Authorize with the RPC using a bearer token
* Authorization: `Bearer ${process.env.RPC_AUTH_TOKEN}`,
* },
* url: 'https://several-neat-iguana.quiknode.pro',
* });
* ```
*/
headers?: AllowedHttpRequestHeaders;
/**
* An optional function that takes the request payload and converts it to a JSON string.
*
* @defaultValue When not provided, `JSON.stringify` will be used.
*/
toJson?: (payload: unknown) => string;
/**
* A string representing the target endpoint.
*
* In Node, it must be an absolute URL using the `http` or `https` protocol.
*/
url: string;
}>;
23 changes: 16 additions & 7 deletions packages/rpc-transport-http/src/http-transport-for-solana-rpc.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { RpcTransport } from '@solana/rpc-spec';
import { parseJsonWithBigInts, stringifyJsonWithBigints } from '@solana/rpc-spec-types';
import type Dispatcher from 'undici-types/dispatcher';

import { createHttpTransport } from './http-transport';
import { AllowedHttpRequestHeaders } from './http-transport-headers';
import { HttpTransportConfig } from './http-transport-config';
import { isSolanaRequest } from './is-solana-request';

type Config = Readonly<{
dispatcher_NODE_ONLY?: Dispatcher;
headers?: AllowedHttpRequestHeaders;
url: string;
}>;
type Config = Pick<HttpTransportConfig, 'dispatcher_NODE_ONLY' | 'headers' | 'url'>;

/**
* Creates a {@link RpcTransport} that uses JSON HTTP requests — much like the
* {@link 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 {@link createHttpTransport}, but without the `fromJson`
* and `toJson` options.
*/
export function createHttpTransportForSolanaRpc(config: Config): RpcTransport {
return createHttpTransport({
...config,
Expand Down
16 changes: 6 additions & 10 deletions packages/rpc-transport-http/src/http-transport-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@ export type AllowedHttpRequestHeaders = Readonly<
type DisallowedHeaders = 'Accept' | 'Content-Length' | 'Content-Type' | 'Solana-Client';
type ForbiddenHeaders =
| 'Accept-Charset'
/**
* Though technically forbidden in non-Node environments, we don't have a way to target
* TypeScript types depending on which platform you are authoring for. `Accept-Encoding` is
* therefore omitted from the forbidden headers type, but is still a runtime error in dev mode
* when supplied in a non-Node context.
*/
// Though technically forbidden in non-Node environments, we don't have a way to target
// TypeScript types depending on which platform you are authoring for. `Accept-Encoding` is
// therefore omitted from the forbidden headers type, but is still a runtime error in dev mode
// when supplied in a non-Node context.
// | 'Accept-Encoding'
| 'Access-Control-Request-Headers'
| 'Access-Control-Request-Method'
Expand Down Expand Up @@ -100,10 +98,8 @@ export function assertIsAllowedHttpRequestHeaders(
}
}

/**
* Lowercasing header names makes it easier to override user-supplied headers, such as those defined
* in the `DisallowedHeaders` type.
*/
// Lowercasing header names makes it easier to override user-supplied headers, such as those defined
// in the `DisallowedHeaders` type.
export function normalizeHeaders<T extends Record<string, string>>(
headers: T,
): { [K in string & keyof T as Lowercase<K>]: T[K] } {
Expand Down
30 changes: 17 additions & 13 deletions packages/rpc-transport-http/src/http-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,8 @@ import type { RpcTransport } from '@solana/rpc-spec';
import type { RpcResponse } from '@solana/rpc-spec-types';
import type Dispatcher from 'undici-types/dispatcher';

import {
AllowedHttpRequestHeaders,
assertIsAllowedHttpRequestHeaders,
normalizeHeaders,
} from './http-transport-headers';

type Config = Readonly<{
dispatcher_NODE_ONLY?: Dispatcher;
fromJson?: (rawResponse: string, payload: unknown) => RpcResponse;
headers?: AllowedHttpRequestHeaders;
toJson?: (payload: unknown) => string;
url: string;
}>;
import { HttpTransportConfig as Config } from './http-transport-config';
import { assertIsAllowedHttpRequestHeaders, normalizeHeaders } from './http-transport-headers';

let didWarnDispatcherWasSuppliedInNonNodeEnvironment = false;
function warnDispatcherWasSuppliedInNonNodeEnvironment() {
Expand All @@ -31,6 +20,21 @@ function warnDispatcherWasSuppliedInNonNodeEnvironment() {
);
}

/**
* Creates a function you can use to make `POST` requests with headers suitable for sending JSON
* data to a server.
*
* @example
* ```ts
* import { createHttpTransport } from '@solana/rpc-transport-http';
*
* const transport = createHttpTransport({ url: 'https://api.mainnet-beta.solana.com' });
* const response = await transport({
* payload: { id: 1, jsonrpc: '2.0', method: 'getSlot' },
* });
* const data = await response.json();
* ```
*/
export function createHttpTransport(config: Config): RpcTransport {
if (__DEV__ && !__NODEJS__ && 'dispatcher_NODE_ONLY' in config) {
warnDispatcherWasSuppliedInNonNodeEnvironment();
Expand Down
158 changes: 158 additions & 0 deletions packages/rpc-transport-http/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,160 @@
/**
* This package allows developers to create custom RPC transports. Using these primitives,
* developers can create custom transports that perform transforms on messages sent and received,
* attempt retries, and implement routing strategies between multiple transports.
*
* ## Augmenting Transports
*
* Using this core transport, you can implement specialized functionality for leveraging multiple
* transports, attempting/handling retries, and more.
*
* ### Round Robin
*
* Here’s an example of how someone might implement a “round robin” approach to distribute requests
* to multiple transports:
*
* ```ts
* import { RpcTransport } from '@solana/rpc-spec';
* import { RpcResponse } from '@solana/rpc-spec-types';
* import { createHttpTransport } from '@solana/rpc-transport-http';
*
* // Create a transport for each RPC server
* const transports = [
* createHttpTransport({ url: 'https://mainnet-beta.my-server-1.com' }),
* createHttpTransport({ url: 'https://mainnet-beta.my-server-2.com' }),
* createHttpTransport({ url: 'https://mainnet-beta.my-server-3.com' }),
* ];
*
* // Create a wrapper transport that distributes requests to them
* let nextTransport = 0;
* async function roundRobinTransport<TResponse>(...args: Parameters<RpcTransport>): Promise<RpcResponse<TResponse>> {
* const transport = transports[nextTransport];
* nextTransport = (nextTransport + 1) % transports.length;
* return await transport(...args);
* }
* ```
*
* ### Sharding
*
* Another example of a possible customization for a transport is to shard requests
* deterministically among a set of servers. Here’s an example:
*
* Perhaps your application needs to make a large number of requests, or needs to fan request for
* different methods out to different servers. Here’s an example of an implementation that does the
* latter:
*
* ```ts
* import { RpcTransport } from '@solana/rpc-spec';
* import { RpcResponse } from '@solana/rpc-spec-types';
* import { createHttpTransport } from '@solana/rpc-transport-http';
*
* // Create multiple transports
* const transportA = createHttpTransport({ url: 'https://mainnet-beta.my-server-1.com' });
* const transportB = createHttpTransport({ url: 'https://mainnet-beta.my-server-2.com' });
* const transportC = createHttpTransport({ url: 'https://mainnet-beta.my-server-3.com' });
* const transportD = createHttpTransport({ url: 'https://mainnet-beta.my-server-4.com' });
*
* // Function to determine which shard to use based on the request method
* function selectShard(method: string): RpcTransport {
* switch (method) {
* case 'getAccountInfo':
* case 'getBalance':
* return transportA;
* case 'getLatestBlockhash':
* case 'getTransaction':
* return transportB;
* case 'sendTransaction':
* return transportC;
* default:
* return transportD;
* }
* }
*
* async function shardingTransport<TResponse>(...args: Parameters<RpcTransport>): Promise<RpcResponse<TResponse>> {
* const payload = args[0].payload as { method: string };
* const selectedTransport = selectShard(payload.method);
* return await selectedTransport(...args);
* }
* ```
*
* ### Retry Logic
*
* The transport library can also be used to implement custom retry logic on any request:
*
* ```ts
* import { RpcTransport } from '@solana/rpc-spec';
* import { RpcResponse } from '@solana/rpc-spec-types';
* import { createHttpTransport } from '@solana/rpc-transport-http';
*
* // Set the maximum number of attempts to retry a request
* const MAX_ATTEMPTS = 4;
*
* // Create the default transport
* const defaultTransport = createHttpTransport({ url: 'https://mainnet-beta.my-server-1.com' });
*
* // Sleep function to wait for a given number of milliseconds
* function sleep(ms: number): Promise<void> {
* return new Promise(resolve => setTimeout(resolve, ms));
* }
*
* // Calculate the delay for a given attempt
* function calculateRetryDelay(attempt: number): number {
* // Exponential backoff with a maximum of 1.5 seconds
* return Math.min(100 * Math.pow(2, attempt), 1500);
* }
*
* // A retrying transport that will retry up to `MAX_ATTEMPTS` times before failing
* async function retryingTransport<TResponse>(...args: Parameters<RpcTransport>): Promise<RpcResponse<TResponse>> {
* let requestError;
* for (let attempts = 0; attempts < MAX_ATTEMPTS; attempts++) {
* try {
* return await defaultTransport(...args);
* } catch (err) {
* requestError = err;
* // Only sleep if we have more attempts remaining
* if (attempts < MAX_ATTEMPTS - 1) {
* const retryDelay = calculateRetryDelay(attempts);
* await sleep(retryDelay);
* }
* }
* }
* throw requestError;
* }
* ```
*
* ### Failover
*
* Here’s an example of some failover logic integrated into a transport:
*
* ```ts
* import { RpcTransport } from '@solana/rpc-spec';
* import { RpcResponse } from '@solana/rpc-spec-types';
* import { createHttpTransport } from '@solana/rpc-transport-http';
*
* // Create a transport for each RPC server
* const transports = [
* createHttpTransport({ url: 'https://mainnet-beta.my-server-1.com' }),
* createHttpTransport({ url: 'https://mainnet-beta.my-server-2.com' }),
* createHttpTransport({ url: 'https://mainnet-beta.my-server-2.com' }),
* ];
*
* // A failover transport that will try each transport in order until one succeeds before failing
* async function failoverTransport<TResponse>(...args: Parameters<RpcTransport>): Promise<RpcResponse<TResponse>> {
* let requestError;
*
* for (const transport of transports) {
* try {
* return await transport(...args);
* } catch (err) {
* requestError = err;
* console.error(err);
* }
* }
* throw requestError;
* }
* ```
*
* @packageDocumentation
*/
export * from './http-transport';
export * from './http-transport-for-solana-rpc';