import { URL } from "node:url";
import type { Common } from "@/internal/common.js";
import type { Chain } from "@/internal/types.js";
import { HttpRequestError, RpcRequestError, TimeoutError } from "viem";
import {
type HttpRequestParameters,
type HttpRequestReturnType,
type HttpRpcClientOptions,
stringify,
} from "viem/utils";
export type RpcRequest = {
jsonrpc?: "2.0" | undefined;
method: string;
params?: any | undefined;
id?: number | undefined;
};
export type HttpRpcClient = {
request
(
params: HttpRequestParameters,
): Promise>;
};
export function getHttpRpcClient(
url: string,
options: HttpRpcClientOptions & { common: Common; chain: Chain },
): HttpRpcClient {
const { common, chain } = options;
const timeoutMs = options?.timeout ?? 10_000;
let id = 1;
return {
async request(params) {
// biome-ignore lint/suspicious/noAsyncPromiseExecutor:
return new Promise(async (resolve, reject) => {
let isTimeoutRejected = false;
const { body } = params;
const fetchOptions = {
...(params.fetchOptions ?? {}),
};
const { headers, method } = fetchOptions;
let reader: ReadableStreamDefaultReader | undefined;
const controller = new AbortController();
const timeoutId = setTimeout(async () => {
isTimeoutRejected = true;
controller.abort();
reject(new TimeoutError({ body, url }));
if (reader) {
common.logger.warn({
msg: "JSON-RPC request timed out reading response body",
chain: chain.name,
chain_id: chain.id,
hostname: new URL(url).hostname,
// @ts-ignore
request_id: headers ? headers["X-Request-ID"] : undefined,
method: body.method,
request: JSON.stringify(body),
duration: timeoutMs,
});
try {
await reader.cancel("Timeout");
} catch {}
}
}, timeoutMs);
try {
const init: RequestInit = {
body: stringify({
jsonrpc: "2.0",
id: body.id ?? id++,
...body,
}),
headers: {
"Content-Type": "application/json",
...headers,
},
method: method || "POST",
signal: controller.signal,
};
const request = new Request(url, init);
const response = await fetch(request);
reader = response.body?.getReader()!;
const chunks: Uint8Array[] = [];
let totalLength = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLength += value.length;
}
} finally {
reader.releaseLock();
reader = undefined;
}
if (isTimeoutRejected) return;
let offset = 0;
const fullData = new Uint8Array(totalLength);
for (const chunk of chunks) {
fullData.set(chunk, offset);
offset += chunk.length;
}
const text = new TextDecoder().decode(fullData);
let data: any = text;
try {
data = JSON.parse(data || "{}");
} catch (err) {
if (response.ok) throw err;
data = { error: data };
}
clearTimeout(timeoutId);
if (!response.ok) {
reject(
new HttpRequestError({
body,
details: stringify(data.error) || response.statusText,
headers: response.headers,
status: response.status,
url,
}),
);
return;
}
if (data.error) {
reject(
new RpcRequestError({
body,
error: data.error,
url: url,
}),
);
} else {
resolve(data.result);
}
} catch (_error) {
const error = _error as Error;
clearTimeout(timeoutId);
if (isTimeoutRejected) return;
if (error.name === "AbortError") {
reject(new TimeoutError({ body, url }));
}
if (error instanceof HttpRequestError) reject(error);
reject(new HttpRequestError({ body, cause: error, url }));
}
});
},
};
}