import { Hex, RpcRequest, RpcResponse } from 'ox' import { KeyAuthorization, Transaction as core_Transaction, TxEnvelopeTempo } from 'ox/tempo' import { type Client, BaseError } from 'viem' import * as z from 'zod/mini' import * as ApiResponse from '../../internal/Response.js' export function resolveChainId(value: unknown) { if (typeof value === 'number') return value if (typeof value === 'bigint') return Number(value) if (typeof value === 'string') { if (Hex.validate(value)) return Hex.toNumber(value) const n = Number(value) if (Number.isFinite(n)) return n } return undefined } export function formatFillTransactionRequest(client: Client, value: Record) { const format = client.chain?.formatters?.transactionRequest?.format if (!format) return value return format({ ...value } as never, 'fillTransaction') as Record } export function normalizeFillTransactionRequest( tx: Record, ): Record & { calls: unknown[] } { const { to, data, value, ...rest } = tx const keyAuthorization = normalizeKeyAuthorization(tx['keyAuthorization']) const withKeyAuthorization = keyAuthorization ? { keyAuthorization } : {} if (Array.isArray(tx['calls']) && tx['calls'].length > 0) return { ...tx, ...withKeyAuthorization, calls: tx['calls'].map((call) => ({ ...call, value: normalizeFillValue(call.value), })), } const call = { ...(typeof to !== 'undefined' ? { to } : {}), ...(typeof data !== 'undefined' ? { data } : {}), ...(typeof value !== 'undefined' ? { value: normalizeFillValue(value) } : {}), } return { ...rest, ...withKeyAuthorization, calls: [call] } } /** * Forwards `keyAuthorization` to the chain in RPC shape. Pass-through * when already RPC; convert via `KeyAuthorization.toRpc` when internal. */ function normalizeKeyAuthorization(value: unknown) { if (!value || typeof value !== 'object') return undefined const ka = value as Record const signature = ka['signature'] as Record | undefined if (!signature || typeof signature !== 'object') return undefined const isInternal = typeof signature['signature'] === 'object' && signature['signature'] !== null return isInternal ? KeyAuthorization.toRpc(value as never) : value } function normalizeFillValue(value: unknown) { if (typeof value !== 'string' || !value.startsWith('0x')) return value return BigInt(value === '0x' ? '0x0' : value) } /** Returns whether a raw transaction uses a Tempo sender or fee-payer wire prefix. */ export function isSerializedTempoTransaction( value: unknown, ): value is `0x76${string}` | `0x78${string}` { if (typeof value !== 'string') return false // `0x78` is Tempo's fee-payer handoff magic, not a separate EIP-2718 type. return ( value.startsWith(TxEnvelopeTempo.serializedType) || value.startsWith(TxEnvelopeTempo.feePayerMagic) ) } export function normalizeTempoTransaction(value: Record | undefined) { if (!value) throw new Error('Expected `tx` in eth_fillTransaction response.') return core_Transaction.fromRpc({ type: '0x76', ...value } as core_Transaction.Rpc)! } /** Returns a raw JSON-RPC error response object (not wrapped in a `Response`). */ export function rpcErrorJson(request: RpcRequest.RpcRequest, error: unknown) { // Serialize error instances to plain objects: `Error.message` is // non-enumerable, so instances stringify onto the wire without it. if ( error instanceof RpcResponse.InvalidParamsError || error instanceof RpcResponse.MethodNotSupportedError ) return RpcResponse.from( { error: { code: error.code, message: error.message, ...(error.data !== undefined ? { data: error.data } : {}), }, }, { request }, ) if (error instanceof z.core.$ZodError) return RpcResponse.from( { error: { code: RpcResponse.InvalidParamsError.code, data: { code: 'invalid_params', issues: ApiResponse.validationDetails(error.issues.slice(0, 10)), }, message: 'Invalid params.', }, }, { request }, ) const inner = resolveError(error) if (inner.code === undefined || inner.message === undefined) return RpcResponse.from( { error: { code: RpcResponse.InternalError.code, data: { code: 'internal_error' }, message: 'Internal error', }, }, { request }, ) if ( inner.code === RpcResponse.InternalError.code && /^Revm error: transaction expired(?:\.|: .+)?$/.test(inner.message) ) return RpcResponse.from( { error: { code: RpcResponse.TransactionRejectedError.code, data: { code: 'transaction_expired' }, message: 'Transaction expired.', }, }, { request }, ) const { code, data, message } = inner return RpcResponse.from( { error: { code, message, ...(data === undefined ? {} : { data }) }, }, { request }, ) } export function rpcError(request: RpcRequest.RpcRequest, error: unknown) { return Response.json(rpcErrorJson(request, error)) } export function rpcResult(request: RpcRequest.RpcRequest, result: unknown) { return Response.json(RpcResponse.from({ result }, { request })) } export const parseParams = z.readonly(z.tuple([z.record(z.string(), z.unknown())])) function resolveError(error: unknown): { message?: string | undefined code?: number | undefined data?: unknown } { if (!error || typeof error !== 'object') return {} // Walk the `cause` chain to the deepest object that looks like an upstream // JSON-RPC error (`{ code, message }`). This unwraps viem's // `RpcRequestError`, whose own `message` is the verbose // "RPC Request failed.\nURL: …\nRequest body: …" formatting, and surfaces // the raw upstream error instead. let deepest: | { message?: string | undefined; code?: number | undefined; data?: unknown } | undefined let current: unknown = error const seen = new Set() while (current && typeof current === 'object' && !seen.has(current)) { seen.add(current) const e = current as Record if (typeof e['code'] === 'number' && typeof e['message'] === 'string') deepest = { code: e['code'], message: e['message'], data: e['data'] } current = e['cause'] } if (deepest) return deepest if (error instanceof BaseError) { const inner = error.walk( (e) => typeof (e as Record)['code'] === 'number', ) as Record | null if (inner && typeof inner['code'] === 'number' && typeof inner['message'] === 'string') return { message: inner['message'], code: inner['code'], data: inner['data'] } } return {} }