@solana/rpc-transport-http
Advanced tools
Comparing version 2.0.0-experimental.356279e to 2.0.0-experimental.378499d
@@ -0,1 +1,3 @@ | ||
import { SolanaError, SOLANA_ERROR__RPC_TRANSPORT_HTTP_ERROR, SOLANA_ERROR__RPC_TRANSPORT_HEADER_FORBIDDEN } from '@solana/errors'; | ||
// ../build-scripts/env-shim.ts | ||
@@ -6,17 +8,2 @@ var __DEV__ = /* @__PURE__ */ (() => process["env"].NODE_ENV === "development")(); | ||
var e = globalThis.fetch; | ||
// src/http-transport-errors.ts | ||
var SolanaHttpError = class extends Error { | ||
statusCode; | ||
constructor(details) { | ||
super(`HTTP error (${details.statusCode}): ${details.message}`); | ||
Error.captureStackTrace(this, this.constructor); | ||
this.statusCode = details.statusCode; | ||
} | ||
get name() { | ||
return "SolanaHttpError"; | ||
} | ||
}; | ||
// src/http-transport-headers.ts | ||
var DISALLOWED_HEADERS = { | ||
@@ -42,5 +29,5 @@ accept: true, | ||
"permissions-policy": true, | ||
// No currently available Typescript technique allows you to match on a prefix. | ||
// 'proxy-':true, | ||
// 'sec-':true, | ||
// Prefix matching is implemented in code, below. | ||
// 'proxy-': true, | ||
// 'sec-': true, | ||
referer: true, | ||
@@ -59,5 +46,5 @@ te: true, | ||
if (badHeaders.length > 0) { | ||
throw new Error( | ||
`${badHeaders.length > 1 ? "These headers are" : "This header is"} forbidden: \`${badHeaders.join("`, `")}\`. Learn more at https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name.` | ||
); | ||
throw new SolanaError(SOLANA_ERROR__RPC_TRANSPORT_HEADER_FORBIDDEN, { | ||
headers: badHeaders | ||
}); | ||
} | ||
@@ -74,6 +61,21 @@ } | ||
// src/http-transport.ts | ||
function createHttpTransport({ headers, url }) { | ||
var didWarnDispatcherWasSuppliedInNonNodeEnvironment = false; | ||
function warnDispatcherWasSuppliedInNonNodeEnvironment() { | ||
if (didWarnDispatcherWasSuppliedInNonNodeEnvironment) { | ||
return; | ||
} | ||
didWarnDispatcherWasSuppliedInNonNodeEnvironment = true; | ||
console.warn( | ||
"You have supplied a `Dispatcher` to `createHttpTransport()`. It has been ignored because Undici dispatchers only work in Node environments. To eliminate this warning, omit the `dispatcher_NODE_ONLY` property from your config when running in a non-Node environment." | ||
); | ||
} | ||
function createHttpTransport(config) { | ||
if (__DEV__ && true && "dispatcher_NODE_ONLY" in config) { | ||
warnDispatcherWasSuppliedInNonNodeEnvironment(); | ||
} | ||
const { headers, url } = config; | ||
if (__DEV__ && headers) { | ||
assertIsAllowedHttpRequestHeaders(headers); | ||
} | ||
let dispatcherConfig; | ||
const customHeaders = headers && normalizeHeaders(headers); | ||
@@ -86,2 +88,3 @@ return async function makeHttpRequest({ | ||
const requestInfo = { | ||
...dispatcherConfig, | ||
body, | ||
@@ -100,3 +103,3 @@ headers: { | ||
if (!response.ok) { | ||
throw new SolanaHttpError({ | ||
throw new SolanaError(SOLANA_ERROR__RPC_TRANSPORT_HTTP_ERROR, { | ||
message: response.statusText, | ||
@@ -103,0 +106,0 @@ statusCode: response.status |
@@ -0,1 +1,3 @@ | ||
import { SolanaError, SOLANA_ERROR__RPC_TRANSPORT_HTTP_ERROR, SOLANA_ERROR__RPC_TRANSPORT_HEADER_FORBIDDEN } from '@solana/errors'; | ||
// ../build-scripts/env-shim.ts | ||
@@ -6,17 +8,2 @@ var __DEV__ = /* @__PURE__ */ (() => process["env"].NODE_ENV === "development")(); | ||
var e = globalThis.fetch; | ||
// src/http-transport-errors.ts | ||
var SolanaHttpError = class extends Error { | ||
statusCode; | ||
constructor(details) { | ||
super(`HTTP error (${details.statusCode}): ${details.message}`); | ||
Error.captureStackTrace(this, this.constructor); | ||
this.statusCode = details.statusCode; | ||
} | ||
get name() { | ||
return "SolanaHttpError"; | ||
} | ||
}; | ||
// src/http-transport-headers.ts | ||
var DISALLOWED_HEADERS = { | ||
@@ -42,5 +29,5 @@ accept: true, | ||
"permissions-policy": true, | ||
// No currently available Typescript technique allows you to match on a prefix. | ||
// 'proxy-':true, | ||
// 'sec-':true, | ||
// Prefix matching is implemented in code, below. | ||
// 'proxy-': true, | ||
// 'sec-': true, | ||
referer: true, | ||
@@ -59,5 +46,5 @@ te: true, | ||
if (badHeaders.length > 0) { | ||
throw new Error( | ||
`${badHeaders.length > 1 ? "These headers are" : "This header is"} forbidden: \`${badHeaders.join("`, `")}\`. Learn more at https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name.` | ||
); | ||
throw new SolanaError(SOLANA_ERROR__RPC_TRANSPORT_HEADER_FORBIDDEN, { | ||
headers: badHeaders | ||
}); | ||
} | ||
@@ -74,6 +61,21 @@ } | ||
// src/http-transport.ts | ||
function createHttpTransport({ headers, url }) { | ||
var didWarnDispatcherWasSuppliedInNonNodeEnvironment = false; | ||
function warnDispatcherWasSuppliedInNonNodeEnvironment() { | ||
if (didWarnDispatcherWasSuppliedInNonNodeEnvironment) { | ||
return; | ||
} | ||
didWarnDispatcherWasSuppliedInNonNodeEnvironment = true; | ||
console.warn( | ||
"You have supplied a `Dispatcher` to `createHttpTransport()`. It has been ignored because Undici dispatchers only work in Node environments. To eliminate this warning, omit the `dispatcher_NODE_ONLY` property from your config when running in a non-Node environment." | ||
); | ||
} | ||
function createHttpTransport(config) { | ||
if (__DEV__ && true && "dispatcher_NODE_ONLY" in config) { | ||
warnDispatcherWasSuppliedInNonNodeEnvironment(); | ||
} | ||
const { headers, url } = config; | ||
if (__DEV__ && headers) { | ||
assertIsAllowedHttpRequestHeaders(headers); | ||
} | ||
let dispatcherConfig; | ||
const customHeaders = headers && normalizeHeaders(headers); | ||
@@ -86,2 +88,3 @@ return async function makeHttpRequest({ | ||
const requestInfo = { | ||
...dispatcherConfig, | ||
body, | ||
@@ -100,3 +103,3 @@ headers: { | ||
if (!response.ok) { | ||
throw new SolanaHttpError({ | ||
throw new SolanaError(SOLANA_ERROR__RPC_TRANSPORT_HTTP_ERROR, { | ||
message: response.statusText, | ||
@@ -103,0 +106,0 @@ statusCode: response.status |
@@ -0,21 +1,6 @@ | ||
import { SolanaError, SOLANA_ERROR__RPC_TRANSPORT_HTTP_ERROR, SOLANA_ERROR__RPC_TRANSPORT_HEADER_FORBIDDEN } from '@solana/errors'; | ||
import { fetch } from 'undici'; | ||
// ../build-scripts/env-shim.ts | ||
var __DEV__ = /* @__PURE__ */ (() => process["env"].NODE_ENV === "development")(); | ||
// ../fetch-impl/dist/index.node.js | ||
var e = globalThis.fetch; | ||
// src/http-transport-errors.ts | ||
var SolanaHttpError = class extends Error { | ||
statusCode; | ||
constructor(details) { | ||
super(`HTTP error (${details.statusCode}): ${details.message}`); | ||
Error.captureStackTrace(this, this.constructor); | ||
this.statusCode = details.statusCode; | ||
} | ||
get name() { | ||
return "SolanaHttpError"; | ||
} | ||
}; | ||
// src/http-transport-headers.ts | ||
var DISALLOWED_HEADERS = { | ||
@@ -41,5 +26,5 @@ accept: true, | ||
"permissions-policy": true, | ||
// No currently available Typescript technique allows you to match on a prefix. | ||
// 'proxy-':true, | ||
// 'sec-':true, | ||
// Prefix matching is implemented in code, below. | ||
// 'proxy-': true, | ||
// 'sec-': true, | ||
referer: true, | ||
@@ -58,5 +43,5 @@ te: true, | ||
if (badHeaders.length > 0) { | ||
throw new Error( | ||
`${badHeaders.length > 1 ? "These headers are" : "This header is"} forbidden: \`${badHeaders.join("`, `")}\`. Learn more at https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name.` | ||
); | ||
throw new SolanaError(SOLANA_ERROR__RPC_TRANSPORT_HEADER_FORBIDDEN, { | ||
headers: badHeaders | ||
}); | ||
} | ||
@@ -73,6 +58,14 @@ } | ||
// src/http-transport.ts | ||
function createHttpTransport({ headers, url }) { | ||
function createHttpTransport(config) { | ||
if (__DEV__ && false) { | ||
warnDispatcherWasSuppliedInNonNodeEnvironment(); | ||
} | ||
const { headers, url } = config; | ||
if (__DEV__ && headers) { | ||
assertIsAllowedHttpRequestHeaders(headers); | ||
} | ||
let dispatcherConfig; | ||
if ("dispatcher_NODE_ONLY" in config) { | ||
dispatcherConfig = { dispatcher: config.dispatcher_NODE_ONLY }; | ||
} | ||
const customHeaders = headers && normalizeHeaders(headers); | ||
@@ -85,2 +78,3 @@ return async function makeHttpRequest({ | ||
const requestInfo = { | ||
...dispatcherConfig, | ||
body, | ||
@@ -97,5 +91,5 @@ headers: { | ||
}; | ||
const response = await e(url, requestInfo); | ||
const response = await fetch(url, requestInfo); | ||
if (!response.ok) { | ||
throw new SolanaHttpError({ | ||
throw new SolanaError(SOLANA_ERROR__RPC_TRANSPORT_HTTP_ERROR, { | ||
message: response.statusText, | ||
@@ -102,0 +96,0 @@ statusCode: response.status |
@@ -7,3 +7,3 @@ export type AllowedHttpRequestHeaders = Readonly<{ | ||
type DisallowedHeaders = 'Accept' | 'Content-Length' | 'Content-Type' | 'Solana-Client'; | ||
type ForbiddenHeaders = 'Accept-Charset' | 'Accept-Encoding' | 'Access-Control-Request-Headers' | 'Access-Control-Request-Method' | 'Connection' | 'Content-Length' | 'Cookie' | 'Date' | 'DNT' | 'Expect' | 'Host' | 'Keep-Alive' | 'Origin' | 'Permissions-Policy' | 'Referer' | 'TE' | 'Trailer' | 'Transfer-Encoding' | 'Upgrade' | 'Via'; | ||
type ForbiddenHeaders = 'Accept-Charset' | 'Accept-Encoding' | 'Access-Control-Request-Headers' | 'Access-Control-Request-Method' | 'Connection' | 'Content-Length' | 'Cookie' | 'Date' | 'DNT' | 'Expect' | 'Host' | 'Keep-Alive' | 'Origin' | 'Permissions-Policy' | `Proxy-${string}` | `Sec-${string}` | 'Referer' | 'TE' | 'Trailer' | 'Transfer-Encoding' | 'Upgrade' | 'Via'; | ||
export declare function assertIsAllowedHttpRequestHeaders(headers: Record<string, string>): asserts headers is AllowedHttpRequestHeaders; | ||
@@ -10,0 +10,0 @@ /** |
import { RpcTransport } from '@solana/rpc-spec'; | ||
import type Dispatcher from 'undici/types/dispatcher'; | ||
import { AllowedHttpRequestHeaders } from './http-transport-headers.js'; | ||
type Config = Readonly<{ | ||
dispatcher_NODE_ONLY?: Dispatcher; | ||
headers?: AllowedHttpRequestHeaders; | ||
url: string; | ||
}>; | ||
export declare function createHttpTransport({ headers, url }: Config): RpcTransport; | ||
export declare function createHttpTransport(config: Config): RpcTransport; | ||
export {}; | ||
//# sourceMappingURL=http-transport.d.ts.map |
{ | ||
"name": "@solana/rpc-transport-http", | ||
"version": "2.0.0-experimental.356279e", | ||
"version": "2.0.0-experimental.378499d", | ||
"description": "An RPC transport that uses HTTP requests", | ||
@@ -49,3 +49,5 @@ "exports": { | ||
"dependencies": { | ||
"@solana/rpc-spec": "2.0.0-experimental.356279e" | ||
"undici": "^6.6.2", | ||
"@solana/errors": "2.0.0-experimental.378499d", | ||
"@solana/rpc-spec": "2.0.0-experimental.378499d" | ||
}, | ||
@@ -52,0 +54,0 @@ "bundlewatch": { |
229
README.md
@@ -17,2 +17,229 @@ [![npm][npm-image]][npm-url] | ||
TODO | ||
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. | ||
## Functions | ||
### `createHttpTransport()` | ||
Call this to create a function that conforms to the `RpcTransport` interface (see `@solana/rpc-spec`). You can use that function in your programs to make `POST` requests with headers suitable for sending JSON data to a server. | ||
```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' }, | ||
}); | ||
``` | ||
#### Config | ||
##### `dispatcher_NODE_ONLY` | ||
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. | ||
```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(account => | ||
transport({ | ||
payload: { | ||
id: ++id, | ||
jsonrpc: '2.0', | ||
method: 'getBalance', | ||
params: [account], | ||
}, | ||
}), | ||
), | ||
); | ||
``` | ||
##### `headers` | ||
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. | ||
```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', | ||
}); | ||
``` | ||
##### `url` | ||
A string representing the target endpoint. In Node, it must be an absolute URL using the `http` or `https` protocol. | ||
## 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 { 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<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 { 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 'getTransaction': | ||
case 'getRecentBlockhash': | ||
return transportB; | ||
case 'sendTransaction': | ||
return transportC; | ||
default: | ||
return transportD; | ||
} | ||
} | ||
async function shardingTransport<TResponse>(...args: Parameters<RpcTransport>): Promise<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 { 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<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 { 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<TResponse> { | ||
let requestError; | ||
for (const transport of transports) { | ||
try { | ||
return await transport(...args); | ||
} catch (err) { | ||
requestError = err; | ||
console.error(err); | ||
} | ||
} | ||
throw requestError; | ||
} | ||
``` |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
79150
245
3
20
631
1
23
+ Addedundici@^6.6.2
+ Added@solana/errors@2.0.0-experimental.378499d(transitive)
+ Added@solana/rpc-spec@2.0.0-experimental.378499d(transitive)
+ Added@solana/rpc-spec-types@2.0.0-experimental.378499d(transitive)
+ Addedchalk@5.4.1(transitive)
+ Addedcommander@12.1.0(transitive)
+ Addedundici@6.21.0(transitive)
- Removed@solana/rpc-spec@2.0.0-experimental.356279e(transitive)
- Removed@solana/rpc-spec-types@2.0.0-experimental.356279e(transitive)