SPB Git

Repository Public

rpc.ts 4.1 KB 58 lines typescript

1import { UserError } from '../config.ts';23const TIMEOUT = 9000;4/** One retry on connection-level failures (never on an HTTP answer). Safe for reads; re-sending signed bytes is idempotent on-chain. */5export async function http(url: string, init: RequestInit = {}): Promise<Response> {6  try { return await fetch(url, { ...init, signal: AbortSignal.timeout(TIMEOUT), redirect: 'error' }); }7  catch { await new Promise((r) => setTimeout(r, 300)); return fetch(url, { ...init, signal: AbortSignal.timeout(TIMEOUT), redirect: 'error' }); }8}910let idc = 0;11export async function rpcOne<T>(url: string, method: string, params: unknown[]): Promise<T> {12  const r = await http(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: ++idc, method, params }) });13  if (!r.ok) throw new Error(`rpc http ${r.status}`);14  const j = (await r.json()) as { result?: T; error?: { code: number; message: string } };15  if (j.error) throw Object.assign(new Error(j.error.message), { rpcCode: j.error.code });16  if (j.result === undefined) throw new Error('rpc: empty result');17  return j.result;18}1920/** First endpoint that answers wins (failover). Use for display-only reads. */21export async function rpcAny<T>(urls: string[], method: string, params: unknown[]): Promise<T> {22  let last: unknown;23  for (const u of urls) { try { return await rpcOne<T>(u, method, params); } catch (e) { last = e; if ((e as { rpcCode?: number }).rpcCode !== undefined) throw e; } }24  throw last;25}2627/** Every endpoint must answer and agree. Use for anything that feeds a signature (nonce, chain identity). */28export async function rpcQuorum<T>(urls: string[], method: string, params: unknown[], same: (a: T, b: T) => boolean = (a, b) => JSON.stringify(a) === JSON.stringify(b)): Promise<T> {29  const rs = await Promise.allSettled(urls.map((u) => rpcOne<T>(u, method, params)));30  const ok = rs.flatMap((r) => (r.status === 'fulfilled' ? [r.value as T] : []));31  if (ok.length < urls.length || ok.length === 0) throw new UserError('rpc_degraded', `network data source unavailable (${method}); transfer refused`, 503);32  for (const v of ok) if (!same(ok[0]!, v)) throw new UserError('rpc_disagreement', `network data sources disagree (${method}); transfer refused`, 503);33  return ok[0]!;34}3536export interface Health { network: string; /** every source up and identity verified: transfers possible */ ok: boolean; /** at least one source up: balances readable */ readable: boolean; identityVerified: boolean; height: number | null; endpoints: { url: string; ok: boolean; ms: number | null }[]; checkedAt: string }37export interface Prepared {38  /** chain section merged into the intent (hashed and approved) */39  section: Record<string, unknown>; maxFee: bigint; feeEstimate: bigint; warnings: string[]; ttlSec: number;40  /** unauthenticated helper data for the signer (it re-verifies everything in it against the intent) */41  aux?: Record<string, unknown>;42}43export interface Tracked { state: 'broadcast' | 'confirming' | 'finalized' | 'failed' | 'replaced' | 'reorged'; confirmations: number; blockRef?: string; error?: string; rebroadcast?: boolean }44export interface Deposit { txid: string; idx: number; asset: string; amount: bigint; from: string; block: number; time: number | null }45export interface Adapter {46  health(): Promise<Health>;47  balances(addr: string): Promise<Record<string, bigint>>;48  /** asset ids hidden because their on-chain identity check failed */49  disabledAssets?(): string[];50  prepare(d: { asset: string; from: string; to: string; amount: bigint; memo?: string;51    /** UTXO chains, THORChain vault payment: extra output bytes (the memo), a minimum fee rate, and change always returned to the wallet */52    thor?: { extraBytes: number; minRate: number } }): Promise<Prepared>;53  broadcast(raw: string): Promise<void>;54  track(p: { txid: string; intent: any; blockRef: string | null; broadcastAt: number }): Promise<Tracked>;55  deposits(addr: string, cursor: number | null): Promise<{ items: Deposit[]; cursor: number; finalizedHeight: number }>;56}57export const hostOf = (u: string) => new URL(u.replace(/^[a-z]+:(?=https?:)/, '')).host;58
Files main
Menu

Theme