UNPKG

ox

Version:

Ethereum Standard Library

226 lines (208 loc) 6.69 kB
import * as Errors from './Errors.js' import { getUrl } from './internal/errors.js' import * as promise from './internal/promise.js' import type * as RpcSchema_internal from './internal/rpcSchema.js' import * as internal from './internal/rpcTransport.js' import type { Compute } from './internal/types.js' import type * as RpcResponse from './RpcResponse.js' import type * as RpcSchema from './RpcSchema.js' /** Root type for an RPC Transport. */ export type RpcTransport< raw extends boolean = false, options extends Record<string, unknown> = {}, schema extends RpcSchema.Generic = RpcSchema.Default, > = Compute<{ request: RequestFn<raw, options, schema> }> /** HTTP-based RPC Transport. */ export type Http< raw extends boolean = false, schema extends RpcSchema.Generic = RpcSchema.Default, > = RpcTransport<raw, HttpOptions, schema> export type HttpOptions = { /** Request configuration to pass to `fetch`. */ fetchOptions?: | Omit<RequestInit, 'body'> | (( method: RpcSchema.Generic['Request'], ) => Omit<RequestInit, 'body'> | Promise<Omit<RequestInit, 'body'>>) | undefined /** Function to use to make the request. @default fetch */ fetchFn?: typeof fetch | undefined /** Timeout for the request in milliseconds. @default 10_000 */ timeout?: number | undefined } export type RequestFn< raw extends boolean = false, options extends Record<string, unknown> = {}, schema extends RpcSchema.Generic = RpcSchema.Default, > = < methodName extends RpcSchema.MethodNameGeneric, raw_override extends boolean | undefined = undefined, >( parameters: Compute< RpcSchema_internal.ExtractRequestOpaque<schema, methodName> >, options?: internal.Options<raw_override, options, schema>, ) => Promise< raw_override extends boolean ? raw_override extends true ? RpcResponse.RpcResponse<RpcSchema.ExtractReturnType<schema, methodName>> : RpcSchema.ExtractReturnType<schema, methodName> : raw extends true ? RpcResponse.RpcResponse<RpcSchema.ExtractReturnType<schema, methodName>> : RpcSchema.ExtractReturnType<schema, methodName> > /** * Creates a HTTP JSON-RPC Transport from a URL. * * @example * ```ts twoslash * import { RpcTransport } from 'ox' * * const transport = RpcTransport.fromHttp( * 'https://1.rpc.thirdweb.com' * ) * * const blockNumber = await transport.request({ * method: 'eth_blockNumber' * }) * // @log: '0x1a2b3c' * ``` * * @param url - URL to perform the JSON-RPC requests to. * @param options - Transport options. * @returns HTTP JSON-RPC Transport. */ export function fromHttp< raw extends boolean = false, schema extends RpcSchema.Schema = RpcSchema.Default, >( url: string, options: fromHttp.Options<raw, schema> = {}, ): Http<raw, RpcSchema.ToGeneric<schema>> { return internal.create<HttpOptions, RpcSchema.ToGeneric<schema>, raw>( { async request(body_, options_) { const { fetchFn = options.fetchFn ?? fetch, fetchOptions: fetchOptions_ = options.fetchOptions, timeout = options.timeout ?? 10_000, } = options_ const body = JSON.stringify(body_) const fetchOptions = typeof fetchOptions_ === 'function' ? await fetchOptions_(body_) : fetchOptions_ const response = await promise.withTimeout( ({ signal }) => { const externalSignal = fetchOptions?.signal const timeoutSignal = timeout > 0 ? signal : null const signals = [externalSignal, timeoutSignal].filter( (s): s is AbortSignal => Boolean(s), ) const composedSignal = signals.length === 0 ? null : signals.length === 1 ? signals[0]! : AbortSignal.any(signals) const init: RequestInit = { ...fetchOptions, body, headers: { 'Content-Type': 'application/json', ...Object.fromEntries(new Headers(fetchOptions?.headers)), }, method: fetchOptions?.method ?? 'POST', signal: composedSignal, } return fetchFn(url, init) }, { timeout, signal: true, }, ) // Always read as text so we can handle empty bodies and non-JSON // payloads (some servers return `Content-Type: application/json` with // an empty body for error responses). const data = await response.text().then((data) => { if (data === '') { if (response.ok) throw new MalformedResponseError({ response: data }) return { error: undefined } } try { return JSON.parse(data) } catch { if (response.ok) throw new MalformedResponseError({ response: data, }) return { error: data } } }) if (!response.ok) { const error = (data as { error?: unknown })?.error throw new HttpError({ body, details: typeof error === 'string' ? error : error ? JSON.stringify(error) : response.statusText, response, url, }) } return data as never }, }, { raw: options.raw }, ) } export declare namespace fromHttp { type Options< raw extends boolean = false, schema extends RpcSchema.Schema = RpcSchema.Default, > = internal.Options<raw, HttpOptions, schema> type ErrorType = | promise.withTimeout.ErrorType | HttpError | Errors.GlobalErrorType } /** Thrown when a HTTP request fails. */ export class HttpError extends Errors.BaseError { override readonly name = 'RpcTransport.HttpError' constructor({ body, details, response, url, }: { body: unknown details: string response: Response url: string }) { super('HTTP request failed.', { details, metaMessages: [ `Status: ${response.status}`, `URL: ${getUrl(url)}`, body ? `Body: ${JSON.stringify(body)}` : undefined, ], }) } } /** Thrown when a HTTP response is malformed. */ export class MalformedResponseError extends Errors.BaseError { override readonly name = 'RpcTransport.MalformedResponseError' constructor({ response }: { response: string }) { super('HTTP Response could not be parsed as JSON.', { metaMessages: [`Response: ${response}`], }) } }