Appearance
swap-sdk internals
The aggregation engine. One class per provider, a factory that fans out over them, and a relayer builder that turns a route into a signed meta-transaction.
Version: 1.12.0 · Runtime: ESM, ethers v6 · Embedded secrets: none
Entry point
src/index.ts exports TradeManager, relayerAddresses and configure.
The constructor registers every enabled aggregator in a shared AggregatorFactory:
ts
this.aggregatorFactory.register(AGGREGATORS.ONE_INCH, new OneInchAggregator())
this.aggregatorFactory.register(AGGREGATORS.LIFI, new LifiAggregator())
// … 21 registrationsRegistration is where providers get disabled
Four are commented out in-place with the reason:
| Provider | Comment |
|---|---|
NITRO (Router Protocol) | Mainnet pathfinder hostnames retired — api-beta.pathfinder.routerprotocol.com is NXDOMAIN |
ICECREAM_SWAP | aggregator.icecreamswap.com returns 404; endpoint appears removed |
KIMA | kima.blazpay.com origin down (Cloudflare 520) |
CHANGE_NOW | Commented out |
src/index.ts is the only authority on what is live
26 aggregator classes exist; 21 are registered. Counting files in src/aggregators/ overstates coverage, and marketing tables go stale. Read the constructor.
Public API
| Method | Purpose |
|---|---|
getQuotes(params) | Fan out to all registered aggregators in parallel, honouring excludeSwap / excludeBridge. Streams via onNewQuote, fires onLastQuote(true) when all settle |
sortQuotes(provider, relayerTxs[]) | Simulate executeMetaTransactionSwap.estimateGas per candidate; keep only non-reverting ones |
simulateTx(provider, relayerTxData) | Fetch a server signature from ${baseUrl}/sign, then staticCall the relayer |
triggerTransaction(provider, relayerTxData) | Same, then send and await tx.wait() |
sendSignTxDataRaw(relayerTxData) | Headless — returns { approvalData, executeData, value, to } with no BrowserProvider |
getTransactionStatus(queries[]) | Per-provider getTxStatus(chainId, hash) |
AggregatorFactory
src/aggregator.factory.ts — a Map<name, aggregator> and two fan-out methods.
ts
getQuotes: Promise.all(filtered.map(a => a.getQuotes(params))) // errors swallowed per aggregator
getStatus: Promise.all(items.map(({provider, chainId, hash}) => …))Swallowing per-aggregator errors is load-bearing
One provider returning a 500 must not kill an SSE stream that eleven others are still writing to. The cost is that a permanently-broken provider disappears silently — nothing alerts, the user just sees fewer routes. See monitoring.
The aggregator contract
Every provider extends Base and implements three methods:
ts
getQuotes(params: IQuoteParams): Promise<Quote | Quote[]>
getTransactionData(data, restProps, meta?): Promise<{ tx, spender, metaData? }>
getTxStatus(chainId, hash): Promise<{ status: 'pending'|'success'|'failed', hash }>getTransactionData must produce tx.to, tx.data and tx.value, because that is what RelayerFactory wraps into a MetaTransaction.
Base helpers
src/aggregators/base.aggregator.ts:
| Member | Purpose |
|---|---|
senderAddress, senderTronNitro | Wallet context |
tronFeeLimit | Default 1_000_000_000 |
setSenderAddress(address) | Setter |
getGasPrice(chainId) | OpenOcean v4 gasPrice endpoint; sends apikey when openOceanApiKey is configured |
isNativeAddresss(address) | Recognises 0x0…0, 0xeee…eee, and Polygon's 0x…1010. Triple s is in the source |
Two ways to reach a provider
| Pattern | Providers | Why |
|---|---|---|
| Direct | LiFi, Butter, KyberSwap, Symbiosis, Across, Relay, deBridge, Rango, XY Finance, Houdini, GasZip, RhinoFi, Near 1Click, Nordstern, SushiSwap, Nitro, IceCream | Public API, no secret needed |
| Backend-proxied | 1inch, OpenOcean, Unizen, Squid, OKX, Uniswap, ChangeNow, Changelly | Needs an API key or a registered referrer that must not ship in a browser bundle |
Proxied providers call ${baseUrl}/1inch, ${baseUrl}/openocean, ${baseUrl}/unizen/…, ${baseUrl}/squidrouter, ${baseUrl}/okx/…, ${baseUrl}/uniswap/…, ${baseUrl}/change-now/….
baseUrl resolution
src/utils/constants.ts resolves it at module load:
ts
const PROD_API_URL = 'https://api.blazpay.com/api/defi'
const LOCAL_API_URL = 'http://localhost:5000/api/defi'
// browser: local only for localhost / 127. / 192.168.
// Node: always prod (localhost would be wrong for any deployed consumer)Consumers override with configure({ baseApiUrl }).
Kima — the odd one out
Kima.aggregator.ts uses its own backend (process.env.KIMA_BACKEND_URL || 'https://kima.blazpay.com') with /trade/quote, /trade/submit, /trade/status/:hash. It returns tx: null because Kima submits server-side; the frontend then polls getTxStatus.
That is the template for any future provider that broadcasts itself: return tx: null plus a metaData payload, and add a dedicated sendTransaction<Provider> branch on the frontend instead of routing through the relayer.
Quote
src/utils/quote.ts — a thin wrapper with an important split:
ts
class Quote {
data: any // raw provider response — never sent to the browser
meta: IQuote // small JSON serialised over SSE
restProps: IRestQuoteProps // quotePayload, chains, wallets, slippageTolerance
getTransactionData() // dispatches to the right aggregator
}restProps exists because the second round-trip (GET /defi/swap/:id) needs context that meta does not carry. data exists because most provider build endpoints require echoing back their own quote object.
RelayerFactory
src/relayer.ts. Uses relayerAbi and relayerAddresses(chainId) from utils/constants.ts.
Builds a MetaTransaction from IRelayerTxData, fetches a signature from POST ${baseUrl}/sign, computes txValue, estimates gas, and then either staticCalls or sends.
js
{
targetContract: tx.to,
data: tx.data,
recipient: spender || 0x0, // field name is 'recipient', value is the spender
amount, token,
isNative: token === 0x0 || 0xeee…,
nonce: await relayer.nonces(user),
deadline: now + 100,
nativeValue: tx.value,
}getMetaTransactionByteData is the headless variant.
Chains and addresses
src/utils/constants.ts holds:
| Export | Contents |
|---|---|
ChainName | 38 slugs, ethereum … robinhood |
ChainId | The matching numeric ids |
getChainNameById(chainId) | Lookup |
RELAYER_ADDRESSES | chainId → proxy address, 22 entries |
RELAYER_DEPLOYED_CHAINS | ReadonlySet<number> of the same 22 |
relayerAddresses(chain) | Throws a descriptive error when absent |
isRelayerDeployed(chain) | Boolean |
assertRelayerDeployed(chain) | Throws — "Swap blocked to prevent loss of funds" |
ChainContractAddress | Legacy per-chain product addresses |
ChainIdUnizen | Unizen's supported subset |
routers | Per-aggregator status API URLs |
addressZero, addressE | Native sentinels |
Why there are four distinct relayer addresses
| Address | Chains | Reason |
|---|---|---|
0x7d98E59FabFBaDD5eCB61CC2cf876AA97f505531 | 137, 146, 204, 480, 999, 1329, 1868, 2020, 33139, 42220, 43111, 80094 | The v2 cohort — deterministic CREATE from TX_SIGNER at the same nonce |
0x5c23c9a42626Ade38ae1c9a3407096d4381EE6E6 | 56, 42161, 59144, 534352 | Pre-existing, upgraded in place |
0xb8Bd470f3C2610F83025D049085A64f1C7b78F14 | 10, 8453, 43114, 2741, 4663 | Pre-existing, plus Abstract (zkSync-stack CREATE differs) and Robinhood |
0xA01da2d3AbEFFbaa347B08C76EEC47169DdE72e9 | 130 | Unichain — the deployer's nonce had drifted before deploy |
assertRelayerDeployed exists to prevent loss of funds
Earlier versions fell back to a default address for unknown chains. On a chain with no deployment that address has no code, so the transaction would succeed at the EVM level while doing nothing — and the user's tokens would already have been approved. The hard failure is the feature.
Configuration
src/utils/config.ts:
ts
configure({
openOceanApiKey: '…',
unizenApiKey: '…',
baseApiUrl: 'https://api.blazpay.com/api/defi',
})The SDK ships with no keys. Anything key-bearing either goes through the backend proxy or is injected here.
Full file map
src/
index.ts TradeManager
aggregator.factory.ts Map + parallel fan-out
relayer.ts RelayerFactory
enums/aggregator.enum.ts AGGREGATORS (26)
aggregators/
base.aggregator.ts
Across ButterNetwork ChangeNow DeBridge GasZip Houdini IceCream
Kima KyberSwap Lifi NearOneClick Nitro Nordstern OKX Odos OneInch
OpenOcean Rango Relay RhinoFi SquidRouter Sushi Symbiosis Uniswap
Unizen XYFinance
utils/
constants.ts config.ts quote.ts axios.ts helper.ts types.ts
jsons/relayerAbi.ts
@types/
aggregator.type.ts chain.type.ts quote.type.ts relayer.type.ts
index.ts globals.d.tsAdding a provider
Checklist: add a liquidity provider. There is also a self-contained integration guide in the SDK repo itself at swap-sdk/INTEGRATION_GUIDE.md.
Gotchas
| Gotcha | Detail |
|---|---|
| ethers v6 | Top-level exports, bigint, JsonRpcProvider — not v5's ethers.utils.* and BigNumber. Both consumers use v5 |
| ESM with explicit extensions | Every relative import ends in .js, even from .ts sources |
prepare runs tsc | A git install builds automatically; a broken build breaks consumers at install time |
isNativeAddresss | Triple s. Do not rename without updating every call site |
MetaTransaction.recipient | Holds the spender, not the end user |
| Errors are swallowed per aggregator | Good for resilience, bad for observability |