Skip to content

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 registrations

Registration is where providers get disabled

Four are commented out in-place with the reason:

ProviderComment
NITRO (Router Protocol)Mainnet pathfinder hostnames retired — api-beta.pathfinder.routerprotocol.com is NXDOMAIN
ICECREAM_SWAPaggregator.icecreamswap.com returns 404; endpoint appears removed
KIMAkima.blazpay.com origin down (Cloudflare 520)
CHANGE_NOWCommented 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

MethodPurpose
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:

MemberPurpose
senderAddress, senderTronNitroWallet context
tronFeeLimitDefault 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

PatternProvidersWhy
DirectLiFi, Butter, KyberSwap, Symbiosis, Across, Relay, deBridge, Rango, XY Finance, Houdini, GasZip, RhinoFi, Near 1Click, Nordstern, SushiSwap, Nitro, IceCreamPublic API, no secret needed
Backend-proxied1inch, OpenOcean, Unizen, Squid, OKX, Uniswap, ChangeNow, ChangellyNeeds 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:

ExportContents
ChainName38 slugs, ethereumrobinhood
ChainIdThe matching numeric ids
getChainNameById(chainId)Lookup
RELAYER_ADDRESSESchainId → proxy address, 22 entries
RELAYER_DEPLOYED_CHAINSReadonlySet<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"
ChainContractAddressLegacy per-chain product addresses
ChainIdUnizenUnizen's supported subset
routersPer-aggregator status API URLs
addressZero, addressENative sentinels

Why there are four distinct relayer addresses

AddressChainsReason
0x7d98E59FabFBaDD5eCB61CC2cf876AA97f505531137, 146, 204, 480, 999, 1329, 1868, 2020, 33139, 42220, 43111, 80094The v2 cohort — deterministic CREATE from TX_SIGNER at the same nonce
0x5c23c9a42626Ade38ae1c9a3407096d4381EE6E656, 42161, 59144, 534352Pre-existing, upgraded in place
0xb8Bd470f3C2610F83025D049085A64f1C7b78F1410, 8453, 43114, 2741, 4663Pre-existing, plus Abstract (zkSync-stack CREATE differs) and Robinhood
0xA01da2d3AbEFFbaa347B08C76EEC47169DdE72e9130Unichain — 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.ts

Adding 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

GotchaDetail
ethers v6Top-level exports, bigint, JsonRpcProvider — not v5's ethers.utils.* and BigNumber. Both consumers use v5
ESM with explicit extensionsEvery relative import ends in .js, even from .ts sources
prepare runs tscA git install builds automatically; a broken build breaks consumers at install time
isNativeAddresssTriple s. Do not rename without updating every call site
MetaTransaction.recipientHolds the spender, not the end user
Errors are swallowed per aggregatorGood for resilience, bad for observability

Internal + developer documentation. Usage figures are point-in-time snapshots — re-verify before publishing them.