diff --git a/CHANGELOG.md b/CHANGELOG.md index 4237f0d0..99164be8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,10 @@ - added: `AGENTS.md` and `.cursor/BUGBOT.md`, recording the swap plugin conventions that recent provider reviews settled. - added: A test suite for the swap plugin template, covering the max-quote probe, limit handling, the trust boundary on provider amounts, and memo cleaning. A new integration can copy it alongside the template. +- added: HoudiniSwap swap plugin: privacy-routed CEX swaps with forward and reverse (receive-amount) quotes, swap-to-address destinations with destination-tag support for memo chains, and a cached-fixture acceptance suite. Honors `EdgeSwapRequest.privacy`, so a request that does not demand privacy may also take Houdini's standard routes, which are the only ones served between the 10 USD standard floor and the 25 USD private floor. Rate-limited calls back off behind the window the API reports and fail as a rate limit rather than as an unavailable route. Only the exact-out path reports a fixed rate, since that is the only path the provider serves fixed rates on. Token lookups are memoized including misses and expire after ten minutes, so a chain the provider lists but serves no native coin for costs one lookup per window rather than one per quote, and a chain the provider later lists is picked up without restarting the app; a lookup the provider failed to answer is not cached, since a rate limit says nothing about whether the chain is served. - changed: The swap plugin template now models the max-quote probe, the trust boundary on provider-returned amounts, integer native-amount rounding, and funds-safe memo cleaning, so a new integration starts from them rather than rediscovering them in review. - changed: `docs/CREATING_AN_EXCHANGE_PLUGIN.md` covers max quotes and amount rounding in depth, and ends with a pre-PR checklist drawn from findings on shipped integrations. +- changed: `checkInvalidTokenIds` takes an `allowSameAsset` option, for a privacy provider whose main flow is routing an asset to itself. ## 2.53.0 (2026-08-18) diff --git a/src/index.ts b/src/index.ts index 70355588..ae532f7b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { makeChangellyPlugin } from './swap/central/changelly' import { makeChangeNowPlugin } from './swap/central/changenow' import { makeExolixPlugin } from './swap/central/exolix' import { makeGodexPlugin } from './swap/central/godex' +import { makeHoudiniPlugin } from './swap/central/houdini' import { makeLetsExchangePlugin } from './swap/central/letsexchange' import { makeNexchangePlugin } from './swap/central/nexchange' import { makeNymPlugin } from './swap/central/nym' @@ -40,6 +41,7 @@ const plugins = { cosmosibc: makeCosmosIbcPlugin, exolix: makeExolixPlugin, godex: makeGodexPlugin, + houdini: makeHoudiniPlugin, letsexchange: makeLetsExchangePlugin, lifi: makeLifiPlugin, nexchange: makeNexchangePlugin, diff --git a/src/mappings/houdini.ts b/src/mappings/houdini.ts new file mode 100644 index 00000000..74747392 --- /dev/null +++ b/src/mappings/houdini.ts @@ -0,0 +1,107 @@ +/** + * Maps Edge `EdgeCurrencyPluginId` -> HoudiniSwap chain `shortName` (v2 `GET /chains`), + * or `null` when Houdini has no compatible chain for that asset. + * + * Derived from the live `GET /chains` list intersected with Edge's currency + * pluginIds. Memo-required chains (`Chain.memoNeeded`, e.g. XRP/XLM) are + * supported: the plugin forwards a destination memo as `destinationTag` on + * order creation. The IBC-family chains (coreum/osmosis/axelar) stay `null` + * because Houdini reports no `memoNeeded` flag and a permissive `^.*$` address + * validation for them, so their payout semantics are not yet trustworthy. + * + * This table answers "what does Houdini call this chain", not "does Houdini + * serve it". Whether a chain has a tradable native coin is discovered at + * runtime by `resolveTokenId`, which declines with the same `SwapCurrencyError` + * the whitelist check throws, so a name that turns out to be unserved costs one + * cached lookup rather than a wrong answer. Keeping the two questions apart is + * what lets the table stay correct when Houdini adds or drops a native. + */ + +import { EdgeCurrencyPluginId } from '../util/edgeCurrencyPluginIds' + +export const houdini = new Map() +houdini.set('abstract', null) +houdini.set('algorand', 'algorand') +houdini.set('amoy', null) +houdini.set('arbitrum', 'arbitrum') +houdini.set('avalanche', 'avalanche') +houdini.set('axelar', null) +houdini.set('badcoin', null) +houdini.set('base', 'base') +houdini.set('binance', null) +houdini.set('binancesmartchain', 'bsc') +houdini.set('bitcoin', 'bitcoin') +houdini.set('bitcoincash', 'bitcoincash') +houdini.set('bitcoincashtestnet', null) +houdini.set('bitcoingold', null) +houdini.set('bitcoingoldtestnet', null) +houdini.set('bitcoinsv', 'bsv') +houdini.set('bitcointestnet', null) +houdini.set('bitcointestnet4', null) +houdini.set('bobevm', null) +houdini.set('botanix', null) +houdini.set('calibration', null) +houdini.set('cardano', 'cardano') +houdini.set('cardanotestnet', null) +houdini.set('celo', 'celo') +houdini.set('coreum', null) +houdini.set('cosmoshub', 'cosmoshub-4') +houdini.set('dash', 'dash') +houdini.set('digibyte', null) +houdini.set('dogecoin', 'doge') +houdini.set('eboost', null) +houdini.set('ecash', 'eCash') +houdini.set('eos', null) +houdini.set('ethDev', null) +houdini.set('ethereum', 'ethereum') +houdini.set('ethereumclassic', null) +houdini.set('ethereumpow', null) +houdini.set('fantom', 'fantom') +houdini.set('feathercoin', null) +houdini.set('filecoin', null) +houdini.set('filecoinfevm', null) +houdini.set('filecoinfevmcalibration', null) +houdini.set('fio', null) +houdini.set('groestlcoin', null) +houdini.set('hedera', 'hedera') +houdini.set('holesky', null) +houdini.set('hyperevm', 'hyperevm') +houdini.set('liberland', null) +houdini.set('liberlandtestnet', null) +houdini.set('litecoin', 'litecoin') +houdini.set('mayachain', null) +houdini.set('monad', null) +houdini.set('monero', 'monero') +houdini.set('nym', null) +houdini.set('opbnb', 'opbnb') +houdini.set('optimism', 'optimism') +houdini.set('osmosis', null) +houdini.set('piratechain', null) +houdini.set('pivx', 'pivx') +houdini.set('polkadot', 'polkadot') +houdini.set('polygon', 'polygon') +houdini.set('pulsechain', 'pulsechain') +houdini.set('qtum', null) +houdini.set('ravencoin', null) +houdini.set('ripple', 'ripple') +houdini.set('rsk', 'rootstock') +houdini.set('sepolia', null) +houdini.set('smartcash', null) +houdini.set('solana', 'solana') +houdini.set('sonic', 'sonic') +houdini.set('stellar', 'xlm') +houdini.set('sui', 'sui') +houdini.set('suitestnet', null) +houdini.set('telos', 'telos') +houdini.set('tezos', null) +houdini.set('thorchainrune', 'thorchain') +houdini.set('thorchainrunestagenet', null) +houdini.set('ton', 'ton') +houdini.set('tron', 'tron') +houdini.set('ufo', null) +houdini.set('vertcoin', null) +houdini.set('wax', null) +houdini.set('zano', null) +houdini.set('zcash', 'Zcash') +houdini.set('zcoin', null) +houdini.set('zksync', 'zksync-era') diff --git a/src/swap/central/houdini.ts b/src/swap/central/houdini.ts new file mode 100644 index 00000000..64568533 --- /dev/null +++ b/src/swap/central/houdini.ts @@ -0,0 +1,1033 @@ +import { ceil, floor, gt, lt } from 'biggystring' +import { + asArray, + asDate, + asEither, + asJSON, + asMaybe, + asNull, + asNumber, + asObject, + asOptional, + asString, + asValue +} from 'cleaners' +import { + EdgeCorePluginOptions, + EdgeCurrencyWallet, + EdgeFetchResponse, + EdgeMemo, + EdgeSpendInfo, + EdgeSwapInfo, + EdgeSwapPlugin, + EdgeSwapQuote, + EdgeSwapRequest, + EdgeTokenId, + SwapAboveLimitError, + SwapBelowLimitError, + SwapCurrencyError +} from 'edge-core-js/types' + +import { houdini as houdiniMapping } from '../../mappings/houdini' +import { EdgeCurrencyPluginId } from '../../util/edgeCurrencyPluginIds' +import { + checkInvalidTokenIds, + checkWhitelistedMainnetCodes, + CurrencyPluginIdSwapChainCodeMap, + ensureInFuture, + getContractAddresses, + getMaxSwappable, + InvalidTokenIds, + makeSwapPluginQuote, + mapToRecord, + SwapOrder +} from '../../util/swapHelpers' +import { + convertRequest, + denominationToNative, + getAddress, + memoType, + nativeToDenomination, + snooze +} from '../../util/utils' +import { asNumberString, EdgeSwapRequestPlugin, StringMap } from '../types' +import { asOptionalBlank } from './changenow' + +const pluginId = 'houdini' + +export const swapInfo: EdgeSwapInfo = { + pluginId, + isDex: false, + displayName: 'HoudiniSwap', + supportEmail: 'support@houdiniswap.com' +} + +// Houdini's v2 partner API. Note the auth header is `Authorization: :` +// (no `Bearer`); every endpoint returns 402 without it. +const asInitOptions = asObject({ + apiKey: asString, + apiSecret: asString +}) + +const orderUri = 'https://houdiniswap.com/order/' +const uri = 'https://api-partner.houdiniswap.com/v2/' + +// Houdini quotes/exchanges are keyed by an opaque token id, so destination +// addresses pass straight through. Zcash is the lone exception: Houdini only +// accepts transparent `t1` addresses, mirroring how ChangeNow special-cases it. +const addressTypeMap: StringMap = { + zcash: 'transparentAddress' +} + +/** + * A swap-to-address destination arrives as a core-built synthetic wallet whose + * id carries this prefix. Synthetic wallets hold exactly one pasted address + * (already validated by the caller), so typed-address lookups do not apply, + * and they may expose destination memos through a `getMemos` method. + */ +const SYNTHETIC_WALLET_ID_PREFIX = 'synthetic://' + +/** + * Assets this plugin refuses outright. Empty: the provider's own token list is + * the authority on what it serves, and anything absent from it already fails + * the token-id lookup. The shared blocked-token defaults still apply. + */ +const INVALID_TOKEN_IDS: InvalidTokenIds = { from: {}, to: {} } + +interface SyntheticDestinationMethods { + getMemos?: () => Promise +} + +/** + * Reads the destination memos (e.g. an XRP destination tag) off a core-built + * synthetic destination wallet. Real wallets have no `getMemos`; their payout + * goes to the user's own address, which needs no tag. + */ +async function getDestinationMemos( + toWallet: EdgeCurrencyWallet +): Promise { + const synthetic = toWallet as EdgeCurrencyWallet & SyntheticDestinationMethods + if (synthetic.getMemos == null) return [] + // Called on the wallet rather than destructured first: the destination is a + // bridged object, and a bridge proxy's methods need their receiver to route + // the call. Every other wallet call in this plugin reads the same way. + return await synthetic.getMemos() +} + +/** + * Edge `EdgeCurrencyPluginId` -> Houdini chain `shortName`. Chains that Houdini + * cannot serve map to `null` and are rejected up front by + * `checkWhitelistedMainnetCodes`. + */ +export const MAINNET_CODE_TRANSCRIPTION: CurrencyPluginIdSwapChainCodeMap = mapToRecord( + houdiniMapping +) + +const asHoudiniToken = asObject({ + id: asString, + address: asEither(asNull, asString), + chain: asString +}) + +const asHoudiniTokensResponse = asObject({ + tokens: asArray(asMaybe(asHoudiniToken)) +}) + +const asHoudiniQuote = asObject({ + quoteId: asString, + type: asString, + amountOut: asNumber, + amountIn: asOptional(asNumber), + min: asOptional(asNumber), + max: asOptional(asNumber), + minOut: asOptional(asNumber), + maxOut: asOptional(asNumber), + validUntil: asOptional(asString) +}) + +type HoudiniQuote = ReturnType + +const asHoudiniQuotesResponse = asObject({ + quotes: asArray(asMaybe(asHoudiniQuote)) +}) + +/** + * The API's error envelope. `message` is human-readable, EXCEPT on a + * `VALIDATION_ERROR`, where it is the generic "Validation Failed" and the + * actionable text (an expired quote, a rejected address) sits under + * `fields..message`. + */ +const asHoudiniApiError = asMaybe( + asJSON( + asObject({ + message: asString, + // The machine-readable half of the envelope (`VALIDATION_ERROR`, + // `STATIC_DEPOSIT_IN_USE`, `ADDRESS_TO_IN_DEPOSIT_LOG`). Read through + // `asNumberString` because a provider that returns amounts as either a + // number or a string usually does the same with codes. + code: asOptional(asNumberString), + fields: asOptional( + asObject(asMaybe(asObject({ message: asString }).withRest)) + ) + }).withRest + ) +) + +/** + * The API's machine-readable error code, or `undefined` when the body is not + * the error envelope. Classification reads THIS rather than searching the raw + * response text: a code that also appears inside a human-readable message + * would match a substring test on a body that means something else. + */ +function houdiniErrorCode(text: string): string | undefined { + return asHoudiniApiError(text)?.code +} + +/** + * The most specific human-readable reason in an error body, or `undefined` if + * the body is not the API's error envelope at all. Field messages win over the + * top-level one, which is generic exactly when they are present. + */ +function houdiniErrorMessage(text: string): string | undefined { + const apiError = asHoudiniApiError(text) + if (apiError == null) return undefined + + const fieldMessages = Object.values(apiError.fields ?? {}) + .map(field => field?.message) + .filter((message): message is string => message != null && message !== '') + if (fieldMessages.length > 0) return fieldMessages.join('; ') + + return apiError.message +} + +/** + * The 429 envelope, per Houdini's rate-limits-and-tiers doc. `retryAfter` is + * in seconds and names the earliest moment the window reopens. + */ +const asHoudiniRateLimitError = asMaybe( + asJSON( + asObject({ + type: asValue('RATE_LIMIT_EXCEEDED'), + retryAfter: asOptional(asNumber), + limit: asOptional(asNumber), + windowMs: asOptional(asNumber) + }).withRest + ) +) + +// Backoff for a rate-limited call. Houdini is an aggregator whose per-pair +// availability fluctuates, so a 429 must never be mistaken for a missing +// route: the call is retried behind `retryAfter` and, once the retries are +// spent, fails with a message that says rate limit rather than unavailable. +const RATE_LIMIT_MAX_RETRIES = 3 +const RATE_LIMIT_MIN_DELAY_MS = 1000 +const RATE_LIMIT_MAX_DELAY_MS = 30000 + +/** + * How long to wait before retrying a 429, given the attempt number and the + * `retryAfter` seconds the API reported (if any). + * + * `RATE_LIMIT_MAX_DELAY_MS` bounds OUR OWN doubling; it is not a ceiling on + * the window the provider asked for. Houdini's 1-per-minute exchange budget + * reports `retryAfter` near 60, so capping that at 30s retried while still + * inside the window, drew another 429, and spent the retries for nothing. + */ +export function rateLimitDelayMs( + attempt: number, + retryAfterSec: number | undefined +): number { + // `retryAfter` is the floor the API asked for; the doubling on top of it + // keeps a burst from re-colliding at the moment the window reopens. + const apiFloorMs = retryAfterSec == null ? 0 : retryAfterSec * 1000 + const baseMs = Math.max(apiFloorMs, RATE_LIMIT_MIN_DELAY_MS) + const backoffMs = Math.min(baseMs * 2 ** attempt, RATE_LIMIT_MAX_DELAY_MS) + return Math.max(backoffMs, apiFloorMs) +} + +/** + * Houdini quotes live about a minute. `validUntil` is the API's own word on it + * when present; the constant is the documented default for when it is not. + */ +const QUOTE_LIFETIME_MS = 60000 + +export function quoteValidUntilMs(quote: HoudiniQuote): number { + const { validUntil } = quote + if (validUntil != null) { + // The API reports this as Unix SECONDS inside a string ("1783037880"), + // which `new Date` reads as an invalid date rather than as a timestamp. + const seconds = Number(validUntil) + if (isFinite(seconds) && seconds > 0) return seconds * 1000 + + const parsed = new Date(validUntil).valueOf() + if (!isNaN(parsed)) return parsed + } + return Date.now() + QUOTE_LIFETIME_MS +} + +const asHoudiniOrder = asObject({ + houdiniId: asString, + depositAddress: asString, + /** + * Deposit tag/memo for a memo-based deposit chain (an XRP destination tag, + * an XLM memo). `asOptionalBlank(asNumberString)` rather than + * `asOptional(asString)`: a NUMERIC tag (the common shape, including the + * valid tag `0`) fails a string-only cleaner and takes the whole order down + * with it, and an EMPTY STRING becomes an empty `EdgeMemo` on the deposit, + * which sends an effectively untagged deposit. Both are lost-funds paths on + * those chains. No response Houdini has returned in testing carried this + * field, so its shape is the repo convention rather than observed. + */ + depositTag: asOptionalBlank(asNumberString), + expires: asOptional(asDate), + inAmount: asNumber, + outAmount: asNumber +}) + +/** + * Whether a token entry is the chain's own coin rather than a contract token. + * Houdini spells "no contract" as either `null` or an empty string. + */ +function isNativeToken(token: { address: string | null }): boolean { + return token.address == null || token.address === '' +} + +/** + * Convert a JSON float to a decimal string, expanding any scientific notation + * (Houdini returns amounts as BSON doubles, so very small/large values can come + * back as e.g. `2.53e-05`). biggystring needs a plain decimal string. + */ +function floatToDecimalString(value: number): string { + if (!isFinite(value)) return '0' + const str = String(value) + if (!str.includes('e') && !str.includes('E')) return str + + // `toFixed` only expands the small end: at magnitudes >= 1e21 it returns the + // same exponential string it was given, so a large amount would reach + // biggystring still in scientific notation. Expand the mantissa by hand for + // those, and let `toFixed` handle the negative exponents it does cover. + const [mantissa, exponent] = str.split(/[eE]/) + const power = Number(exponent) + if (power < 0) return value.toFixed(20).replace(/0+$/, '').replace(/\.$/, '') + + const sign = mantissa.startsWith('-') ? '-' : '' + const [whole, fraction = ''] = mantissa.replace('-', '').split('.') + const digits = whole + fraction + const zeros = power - fraction.length + return zeros >= 0 + ? sign + digits + '0'.repeat(zeros) + : sign + digits.slice(0, digits.length + zeros) + '.' + digits.slice(zeros) +} + +/** + * Convert a provider amount in display units to a WHOLE atomic-unit native + * string. `denominationToNative` is a plain multiply, so a Houdini amount + * carrying more decimals than the asset's denomination yields a fractional + * native string, which Edge's native-amount contract does not allow. + * + * The rounding DIRECTION is load-bearing, per the pre-PR checklist in + * `docs/CREATING_AN_EXCHANGE_PLUGIN.md`: + * - `'up'` for a minimum, so the floor Edge enforces never lands BELOW the + * provider's real floor and the deposit is rejected on arrival + * - `'down'` for a maximum, for the receive amount and for the deposit + * amount, so none of them is ever larger than what the provider honors + */ +function decimalToNativeAmount( + wallet: EdgeCurrencyWallet, + decimalAmount: string, + tokenId: EdgeTokenId, + rounding: 'up' | 'down' +): string { + const native = denominationToNative(wallet, decimalAmount, tokenId) + return rounding === 'up' ? ceil(native, 0) : floor(native, 0) +} + +function floatToNativeAmount( + wallet: EdgeCurrencyWallet, + value: number, + tokenId: EdgeTokenId, + rounding: 'up' | 'down' +): string { + return decimalToNativeAmount( + wallet, + floatToDecimalString(value), + tokenId, + rounding + ) +} + +export function makeHoudiniPlugin(opts: EdgeCorePluginOptions): EdgeSwapPlugin { + const { io, log } = opts + const { fetchCors = io.fetch } = io + const { apiKey, apiSecret } = asInitOptions(opts.initOptions) + + const headers = { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: `${apiKey}:${apiSecret}` + } + + // Houdini's partner API is server-to-server and rejects browser-origin + // requests: the core runs plugins inside a WebView, so `io.fetch` carries an + // Origin / Sec-Fetch-* header set that Houdini answers with HTTP 403. Force + // Edge's CORS proxy (`corsBypass: 'always'`) so each call is made host-side, + // matching the server-to-server contract the API expects. + const corsBypass = 'always' as const + + /** + * Run a cleaner over a response body, logging the payload when it does not + * fit. A cleaner failure is the one error whose message says nothing about + * what actually arrived, and the payload is what a report needs to be + * actionable. Prescribed by `src/swap/central/template.ts`. + */ + function cleanResponse(cleaner: (raw: any) => T, raw: unknown): T { + try { + return cleaner(raw) + } catch (error: unknown) { + log.warn('Unexpected Houdini API response:', JSON.stringify(raw)) + throw error + } + } + + /** + * Every call to the partner API. Retries a rate-limited call behind the + * window the API reports, and turns an exhausted retry budget into an error + * that names the rate limit. Any other status is handed back untouched for + * the caller to interpret. + * + * `validUntilMs` is the moment the thing being sent goes stale (a quote id + * expires about 60s after it is issued). Waiting out a window that outlives + * it retries something the API will reject anyway, so the call fails as a + * rate limit right away rather than hanging the user for the full window and + * then reporting an expired quote. + */ + async function fetchHoudini( + path: string, + init: { method?: string; body?: string } = {}, + validUntilMs?: number + ): Promise { + for (let attempt = 0; ; ++attempt) { + const response = await fetchCors(uri + path, { + ...init, + headers, + corsBypass + }) + if (response.status !== 429) return response + + const text = await response.text() + const rateLimit = asHoudiniRateLimitError(text) + if (attempt >= RATE_LIMIT_MAX_RETRIES) { + throw new Error( + 'HoudiniSwap: rate limit exceeded, please try again shortly' + ) + } + + const delayMs = rateLimitDelayMs(attempt, rateLimit?.retryAfter) + if (validUntilMs != null && Date.now() + delayMs >= validUntilMs) { + log.warn( + `Houdini rate limited for ${delayMs}ms, longer than this quote lives` + ) + throw new Error( + 'HoudiniSwap: rate limit exceeded, please try again shortly' + ) + } + log.warn( + `Houdini rate limited (${ + rateLimit?.limit ?? '?' + }/window), retrying in ${delayMs}ms` + ) + await snooze(delayMs) + } + } + + /** + * Memoized chain -> (addressKey -> tokenId) lookups, so repeat quotes on the + * same assets do not re-hit `GET /tokens`. + * + * A cached `undefined` is a MISS the provider answered: it listed this chain's + * tokens and none matched. That is a stable fact for the session, and caching + * it is what keeps a chain the provider serves no native for from spending a + * rate-limited call on every quote. This is the whole mechanism by which an + * unserved chain declines cheaply, so the chain table never has to assert + * which chains are served. + * + * Entries are the in-flight promises, so concurrent askers share one call. + * + * Entries EXPIRE. Houdini renames, delists and relists assets, so a hit + * cached for the life of the plugin eventually names a token id the provider + * has dropped, and a cached miss refuses a pair the provider has since + * listed. `.cursor/BUGBOT.md`'s `catalog-cache-expiry` is explicit that "these + * do not change" is not a reason to skip the TTL. + */ + const TOKEN_ID_CACHE_TTL_MS = 10 * 60 * 1000 + interface TokenIdCacheEntry { + lookup: Promise + expires: number + } + const tokenIdCache = new Map() + + async function resolveTokenId( + chain: string, + contractAddress: string | undefined + ): Promise { + const addressKey = contractAddress?.toLowerCase() ?? 'native' + const cacheKey = `${chain}:${addressKey}` + const cached = tokenIdCache.get(cacheKey) + if (cached != null && cached.expires > Date.now()) + return await cached.lookup + + // The IN-FLIGHT promise is what gets cached, not its result. A quote + // resolves both legs with `Promise.all`, so a same-asset quote asks the + // same question twice at once; caching only on completion lets both miss + // and spend two calls on one answer. + const lookup = fetchTokenId(chain, contractAddress, addressKey) + const entry: TokenIdCacheEntry = { + lookup, + expires: Date.now() + TOKEN_ID_CACHE_TTL_MS + } + tokenIdCache.set(cacheKey, entry) + // A rejected lookup must not stick, for the same reason a failure is not a + // decline: the provider never answered. Only evict THIS entry: a retry may + // already have replaced it, and dropping the newer one would spend an extra + // call to learn the same answer. + lookup.catch(() => { + if (tokenIdCache.get(cacheKey) === entry) tokenIdCache.delete(cacheKey) + }) + return await lookup + } + + async function fetchTokenId( + chain: string, + contractAddress: string | undefined, + addressKey: string + ): Promise { + // The lowercased key goes on the wire, not Edge's own spelling. EVM token + // metadata carries checksummed (mixed-case) contract addresses, and a + // case-sensitive filter on the provider's side answers a checksummed + // address with no tokens at all, which this plugin would then report as a + // pair Houdini cannot route. Matching below is lowercased for the same + // reason, so sending the same form keeps query and match in agreement. + const query = + contractAddress != null + ? `tokens?chain=${chain}&address=${addressKey}&pageSize=100` + : `tokens?chain=${chain}&mainnet=true&pageSize=100` + const response = await fetchHoudini(query) + if (!response.ok) { + // Nothing is cached, and nothing is RETURNED either. Returning a miss + // here would be indistinguishable from the provider answering "no such + // token", so a bad minute at the API would read to the user as a pair + // Houdini cannot route. Throwing names the real cause, the same way the + // quote path surfaces its own failures. + const text = await response.text() + log.warn('Houdini tokens lookup error:', text) + throw new Error(`Houdini tokens returned ${response.status}: ${text}`) + } + const { tokens } = cleanResponse( + asHoudiniTokensResponse, + await response.json() + ) + + const match = tokens.find(token => { + if (token == null || token.chain !== chain) return false + // A chain's own coin carries no contract address, which the API spells + // either as `null` or as an empty string depending on the chain. Testing + // for `null` alone missed the empty-string chains (ALGO, XEC, HYPE, S, + // XLM, ZEC), so none of them could resolve a token id at all. + if (contractAddress == null) return isNativeToken(token) + return token.address?.toLowerCase() === addressKey + }) + + return match?.id + } + + const fetchSwapQuoteInner = async ( + request: EdgeSwapRequestPlugin, + probeOnly: boolean = false + ): Promise => { + const { fromWallet, toWallet, quoteFor, nativeAmount } = request + + // A `max` request is resolved to a balance-sized `from` request by + // `getMaxSwappable` before it reaches this function. + const reverseQuote = quoteFor === 'to' + + const fromMainnet = + MAINNET_CODE_TRANSCRIPTION[ + fromWallet.currencyInfo.pluginId as EdgeCurrencyPluginId + ] + const toMainnet = + MAINNET_CODE_TRANSCRIPTION[ + toWallet.currencyInfo.pluginId as EdgeCurrencyPluginId + ] + if (fromMainnet == null || toMainnet == null) { + throw new SwapCurrencyError(swapInfo, request) + } + + const { fromContractAddress, toContractAddress } = getContractAddresses( + request + ) + + // A synthetic (swap-to-address) destination holds exactly one pasted, + // caller-validated address, so a typed-address lookup does not apply. + const isSyntheticDestination = toWallet.id.startsWith( + SYNTHETIC_WALLET_ID_PREFIX + ) + const toAddressType = isSyntheticDestination + ? undefined + : addressTypeMap[toWallet.currencyInfo.pluginId] + + const [ + fromTokenId, + toTokenId, + fromAddress, + toAddress, + toMemos + ] = await Promise.all([ + resolveTokenId(fromMainnet, fromContractAddress), + resolveTokenId(toMainnet, toContractAddress), + getAddress(fromWallet, addressTypeMap[fromWallet.currencyInfo.pluginId]), + getAddress(toWallet, toAddressType), + getDestinationMemos(toWallet) + ]) + + if (fromTokenId == null || toTokenId == null) { + throw new SwapCurrencyError(swapInfo, request) + } + + // The quote amount is in the display units of whichever side the caller + // fixed: the `from` amount normally, or the `to` (receive) amount for a + // reverse quote, which Houdini prices via `amountType=receive`. + const exchangeAmount = reverseQuote + ? nativeToDenomination(toWallet, nativeAmount, request.toTokenId) + : nativeToDenomination(fromWallet, nativeAmount, request.fromTokenId) + + // Fetch quotes and keep the best private route. Pricing by the receive + // amount (`amountType=receive`) is only offered on fixed-rate quotes. + const quoteResponse = await fetchHoudini( + `quotes?amount=${exchangeAmount}&from=${fromTokenId}&to=${toTokenId}` + + (reverseQuote ? '&amountType=receive&fixed=true' : '') + ) + if (!quoteResponse.ok) { + const text = await quoteResponse.text() + // Surface the API's own human-readable message when it carries one + // (e.g. "Amount is too low, minimum is 25 USD") instead of raw JSON: + const apiMessage = houdiniErrorMessage(text) + throw new Error( + apiMessage != null + ? `HoudiniSwap: ${apiMessage}` + : `Houdini quotes returned ${quoteResponse.status}: ${text}` + ) + } + const { quotes } = cleanResponse( + asHoudiniQuotesResponse, + await quoteResponse.json() + ) + + // A `max` probe deliberately quotes the full PRE-FEE balance to discover + // the ceiling, so its answer must CLAMP through `getMaxSpendable` rather + // than reject the swap outright: a balance above the route's maximum still + // makes a perfectly good max swap once the network fee is subtracted. + // Minimums still apply to the probe, since no amount below the floor is + // reachable by trimming. + const enforceMax = !probeOnly + + // Forward limits (`min`/`max`) are in the `from` token's display units; + // reverse limits (`minOut`/`maxOut`) are in the `to` token's. A reverse + // quote must also clear the route's from-side bounds with its own priced + // send amount (`amountIn`), which the API enforces at order creation. + const isWithinLimits = (candidate: HoudiniQuote): boolean => { + if (reverseQuote) { + const amountIn = + candidate.amountIn == null + ? undefined + : floatToDecimalString(candidate.amountIn) + return ( + (candidate.minOut == null || + !lt(exchangeAmount, floatToDecimalString(candidate.minOut))) && + (!enforceMax || + candidate.maxOut == null || + !gt(exchangeAmount, floatToDecimalString(candidate.maxOut))) && + (amountIn == null || + candidate.min == null || + !lt(amountIn, floatToDecimalString(candidate.min))) && + (!enforceMax || + amountIn == null || + candidate.max == null || + !gt(amountIn, floatToDecimalString(candidate.max))) + ) + } + return ( + (candidate.min == null || + !lt(exchangeAmount, floatToDecimalString(candidate.min))) && + (!enforceMax || + candidate.max == null || + !gt(exchangeAmount, floatToDecimalString(candidate.max))) + ) + } + + // A request that demands privacy takes `private` (multi-exchange) routes + // only, since a `standard` route settles through a single exchange leg + // that can relink the two sides. Everything else may also take standard + // routes, which matters below Houdini's 25 USD private floor, where + // standard is the only thing on offer down to 10 USD. Private stays + // preferred wherever the API offers it. + // + // Houdini prices exact-out on fixed-rate quotes alone, which its private + // routing does not serve, so a private request priced by the receive side + // finds nothing and declines. That is the honest answer: the caller can + // re-price by the send side and keep its privacy, which is what the send + // scene's fixed-to fallback does. + const privateOnly = request.privacy === 'required' + const candidateQuotes = quotes + .filter( + (quote): quote is HoudiniQuote => + quote != null && + (quote.type === 'private' || + (!privateOnly && quote.type === 'standard')) + ) + // Rank private routes first, then by best rate: highest output for a + // fixed input, or lowest input for a fixed output. Comparison runs + // through biggystring on the expanded decimal strings rather than on the + // JSON floats, per `.cursor/BUGBOT.md`'s `biggystring-not-floats`, which + // covers sorting as well as arithmetic. + .sort((a, b) => { + if (a.type !== b.type) return a.type === 'private' ? -1 : 1 + if (reverseQuote) { + // A route that priced no send amount sorts last: there is nothing to + // rank it by, and the order call would have no amount to bound. + if (a.amountIn == null || b.amountIn == null) { + return a.amountIn == null ? (b.amountIn == null ? 0 : 1) : -1 + } + const aIn = floatToDecimalString(a.amountIn) + const bIn = floatToDecimalString(b.amountIn) + return lt(aIn, bIn) ? -1 : gt(aIn, bIn) ? 1 : 0 + } + const aOut = floatToDecimalString(a.amountOut) + const bOut = floatToDecimalString(b.amountOut) + return gt(aOut, bOut) ? -1 : lt(aOut, bOut) ? 1 : 0 + }) + + if (candidateQuotes.length === 0) { + throw new SwapCurrencyError(swapInfo, request) + } + + // Keep the routes that actually accept this amount, rather than + // rejecting the swap when only the top route is out of range. + const inRangeQuotes = candidateQuotes.filter(isWithinLimits) + if (inRangeQuotes.length === 0) { + // No route accepts the amount. Surface the most permissive limit, in + // the units (and direction) of the side the caller fixed. The widest + // bound is picked with biggystring on the expanded decimal strings, not + // `Math.min`/`Math.max` over the JSON floats. + const widest = ( + pick: (candidate: HoudiniQuote) => number | undefined, + wider: (a: string, b: string) => boolean + ): string | undefined => + candidateQuotes + .map(pick) + .filter((value): value is number => value != null) + .map(floatToDecimalString) + .reduce( + (best, value) => + best == null || wider(value, best) ? value : best, + undefined + ) + const smallestMin = widest( + candidate => (reverseQuote ? candidate.minOut : candidate.min), + lt + ) + const largestMax = widest( + candidate => (reverseQuote ? candidate.maxOut : candidate.max), + gt + ) + const limitWallet = reverseQuote ? toWallet : fromWallet + const limitTokenId = reverseQuote + ? request.toTokenId + : request.fromTokenId + const limitDirection = reverseQuote ? 'to' : 'from' + if (smallestMin != null && lt(exchangeAmount, smallestMin)) { + throw new SwapBelowLimitError( + swapInfo, + // A minimum rounds UP, so the floor Edge enforces is never below the + // provider's own. + decimalToNativeAmount(limitWallet, smallestMin, limitTokenId, 'up'), + limitDirection + ) + } + if (largestMax != null && gt(exchangeAmount, largestMax)) { + throw new SwapAboveLimitError( + swapInfo, + // A maximum rounds DOWN, so the ceiling Edge offers is never above + // the provider's own. + decimalToNativeAmount(limitWallet, largestMax, limitTokenId, 'down'), + limitDirection + ) + } + + // A reverse quote can also fail the route's from-side bounds with its + // priced send amount. Report those in from units, and against the WIDEST + // from-side bound across the candidates rather than the top route's own: + // the to-side check above already reads every candidate, and reporting + // the best-ranked route's narrower floor told the user a limit no route + // actually imposes. + if (reverseQuote) { + const bestIn = + candidateQuotes[0].amountIn == null + ? undefined + : floatToDecimalString(candidateQuotes[0].amountIn) + const smallestFromMin = widest(candidate => candidate.min, lt) + const largestFromMax = widest(candidate => candidate.max, gt) + if ( + bestIn != null && + smallestFromMin != null && + lt(bestIn, smallestFromMin) + ) { + throw new SwapBelowLimitError( + swapInfo, + decimalToNativeAmount( + fromWallet, + smallestFromMin, + request.fromTokenId, + 'up' + ), + 'from' + ) + } + if ( + bestIn != null && + largestFromMax != null && + gt(bestIn, largestFromMax) + ) { + throw new SwapAboveLimitError( + swapInfo, + decimalToNativeAmount( + fromWallet, + largestFromMax, + request.fromTokenId, + 'down' + ), + 'from' + ) + } + } + throw new SwapCurrencyError(swapInfo, request) + } + + // Create the exchange. Assets and amounts ride on the quote; only the + // destination (and optional refund) addresses go on the order, plus the + // destination memo (e.g. an XRP destination tag) when one was provided. + // A fixed-rate route's static deposit address can be held by another live + // order (HTTP 409 STATIC_DEPOSIT_IN_USE); fall through to the next-best + // in-range route when that happens. + const destinationTag = toMemos.length > 0 ? toMemos[0].value : undefined + let order: ReturnType | undefined + let lastError = '' + // The route the order was created on, for the trust boundary below: on a + // reverse quote its published from-side ceiling is the only local bound + // the deposit amount has. + let usedQuote: HoudiniQuote | undefined + + if (probeOnly) { + // A `max` request runs this function twice: once for `getMaxSwappable` + // to learn the shape of the spend, then again for the real quote at the + // adjusted amount. `getMaxSwappable` reads nothing but `spendInfo`, so + // creating a real exchange for that probe spends one of Houdini's + // one-per-minute exchange slots and guarantees the follow-up create is + // rate limited, stalling every max quote for the retry window. Stand in + // the user's own refund address, which is on the from chain exactly as + // the deposit address would be, so fee estimation sees the same shape. + const best = inRangeQuotes[0] + usedQuote = best + return makeHoudiniSwapOrder( + { + houdiniId: '', + depositAddress: fromAddress, + depositTag: undefined, + expires: undefined, + inAmount: best.amountIn ?? 0, + outAmount: best.amountOut + }, + true + ) + } + + for (const candidate of inRangeQuotes.slice(0, 3)) { + const orderBody = { + addressTo: toAddress, + quoteId: candidate.quoteId, + refundAddress: fromAddress, + ...(destinationTag == null ? {} : { destinationTag }) + } + const orderResponse = await fetchHoudini( + 'exchanges', + { method: 'POST', body: JSON.stringify(orderBody) }, + quoteValidUntilMs(candidate) + ) + if (orderResponse.ok) { + order = cleanResponse(asHoudiniOrder, await orderResponse.json()) + usedQuote = candidate + break + } + const text = await orderResponse.text() + const apiMessage = houdiniErrorMessage(text) + lastError = + apiMessage != null + ? `HoudiniSwap: ${apiMessage}` + : `Houdini exchange returned ${orderResponse.status}: ${text}` + if ( + orderResponse.status !== 409 || + houdiniErrorCode(text) !== 'STATIC_DEPOSIT_IN_USE' + ) { + throw new Error(lastError) + } + } + if (order == null) { + throw new Error(lastError) + } + return makeHoudiniSwapOrder(order) + + function makeHoudiniSwapOrder( + order: ReturnType, + isProbe: boolean = false + ): SwapOrder { + // Both amounts round DOWN: the deposit so it never exceeds what the user + // asked to send, and the receive amount so the figure shown is never + // larger than what actually arrives. + const fromNativeAmount = floatToNativeAmount( + fromWallet, + order.inAmount, + request.fromTokenId, + 'down' + ) + const toNativeAmount = floatToNativeAmount( + toWallet, + order.outAmount, + request.toTokenId, + 'down' + ) + + // TRUST BOUNDARY. `order.inAmount` comes back from Houdini and becomes a + // SIGNED SPEND below, so it is bounded before it can move funds: a + // malformed or compromised response must not be able to spend more of + // the source asset than the quote justified. + // + // A `from` quote pins the source amount locally, so the request's own + // amount is the bound. A reverse (`to`) quote pins the RECEIVE amount + // instead, so the send side is Houdini's to price; the bound there is + // the route's published from-side ceiling, which the API enforces at + // order creation anyway. A route that publishes no ceiling leaves + // nothing to check, which is the honest state rather than a guess. + const requestedBound = + request.quoteFor === 'from' + ? request.nativeAmount + : usedQuote?.max == null + ? undefined + : floatToNativeAmount( + fromWallet, + usedQuote.max, + request.fromTokenId, + 'down' + ) + if (requestedBound != null && gt(fromNativeAmount, requestedBound)) { + throw new Error( + 'HoudiniSwap returned a deposit amount above the requested amount' + ) + } + + const memos: EdgeMemo[] = + order.depositTag == null + ? [] + : [ + { + type: memoType(fromWallet.currencyInfo.pluginId), + value: order.depositTag + } + ] + + const spendInfo: EdgeSpendInfo = { + tokenId: request.fromTokenId, + spendTargets: [ + { + nativeAmount: fromNativeAmount, + publicAddress: order.depositAddress + } + ], + memos, + networkFeeOption: 'high', + // The probe spend is NEVER broadcast: it exists only so + // `getMaxSpendable` can price the network fee before a real order (and + // its deposit address) exists, so it targets the user's OWN from-chain + // address. Engines that compare the spend target against their own + // public key reject that with `SpendToSelfError` (every EVM chain, + // where the public key IS the address), and that error escapes + // `getMaxSwappable` and fails every max swap from an EVM wallet. The + // real order below keeps all checks. + ...(isProbe ? { skipChecks: true } : {}), + assetAction: { + assetActionType: 'swap' + }, + savedAction: { + actionType: 'swap', + swapInfo, + orderId: order.houdiniId, + orderUri: orderUri + order.houdiniId, + // Only the exact-out path asks for `fixed=true`, and Houdini serves + // fixed rates on that path alone: a forward quote floats, private or + // standard. Reporting every quote as fixed showed users a locked + // receive amount for a leg whose rate can still move. + isEstimate: !reverseQuote, + toAsset: { + pluginId: toWallet.currencyInfo.pluginId, + tokenId: request.toTokenId, + nativeAmount: toNativeAmount + }, + fromAsset: { + pluginId: fromWallet.currencyInfo.pluginId, + tokenId: request.fromTokenId, + nativeAmount: fromNativeAmount + }, + payoutAddress: toAddress, + payoutWalletId: toWallet.id, + refundAddress: fromAddress + } + } + + return { + request, + spendInfo, + swapInfo, + fromNativeAmount, + expirationDate: ensureInFuture(order.expires) + } + } + } + + const out: EdgeSwapPlugin = { + swapInfo, + + async fetchSwapQuote(req: EdgeSwapRequest): Promise { + const request = convertRequest(req) + + // Same-asset is allowed here where other plugins reject it: routing an + // asset to itself through the mixer is this provider's main flow, not a + // user mistake. The blocked-token checks still apply. + checkInvalidTokenIds(INVALID_TOKEN_IDS, request, swapInfo, { + allowSameAsset: true + }) + checkWhitelistedMainnetCodes( + MAINNET_CODE_TRANSCRIPTION, + request, + swapInfo + ) + + // The probe flag rides the extra argument `getMaxSwappable` forwards, so + // only the max-sizing pass skips creating an exchange. + const newRequest = await getMaxSwappable( + fetchSwapQuoteInner, + request, + true + ) + const swapOrder = await fetchSwapQuoteInner(newRequest) + return await makeSwapPluginQuote(swapOrder) + } + } + return out +} diff --git a/src/swap/types.ts b/src/swap/types.ts index 45c30094..d2b8646f 100644 --- a/src/swap/types.ts +++ b/src/swap/types.ts @@ -28,6 +28,15 @@ export interface EdgeSwapRequestPlugin { quoteFor: 'from' | 'max' | 'to' fromCurrencyCode: string toCurrencyCode: string + + /** + * Route privacy requirement, carried through from `EdgeSwapRequest`. + * `'required'` means the caller needs a route that keeps the sender + * unlinkable to the recipient, and a plugin that cannot offer one must + * decline rather than answer with a transparent route. Declared here because + * the installed `edge-core-js` types predate the field. + */ + privacy?: 'required' } export const asNumberString = (raw: any): string => { diff --git a/src/util/swapHelpers.ts b/src/util/swapHelpers.ts index 9e9f1907..3eaaab8d 100644 --- a/src/util/swapHelpers.ts +++ b/src/util/swapHelpers.ts @@ -360,14 +360,26 @@ const defaultInvalidCodes: InvalidTokenIds = { to: { ethereum: ['1985365e9f78359a9b6ad760e32412f4a445e862' /* REP */] } } +interface CheckInvalidTokenIdsOpts { + /** + * Permit a swap from an asset to ITSELF. Rejected by default, since for an + * ordinary provider it is a no-op the user cannot have meant. A privacy + * provider is the exception: routing an asset to itself through a mixer is + * the point, not a mistake, so those plugins opt out. + */ + allowSameAsset?: boolean +} + /** * Throws if either currency code has been disabled by the plugin */ export function checkInvalidTokenIds( invalidCodes: InvalidTokenIds, request: EdgeSwapRequestPlugin, - swapInfo: EdgeSwapInfo + swapInfo: EdgeSwapInfo, + opts: CheckInvalidTokenIdsOpts = {} ): void { + const { allowSameAsset = false } = opts const { fromPluginId, toPluginId } = getPluginIds(request) const { fromTokenId, toTokenId } = request @@ -394,7 +406,7 @@ export function checkInvalidTokenIds( check(defaultInvalidCodes, 'from', fromPluginId, fromTokenId) || check(invalidCodes, 'to', toPluginId, toTokenId) || check(defaultInvalidCodes, 'to', toPluginId, toTokenId) || - isSameAsset(request) + (!allowSameAsset && isSameAsset(request)) ) throw new SwapCurrencyError(swapInfo, request) } diff --git a/test/houdini.test.ts b/test/houdini.test.ts new file mode 100644 index 00000000..a5bca32b --- /dev/null +++ b/test/houdini.test.ts @@ -0,0 +1,1239 @@ +/** + * Acceptance tests for the HoudiniSwap plugin. + * + * Runs the real `makeHoudiniPlugin` quote -> create-exchange flow. Every + * Houdini API response is cached to disk (`test/houdiniFixtures/`) on first + * fetch and replayed afterwards, so repeat runs make zero live calls and stay + * inside the partner API budget (5/min, 20/hr, 50/day quotes; 1/min, 5/hr, + * 10/day exchanges). On a live first run, a single backoff retry handles the + * 1-per-minute exchange limit. + * + * Live re-recording needs credentials in `testconfig.json` -> `HOUDINI_INIT`, + * the same way other partners are wired; cached replays run with placeholder + * credentials (the fixture key ignores headers). The on-device pieces a swap + * quote does not depend on (address derivation, `makeSpend`) are stubbed; the + * tests exercise token resolution, quoting (both directions), order creation + * (including a destination tag), and `SwapOrder` construction. + */ +import { expect } from 'chai' +import { makeConfig } from 'cleaner-config' +import { createHash } from 'crypto' +import { + EdgeCurrencyWallet, + EdgeMemo, + EdgeSwapQuote, + EdgeSwapRequest +} from 'edge-core-js/types' +import fs from 'fs' +import { describe, it } from 'mocha' +import path from 'path' + +import { + makeHoudiniPlugin, + quoteValidUntilMs, + rateLimitDelayMs +} from '../src/swap/central/houdini' +import { asTestConfig } from './testconfig' + +const config = makeConfig(asTestConfig, './testconfig.json') + +const FIXTURE_DIR = path.join(__dirname, 'houdiniFixtures') + +interface CachedResponse { + status: number + ok: boolean + body: string +} + +interface FetchResponseLike { + ok: boolean + status: number + json: () => Promise + text: () => Promise +} + +const sleep = async (ms: number): Promise => + await new Promise(resolve => setTimeout(resolve, ms)) + +const fixturePath = (method: string, url: string, body: string): string => { + const hash = createHash('sha256') + .update(`${method} ${url} ${body}`) + .digest('hex') + .slice(0, 24) + return path.join(FIXTURE_DIR, `${hash}.json`) +} + +const toResponse = (cached: CachedResponse): FetchResponseLike => ({ + ok: cached.ok, + status: cached.status, + json: async () => JSON.parse(cached.body), + text: async () => cached.body +}) + +/** The last create-exchange request body, for asserting what went out. */ +let lastExchangeBody: string | undefined + +/** + * Disk-caching fetch. Replays a saved fixture when present; otherwise hits the + * live API, persists the response, and retries once on a rate-limit response + * after the server-reported backoff. + */ +const cachingFetch = async ( + url: string, + opts: { + method?: string + headers?: { [key: string]: string } + body?: string + } = {} +): Promise => { + const method = opts.method ?? 'GET' + const body = opts.body ?? '' + if (method === 'POST' && url.endsWith('/exchanges')) { + lastExchangeBody = body + } + const file = fixturePath(method, url, body) + + if (fs.existsSync(file)) { + const cached: CachedResponse = JSON.parse(fs.readFileSync(file, 'utf8')) + return toResponse(cached) + } + + for (let attempt = 0; attempt < 2; attempt++) { + const live = await fetch(url, { + method, + headers: opts.headers, + body: opts.body + }) + const text = await live.text() + + if (live.status === 429 || text.includes('RATE_LIMIT_EXCEEDED')) { + let retryAfter = 15 + try { + retryAfter = JSON.parse(text).retryAfter ?? 15 + } catch (error: unknown) {} + if (attempt === 0) { + await sleep((retryAfter + 2) * 1000) + continue + } + } + + const cached: CachedResponse = { + status: live.status, + ok: live.ok, + body: text + } + fs.mkdirSync(FIXTURE_DIR, { recursive: true }) + fs.writeFileSync(file, JSON.stringify(cached, null, 2)) + return toResponse(cached) + } + throw new Error('Unreachable') +} + +const fakeLog = Object.assign(() => {}, { + warn: () => {}, + error: () => {}, + crash: async () => {}, + breadcrumb: () => {} +}) + +interface FakeToken { + currencyCode: string + denominations: Array<{ name: string; multiplier: string }> + networkLocation: { contractAddress: string } +} + +// Records the last spend the plugin built, so the tests can prove the order's +// deposit address and swap action landed on the transaction. +interface SpendCapture { + depositAddress?: string + savedAction?: unknown + /** The spendInfo `getMaxSwappable` priced, so the probe can be asserted. */ + maxSpendInfo?: { skipChecks?: boolean } +} + +const makeFakeWallet = ( + params: { + pluginId: string + currencyCode: string + decimals: number + address: string + tokens?: { [tokenId: string]: FakeToken } + /** Native balance, for the `max` path that sizes a quote off it. */ + balance?: string + /** + * What `getMaxSpendable` reports, when it differs from the balance. A real + * engine subtracts the network fee here, which is what lets a max swap off + * an above-limit balance land inside the route's range. + */ + maxSpendable?: string + }, + capture: SpendCapture +): EdgeCurrencyWallet => { + const { + pluginId, + currencyCode, + decimals, + address, + tokens = {}, + balance = '0', + maxSpendable = balance + } = params + const multiplier = `1${'0'.repeat(decimals)}` + const currencyInfo = { + pluginId, + currencyCode, + denominations: [{ name: currencyCode, multiplier }] + } + const wallet = { + id: `${pluginId}-wallet`, + currencyInfo, + currencyConfig: { currencyInfo, allTokens: tokens }, + balanceMap: new Map([[null, balance]]), + // The whole balance is spendable; these tests care about how many calls + // the max path makes, not about fee arithmetic. The spend-to-self guard + // mirrors an EVM engine, where the public key IS the address: without + // `skipChecks` on the probe, that engine rejects the max-sizing spend and + // the error escapes `getMaxSwappable`. + getMaxSpendable: async (spendInfo: { + skipChecks?: boolean + spendTargets: Array<{ publicAddress: string }> + }) => { + capture.maxSpendInfo = spendInfo + const { skipChecks = false } = spendInfo + for (const spendTarget of spendInfo.spendTargets) { + if (!skipChecks && spendTarget.publicAddress === address) { + throw new Error('SpendToSelfError') + } + } + return maxSpendable + }, + getAddresses: async () => [ + { publicAddress: address, addressType: 'publicAddress' }, + { publicAddress: address, addressType: 'transparentAddress' } + ], + makeSpend: async (spendInfo: { + tokenId: string | null + spendTargets: Array<{ nativeAmount: string; publicAddress: string }> + savedAction: unknown + }) => { + capture.depositAddress = spendInfo.spendTargets[0].publicAddress + capture.savedAction = spendInfo.savedAction + return { + savedAction: spendInfo.savedAction, + nativeAmount: `-${spendInfo.spendTargets[0].nativeAmount}`, + networkFee: '0', + tokenId: spendInfo.tokenId, + currencyCode, + metadata: {} + } + } + } + // The tests only need the wallet surface the plugin and swap helpers touch. + return (wallet as unknown) as EdgeCurrencyWallet +} + +/** + * Mirrors the core-built synthetic destination wallet of a swap-to-address + * request: one pasted address (`publicAddress` type only), a `synthetic://` + * id, no spend methods, and destination memos exposed through `getMemos`. + */ +const makeFakeSyntheticDestination = (params: { + pluginId: string + currencyCode: string + decimals: number + toAddress: string + toMemos?: EdgeMemo[] +}): EdgeCurrencyWallet => { + const { pluginId, currencyCode, decimals, toAddress, toMemos = [] } = params + const multiplier = `1${'0'.repeat(decimals)}` + const currencyInfo = { + pluginId, + currencyCode, + denominations: [{ name: currencyCode, multiplier }] + } + const wallet = { + id: `synthetic://${pluginId}`, + currencyInfo, + currencyConfig: { currencyInfo, allTokens: {} }, + getAddresses: async () => [ + { publicAddress: toAddress, addressType: 'publicAddress' } + ], + getMemos: async () => toMemos + } + return (wallet as unknown) as EdgeCurrencyWallet +} + +/** 1300 S, the send amount the sim drive used. */ +const SONIC_NATIVE_AMOUNT = '1300000000000000000000' + +describe('houdini', function () { + // Live re-recording waits out the 1/min exchange budget: + this.timeout(120000) + + // Live calls need real credentials from `testconfig.json` -> `HOUDINI_INIT`. + // Cache hits do not (the fixture key ignores headers), so committed fixtures + // replay anywhere with placeholder credentials. + const initOptions = + config.HOUDINI_INIT === false + ? { apiKey: 'CACHE_REPLAY', apiSecret: 'CACHE_REPLAY' } + : config.HOUDINI_INIT + + // Minimal plugin options: only `initOptions`, `io.fetch`/`io.fetchCors`, and + // `log` are touched by the plugin. + const pluginOpts = ({ + initOptions, + io: { fetch: cachingFetch, fetchCors: cachingFetch }, + log: fakeLog + } as unknown) as Parameters[0] + const plugin = makeHoudiniPlugin(pluginOpts) + + const usdcContract = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' + const usdcTokenId = usdcContract.slice(2) + + const capture: SpendCapture = {} + + const btcWallet = makeFakeWallet( + { + pluginId: 'bitcoin', + currencyCode: 'BTC', + decimals: 8, + address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa' + }, + capture + ) + const ethWallet = makeFakeWallet( + { + pluginId: 'ethereum', + currencyCode: 'ETH', + decimals: 18, + address: '0x9f1f9a5c0f1d9a5c0f1d9a5c0f1d9a5c0f1d9a5c', + tokens: { + [usdcTokenId]: { + currencyCode: 'USDC', + denominations: [{ name: 'USDC', multiplier: '1000000' }], + networkLocation: { contractAddress: usdcContract } + } + } + }, + capture + ) + + const fetchQuote = async ( + request: EdgeSwapRequest + ): Promise => { + capture.depositAddress = undefined + lastExchangeBody = undefined + return await plugin.fetchSwapQuote(request, undefined, { infoPayload: {} }) + } + + it('quotes and orders a forward BTC -> ETH private swap', async function () { + const quote = await fetchQuote({ + fromWallet: btcWallet, + toWallet: ethWallet, + fromTokenId: null, + toTokenId: null, + nativeAmount: '5000000', // 0.05 BTC + quoteFor: 'from' + }) + + expect(quote.fromNativeAmount).is.a('string').and.not.equals('0') + expect(quote.toNativeAmount).is.a('string').and.not.equals('0') + expect(capture.depositAddress).is.a('string') + expect(quote.pluginId).equals('houdini') + }) + + it('quotes and orders a forward ETH -> USDC private swap', async function () { + const quote = await fetchQuote({ + fromWallet: ethWallet, + toWallet: ethWallet, + fromTokenId: null, + toTokenId: usdcTokenId, + nativeAmount: '300000000000000000', // 0.3 ETH + quoteFor: 'from' + }) + + expect(quote.fromNativeAmount).equals('300000000000000000') + expect(quote.toNativeAmount).is.a('string').and.not.equals('0') + expect(capture.depositAddress).is.a('string') + }) + + it('quotes a reverse BTC -> ETH swap by the receive amount', async function () { + const quote = await fetchQuote({ + fromWallet: btcWallet, + toWallet: ethWallet, + fromTokenId: null, + toTokenId: null, + nativeAmount: '150000000000000000', // 0.15 ETH, the amount the recipient gets + quoteFor: 'to' + }) + + // The provider prices the send side for the fixed receive amount: + expect(quote.fromNativeAmount).is.a('string').and.not.equals('0') + expect(quote.toNativeAmount).is.a('string').and.not.equals('0') + expect(capture.depositAddress).is.a('string') + }) + + it('passes a destination tag through to order creation for a synthetic memo-chain destination', async function () { + const xrpDestination = makeFakeSyntheticDestination({ + pluginId: 'ripple', + currencyCode: 'XRP', + decimals: 6, + toAddress: 'rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe', + toMemos: [{ type: 'number', value: '12345' }] + }) + + const quote = await fetchQuote({ + fromWallet: ethWallet, + toWallet: xrpDestination, + fromTokenId: null, + toTokenId: null, + nativeAmount: '100000000000000000', // 0.1 ETH + quoteFor: 'from' + }) + + expect(quote.fromNativeAmount).is.a('string').and.not.equals('0') + expect(capture.depositAddress).is.a('string') + + // The order carried the pasted address and the destination tag: + expect(lastExchangeBody).is.a('string') + const orderBody = JSON.parse(lastExchangeBody ?? '{}') + expect(orderBody.addressTo).equals('rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe') + expect(orderBody.destinationTag).equals('12345') + }) +}) + +/** + * A canned `quotes` entry, in the shape the API returns. + */ +interface ScriptedQuote { + quoteId: string + type: string + amountOut: number + amountIn?: number + min?: number + max?: number + minOut?: number + maxOut?: number + /** ISO timestamp the quote id stops being accepted. */ + validUntil?: string +} + +interface Script { + /** + * What the token list reports as the native coin's contract address. Houdini + * spells "no contract" as `null` on most chains and as an empty string on + * others, which is the whole point of these cases. + */ + nativeAddress: string | null + quotes: ScriptedQuote[] + /** + * HTTP statuses handed to successive `quotes` calls. The final entry repeats + * once the list runs out, so `[429]` rate-limits forever and `[429, 200]` + * rate-limits once. + */ + quoteStatuses?: number[] + /** + * Chains the provider lists tokens for but serves no native coin on. The + * token list still answers 200 for them, it just holds nothing that matches, + * which is how celo, fantom, polkadot and ton actually behave. + */ + unservedChains?: string[] + /** + * An error body the create-exchange call answers with (status 400) instead of + * an order, for asserting how a rejection is phrased to the user. + */ + orderError?: unknown + /** + * HTTP statuses handed to successive `exchanges` calls, same repeat-last rule + * as `quoteStatuses`. A 429 answers with the rate-limit envelope and a 409 + * with the `STATIC_DEPOSIT_IN_USE` one. + */ + orderStatuses?: number[] + /** + * HTTP statuses handed to successive `tokens` calls, same repeat-last rule as + * `quoteStatuses`. A non-200 is the provider failing to answer, which must + * not be mistaken for a miss. + */ + tokenStatuses?: number[] + /** Seconds the rate-limit envelope asks the caller to wait. */ + retryAfter?: number + /** + * Fields merged into every successful create-exchange response, for the + * cases that need a specific order shape (an inflated `inAmount` that the + * trust boundary has to reject, a numeric or blank `depositTag`). + */ + orderOverrides?: { [key: string]: unknown } +} + +interface ScriptedRun { + plugin: ReturnType + /** Every `quotes` URL requested, in order. */ + quoteUrls: string[] + /** Every `tokens` URL requested, in order, to prove lookups are cached. */ + tokenUrls: string[] + /** Every `exchanges` body posted, in order. */ + orderBodies: string[] + warnings: string[] +} + +/** + * A plugin whose every HTTP call is answered locally. + * + * The recorded-fixture tests above replay one canned answer per URL, which + * cannot express what these cases need: a specific SEQUENCE of statuses (the + * rate-limit backoff), or a route mix the live API will not produce on demand + * (a pair that offers standard routes and no private one). Answering locally + * also keeps them deterministic and free of partner-API budget. + */ +const makeScriptedPlugin = (script: Script): ScriptedRun => { + const { + nativeAddress, + quotes, + quoteStatuses = [200], + unservedChains = [], + orderError, + orderStatuses = [200], + tokenStatuses = [200], + retryAfter, + orderOverrides = {} + } = script + const run: ScriptedRun = { + plugin: (undefined as unknown) as ReturnType, + quoteUrls: [], + tokenUrls: [], + orderBodies: [], + warnings: [] + } + let quoteCall = 0 + let tokenCall = 0 + let orderCall = 0 + + const reply = (status: number, body: unknown): FetchResponseLike => { + const text = JSON.stringify(body) + return { + ok: status >= 200 && status < 300, + status, + json: async () => JSON.parse(text), + text: async () => text + } + } + + const scriptedFetch = async ( + url: string, + opts: { method?: string; body?: string } = {} + ): Promise => { + const path = url.slice(url.indexOf('/v2/') + 4) + + if (path.startsWith('tokens')) { + run.tokenUrls.push(path) + const tokenStatus = + tokenStatuses[Math.min(tokenCall++, tokenStatuses.length - 1)] + if (tokenStatus !== 200) return reply(tokenStatus, { message: 'nope' }) + const chain = /chain=([^&]*)/.exec(path)?.[1] ?? '' + if (unservedChains.includes(chain)) { + // The provider answers, it just has no native to offer: + return reply(200, { tokens: [] }) + } + return reply(200, { + tokens: [ + // A contract token on the same chain, to prove the native match is + // picking the right entry rather than the first one: + { + id: `${chain}-token`, + address: '0x0000000000000000000000000000000000000001', + chain + }, + { id: `${chain}-native`, address: nativeAddress, chain } + ] + }) + } + + if (path.startsWith('quotes')) { + run.quoteUrls.push(path) + const status = + quoteStatuses[Math.min(quoteCall++, quoteStatuses.length - 1)] + if (status === 429) { + return reply(429, { + type: 'RATE_LIMIT_EXCEEDED', + limit: 5, + windowMs: 60000, + ...(retryAfter == null ? {} : { retryAfter }) + }) + } + return reply(200, { quotes }) + } + + if (path.startsWith('exchanges')) { + run.orderBodies.push(opts.body ?? '') + const orderStatus = + orderStatuses[Math.min(orderCall++, orderStatuses.length - 1)] + if (orderStatus === 429) { + return reply(429, { + type: 'RATE_LIMIT_EXCEEDED', + limit: 1, + windowMs: 60000, + ...(retryAfter == null ? {} : { retryAfter }) + }) + } + if (orderStatus === 409) { + // A fixed-rate route's static deposit address held by another live + // order. The plugin falls through to the next in-range route. + return reply(409, { + code: 'STATIC_DEPOSIT_IN_USE', + message: 'Deposit address for ch BTC is in use by another live order' + }) + } + if (orderError != null) return reply(400, orderError) + const { quoteId } = JSON.parse(opts.body ?? '{}') + const used = quotes.find(quote => quote.quoteId === quoteId) + return reply(200, { + houdiniId: `order-${String(quoteId)}`, + depositAddress: 'DEPOSIT_ADDRESS', + inAmount: used?.amountIn ?? 100, + outAmount: used?.amountOut ?? 100, + ...orderOverrides + }) + } + + throw new Error(`Unscripted Houdini path: ${path}`) + } + + const log = Object.assign(() => {}, { + warn: (...args: unknown[]) => { + run.warnings.push(args.map(String).join(' ')) + }, + error: () => {}, + crash: async () => {}, + breadcrumb: () => {} + }) + + run.plugin = makeHoudiniPlugin(({ + initOptions: { apiKey: 'SCRIPTED', apiSecret: 'SCRIPTED' }, + io: { fetch: scriptedFetch, fetchCors: scriptedFetch }, + log + } as unknown) as Parameters[0]) + + return run +} + +describe('houdini offline behaviors', function () { + // The rate-limit cases wait out the plugin's own backoff (1s, 2s, 4s): + this.timeout(30000) + + const capture: SpendCapture = {} + + /** Sonic and Stellar are two of the empty-string-native chains. */ + const sonicWallet = makeFakeWallet( + { + pluginId: 'sonic', + currencyCode: 'S', + decimals: 18, + address: '0x1111111111111111111111111111111111111111', + balance: SONIC_NATIVE_AMOUNT + }, + capture + ) + const stellarWallet = makeFakeWallet( + { + pluginId: 'stellar', + currencyCode: 'XLM', + decimals: 7, + address: 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN' + }, + capture + ) + const celoWallet = makeFakeWallet( + { + pluginId: 'celo', + currencyCode: 'CELO', + decimals: 18, + address: '0x2222222222222222222222222222222222222222' + }, + capture + ) + + const privateQuote: ScriptedQuote = { + quoteId: 'q-private', + type: 'private', + amountOut: 150, + amountIn: 1300 + } + const standardQuote: ScriptedQuote = { + quoteId: 'q-standard', + type: 'standard', + amountOut: 160, + amountIn: 1300 + } + const dexQuote: ScriptedQuote = { + quoteId: 'q-dex', + type: 'dex', + amountOut: 170, + amountIn: 1300 + } + + const quoteSonicToStellar = async ( + run: ScriptedRun, + overrides: Partial = {} + ): Promise => { + capture.depositAddress = undefined + capture.savedAction = undefined + const request = ({ + fromWallet: sonicWallet, + toWallet: stellarWallet, + fromTokenId: null, + toTokenId: null, + nativeAmount: SONIC_NATIVE_AMOUNT, + quoteFor: 'from', + ...overrides + } as unknown) as EdgeSwapRequest + return await run.plugin.fetchSwapQuote(request, undefined, { + infoPayload: {} + }) + } + + it('resolves a native coin whose contract address is an empty string', async function () { + // Both sides of this pair are chains Houdini reports with `address: ""`. + // Matching on `address == null` alone found no native token for either, so + // every quote to or from them failed before reaching the provider. + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [privateQuote] + }) + const quote = await quoteSonicToStellar(run) + + expect(quote.pluginId).equals('houdini') + expect(capture.depositAddress).equals('DEPOSIT_ADDRESS') + // The native entry was chosen over the contract token on the same chain: + expect(run.quoteUrls[0]).contains('from=sonic-native') + expect(run.quoteUrls[0]).contains('to=xlm-native') + }) + + it('resolves a native coin whose contract address is null', async function () { + const run = makeScriptedPlugin({ + nativeAddress: null, + quotes: [privateQuote] + }) + await quoteSonicToStellar(run) + expect(run.quoteUrls[0]).contains('from=sonic-native') + }) + + it('declines when privacy is required and only transparent routes exist', async function () { + // Below Houdini's 25 USD private floor the API answers with standard and + // dex routes only. Handing one of those to a caller that asked for privacy + // would be a downgrade it cannot detect, so the plugin declines instead. + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [standardQuote, dexQuote] + }) + let error: unknown + await quoteSonicToStellar(run, ({ + privacy: 'required' + } as unknown) as Partial).catch((caught: unknown) => { + error = caught + }) + + expect(String(error)).contains('SwapCurrencyError') + expect(run.orderBodies).deep.equals([]) + }) + + it('takes a standard route when privacy was not requested', async function () { + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [standardQuote, dexQuote] + }) + await quoteSonicToStellar(run) + expect(JSON.parse(run.orderBodies[0]).quoteId).equals('q-standard') + }) + + it('prefers a private route over a better-priced standard one', async function () { + // The standard quote pays out more here. Privacy still wins the ranking, + // because a transparent route is not a cheaper version of a private one. + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [standardQuote, privateQuote] + }) + await quoteSonicToStellar(run) + expect(JSON.parse(run.orderBodies[0]).quoteId).equals('q-private') + }) + + it('never routes through a dex, with or without privacy', async function () { + // A dex route is an on-chain swap that links both sides permanently, and + // it is not what either the Exchange scene or a stealth send asked for. + const run = makeScriptedPlugin({ nativeAddress: '', quotes: [dexQuote] }) + let error: unknown + await quoteSonicToStellar(run).catch((caught: unknown) => { + error = caught + }) + expect(String(error)).contains('SwapCurrencyError') + expect(run.orderBodies).deep.equals([]) + }) + + it('allows a same-asset swap, which is this provider main flow', async function () { + // The shared `checkInvalidTokenIds` guard rejects same-asset requests for + // every other plugin. Routing an asset to itself through the mixer is what + // Houdini is for, so it opts out of that half of the guard. + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [privateQuote] + }) + const quote = await quoteSonicToStellar(run, { + toWallet: sonicWallet + }) + expect(quote.pluginId).equals('houdini') + }) + + it('declines a chain the provider serves no native coin for', async function () { + // Houdini lists celo, fantom, polkadot and ton but has no mainnet native + // for any of them. The chain table does not encode that: the decline comes + // from the token lookup finding nothing, which is the same + // `SwapCurrencyError` the whitelist check raises, so the aggregator moves + // on to another provider either way. + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [privateQuote], + unservedChains: ['celo'] + }) + let error: unknown + await quoteSonicToStellar(run, { toWallet: celoWallet }).catch( + (caught: unknown) => { + error = caught + } + ) + expect(String(error)).contains('SwapCurrencyError') + // It declined before pricing anything: + expect(run.quoteUrls).deep.equals([]) + }) + + it('asks about an unserved chain once, then remembers', async function () { + // The miss is the expensive part: without caching it, every quote naming + // an unserved chain spends another call against a rate-limited API to be + // told the same thing. Caching it is what lets the chain table stay a + // name map instead of a hand-maintained list of what is served. + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [privateQuote], + unservedChains: ['celo'] + }) + const swallow = (): void => {} + await quoteSonicToStellar(run, { toWallet: celoWallet }).catch(swallow) + const afterFirst = run.tokenUrls.filter(url => url.includes('chain=celo')) + .length + await quoteSonicToStellar(run, { toWallet: celoWallet }).catch(swallow) + const afterSecond = run.tokenUrls.filter(url => url.includes('chain=celo')) + .length + + expect(afterFirst).equals(1) + expect(afterSecond).equals(1) + }) + + it('does not cache a lookup the provider failed to answer', async function () { + // A rate limit or a server error says nothing about whether the chain is + // served. Caching that would turn one bad minute into a chain that stays + // dead for the rest of the session. + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [privateQuote], + tokenStatuses: [500, 200] + }) + const swallow = (): void => {} + await quoteSonicToStellar(run).catch(swallow) + const afterFailure = run.tokenUrls.length + await quoteSonicToStellar(run).catch(swallow) + + expect(afterFailure).is.greaterThan(0) + expect(run.tokenUrls.length).is.greaterThan(afterFailure) + }) + + it('reads the quote expiry the API actually sends', function () { + // Live fixtures record `validUntil` as Unix SECONDS in a string + // ("1783037880"), which `new Date` reads as an invalid date. Parsing it + // that way silently fell back to a fresh 60s window on every quote, so + // the expiry guard could never fire. + const at = 1783037880 + const quote = ({ validUntil: String(at) } as unknown) as Parameters< + typeof quoteValidUntilMs + >[0] + expect(quoteValidUntilMs(quote)).equals(at * 1000) + + // An absent expiry still falls back to the documented lifetime: + const noExpiry = ({} as unknown) as Parameters[0] + expect(quoteValidUntilMs(noExpiry)).is.greaterThan(Date.now()) + }) + + it('does not wait out a window that outlives the quote', async function () { + // Houdini's one-per-minute exchange budget reports `retryAfter` near 60, + // and a quote id lives about that long. Waiting the full window and then + // POSTing the same quote id hangs the user for a minute only to fail as an + // expired quote, so the rate limit is reported straight away instead. + const start = Date.now() + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [ + { + ...privateQuote, + // Unix seconds in a string, exactly as the API reports it: + validUntil: String(Math.floor((start + 20000) / 1000)) + } + ], + orderStatuses: [429], + retryAfter: 60 + }) + const error = await quoteSonicToStellar(run).then( + () => undefined, + (error: unknown) => error + ) + + expect(String(error)).contains('rate limit') + // It failed rather than sleeping out the window: + expect(Date.now() - start).is.lessThan(5000) + // And it did not burn the remaining candidates on the same dead window: + expect(run.orderBodies.length).equals(1) + }) + + it('reads the field message out of a validation failure', async function () { + // A `VALIDATION_ERROR` sets the top-level message to a generic "Validation + // Failed" and puts the real reason under `fields..message`, so + // reading the top level alone told the user nothing they could act on. + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [privateQuote], + orderError: { + code: 'VALIDATION_ERROR', + message: 'Validation Failed', + fields: { quoteId: { message: 'Quote has expired' } } + } + }) + const error = await quoteSonicToStellar(run).then( + () => undefined, + (error: unknown) => error + ) + + expect(String(error)).contains('Quote has expired') + expect(String(error)).does.not.contain('Validation Failed') + }) + + it('creates one exchange for a max quote, not two', async function () { + // `getMaxSwappable` runs the quote function once to size the spend, then + // the real quote runs it again. Creating an exchange on the sizing pass + // spends one of Houdini's one-per-minute exchange slots, so the real + // create is rate limited and the whole max quote stalls for the window. + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [privateQuote] + }) + await quoteSonicToStellar(run, { quoteFor: 'max' }) + + expect(run.orderBodies.length).equals(1) + }) + + it('sets skipChecks on the max probe, so EVM max swaps work', async function () { + // The probe stands in the user's OWN from-chain address for the deposit + // address that does not exist yet. An EVM engine compares the spend target + // against its own public key and rejects that with `SpendToSelfError`, + // which escapes `getMaxSwappable` and fails every max swap from an EVM + // wallet unless the probe's spendInfo opts out of the checks. + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [privateQuote] + }) + await quoteSonicToStellar(run, { quoteFor: 'max' }) + + expect(capture.maxSpendInfo?.skipChecks).equals(true) + }) + + it('clamps an above-limit balance on the max probe instead of throwing', async function () { + // The probe deliberately quotes the full PRE-FEE balance to find the + // ceiling. Throwing `SwapAboveLimitError` there aborts a max swap that + // fits perfectly well once the network fee comes off, so the maximum is + // enforced on the real quote only. This wallet holds 1300 S, above the + // route's 1000 ceiling, and reports 900 S spendable once the fee is out. + const trimmedSonicWallet = makeFakeWallet( + { + pluginId: 'sonic', + currencyCode: 'S', + decimals: 18, + address: '0x1111111111111111111111111111111111111111', + balance: SONIC_NATIVE_AMOUNT, + maxSpendable: '900000000000000000000' + }, + capture + ) + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [{ ...privateQuote, min: 1, max: 1000 }], + // The scripted exchange echoes the quote's `amountIn` for every request, + // so the real (trimmed) create needs the trimmed amount spelled out. A + // live provider prices the order off the amount it was asked for. + orderOverrides: { inAmount: 900 } + }) + + const quote = await run.plugin.fetchSwapQuote( + ({ + fromWallet: trimmedSonicWallet, + toWallet: stellarWallet, + fromTokenId: null, + toTokenId: null, + nativeAmount: SONIC_NATIVE_AMOUNT, + quoteFor: 'max' + } as unknown) as EdgeSwapRequest, + undefined, + { infoPayload: {} } + ) + expect(quote.pluginId).equals('houdini') + expect(quote.fromNativeAmount).equals('900000000000000000000') + }) + + it('refuses an order whose deposit amount exceeds the request', async function () { + // TRUST BOUNDARY: `inAmount` comes back from the provider and becomes a + // signed spend, so a response asking for more of the source asset than the + // user requested must not reach `makeSpend`. + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [privateQuote], + orderOverrides: { inAmount: 2600 } + }) + + let error: unknown + try { + await quoteSonicToStellar(run) + } catch (caught: unknown) { + error = caught + } + expect(String(error)).contains('above the requested amount') + }) + + it('accepts a numeric deposit tag and ignores a blank one', async function () { + // A numeric destination tag is the common shape on memo chains, and the + // valid tag `0` is numeric too. A string-only cleaner rejects both, taking + // the whole order down; a blank one becomes an empty memo on the deposit. + const numeric = makeScriptedPlugin({ + nativeAddress: '', + quotes: [privateQuote], + orderOverrides: { depositTag: 0 } + }) + await quoteSonicToStellar(numeric) + expect(numeric.orderBodies.length).equals(1) + + const blank = makeScriptedPlugin({ + nativeAddress: '', + quotes: [privateQuote], + orderOverrides: { depositTag: '' } + }) + await quoteSonicToStellar(blank) + expect(blank.orderBodies.length).equals(1) + }) + + it('rounds a minimum up and a maximum down', async function () { + // A minimum that rounds DOWN lets a deposit through that the provider then + // rejects; a maximum that rounds UP offers a ceiling the provider will not + // honor. XLM's 7 decimals make both directions visible: each limit below + // carries an eighth decimal place, so the native value is fractional and + // the rounding has to pick a side. + const quoteStellarToSonic = async ( + run: ScriptedRun, + overrides: Partial = {} + ): Promise => + await run.plugin.fetchSwapQuote( + ({ + fromWallet: stellarWallet, + toWallet: sonicWallet, + fromTokenId: null, + toTokenId: null, + nativeAmount: '10000000', // 1 XLM + quoteFor: 'from', + ...overrides + } as unknown) as EdgeSwapRequest, + undefined, + { infoPayload: {} } + ) + + const belowRun = makeScriptedPlugin({ + nativeAddress: '', + quotes: [{ ...privateQuote, min: 1.00000004, max: 1000 }] + }) + let belowError: any + try { + await quoteStellarToSonic(belowRun) + } catch (caught: unknown) { + belowError = caught + } + expect(belowError?.name).equals('SwapBelowLimitError') + // 1.00000004 XLM is 10000000.4 stroops, which rounds UP: + expect(belowError?.nativeMin).equals('10000001') + + const aboveRun = makeScriptedPlugin({ + nativeAddress: '', + quotes: [{ ...privateQuote, min: 0.0001, max: 0.90000005 }] + }) + let aboveError: any + try { + await quoteStellarToSonic(aboveRun) + } catch (caught: unknown) { + aboveError = caught + } + expect(aboveError?.name).equals('SwapAboveLimitError') + // 0.90000005 XLM is 9000000.5 stroops, which rounds DOWN: + expect(aboveError?.nativeMax).equals('9000000') + }) + + it('falls through to the next route when a deposit address is in use', async function () { + // A fixed-rate route's static deposit address can be held by another live + // order (409 STATIC_DEPOSIT_IN_USE). That is the one order failure worth + // retrying on a different route, and it is recognized by the envelope's + // machine-readable `code`, not by searching the raw body: a code that also + // appeared inside a human-readable message would match a substring test on + // a response that meant something else. + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [privateQuote, { ...privateQuote, quoteId: 'q-private-2' }], + orderStatuses: [409, 200] + }) + await quoteSonicToStellar(run) + + expect(run.orderBodies.length).equals(2) + expect(JSON.parse(run.orderBodies[0]).quoteId).equals('q-private') + expect(JSON.parse(run.orderBodies[1]).quoteId).equals('q-private-2') + }) + + it('bounds a reverse quote deposit by the route from-side ceiling', async function () { + // A reverse quote pins the RECEIVE amount, so the send side is the + // provider's to price and `request.nativeAmount` says nothing about it. + // The route's own published `max` is the local bound that remains, and an + // order asking for more than it must not become a signed spend. + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [ + { + ...privateQuote, + amountIn: 1000, + minOut: 0.01, + maxOut: 1000, + min: 1, + max: 1200 + } + ], + orderOverrides: { inAmount: 5000 } + }) + + let error: unknown + try { + await quoteSonicToStellar(run, { + quoteFor: 'to', + nativeAmount: '1500000' // 0.15 XLM, inside minOut/maxOut + }) + } catch (caught: unknown) { + error = caught + } + expect(String(error)).contains('above the requested amount') + }) + + it('never retries before the window the API asked for', function () { + // Houdini's 1-per-minute exchange budget reports `retryAfter` near 60. The + // 30s cap on our own doubling used to truncate that, so the retry fired + // while still inside the window, drew another 429, and spent the retries + // for nothing. + expect(rateLimitDelayMs(0, 60)).is.at.least(60000) + expect(rateLimitDelayMs(2, 60)).is.at.least(60000) + + // Without a reported window the cap still bounds our own growth. + expect(rateLimitDelayMs(0, undefined)).equals(1000) + expect(rateLimitDelayMs(9, undefined)).equals(30000) + }) + + it('asks once when both legs of a quote want the same token', async function () { + // A quote resolves both legs with `Promise.all`, so a same-asset quote asks + // the same question twice at once. Caching only on completion let both + // miss and spend two calls against a rate-limited API on one answer. + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [privateQuote] + }) + const swallow = (): void => {} + await quoteSonicToStellar(run, { + toWallet: sonicWallet + }).catch(swallow) + + const sonicLookups = run.tokenUrls.filter(url => url.includes('sonic')) + expect(sonicLookups.length).equals(1) + }) + + it('blames the provider when a lookup fails, not the pair', async function () { + // Returning a miss on a failed lookup would be indistinguishable from the + // provider answering "no such token", so a bad minute at the API would + // reach the user as a pair Houdini cannot route. + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [privateQuote], + tokenStatuses: [503] + }) + const error = await quoteSonicToStellar(run).then( + () => undefined, + (error: unknown) => error + ) + + expect(error == null).equals(false) + expect(String(error)).does.not.contain('SwapCurrencyError') + expect(String(error)).contains('503') + }) + + it('reports a floating rate on a forward quote and a fixed one on a reverse quote', async function () { + // Houdini prices exact-out on fixed-rate quotes alone. A forward quote + // floats whether the route is private or standard, so labelling every + // quote fixed showed a locked receive amount for a leg that can still move. + const forward = makeScriptedPlugin({ + nativeAddress: '', + quotes: [privateQuote] + }) + await quoteSonicToStellar(forward) + const forwardAction = capture.savedAction as { isEstimate?: boolean } + expect(forwardAction.isEstimate).equals(true) + + const reverse = makeScriptedPlugin({ + nativeAddress: '', + quotes: [{ ...standardQuote, minOut: 0.01, maxOut: 1000 }] + }) + await quoteSonicToStellar(reverse, { + quoteFor: 'to', + nativeAmount: '1500000' // 0.15 XLM, priced on the receive side + }) + const reverseAction = capture.savedAction as { isEstimate?: boolean } + expect(reverseAction.isEstimate).equals(false) + expect(reverse.quoteUrls[0]).contains('amountType=receive') + expect(reverse.quoteUrls[0]).contains('fixed=true') + }) + + it('retries a rate-limited call behind the window the API reports', async function () { + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [privateQuote], + quoteStatuses: [429, 200], + retryAfter: 1 + }) + const quote = await quoteSonicToStellar(run) + + expect(quote.pluginId).equals('houdini') + expect(run.quoteUrls.length).equals(2) + expect(run.warnings.join(' ')).contains('rate limited') + }) + + it('names the rate limit rather than the route once retries are spent', async function () { + // A 429 must never surface as "this pair is unavailable": the pair is + // fine, the caller was too fast, and the two failures want different + // handling from both the user and the send scene pair-capability cache. + const run = makeScriptedPlugin({ + nativeAddress: '', + quotes: [privateQuote], + quoteStatuses: [429], + retryAfter: 0 + }) + let error: unknown + await quoteSonicToStellar(run).catch((caught: unknown) => { + error = caught + }) + + expect(String(error)).contains('rate limit exceeded') + expect(String(error)).does.not.contain('SwapCurrencyError') + // Three retries on top of the first attempt, then it gives up: + expect(run.quoteUrls.length).equals(4) + }) +}) diff --git a/test/houdiniFixtures/06ea8bd864da2043c9833ae6.json b/test/houdiniFixtures/06ea8bd864da2043c9833ae6.json new file mode 100644 index 00000000..42e22b0d --- /dev/null +++ b/test/houdiniFixtures/06ea8bd864da2043c9833ae6.json @@ -0,0 +1,5 @@ +{ + "status": 200, + "ok": true, + "body": "{\"total\":225,\"quotes\":[{\"swap\":\"pt\",\"quoteId\":\"6a2b55e3a5f07ecb32c99853\",\"duration\":0.87,\"amountOut\":1.898845566589908,\"amountOutUsd\":3177.6336869985003,\"netAmountOut\":1.898845566589908,\"swapName\":\"PactSwap\",\"type\":\"dex\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/pactswap-co22dt.png\",\"markupSupported\":false,\"apiMarkupValue\":0,\"markupType\":\"bp\",\"restrictedCountries\":[],\"rewardsAvailable\":false,\"min\":20,\"max\":20000,\"requiresApproval\":false},{\"amountIn\":0.05,\"amountOut\":1.88665757,\"min\":0.00047,\"max\":141.43,\"duration\":1,\"amountOutUsd\":3157.5855754548,\"swap\":\"eb\",\"swapName\":\"EasyBit\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/easybit.jpg\",\"quoteId\":\"6a2b55e2a5f07ecb32c99752\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8858,\"min\":0.0015708110631595,\"max\":13.47200777105,\"duration\":33,\"amountOutUsd\":3156.150312,\"swap\":\"cc\",\"swapName\":\"Coincraddle\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/coincraddle.jpg\",\"quoteId\":\"6a2b55e2a5f07ecb32c99751\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.88579364,\"min\":0.00157199,\"max\":27.18790798,\"duration\":15,\"amountOutUsd\":3156.1396676496,\"swap\":\"cl\",\"swapName\":\"Changelly\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/changelly.jpg\",\"quoteId\":\"6a2b55e2a5f07ecb32c99750\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8857704,\"min\":0.0000252,\"max\":9007199254740991,\"duration\":29,\"amountOutUsd\":3156.1007722560003,\"swap\":\"cn\",\"swapName\":\"ChangeNow\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/changenow.png\",\"quoteId\":\"6a2b55e3a5f07ecb32c9976f\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.88116,\"min\":0.003142,\"max\":47.127411,\"duration\":15,\"amountOutUsd\":3148.3846224,\"swap\":\"sz\",\"swapName\":\"Swapuz\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/swapuz-d3ssum.png\",\"quoteId\":\"6a2b55e2a5f07ecb32c99754\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.87845337,\"min\":0.00047,\"max\":100,\"duration\":15,\"amountOutUsd\":3143.8546981668,\"swap\":\"tc\",\"swapName\":\"Verified partner\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/verified partner-dncsmn.png\",\"quoteId\":\"6a2b55e2a5f07ecb32c99762\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.87388911,\"min\":0.001,\"max\":370,\"duration\":15,\"amountOutUsd\":3136.2157700604002,\"swap\":\"le\",\"swapName\":\"LetsExchange\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/letsexchange.jpg\",\"quoteId\":\"6a2b55e3a5f07ecb32c99768\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.8723736848,\"min\":0.0015729292582931,\"max\":14.671787663267,\"duration\":29,\"amountOutUsd\":3133.6794938286725,\"quoteId\":\"6a2b55e3a5f07ecb32c9977a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.87226947058,\"min\":0.001572113721047,\"max\":15.351467823236,\"duration\":33,\"amountOutUsd\":3133.5050767415114,\"quoteId\":\"6a2b55e3a5f07ecb32c99791\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.87221642,\"min\":0.0015729292582931,\"max\":14.671787663267,\"duration\":26,\"amountOutUsd\":3133.4162891688,\"quoteId\":\"6a2b55e2a5f07ecb32c99759\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8718044469880002,\"min\":0.0000192,\"max\":9007199254740991,\"duration\":40,\"amountOutUsd\":3132.726794656997,\"quoteId\":\"6a2b55e3a5f07ecb32c997fc\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.87161165,\"min\":0.001572113721047,\"max\":15.351467823236,\"duration\":46,\"amountOutUsd\":3132.404121906,\"quoteId\":\"6a2b55e4a5f07ecb32c9986c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.87158845,\"min\":0.0000192,\"max\":9007199254740991,\"duration\":34,\"amountOutUsd\":3132.365293458,\"quoteId\":\"6a2b55e3a5f07ecb32c997c0\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.87153984,\"min\":0.001572113721047,\"max\":15.351467823236,\"duration\":30,\"amountOutUsd\":3132.2839378176004,\"quoteId\":\"6a2b55e3a5f07ecb32c99764\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.87144634,\"min\":0.0015729292582931,\"max\":14.671787663267,\"duration\":37,\"amountOutUsd\":3132.1274524776004,\"quoteId\":\"6a2b55e4a5f07ecb32c99882\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8714358950260004,\"min\":0.0000223,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":3132.1099713513154,\"quoteId\":\"6a2b55e3a5f07ecb32c9979a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.87135317,\"min\":0.0000192,\"max\":9007199254740991,\"duration\":34,\"amountOutUsd\":3131.9715194388,\"quoteId\":\"6a2b55e3a5f07ecb32c997f1\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8712490235,\"min\":0.0000221,\"max\":9007199254740991,\"duration\":31,\"amountOutUsd\":3131.7972156905403,\"quoteId\":\"6a2b55e4a5f07ecb32c99862\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.87122926,\"min\":0.0000221,\"max\":9007199254740991,\"duration\":28,\"amountOutUsd\":3131.7641387064,\"quoteId\":\"6a2b55e3a5f07ecb32c99827\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.870999260212,\"min\":0.00157199,\"max\":29.69741861,\"duration\":12,\"amountOutUsd\":3131.379201861212,\"quoteId\":\"6a2b55e4a5f07ecb32c99890\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8709664942,\"min\":0.00157199,\"max\":21.80460836,\"duration\":24,\"amountOutUsd\":3131.324363352888,\"quoteId\":\"6a2b55e3a5f07ecb32c9978f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.870898822444,\"min\":0.00047,\"max\":141.36,\"duration\":9,\"amountOutUsd\":3131.2111051951765,\"quoteId\":\"6a2b55e3a5f07ecb32c99796\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.87077825,\"min\":0.0000223,\"max\":9007199254740991,\"duration\":43,\"amountOutUsd\":3131.0093103300005,\"quoteId\":\"6a2b55e4a5f07ecb32c9985d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8706711,\"min\":0.001595809,\"max\":9007199254740991,\"duration\":37,\"amountOutUsd\":3130.8299798040002,\"quoteId\":\"6a2b55e4a5f07ecb32c9988d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.87065941,\"min\":0.0000223,\"max\":9007199254740991,\"duration\":26,\"amountOutUsd\":3130.8104149524,\"quoteId\":\"6a2b55e3a5f07ecb32c997c1\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8705679100000001,\"min\":0.001595809,\"max\":9007199254740991,\"duration\":20,\"amountOutUsd\":3130.6572768924,\"quoteId\":\"6a2b55e3a5f07ecb32c997ff\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.87053588,\"min\":0.00047,\"max\":141.36,\"duration\":3,\"amountOutUsd\":3130.6036702032,\"quoteId\":\"6a2b55e3a5f07ecb32c9976c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8704992492999999,\"min\":0.00157199,\"max\":6.40645822,\"duration\":21,\"amountOutUsd\":3130.542363598452,\"quoteId\":\"6a2b55e3a5f07ecb32c997d4\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.87049227,\"min\":0.0000221,\"max\":9007199254740991,\"duration\":39,\"amountOutUsd\":3130.5306827628,\"quoteId\":\"6a2b55e4a5f07ecb32c99888\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.87034171,\"min\":0.00157199,\"max\":29.69741861,\"duration\":25,\"amountOutUsd\":3130.2786995244,\"quoteId\":\"6a2b55e4a5f07ecb32c9985e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.87032097,\"min\":0.00157199,\"max\":21.80460836,\"duration\":18,\"amountOutUsd\":3130.2439882308004,\"quoteId\":\"6a2b55e3a5f07ecb32c99781\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8702,\"min\":0.0000223,\"max\":9007199254740991,\"duration\":31,\"amountOutUsd\":3130.041528,\"quoteId\":\"6a2b55e3a5f07ecb32c9980d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8701304699999999,\"min\":0.0000193,\"max\":9007199254740991,\"duration\":39,\"amountOutUsd\":3129.9251598108,\"quoteId\":\"6a2b55e3a5f07ecb32c99824\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8701,\"min\":0.001595809,\"max\":9007199254740991,\"duration\":28,\"amountOutUsd\":3129.8741640000003,\"quoteId\":\"6a2b55e3a5f07ecb32c99833\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8698,\"min\":0.0000221,\"max\":9007199254740991,\"duration\":31,\"amountOutUsd\":3129.372072,\"quoteId\":\"6a2b55e3a5f07ecb32c99835\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8698,\"min\":0.00157199,\"max\":29.69741861,\"duration\":13,\"amountOutUsd\":3129.372072,\"quoteId\":\"6a2b55e3a5f07ecb32c9980c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.869752256272,\"min\":0.0000193,\"max\":9007199254740991,\"duration\":31,\"amountOutUsd\":3129.29216618707,\"quoteId\":\"6a2b55e4a5f07ecb32c99861\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86971815,\"min\":0.00157199,\"max\":6.40645822,\"duration\":29,\"amountOutUsd\":3129.2350845660003,\"quoteId\":\"6a2b55e4a5f07ecb32c99884\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8696005687359998,\"min\":0.00157199,\"max\":7.47442658,\"duration\":25,\"amountOutUsd\":3129.038295859319,\"quoteId\":\"6a2b55e3a5f07ecb32c9983a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8693321,\"min\":0.001572113721047,\"max\":15.351467823236,\"duration\":30,\"amountOutUsd\":3128.5889758440003,\"quoteId\":\"6a2b55e3a5f07ecb32c99786\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8693164,\"min\":0.001572113721047,\"max\":15.351467823236,\"duration\":31,\"amountOutUsd\":3128.562699696,\"quoteId\":\"6a2b55e2a5f07ecb32c9975d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8692985,\"min\":0.0015729292582931,\"max\":14.671787663267,\"duration\":27,\"amountOutUsd\":3128.53274154,\"quoteId\":\"6a2b55e2a5f07ecb32c9975f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86912018,\"min\":0.001595809,\"max\":9007199254740991,\"duration\":13,\"amountOutUsd\":3128.2342980552,\"quoteId\":\"6a2b55e3a5f07ecb32c997eb\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8691,\"min\":0.00157199,\"max\":6.40645822,\"duration\":21,\"amountOutUsd\":3128.2005240000003,\"quoteId\":\"6a2b55e3a5f07ecb32c9980e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8690652300000001,\"min\":0.001595569,\"max\":9007199254740991,\"duration\":24,\"amountOutUsd\":3128.1423315372003,\"quoteId\":\"6a2b55e3a5f07ecb32c9981d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86888184,\"min\":0.001595809,\"max\":9007199254740991,\"duration\":12,\"amountOutUsd\":3127.8354026976003,\"quoteId\":\"6a2b55e3a5f07ecb32c99826\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86866057,\"min\":0.00047,\"max\":141.32,\"duration\":5,\"amountOutUsd\":3127.4650763748004,\"quoteId\":\"6a2b55e3a5f07ecb32c99767\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8684873,\"min\":0.0000223,\"max\":9007199254740991,\"duration\":28,\"amountOutUsd\":3127.175084772,\"quoteId\":\"6a2b55e3a5f07ecb32c997a1\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86846499,\"min\":0.00157199,\"max\":0.28618039,\"duration\":23,\"amountOutUsd\":3127.1377458636002,\"quoteId\":\"6a2b55e3a5f07ecb32c997ba\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86839787,\"min\":0.0015729292582931,\"max\":14.671787663267,\"duration\":27,\"amountOutUsd\":3127.0254111468003,\"quoteId\":\"6a2b55e3a5f07ecb32c997ca\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8683949,\"min\":0.001595809,\"max\":9007199254740991,\"duration\":20,\"amountOutUsd\":3127.020440436,\"quoteId\":\"6a2b55e3a5f07ecb32c997e6\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8683725,\"min\":0.00157199,\"max\":21.80460836,\"duration\":17,\"amountOutUsd\":3126.9829509,\"quoteId\":\"6a2b55e3a5f07ecb32c997d0\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8683464,\"min\":0.001595809,\"max\":9007199254740991,\"duration\":22,\"amountOutUsd\":3126.939268896,\"quoteId\":\"6a2b55e3a5f07ecb32c997d9\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8683422,\"min\":0.0015729292582931,\"max\":14.671787663267,\"duration\":25,\"amountOutUsd\":3126.932239608,\"quoteId\":\"6a2b55e3a5f07ecb32c9976b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86832923,\"min\":0.001595569,\"max\":9007199254740991,\"duration\":35,\"amountOutUsd\":3126.9105324972,\"quoteId\":\"6a2b55e4a5f07ecb32c99869\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8683217,\"min\":0.00047,\"max\":141.36,\"duration\":3,\"amountOutUsd\":3126.897929988,\"quoteId\":\"6a2b55e3a5f07ecb32c997a3\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8683106,\"min\":0.0000221,\"max\":9007199254740991,\"duration\":28,\"amountOutUsd\":3126.879352584,\"quoteId\":\"6a2b55e3a5f07ecb32c997f5\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86830593,\"min\":0.001589582,\"max\":9007199254740991,\"duration\":14,\"amountOutUsd\":3126.8715366852,\"quoteId\":\"6a2b55e3a5f07ecb32c997e5\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8680652,\"min\":0.00157199,\"max\":29.69741861,\"duration\":8,\"amountOutUsd\":3126.468641328,\"quoteId\":\"6a2b55e3a5f07ecb32c997bb\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8680458,\"min\":0.00157199,\"max\":29.69741861,\"duration\":10,\"amountOutUsd\":3126.4361727120004,\"quoteId\":\"6a2b55e3a5f07ecb32c9979f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.867785921668,\"min\":0.003142,\"max\":47.127411,\"duration\":30,\"amountOutUsd\":3126.0012299404316,\"quoteId\":\"6a2b55e3a5f07ecb32c99794\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8677,\"min\":0.001595569,\"max\":9007199254740991,\"duration\":27,\"amountOutUsd\":3125.8574280000003,\"quoteId\":\"6a2b55e3a5f07ecb32c99849\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86764,\"min\":0.001572113721047,\"max\":15.351467823236,\"duration\":32,\"amountOutUsd\":3125.7570096,\"quoteId\":\"6a2b55e3a5f07ecb32c99790\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8675746,\"min\":0.00157199,\"max\":6.40645822,\"duration\":18,\"amountOutUsd\":3125.647553544,\"quoteId\":\"6a2b55e3a5f07ecb32c997a7\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8675061817000003,\"min\":0.003142,\"max\":47.137363,\"duration\":30,\"amountOutUsd\":3125.5330459403885,\"quoteId\":\"6a2b55e4a5f07ecb32c99863\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86747494,\"min\":0.003142,\"max\":47.127411,\"duration\":30,\"amountOutUsd\":3125.4807585816,\"quoteId\":\"6a2b55e3a5f07ecb32c99782\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8673909800000001,\"min\":0.003142,\"max\":47.127411,\"duration\":30,\"amountOutUsd\":3125.3402397672003,\"quoteId\":\"6a2b55e3a5f07ecb32c9978c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86737458,\"min\":0.001589582,\"max\":9007199254740991,\"duration\":26,\"amountOutUsd\":3125.3127920712,\"quoteId\":\"6a2b55e3a5f07ecb32c9982e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86735,\"min\":0.0015729292582931,\"max\":14.671787663267,\"duration\":27,\"amountOutUsd\":3125.271654,\"quoteId\":\"6a2b55e2a5f07ecb32c99757\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8672283231820002,\"min\":0.00048141,\"max\":8.38020876,\"duration\":7,\"amountOutUsd\":3125.068010810323,\"quoteId\":\"6a2b55e4a5f07ecb32c9986a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86715488,\"min\":0.001589582,\"max\":9007199254740991,\"duration\":12,\"amountOutUsd\":3124.9450933632,\"quoteId\":\"6a2b55e3a5f07ecb32c9982f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86715488,\"min\":0.003142,\"max\":47.127411,\"duration\":30,\"amountOutUsd\":3124.9450933632,\"quoteId\":\"6a2b55e3a5f07ecb32c99787\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.867135095824,\"min\":0.00048179,\"max\":6.6845044,\"duration\":40,\"amountOutUsd\":3124.9119817748797,\"quoteId\":\"6a2b55e4a5f07ecb32c9986e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8671,\"min\":0.0000192,\"max\":9007199254740991,\"duration\":42,\"amountOutUsd\":3124.8532440000004,\"quoteId\":\"6a2b55e3a5f07ecb32c99797\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8669634499999999,\"min\":0.00048179,\"max\":6.6845044,\"duration\":34,\"amountOutUsd\":3124.624708458,\"quoteId\":\"6a2b55e3a5f07ecb32c99829\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86684937,\"min\":0.003142,\"max\":47.137363,\"duration\":30,\"amountOutUsd\":3124.4337796068003,\"quoteId\":\"6a2b55e4a5f07ecb32c9986f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86682954,\"min\":0.0000192,\"max\":9007199254740991,\"duration\":35,\"amountOutUsd\":3124.4005913256,\"quoteId\":\"6a2b55e4a5f07ecb32c99872\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86681,\"min\":0.0000223,\"max\":9007199254740991,\"duration\":28,\"amountOutUsd\":3124.3678884000005,\"quoteId\":\"6a2b55e3a5f07ecb32c9977b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86680044,\"min\":0.003142,\"max\":47.137363,\"duration\":30,\"amountOutUsd\":3124.3518884016003,\"quoteId\":\"6a2b55e3a5f07ecb32c997f8\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8667629,\"min\":0.001595809,\"max\":9007199254740991,\"duration\":12,\"amountOutUsd\":3124.289059956,\"quoteId\":\"6a2b55e3a5f07ecb32c99804\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8667503783,\"min\":0.00048166,\"max\":0.32045,\"duration\":10,\"amountOutUsd\":3124.268103138012,\"quoteId\":\"6a2b55e4a5f07ecb32c9986d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86671818,\"min\":0.00048179,\"max\":6.6845044,\"duration\":34,\"amountOutUsd\":3124.2142147752,\"quoteId\":\"6a2b55e4a5f07ecb32c99881\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86671,\"min\":0.001595809,\"max\":9007199254740991,\"duration\":25,\"amountOutUsd\":3124.2005244,\"quoteId\":\"6a2b55e3a5f07ecb32c997d5\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86668694,\"min\":0.00157199,\"max\":6.40645822,\"duration\":18,\"amountOutUsd\":3124.1619302616,\"quoteId\":\"6a2b55e3a5f07ecb32c99807\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86668626,\"min\":0.00048166,\"max\":0.32045,\"duration\":7,\"amountOutUsd\":3124.1607921864,\"quoteId\":\"6a2b55e3a5f07ecb32c99822\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8666322,\"min\":0.00157199,\"max\":7.47442658,\"duration\":25,\"amountOutUsd\":3124.070315208,\"quoteId\":\"6a2b55e3a5f07ecb32c997b4\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8666173,\"min\":0.00157199,\"max\":6.40645822,\"duration\":17,\"amountOutUsd\":3124.045377972,\"quoteId\":\"6a2b55e3a5f07ecb32c997c8\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86657158,\"min\":0.00048141,\"max\":8.38020876,\"duration\":19,\"amountOutUsd\":3123.9688591512004,\"quoteId\":\"6a2b55e4a5f07ecb32c99883\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86647019,\"min\":0.00048141,\"max\":8.38020876,\"duration\":3,\"amountOutUsd\":3123.7991687916,\"quoteId\":\"6a2b55e3a5f07ecb32c9982d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8664520330240002,\"min\":0.00157199,\"max\":0.28618039,\"duration\":28,\"amountOutUsd\":3123.768780550288,\"quoteId\":\"6a2b55e3a5f07ecb32c997b0\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86639,\"min\":0.0000221,\"max\":9007199254740991,\"duration\":29,\"amountOutUsd\":3123.6649596,\"quoteId\":\"6a2b55e3a5f07ecb32c997e0\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86638,\"min\":0.00157199,\"max\":29.69741861,\"duration\":11,\"amountOutUsd\":3123.6482232,\"quoteId\":\"6a2b55e3a5f07ecb32c99779\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8663,\"min\":0.003142,\"max\":47.137363,\"duration\":45,\"amountOutUsd\":3123.514332,\"quoteId\":\"6a2b55e3a5f07ecb32c99834\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8662517,\"min\":0.003142,\"max\":47.127411,\"duration\":30,\"amountOutUsd\":3123.4334951880005,\"quoteId\":\"6a2b55e3a5f07ecb32c997a6\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86624,\"min\":0.00157199,\"max\":21.80460836,\"duration\":24,\"amountOutUsd\":3123.4139136,\"quoteId\":\"6a2b55e3a5f07ecb32c9976e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86618,\"min\":0.00047,\"max\":141.36,\"duration\":10,\"amountOutUsd\":3123.3134952,\"quoteId\":\"6a2b55e3a5f07ecb32c9976d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8661513,\"min\":0.001595569,\"max\":9007199254740991,\"duration\":25,\"amountOutUsd\":3123.2654617320004,\"quoteId\":\"6a2b55e3a5f07ecb32c997f3\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.866,\"min\":0.00048141,\"max\":8.38020876,\"duration\":8,\"amountOutUsd\":3123.0122400000005,\"quoteId\":\"6a2b55e3a5f07ecb32c9984a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86596815,\"min\":0.00157199,\"max\":21.80460836,\"duration\":17,\"amountOutUsd\":3122.958934566,\"quoteId\":\"6a2b55e3a5f07ecb32c99814\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86595063,\"min\":0.00048166,\"max\":0.32045,\"duration\":18,\"amountOutUsd\":3122.9296123932,\"quoteId\":\"6a2b55e4a5f07ecb32c9988a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86590874,\"min\":0.00047,\"max\":141.36,\"duration\":3,\"amountOutUsd\":3122.8595036136003,\"quoteId\":\"6a2b55e3a5f07ecb32c99815\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86562,\"min\":0.00157199,\"max\":6.40645822,\"duration\":19,\"amountOutUsd\":3122.3762568,\"quoteId\":\"6a2b55e3a5f07ecb32c9977d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.865403383072,\"min\":0.00047,\"max\":141.32,\"duration\":8,\"amountOutUsd\":3122.0137180446222,\"quoteId\":\"6a2b55e3a5f07ecb32c997af\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86532,\"min\":0.00157199,\"max\":0.28618039,\"duration\":24,\"amountOutUsd\":3121.8741648000005,\"quoteId\":\"6a2b55e3a5f07ecb32c997b3\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8653,\"min\":0.00048166,\"max\":0.32045,\"duration\":10,\"amountOutUsd\":3121.840692,\"quoteId\":\"6a2b55e3a5f07ecb32c9984b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.865231,\"min\":0.001595569,\"max\":9007199254740991,\"duration\":23,\"amountOutUsd\":3121.7252108400003,\"quoteId\":\"6a2b55e3a5f07ecb32c99805\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8652086,\"min\":0.003142,\"max\":47.127411,\"duration\":30,\"amountOutUsd\":3121.687721304,\"quoteId\":\"6a2b55e3a5f07ecb32c997c6\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86488893,\"min\":0.0000193,\"max\":9007199254740991,\"duration\":35,\"amountOutUsd\":3121.1527088052003,\"quoteId\":\"6a2b55e4a5f07ecb32c99880\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86474042,\"min\":0.00157199,\"max\":7.47442658,\"duration\":29,\"amountOutUsd\":3120.9041565288003,\"quoteId\":\"6a2b55e4a5f07ecb32c99866\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86463,\"min\":0.001595809,\"max\":9007199254740991,\"duration\":19,\"amountOutUsd\":3120.7193532,\"quoteId\":\"6a2b55e3a5f07ecb32c997b2\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.864609,\"min\":0.00048179,\"max\":6.6845044,\"duration\":33,\"amountOutUsd\":3120.68420676,\"quoteId\":\"6a2b55e3a5f07ecb32c99840\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8645764,\"min\":0.003142,\"max\":47.137363,\"duration\":30,\"amountOutUsd\":3120.629646096,\"quoteId\":\"6a2b55e3a5f07ecb32c997e7\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8645228,\"min\":0.003142,\"max\":47.137363,\"duration\":45,\"amountOutUsd\":3120.5399389920003,\"quoteId\":\"6a2b55e3a5f07ecb32c997d8\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86436418,\"min\":0.001595809,\"max\":9007199254740991,\"duration\":12,\"amountOutUsd\":3120.2744662152,\"quoteId\":\"6a2b55e4a5f07ecb32c99867\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8643022,\"min\":0.00048141,\"max\":8.38020876,\"duration\":3,\"amountOutUsd\":3120.170734008,\"quoteId\":\"6a2b55e3a5f07ecb32c9982c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86427,\"min\":0.00047,\"max\":141.32,\"duration\":4,\"amountOutUsd\":3120.1168428000005,\"quoteId\":\"6a2b55e2a5f07ecb32c99763\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8642484,\"min\":0.00048141,\"max\":8.38020876,\"duration\":5,\"amountOutUsd\":3120.080692176,\"quoteId\":\"6a2b55e3a5f07ecb32c99813\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86423,\"min\":0.001595569,\"max\":9007199254740991,\"duration\":25,\"amountOutUsd\":3120.0498972000005,\"quoteId\":\"6a2b55e3a5f07ecb32c997e1\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86419123468,\"min\":0.003142,\"max\":47.127411,\"duration\":30,\"amountOutUsd\":3119.9850180098356,\"quoteId\":\"6a2b55e3a5f07ecb32c99780\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8640345,\"min\":0.001589582,\"max\":9007199254740991,\"duration\":18,\"amountOutUsd\":3119.72270058,\"quoteId\":\"6a2b55e3a5f07ecb32c99800\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.864,\"min\":0.001589582,\"max\":9007199254740991,\"duration\":13,\"amountOutUsd\":3119.6649600000005,\"quoteId\":\"6a2b55e3a5f07ecb32c997d2\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8637712,\"min\":0.00048166,\"max\":0.32045,\"duration\":8,\"amountOutUsd\":3119.2820311680002,\"quoteId\":\"6a2b55e3a5f07ecb32c99816\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86340607,\"min\":0.00047,\"max\":2.009421130413457,\"duration\":17,\"amountOutUsd\":3118.6709349948,\"quoteId\":\"6a2b55e3a5f07ecb32c99788\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8633763944,\"min\":0.003142,\"max\":47.127411,\"duration\":30,\"amountOutUsd\":3118.621268723616,\"quoteId\":\"6a2b55e3a5f07ecb32c9984d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86324671,\"min\":0.003142,\"max\":47.127411,\"duration\":30,\"amountOutUsd\":3118.4042237244003,\"quoteId\":\"6a2b55e3a5f07ecb32c99821\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8629263171519999,\"min\":0.00047,\"max\":2.009421130413457,\"duration\":9,\"amountOutUsd\":3117.868001438273,\"quoteId\":\"6a2b55e3a5f07ecb32c99799\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8628569,\"min\":0.00048166,\"max\":0.32045,\"duration\":6,\"amountOutUsd\":3117.751822116,\"quoteId\":\"6a2b55e3a5f07ecb32c9983e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86280971,\"min\":0.003142,\"max\":47.127411,\"duration\":30,\"amountOutUsd\":3117.6728430444005,\"quoteId\":\"6a2b55e3a5f07ecb32c9983f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86261,\"min\":0.00048141,\"max\":8.38020876,\"duration\":5,\"amountOutUsd\":3117.3386004000004,\"quoteId\":\"6a2b55e3a5f07ecb32c99819\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86251112,\"min\":0.003142,\"max\":47.127411,\"duration\":30,\"amountOutUsd\":3117.1731108768,\"quoteId\":\"6a2b55e4a5f07ecb32c9988e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86247,\"min\":0.00048179,\"max\":6.6845044,\"duration\":40,\"amountOutUsd\":3117.1042908000004,\"quoteId\":\"6a2b55e3a5f07ecb32c99817\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86220575,\"min\":0.00048179,\"max\":6.6845044,\"duration\":33,\"amountOutUsd\":3116.6620314300003,\"quoteId\":\"6a2b55e4a5f07ecb32c9987e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86214634,\"min\":0.001589582,\"max\":9007199254740991,\"duration\":22,\"amountOutUsd\":3116.5626004776004,\"quoteId\":\"6a2b55e4a5f07ecb32c9987f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8619920423679999,\"min\":0.00047,\"max\":141.32,\"duration\":10,\"amountOutUsd\":3116.3043617887793,\"quoteId\":\"6a2b55e3a5f07ecb32c99837\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8619,\"min\":0.003142,\"max\":47.127411,\"duration\":45,\"amountOutUsd\":3116.150316,\"quoteId\":\"6a2b55e3a5f07ecb32c99848\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86186,\"min\":0.00048166,\"max\":0.32045,\"duration\":8,\"amountOutUsd\":3116.0833704,\"quoteId\":\"6a2b55e3a5f07ecb32c99818\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8618109999999999,\"min\":0.00047,\"max\":141.32,\"duration\":3,\"amountOutUsd\":3116.00136204,\"quoteId\":\"6a2b55e3a5f07ecb32c997f4\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.86156709,\"min\":0.00047,\"max\":141.32,\"duration\":3,\"amountOutUsd\":3115.5931445076003,\"quoteId\":\"6a2b55e3a5f07ecb32c99825\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8603353,\"min\":0.003142,\"max\":47.127411,\"duration\":45,\"amountOutUsd\":3113.5315714920002,\"quoteId\":\"6a2b55e3a5f07ecb32c997f2\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.860004001714,\"min\":0.001,\"max\":150,\"duration\":10,\"amountOutUsd\":3112.9770974286193,\"quoteId\":\"6a2b55e3a5f07ecb32c997fd\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.8599821,\"min\":0.00047,\"max\":2.009421130413457,\"duration\":9,\"amountOutUsd\":3112.9404418440004,\"quoteId\":\"6a2b55e4a5f07ecb32c99885\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85982078,\"min\":0.0015729292582931,\"max\":14.671787663267,\"duration\":33,\"amountOutUsd\":3112.6704502392,\"quoteId\":\"6a2b55e3a5f07ecb32c99775\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85981496695,\"min\":0.001,\"max\":45.5,\"duration\":9,\"amountOutUsd\":3112.660721286198,\"quoteId\":\"6a2b55e3a5f07ecb32c99844\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.85977425,\"min\":0.001572113721047,\"max\":15.351467823236,\"duration\":36,\"amountOutUsd\":3112.59257577,\"quoteId\":\"6a2b55e3a5f07ecb32c99784\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85975887442,\"min\":0.001,\"max\":83.4,\"duration\":30,\"amountOutUsd\":3112.566842584289,\"quoteId\":\"6a2b55e3a5f07ecb32c9981b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.85972542,\"min\":0.001,\"max\":8.9,\"duration\":18,\"amountOutUsd\":3112.5108519288,\"quoteId\":\"6a2b55e3a5f07ecb32c997a8\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.8596533000000002,\"min\":0.001,\"max\":45.5,\"duration\":6,\"amountOutUsd\":3112.3901490120006,\"quoteId\":\"6a2b55e3a5f07ecb32c997e4\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.8595494099999998,\"min\":0.001,\"max\":83.4,\"duration\":30,\"amountOutUsd\":3112.2162745524,\"quoteId\":\"6a2b55e3a5f07ecb32c997c4\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.85948038,\"min\":0.0000192,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":3112.1007431832,\"quoteId\":\"6a2b55e3a5f07ecb32c997cd\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8594631,\"min\":0.00047,\"max\":141.32,\"duration\":3,\"amountOutUsd\":3112.071822684,\"quoteId\":\"6a2b55e3a5f07ecb32c997f6\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8594244,\"min\":0.003142,\"max\":47.127411,\"duration\":30,\"amountOutUsd\":3112.007052816,\"quoteId\":\"6a2b55e3a5f07ecb32c99851\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85936863,\"min\":0.001,\"max\":150,\"duration\":23,\"amountOutUsd\":3111.9137139132004,\"quoteId\":\"6a2b55e4a5f07ecb32c99889\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.8593573300000001,\"min\":0.001,\"max\":150,\"duration\":7,\"amountOutUsd\":3111.8948017812004,\"quoteId\":\"6a2b55e3a5f07ecb32c997a2\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.85932403,\"min\":0.001,\"max\":83.4,\"duration\":30,\"amountOutUsd\":3111.8390695692,\"quoteId\":\"6a2b55e3a5f07ecb32c99803\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.859260535032,\"min\":0.001,\"max\":8.9,\"duration\":11,\"amountOutUsd\":3111.7328018509565,\"quoteId\":\"6a2b55e3a5f07ecb32c997fa\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.85894671,\"min\":0.0000223,\"max\":9007199254740991,\"duration\":32,\"amountOutUsd\":3111.2075717244,\"quoteId\":\"6a2b55e3a5f07ecb32c997bd\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85891792,\"min\":0.001,\"max\":45.5,\"duration\":17,\"amountOutUsd\":3111.1593876288,\"quoteId\":\"6a2b55e4a5f07ecb32c9986b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.85887345,\"min\":0.0000221,\"max\":9007199254740991,\"duration\":35,\"amountOutUsd\":3111.084960858,\"quoteId\":\"6a2b55e3a5f07ecb32c99812\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85884182,\"min\":0.001595809,\"max\":9007199254740991,\"duration\":29,\"amountOutUsd\":3111.0320236248003,\"quoteId\":\"6a2b55e3a5f07ecb32c997ee\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8588,\"min\":0.001,\"max\":150,\"duration\":11,\"amountOutUsd\":3110.9620320000004,\"quoteId\":\"6a2b55e3a5f07ecb32c9980b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.85861523,\"min\":0.00157199,\"max\":21.80460836,\"duration\":30,\"amountOutUsd\":3110.6527935372,\"quoteId\":\"6a2b55e3a5f07ecb32c99789\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8586,\"min\":0.001,\"max\":45.5,\"duration\":9,\"amountOutUsd\":3110.627304,\"quoteId\":\"6a2b55e3a5f07ecb32c99831\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.85856469,\"min\":0.00047,\"max\":141.36,\"duration\":30,\"amountOutUsd\":3110.5682077716,\"quoteId\":\"6a2b55e3a5f07ecb32c9978a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85851373,\"min\":0.00157199,\"max\":29.69741861,\"duration\":14,\"amountOutUsd\":3110.4829190772,\"quoteId\":\"6a2b55e3a5f07ecb32c997b7\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85810372,\"min\":0.00157199,\"max\":6.40645822,\"duration\":25,\"amountOutUsd\":3109.7967099408,\"quoteId\":\"6a2b55e3a5f07ecb32c997bf\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8580863,\"min\":0.0000193,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":3109.7675551320003,\"quoteId\":\"6a2b55e3a5f07ecb32c99823\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85793428,\"min\":0.00157199,\"max\":7.47442658,\"duration\":23,\"amountOutUsd\":3109.5131283792,\"quoteId\":\"6a2b55e3a5f07ecb32c997be\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85733,\"min\":0.00047,\"max\":141.32,\"duration\":10,\"amountOutUsd\":3108.5017812,\"quoteId\":\"6a2b55e3a5f07ecb32c997d1\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8572181,\"min\":0.001,\"max\":83.4,\"duration\":30,\"amountOutUsd\":3108.314500884,\"quoteId\":\"6a2b55e3a5f07ecb32c997c9\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.8571093,\"min\":0.001,\"max\":150,\"duration\":7,\"amountOutUsd\":3108.1324088520005,\"quoteId\":\"6a2b55e3a5f07ecb32c997c2\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.8570819,\"min\":0.001,\"max\":150,\"duration\":8,\"amountOutUsd\":3108.0865511160005,\"quoteId\":\"6a2b55e3a5f07ecb32c997a0\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.85701402,\"min\":0.001595809,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":3107.9729444328004,\"quoteId\":\"6a2b55e3a5f07ecb32c997e9\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8567443,\"min\":0.001,\"max\":45.5,\"duration\":7,\"amountOutUsd\":3107.521530252,\"quoteId\":\"6a2b55e3a5f07ecb32c997dc\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.85672371,\"min\":0.001595569,\"max\":9007199254740991,\"duration\":31,\"amountOutUsd\":3107.4870700044003,\"quoteId\":\"6a2b55e3a5f07ecb32c9980a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8563007,\"min\":0.001,\"max\":8.9,\"duration\":11,\"amountOutUsd\":3106.7791035480004,\"quoteId\":\"6a2b55e3a5f07ecb32c997a9\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.8558384,\"min\":0.001,\"max\":45.5,\"duration\":5,\"amountOutUsd\":3106.005379776,\"quoteId\":\"6a2b55e3a5f07ecb32c997de\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.85546775,\"min\":0.003142,\"max\":47.127411,\"duration\":30,\"amountOutUsd\":3105.38504511,\"quoteId\":\"6a2b55e3a5f07ecb32c9979b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85542,\"min\":0.001,\"max\":150,\"duration\":9,\"amountOutUsd\":3105.3051288,\"quoteId\":\"6a2b55e3a5f07ecb32c9977c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.8553487,\"min\":0.001589582,\"max\":9007199254740991,\"duration\":16,\"amountOutUsd\":3105.1857982680003,\"quoteId\":\"6a2b55e3a5f07ecb32c99811\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85509,\"min\":0.001,\"max\":83.4,\"duration\":30,\"amountOutUsd\":3104.7528276,\"quoteId\":\"6a2b55e3a5f07ecb32c99793\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.85504275,\"min\":0.003142,\"max\":47.137363,\"duration\":30,\"amountOutUsd\":3104.67374811,\"quoteId\":\"6a2b55e3a5f07ecb32c997e8\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85487128,\"min\":0.00048179,\"max\":6.6845044,\"duration\":30,\"amountOutUsd\":3104.3867690592,\"quoteId\":\"6a2b55e3a5f07ecb32c99843\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85484,\"min\":0.001,\"max\":45.5,\"duration\":7,\"amountOutUsd\":3104.3344176,\"quoteId\":\"6a2b55e3a5f07ecb32c997b1\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.85482947,\"min\":0.001,\"max\":83.4,\"duration\":30,\"amountOutUsd\":3104.3167941708,\"quoteId\":\"6a2b55e4a5f07ecb32c99868\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.85477,\"min\":0.00048141,\"max\":8.38020876,\"duration\":9,\"amountOutUsd\":3104.2172628000003,\"quoteId\":\"6a2b55e3a5f07ecb32c99842\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85441362,\"min\":0.001,\"max\":8.9,\"duration\":15,\"amountOutUsd\":3103.6208109768004,\"quoteId\":\"6a2b55e4a5f07ecb32c99865\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.85436044,\"min\":0.00048166,\"max\":0.32045,\"duration\":14,\"amountOutUsd\":3103.5318068016004,\"quoteId\":\"6a2b55e3a5f07ecb32c99841\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8524621276540003,\"min\":0.001,\"max\":150,\"duration\":32,\"amountOutUsd\":3100.3547153268414,\"quoteId\":\"6a2b55e4a5f07ecb32c99860\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.852179637748,\"min\":0.001,\"max\":83.4,\"duration\":26,\"amountOutUsd\":3099.881928920563,\"quoteId\":\"6a2b55e4a5f07ecb32c99871\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8521297813999997,\"min\":0.001,\"max\":45.5,\"duration\":51,\"amountOutUsd\":3099.7984873422956,\"quoteId\":\"6a2b55e4a5f07ecb32c9988b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85210753,\"min\":0.001,\"max\":45.5,\"duration\":47,\"amountOutUsd\":3099.7612465092,\"quoteId\":\"6a2b55e4a5f07ecb32c9987a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85201846,\"min\":0.001,\"max\":83.4,\"duration\":20,\"amountOutUsd\":3099.6121753944003,\"quoteId\":\"6a2b55e3a5f07ecb32c99820\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85193644,\"min\":0.0000192,\"max\":9007199254740991,\"duration\":45,\"amountOutUsd\":3099.4749034416004,\"quoteId\":\"6a2b55e3a5f07ecb32c99839\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.851781,\"min\":0.001,\"max\":83.4,\"duration\":20,\"amountOutUsd\":3099.21475284,\"quoteId\":\"6a2b55e3a5f07ecb32c99852\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85172485,\"min\":0.001,\"max\":150,\"duration\":29,\"amountOutUsd\":3099.1207779540005,\"quoteId\":\"6a2b55e3a5f07ecb32c9982b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85131513,\"min\":0.00047,\"max\":2.009421130413457,\"duration\":7,\"amountOutUsd\":3098.4350541732,\"quoteId\":\"6a2b55e3a5f07ecb32c9979e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8513,\"min\":0.001,\"max\":150,\"duration\":33,\"amountOutUsd\":3098.409732,\"quoteId\":\"6a2b55e3a5f07ecb32c9984c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85107481,\"min\":0.00157199,\"max\":21.80460836,\"duration\":29,\"amountOutUsd\":3098.0328450084003,\"quoteId\":\"6a2b55e3a5f07ecb32c997d6\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85102447,\"min\":0.00047,\"max\":141.36,\"duration\":14,\"amountOutUsd\":3097.9485939708,\"quoteId\":\"6a2b55e3a5f07ecb32c997e2\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.85094359,\"min\":0.003142,\"max\":47.127411,\"duration\":30,\"amountOutUsd\":3097.8132299676004,\"quoteId\":\"6a2b55e3a5f07ecb32c99809\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8507,\"min\":0.001,\"max\":45.5,\"duration\":50,\"amountOutUsd\":3097.405548,\"quoteId\":\"6a2b55e4a5f07ecb32c99879\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.84975213,\"min\":0.00047,\"max\":141.32,\"duration\":30,\"amountOutUsd\":3095.8191548532,\"quoteId\":\"6a2b55e3a5f07ecb32c997f7\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8496829,\"min\":0.001,\"max\":83.4,\"duration\":19,\"amountOutUsd\":3095.703288756,\"quoteId\":\"6a2b55e3a5f07ecb32c9983c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8495749,\"min\":0.001,\"max\":150,\"duration\":29,\"amountOutUsd\":3095.522535636,\"quoteId\":\"6a2b55e3a5f07ecb32c99830\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8495087,\"min\":0.001,\"max\":150,\"duration\":30,\"amountOutUsd\":3095.411740668,\"quoteId\":\"6a2b55e3a5f07ecb32c99801\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.84948009,\"min\":0.001595809,\"max\":9007199254740991,\"duration\":23,\"amountOutUsd\":3095.3638578276,\"quoteId\":\"6a2b55e3a5f07ecb32c99859\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8492045,\"min\":0.001,\"max\":45.5,\"duration\":48,\"amountOutUsd\":3094.90261938,\"quoteId\":\"6a2b55e3a5f07ecb32c9985c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.84837701,\"min\":0.001,\"max\":45.5,\"duration\":48,\"amountOutUsd\":3093.5176990164005,\"quoteId\":\"6a2b55e4a5f07ecb32c9988f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.8483082,\"min\":0.001,\"max\":45.5,\"duration\":47,\"amountOutUsd\":3093.4025358480003,\"quoteId\":\"6a2b55e4a5f07ecb32c99864\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.84794009,\"min\":0.003142,\"max\":47.127411,\"duration\":30,\"amountOutUsd\":3092.7864522276004,\"quoteId\":\"6a2b55e3a5f07ecb32c997ae\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.84789,\"min\":0.001,\"max\":150,\"duration\":31,\"amountOutUsd\":3092.7026196,\"quoteId\":\"6a2b55e3a5f07ecb32c997fb\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.84755,\"min\":0.001,\"max\":83.4,\"duration\":26,\"amountOutUsd\":3092.1335820000004,\"quoteId\":\"6a2b55e3a5f07ecb32c997fe\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.84734604,\"min\":0.00048179,\"max\":6.6845044,\"duration\":45,\"amountOutUsd\":3091.7922263856,\"quoteId\":\"6a2b55e4a5f07ecb32c9987d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.84731,\"min\":0.001,\"max\":45.5,\"duration\":48,\"amountOutUsd\":3091.7319084,\"quoteId\":\"6a2b55e3a5f07ecb32c99858\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.84730467,\"min\":0.001,\"max\":83.4,\"duration\":19,\"amountOutUsd\":3091.7229878988,\"quoteId\":\"6a2b55e4a5f07ecb32c9987b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.84224766,\"min\":0.00047,\"max\":141.32,\"duration\":14,\"amountOutUsd\":3083.2593736824,\"quoteId\":\"6a2b55e4a5f07ecb32c9985f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.84011788,\"min\":0.001,\"max\":150,\"duration\":34,\"amountOutUsd\":3079.6948886832,\"quoteId\":\"6a2b55e3a5f07ecb32c9981c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.84002339,\"min\":0.001,\"max\":83.4,\"duration\":30,\"amountOutUsd\":3079.5367464396004,\"quoteId\":\"6a2b55e3a5f07ecb32c99838\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.05,\"amountOut\":1.84002285,\"min\":0.001,\"max\":83.4,\"duration\":30,\"amountOutUsd\":3079.5358426740004,\"quoteId\":\"6a2b55e3a5f07ecb32c9981f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.83987795,\"min\":0.001,\"max\":45.5,\"duration\":54,\"amountOutUsd\":3079.293332238,\"quoteId\":\"6a2b55e4a5f07ecb32c99877\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.81356623,\"min\":0.001572113721047,\"max\":15.351467823236,\"duration\":31,\"amountOutUsd\":3035.2569851772,\"quoteId\":\"6a2b55e3a5f07ecb32c99806\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.81270504,\"min\":0.0000223,\"max\":9007199254740991,\"duration\":28,\"amountOutUsd\":3033.8156631456,\"quoteId\":\"6a2b55e3a5f07ecb32c997da\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.81228282,\"min\":0.00157199,\"max\":29.69741861,\"duration\":10,\"amountOutUsd\":3033.1090188648004,\"quoteId\":\"6a2b55e3a5f07ecb32c99828\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":1.80165346,\"min\":0.001,\"max\":150,\"duration\":8,\"amountOutUsd\":3015.3192967944,\"quoteId\":\"6a2b55e3a5f07ecb32c99846\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"}]}" +} \ No newline at end of file diff --git a/test/houdiniFixtures/156a7ca52b57e25148623452.json b/test/houdiniFixtures/156a7ca52b57e25148623452.json new file mode 100644 index 00000000..af578754 --- /dev/null +++ b/test/houdiniFixtures/156a7ca52b57e25148623452.json @@ -0,0 +1,5 @@ +{ + "status": 200, + "ok": true, + "body": "{\"tokens\":[{\"address\":null,\"chain\":\"bitcoin\",\"decimals\":8,\"symbol\":\"BTC\",\"name\":\"Bitcoin\",\"created\":\"2024-07-06T21:29:34.822Z\",\"modified\":\"2026-06-09T00:21:14.464Z\",\"chainData\":{\"created\":\"2024-07-06T21:02:11.671Z\",\"modified\":\"2026-06-10T08:00:33.712Z\",\"name\":\"Bitcoin\",\"shortName\":\"bitcoin\",\"memoNeeded\":false,\"explorerUrl\":\"https://blockchair.com/bitcoin/transaction/{txHash}\",\"addressUrl\":\"https://blockchair.com/bitcoin/address/{address}\",\"priority\":3,\"kind\":\"bitcoin\",\"enabled\":true,\"shortNameV1\":\"BTC\",\"addressValidation\":\"^([13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[a-z0-9]{39}|bc1[a-z0-9]{59})$\",\"tokenAddressValidation\":\"^([13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[a-z0-9]{39}|bc1[a-z0-9]{59})$\",\"id\":\"6689b0d3c52f263938c44843\",\"icon\":\"https://api.houdiniswap.com/assets/networks/BTC.png\"},\"description\":\"Bitcoin is the first successful internet money based on peer-to-peer technology; whereby no central bank or authority is involved in the transaction and production of the Bitcoin currency. It was created by an anonymous individual/group under the name, Satoshi Nakamoto. The source code is available publicly as an open source project, anybody can look at it and be part of the developmental process.\\r\\n\\r\\nBitcoin is changing the way we see money as we speak. The idea was to produce a means of exchange, independent of any central authority, that could be transferred electronically in a secure, verifiable and immutable way. It is a decentralized peer-to-peer internet currency making mobile payment easy, very low transaction fees, protects your identity, and it works anywhere all the time with no central authority and banks.\\r\\n\\r\\nBitcoin is designed to have only 21 million BTC ever created, thus making it a deflationary currency. Bitcoin uses the SHA-256 hashing algorithm with an average transaction confirmation time of 10 minutes. Miners today are mining Bitcoin using ASIC chip dedicated to only mining Bitcoin, and the hash rate has shot up to peta hashes.\\r\\n\\r\\nBeing the first successful online cryptography currency, Bitcoin has inspired other alternative currencies such as Litecoin, Peercoin, Primecoin, and so on.\\r\\n\\r\\nThe cryptocurrency then took off with the innovation of the turing-complete smart contract by Ethereum which led to the development of other amazing projects such as EOS, Tron, and even crypto-collectibles such as CryptoKitties.\",\"mainnet\":true,\"enabled\":true,\"unverified\":false,\"hasDex\":true,\"hasCex\":true,\"hasSelfPrivate\":true,\"cexTokenId\":\"BTC\",\"rank\":1,\"cgId\":\"bitcoin\",\"marketCapChange24h\":2.9975,\"circulatingSupply\":20041521,\"price\":63674.49,\"marketCap\":1274943393465,\"volume\":30050534298,\"fdv\":1274944029616,\"change\":2.96638,\"id\":\"6689b73ec90e45f3b3e51551\",\"icon\":\"https://api.houdiniswap.com/assets/tokens/BTC-BTC-v79cxh.png\"}],\"total\":1,\"totalPages\":1}" +} \ No newline at end of file diff --git a/test/houdiniFixtures/16d07fa292879c91f38b4355.json b/test/houdiniFixtures/16d07fa292879c91f38b4355.json new file mode 100644 index 00000000..31550776 --- /dev/null +++ b/test/houdiniFixtures/16d07fa292879c91f38b4355.json @@ -0,0 +1,5 @@ +{ + "status": 200, + "ok": true, + "body": "{\"created\":\"2026-06-12T00:42:14.425Z\",\"houdiniId\":\"dvsVRFqLQSonDqkLzArFJP\",\"receiverAddress\":\"0x9f1f9a5c0f1d9a5c0f1d9a5c0f1d9a5c0f1d9a5c\",\"status\":0,\"anonymous\":true,\"expires\":\"2026-06-12T01:12:14.425Z\",\"inAmount\":0.05,\"inSymbol\":\"BTC\",\"inStatus\":0,\"inCreated\":\"2026-06-12T00:42:14.425Z\",\"outAmount\":1.8711460317750002,\"outSymbol\":\"ETH\",\"outStatus\":0,\"eta\":29,\"inAmountUsd\":3181,\"inToken\":{\"address\":null,\"chain\":\"bitcoin\",\"decimals\":8,\"symbol\":\"BTC\",\"name\":\"Bitcoin\",\"created\":\"2024-07-06T21:29:34.822Z\",\"modified\":\"2026-06-09T00:21:14.464Z\",\"chainData\":{\"created\":\"2024-07-06T21:02:11.671Z\",\"modified\":\"2026-06-10T08:00:33.712Z\",\"name\":\"Bitcoin\",\"shortName\":\"bitcoin\",\"memoNeeded\":false,\"explorerUrl\":\"https://blockchair.com/bitcoin/transaction/{txHash}\",\"addressUrl\":\"https://blockchair.com/bitcoin/address/{address}\",\"priority\":3,\"kind\":\"bitcoin\",\"enabled\":true,\"shortNameV1\":\"BTC\",\"addressValidation\":\"^([13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[a-z0-9]{39}|bc1[a-z0-9]{59})$\",\"tokenAddressValidation\":\"^([13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[a-z0-9]{39}|bc1[a-z0-9]{59})$\",\"id\":\"6689b0d3c52f263938c44843\",\"icon\":\"https://api.houdiniswap.com/assets/networks/BTC.png\"},\"description\":\"Bitcoin is the first successful internet money based on peer-to-peer technology; whereby no central bank or authority is involved in the transaction and production of the Bitcoin currency. It was created by an anonymous individual/group under the name, Satoshi Nakamoto. The source code is available publicly as an open source project, anybody can look at it and be part of the developmental process.\\r\\n\\r\\nBitcoin is changing the way we see money as we speak. The idea was to produce a means of exchange, independent of any central authority, that could be transferred electronically in a secure, verifiable and immutable way. It is a decentralized peer-to-peer internet currency making mobile payment easy, very low transaction fees, protects your identity, and it works anywhere all the time with no central authority and banks.\\r\\n\\r\\nBitcoin is designed to have only 21 million BTC ever created, thus making it a deflationary currency. Bitcoin uses the SHA-256 hashing algorithm with an average transaction confirmation time of 10 minutes. Miners today are mining Bitcoin using ASIC chip dedicated to only mining Bitcoin, and the hash rate has shot up to peta hashes.\\r\\n\\r\\nBeing the first successful online cryptography currency, Bitcoin has inspired other alternative currencies such as Litecoin, Peercoin, Primecoin, and so on.\\r\\n\\r\\nThe cryptocurrency then took off with the innovation of the turing-complete smart contract by Ethereum which led to the development of other amazing projects such as EOS, Tron, and even crypto-collectibles such as CryptoKitties.\",\"mainnet\":true,\"enabled\":true,\"unverified\":false,\"hasDex\":true,\"hasCex\":true,\"hasSelfPrivate\":true,\"cexTokenId\":\"BTC\",\"rank\":1,\"cgId\":\"bitcoin\",\"marketCapChange24h\":2.9975,\"circulatingSupply\":20041521,\"price\":63681.63,\"marketCap\":1274943393465,\"volume\":30488002875,\"fdv\":1274944029616,\"change\":2.9789,\"id\":\"6689b73ec90e45f3b3e51551\",\"icon\":\"https://api.houdiniswap.com/assets/tokens/BTC-BTC-v79cxh.png\"},\"outToken\":{\"address\":null,\"chain\":\"ethereum\",\"decimals\":18,\"symbol\":\"ETH\",\"name\":\"Ethereum\",\"created\":\"2024-07-06T21:29:34.823Z\",\"modified\":\"2026-06-11T21:36:16.580Z\",\"chainData\":{\"created\":\"2024-07-06T21:02:11.672Z\",\"modified\":\"2026-06-10T08:00:33.714Z\",\"name\":\"Ethereum Mainnet\",\"shortName\":\"ethereum\",\"memoNeeded\":false,\"explorerUrl\":\"https://etherscan.io/tx/{txHash}\",\"addressUrl\":\"https://etherscan.io/address/{address}\",\"priority\":2,\"kind\":\"evm\",\"chainId\":1,\"enabled\":true,\"shortNameV1\":\"ETH\",\"addressValidation\":\"^(0x)[0-9A-Za-z]{40}$\",\"tokenAddressValidation\":\"^(0x)[0-9A-Za-z]{40}$\",\"id\":\"6689b0d3c52f263938c44844\",\"icon\":\"https://api.houdiniswap.com/assets/networks/ETH.png\"},\"description\":\"Ethereum is a global, open-source platform for decentralized applications. In other words, the vision is to create a world computer that anyone can build applications in a decentralized manner; while all states and data are distributed and publicly accessible. Ethereum supports smart contracts in which developers can write code in order to program digital value. Examples of decentralized apps (dapps) that are built on Ethereum includes tokens, non-fungible tokens, decentralized finance apps, lending protocol, decentralized exchanges, and much more.\\r\\n\\r\\nOn Ethereum, all transactions and smart contract executions require a small fee to be paid. This fee is called Gas. In technical terms, Gas refers to the unit of measure on the amount of computational effort required to execute an operation or a smart contract. The more complex the execution operation is, the more gas is required to fulfill that operation. Gas fees are paid entirely in Ether (ETH), which is the native coin of the blockchain. The price of gas can fluctuate from time to time depending on the network demand.\",\"mainnet\":true,\"enabled\":true,\"unverified\":false,\"hasDex\":true,\"hasCex\":true,\"hasSelfPrivate\":true,\"cexTokenId\":\"ETH\",\"rank\":2,\"cgId\":\"ethereum\",\"marketCapChange24h\":2.86273,\"circulatingSupply\":120684325.0427515,\"price\":1675.21,\"marketCap\":201979868266,\"volume\":12414318165,\"fdv\":201979868266,\"change\":2.85178,\"priority\":2,\"id\":\"6689b73ec90e45f3b3e51566\",\"icon\":\"https://api.houdiniswap.com/assets/tokens/ETH-ETH-lejbdy.png\"},\"depositAddress\":\"bc1qa0cw8r89n974366y7cakn7pj2p66u05shvvshm\",\"id\":\"6a2b55e6a5f07ecb32c99895\",\"hashUrl\":\"\",\"statusLabel\":\"WAITING\",\"inStatusLabel\":\"NEW\",\"outStatusLabel\":\"NEW\",\"displayStatus\":\"WAITING_FOR_DEPOSIT\"}" +} \ No newline at end of file diff --git a/test/houdiniFixtures/313028cd38f51324e739af87.json b/test/houdiniFixtures/313028cd38f51324e739af87.json new file mode 100644 index 00000000..da8a2543 --- /dev/null +++ b/test/houdiniFixtures/313028cd38f51324e739af87.json @@ -0,0 +1,5 @@ +{ + "status": 200, + "ok": true, + "body": "{\"total\":3,\"quotes\":[{\"amountIn\":0.004214984832207085,\"amountOut\":0.15,\"min\":0.001615509,\"max\":6.25201983,\"duration\":15,\"amountOutUsd\":254.9835,\"amountInUsd\":258.776506932303,\"swap\":\"ch\",\"swapName\":\"ChangeHero\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/changehero-o8i3zz.png\",\"quoteId\":\"6a46ff6e732524ae484188ac\",\"type\":\"standard\",\"rewardsAvailable\":true,\"fixed\":true,\"rateId\":\"04075786-8903-4f6e-8063-a4f101946f92\",\"validUntil\":\"1783037880\",\"amountType\":\"receive\"},{\"amountIn\":0.00421549,\"amountOut\":0.15,\"min\":0.00162976,\"max\":220.01688808,\"duration\":19,\"amountOutUsd\":254.9835,\"amountInUsd\":258.8075214109,\"swap\":\"cl\",\"swapName\":\"Changelly\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/changelly.jpg\",\"quoteId\":\"6a46ff6e732524ae484188ab\",\"type\":\"standard\",\"rewardsAvailable\":true,\"fixed\":true,\"rateId\":\"MGxO#aUNt733xu6IKGV@CpPkDFX4kd\",\"validUntil\":\"1783037866\",\"amountType\":\"receive\"},{\"amountIn\":0.00422954,\"amountOut\":0.15,\"min\":0.0000191,\"max\":9007199254740991,\"duration\":23,\"amountOutUsd\":254.9835,\"amountInUsd\":259.6701128714,\"swap\":\"cn\",\"swapName\":\"ChangeNow\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/changenow.png\",\"quoteId\":\"6a46ff6f732524ae484188ad\",\"type\":\"standard\",\"rewardsAvailable\":true,\"fixed\":true,\"rateId\":\"nktHlvXv+qWlAE7EkSk9a3BGCznxNjb3VKwHFXbKXDQsLbt4nGd6gINjtYgsX6vozUxh3z1+LRMr942fUBKBeyyiXUQT3YTVHtkbjSDQ/30SXVpsuC3oOnd71AquPKoYQ/MfK+tLm9kQn4al7EP8wy7w6mh/GqDv3HmLVf8A+GY1u8WAjZv3xZE8CsWAyNTvXe9sjs15yCbzUYvQgK7jvQ==\",\"validUntil\":\"1783037927\",\"amountType\":\"receive\"}]}" +} \ No newline at end of file diff --git a/test/houdiniFixtures/31b70954973add1cec4f0f9a.json b/test/houdiniFixtures/31b70954973add1cec4f0f9a.json new file mode 100644 index 00000000..a63f2b66 --- /dev/null +++ b/test/houdiniFixtures/31b70954973add1cec4f0f9a.json @@ -0,0 +1,5 @@ +{ + "status": 422, + "ok": false, + "body": "{\"code\":\"VALIDATION_ERROR\",\"requestId\":\"eb2e2cbe-7354-47e5-a45b-0c9f5e1fd99c\",\"fields\":{\"quoteId\":{\"value\":\"6a46fd23350134bfbdac5606\",\"message\":\"The provided quote has expired, max age is 60 seconds\"}},\"message\":\"Validation Failed\"}" +} \ No newline at end of file diff --git a/test/houdiniFixtures/3ce403c04da7f57d376f6ece.json b/test/houdiniFixtures/3ce403c04da7f57d376f6ece.json new file mode 100644 index 00000000..1be59878 --- /dev/null +++ b/test/houdiniFixtures/3ce403c04da7f57d376f6ece.json @@ -0,0 +1,5 @@ +{ + "status": 422, + "ok": false, + "body": "{\"code\":\"ADDRESS_TO_IN_DEPOSIT_LOG\",\"requestId\":\"2379a49a-af48-47e2-868d-9350c032a006\",\"fields\":{},\"message\":\"addressTo cannot be used as it exists in deposit log\"}" +} \ No newline at end of file diff --git a/test/houdiniFixtures/4a5f43a0830230f902b1c76d.json b/test/houdiniFixtures/4a5f43a0830230f902b1c76d.json new file mode 100644 index 00000000..5ffb8e6b --- /dev/null +++ b/test/houdiniFixtures/4a5f43a0830230f902b1c76d.json @@ -0,0 +1,5 @@ +{ + "status": 200, + "ok": true, + "body": "{\"tokens\":[{\"address\":\"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48\",\"chain\":\"ethereum\",\"decimals\":6,\"symbol\":\"USDC\",\"name\":\"USD Coin\",\"created\":\"2024-07-06T21:29:34.822Z\",\"modified\":\"2026-06-10T20:21:14.723Z\",\"chainData\":{\"created\":\"2024-07-06T21:02:11.672Z\",\"modified\":\"2026-06-10T08:00:33.714Z\",\"name\":\"Ethereum Mainnet\",\"shortName\":\"ethereum\",\"memoNeeded\":false,\"explorerUrl\":\"https://etherscan.io/tx/{txHash}\",\"addressUrl\":\"https://etherscan.io/address/{address}\",\"priority\":2,\"kind\":\"evm\",\"chainId\":1,\"enabled\":true,\"shortNameV1\":\"ETH\",\"addressValidation\":\"^(0x)[0-9A-Za-z]{40}$\",\"tokenAddressValidation\":\"^(0x)[0-9A-Za-z]{40}$\",\"id\":\"6689b0d3c52f263938c44844\",\"icon\":\"https://api.houdiniswap.com/assets/networks/ETH.png\"},\"description\":\"USDC is a fully collateralized US dollar stablecoin. USDC is the bridge between dollars and trading on cryptocurrency exchanges. The technology behind CENTRE makes it possible to exchange value between people, businesses and financial institutions just like email between mail services and texts between SMS providers. We believe by removing artificial economic borders, we can create a more inclusive global economy.\",\"mainnet\":false,\"enabled\":true,\"unverified\":false,\"hasDex\":true,\"hasCex\":true,\"hasSelfPrivate\":true,\"cexTokenId\":\"USDC\",\"rank\":5,\"cgId\":\"usd-coin\",\"marketCapChange24h\":-0.10051,\"circulatingSupply\":74904676501.2519,\"price\":0.99986,\"marketCap\":74893649759,\"volume\":13374265209,\"fdv\":74893649759,\"change\":0.00488,\"id\":\"6689b73ec90e45f3b3e51554\",\"icon\":\"https://api.houdiniswap.com/assets/tokens/USDC-ETH-7b8for.png\"}],\"total\":1,\"totalPages\":1}" +} \ No newline at end of file diff --git a/test/houdiniFixtures/4b39758d02ca7a9c171afb57.json b/test/houdiniFixtures/4b39758d02ca7a9c171afb57.json new file mode 100644 index 00000000..284957ad --- /dev/null +++ b/test/houdiniFixtures/4b39758d02ca7a9c171afb57.json @@ -0,0 +1,5 @@ +{ + "status": 409, + "ok": false, + "body": "{\"code\":\"STATIC_DEPOSIT_IN_USE\",\"requestId\":\"a63dbaba-e067-45e2-a64f-a88d15eed6c3\",\"message\":\"Deposit address for ch BTC is in use by another live order\"}" +} \ No newline at end of file diff --git a/test/houdiniFixtures/4fac6bf25458eb12b49fc1b8.json b/test/houdiniFixtures/4fac6bf25458eb12b49fc1b8.json new file mode 100644 index 00000000..3a8960bd --- /dev/null +++ b/test/houdiniFixtures/4fac6bf25458eb12b49fc1b8.json @@ -0,0 +1,5 @@ +{ + "status": 200, + "ok": true, + "body": "{\"total\":64,\"quotes\":[{\"amountIn\":0.1,\"amountOut\":155.148,\"min\":0.017599242,\"max\":9007199254740991,\"duration\":5,\"amountOutUsd\":168.4752132,\"swap\":\"ch\",\"swapName\":\"ChangeHero\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/changehero-o8i3zz.png\",\"quoteId\":\"6a46fdfe732524ae48417a6f\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":155.010521,\"min\":0.01767,\"max\":4589.32,\"duration\":3,\"amountOutUsd\":168.32592475390004,\"swap\":\"eb\",\"swapName\":\"EasyBit\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/easybit.jpg\",\"quoteId\":\"6a46fdfd732524ae48417a60\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":154.882348,\"min\":0.0001464,\"max\":9007199254740991,\"duration\":10,\"amountOutUsd\":168.18674169320002,\"swap\":\"cn\",\"swapName\":\"ChangeNow\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/changenow.png\",\"quoteId\":\"6a46fdfe732524ae48417a72\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":154.77816199999998,\"min\":0.05890077,\"max\":241.73096318,\"duration\":3,\"amountOutUsd\":168.0736061158,\"swap\":\"cl\",\"swapName\":\"Changelly\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/changelly.jpg\",\"quoteId\":\"6a46fdfd732524ae48417a5c\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.89408057,\"min\":0.01767,\"max\":4589.32,\"duration\":4,\"amountOutUsd\":167.113582090963,\"quoteId\":\"6a46fdfe732524ae48417aba\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.88370351,\"min\":0.017599242,\"max\":23944.70879402953,\"duration\":8,\"amountOutUsd\":167.10231364150903,\"quoteId\":\"6a46fdfe732524ae48417abd\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.76451732,\"min\":0.017599242,\"max\":1163.10566789146,\"duration\":11,\"amountOutUsd\":166.97288935778803,\"quoteId\":\"6a46fdff732524ae48417ac2\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.69679,\"min\":0.017599242,\"max\":9007199254740991,\"duration\":16,\"amountOutUsd\":166.899344261,\"quoteId\":\"6a46fdfe732524ae48417aa4\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.66507913,\"min\":0.005351067811277025,\"max\":23978.731440701653,\"duration\":11,\"amountOutUsd\":166.86490942726704,\"quoteId\":\"6a46fdfe732524ae48417abe\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.664,\"min\":0.017693959046186154,\"max\":4589.32,\"duration\":8,\"amountOutUsd\":166.8637376,\"quoteId\":\"6a46fdfe732524ae48417a77\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.601423,\"min\":0.01767,\"max\":4589.32,\"duration\":13,\"amountOutUsd\":166.79578523570004,\"quoteId\":\"6a46fdfe732524ae48417a86\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.591669,\"min\":0.017848385935069613,\"max\":4632.9180363040105,\"duration\":10,\"amountOutUsd\":166.78519336710002,\"quoteId\":\"6a46fdfe732524ae48417a7e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.583841,\"min\":0.01767,\"max\":4589.32,\"duration\":13,\"amountOutUsd\":166.77669294190002,\"quoteId\":\"6a46fdfe732524ae48417a8c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.573495,\"min\":0.017599242,\"max\":9007199254740991,\"duration\":17,\"amountOutUsd\":166.76545822050002,\"quoteId\":\"6a46fdfe732524ae48417aac\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.55993975,\"min\":0.05890077,\"max\":1303.65994742,\"duration\":3,\"amountOutUsd\":166.75073857452503,\"quoteId\":\"6a46fdfe732524ae48417ab8\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.54928706,\"min\":0.05890026,\"max\":354.36360574,\"duration\":8,\"amountOutUsd\":166.73917081845403,\"quoteId\":\"6a46fdff732524ae48417ac0\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.54886767,\"min\":0.001420916655378517,\"max\":1870546.4980335117,\"duration\":11,\"amountOutUsd\":166.73871540285302,\"quoteId\":\"6a46fdfe732524ae48417aab\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.53525896,\"min\":0.005355586871060738,\"max\":1164.8401541627118,\"duration\":11,\"amountOutUsd\":166.723937704664,\"quoteId\":\"6a46fdfe732524ae48417abc\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.532137,\"min\":0.017752334247759422,\"max\":4618.437520584942,\"duration\":9,\"amountOutUsd\":166.72054756830002,\"quoteId\":\"6a46fdfe732524ae48417a92\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.436,\"min\":0.01767,\"max\":4589.32,\"duration\":10,\"amountOutUsd\":166.61615240000003,\"quoteId\":\"6a46fdfe732524ae48417a7f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.436,\"min\":0.017720293430614088,\"max\":9007199254740991,\"duration\":15,\"amountOutUsd\":166.61615240000003,\"quoteId\":\"6a46fdfe732524ae48417aa1\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.417484,\"min\":0.017599242,\"max\":9007199254740991,\"duration\":20,\"amountOutUsd\":166.59604587560003,\"quoteId\":\"6a46fdfe732524ae48417a9e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.37794683,\"min\":0.0169864374,\"max\":24023.562540766183,\"duration\":4,\"amountOutUsd\":166.55311246269702,\"quoteId\":\"6a46fdff732524ae48417abf\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.373384,\"min\":0.017770734972053635,\"max\":4623.224642903634,\"duration\":13,\"amountOutUsd\":166.5481576856,\"quoteId\":\"6a46fdfe732524ae48417a71\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.36611682,\"min\":0.017599242,\"max\":1872772.5356961796,\"duration\":15,\"amountOutUsd\":166.540266254838,\"quoteId\":\"6a46fdfe732524ae48417aa2\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.354408,\"min\":0.05890077,\"max\":54.00050986,\"duration\":16,\"amountOutUsd\":166.52755164720003,\"quoteId\":\"6a46fdfe732524ae48417a94\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.35354,\"min\":0.017873746415899013,\"max\":4639.500874072719,\"duration\":13,\"amountOutUsd\":166.52660908600004,\"quoteId\":\"6a46fdfe732524ae48417a96\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.331,\"min\":0.05890077,\"max\":1303.65994742,\"duration\":7,\"amountOutUsd\":166.5021329,\"quoteId\":\"6a46fdfe732524ae48417a76\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.313907,\"min\":0.059435610211757604,\"max\":113.69228669675921,\"duration\":22,\"amountOutUsd\":166.48357161130002,\"quoteId\":\"6a46fdfe732524ae48417a91\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.301055,\"min\":0.05941493665066147,\"max\":1035.3767671938476,\"duration\":6,\"amountOutUsd\":166.4696156245,\"quoteId\":\"6a46fdfd732524ae48417a65\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.299208,\"min\":0.05941893806174288,\"max\":1035.446496596978,\"duration\":9,\"amountOutUsd\":166.46760996720002,\"quoteId\":\"6a46fdfe732524ae48417aa6\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.297,\"min\":0.017609024273836264,\"max\":9007199254740991,\"duration\":17,\"amountOutUsd\":166.46521230000002,\"quoteId\":\"6a46fdfe732524ae48417a8f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.28275948,\"min\":0.01550185,\"max\":24038.46153846154,\"duration\":10,\"amountOutUsd\":166.44974851933202,\"quoteId\":\"6a46fdfe732524ae48417ab9\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.264242,\"min\":0.05890077,\"max\":1303.65994742,\"duration\":5,\"amountOutUsd\":166.42964038780002,\"quoteId\":\"6a46fdfd732524ae48417a63\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.250687,\"min\":0.05890077,\"max\":1303.65994742,\"duration\":12,\"amountOutUsd\":166.41492101330002,\"quoteId\":\"6a46fdfe732524ae48417a81\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.24754199999998,\"min\":0.059270734129014936,\"max\":164.1814477334242,\"duration\":11,\"amountOutUsd\":166.4115058578,\"quoteId\":\"6a46fdfe732524ae48417a93\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.24546457,\"min\":0.05890026,\"max\":153.56519023,\"duration\":5,\"amountOutUsd\":166.40924997656302,\"quoteId\":\"6a46fdfe732524ae48417a88\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.24375585,\"min\":0.0169965456,\"max\":1874265.9444528047,\"duration\":5,\"amountOutUsd\":166.40739447751503,\"quoteId\":\"6a46fdfe732524ae48417ab7\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.225,\"min\":0.017830275952164285,\"max\":9007199254740991,\"duration\":16,\"amountOutUsd\":166.38702750000002,\"quoteId\":\"6a46fdfe732524ae48417a9a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.202988,\"min\":0.05890026,\"max\":354.36360574,\"duration\":17,\"amountOutUsd\":166.3631246692,\"quoteId\":\"6a46fdfe732524ae48417ab3\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.194787,\"min\":0.05890077,\"max\":54.00050986,\"duration\":9,\"amountOutUsd\":166.3542192033,\"quoteId\":\"6a46fdfe732524ae48417a73\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.169215,\"min\":0.05957149951615576,\"max\":240.03486918187582,\"duration\":13,\"amountOutUsd\":166.32645056850004,\"quoteId\":\"6a46fdfe732524ae48417a82\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.15589599999998,\"min\":0.059307530822568684,\"max\":164.2833755146173,\"duration\":9,\"amountOutUsd\":166.31198746639998,\"quoteId\":\"6a46fdfd732524ae48417a66\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.149,\"min\":0.01775342363393303,\"max\":9007199254740991,\"duration\":7,\"amountOutUsd\":166.30449910000002,\"quoteId\":\"6a46fdfe732524ae48417ab4\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.147,\"min\":0.017764434,\"max\":9007199254740991,\"duration\":14,\"amountOutUsd\":166.3023273,\"quoteId\":\"6a46fdfe732524ae48417a7b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.11566,\"min\":0.059520581560683905,\"max\":113.8618308025671,\"duration\":22,\"amountOutUsd\":166.26829519400002,\"quoteId\":\"6a46fdfe732524ae48417aad\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.114,\"min\":0.05890077,\"max\":54.00050986,\"duration\":12,\"amountOutUsd\":166.26649260000002,\"quoteId\":\"6a46fdfe732524ae48417a85\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.08716099999998,\"min\":0.05933216968009246,\"max\":164.3516257794098,\"duration\":16,\"amountOutUsd\":166.2373481299,\"quoteId\":\"6a46fdfe732524ae48417aaa\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.078677,\"min\":0.059503365462915174,\"max\":1036.2633482341967,\"duration\":13,\"amountOutUsd\":166.2281353543,\"quoteId\":\"6a46fdfe732524ae48417a95\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.069232,\"min\":0.0169864374,\"max\":9007199254740991,\"duration\":13,\"amountOutUsd\":166.2178790288,\"quoteId\":\"6a46fdfe732524ae48417ab2\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.065801,\"min\":0.017907163517887112,\"max\":4648.1749746376045,\"duration\":6,\"amountOutUsd\":166.2141533059,\"quoteId\":\"6a46fdfe732524ae48417aa9\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.060063,\"min\":0.017599242,\"max\":9007199254740991,\"duration\":24,\"amountOutUsd\":166.20792241170003,\"quoteId\":\"6a46fdfe732524ae48417a9b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":153.043,\"min\":0.01781894811827839,\"max\":9007199254740991,\"duration\":15,\"amountOutUsd\":166.1893937,\"quoteId\":\"6a46fdfe732524ae48417a84\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":152.99416200000002,\"min\":0.05964239238178833,\"max\":240.32052188268497,\"duration\":17,\"amountOutUsd\":166.13636051580002,\"quoteId\":\"6a46fdfe732524ae48417a79\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":152.986424,\"min\":0.01791826923076923,\"max\":4651.057692307692,\"duration\":12,\"amountOutUsd\":166.1279578216,\"quoteId\":\"6a46fdfd732524ae48417a62\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":152.974326,\"min\":0.01550185,\"max\":9007199254740991,\"duration\":19,\"amountOutUsd\":166.1148206034,\"quoteId\":\"6a46fdfe732524ae48417a7d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":152.965,\"min\":0.05890026,\"max\":354.36360574,\"duration\":12,\"amountOutUsd\":166.10469350000002,\"quoteId\":\"6a46fdfe732524ae48417ab6\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":152.939777,\"min\":0.05890026,\"max\":153.56519023,\"duration\":14,\"amountOutUsd\":166.07730384430002,\"quoteId\":\"6a46fdfe732524ae48417a7c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":152.938074,\"min\":0.0169965456,\"max\":9007199254740991,\"duration\":14,\"amountOutUsd\":166.07545455660002,\"quoteId\":\"6a46fdfe732524ae48417aaf\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":152.935,\"min\":0.017865730166274976,\"max\":9007199254740991,\"duration\":9,\"amountOutUsd\":166.07211650000002,\"quoteId\":\"6a46fdfe732524ae48417ab1\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":152.935,\"min\":0.05890026,\"max\":153.56519023,\"duration\":9,\"amountOutUsd\":166.07211650000002,\"quoteId\":\"6a46fdfe732524ae48417a7a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":152.872231,\"min\":0.059690367555308474,\"max\":240.3337389391876,\"duration\":6,\"amountOutUsd\":166.00395564290002,\"quoteId\":\"6a46fdfe732524ae48417aa3\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":152.786914,\"min\":0.05961461410581528,\"max\":1038.8563871287172,\"duration\":5,\"amountOutUsd\":165.91130991260002,\"quoteId\":\"6a46fdfe732524ae48417aae\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.1,\"amountOut\":152.71112499999998,\"min\":0.05965158605769231,\"max\":1039.5006678798077,\"duration\":11,\"amountOutUsd\":165.8290106375,\"quoteId\":\"6a46fdfd732524ae48417a61\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null}]}" +} \ No newline at end of file diff --git a/test/houdiniFixtures/80f1e3e8efcf07c1c616ead1.json b/test/houdiniFixtures/80f1e3e8efcf07c1c616ead1.json new file mode 100644 index 00000000..edbc6301 --- /dev/null +++ b/test/houdiniFixtures/80f1e3e8efcf07c1c616ead1.json @@ -0,0 +1,5 @@ +{ + "status": 200, + "ok": true, + "body": "{\"total\":235,\"quotes\":[{\"swap\":\"su\",\"quoteId\":\"6a2b55e7a5f07ecb32c9989b\",\"duration\":1,\"amountOut\":503.318082,\"amountOutUsd\":503.256173875914,\"feeUsd\":0,\"netAmountOut\":503.318082,\"swapName\":\"SushiSwap\",\"type\":\"dex\",\"supportsSignatures\":false,\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/sushiswap-y6ba3a.png\",\"markupSupported\":false,\"apiMarkupValue\":0,\"markupType\":\"percentage\",\"restrictedCountries\":[],\"rewardsAvailable\":false,\"min\":0,\"max\":9007199254740991,\"requiresApproval\":false},{\"swap\":\"ps\",\"quoteId\":\"6a2b55e7a5f07ecb32c99953\",\"duration\":1,\"gas\":2.26e-13,\"gasUsd\":3.7859746e-10,\"amountOut\":501.854418,\"amountOutUsd\":501.792689906586,\"feeUsd\":0,\"netAmountOut\":501.854418,\"swapName\":\"PancakeSwap\",\"type\":\"dex\",\"supportsSignatures\":true,\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/pancakeswap-2qgusa.png\",\"markupSupported\":false,\"apiMarkupValue\":0,\"markupType\":\"percentage\",\"restrictedCountries\":[],\"rewardsAvailable\":false,\"min\":0,\"max\":9007199254740991,\"requiresApproval\":false},{\"swap\":\"pt\",\"quoteId\":\"6a2b55e7a5f07ecb32c99914\",\"duration\":0.87,\"amountOut\":501.650013,\"amountOutUsd\":501.53463349701,\"netAmountOut\":501.650013,\"swapName\":\"PactSwap\",\"type\":\"dex\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/pactswap-co22dt.png\",\"markupSupported\":false,\"apiMarkupValue\":0,\"markupType\":\"bp\",\"restrictedCountries\":[],\"rewardsAvailable\":false,\"min\":20,\"max\":10000,\"requiresApproval\":false},{\"swap\":\"zx\",\"quoteId\":\"6a2b55e8a5f07ecb32c999df\",\"duration\":1,\"amountOut\":501.510328,\"amountOutUsd\":501.448642229656,\"netAmountOut\":501.510328,\"swapName\":\"0x Protocol\",\"type\":\"dex\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/0x protocol-ct9vjw.png\",\"markupSupported\":true,\"apiMarkupValue\":10,\"markupType\":\"bp\",\"restrictedCountries\":[],\"rewardsAvailable\":false,\"min\":0,\"max\":9007199254740991,\"requiresApproval\":false},{\"swap\":\"mn\",\"quoteId\":\"6a2b55e7a5f07ecb32c99933\",\"duration\":5,\"amountOut\":501.482671,\"netAmountOut\":501.482671,\"swapName\":\"Mayan\",\"type\":\"dex\",\"supportsSignatures\":false,\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/mayan.jpg\",\"markupSupported\":true,\"apiMarkupValue\":10,\"markupType\":\"bp\",\"restrictedCountries\":[],\"rewardsAvailable\":false,\"min\":0,\"max\":9007199254740991,\"requiresApproval\":false},{\"swap\":\"dl\",\"quoteId\":\"6a2b55e8a5f07ecb32c9998b\",\"duration\":1,\"gas\":0.00025,\"gasUsd\":0.4188025,\"amountOut\":501.18082,\"amountOutUsd\":501.11917475914,\"netAmountOut\":501.18082,\"swapName\":\"Debridge\",\"type\":\"dex\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/debridge.jpg\",\"markupSupported\":true,\"apiMarkupValue\":0.1,\"markupType\":\"percentage\",\"restrictedCountries\":[],\"rewardsAvailable\":false,\"min\":0,\"max\":9007199254740991,\"requiresApproval\":false},{\"amountIn\":0.3,\"amountOut\":497.692,\"min\":0.0181619661,\"max\":9007199254740991,\"duration\":4,\"amountOutUsd\":497.692,\"swap\":\"ch\",\"swapName\":\"ChangeHero\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/changehero-o8i3zz.png\",\"quoteId\":\"6a2b55e7a5f07ecb32c998ae\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":497.62642,\"min\":0.01798,\"max\":5387.23,\"duration\":2,\"amountOutUsd\":497.62642,\"swap\":\"eb\",\"swapName\":\"EasyBit\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/easybit.jpg\",\"quoteId\":\"6a2b55e7a5f07ecb32c998a1\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":497.60758,\"min\":0.0003207,\"max\":9007199254740991,\"duration\":2,\"amountOutUsd\":497.60758,\"swap\":\"cn\",\"swapName\":\"ChangeNow\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/changenow.png\",\"quoteId\":\"6a2b55e7a5f07ecb32c998c0\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":496.63554800000003,\"min\":0.05973002,\"max\":475.8661,\"duration\":15,\"amountOutUsd\":496.63554800000003,\"swap\":\"cl\",\"swapName\":\"Changelly\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/changelly.jpg\",\"quoteId\":\"6a2b55e7a5f07ecb32c998aa\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":494.811726,\"min\":0.07,\"max\":14000,\"duration\":15,\"amountOutUsd\":494.811726,\"swap\":\"le\",\"swapName\":\"LetsExchange\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/letsexchange.jpg\",\"quoteId\":\"6a2b55e7a5f07ecb32c998b8\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":493.633996,\"min\":0.01794902476965418,\"max\":9007199254740991,\"duration\":25,\"amountOutUsd\":493.633996,\"quoteId\":\"6a2b55e7a5f07ecb32c99973\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":493.622,\"min\":0.017972466181809466,\"max\":9007199254740991,\"duration\":21,\"amountOutUsd\":493.622,\"quoteId\":\"6a2b55e9a5f07ecb32c999ef\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":493.586224,\"min\":0.017972466181809466,\"max\":9007199254740991,\"duration\":17,\"amountOutUsd\":493.586224,\"quoteId\":\"6a2b55e8a5f07ecb32c99994\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":493.527271,\"min\":0.0181988124,\"max\":9007199254740991,\"duration\":5,\"amountOutUsd\":493.527271,\"quoteId\":\"6a2b55e7a5f07ecb32c99918\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":493.519079,\"min\":0.01794,\"max\":5375.51,\"duration\":4,\"amountOutUsd\":493.519079,\"quoteId\":\"6a2b55e7a5f07ecb32c99913\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":493.483937,\"min\":0.0181988124,\"max\":9007199254740991,\"duration\":10,\"amountOutUsd\":493.483937,\"quoteId\":\"6a2b55e7a5f07ecb32c99915\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":493.46575,\"min\":0.01794902476965418,\"max\":9007199254740991,\"duration\":25,\"amountOutUsd\":493.46575,\"quoteId\":\"6a2b55e8a5f07ecb32c999cb\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":493.36645,\"min\":0.0181988124,\"max\":9007199254740991,\"duration\":5,\"amountOutUsd\":493.36645,\"quoteId\":\"6a2b55e7a5f07ecb32c99975\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":493.19764,\"min\":0.0001474,\"max\":9007199254740991,\"duration\":4,\"amountOutUsd\":493.19764,\"quoteId\":\"6a2b55e7a5f07ecb32c99974\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":493.116441,\"min\":0.0180309222,\"max\":9007199254740991,\"duration\":15,\"amountOutUsd\":493.116441,\"quoteId\":\"6a2b55e7a5f07ecb32c9992c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":493.09,\"min\":0.0002647,\"max\":9007199254740991,\"duration\":7,\"amountOutUsd\":493.09,\"quoteId\":\"6a2b55e7a5f07ecb32c9991e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":493.062,\"min\":0.059684388951226,\"max\":577.10934625825,\"duration\":12,\"amountOutUsd\":493.062,\"quoteId\":\"6a2b55e7a5f07ecb32c99947\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":493.040943,\"min\":0.0181988124,\"max\":9007199254740991,\"duration\":18,\"amountOutUsd\":493.040943,\"quoteId\":\"6a2b55e8a5f07ecb32c999c6\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":493.00822,\"min\":0.01794902476965418,\"max\":9007199254740991,\"duration\":21,\"amountOutUsd\":493.00822,\"quoteId\":\"6a2b55e8a5f07ecb32c999b6\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.979754,\"min\":0.29843085058761,\"max\":539.02992548976,\"duration\":6,\"amountOutUsd\":492.979754,\"quoteId\":\"6a2b55e7a5f07ecb32c99978\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.952969,\"min\":0.059684388951226,\"max\":577.10934625825,\"duration\":7,\"amountOutUsd\":492.952969,\"quoteId\":\"6a2b55e7a5f07ecb32c99932\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.929,\"min\":0.01567259,\"max\":9007199254740991,\"duration\":15,\"amountOutUsd\":492.929,\"quoteId\":\"6a2b55e7a5f07ecb32c998d8\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.873,\"min\":0.0001474,\"max\":9007199254740991,\"duration\":6,\"amountOutUsd\":492.873,\"quoteId\":\"6a2b55e7a5f07ecb32c998c6\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.832497,\"min\":0.01568933,\"max\":9007199254740991,\"duration\":8,\"amountOutUsd\":492.832497,\"quoteId\":\"6a2b55e7a5f07ecb32c99939\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.820227,\"min\":0.01567259,\"max\":9007199254740991,\"duration\":11,\"amountOutUsd\":492.820227,\"quoteId\":\"6a2b55e7a5f07ecb32c99912\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.819,\"min\":0.01794902476965418,\"max\":9007199254740991,\"duration\":24,\"amountOutUsd\":492.819,\"quoteId\":\"6a2b55e8a5f07ecb32c999c5\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.819,\"min\":0.29843085058761,\"max\":539.02992548976,\"duration\":10,\"amountOutUsd\":492.819,\"quoteId\":\"6a2b55e7a5f07ecb32c9991a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.81037,\"min\":0.0181619661,\"max\":9007199254740991,\"duration\":7,\"amountOutUsd\":492.81037,\"quoteId\":\"6a2b55e7a5f07ecb32c99979\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.774671,\"min\":0.01794902476965418,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":492.774671,\"quoteId\":\"6a2b55e8a5f07ecb32c999e4\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.772,\"min\":0.01794902476965418,\"max\":9007199254740991,\"duration\":27,\"amountOutUsd\":492.772,\"quoteId\":\"6a2b55e7a5f07ecb32c99962\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.741897,\"min\":0.01794,\"max\":5375.51,\"duration\":30,\"amountOutUsd\":492.741897,\"quoteId\":\"6a2b55e8a5f07ecb32c999a3\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.734234,\"min\":0.017972466181809466,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":492.734234,\"quoteId\":\"6a2b55e8a5f07ecb32c999e9\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.71,\"min\":0.01792,\"max\":76.4414468189373,\"duration\":13,\"amountOutUsd\":492.71,\"quoteId\":\"6a2b55e8a5f07ecb32c9999f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.702,\"min\":0.0001491,\"max\":9007199254740991,\"duration\":17,\"amountOutUsd\":492.702,\"quoteId\":\"6a2b55e7a5f07ecb32c9995f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.681552,\"min\":0.01792,\"max\":76.4414468189373,\"duration\":11,\"amountOutUsd\":492.681552,\"quoteId\":\"6a2b55e8a5f07ecb32c999bb\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.662733,\"min\":0.0183134574,\"max\":72.2585277,\"duration\":4,\"amountOutUsd\":492.662733,\"quoteId\":\"6a2b55e7a5f07ecb32c99963\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.662,\"min\":0.01568933,\"max\":9007199254740991,\"duration\":11,\"amountOutUsd\":492.662,\"quoteId\":\"6a2b55e7a5f07ecb32c998da\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.657,\"min\":0.01794,\"max\":5375.51,\"duration\":6,\"amountOutUsd\":492.657,\"quoteId\":\"6a2b55e7a5f07ecb32c998dd\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.646,\"min\":0.0182534304,\"max\":64.0510039,\"duration\":8,\"amountOutUsd\":492.646,\"quoteId\":\"6a2b55e7a5f07ecb32c99955\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.64156,\"min\":0.11965,\"max\":1794.7945,\"duration\":3,\"amountOutUsd\":492.64156,\"quoteId\":\"6a2b55e7a5f07ecb32c99971\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.615318,\"min\":0.01794902476965418,\"max\":9007199254740991,\"duration\":28,\"amountOutUsd\":492.615318,\"quoteId\":\"6a2b55e7a5f07ecb32c99956\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.582825,\"min\":0.0001474,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":492.582825,\"quoteId\":\"6a2b55e8a5f07ecb32c999b9\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.574078,\"min\":0.11945,\"max\":1791.72878,\"duration\":5,\"amountOutUsd\":492.574078,\"quoteId\":\"6a2b55e8a5f07ecb32c9997d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.539762,\"min\":0.29843085058761,\"max\":539.02992548976,\"duration\":16,\"amountOutUsd\":492.539762,\"quoteId\":\"6a2b55e8a5f07ecb32c999d4\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.537096,\"min\":0.0182534304,\"max\":64.0510039,\"duration\":4,\"amountOutUsd\":492.537096,\"quoteId\":\"6a2b55e8a5f07ecb32c99980\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.536244,\"min\":0.0002647,\"max\":9007199254740991,\"duration\":11,\"amountOutUsd\":492.536244,\"quoteId\":\"6a2b55e8a5f07ecb32c9999d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.509681,\"min\":0.059684388951226,\"max\":577.10934625825,\"duration\":16,\"amountOutUsd\":492.509681,\"quoteId\":\"6a2b55e8a5f07ecb32c999a0\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.508869,\"min\":0.0181988124,\"max\":9007199254740991,\"duration\":9,\"amountOutUsd\":492.508869,\"quoteId\":\"6a2b55e7a5f07ecb32c998eb\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.50062,\"min\":0.01794,\"max\":5375.51,\"duration\":7,\"amountOutUsd\":492.50062,\"quoteId\":\"6a2b55e7a5f07ecb32c998ea\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.49287400000003,\"min\":0.017972466181809466,\"max\":9007199254740991,\"duration\":21,\"amountOutUsd\":492.49287400000003,\"quoteId\":\"6a2b55e8a5f07ecb32c9999a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.49261,\"min\":0.0183134574,\"max\":72.2585277,\"duration\":4,\"amountOutUsd\":492.49261,\"quoteId\":\"6a2b55e8a5f07ecb32c999a4\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.474,\"min\":0.0002572,\"max\":9007199254740991,\"duration\":6,\"amountOutUsd\":492.474,\"quoteId\":\"6a2b55e7a5f07ecb32c99930\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.42661599999997,\"min\":0.0181988124,\"max\":9007199254740991,\"duration\":14,\"amountOutUsd\":492.42661599999997,\"quoteId\":\"6a2b55e7a5f07ecb32c998e0\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.418924,\"min\":0.0181619661,\"max\":9007199254740991,\"duration\":13,\"amountOutUsd\":492.418924,\"quoteId\":\"6a2b55e7a5f07ecb32c998e1\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.418,\"min\":0.01797,\"max\":5385.34,\"duration\":8,\"amountOutUsd\":492.418,\"quoteId\":\"6a2b55e7a5f07ecb32c998f1\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.41317,\"min\":0.11945,\"max\":1791.72878,\"duration\":5,\"amountOutUsd\":492.41317,\"quoteId\":\"6a2b55e8a5f07ecb32c999a6\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.411469,\"min\":0.05973002,\"max\":225.91343964,\"duration\":4,\"amountOutUsd\":492.411469,\"quoteId\":\"6a2b55e7a5f07ecb32c99929\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.404,\"min\":0.05973002,\"max\":475.8661,\"duration\":8,\"amountOutUsd\":492.404,\"quoteId\":\"6a2b55e7a5f07ecb32c998db\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.398,\"min\":0.1192,\"max\":1788.04453,\"duration\":7,\"amountOutUsd\":492.398,\"quoteId\":\"6a2b55e7a5f07ecb32c998c8\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.392346,\"min\":0.01568933,\"max\":9007199254740991,\"duration\":18,\"amountOutUsd\":492.392346,\"quoteId\":\"6a2b55e8a5f07ecb32c999d3\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.39,\"min\":0.03487666,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":492.39,\"swap\":\"sp\",\"swapName\":\"Swapter\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/swapter.jpg\",\"quoteId\":\"6a2b55e7a5f07ecb32c998a3\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.376865,\"min\":0.01567259,\"max\":9007199254740991,\"duration\":19,\"amountOutUsd\":492.376865,\"quoteId\":\"6a2b55e8a5f07ecb32c9998c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.374135,\"min\":0.01568933,\"max\":9007199254740991,\"duration\":45,\"amountOutUsd\":492.374135,\"quoteId\":\"6a2b55e8a5f07ecb32c999ac\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.357197,\"min\":0.0180309222,\"max\":9007199254740991,\"duration\":22,\"amountOutUsd\":492.357197,\"quoteId\":\"6a2b55e7a5f07ecb32c998ec\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.341626,\"min\":0.0001474,\"max\":9007199254740991,\"duration\":7,\"amountOutUsd\":492.341626,\"quoteId\":\"6a2b55e7a5f07ecb32c998c5\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.32539,\"min\":0.01792,\"max\":5372.24,\"duration\":4,\"amountOutUsd\":492.32539,\"quoteId\":\"6a2b55e8a5f07ecb32c999ad\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.295913,\"min\":0.05973002,\"max\":475.8661,\"duration\":3,\"amountOutUsd\":492.295913,\"quoteId\":\"6a2b55e7a5f07ecb32c99916\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.289259,\"min\":0.1192,\"max\":1788.04453,\"duration\":3,\"amountOutUsd\":492.289259,\"quoteId\":\"6a2b55e7a5f07ecb32c998e4\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.28011499999997,\"min\":0.29843085058761,\"max\":539.02992548976,\"duration\":9,\"amountOutUsd\":492.28011499999997,\"quoteId\":\"6a2b55e7a5f07ecb32c99901\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.27115,\"min\":0.01794902476965418,\"max\":9007199254740991,\"duration\":24,\"amountOutUsd\":492.27115,\"quoteId\":\"6a2b55e8a5f07ecb32c999d0\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.24436,\"min\":0.05973002,\"max\":225.91343964,\"duration\":4,\"amountOutUsd\":492.24436,\"quoteId\":\"6a2b55e8a5f07ecb32c9997e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.229,\"min\":0.11965,\"max\":1794.7945,\"duration\":7,\"amountOutUsd\":492.229,\"quoteId\":\"6a2b55e7a5f07ecb32c99957\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.194,\"min\":0.0183134574,\"max\":72.2585277,\"duration\":6,\"amountOutUsd\":492.194,\"quoteId\":\"6a2b55e7a5f07ecb32c99961\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.17209,\"min\":0.0181988124,\"max\":9007199254740991,\"duration\":5,\"amountOutUsd\":492.17209,\"quoteId\":\"6a2b55e8a5f07ecb32c999b7\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.16492,\"min\":0.01792,\"max\":5372.24,\"duration\":4,\"amountOutUsd\":492.16492,\"quoteId\":\"6a2b55e8a5f07ecb32c999d8\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.137558,\"min\":0.059684388951226,\"max\":577.10934625825,\"duration\":45,\"amountOutUsd\":492.137558,\"quoteId\":\"6a2b55e8a5f07ecb32c999da\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.133036,\"min\":0.01568933,\"max\":9007199254740991,\"duration\":11,\"amountOutUsd\":492.133036,\"quoteId\":\"6a2b55e7a5f07ecb32c998e3\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.091312,\"min\":0.0182534304,\"max\":64.0510039,\"duration\":12,\"amountOutUsd\":492.091312,\"quoteId\":\"6a2b55e8a5f07ecb32c999e6\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.033,\"min\":0.05973002,\"max\":284.81522292,\"duration\":16,\"amountOutUsd\":492.033,\"quoteId\":\"6a2b55e7a5f07ecb32c998f0\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.004131,\"min\":0.05973002,\"max\":284.81522292,\"duration\":14,\"amountOutUsd\":492.004131,\"quoteId\":\"6a2b55e7a5f07ecb32c99931\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492.003688,\"min\":0.0001474,\"max\":9007199254740991,\"duration\":4,\"amountOutUsd\":492.003688,\"quoteId\":\"6a2b55e8a5f07ecb32c999bc\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":492,\"min\":0.059787336444268,\"max\":555.25106138478,\"duration\":30,\"amountOutUsd\":492,\"swap\":\"cc\",\"swapName\":\"Coincraddle\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/coincraddle.jpg\",\"quoteId\":\"6a2b55e7a5f07ecb32c998a9\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.978329,\"min\":0.0181619661,\"max\":9007199254740991,\"duration\":23,\"amountOutUsd\":491.978329,\"quoteId\":\"6a2b55e8a5f07ecb32c999d2\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.97397,\"min\":0.0180309222,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":491.97397,\"quoteId\":\"6a2b55e8a5f07ecb32c999ca\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.960859,\"min\":0.0181619661,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":491.960859,\"quoteId\":\"6a2b55e8a5f07ecb32c999b8\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.951535,\"min\":0.0002572,\"max\":9007199254740991,\"duration\":5,\"amountOutUsd\":491.951535,\"quoteId\":\"6a2b55e7a5f07ecb32c99923\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.922342,\"min\":0.01792,\"max\":76.4414468189373,\"duration\":18,\"amountOutUsd\":491.922342,\"quoteId\":\"6a2b55e8a5f07ecb32c999ab\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.913902,\"min\":0.0001491,\"max\":9007199254740991,\"duration\":23,\"amountOutUsd\":491.913902,\"quoteId\":\"6a2b55e7a5f07ecb32c99950\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.90674,\"min\":0.05973002,\"max\":28.18870285,\"duration\":5,\"amountOutUsd\":491.90674,\"quoteId\":\"6a2b55e8a5f07ecb32c9997f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.896497,\"min\":0.059684388951226,\"max\":577.10934625825,\"duration\":12,\"amountOutUsd\":491.896497,\"quoteId\":\"6a2b55e7a5f07ecb32c99905\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.89649000000003,\"min\":0.0002647,\"max\":9007199254740991,\"duration\":7,\"amountOutUsd\":491.89649000000003,\"quoteId\":\"6a2b55e7a5f07ecb32c99902\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.852244,\"min\":0.05973002,\"max\":475.8661,\"duration\":12,\"amountOutUsd\":491.852244,\"quoteId\":\"6a2b55e8a5f07ecb32c999d5\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.845603,\"min\":0.1192,\"max\":1788.04453,\"duration\":12,\"amountOutUsd\":491.845603,\"quoteId\":\"6a2b55e8a5f07ecb32c999c8\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.794579,\"min\":0.05972984,\"max\":97.84459035,\"duration\":3,\"amountOutUsd\":491.794579,\"quoteId\":\"6a2b55e7a5f07ecb32c998cf\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.77905100000004,\"min\":0.01797,\"max\":5385.34,\"duration\":9,\"amountOutUsd\":491.77905100000004,\"quoteId\":\"6a2b55e7a5f07ecb32c998e7\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.763967,\"min\":0.01567259,\"max\":9007199254740991,\"duration\":15,\"amountOutUsd\":491.763967,\"quoteId\":\"6a2b55e7a5f07ecb32c998e5\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.754025,\"min\":0.0181619661,\"max\":9007199254740991,\"duration\":12,\"amountOutUsd\":491.754025,\"quoteId\":\"6a2b55e7a5f07ecb32c998e6\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.733623,\"min\":0.11945,\"max\":1791.72878,\"duration\":30,\"amountOutUsd\":491.733623,\"quoteId\":\"6a2b55e8a5f07ecb32c999d7\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.71996299999995,\"min\":0.0181619661,\"max\":9007199254740991,\"duration\":16,\"amountOutUsd\":491.71996299999995,\"quoteId\":\"6a2b55e7a5f07ecb32c998ee\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.713,\"min\":0.11945,\"max\":1791.72878,\"duration\":7,\"amountOutUsd\":491.713,\"quoteId\":\"6a2b55e7a5f07ecb32c9993b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.645871,\"min\":0.0183134574,\"max\":72.2585277,\"duration\":7,\"amountOutUsd\":491.645871,\"quoteId\":\"6a2b55e7a5f07ecb32c99964\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.628,\"min\":0.05972984,\"max\":97.84459035,\"duration\":6,\"amountOutUsd\":491.628,\"quoteId\":\"6a2b55e7a5f07ecb32c998c7\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.58822299999997,\"min\":0.11965,\"max\":1794.7945,\"duration\":8,\"amountOutUsd\":491.58822299999997,\"quoteId\":\"6a2b55e7a5f07ecb32c998bf\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.55739,\"min\":0.11945,\"max\":1791.72878,\"duration\":9,\"amountOutUsd\":491.55739,\"quoteId\":\"6a2b55e7a5f07ecb32c9994c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.551,\"min\":0.05973002,\"max\":225.91343964,\"duration\":7,\"amountOutUsd\":491.551,\"quoteId\":\"6a2b55e7a5f07ecb32c998dc\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.492,\"min\":0.05973002,\"max\":28.18870285,\"duration\":9,\"amountOutUsd\":491.492,\"quoteId\":\"6a2b55e7a5f07ecb32c998f3\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.432635,\"min\":0.0182534304,\"max\":64.0510039,\"duration\":8,\"amountOutUsd\":491.432635,\"quoteId\":\"6a2b55e7a5f07ecb32c9993a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.35103,\"min\":0.05972984,\"max\":97.84459035,\"duration\":12,\"amountOutUsd\":491.35103,\"quoteId\":\"6a2b55e8a5f07ecb32c9998f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.336964,\"min\":0.05972984,\"max\":97.84459035,\"duration\":30,\"amountOutUsd\":491.336964,\"quoteId\":\"6a2b55e8a5f07ecb32c999a1\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.300362,\"min\":0.0183134574,\"max\":72.2585277,\"duration\":3,\"amountOutUsd\":491.300362,\"quoteId\":\"6a2b55e8a5f07ecb32c999cd\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.23384699999997,\"min\":0.1192,\"max\":1788.04453,\"duration\":7,\"amountOutUsd\":491.23384699999997,\"quoteId\":\"6a2b55e7a5f07ecb32c998e8\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.221114,\"min\":0.11945,\"max\":1791.72878,\"duration\":5,\"amountOutUsd\":491.221114,\"quoteId\":\"6a2b55e8a5f07ecb32c999cc\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.13196,\"min\":0.01794,\"max\":5375.51,\"duration\":3,\"amountOutUsd\":491.13196,\"quoteId\":\"6a2b55e8a5f07ecb32c999a2\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":491.052712,\"min\":0.05973002,\"max\":225.91343964,\"duration\":4,\"amountOutUsd\":491.052712,\"quoteId\":\"6a2b55e8a5f07ecb32c999b5\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":490.864498,\"min\":0.05973002,\"max\":284.81522292,\"duration\":30,\"amountOutUsd\":490.864498,\"quoteId\":\"6a2b55e8a5f07ecb32c999c1\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":490.806,\"min\":0.07,\"max\":5700,\"duration\":27,\"amountOutUsd\":490.806,\"quoteId\":\"6a2b55e7a5f07ecb32c99943\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":490.750087,\"min\":0.07,\"max\":3200,\"duration\":30,\"amountOutUsd\":490.750087,\"quoteId\":\"6a2b55e7a5f07ecb32c99936\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":490.698342,\"min\":0.07,\"max\":5700,\"duration\":22,\"amountOutUsd\":490.698342,\"quoteId\":\"6a2b55e7a5f07ecb32c99948\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":490.683262,\"min\":0.017972466181809466,\"max\":9007199254740991,\"duration\":22,\"amountOutUsd\":490.683262,\"quoteId\":\"6a2b55e8a5f07ecb32c999aa\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":490.604,\"min\":0.07,\"max\":340,\"duration\":31,\"amountOutUsd\":490.604,\"quoteId\":\"6a2b55e7a5f07ecb32c9991b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":490.581491,\"min\":0.0181988124,\"max\":9007199254740991,\"duration\":15,\"amountOutUsd\":490.581491,\"quoteId\":\"6a2b55e7a5f07ecb32c998f8\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":490.576418,\"min\":0.07,\"max\":340,\"duration\":29,\"amountOutUsd\":490.576418,\"quoteId\":\"6a2b55e7a5f07ecb32c9995d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":490.57612,\"min\":0.07,\"max\":3200,\"duration\":30,\"amountOutUsd\":490.57612,\"quoteId\":\"6a2b55e8a5f07ecb32c99981\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":490.519153,\"min\":0.01794902476965418,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":490.519153,\"quoteId\":\"6a2b55e7a5f07ecb32c99976\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":490.413003,\"min\":0.0181988124,\"max\":9007199254740991,\"duration\":10,\"amountOutUsd\":490.413003,\"quoteId\":\"6a2b55e7a5f07ecb32c998fa\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":490.404854,\"min\":0.01794,\"max\":5375.51,\"duration\":9,\"amountOutUsd\":490.404854,\"quoteId\":\"6a2b55e7a5f07ecb32c998f5\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":490.400077,\"min\":0.0180309222,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":490.400077,\"quoteId\":\"6a2b55e7a5f07ecb32c99909\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":490.372657,\"min\":0.01794902476965418,\"max\":9007199254740991,\"duration\":27,\"amountOutUsd\":490.372657,\"quoteId\":\"6a2b55e8a5f07ecb32c999c3\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":490.344318,\"min\":0.29843085058761,\"max\":539.02992548976,\"duration\":12,\"amountOutUsd\":490.344318,\"quoteId\":\"6a2b55e7a5f07ecb32c9992e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":490.27,\"min\":0.07,\"max\":3200,\"duration\":30,\"amountOutUsd\":490.27,\"quoteId\":\"6a2b55e7a5f07ecb32c99911\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":490.251819,\"min\":0.07,\"max\":5700,\"duration\":31,\"amountOutUsd\":490.251819,\"quoteId\":\"6a2b55e8a5f07ecb32c99991\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":490.246361,\"min\":0.0001474,\"max\":9007199254740991,\"duration\":9,\"amountOutUsd\":490.246361,\"quoteId\":\"6a2b55e7a5f07ecb32c998e9\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":490.197721,\"min\":0.01568933,\"max\":9007199254740991,\"duration\":13,\"amountOutUsd\":490.197721,\"quoteId\":\"6a2b55e7a5f07ecb32c998f4\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":490.080932,\"min\":0.0002647,\"max\":9007199254740991,\"duration\":8,\"amountOutUsd\":490.080932,\"quoteId\":\"6a2b55e7a5f07ecb32c9992a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":490.053197,\"min\":0.059684388951226,\"max\":577.10934625825,\"duration\":13,\"amountOutUsd\":490.053197,\"quoteId\":\"6a2b55e7a5f07ecb32c9992d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":490.030317,\"min\":0.07,\"max\":1800,\"duration\":22,\"amountOutUsd\":490.030317,\"quoteId\":\"6a2b55e7a5f07ecb32c99967\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":490.016816,\"min\":0.0002572,\"max\":9007199254740991,\"duration\":8,\"amountOutUsd\":490.016816,\"quoteId\":\"6a2b55e7a5f07ecb32c9993d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":489.967191,\"min\":0.01792,\"max\":76.4414468189373,\"duration\":30,\"amountOutUsd\":489.967191,\"quoteId\":\"6a2b55e8a5f07ecb32c999bd\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":489.958848,\"min\":0.0001491,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":489.958848,\"quoteId\":\"6a2b55e7a5f07ecb32c99972\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":489.921123,\"min\":0.01567259,\"max\":9007199254740991,\"duration\":16,\"amountOutUsd\":489.921123,\"quoteId\":\"6a2b55e7a5f07ecb32c998f7\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":489.841,\"min\":0.07,\"max\":1800,\"duration\":25,\"amountOutUsd\":489.841,\"quoteId\":\"6a2b55e7a5f07ecb32c9993f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":489.819844,\"min\":0.07,\"max\":340,\"duration\":36,\"amountOutUsd\":489.819844,\"quoteId\":\"6a2b55e7a5f07ecb32c99904\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":489.81,\"min\":0.11968,\"max\":1795.25633,\"duration\":15,\"amountOutUsd\":489.81,\"swap\":\"sz\",\"swapName\":\"Swapuz\",\"logoUrl\":\"https://api.houdiniswap.com/assets/logos/swapuz-d3ssum.png\",\"quoteId\":\"6a2b55e7a5f07ecb32c9989c\",\"type\":\"standard\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":489.786002,\"min\":0.0181619661,\"max\":9007199254740991,\"duration\":19,\"amountOutUsd\":489.786002,\"quoteId\":\"6a2b55e7a5f07ecb32c998fc\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":489.736583,\"min\":0.07,\"max\":3200,\"duration\":30,\"amountOutUsd\":489.736583,\"quoteId\":\"6a2b55e7a5f07ecb32c99907\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":489.643489,\"min\":0.07,\"max\":5700,\"duration\":27,\"amountOutUsd\":489.643489,\"quoteId\":\"6a2b55e7a5f07ecb32c99906\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":489.639418,\"min\":0.0182534304,\"max\":64.0510039,\"duration\":9,\"amountOutUsd\":489.639418,\"quoteId\":\"6a2b55e7a5f07ecb32c9995e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":489.585181,\"min\":0.07,\"max\":1800,\"duration\":32,\"amountOutUsd\":489.585181,\"quoteId\":\"6a2b55e8a5f07ecb32c999e2\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":489.553118,\"min\":0.0183134574,\"max\":72.2585277,\"duration\":9,\"amountOutUsd\":489.553118,\"quoteId\":\"6a2b55e7a5f07ecb32c99952\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":489.46494,\"min\":0.11945,\"max\":1791.72878,\"duration\":10,\"amountOutUsd\":489.46494,\"quoteId\":\"6a2b55e7a5f07ecb32c9994a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":489.438034,\"min\":0.07,\"max\":340,\"duration\":30,\"amountOutUsd\":489.438034,\"quoteId\":\"6a2b55e8a5f07ecb32c999c9\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":489.399449,\"min\":0.05973002,\"max\":475.8661,\"duration\":9,\"amountOutUsd\":489.399449,\"quoteId\":\"6a2b55e7a5f07ecb32c998f6\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":489.392829,\"min\":0.1192,\"max\":1788.04453,\"duration\":8,\"amountOutUsd\":489.392829,\"quoteId\":\"6a2b55e7a5f07ecb32c998df\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":489.388504,\"min\":0.07,\"max\":3200,\"duration\":30,\"amountOutUsd\":489.388504,\"quoteId\":\"6a2b55e8a5f07ecb32c999ba\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":489.33424199999996,\"min\":0.07,\"max\":1800,\"duration\":25,\"amountOutUsd\":489.33424199999996,\"quoteId\":\"6a2b55e7a5f07ecb32c99900\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":489.303207,\"min\":0.05973002,\"max\":225.91343964,\"duration\":9,\"amountOutUsd\":489.303207,\"quoteId\":\"6a2b55e7a5f07ecb32c99908\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":489.29289,\"min\":0.05973002,\"max\":284.81522292,\"duration\":30,\"amountOutUsd\":489.29289,\"quoteId\":\"6a2b55e7a5f07ecb32c99924\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":489.217591,\"min\":0.01792,\"max\":5372.24,\"duration\":9,\"amountOutUsd\":489.217591,\"quoteId\":\"6a2b55e8a5f07ecb32c999c0\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":489.164455,\"min\":0.05972984,\"max\":97.84459035,\"duration\":8,\"amountOutUsd\":489.164455,\"quoteId\":\"6a2b55e7a5f07ecb32c998de\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":488.421064,\"min\":0.0181988124,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":488.421064,\"quoteId\":\"6a2b55e7a5f07ecb32c99968\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":488.412948,\"min\":0.01794,\"max\":5375.51,\"duration\":30,\"amountOutUsd\":488.412948,\"quoteId\":\"6a2b55e7a5f07ecb32c9996b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":488.27,\"min\":0.017972466181809466,\"max\":9007199254740991,\"duration\":45,\"amountOutUsd\":488.27,\"quoteId\":\"6a2b55e8a5f07ecb32c99992\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":488.255098,\"min\":0.0001474,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":488.255098,\"quoteId\":\"6a2b55e7a5f07ecb32c9995a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":488.16,\"min\":0.0181988124,\"max\":9007199254740991,\"duration\":45,\"amountOutUsd\":488.16,\"quoteId\":\"6a2b55e7a5f07ecb32c998d0\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":488,\"min\":0.01794902476965418,\"max\":9007199254740991,\"duration\":26,\"amountOutUsd\":488,\"quoteId\":\"6a2b55e8a5f07ecb32c999d9\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":487.86,\"min\":0.01794902476965418,\"max\":9007199254740991,\"duration\":45,\"amountOutUsd\":487.86,\"quoteId\":\"6a2b55e8a5f07ecb32c999a9\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":487.83,\"min\":0.29843085058761,\"max\":539.02992548976,\"duration\":60,\"amountOutUsd\":487.83,\"quoteId\":\"6a2b55e7a5f07ecb32c99903\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":487.66,\"min\":0.0002647,\"max\":9007199254740991,\"duration\":45,\"amountOutUsd\":487.66,\"quoteId\":\"6a2b55e7a5f07ecb32c998fd\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":487.63,\"min\":0.059684388951226,\"max\":577.10934625825,\"duration\":60,\"amountOutUsd\":487.63,\"quoteId\":\"6a2b55e7a5f07ecb32c998f9\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":487.564667,\"min\":0.0183134574,\"max\":72.2585277,\"duration\":30,\"amountOutUsd\":487.564667,\"quoteId\":\"6a2b55e8a5f07ecb32c99990\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":487.5,\"min\":0.0002572,\"max\":9007199254740991,\"duration\":45,\"amountOutUsd\":487.5,\"quoteId\":\"6a2b55e7a5f07ecb32c99925\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":487.476847,\"min\":0.11945,\"max\":1791.72878,\"duration\":30,\"amountOutUsd\":487.476847,\"quoteId\":\"6a2b55e8a5f07ecb32c9999c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":487.31577,\"min\":0.05973002,\"max\":225.91343964,\"duration\":30,\"amountOutUsd\":487.31577,\"quoteId\":\"6a2b55e7a5f07ecb32c9996f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":487.26,\"min\":0.0181619661,\"max\":9007199254740991,\"duration\":45,\"amountOutUsd\":487.26,\"quoteId\":\"6a2b55e7a5f07ecb32c998d2\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":487.230501,\"min\":0.01792,\"max\":5372.24,\"duration\":30,\"amountOutUsd\":487.230501,\"quoteId\":\"6a2b55e8a5f07ecb32c999de\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":487.22,\"min\":0.0182534304,\"max\":64.0510039,\"duration\":45,\"amountOutUsd\":487.22,\"quoteId\":\"6a2b55e7a5f07ecb32c99935\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":487,\"min\":0.0002572,\"max\":9007199254740991,\"duration\":8,\"amountOutUsd\":487,\"quoteId\":\"6a2b55e7a5f07ecb32c99941\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":487,\"min\":0.01568933,\"max\":9007199254740991,\"duration\":13,\"amountOutUsd\":487,\"quoteId\":\"6a2b55e7a5f07ecb32c9992b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":487,\"min\":0.0181619661,\"max\":9007199254740991,\"duration\":18,\"amountOutUsd\":487,\"quoteId\":\"6a2b55e7a5f07ecb32c9992f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":486.97,\"min\":0.05973002,\"max\":475.8661,\"duration\":45,\"amountOutUsd\":486.97,\"quoteId\":\"6a2b55e7a5f07ecb32c998d1\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":486.97,\"min\":0.1192,\"max\":1788.04453,\"duration\":45,\"amountOutUsd\":486.97,\"quoteId\":\"6a2b55e7a5f07ecb32c998be\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":486.64,\"min\":0.05972984,\"max\":97.84459035,\"duration\":45,\"amountOutUsd\":486.64,\"quoteId\":\"6a2b55e7a5f07ecb32c998c1\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":486,\"min\":0.05972984,\"max\":97.84459035,\"duration\":8,\"amountOutUsd\":486,\"quoteId\":\"6a2b55e7a5f07ecb32c9990a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":485.79,\"min\":0.017972466181809466,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":485.79,\"quoteId\":\"6a2b55e8a5f07ecb32c9997b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":485.76,\"min\":0.01794902476965418,\"max\":9007199254740991,\"duration\":31,\"amountOutUsd\":485.76,\"quoteId\":\"6a2b55e8a5f07ecb32c9997a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":485.74,\"min\":0.0181988124,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":485.74,\"quoteId\":\"6a2b55e7a5f07ecb32c998d4\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":485.7,\"min\":0.0181988124,\"max\":9007199254740991,\"duration\":11,\"amountOutUsd\":485.7,\"quoteId\":\"6a2b55e7a5f07ecb32c998d5\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":485.670035,\"min\":0.07,\"max\":3200,\"duration\":30,\"amountOutUsd\":485.670035,\"quoteId\":\"6a2b55e8a5f07ecb32c9997c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":485.64,\"min\":0.01794,\"max\":5375.51,\"duration\":10,\"amountOutUsd\":485.64,\"quoteId\":\"6a2b55e7a5f07ecb32c9996c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":485.54,\"min\":0.0001474,\"max\":9007199254740991,\"duration\":10,\"amountOutUsd\":485.54,\"quoteId\":\"6a2b55e7a5f07ecb32c99920\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":485.41,\"min\":0.29843085058761,\"max\":539.02992548976,\"duration\":45,\"amountOutUsd\":485.41,\"quoteId\":\"6a2b55e7a5f07ecb32c9990f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":485.39,\"min\":0.01794902476965418,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":485.39,\"quoteId\":\"6a2b55e8a5f07ecb32c999c4\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":485.37,\"min\":0.07,\"max\":5700,\"duration\":45,\"amountOutUsd\":485.37,\"quoteId\":\"6a2b55e7a5f07ecb32c998fb\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":485.26,\"min\":0.01568933,\"max\":9007199254740991,\"duration\":45,\"amountOutUsd\":485.26,\"quoteId\":\"6a2b55e7a5f07ecb32c998cc\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":485.23,\"min\":0.0002647,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":485.23,\"quoteId\":\"6a2b55e7a5f07ecb32c9990c\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":485.21,\"min\":0.059684388951226,\"max\":577.10934625825,\"duration\":45,\"amountOutUsd\":485.21,\"quoteId\":\"6a2b55e7a5f07ecb32c998f2\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":485.08,\"min\":0.0002572,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":485.08,\"quoteId\":\"6a2b55e7a5f07ecb32c99921\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":485.07,\"min\":0.01567259,\"max\":9007199254740991,\"duration\":45,\"amountOutUsd\":485.07,\"quoteId\":\"6a2b55e7a5f07ecb32c998cb\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":485,\"min\":0.07,\"max\":1800,\"duration\":27,\"amountOutUsd\":485,\"quoteId\":\"6a2b55e7a5f07ecb32c99937\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":484.91,\"min\":0.01797,\"max\":5385.34,\"duration\":6,\"amountOutUsd\":484.91,\"quoteId\":\"6a2b55e7a5f07ecb32c998c9\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":484.88,\"min\":0.07,\"max\":1800,\"duration\":45,\"amountOutUsd\":484.88,\"quoteId\":\"6a2b55e7a5f07ecb32c998fe\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":484.88,\"min\":0.0181619661,\"max\":9007199254740991,\"duration\":9,\"amountOutUsd\":484.88,\"quoteId\":\"6a2b55e7a5f07ecb32c998ca\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":484.85,\"min\":0.0181619661,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":484.85,\"quoteId\":\"6a2b55e7a5f07ecb32c998d6\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":484.79,\"min\":0.0183134574,\"max\":72.2585277,\"duration\":10,\"amountOutUsd\":484.79,\"quoteId\":\"6a2b55e7a5f07ecb32c99969\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":484.74,\"min\":0.0182534304,\"max\":64.0510039,\"duration\":30,\"amountOutUsd\":484.74,\"quoteId\":\"6a2b55e7a5f07ecb32c9996e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":484.59,\"min\":0.05973002,\"max\":225.91343964,\"duration\":10,\"amountOutUsd\":484.59,\"quoteId\":\"6a2b55e7a5f07ecb32c998d9\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":484.55,\"min\":0.05973002,\"max\":475.8661,\"duration\":30,\"amountOutUsd\":484.55,\"quoteId\":\"6a2b55e7a5f07ecb32c998cd\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":484.543213,\"min\":0.11945,\"max\":1791.72878,\"duration\":3,\"amountOutUsd\":484.543213,\"quoteId\":\"6a2b55e7a5f07ecb32c998c4\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":484.45,\"min\":0.01792,\"max\":5372.24,\"duration\":10,\"amountOutUsd\":484.45,\"quoteId\":\"6a2b55e8a5f07ecb32c9998e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":484.356,\"min\":0.11945,\"max\":1791.72878,\"duration\":6,\"amountOutUsd\":484.356,\"quoteId\":\"6a2b55e7a5f07ecb32c998ce\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":484.22,\"min\":0.05972984,\"max\":97.84459035,\"duration\":30,\"amountOutUsd\":484.22,\"quoteId\":\"6a2b55e7a5f07ecb32c998b6\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":484.093173,\"min\":0.11945,\"max\":1791.72878,\"duration\":13,\"amountOutUsd\":484.093173,\"quoteId\":\"6a2b55e8a5f07ecb32c9999e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":484.090814,\"min\":0.11945,\"max\":1791.72878,\"duration\":30,\"amountOutUsd\":484.090814,\"quoteId\":\"6a2b55e8a5f07ecb32c9999b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":483.98,\"min\":0.05973002,\"max\":28.18870285,\"duration\":7,\"amountOutUsd\":483.98,\"quoteId\":\"6a2b55e7a5f07ecb32c998d7\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":483.853769,\"min\":0.11945,\"max\":1791.72878,\"duration\":6,\"amountOutUsd\":483.853769,\"quoteId\":\"6a2b55e7a5f07ecb32c998c3\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":482.95,\"min\":0.07,\"max\":5700,\"duration\":30,\"amountOutUsd\":482.95,\"quoteId\":\"6a2b55e7a5f07ecb32c99910\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":482.93,\"min\":0.07,\"max\":3200,\"duration\":30,\"amountOutUsd\":482.93,\"quoteId\":\"6a2b55e7a5f07ecb32c9990e\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":482.46,\"min\":0.07,\"max\":1800,\"duration\":30,\"amountOutUsd\":482.46,\"quoteId\":\"6a2b55e7a5f07ecb32c9990d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":\"\"},{\"amountIn\":0.3,\"amountOut\":481.945583,\"min\":0.11945,\"max\":1791.72878,\"duration\":9,\"amountOutUsd\":481.945583,\"quoteId\":\"6a2b55e7a5f07ecb32c998e2\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":479.39,\"min\":0.11945,\"max\":1791.72878,\"duration\":45,\"amountOutUsd\":479.39,\"quoteId\":\"6a2b55e7a5f07ecb32c998c2\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":479,\"min\":0.11945,\"max\":1791.72878,\"duration\":8,\"amountOutUsd\":479,\"quoteId\":\"6a2b55e7a5f07ecb32c9990b\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":477.735041,\"min\":0.0183066132,\"max\":12.17116674,\"duration\":5,\"amountOutUsd\":477.735041,\"quoteId\":\"6a2b55e7a5f07ecb32c99970\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":477.555,\"min\":0.0183066132,\"max\":12.17116674,\"duration\":9,\"amountOutUsd\":477.555,\"quoteId\":\"6a2b55e7a5f07ecb32c99949\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":477.277563,\"min\":0.0183066132,\"max\":12.17116674,\"duration\":15,\"amountOutUsd\":477.277563,\"quoteId\":\"6a2b55e8a5f07ecb32c999c7\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":477.05382299999997,\"min\":0.0183066132,\"max\":12.17116674,\"duration\":8,\"amountOutUsd\":477.05382299999997,\"quoteId\":\"6a2b55e7a5f07ecb32c9994f\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":475.16792,\"min\":0.0183066132,\"max\":12.17116674,\"duration\":11,\"amountOutUsd\":475.16792,\"quoteId\":\"6a2b55e7a5f07ecb32c99954\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":472.58,\"min\":0.0183066132,\"max\":12.17116674,\"duration\":45,\"amountOutUsd\":472.58,\"quoteId\":\"6a2b55e7a5f07ecb32c99934\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":472,\"min\":0.0183066132,\"max\":12.17116674,\"duration\":10,\"amountOutUsd\":472,\"quoteId\":\"6a2b55e7a5f07ecb32c9994d\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.3,\"amountOut\":470.12,\"min\":0.0183066132,\"max\":12.17116674,\"duration\":30,\"amountOutUsd\":470.12,\"quoteId\":\"6a2b55e7a5f07ecb32c9996a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null}]}" +} \ No newline at end of file diff --git a/test/houdiniFixtures/8b07891ac9cd8a7411ea5f7d.json b/test/houdiniFixtures/8b07891ac9cd8a7411ea5f7d.json new file mode 100644 index 00000000..00ce39f5 --- /dev/null +++ b/test/houdiniFixtures/8b07891ac9cd8a7411ea5f7d.json @@ -0,0 +1,5 @@ +{ + "status": 200, + "ok": true, + "body": "{\"created\":\"2026-07-03T00:16:48.681Z\",\"houdiniId\":\"3UJocbNVFMn4wZ4b48tdUq\",\"receiverAddress\":\"0x9f1f9a5c0f1d9a5c0f1d9a5c0f1d9a5c0f1d9a5c\",\"status\":0,\"anonymous\":false,\"expires\":\"2026-07-03T00:36:48.595Z\",\"fixed\":true,\"validUntil\":\"1783037866\",\"refundAddress\":\"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa\",\"in\":\"cl\",\"inAmount\":0.00421549,\"amountType\":\"receive\",\"inSymbol\":\"BTC\",\"inStatus\":0,\"inCreated\":\"2026-07-03T00:16:48.681Z\",\"outAmount\":0.15,\"outSymbol\":\"ETH\",\"eta\":19,\"inAmountUsd\":258.8075214109,\"inToken\":{\"address\":null,\"chain\":\"bitcoin\",\"decimals\":8,\"symbol\":\"BTC\",\"name\":\"Bitcoin\",\"created\":\"2024-07-06T21:29:34.822Z\",\"modified\":\"2026-07-02T15:35:28.463Z\",\"chainData\":{\"created\":\"2024-07-06T21:02:11.671Z\",\"modified\":\"2026-07-02T11:12:57.899Z\",\"name\":\"Bitcoin\",\"shortName\":\"bitcoin\",\"memoNeeded\":false,\"explorerUrl\":\"https://blockchair.com/bitcoin/transaction/{txHash}\",\"addressUrl\":\"https://blockchair.com/bitcoin/address/{address}\",\"priority\":3,\"kind\":\"bitcoin\",\"enabled\":true,\"shortNameV1\":\"BTC\",\"addressValidation\":\"^([13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[a-z0-9]{39}|bc1[a-z0-9]{59})$\",\"tokenAddressValidation\":\"^([13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[a-z0-9]{39}|bc1[a-z0-9]{59})$\",\"id\":\"6689b0d3c52f263938c44843\",\"icon\":\"https://api.houdiniswap.com/assets/networks/BTC.png\"},\"description\":\"Bitcoin is the first successful internet money based on peer-to-peer technology; whereby no central bank or authority is involved in the transaction and production of the Bitcoin currency. It was created by an anonymous individual/group under the name, Satoshi Nakamoto. The source code is available publicly as an open source project, anybody can look at it and be part of the developmental process.\\r\\n\\r\\nBitcoin is changing the way we see money as we speak. The idea was to produce a means of exchange, independent of any central authority, that could be transferred electronically in a secure, verifiable and immutable way. It is a decentralized peer-to-peer internet currency making mobile payment easy, very low transaction fees, protects your identity, and it works anywhere all the time with no central authority and banks.\\r\\n\\r\\nBitcoin is designed to have only 21 million BTC ever created, thus making it a deflationary currency. Bitcoin uses the SHA-256 hashing algorithm with an average transaction confirmation time of 10 minutes. Miners today are mining Bitcoin using ASIC chip dedicated to only mining Bitcoin, and the hash rate has shot up to peta hashes.\\r\\n\\r\\nBeing the first successful online cryptography currency, Bitcoin has inspired other alternative currencies such as Litecoin, Peercoin, Primecoin, and so on.\\r\\n\\r\\nThe cryptocurrency then took off with the innovation of the turing-complete smart contract by Ethereum which led to the development of other amazing projects such as EOS, Tron, and even crypto-collectibles such as CryptoKitties.\",\"mainnet\":true,\"enabled\":true,\"unverified\":false,\"hasDex\":true,\"hasCex\":true,\"hasSelfPrivate\":true,\"cexTokenId\":\"BTC\",\"rank\":1,\"cgId\":\"bitcoin\",\"marketCapChange24h\":2.60446,\"circulatingSupply\":20051253,\"price\":61394.41,\"marketCap\":1230506511065,\"volume\":39927686717,\"fdv\":1230506511065,\"change\":2.65105,\"id\":\"6689b73ec90e45f3b3e51551\",\"icon\":\"https://api.houdiniswap.com/assets/tokens/BTC-BTC-v79cxh.png\"},\"outToken\":{\"address\":null,\"chain\":\"ethereum\",\"decimals\":18,\"symbol\":\"ETH\",\"name\":\"Ethereum\",\"created\":\"2024-07-06T21:29:34.823Z\",\"modified\":\"2026-07-02T12:17:59.943Z\",\"chainData\":{\"created\":\"2024-07-06T21:02:11.672Z\",\"modified\":\"2026-07-02T12:51:46.188Z\",\"name\":\"Ethereum Mainnet\",\"shortName\":\"ethereum\",\"memoNeeded\":false,\"explorerUrl\":\"https://etherscan.io/tx/{txHash}\",\"addressUrl\":\"https://etherscan.io/address/{address}\",\"priority\":2,\"kind\":\"evm\",\"chainId\":1,\"enabled\":true,\"shortNameV1\":\"ETH\",\"addressValidation\":\"^(0x)[0-9A-Za-z]{40}$\",\"tokenAddressValidation\":\"^(0x)[0-9A-Za-z]{40}$\",\"id\":\"6689b0d3c52f263938c44844\",\"icon\":\"https://api.houdiniswap.com/assets/networks/ETH.png\"},\"description\":\"Ethereum is a global, open-source platform for decentralized applications. In other words, the vision is to create a world computer that anyone can build applications in a decentralized manner; while all states and data are distributed and publicly accessible. Ethereum supports smart contracts in which developers can write code in order to program digital value. Examples of decentralized apps (dapps) that are built on Ethereum includes tokens, non-fungible tokens, decentralized finance apps, lending protocol, decentralized exchanges, and much more.\\r\\n\\r\\nOn Ethereum, all transactions and smart contract executions require a small fee to be paid. This fee is called Gas. In technical terms, Gas refers to the unit of measure on the amount of computational effort required to execute an operation or a smart contract. The more complex the execution operation is, the more gas is required to fulfill that operation. Gas fees are paid entirely in Ether (ETH), which is the native coin of the blockchain. The price of gas can fluctuate from time to time depending on the network demand.\",\"mainnet\":true,\"enabled\":true,\"unverified\":false,\"hasDex\":true,\"hasCex\":true,\"hasSelfPrivate\":true,\"cexTokenId\":\"ETH\",\"rank\":2,\"cgId\":\"ethereum\",\"marketCapChange24h\":6.01478,\"circulatingSupply\":120683347.0606021,\"price\":1699.9,\"marketCap\":205035853989,\"volume\":12996267569,\"fdv\":205035853989,\"change\":6.03288,\"priority\":2,\"id\":\"6689b73ec90e45f3b3e51566\",\"icon\":\"https://api.houdiniswap.com/assets/tokens/ETH-ETH-lejbdy.png\"},\"swapName\":\"Changelly\",\"depositAddress\":\"bc1qj5ll8gghgp559m0q3r7mpzjjhq2y3cuep9l9he\",\"id\":\"6a46ff70350134bfbdac6df7\",\"hashUrl\":\"\",\"statusLabel\":\"WAITING\",\"inStatusLabel\":\"NEW\",\"displayStatus\":\"WAITING_FOR_DEPOSIT\"}" +} \ No newline at end of file diff --git a/test/houdiniFixtures/9ab7ff504579be97cb040649.json b/test/houdiniFixtures/9ab7ff504579be97cb040649.json new file mode 100644 index 00000000..71a90deb --- /dev/null +++ b/test/houdiniFixtures/9ab7ff504579be97cb040649.json @@ -0,0 +1,5 @@ +{ + "status": 200, + "ok": true, + "body": "{\"created\":\"2026-06-12T00:43:17.470Z\",\"houdiniId\":\"hfwkBS65s6PKVnQHNSNX7q\",\"receiverAddress\":\"0x9f1f9a5c0f1d9a5c0f1d9a5c0f1d9a5c0f1d9a5c\",\"status\":0,\"anonymous\":true,\"expires\":\"2026-06-12T01:13:17.470Z\",\"inAmount\":0.3,\"inSymbol\":\"ETH\",\"inStatus\":0,\"inCreated\":\"2026-06-12T00:43:17.470Z\",\"outAmount\":493.96269,\"outSymbol\":\"USDC\",\"outStatus\":0,\"eta\":5,\"inAmountUsd\":502.59899999999993,\"inToken\":{\"address\":null,\"chain\":\"ethereum\",\"decimals\":18,\"symbol\":\"ETH\",\"name\":\"Ethereum\",\"created\":\"2024-07-06T21:29:34.823Z\",\"modified\":\"2026-06-11T21:36:16.580Z\",\"chainData\":{\"created\":\"2024-07-06T21:02:11.672Z\",\"modified\":\"2026-06-10T08:00:33.714Z\",\"name\":\"Ethereum Mainnet\",\"shortName\":\"ethereum\",\"memoNeeded\":false,\"explorerUrl\":\"https://etherscan.io/tx/{txHash}\",\"addressUrl\":\"https://etherscan.io/address/{address}\",\"priority\":2,\"kind\":\"evm\",\"chainId\":1,\"enabled\":true,\"shortNameV1\":\"ETH\",\"addressValidation\":\"^(0x)[0-9A-Za-z]{40}$\",\"tokenAddressValidation\":\"^(0x)[0-9A-Za-z]{40}$\",\"id\":\"6689b0d3c52f263938c44844\",\"icon\":\"https://api.houdiniswap.com/assets/networks/ETH.png\"},\"description\":\"Ethereum is a global, open-source platform for decentralized applications. In other words, the vision is to create a world computer that anyone can build applications in a decentralized manner; while all states and data are distributed and publicly accessible. Ethereum supports smart contracts in which developers can write code in order to program digital value. Examples of decentralized apps (dapps) that are built on Ethereum includes tokens, non-fungible tokens, decentralized finance apps, lending protocol, decentralized exchanges, and much more.\\r\\n\\r\\nOn Ethereum, all transactions and smart contract executions require a small fee to be paid. This fee is called Gas. In technical terms, Gas refers to the unit of measure on the amount of computational effort required to execute an operation or a smart contract. The more complex the execution operation is, the more gas is required to fulfill that operation. Gas fees are paid entirely in Ether (ETH), which is the native coin of the blockchain. The price of gas can fluctuate from time to time depending on the network demand.\",\"mainnet\":true,\"enabled\":true,\"unverified\":false,\"hasDex\":true,\"hasCex\":true,\"hasSelfPrivate\":true,\"cexTokenId\":\"ETH\",\"rank\":2,\"cgId\":\"ethereum\",\"marketCapChange24h\":2.86273,\"circulatingSupply\":120684325.0427515,\"price\":1675.33,\"marketCap\":201979868266,\"volume\":12286016626,\"fdv\":201979868266,\"change\":2.84053,\"priority\":2,\"id\":\"6689b73ec90e45f3b3e51566\",\"icon\":\"https://api.houdiniswap.com/assets/tokens/ETH-ETH-lejbdy.png\"},\"outToken\":{\"address\":\"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48\",\"chain\":\"ethereum\",\"decimals\":6,\"symbol\":\"USDC\",\"name\":\"USD Coin\",\"created\":\"2024-07-06T21:29:34.822Z\",\"modified\":\"2026-06-10T20:21:14.723Z\",\"chainData\":{\"created\":\"2024-07-06T21:02:11.672Z\",\"modified\":\"2026-06-10T08:00:33.714Z\",\"name\":\"Ethereum Mainnet\",\"shortName\":\"ethereum\",\"memoNeeded\":false,\"explorerUrl\":\"https://etherscan.io/tx/{txHash}\",\"addressUrl\":\"https://etherscan.io/address/{address}\",\"priority\":2,\"kind\":\"evm\",\"chainId\":1,\"enabled\":true,\"shortNameV1\":\"ETH\",\"addressValidation\":\"^(0x)[0-9A-Za-z]{40}$\",\"tokenAddressValidation\":\"^(0x)[0-9A-Za-z]{40}$\",\"id\":\"6689b0d3c52f263938c44844\",\"icon\":\"https://api.houdiniswap.com/assets/networks/ETH.png\"},\"description\":\"USDC is a fully collateralized US dollar stablecoin. USDC is the bridge between dollars and trading on cryptocurrency exchanges. The technology behind CENTRE makes it possible to exchange value between people, businesses and financial institutions just like email between mail services and texts between SMS providers. We believe by removing artificial economic borders, we can create a more inclusive global economy.\",\"mainnet\":false,\"enabled\":true,\"unverified\":false,\"hasDex\":true,\"hasCex\":true,\"hasSelfPrivate\":true,\"cexTokenId\":\"USDC\",\"rank\":5,\"cgId\":\"usd-coin\",\"marketCapChange24h\":-0.09338,\"circulatingSupply\":74904676501.2519,\"price\":0.999864,\"marketCap\":74895929374,\"volume\":13377491029,\"fdv\":74895929374,\"change\":0.00527,\"id\":\"6689b73ec90e45f3b3e51554\",\"icon\":\"https://api.houdiniswap.com/assets/tokens/USDC-ETH-7b8for.png\"},\"depositAddress\":\"0xe841b557b791e28bc71d05377cf8f40205c44077\",\"id\":\"6a2b5625a3ad2fca3a863a53\",\"hashUrl\":\"\",\"statusLabel\":\"WAITING\",\"inStatusLabel\":\"NEW\",\"outStatusLabel\":\"NEW\",\"displayStatus\":\"WAITING_FOR_DEPOSIT\"}" +} \ No newline at end of file diff --git a/test/houdiniFixtures/a4217376dc145b8c64ef4a78.json b/test/houdiniFixtures/a4217376dc145b8c64ef4a78.json new file mode 100644 index 00000000..0e97943b --- /dev/null +++ b/test/houdiniFixtures/a4217376dc145b8c64ef4a78.json @@ -0,0 +1,5 @@ +{ + "status": 422, + "ok": false, + "body": "{\"code\":\"VALIDATION_ERROR\",\"requestId\":\"3420f824-1a52-402b-8791-b6bf9823f657\",\"fields\":{\"quoteId\":{\"value\":\"6a46fd20350134bfbdac5515\",\"message\":\"The provided quote has expired, max age is 60 seconds\"}},\"message\":\"Validation Failed\"}" +} \ No newline at end of file diff --git a/test/houdiniFixtures/afd46d8607a9d80732997ae4.json b/test/houdiniFixtures/afd46d8607a9d80732997ae4.json new file mode 100644 index 00000000..46c4cac2 --- /dev/null +++ b/test/houdiniFixtures/afd46d8607a9d80732997ae4.json @@ -0,0 +1,5 @@ +{ + "status": 200, + "ok": true, + "body": "{\"tokens\":[{\"address\":null,\"chain\":\"ripple\",\"decimals\":0,\"symbol\":\"XRP\",\"name\":\"XRP\",\"created\":\"2024-07-06T21:29:34.822Z\",\"modified\":\"2026-07-03T00:00:19.503Z\",\"chainData\":{\"created\":\"2024-07-06T21:02:11.673Z\",\"modified\":\"2026-07-02T11:12:57.900Z\",\"name\":\"Ripple\",\"shortName\":\"ripple\",\"memoNeeded\":true,\"explorerUrl\":\"https://xrpscan.com/tx/{txHash}\",\"addressUrl\":\"https://xrpscan.com/account/{address}\",\"priority\":13,\"kind\":\"xrp\",\"enabled\":true,\"shortNameV1\":\"XRP\",\"addressValidation\":\"^r[1-9A-HJ-NP-Za-km-z]{25,34}$\",\"tokenAddressValidation\":\"^r[1-9A-HJ-NP-Za-km-z]{25,34}$\",\"id\":\"6689b0d3c52f263938c4484d\",\"icon\":\"https://api.houdiniswap.com/assets/networks/XRP.png\"},\"description\":\"Ripple is the catchall name for the cryptocurrency platform, the transactional protocol for which is actually XRP, in the same fashion as Ethereum is the name for the platform that facilitates trades in Ether. Like other cryptocurrencies, Ripple is built atop the idea of a distributed ledger network which requires various parties to participate in validating transactions, rather than any singular centralized authority. That facilitates transactions all over the world, and transfer fees are far cheaper than the likes of bitcoin. Unlike other cryptocurrencies, XRP transfers are effectively immediate, requiring no typical confirmation time.\\r\\n\\r\\nRipple was originally founded by a single company, Ripple Labs, and continues to be backed by it, rather than the larger network of developers that continue bitcoin\u2019s development. It also doesn\u2019t have a fluctuating amount of its currency in existence. Where bitcoin has a continually growing pool with an eventual maximum, and Ethereum theoretically has no limit, Ripple was created with all of its 100 billion XRP tokens right out of the gate. That number is maintained with no mining and most of the tokens are owned and held by Ripple Labs itself \u2014 around 60 billion at the latest count.\\r\\n\\r\\nEven at the recently reduced value of around half a dollar per XRP, that means Ripple Labs is currently sitting on around $20 billion worth of the cryptocurrency (note: Ripple\u2019s price crashed hard recently, and may be worth far less than $60 billion by time you read this). It holds 55 billion XRP in an escrow account, which allows it to sell up to a billion per month if it so chooses in order to fund new projects and acquisitions. Selling such an amount would likely have a drastic effect on the cryptocurrency\u2019s value, and isn\u2019t something Ripple Labs plans to do anytime soon.\\r\\n\\r\\nIn actuality, Ripple Labs is looking to leverage the technology behind XRP to allow for faster banking transactions around the world. While Bitcoin and other cryptocurrencies are built on the idea of separating financial transactions from the financial organizations of traditional currencies, Ripple is almost the opposite in every sense.\\r\\n\\r\\nXRP by Ripple price can be found on this page alongside the market capitalization and additional stats.\\r\\n\\r\\n\",\"mainnet\":true,\"enabled\":true,\"unverified\":false,\"hasDex\":false,\"hasCex\":true,\"hasSelfPrivate\":true,\"cexTokenId\":\"XRP\",\"rank\":6,\"cgId\":\"ripple\",\"marketCapChange24h\":3.26934,\"circulatingSupply\":62241508805,\"price\":1.088,\"marketCap\":67623334700,\"volume\":1745402932,\"fdv\":108631086694,\"change\":3.27574,\"id\":\"6689b73ec90e45f3b3e5155b\",\"icon\":\"https://api.houdiniswap.com/assets/tokens/xrp-ripple.png\"}],\"total\":1,\"totalPages\":1}" +} \ No newline at end of file diff --git a/test/houdiniFixtures/d713b25ae09e70162f8835d7.json b/test/houdiniFixtures/d713b25ae09e70162f8835d7.json new file mode 100644 index 00000000..ee7e45ca --- /dev/null +++ b/test/houdiniFixtures/d713b25ae09e70162f8835d7.json @@ -0,0 +1,5 @@ +{ + "status": 200, + "ok": true, + "body": "{\"created\":\"2026-07-03T00:10:47.143Z\",\"houdiniId\":\"wDWZFCAen77nwoCWW5VYsN\",\"receiverAddress\":\"rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe\",\"status\":0,\"anonymous\":true,\"expires\":\"2026-07-03T00:40:47.143Z\",\"inAmount\":0.1,\"inSymbol\":\"ETH\",\"inStatus\":0,\"inCreated\":\"2026-07-03T00:10:47.143Z\",\"outAmount\":153.90872341,\"outSymbol\":\"XRP\",\"outStatus\":0,\"receiverTag\":\"12345\",\"eta\":4,\"inAmountUsd\":169.99300000000002,\"inToken\":{\"address\":null,\"chain\":\"ethereum\",\"decimals\":18,\"symbol\":\"ETH\",\"name\":\"Ethereum\",\"created\":\"2024-07-06T21:29:34.823Z\",\"modified\":\"2026-07-02T12:17:59.943Z\",\"chainData\":{\"created\":\"2024-07-06T21:02:11.672Z\",\"modified\":\"2026-07-02T12:51:46.188Z\",\"name\":\"Ethereum Mainnet\",\"shortName\":\"ethereum\",\"memoNeeded\":false,\"explorerUrl\":\"https://etherscan.io/tx/{txHash}\",\"addressUrl\":\"https://etherscan.io/address/{address}\",\"priority\":2,\"kind\":\"evm\",\"chainId\":1,\"enabled\":true,\"shortNameV1\":\"ETH\",\"addressValidation\":\"^(0x)[0-9A-Za-z]{40}$\",\"tokenAddressValidation\":\"^(0x)[0-9A-Za-z]{40}$\",\"id\":\"6689b0d3c52f263938c44844\",\"icon\":\"https://api.houdiniswap.com/assets/networks/ETH.png\"},\"description\":\"Ethereum is a global, open-source platform for decentralized applications. In other words, the vision is to create a world computer that anyone can build applications in a decentralized manner; while all states and data are distributed and publicly accessible. Ethereum supports smart contracts in which developers can write code in order to program digital value. Examples of decentralized apps (dapps) that are built on Ethereum includes tokens, non-fungible tokens, decentralized finance apps, lending protocol, decentralized exchanges, and much more.\\r\\n\\r\\nOn Ethereum, all transactions and smart contract executions require a small fee to be paid. This fee is called Gas. In technical terms, Gas refers to the unit of measure on the amount of computational effort required to execute an operation or a smart contract. The more complex the execution operation is, the more gas is required to fulfill that operation. Gas fees are paid entirely in Ether (ETH), which is the native coin of the blockchain. The price of gas can fluctuate from time to time depending on the network demand.\",\"mainnet\":true,\"enabled\":true,\"unverified\":false,\"hasDex\":true,\"hasCex\":true,\"hasSelfPrivate\":true,\"cexTokenId\":\"ETH\",\"rank\":2,\"cgId\":\"ethereum\",\"marketCapChange24h\":5.78395,\"circulatingSupply\":120683347.0606021,\"price\":1699.64,\"marketCap\":204925300984,\"volume\":12965251347,\"fdv\":204925300984,\"change\":5.75434,\"priority\":2,\"id\":\"6689b73ec90e45f3b3e51566\",\"icon\":\"https://api.houdiniswap.com/assets/tokens/ETH-ETH-lejbdy.png\"},\"outToken\":{\"address\":null,\"chain\":\"ripple\",\"decimals\":0,\"symbol\":\"XRP\",\"name\":\"XRP\",\"created\":\"2024-07-06T21:29:34.822Z\",\"modified\":\"2026-07-03T00:10:42.490Z\",\"chainData\":{\"created\":\"2024-07-06T21:02:11.673Z\",\"modified\":\"2026-07-02T11:12:57.900Z\",\"name\":\"Ripple\",\"shortName\":\"ripple\",\"memoNeeded\":true,\"explorerUrl\":\"https://xrpscan.com/tx/{txHash}\",\"addressUrl\":\"https://xrpscan.com/account/{address}\",\"priority\":13,\"kind\":\"xrp\",\"enabled\":true,\"shortNameV1\":\"XRP\",\"addressValidation\":\"^r[1-9A-HJ-NP-Za-km-z]{25,34}$\",\"tokenAddressValidation\":\"^r[1-9A-HJ-NP-Za-km-z]{25,34}$\",\"id\":\"6689b0d3c52f263938c4484d\",\"icon\":\"https://api.houdiniswap.com/assets/networks/XRP.png\"},\"description\":\"Ripple is the catchall name for the cryptocurrency platform, the transactional protocol for which is actually XRP, in the same fashion as Ethereum is the name for the platform that facilitates trades in Ether. Like other cryptocurrencies, Ripple is built atop the idea of a distributed ledger network which requires various parties to participate in validating transactions, rather than any singular centralized authority. That facilitates transactions all over the world, and transfer fees are far cheaper than the likes of bitcoin. Unlike other cryptocurrencies, XRP transfers are effectively immediate, requiring no typical confirmation time.\\r\\n\\r\\nRipple was originally founded by a single company, Ripple Labs, and continues to be backed by it, rather than the larger network of developers that continue bitcoin’s development. It also doesn’t have a fluctuating amount of its currency in existence. Where bitcoin has a continually growing pool with an eventual maximum, and Ethereum theoretically has no limit, Ripple was created with all of its 100 billion XRP tokens right out of the gate. That number is maintained with no mining and most of the tokens are owned and held by Ripple Labs itself — around 60 billion at the latest count.\\r\\n\\r\\nEven at the recently reduced value of around half a dollar per XRP, that means Ripple Labs is currently sitting on around $20 billion worth of the cryptocurrency (note: Ripple’s price crashed hard recently, and may be worth far less than $60 billion by time you read this). It holds 55 billion XRP in an escrow account, which allows it to sell up to a billion per month if it so chooses in order to fund new projects and acquisitions. Selling such an amount would likely have a drastic effect on the cryptocurrency’s value, and isn’t something Ripple Labs plans to do anytime soon.\\r\\n\\r\\nIn actuality, Ripple Labs is looking to leverage the technology behind XRP to allow for faster banking transactions around the world. While Bitcoin and other cryptocurrencies are built on the idea of separating financial transactions from the financial organizations of traditional currencies, Ripple is almost the opposite in every sense.\\r\\n\\r\\nXRP by Ripple price can be found on this page alongside the market capitalization and additional stats.\\r\\n\\r\\n\",\"mainnet\":true,\"enabled\":true,\"unverified\":false,\"hasDex\":false,\"hasCex\":true,\"hasSelfPrivate\":true,\"cexTokenId\":\"XRP\",\"rank\":6,\"cgId\":\"ripple\",\"marketCapChange24h\":3.3826,\"circulatingSupply\":62241508805,\"price\":1.0859,\"marketCap\":67587129117,\"volume\":1743143552,\"fdv\":108572925532,\"change\":3.37677,\"id\":\"6689b73ec90e45f3b3e5155b\",\"icon\":\"https://api.houdiniswap.com/assets/tokens/xrp-ripple.png\"},\"depositAddress\":\"0x0588ded83bb478ee309bd73a03fbd34018f16e37\",\"id\":\"6a46fe07732524ae48417b65\",\"hashUrl\":\"\",\"statusLabel\":\"WAITING\",\"inStatusLabel\":\"NEW\",\"outStatusLabel\":\"NEW\",\"displayStatus\":\"WAITING_FOR_DEPOSIT\"}" +} \ No newline at end of file diff --git a/test/houdiniFixtures/f4d1a1a692658a2e078b3713.json b/test/houdiniFixtures/f4d1a1a692658a2e078b3713.json new file mode 100644 index 00000000..829b8f70 --- /dev/null +++ b/test/houdiniFixtures/f4d1a1a692658a2e078b3713.json @@ -0,0 +1,5 @@ +{ + "status": 200, + "ok": true, + "body": "{\"tokens\":[{\"address\":null,\"chain\":\"ethereum\",\"decimals\":18,\"symbol\":\"ETH\",\"name\":\"Ethereum\",\"created\":\"2024-07-06T21:29:34.823Z\",\"modified\":\"2026-06-11T21:36:16.580Z\",\"chainData\":{\"created\":\"2024-07-06T21:02:11.672Z\",\"modified\":\"2026-06-10T08:00:33.714Z\",\"name\":\"Ethereum Mainnet\",\"shortName\":\"ethereum\",\"memoNeeded\":false,\"explorerUrl\":\"https://etherscan.io/tx/{txHash}\",\"addressUrl\":\"https://etherscan.io/address/{address}\",\"priority\":2,\"kind\":\"evm\",\"chainId\":1,\"enabled\":true,\"shortNameV1\":\"ETH\",\"addressValidation\":\"^(0x)[0-9A-Za-z]{40}$\",\"tokenAddressValidation\":\"^(0x)[0-9A-Za-z]{40}$\",\"id\":\"6689b0d3c52f263938c44844\",\"icon\":\"https://api.houdiniswap.com/assets/networks/ETH.png\"},\"description\":\"Ethereum is a global, open-source platform for decentralized applications. In other words, the vision is to create a world computer that anyone can build applications in a decentralized manner; while all states and data are distributed and publicly accessible. Ethereum supports smart contracts in which developers can write code in order to program digital value. Examples of decentralized apps (dapps) that are built on Ethereum includes tokens, non-fungible tokens, decentralized finance apps, lending protocol, decentralized exchanges, and much more.\\r\\n\\r\\nOn Ethereum, all transactions and smart contract executions require a small fee to be paid. This fee is called Gas. In technical terms, Gas refers to the unit of measure on the amount of computational effort required to execute an operation or a smart contract. The more complex the execution operation is, the more gas is required to fulfill that operation. Gas fees are paid entirely in Ether (ETH), which is the native coin of the blockchain. The price of gas can fluctuate from time to time depending on the network demand.\",\"mainnet\":true,\"enabled\":true,\"unverified\":false,\"hasDex\":true,\"hasCex\":true,\"hasSelfPrivate\":true,\"cexTokenId\":\"ETH\",\"rank\":2,\"cgId\":\"ethereum\",\"marketCapChange24h\":2.86273,\"circulatingSupply\":120684325.0427515,\"price\":1675.19,\"marketCap\":201979868266,\"volume\":12942627219,\"fdv\":201979868266,\"change\":2.83865,\"priority\":2,\"id\":\"6689b73ec90e45f3b3e51566\",\"icon\":\"https://api.houdiniswap.com/assets/tokens/ETH-ETH-lejbdy.png\"}],\"total\":1,\"totalPages\":1}" +} \ No newline at end of file diff --git a/test/testconfig.ts b/test/testconfig.ts index b9cca796..f2a6c375 100644 --- a/test/testconfig.ts +++ b/test/testconfig.ts @@ -71,6 +71,12 @@ export const asTestConfig = asObject({ apiKey: asOptional(asString, '') }).withRest ), + HOUDINI_INIT: asCorePluginInit( + asObject({ + apiKey: asOptional(asString, ''), + apiSecret: asOptional(asString, '') + }).withRest + ), LIFI_INIT: asCorePluginInit( asObject({ affiliateFeeBasis: asOptional(asString, '50'),