Repository Public
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