import { ZoneRpcAuthentication } from 'ox/tempo' import { createClient as core_createClient, fallback, http, publicActions } from 'viem' import type { Chain as core_Chain, Client as core_Client, FallbackTransport, HttpTransport, PublicActions, } from 'viem' import { tempoActions } from 'viem/tempo' import type { TempoActions } from 'viem/tempo' import { tempoMainnet, tempoTestnet } from 'viem/tempo/chains' import type * as z from 'zod/mini' import type * as Auth from './Auth.js' import * as Credential from './Credential.js' import type * as Schema from './Schema.js' /** Known Tempo chains keyed by id, for metadata lookups. */ export const chains = [tempoMainnet, tempoTestnet] /** Tempo chain ids keyed by network. */ export const chainId = { mainnet: tempoMainnet.id, testnet: tempoTestnet.id, } as const /** Default Tempo chain id. */ export const defaultChainId = chainId.mainnet /** Whether a chain id is Tempo mainnet. */ export function isMainnet(id: number): boolean { return id === chainId.mainnet } /** Default Tempo RPC URLs keyed by chain id. */ export const url: Record = { [chainId.mainnet]: 'https://rpc.tempo.xyz', [chainId.testnet]: 'https://rpc.testnet.tempo.xyz', } /** Tempo chain id. */ export type ChainId = z.output /** Tempo chain metadata with a configurable chain id. */ export type Chain = Omit & { id: ChainId } /** Gets a Tempo viem client for a chain id. */ export type GetClient = (chainId?: ChainId) => getClient.ReturnType /** Shared, per-chain, or dynamically resolved RPC URLs. */ export type UrlResolver = | string | Record | ((chainId: ChainId) => string | undefined) /** Headers required when connecting to a Zone upstream. */ export type ZoneHeaders = Record /** Creates a generic EVM client with ordered RPC endpoint fallback. */ export function createEvmClient(options: createEvmClient.Options): createEvmClient.ReturnType { return core_createClient({ transport: fallback( options.urls.map((url) => http(url, { fetchFn: options.fetch, ...(options.signal ? { fetchOptions: { signal: options.signal } } : {}), retryCount: 0, }), ), ), }) } export declare namespace createEvmClient { /** Generic EVM RPC client configuration. */ type Options = { /** Fetch implementation used for RPC requests. */ fetch: typeof globalThis.fetch /** Optional request cancellation signal. */ signal?: AbortSignal | undefined /** Ordered RPC endpoint URLs. */ urls: readonly string[] } /** Generic EVM client with fallback transport. */ type ReturnType = core_Client } /** Returns chain ids declared by a static chain-id-to-URL map. */ export function configuredChainIds(value: UrlResolver | undefined): number[] { const urls = parseUrls(value) if (!urls || typeof urls === 'string' || typeof urls === 'function') return [] return Object.keys(urls).map(Number) } /** * Resolves the effective RPC config for a single request. * * Accepts either a static config object or a per-request resolver function. The * resolver receives a {@link resolveRpc.Context} (`chainId` plus the request * `principal`), so callers can route traffic per caller — e.g. send anonymous * (`principal.type === 'public'`) traffic to a separate, no-auth upstream. A * `null` principal denotes a trusted, non-request caller (the scheduled * webhook poller). A missing `url` falls back to the built-in public host map * ({@link url}); missing `auth` means no Authorization header is sent. * * Memoization is keyed on the resolved output (see {@link createGetClient} and * {@link Tidx.createGetClient}), not the context, so a resolver may branch on * any principal field without unbounded client cardinality: callers that * resolve to the same upstream share a client. Pass the result to * {@link getClient} (or let `getClient` resolve for you) to build a client. */ export function resolveRpc( rpc: getClient.Rpc | undefined, context: resolveRpc.Context, ): resolveRpc.Resolved { const config = typeof rpc === 'function' ? rpc(context) : rpc const auth = Credential.resolveCredential({ chainId: context.chainId, value: config?.auth }) const publicZoneUrl = context.zone ? (resolveUrl(config?.publicZoneUrl, context.chainId) ?? context.zone.rpcUrls.default.http[0]) : undefined return { ...auth, ...(publicZoneUrl ? { publicZoneUrl } : {}), ...(context.zone && config?.zoneHeaders ? { zoneHeaders: config.zoneHeaders } : {}), url: resolveUrl(config?.url, context.chainId) ?? context.zone?.rpcUrls['internal']?.http[0] ?? context.zone?.rpcUrls.default.http[0] ?? url[context.chainId], } } export declare namespace resolveRpc { /** RPC resolution context. */ type Context = { /** Tempo chain id. */ chainId: ChainId /** * Request principal, or `null` for a trusted non-request caller (the * scheduled webhook poller). Anonymous and MPP-paid public requests carry a * `type: 'public'` principal; authenticated requests a `type: 'api_key'` one. */ principal: Auth.Principal | null /** Zone chain selected for this request, when applicable. */ zone?: core_Chain | undefined } /** Resolved RPC config for a single request. */ type Resolved = { /** Basic auth credentials, or undefined to send no Basic auth. */ basicAuth?: string | undefined /** Bearer auth credential, or undefined to send no Bearer auth. */ bearerAuth?: string | undefined /** Public Zone RPC URL that accepts a caller-provided authorization token. */ publicZoneUrl?: string | undefined /** Headers sent to Zone RPC upstreams. */ zoneHeaders?: ZoneHeaders | undefined /** * Resolved RPC URL, or `undefined` when neither a configured `rpc` nor the * built-in {@link url} map covers the chain (e.g. an unconfigured localnet). * A `undefined` URL falls through to viem's chain default transport. */ url: string | undefined } } export function getClient(options: getClient.Options): getClient.ReturnType { const { chainId = defaultChainId, principal = null, rpc, zone, zoneToken } = options const { basicAuth, bearerAuth, publicZoneUrl, url, zoneHeaders } = resolveRpc(rpc, { chainId, principal, zone, }) const usePublicZoneUrl = Boolean(zoneToken && publicZoneUrl) const rpcUrl = usePublicZoneUrl ? publicZoneUrl : url // Public Zone endpoints authenticate with the caller's token, never internal RPC credentials. const authorization = usePublicZoneUrl ? undefined : basicAuth ? `Basic ${btoa(basicAuth)}` : bearerAuth ? `Bearer ${bearerAuth}` : undefined const fetchOptions = authorization || zoneToken || zoneHeaders ? { headers: { ...(zoneHeaders ? Object.fromEntries(new Headers(zoneHeaders)) : {}), ...(authorization ? { Authorization: authorization } : {}), ...(zoneToken ? { [ZoneRpcAuthentication.headerName]: zoneToken } : {}), }, } : undefined return core_createClient({ // Auto-batch `eth_call`s within the same macrotask into a single // deployless multicall. Lets call sites issue `Promise.all` of N reads // (e.g. `balanceOf` across many tokens) and pay one RPC round-trip. // Deployless mode bytecode-injects multicall3 so Tempo, which doesn't // configure a deployed multicall3, can still serve the aggregated call. // Zones disable multicall entirely and issue concurrent direct `eth_call`s instead. batch: { multicall: zone ? false : { deployless: true } }, ccipRead: false, chain: { ...tempoMainnet, id: chainId }, transport: http(rpcUrl, fetchOptions ? { fetchOptions } : {}), }) .extend(publicActions) .extend(tempoActions()) } /** Builds a memoized Tempo client resolver. */ export function createGetClient(options: createGetClient.Options = {}): createGetClient.ReturnType { const { defaultChainId: defaultChainId_option = defaultChainId, rpc } = options const clients = new Map() const zones = new Map((options.zones ?? []).map((zone) => [zone.id, zone])) return (chainId = defaultChainId_option, principal = null) => { const { basicAuth, bearerAuth, url, zoneHeaders } = resolveRpc(rpc, { chainId, principal, zone: zones.get(chainId), }) const cacheKey = JSON.stringify([url, basicAuth, bearerAuth, zoneHeaders, chainId]) const existing = clients.get(cacheKey) if (existing) return existing const client = getClient({ chainId, rpc: { auth: basicAuth ?? bearerAuth, url: () => url, ...(zoneHeaders ? { zoneHeaders } : {}), }, zone: zones.get(chainId), }) clients.set(cacheKey, client) return client } } export declare namespace createGetClient { /** Options for building a memoized Tempo client resolver. */ type Options = { /** Tempo chain id used when a caller omits one. */ defaultChainId?: ChainId | undefined /** Tempo RPC options applied to every constructed client. */ rpc?: getClient.Rpc | undefined /** Zone chains whose internal RPC URLs serve as per-chain defaults. */ zones?: readonly core_Chain[] | undefined } /** Memoized Tempo client resolver. */ type ReturnType = (chainId?: ChainId, principal?: Auth.Principal | null) => getClient.ReturnType } export declare namespace getClient { /** Tempo viem client with public and Tempo action helpers. */ type ReturnType = core_Client< HttpTransport, Chain, undefined, undefined, // Omit core's ERC-20 `token` namespace; `tempoActions` shadows it at runtime. Omit, 'token'> & TempoActions > /** Options for getting a Tempo viem client. */ type Options = { /** Tempo chain id. */ chainId?: ChainId | undefined /** Request principal, or `null`/omitted for a trusted non-request caller. */ principal?: Auth.Principal | null | undefined /** Tempo RPC options. */ rpc?: Rpc | undefined /** Zone chain metadata used for RPC URL resolution. */ zone?: core_Chain | undefined /** Zone authorization token forwarded to the RPC node. */ zoneToken?: string | undefined } /** * Tempo RPC options: either a static config, or a per-request resolver * function that receives a {@link resolveRpc.Context} and returns the config to * use (e.g. routing anonymous callers to a public, no-auth RPC). */ type Rpc = | { /** Shared or per-chain RPC credentials. Values containing `:` use Basic auth; others use Bearer auth. */ auth?: Credential.ChainCredential | undefined /** Resolves the public Zone RPC URL used with caller-provided authorization. */ publicZoneUrl?: UrlResolver | undefined /** Headers sent to Zone RPC upstreams. */ zoneHeaders?: ZoneHeaders | undefined /** Resolves the RPC URL for a Tempo chain id. */ url?: UrlResolver | undefined } | ((context: resolveRpc.Context) => { /** Shared or per-chain RPC credentials, or undefined for none. */ auth?: Credential.ChainCredential | undefined /** Public Zone RPC URL used with caller-provided authorization. */ publicZoneUrl?: string | undefined /** Headers sent to Zone RPC upstreams. */ zoneHeaders?: ZoneHeaders | undefined /** RPC URL to use; falls back to the built-in public host when omitted. */ url?: string | undefined }) } /** Resolves a shared or per-chain URL for one chain id. */ export function resolveUrl(value: UrlResolver | undefined, chainId: ChainId) { if (typeof value === 'function') return value(chainId) const urls = parseUrls(value) if (!urls) return undefined if (typeof urls === 'string') return urls return urls[chainId] } function parseUrls(value: UrlResolver | undefined): Record | string | undefined { if (!value || typeof value === 'function') return undefined const urls = typeof value === 'string' ? (() => { try { return JSON.parse(value) as unknown } catch { return value } })() : value if (typeof urls === 'string') return urls if ( !urls || typeof urls !== 'object' || Array.isArray(urls) || Object.keys(urls).some((chainId) => { const id = Number(chainId) return !Number.isSafeInteger(id) || id <= 0 || String(id) !== chainId }) || Object.values(urls).some((url) => typeof url !== 'string') ) throw new Error('Expected a URL or a chain-id-to-URL JSON object.') return urls as Record }