Skip to content

Commit c35e123

Browse files
committed
Add Houdini quote/exchange test with cached fixtures
Exercises the Houdini plugin end-to-end for BTC->ETH and ETH->USDC private swaps, asserting both the quote and the exchange (deposit address) succeed. Every API response is cached under test/houdini/fixtures and replayed, so CI runs deterministically without credentials or network. Refresh with HOUDINI_LIVE=1 + credentials.
1 parent b3cc2b7 commit c35e123

9 files changed

Lines changed: 260 additions & 0 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- added: HoudiniSwap swap plugin prototype (private routing) with cached test fixtures
6+
57
## 2.46.0 (2026-04-18)
68

79
- changed: Migrate Thorchain swap endpoints off NineRealms (thornode, tx tracker, Midgard sync fallback).

test/houdini.test.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { assert } from 'chai'
2+
import { createHash } from 'crypto'
3+
import {
4+
EdgeCorePluginOptions,
5+
EdgeCurrencyWallet,
6+
EdgeSwapRequest
7+
} from 'edge-core-js/types'
8+
import fs from 'fs'
9+
import { describe, it } from 'mocha'
10+
import path from 'path'
11+
12+
import { makeHoudiniPlugin } from '../src/swap/central/houdini'
13+
14+
/**
15+
* Houdini partner-API test.
16+
*
17+
* The HARD free-tier rate limits (quote 5/min, exchange 1/min) make repeated
18+
* live calls impractical, so every API response is cached to
19+
* `test/houdini/fixtures/` on first fetch and replayed afterwards. CI runs
20+
* entirely from fixtures with no credentials or network.
21+
*
22+
* To refresh fixtures against the live API:
23+
* HOUDINI_LIVE=1 HOUDINI_API_KEY=... HOUDINI_API_SECRET=... \
24+
* yarn test --grep houdini
25+
* (delete `test/houdini/fixtures/` first for a clean capture).
26+
*/
27+
28+
const FIXTURE_DIR = path.join(__dirname, 'houdini', 'fixtures')
29+
const LIVE = process.env.HOUDINI_LIVE === '1'
30+
31+
const cacheKey = (method: string, url: string, body: string): string => {
32+
const slug = `${method}_${url
33+
.replace('https://api-partner.houdiniswap.com/v2/', '')
34+
.replace(/[^a-z0-9]+/gi, '_')}`.slice(0, 80)
35+
const hash = createHash('sha1')
36+
.update(`${method} ${url} ${body}`)
37+
.digest('hex')
38+
.slice(0, 10)
39+
return `${slug}__${hash}.json`
40+
}
41+
42+
interface CachedResponse {
43+
status: number
44+
body: string
45+
}
46+
47+
const makeResponse = (cached: CachedResponse): any => ({
48+
ok: cached.status >= 200 && cached.status < 300,
49+
status: cached.status,
50+
text: async (): Promise<string> => cached.body,
51+
json: async (): Promise<unknown> => JSON.parse(cached.body)
52+
})
53+
54+
interface FetchOpts {
55+
method?: string
56+
body?: string
57+
headers?: { [key: string]: string }
58+
}
59+
60+
/** A `fetch` that replays cached fixtures and only hits the network on refresh. */
61+
const cachingFetch = async (
62+
url: string,
63+
opts: FetchOpts = {}
64+
): Promise<unknown> => {
65+
const method = opts.method ?? 'GET'
66+
const body = opts.body ?? ''
67+
const file = path.join(FIXTURE_DIR, cacheKey(method, url, body))
68+
69+
if (fs.existsSync(file)) {
70+
const cached: CachedResponse = JSON.parse(fs.readFileSync(file, 'utf8'))
71+
return makeResponse(cached)
72+
}
73+
74+
if (!LIVE) {
75+
throw new Error(
76+
`Houdini fixture missing for ${method} ${url}. Re-run with HOUDINI_LIVE=1 and credentials to capture it.`
77+
)
78+
}
79+
80+
const response = await fetch(url, opts)
81+
const text = await response.text()
82+
const cached: CachedResponse = { status: response.status, body: text }
83+
// Only persist successful responses so a transient rate-limit (429) or server
84+
// error never poisons the replay cache.
85+
if (response.ok) {
86+
fs.mkdirSync(FIXTURE_DIR, { recursive: true })
87+
fs.writeFileSync(file, JSON.stringify(cached, null, 2))
88+
}
89+
return makeResponse(cached)
90+
}
91+
92+
interface MockToken {
93+
currencyCode: string
94+
multiplier: string
95+
contractAddress: string
96+
}
97+
98+
interface MockWalletParams {
99+
pluginId: string
100+
currencyCode: string
101+
multiplier: string
102+
address: string
103+
tokens?: { [tokenId: string]: MockToken }
104+
}
105+
106+
const makeMockWallet = (params: MockWalletParams): EdgeCurrencyWallet => {
107+
const { pluginId, currencyCode, multiplier, address, tokens = {} } = params
108+
109+
const allTokens: { [tokenId: string]: unknown } = {}
110+
for (const tokenId of Object.keys(tokens)) {
111+
const token = tokens[tokenId]
112+
allTokens[tokenId] = {
113+
currencyCode: token.currencyCode,
114+
denominations: [
115+
{ name: token.currencyCode, multiplier: token.multiplier }
116+
],
117+
networkLocation: { contractAddress: token.contractAddress }
118+
}
119+
}
120+
121+
const wallet = {
122+
id: `${pluginId}-wallet`,
123+
currencyInfo: {
124+
pluginId,
125+
currencyCode,
126+
denominations: [{ name: currencyCode, multiplier }]
127+
},
128+
currencyConfig: { allTokens },
129+
getAddresses: async (): Promise<
130+
Array<{ addressType: string; publicAddress: string }>
131+
> => [{ addressType: 'publicAddress', publicAddress: address }],
132+
// Echo the spendInfo back so makeSwapPluginQuote can read the swap action.
133+
makeSpend: async (spendInfo: any): Promise<any> => ({
134+
networkFee: '0',
135+
tokenId: spendInfo.tokenId,
136+
spendTargets: spendInfo.spendTargets,
137+
savedAction: spendInfo.savedAction,
138+
assetAction: spendInfo.assetAction
139+
})
140+
}
141+
return (wallet as unknown) as EdgeCurrencyWallet
142+
}
143+
144+
const BTC_ADDRESS = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'
145+
const ETH_ADDRESS = '0x9f1f9a5c0f1d9a5c0f1d9a5c0f1d9a5c0f1d9a5c'
146+
const USDC_TOKEN_ID = 'a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'
147+
148+
const btcWallet = makeMockWallet({
149+
pluginId: 'bitcoin',
150+
currencyCode: 'BTC',
151+
multiplier: '100000000',
152+
address: BTC_ADDRESS
153+
})
154+
155+
const ethWallet = makeMockWallet({
156+
pluginId: 'ethereum',
157+
currencyCode: 'ETH',
158+
multiplier: '1000000000000000000',
159+
address: ETH_ADDRESS,
160+
tokens: {
161+
[USDC_TOKEN_ID]: {
162+
currencyCode: 'USDC',
163+
multiplier: '1000000',
164+
contractAddress: '0xA0b86991c6218b36C1d19D4a2e9Eb0cE3606eB48'
165+
}
166+
}
167+
})
168+
169+
const makePlugin = (): ReturnType<typeof makeHoudiniPlugin> => {
170+
const opts = {
171+
initOptions: {
172+
apiKey: process.env.HOUDINI_API_KEY ?? '',
173+
apiSecret: process.env.HOUDINI_API_SECRET ?? ''
174+
},
175+
io: { fetch: cachingFetch },
176+
log: Object.assign(() => undefined, {
177+
warn: () => undefined,
178+
error: () => undefined
179+
})
180+
}
181+
return makeHoudiniPlugin((opts as unknown) as EdgeCorePluginOptions)
182+
}
183+
184+
describe('houdini swap quote + exchange', function () {
185+
it('BTC -> ETH (private)', async function () {
186+
this.timeout(60000) // Live capture is slow; replay from fixtures is instant.
187+
const plugin = makePlugin()
188+
const request: EdgeSwapRequest = {
189+
fromWallet: btcWallet,
190+
toWallet: ethWallet,
191+
fromTokenId: null,
192+
toTokenId: null,
193+
nativeAmount: '1000000', // 0.01 BTC
194+
quoteFor: 'from'
195+
}
196+
197+
const quote = await plugin.fetchSwapQuote(request, undefined, {
198+
infoPayload: {}
199+
})
200+
201+
assert.equal(quote.pluginId, 'houdini')
202+
assert.isTrue(
203+
quote.toNativeAmount !== '0' && quote.toNativeAmount.length > 0
204+
)
205+
assert.equal(quote.fromNativeAmount, '1000000')
206+
})
207+
208+
it('ETH -> USDC (private)', async function () {
209+
this.timeout(60000) // Live capture is slow; replay from fixtures is instant.
210+
const plugin = makePlugin()
211+
const request: EdgeSwapRequest = {
212+
fromWallet: ethWallet,
213+
toWallet: ethWallet,
214+
fromTokenId: null,
215+
toTokenId: USDC_TOKEN_ID,
216+
nativeAmount: '50000000000000000', // 0.05 ETH
217+
quoteFor: 'from'
218+
}
219+
220+
const quote = await plugin.fetchSwapQuote(request, undefined, {
221+
infoPayload: {}
222+
})
223+
224+
assert.equal(quote.pluginId, 'houdini')
225+
assert.isTrue(
226+
quote.toNativeAmount !== '0' && quote.toNativeAmount.length > 0
227+
)
228+
assert.equal(quote.fromNativeAmount, '50000000000000000')
229+
})
230+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"status": 200,
3+
"body": "{\"total\":208,\"quotes\":[{\"amountIn\":0.01,\"amountOut\":0.37271038265600004,\"min\":0.00047,\"max\":141.56,\"duration\":9,\"amountOutUsd\":623.9014356135652,\"quoteId\":\"6a2b3b2ca5f07ecb32c972bd\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.01,\"amountOut\":0.37245555,\"min\":0.001595569,\"max\":9007199254740991,\"duration\":37,\"amountOutUsd\":623.4748565126918,\"quoteId\":\"6a2b3b2ea5f07ecb32c973a9\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.01,\"amountOut\":0.372360226348,\"min\":0.0015745613862438,\"max\":14.181258317046,\"duration\":33,\"amountOutUsd\":623.3152887461408,\"quoteId\":\"6a2b3b2ca5f07ecb32c972b9\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.01,\"amountOut\":0.37232770485,\"min\":0.0015744975483105,\"max\":15.437872232434,\"duration\":29,\"amountOutUsd\":623.2608491323423,\"quoteId\":\"6a2b3b2da5f07ecb32c972d0\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.01,\"amountOut\":0.372320611682,\"min\":0.0000255,\"max\":9007199254740991,\"duration\":30,\"amountOutUsd\":623.2489754687574,\"quoteId\":\"6a2b3b2da5f07ecb32c97307\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null}]}"
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"status": 200,
3+
"body": "{\"total\":64,\"quotes\":[{\"amountIn\":0.05,\"amountOut\":82.104448,\"min\":0.0178953835875472,\"max\":9007199254740991,\"duration\":18,\"amountOutUsd\":82.104448,\"quoteId\":\"6a2b3b8da3ad2fca3a860d3a\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":82.021967,\"min\":0.0178953835875472,\"max\":9007199254740991,\"duration\":25,\"amountOutUsd\":82.021967,\"quoteId\":\"6a2b3b8da3ad2fca3a860d39\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":81.98153,\"min\":0.0181619661,\"max\":9007199254740991,\"duration\":5,\"amountOutUsd\":81.98153,\"quoteId\":\"6a2b3b8da3ad2fca3a860d36\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":81.955072,\"min\":0.01789,\"max\":5353.84,\"duration\":4,\"amountOutUsd\":81.955072,\"quoteId\":\"6a2b3b8da3ad2fca3a860d03\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null},{\"amountIn\":0.05,\"amountOut\":81.950096,\"min\":0.0181619661,\"max\":9007199254740991,\"duration\":10,\"amountOutUsd\":81.950096,\"quoteId\":\"6a2b3b8da3ad2fca3a860d00\",\"type\":\"private\",\"rewardsAvailable\":true,\"rateId\":null}]}"
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"status": 200,
3+
"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 <a href=\\\"https://www.coingecko.com/en?hashing_algorithm=SHA-256\\\">SHA-256</a> 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 <a href=\\\"https://www.coingecko.com/en/coins/litecoin\\\">Litecoin</a>, <a href=\\\"https://www.coingecko.com/en/coins/peercoin\\\">Peercoin</a>, <a href=\\\"https://www.coingecko.com/en/coins/primecoin\\\">Primecoin</a>, and so on.\\r\\n\\r\\nThe cryptocurrency then took off with the innovation of the turing-complete smart contract by <a href=\\\"https://www.coingecko.com/en/coins/ethereum\\\">Ethereum</a> which led to the development of other amazing projects such as <a href=\\\"https://www.coingecko.com/en/coins/eos\\\">EOS</a>, <a href=\\\"https://www.coingecko.com/en/coins/tron\\\">Tron</a>, and even crypto-collectibles such as <a href=\\\"https://www.coingecko.com/buzz/ethereum-still-king-dapps-cryptokitties-need-1-billion-on-eos\\\">CryptoKitties</a>.\",\"mainnet\":true,\"enabled\":true,\"unverified\":false,\"hasDex\":true,\"hasCex\":true,\"hasSelfPrivate\":true,\"cexTokenId\":\"BTC\",\"rank\":1,\"cgId\":\"bitcoin\",\"marketCapChange24h\":3.31092,\"circulatingSupply\":20041500,\"price\":63402.6175,\"marketCap\":1271652345828,\"volume\":30162375081,\"fdv\":1271653107240,\"change\":3.35559,\"id\":\"6689b73ec90e45f3b3e51551\",\"icon\":\"https://api.houdiniswap.com/assets/tokens/BTC-BTC-v79cxh.png\"}],\"total\":1,\"totalPages\":1}"
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"status": 200,
3+
"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.11586,\"circulatingSupply\":74888233656.75256,\"price\":0.99979,\"marketCap\":74870523975,\"volume\":13383365572,\"fdv\":74889088908,\"change\":-0.00444,\"id\":\"6689b73ec90e45f3b3e51554\",\"icon\":\"https://api.houdiniswap.com/assets/tokens/USDC-ETH-7b8for.png\"}],\"total\":1,\"totalPages\":1}"
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"status": 200,
3+
"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\":3.5828,\"circulatingSupply\":120684325.0427515,\"price\":1673.95775553,\"marketCap\":202173267522,\"volume\":12046932167,\"fdv\":202173267522,\"change\":3.59651,\"priority\":2,\"id\":\"6689b73ec90e45f3b3e51566\",\"icon\":\"https://api.houdiniswap.com/assets/tokens/ETH-ETH-lejbdy.png\"}],\"total\":1,\"totalPages\":1}"
4+
}

0 commit comments

Comments
 (0)