SPB Git

Repository Public

graph.ts 3.5 KB 53 lines typescript

1// Universal liquidity graph. Nodes are (asset, network) pairs from the wallet's verified registry. Capability edges say2// "this provider claims it can convert A to B"; they only select what to quote. An edge becomes a route only through a3// live, validated quote — a pair is never presented as executable because a provider advertises it.4import { NETWORKS, type NetworkDef } from '@spw/core';5import { assetKey, type AssetRef, type SwapProvider } from './types.ts';67export interface GraphNode extends AssetRef { key: string; symbol: string; name: string; decimals: number; networkName: string; native: boolean; hub: boolean }8export interface CapabilityEdge { from: string; to: string; provider: string; crossChain: boolean }9export interface Observation { provider: string; from: string; to: string; amountIn: string; amountOut: string; minAmountOut: string; ms: number; at: number; ok: boolean }1011/** Intermediate assets worth routing through: deep, widely quoted. Chosen by identity (registry entry), not by symbol alone. */12const HUB_SYMBOLS = new Set(['USDC', 'USDT']);1314export class LiquidityGraph {15  readonly nodes = new Map<string, GraphNode>();16  private out = new Map<string, CapabilityEdge[]>();17  private obs: Observation[] = [];18  constructor(private providers: SwapProvider[]) {19    for (const n of Object.values(NETWORKS) as NetworkDef[]) for (const a of [n.native, ...n.tokens]) {20      const ref = { network: n.id, asset: a.id }, key = assetKey(ref);21      this.nodes.set(key, { ...ref, key, symbol: a.symbol, name: a.name, decimals: a.decimals, networkName: n.name, native: a.id === 'native', hub: a.id === 'native' || HUB_SYMBOLS.has(a.symbol) });22    }23  }24  /** Capability edges out of a node, computed lazily (providers answer locally, no network). */25  edgesFrom(key: string): CapabilityEdge[] {26    let e = this.out.get(key);27    if (e) return e;28    const from = this.nodes.get(key); e = [];29    if (from) for (const p of this.providers) { const cap = p.capabilities(); if (cap.needsKey && !cap.configured) continue;30      for (const to of this.nodes.values()) if (to.key !== key && p.supportsPair(from, to)) e.push({ from: key, to: to.key, provider: p.id, crossChain: from.network !== to.network }); }31    this.out.set(key, e);32    return e;33  }34  /** Is there any advertised way from A to B within `maxHops`? Used to tell "no provider covers this" from "no liquidity right now". */35  reachable(a: AssetRef, b: AssetRef, maxHops = 2): boolean { return this.paths(a, b, maxHops).length > 0; }36  /** Candidate paths A → (hub) → B, direct first; intermediates are hub assets only, never the endpoints again. */37  paths(a: AssetRef, b: AssetRef, maxHops = 2): string[][] {38    const src = assetKey(a), dst = assetKey(b), res: string[][] = [];39    const direct = this.edgesFrom(src).some((e) => e.to === dst);40    if (direct) res.push([src, dst]);41    if (maxHops >= 2) {42      const mids = new Set(this.edgesFrom(src).map((e) => e.to));43      for (const m of mids) { const node = this.nodes.get(m); if (!node?.hub || m === dst || m === src) continue;44        // an intermediate must live on the source or the destination network: no detour through a third chain45        if (node.network !== a.network && node.network !== b.network) continue;46        if (this.edgesFrom(m).some((e) => e.to === dst)) res.push([src, m, dst]); }47    }48    return res;49  }50  observe(o: Observation): void { this.obs.push(o); if (this.obs.length > 5000) this.obs.splice(0, this.obs.length - 5000); }51  observations(): Observation[] { return this.obs; }52}53
Files main
Menu

Theme