SPB Git

Repository Public

thorchain.ts 25.3 KB 336 lines typescript

1// THORChain native cross-chain swaps — price discovery only (docs/swap/providers/thorchain.md).2// The engine in this file is shared with Maya Protocol (maya.ts), a THORChain fork that differs in endpoints,3// pools, error envelope and in how the memo limit is enforced. Nothing here builds, signs or sends a transaction.4//5// Verified live on 2026-11-10 against THORNode 3.21.3 (see the dossier for every source):6//  - GET /thorchain/quote/swap?from_asset&to_asset&amount&destination&liquidity_tolerance_bps7//  - GET /thorchain/inbound_addresses (chain halts, trading pauses, routers, dust thresholds)8//  - amounts are 1e8 fixed point for every asset, whatever the asset's real decimals9import { z } from 'zod';10import { NETWORKS, type NetworkDef } from '@spw/core';11import { assetKey, ProviderError, resolveAsset, sameAsset, type AssetRef, type FeeItem, type ProviderContext, type SwapProvider, type SwapQuote, type SwapQuoteRequest } from '../types.ts';12import type { ProviderEnv } from './index.ts';1314// ───────────────────────────── 1e8 fixed point ⇄ real decimals (exact integer arithmetic) ─────────────────────────────1516/** Protocol precision: every amount inside THORChain / MAYAChain is an integer number of 1e-8 units. */17export const PROTOCOL_DECIMALS = 8;18const pow10 = (n: number): bigint => 10n ** BigInt(n);19const checkDecimals = (d: number) => { if (!Number.isInteger(d) || d < 0 || d > 36) throw new RangeError(`unsupported decimals: ${d}`); };2021/** Base units of an asset → protocol 1e8 units. Rounds DOWN: the part of an 18-decimal amount below 1e-8 is not credited by the protocol. */22export function toProtocolAmount(baseUnits: bigint, decimals: number): bigint {23  checkDecimals(decimals); if (baseUnits < 0n) throw new RangeError('negative amount');24  return decimals >= PROTOCOL_DECIMALS ? baseUnits / pow10(decimals - PROTOCOL_DECIMALS) : baseUnits * pow10(PROTOCOL_DECIMALS - decimals);25}26/** Protocol 1e8 units → base units of an asset. Rounds DOWN for assets with fewer than 8 decimals (the chain cannot pay a fraction of a base unit). */27export function fromProtocolAmount(protocolUnits: bigint, decimals: number): bigint {28  checkDecimals(decimals); if (protocolUnits < 0n) throw new RangeError('negative amount');29  return decimals >= PROTOCOL_DECIMALS ? protocolUnits * pow10(decimals - PROTOCOL_DECIMALS) : protocolUnits / pow10(PROTOCOL_DECIMALS - decimals);30}31/** The limit the protocol writes in the memo for `liquidity_tolerance_bps`: floor(expected_amount_out × (10000 − bps) / 10000), in 1e8 units.32 *  Source: x/thorchain/querier_quotes.go (`outputLimit := emitAmount.Sub(outboundFeeAmount).MulUint64(10000 - bps).QuoUint64(10000)`). */33export function liquidityToleranceLimit(expectedOut: bigint, toleranceBps: number): bigint {34  if (!Number.isInteger(toleranceBps) || toleranceBps < 1 || toleranceBps > 9999) throw new RangeError('tolerance must be an integer in 1..9999 bps');35  return (expectedOut * BigInt(10_000 - toleranceBps)) / 10_000n;36}3738// ───────────────────────────── configuration of one network (THORChain, or its fork Maya) ─────────────────────────────3940export interface NativeSwapConfig {41  id: string; name: string; docs: string;42  /** pinned API base, including the module path (…/thorchain or …/mayachain) */43  api: string;44  /** protocol chain id → wallet network id */45  chains: Record<string, string>;46  /** pool assets that were `Available` when this file was last verified. Only these can be quoted; the wallet asset behind47   *  each one is resolved by chain + contract address against the wallet registry, never by ticker. */48  pools: readonly string[];49  /** memo short codes the protocol may use for the target asset (pool `short_code`), short → full notation */50  shortCodes: Record<string, string>;51  /** router contracts per wallet network, as published by the network itself; a quote naming any other router is rejected */52  routers: Record<string, string>;53  /** streaming parameters sent with every quote, or null to let the protocol decide (THORChain's advanced swap queue) */54  streaming: { interval: number } | null;55  /** true: the memo limit is enforced on the amount net of the outbound fee (THORChain ≥ v3 `swap_current.go`);56   *  false: it is enforced on the gross swap output, so the recipient's real floor is limit − outbound fee (Maya) */57  limitIsNetOfOutboundFee: boolean;58}5960/** One pool asset resolved against the wallet registry. */61export interface MappedAsset { notation: string; chain: string; ref: AssetRef; decimals: number; symbol: string }6263const EVM_ADDRESS = /^0x[0-9a-fA-F]{40}$/;64/** Resolve the pinned pool list against the wallet registry: chain must be mapped, and the contract must be a registry token65 *  on that network (or the pool must be the chain's gas asset and carry the registry's native symbol). */66export function mapPools(cfg: Pick<NativeSwapConfig, 'chains' | 'pools'>): Map<string, MappedAsset> {67  const out = new Map<string, MappedAsset>();68  for (const notation of cfg.pools) {69    const m = /^([A-Z0-9]+)\.([A-Z0-9]+)(?:-([A-Z0-9]+))?$/.exec(notation); if (!m) continue;70    const [, chain, ticker, contract] = m as unknown as [string, string, string, string | undefined];71    const network = cfg.chains[chain]; const net: NetworkDef | undefined = network ? NETWORKS[network] : undefined; if (!network || !net) continue;72    if (contract === undefined) {73      if (net.native.symbol.toUpperCase() !== ticker) continue; // e.g. a non-gas native-denominated pool we do not know74      const ref = { network, asset: 'native' };75      out.set(assetKey(ref), { notation, chain, ref, decimals: net.native.decimals, symbol: net.native.symbol });76    } else {77      const hits = net.tokens.filter((t) => t.id.toUpperCase() === contract);78      if (hits.length !== 1) continue; // not in the registry, or ambiguous once upper-cased: never guess79      const t = hits[0]!; const ref = { network, asset: t.id };80      out.set(assetKey(ref), { notation, chain, ref, decimals: t.decimals, symbol: t.symbol });81    }82  }83  return out;84}8586// ───────────────────────────── response schemas ─────────────────────────────8788const digits = z.string().regex(/^(0|[1-9]\d{0,38})$/, 'not a non-negative integer string');89const count = z.number().int().nonnegative().max(1e9);90export const QuoteSchema = z.looseObject({91  inbound_address: z.string().regex(/^[A-Za-z0-9:]{20,120}$/),92  inbound_confirmation_blocks: count.optional(), inbound_confirmation_seconds: count.optional(),93  outbound_delay_blocks: count.optional(), outbound_delay_seconds: count.optional(),94  fees: z.looseObject({ asset: z.string().min(3).max(120), affiliate: digits, outbound: digits, liquidity: digits, total: digits,95    slippage_bps: z.number().int().min(0).max(10_000), total_bps: z.number().int().min(0).max(10_000) }),96  router: z.string().regex(EVM_ADDRESS).optional(),97  expiry: z.number().int().positive(),98  warning: z.string().max(2000).optional(), notes: z.string().max(4000).optional(),99  dust_threshold: digits.optional(), recommended_min_amount_in: digits,100  recommended_gas_rate: digits.optional(), gas_rate_units: z.string().max(40).optional(),101  memo: z.string().min(5).max(250),102  expected_amount_out: digits,103  max_streaming_quantity: count.optional(), streaming_swap_blocks: count.optional(), streaming_swap_seconds: count.optional(),104  total_swap_seconds: count.optional(),105});106export type NativeQuote = z.infer<typeof QuoteSchema>;107/** THORNode answers errors as HTTP 4xx `{code, message}`; MAYANode as HTTP 200 `{error}`. */108const ErrorSchema = z.union([z.looseObject({ message: z.string() }), z.looseObject({ error: z.string() })]);109export const InboundSchema = z.array(z.looseObject({110  chain: z.string().regex(/^[A-Z0-9]{2,10}$/), address: z.string().min(20).max(160), router: z.string().regex(EVM_ADDRESS).optional(),111  halted: z.boolean(), global_trading_paused: z.boolean().optional(), chain_trading_paused: z.boolean().optional(),112  dust_threshold: digits.optional(), outbound_fee: digits.optional(),113})).min(1).max(200);114export type InboundAddresses = z.infer<typeof InboundSchema>;115116/** Why a chain cannot be used right now, or null. `halted` covers HALT<CHAIN>CHAIN / solvency halts, the two pause flags cover117 *  HALTTRADING and HALT<CHAIN>TRADING mimirs (THORNode derives all three from mimir in `inbound_addresses`). */118export function chainBlocked(inbound: InboundAddresses, chain: string): string | null {119  const e = inbound.find((x) => x.chain === chain);120  if (!e) return `${chain}: no inbound address (chain not active)`;121  if (e.halted) return `${chain}: chain halted`;122  if (e.global_trading_paused) return 'trading paused globally';123  if (e.chain_trading_paused) return `${chain}: trading paused`;124  return null;125}126127// ───────────────────────────── memo ─────────────────────────────128129export interface ParsedSwapMemo { asset: string; destination: string; limit: bigint; interval: number | null; quantity: number | null }130/** Parse `=:ASSET:DEST:LIM[/INTERVAL/QUANTITY]`. Anything else — another handler, a refund override, an affiliate, an131 *  aggregator hop, scientific notation — is rejected: this adapter never asks for those, so their presence is hostile. */132export function parseSwapMemo(memo: string): ParsedSwapMemo {133  const p = memo.split(':');134  if (p.length !== 4) throw new Error(`unexpected memo shape (${p.length} fields)`);135  if (p[0] !== '=') throw new Error('memo is not a market swap');136  const lim = p[3]!.split('/');137  if (lim.length !== 1 && lim.length !== 3) throw new Error('unexpected limit/streaming field');138  if (!lim.every((x) => /^(0|[1-9]\d{0,38})$/.test(x))) throw new Error('non-integer limit/streaming field');139  if (p[2]!.includes('/')) throw new Error('memo carries a refund address override');140  return { asset: p[1]!, destination: p[2]!, limit: BigInt(lim[0]!), interval: lim.length === 3 ? Number(lim[1]) : null, quantity: lim.length === 3 ? Number(lim[2]) : null };141}142/** The memo's target asset must be the requested one: full notation, the pinned short code, or the protocol's own143 *  abbreviations of that exact notation (`CHAIN.TICKER`, `CHAIN.TICKER-<contract suffix>`). */144export function memoAssetMatches(field: string, notation: string, shortCodes: Record<string, string>): boolean {145  if (shortCodes[field] !== undefined) return shortCodes[field] === notation;146  const f = field.toUpperCase();147  if (f === notation) return true;148  const dash = notation.indexOf('-'); if (dash < 0) return false;149  const head = notation.slice(0, dash), contract = notation.slice(dash + 1);150  if (f === head) return true;151  return f.startsWith(head + '-') && f.length > head.length + 1 && contract.endsWith(f.slice(head.length + 1));152}153const normAddress = (net: NetworkDef, a: string) => (net.family === 'evm' ? a.toLowerCase() : net.family === 'bch' ? a.replace(/^bitcoincash:/i, '') : a);154155// ───────────────────────────── provider ─────────────────────────────156157/** Protocol messages that mean "this swap cannot be done now" (→ no route) rather than "the provider is broken". */158const NO_ROUTE = [/dust threshold/i, /min swap amount/i, /not enough asset to pay for fees/i, /pool\(.*\) is not available/i, /pool does not exist/i,159  /halt/i, /paused/i, /less than price limit/i, /zero emit asset/i, /swap (amount )?too small/i, /outbound amount does not meet requirements/i];160const STALE = /stale state/i;161const INBOUND_TTL_MS = 15_000; // halt flags only; the vault address is never cached (it comes with each quote)162const MAX_EXPIRY_AHEAD_S = 3600;163164export function createNativeSwapProvider(cfg: NativeSwapConfig): SwapProvider & { assets: Map<string, MappedAsset> } {165  const assets = mapPools(cfg);166  const networks = [...new Set([...assets.values()].map((a) => a.ref.network))];167  const headers = { 'x-client-id': 'sp-wallet' };168  let inboundCache: { at: number; data: InboundAddresses } | null = null;169  let seq = 0;170  const schemaError = (m: string) => new ProviderError(cfg.id, 'schema', m);171172  async function inbound(ctx: ProviderContext): Promise<InboundAddresses> {173    if (inboundCache && ctx.now() - inboundCache.at < INBOUND_TTL_MS && ctx.now() >= inboundCache.at) return inboundCache.data;174    const p = InboundSchema.safeParse(await ctx.http.getJson(`${cfg.api}/inbound_addresses`, { headers, signal: ctx.signal }));175    if (!p.success) throw schemaError(`inbound_addresses: ${p.error.issues[0]?.path.join('.')} ${p.error.issues[0]?.message}`);176    inboundCache = { at: ctx.now(), data: p.data };177    return p.data;178  }179  /** → validated quote, or a protocol refusal message */180  async function fetchQuote(url: string, ctx: ProviderContext): Promise<{ quote: NativeQuote } | { refusal: string }> {181    let body: unknown;182    try { body = await ctx.http.getJson(url, { headers, signal: ctx.signal }); }183    catch (e) {184      const errBody = e instanceof ProviderError && e.code === 'http' ? ErrorSchema.safeParse((e as ProviderError & { body?: unknown }).body) : null;185      if (!errBody?.success) throw e;186      return { refusal: 'message' in errBody.data ? errBody.data.message as string : errBody.data.error as string };187    }188    if (body && typeof body === 'object' && !Array.isArray(body) && typeof (body as { error?: unknown }).error === 'string' && !('expected_amount_out' in body)) return { refusal: (body as { error: string }).error };189    const p = QuoteSchema.safeParse(body);190    if (!p.success) throw schemaError(`quote: ${p.error.issues[0]?.path.join('.')} ${p.error.issues[0]?.message}`);191    return { quote: p.data };192  }193194  const supportsPair = (from: AssetRef, to: AssetRef): boolean => { try { return !sameAsset(from, to) && assets.has(assetKey(from)) && assets.has(assetKey(to)); } catch { return false; } };195196  return {197    id: cfg.id, name: cfg.name, assets,198    capabilities: () => ({ networks, crossChain: true, needsKey: false, configured: true, kinds: ['native_swap'], docs: cfg.docs }),199    supportsPair,200201    async quote(req: SwapQuoteRequest, ctx: ProviderContext): Promise<SwapQuote[]> {202      if (!supportsPair(req.from, req.to)) return [];203      const src = assets.get(assetKey(req.from))!, dst = assets.get(assetKey(req.to))!;204      // decimals come from the wallet registry, re-read here so a registry/asset-table mismatch can never go unnoticed205      if (resolveAsset(req.from).def.decimals !== src.decimals || resolveAsset(req.to).def.decimals !== dst.decimals) throw schemaError('registry decimals changed');206      const srcNet = NETWORKS[src.ref.network]!, dstNet = NETWORKS[dst.ref.network]!;207      // the protocol's limit parameter is an integer number of basis points in 1..9999; zero tolerance cannot be expressed208      if (!Number.isInteger(req.slippageBps) || req.slippageBps < 1 || req.slippageBps > 9999) return [];209      if (req.amountIn <= 0n) return [];210      const amount = toProtocolAmount(req.amountIn, src.decimals);211      if (amount === 0n) return [];212213      const q = new URLSearchParams({ from_asset: src.notation, to_asset: dst.notation, amount: amount.toString(), destination: req.recipient, liquidity_tolerance_bps: String(req.slippageBps) });214      if (cfg.streaming) { q.set('streaming_interval', String(cfg.streaming.interval)); q.set('streaming_quantity', '0'); }215      // one round: the quote and (unless fresh) the halt flags, concurrently216      // A public gateway load-balances over several nodes; one that lags answers "refusing quote on node with stale state" within217      // milliseconds. That refusal (and only that one) is retried once, which normally lands on another node.218      const url = `${cfg.api}/quote/swap?${q}`;219      const quoteP = fetchQuote(url, ctx).then((first) => ('refusal' in first && STALE.test(first.refusal) ? fetchQuote(url, ctx) : first));220      quoteP.catch(() => undefined); // if inbound() throws first, the quote's own failure must not become an unhandled rejection221      const inb = await inbound(ctx);222      if (chainBlocked(inb, src.chain) || chainBlocked(inb, dst.chain)) { await quoteP.catch(() => undefined); return []; }223      const res = await quoteP;224      if ('refusal' in res) {225        if (STALE.test(res.refusal)) throw new ProviderError(cfg.id, 'unavailable', res.refusal.slice(0, 160));226        if (NO_ROUTE.some((r) => r.test(res.refusal))) return [];227        throw new ProviderError(cfg.id, 'http', res.refusal.slice(0, 160));228      }229      const r = res.quote;230231      // ── everything below treats the answer as hostile ──232      if (r.fees.asset !== dst.notation) throw schemaError(`fees are in ${r.fees.asset.slice(0, 60)}, not ${dst.notation}`);233      const fee = { affiliate: BigInt(r.fees.affiliate), outbound: BigInt(r.fees.outbound), liquidity: BigInt(r.fees.liquidity), total: BigInt(r.fees.total) };234      if (fee.affiliate !== 0n) throw schemaError('affiliate fee on a quote requested without affiliate');235      if (fee.total !== fee.affiliate + fee.outbound + fee.liquidity) throw schemaError('fee total does not add up');236      const expected = BigInt(r.expected_amount_out);237238      let memo: ParsedSwapMemo;239      try { memo = parseSwapMemo(r.memo); } catch (e) { throw schemaError(`memo: ${(e as Error).message}`); }240      if (!memoAssetMatches(memo.asset, dst.notation, cfg.shortCodes)) throw schemaError(`memo targets ${memo.asset.slice(0, 60)}, not ${dst.notation}`);241      if (normAddress(dstNet, memo.destination) !== normAddress(dstNet, req.recipient)) throw schemaError('memo pays another recipient');242      const limit = liquidityToleranceLimit(expected, req.slippageBps);243      if (memo.limit !== limit) throw schemaError(`memo limit ${memo.limit} is not expected_amount_out less ${req.slippageBps} bps (${limit})`);244      if (cfg.streaming ? memo.interval !== cfg.streaming.interval : memo.interval !== null && memo.interval !== 0) throw schemaError('unexpected streaming interval in memo');245246      const srcInbound = inb.find((x) => x.chain === src.chain)!;247      if (srcNet.family === 'evm') {248        const pinned = cfg.routers[src.ref.network];249        if (!r.router || !pinned || r.router.toLowerCase() !== pinned.toLowerCase()) throw schemaError(`router ${r.router ?? '(none)'} is not the pinned ${cfg.name} router for ${src.ref.network}`);250        if (srcInbound.router?.toLowerCase() !== pinned.toLowerCase()) throw schemaError('inbound_addresses names another router');251        if (!EVM_ADDRESS.test(r.inbound_address) || r.inbound_address.toLowerCase() === pinned.toLowerCase()) throw schemaError('inbound vault is not a plain EVM address');252      } else if (r.router !== undefined) throw schemaError('router on a non-EVM source chain');253      if (normAddress(srcNet, r.inbound_address) === normAddress(srcNet, req.sender)) throw schemaError('inbound vault is the sender');254255      const nowS = Math.floor(ctx.now() / 1000);256      if (r.expiry <= nowS) throw new ProviderError(cfg.id, 'bad_response', 'quote already expired');257      if (r.expiry > nowS + MAX_EXPIRY_AHEAD_S) throw schemaError('expiry too far in the future');258259      // protocol minimums: at or below dust the inbound is ignored (funds lost); below the recommended minimum a refund cannot pay its own fee260      if (r.dust_threshold !== undefined && amount <= BigInt(r.dust_threshold)) return [];261      if (amount < BigInt(r.recommended_min_amount_in)) return [];262263      const quantity = memo.quantity !== null && memo.quantity > 0 ? memo.quantity : memo.interval !== null ? (r.max_streaming_quantity ?? 0) : 1;264      const streaming = quantity > 1;265      // what the recipient is guaranteed if the swap executes in full (see NativeSwapConfig.limitIsNetOfOutboundFee)266      const floor1e8 = cfg.limitIsNetOfOutboundFee ? limit : limit > fee.outbound ? limit - fee.outbound : 0n;267      const amountOut = fromProtocolAmount(expected, dst.decimals), minAmountOut = fromProtocolAmount(floor1e8, dst.decimals);268      if (amountOut <= 0n || minAmountOut <= 0n) return [];269270      const feeItem = (kind: FeeItem['kind'], v: bigint): FeeItem => ({ kind, asset: dst.ref, amount: fromProtocolAmount(v, dst.decimals), includedInOutput: true, estimated: true });271      const contracts: SwapQuote['steps'][number]['contracts'] = [{ network: src.ref.network, address: r.inbound_address, role: 'vault' }];272      if (r.router) contracts.push({ network: src.ref.network, address: r.router, role: 'router' });273      return [{274        id: `${cfg.id}-${ctx.now()}-${++seq}`, provider: cfg.id, request: req,275        steps: [{ kind: 'native_swap', provider: cfg.id, from: req.from, to: req.to,276          via: [cfg.name, `${src.notation} → ${dst.notation}`, streaming ? `streaming swap: ${quantity} sub-swaps${memo.interval ? `, every ${memo.interval} block(s)` : ''}` : 'single swap'], contracts }],277        amountOut, minAmountOut,278        // liquidity (slip) fee stays in the pools; the outbound fee pays the destination-chain gas of the vault's outbound transaction.279        // Both are already deducted from expected_amount_out. The sender's own source-chain network fee is not part of the quote.280        fees: [feeItem('protocol', fee.liquidity), feeItem('network', fee.outbound)],281        estimatedSeconds: r.total_swap_seconds ?? null,282        priceImpactBps: r.fees.slippage_bps,283        expiresAt: r.expiry * 1000,284        firmness: 'indicative',285        executability: { class: 'quote_only', reason: `${cfg.name} swaps are a transfer to a rotating vault address that only the network's API can name; the isolated signer cannot yet verify that address or rebuild the memo itself (docs/swap/providers/${cfg.id}.md)` },286        refundable: true, // amount ≥ recommended_min_amount_in: a failed swap is refunded to the sender, less the source-chain outbound fee287        atomic: false,288        raw: { quote: r, request: Object.fromEntries(q), amount1e8: amount.toString(), limit1e8: limit.toString(), guaranteed1e8: floor1e8.toString(),289          streaming: { quoted: streaming, interval: memo.interval, quantity, maxQuantity: r.max_streaming_quantity ?? null, seconds: r.streaming_swap_seconds ?? null },290          dustThreshold1e8: r.dust_threshold ?? null, recommendedMinAmountIn1e8: r.recommended_min_amount_in,291          inboundConfirmationSeconds: r.inbound_confirmation_seconds ?? null, outboundDelaySeconds: r.outbound_delay_seconds ?? null },292      }];293    },294  };295}296297// ───────────────────────────── THORChain ─────────────────────────────298299/** Liquify public gateway — the endpoint the official developer docs list (https://dev.thorchain.org/concepts/connecting-to-thorchain.html). */300export const THORCHAIN_API = 'https://gateway.liquify.com/chain/thorchain_api/thorchain';301export const THORCHAIN_API_VERSION = '3.21.3'; // /thorchain/version on 2026-11-10302303export const THORCHAIN_CONFIG: NativeSwapConfig = {304  id: 'thorchain', name: 'THORChain', docs: 'https://dev.thorchain.org/swap-guide/quickstart-guide.html', api: THORCHAIN_API,305  chains: { BTC: 'bitcoin', ETH: 'ethereum', LTC: 'litecoin', DOGE: 'dogecoin', BCH: 'bitcoincash', AVAX: 'avalanche', BSC: 'bsc', BASE: 'base',306    GAIA: 'cosmos', XRP: 'xrpl', TRON: 'tron', SOL: 'solana' },307  // GET /thorchain/pools on 2026-11-10: every pool with status "Available" on a chain the wallet has (ZEC.ZEC, THOR.* have no wallet network).308  // Pools whose contract is not in the wallet registry are listed anyway and dropped by mapPools(); Staged pools (e.g. ETH.LINK) are not listed.309  pools: [310    'BTC.BTC', 'ETH.ETH', 'LTC.LTC', 'DOGE.DOGE', 'BCH.BCH', 'AVAX.AVAX', 'BSC.BNB', 'BASE.ETH', 'GAIA.ATOM', 'XRP.XRP', 'TRON.TRX', 'SOL.SOL',311    'ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48', 'ETH.USDT-0XDAC17F958D2EE523A2206206994597C13D831EC7',312    'ETH.DAI-0X6B175474E89094C44DA98B954EEDEAC495271D0F', 'ETH.WBTC-0X2260FAC5E5542A773AA44FBCFEDF7C193BC2C599',313    'ETH.AAVE-0X7FC66500C84A76AD7E9C93437BFC5AC33E2DDAE9',314    'AVAX.USDC-0XB97EF9EF8734C71904D8002F8B6BC66DD9C48A6E', 'AVAX.USDT-0X9702230A8EA53601F5CD2DC00FDBC13D4DF4A8C7',315    'BASE.USDC-0X833589FCD6EDB6E08F4C7C32D4F71B54BDA02913',316    'BSC.USDC-0X8AC76A51CC950D9822D68B83FE1AD97B32CD580D', 'BSC.USDT-0X55D398326F99059FF775485246999027B3197955',317    'BSC.ETH-0X2170ED0880AC9A755FD29B2688956BD959F933F8', 'BSC.BTCB-0X7130D2A12B9BCBFAE4F2634D864A1EE1CE3EAD9C', // Binance-Peg ETH and BTCB, in the registry since v5318    'TRON.USDT-TR7NHQJEKQXGTCI8Q8ZY4PL8OTSZGJLJ6T',319  ],320  // pool `short_code` values from GET /thorchain/pools321  shortCodes: { a: 'AVAX.AVAX', f: 'BASE.ETH', c: 'BCH.BCH', s: 'BSC.BNB', b: 'BTC.BTC', d: 'DOGE.DOGE', e: 'ETH.ETH', g: 'GAIA.ATOM', l: 'LTC.LTC', o: 'SOL.SOL', tr: 'TRON.TRX', x: 'XRP.XRP' },322  // `router` in GET /thorchain/inbound_addresses and in every active Asgard vault (GET /thorchain/vaults/asgard) on 2026-11-10323  routers: {324    ethereum: '0xD37BbE5744D730a1d98d8DC97c42F0Ca46aD7146',325    avalanche: '0x00dc6100103BC402d490aEE3F9a5560cBd91f1d4',326    base: '0x00dc6100103BC402d490aEE3F9a5560cBd91f1d4',327    bsc: '0xb30ec53f98ff5947ede720d32ac2da7e52a5f56b',328  },329  streaming: null, // advanced swap queue: the network sizes rapid streaming itself and writes LIM/0/N in the memo when N > 1330  limitIsNetOfOutboundFee: true,331};332333export function createThorchainProvider(_env: ProviderEnv = {}): SwapProvider {334  return createNativeSwapProvider(THORCHAIN_CONFIG);335}336
Files main
Menu

Theme