Add HoudiniSwap prototype swap plugin - #462
Conversation
3252940 to
ae500e8
Compare
Prototype CEX swap plugin for HoudiniSwap built on the v2 partner API:
resolve Edge assets to Houdini token ids via GET /tokens, fetch quotes,
pick the best private route, and create the exchange. Credentials are wired
through testconfig.json as { apiKey, apiSecret }.
Includes a standalone acceptance harness that drives BTC->ETH and ETH->USDC
quote plus order creation, caching every API response to disk so repeat runs
replay from fixtures and stay inside the free-tier rate budget.
ae500e8 to
11a8fbc
Compare
HoudiniSwap's partner API is server-to-server and answers browser-origin requests with HTTP 403. The core runs swap plugins inside a WebView, so the default fetch carries Origin / Sec-Fetch-* headers the API rejects. Pass corsBypass: 'always' so each call is made host-side, matching the API's server-to-server contract. Verified by an in-app DASH/ETH to BTC private send on the iOS simulator.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7c8e50b. Configure here.
| swapInfo, | ||
| floatToNativeAmount(fromWallet, largestMax, request.fromTokenId) | ||
| ) | ||
| } |
There was a problem hiding this comment.
Sentinel max breaks above limit
Medium Severity
When no private route accepts the swap amount, above-limit handling uses Math.max over every route max, including Houdini’s very large sentinel caps. That makes largestMax effectively unlimited, so gt(fromExchangeAmount, floatToDecimalString(largestMax)) almost never holds and SwapAboveLimitError is skipped even when the amount exceeds every finite route maximum.
Reviewed by Cursor Bugbot for commit 7c8e50b. Configure here.





CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
none
Description
Prototype swap plugin for HoudiniSwap (privacy-routing CEX aggregator) on the v2 partner API, modeled on
changenow.tsfromtemplate.ts. Phase 2 of Asana task https://app.asana.com/0/1215088146871429/1215645061309285 (the Phase 1 coverage matrix is attached to that task).What it does
src/swap/central/houdini.ts: resolve Edge assets to Houdini token ids viaGET /tokens, fetch quotes viaGET /quotes, pick the bestprivateroute, and create the order viaPOST /exchanges.initOptionsis{ apiKey, apiSecret }.src/mappings/houdini.ts: hand-authoredEdgeCurrencyPluginIdto Houdini chainshortNamemap (32 non-memo chains from the Phase 1 matrix). Memo-required and unsupported chains map tonulland are rejected bycheckWhitelistedMainnetCodes.test/testconfig.ts: wiresHOUDINI_INITthe same way other partners do.test/houdiniPartner.ts+test/houdiniFixtures/: a standalone acceptance harness that drives the real quote plus order-creation flow and caches every API response to disk, so repeat runs replay from fixtures and stay inside the free-tier budget.Endpoint paths and cleaners are derived from the OpenAPI spec (
https://api-partner.houdiniswap.com/v2/openapi.json), not guessed.Auth quirks
Authorization: <apiKey>:<apiSecret>with noBearerprefix. Every endpoint returns402without it.GET /tokensand memoizes them per chain plus contract.privatequotes are leaner thanstandardones: noswap,validUntil, orfixedfields, so the cleaner treats those as optional.Observed rate-limit and routing behavior
429with aretryAfterseconds value ({ code: "RATE_LIMIT_EXCEEDED", retryAfter, ... }).GET /quotesreturns onlystandardroutes and zeroprivateroutes (e.g. 0.00025 BTC returns 1 standard / 0 private; 0.0016 BTC returns 123 private). The prototype, which filters toprivate, surfaces this as a currency error today. Follow-up: read thestandardroute'sminand raiseSwapBelowLimitErrorinstead.Spec/docs mismatches
GET /chainspageSizemax 1000, but the server rejects anything over 100 withVALIDATION_ERROR. Paginate at <= 100.Chain.enabledis documented as required but is omitted per-object byGET /chains; the endpoint returns only enabled chains.Testing
test/houdiniPartner.tsdrives BTC to ETH and ETH to USDC throughmakeHoudiniPlugin: quote fetch plus order creation both succeed and return a real deposit address. First run hits the live API once (with a single backoff retry for the 1/min exchange limit); committed fixtures make every later run replay with zero live calls.tsc, eslint, and the mocha suite pass.edge-react-guion the iOS simulator: HoudiniSwap registered as a swap provider, a real BTC to ETH swap (0.0019 BTC, ~120 USD) quoted "Powered by HoudiniSwap" and executed to the in-app success scene. Proof screenshots attached below.Scope
This is a prototype. Reverse ("to") and "max" quotes, memo/
destinationTagplumbing for XRP/XLM/Cosmos chains, a sub-minimumSwapBelowLimitError, and production gui wiring are deferred follow-ups.Note
Medium Risk
New third-party swap path that creates real deposit orders and moves user funds on success; prototype limits (from-only quotes, private-route filter, no memo chains) can cause confusing rejections below ~$100 private minimums.
Overview
Adds a HoudiniSwap centralized swap provider (
houdini) wired into the plugin registry, targeting Houdini’s v2 partner API for privacy-oriented private CEX routes.The new plugin resolves Edge assets to Houdini token IDs via
GET /tokens, requestsGET /quotes, selects the best in-range private quote, and books the swap withPOST /exchanges. It usesapiKey/apiSecretauth, forces CORS bypass for host-side calls, supports from-only quotes, and maps ~32 non-memo chains insrc/mappings/houdini.ts(memo chains stay unsupported). Amount handling normalizes API floats/scientific notation into native units; Zcash uses transparent addresses.Testing adds
HOUDINI_INITin test config, a standalonetest/houdiniPartner.tsacceptance harness (BTC→ETH, ETH→USDC), and committedtest/houdiniFixtures/so reruns replay cached API responses without live quota.Reviewed by Cursor Bugbot for commit 7c8e50b. Bugbot is set up for automated code reviews on this repo. Configure here.