UNPKG

viem

Version:

TypeScript Interface for Ethereum

170 lines (156 loc) 4.97 kB
import { HttpRequestError, type HttpRequestErrorType as HttpRequestErrorType_, TimeoutError, type TimeoutErrorType, } from '../../errors/request.js' import type { ErrorType } from '../../errors/utils.js' import type { RpcRequest, RpcResponse } from '../../types/rpc.js' import type { MaybePromise } from '../../types/utils.js' import { type WithTimeoutErrorType, withTimeout, } from '../promise/withTimeout.js' import { stringify } from '../stringify.js' import { idCache } from './id.js' export type HttpRpcClientOptions = { /** Request configuration to pass to `fetch`. */ fetchOptions?: Omit<RequestInit, 'body'> | undefined /** A callback to handle the request. */ onRequest?: | (( request: Request, init: RequestInit, ) => MaybePromise< void | undefined | (RequestInit & { url?: string | undefined }) >) | undefined /** A callback to handle the response. */ onResponse?: ((response: Response) => Promise<void> | void) | undefined /** The timeout (in ms) for the request. */ timeout?: number | undefined } export type HttpRequestParameters< body extends RpcRequest | RpcRequest[] = RpcRequest, > = { /** The RPC request body. */ body: body /** Request configuration to pass to `fetch`. */ fetchOptions?: HttpRpcClientOptions['fetchOptions'] | undefined /** A callback to handle the response. */ onRequest?: | (( request: Request, init: RequestInit, ) => MaybePromise< void | undefined | (RequestInit & { url?: string | undefined }) >) | undefined /** A callback to handle the response. */ onResponse?: ((response: Response) => Promise<void> | void) | undefined /** The timeout (in ms) for the request. */ timeout?: HttpRpcClientOptions['timeout'] | undefined } export type HttpRequestReturnType< body extends RpcRequest | RpcRequest[] = RpcRequest, > = body extends RpcRequest[] ? RpcResponse[] : RpcResponse export type HttpRequestErrorType = | HttpRequestErrorType_ | TimeoutErrorType | WithTimeoutErrorType | ErrorType export type HttpRpcClient = { request<body extends RpcRequest | RpcRequest[]>( params: HttpRequestParameters<body>, ): Promise<HttpRequestReturnType<body>> } export function getHttpRpcClient( url: string, options: HttpRpcClientOptions = {}, ): HttpRpcClient { return { async request(params) { const { body, onRequest = options.onRequest, onResponse = options.onResponse, timeout = options.timeout ?? 10_000, } = params const fetchOptions = { ...(options.fetchOptions ?? {}), ...(params.fetchOptions ?? {}), } const { headers, method, signal: signal_ } = fetchOptions try { const response = await withTimeout( async ({ signal }) => { const init: RequestInit = { ...fetchOptions, body: Array.isArray(body) ? stringify( body.map((body) => ({ jsonrpc: '2.0', id: body.id ?? idCache.take(), ...body, })), ) : stringify({ jsonrpc: '2.0', id: body.id ?? idCache.take(), ...body, }), headers: { 'Content-Type': 'application/json', ...headers, }, method: method || 'POST', signal: signal_ || (timeout > 0 ? signal : null), } const request = new Request(url, init) const args = (await onRequest?.(request, init)) ?? { ...init, url } const response = await fetch(args.url ?? url, args) return response }, { errorInstance: new TimeoutError({ body, url }), timeout, signal: true, }, ) if (onResponse) await onResponse(response) let data: any if ( response.headers.get('Content-Type')?.startsWith('application/json') ) data = await response.json() else { data = await response.text() try { data = JSON.parse(data || '{}') } catch (err) { if (response.ok) throw err data = { error: data } } } if (!response.ok) { throw new HttpRequestError({ body, details: stringify(data.error) || response.statusText, headers: response.headers, status: response.status, url, }) } return data } catch (err) { if (err instanceof HttpRequestError) throw err if (err instanceof TimeoutError) throw err throw new HttpRequestError({ body, cause: err as Error, url, }) } }, } }