UNPKG

accounts

Version:

Tempo Accounts SDK

170 lines (158 loc) 6.09 kB
import { type Provider as ox_Provider } from 'ox' import { type Chain, createClient, type Client, type EIP1193RequestFn, http, type Transport, } from 'viem' import type { tempo } from 'viem/chains' import { Transaction } from 'viem/tempo' import type * as Store from './Store.js' const defaultClients = new Map<string, Client>() const clients = new WeakMap<object, Map<string, Client>>() /** Resolves a viem Client for a given chain ID (cached). */ export function fromChainId( chainId: number | undefined, options: fromChainId.Options, ): Client<Transport, typeof tempo> { const { chains, feePayer: feePayerOption, provider, store, transports } = options const feePayerUrl = (() => { if (feePayerOption === false) return undefined if (typeof feePayerOption === 'string') return normalizeFeePayerUrl(feePayerOption) if (feePayerOption?.url) return normalizeFeePayerUrl(feePayerOption.url) return undefined })() const precedence = (() => { if (typeof feePayerOption === 'object' && feePayerOption !== null) return feePayerOption.precedence ?? 'fee-payer-first' return 'fee-payer-first' })() const id = chainId ?? store.getState().chainId const key = `${id}:${provider ? 'p' : ''}:${feePayerOption === false ? 'no-fp' : (feePayerUrl ?? '')}:${precedence}` // Scope the cache by `provider` (preferred) or `transports`, falling back // to a shared module-level map. The cache key only encodes a boolean for // `provider`, so without scoping, two providers sharing the same chainId // would hit each other's cached clients and route requests to the wrong // adapter via `providerTransport`. const scope = provider ?? transports const cache = (() => { if (!scope) return defaultClients let map = clients.get(scope) if (!map) { map = new Map() clients.set(scope, map) } return map })() let client = cache.get(key) if (!client) { const chain = chains.find((c) => c.id === id) ?? chains[0]! const base = transports?.[id] ?? http() const transport_base = provider ? providerTransport(provider, base) : base const transport = feePayerUrl ? feePayerTransport(transport_base, feePayerUrl, precedence) : transport_base client = createClient({ chain, transport, pollingInterval: 1000 }) cache.set(key, client) } return client as never } export declare namespace fromChainId { type Options = { /** Supported chains. */ chains: readonly [Chain, ...Chain[]] /** Fee payer configuration. A URL string, config object, or `false` to opt out. */ feePayer?: | string | false | { /** Fee payer service URL. */ url: string /** Signing precedence. @default 'fee-payer-first' */ precedence?: 'fee-payer-first' | 'user-first' | undefined } | undefined /** Provider instance. When set, the transport routes requests through the provider first, falling back to HTTP for unknown methods. */ provider?: ox_Provider.Provider | undefined /** Reactive state store. */ store: Store.Store /** Per-chain transports keyed by chain ID. When omitted, defaults to `http()` (uses the chain's default RPC URL). */ transports?: Record<number, Transport> | undefined } } /** * Creates a transport that routes requests through the provider, falling * back to the given base transport for methods the provider proxies to RPC. */ function providerTransport(provider: ox_Provider.Provider, base: Transport): Transport { return (params) => { const baseTransport = base(params) return { ...baseTransport, async request({ method, params: reqParams }) { return (provider as { request: EIP1193RequestFn }).request({ method, params: reqParams, } as any) }, } as ReturnType<Transport> } } /** * Resolves a fee payer URL to an absolute URL string. Relative paths (e.g. * `/relay`) are resolved against `window.location.origin` when running in a * browser; on the server, relative paths are returned as-is. */ function normalizeFeePayerUrl(url: string): string { if (url.startsWith('http://') || url.startsWith('https://')) return url if (typeof window !== 'undefined') return new URL(url, window.location.origin).href return url } function feePayerTransport( base: Transport, url: string, precedence: 'fee-payer-first' | 'user-first', ): Transport { return (params) => { const baseTransport = base(params) const sponsor = http(url)(params) return { ...baseTransport, async request({ method, params: rpcParams }: { method: string; params?: unknown }) { const args = rpcParams as readonly unknown[] | undefined if (precedence === 'fee-payer-first' && method === 'eth_fillTransaction') { const request = args?.[0] if ( request && typeof request === 'object' && 'feePayer' in request && (request.feePayer === true || typeof request.feePayer === 'string') ) return sponsor.request({ method, params: [{ ...request, feePayer: true }], }) } if (method === 'eth_sendRawTransaction' || method === 'eth_sendRawTransactionSync') { const serialized = args?.[0] if ( typeof serialized === 'string' && (serialized.startsWith('0x76') || serialized.startsWith('0x78')) ) { const deserialized = Transaction.deserialize(serialized as `0x76${string}`) if ('feePayerSignature' in deserialized && deserialized.feePayerSignature === null) { const signed = await sponsor.request({ method: 'eth_signRawTransaction', params: [serialized], }) return await baseTransport.request({ method, params: [signed] }) } } } return await baseTransport.request({ method, params: rpcParams }) }, } as ReturnType<Transport> } }