Repository Public
1// SP Universal Swap Router — shared contract. Every provider adapter, the aggregator, the graph, the optimizer,2// the API and the UI speak these types. Amounts are bigint base units end to end; nothing here is ever a float3// except fiat estimates, which are display/ranking aids and never feed a transaction.4import { getAsset, getNetwork, type AssetDef, type NetworkDef } from '@spw/core';56/** An asset is identified by network + contract/mint ('native' for the chain coin). A symbol is never an identifier. */7export interface AssetRef { network: string; asset: string }8export const assetKey = (a: AssetRef): string => `${a.network}:${a.asset === 'native' ? 'native' : getNetwork(a.network).family === 'evm' ? a.asset.toLowerCase() : a.asset}`;9export const sameAsset = (a: AssetRef, b: AssetRef) => assetKey(a) === assetKey(b);10/** Resolve against the wallet's verified registry; throws for anything not allowlisted. */11export function resolveAsset(a: AssetRef): { net: NetworkDef; def: AssetDef } { return { net: getNetwork(a.network), def: getAsset(a.network, a.asset) }; }1213export type RoutingMode = 'best_price' | 'fastest' | 'balanced';1415export interface SwapQuoteRequest {16 from: AssetRef; to: AssetRef;17 /** exact input, base units of `from` */18 amountIn: bigint;19 /** the wallet's address on the source network (pays) and on the destination network (receives) */20 sender: string; recipient: string;21 /** maximum tolerated slippage, basis points (50 = 0.5 %) */22 slippageBps: number;23}2425export type StepKind = 'swap' | 'bridge' | 'native_swap' | 'wrap' | 'unwrap';26export interface RouteStep {27 kind: StepKind; provider: string;28 from: AssetRef; to: AssetRef;29 /** the venue as the provider names it (pool, DEX, bridge), for display only */30 via: string[];31 /** contracts / programs / vault addresses this step would interact with, on `network` */32 contracts: { network: string; address: string; role: 'router' | 'spender' | 'vault' | 'bridge' | 'program' }[];33}3435export type FeeKind = 'network' | 'protocol' | 'provider' | 'bridge' | 'affiliate';36export interface FeeItem {37 kind: FeeKind; asset: AssetRef; amount: bigint;38 /** true when `amountOut` is already net of this fee (so it must not be subtracted again) */39 includedInOutput: boolean;40 /** true when the figure is an estimate (e.g. gas) rather than a fixed charge */41 estimated: boolean;42}4344/** How far the wallet can take this quote today. Reported honestly per quote. */45export type Executability =46 /** the isolated signer can rebuild/verify every value-moving field itself */47 | { class: 'verified' }48 /** provider-built calldata/instructions the signer can only constrain structurally (allowlisted contract, exact allowance) */49 | { class: 'opaque'; reason: string }50 /** price discovery only: the wallet cannot execute this route */51 | { class: 'quote_only'; reason: string };5253export type RiskLevel = 'low' | 'medium' | 'high';54export interface RiskFlag { code: string; level: RiskLevel; detail?: string }55export interface RiskAssessment { level: RiskLevel; score: number; flags: RiskFlag[] }5657export interface SwapQuote {58 /** unique per quote response */59 id: string;60 provider: string;61 request: SwapQuoteRequest;62 steps: RouteStep[];63 /** expected output and the minimum the route guarantees after slippage, base units of `to` */64 amountOut: bigint; minAmountOut: bigint;65 fees: FeeItem[];66 /** provider's own estimate; null when it gives none (never invented) */67 estimatedSeconds: number | null;68 priceImpactBps: number | null;69 /** unix ms after which this quote must not be used to build a transaction */70 expiresAt: number;71 /** indicative = fast price discovery; firm = fetched for execution with the real addresses */72 firmness: 'indicative' | 'firm';73 executability: Executability;74 /** can funds come back to the sender if the destination leg fails */75 refundable: boolean | null;76 /** not refundable, but never lost either: if the destination leg stalls, the funds stay claimable there (e.g. a CCTP burn awaiting its mint) */77 recoverable?: boolean;78 /** single transaction that either fully succeeds or fully reverts */79 atomic: boolean;80 /** opaque provider payload needed to build the transaction later; never shown, never trusted */81 raw?: unknown;82}8384export interface ProviderCapabilities {85 /** network ids (from the wallet registry) this provider can act on */86 networks: string[];87 crossChain: boolean;88 /** true when the provider needs an API key that is not configured: it is skipped and reported as such */89 needsKey: boolean; configured: boolean;90 /** steps this provider can produce */91 kinds: StepKind[];92 docs: string;93}9495export interface ProviderContext {96 /** abort when the caller's deadline budget is exhausted */97 signal: AbortSignal;98 /** schema-validating HTTP helper with keep-alive; adapters must not call fetch directly */99 http: HttpClient;100 now: () => number;101}102export interface HttpClient {103 getJson(url: string, init?: { headers?: Record<string, string>; signal?: AbortSignal }): Promise<unknown>;104 postJson(url: string, body: unknown, init?: { headers?: Record<string, string>; signal?: AbortSignal }): Promise<unknown>;105}106107/** What a provider adapter must implement. Unsupported behaviour is omitted, never faked. */108export interface SwapProvider {109 id: string; name: string;110 /** provider-specific runtime limits (rate, timeout) that override the defaults, e.g. a keyless tier's quota */111 policy?: { timeoutMs?: number; ratePerSec?: number; burst?: number;112 /** tight request quota: only asked for direct cross-chain requests, never for the legs of a composed route or same-chain pairs */113 scarce?: boolean };114 capabilities(): ProviderCapabilities;115 /** cheap, local pre-filter (no network): can this provider possibly serve the pair? */116 supportsPair(from: AssetRef, to: AssetRef): boolean;117 /** real quotes from the provider's API, strictly schema-validated. Empty array = no route. Throws on provider failure. */118 quote(req: SwapQuoteRequest, ctx: ProviderContext): Promise<SwapQuote[]>;119}120121export class ProviderError extends Error {122 constructor(public provider: string, public code: 'timeout' | 'http' | 'schema' | 'rate_limited' | 'unavailable' | 'bad_response', message: string) { super(message); }123}124