Appearance
swap-sdk reference
Package: swap-sdk · Version: 1.12.0 · Install: github:blazpay/swap-sdk#vX.Y.Z
Runtime: ESM, ethers v6, TronWeb · Embedded secrets: none
Internals and design rationale: swap-sdk internals.
Installation
bash
npm install github:blazpay/swap-sdk#v1.12.0Bumping the pin also requires updating the lockfile
package-lock.json pins by resolved commit, and npm install obeys it even after rm -rf node_modules/swap-sdk. Run:
bash
npm install github:blazpay/swap-sdk#vX.Y.Z --package-lock-onlyand commit both files. This has caused a production deploy to install 1.9.1 despite a v1.12.0 pin.
prepare runs tsc, so a git install builds automatically — and a broken build breaks consumers at install time.
Exports
ts
import { TradeManager, relayerAddresses, configure } from 'swap-sdk'
import { AGGREGATORS } from 'swap-sdk/dist/enums/aggregator.enum.js'
import Quote from 'swap-sdk/dist/utils/quote.js'configure(options)
Call once at startup. The SDK ships with no keys.
ts
configure({
openOceanApiKey?: string, // also powers Base.getGasPrice
unizenApiKey?: string,
rangoApiKey?: string,
houdiniApiKey?: string,
baseApiUrl?: string, // proxy base, default https://api.blazpay.com/api/defi
})baseApiUrl resolution when not set: in a browser, localhost only for hosts matching localhost, 127. or 192.168.; in Node, always production.
TradeManager
ts
const trade = new TradeManager() // registers every enabled aggregatorgetQuotes(params)
ts
interface IBaseQuoteParams {
fromChain: IChain
toChain: IChain
fromToken: IToken
toToken: IToken
amount: number // human units
slippage?: number // percent, default 0.5
srcWalletAddress: string
dstWalletAddress?: string
type: 'SWAP' | 'BRIDGE'
excludeSwap?: AGGREGATORS[]
excludeBridge?: AGGREGATORS[]
onNewQuote: (quote: Quote) => Promise<void>
onLastQuote: (isLastQuote: boolean) => boolean
}Fans out to every registered aggregator in parallel. onNewQuote fires per result; onLastQuote(true) when all have settled. Per-aggregator errors are swallowed so one failure cannot kill the stream.
dstWalletAddress semantics differ between callers
For an interactive trade it is the user's wallet. For a vault-executed trade it must be the executing contract, so the vault can measure its own balance delta. Pointing it at the user makes the vault see zero output and revert. See execution engine.
sortQuotes(provider, relayerTxs[])
Runs relayerContract.executeMetaTransactionSwap.estimateGas per candidate and keeps only the non-reverting ones.
simulateTx(provider, relayerTxData)
Fetches a server signature from ${baseUrl}/sign, then staticCalls the relayer.
triggerTransaction(provider, relayerTxData)
Same, then sends and await tx.wait().
sendSignTxDataRaw(relayerTxData)
Headless — returns { approvalData, executeData, value, to } with no BrowserProvider. Used by agent flows and server-built transactions.
getTransactionStatus(queries[])
ts
getTransactionStatus([{ provider, chainId, hash }, …])
// → [{ status: 'pending' | 'success' | 'failed', hash }, …]Uses the routers map in utils/constants.ts for each provider's status URL.
IQuote — the meta object
The only part that reaches a browser.
ts
interface IQuote {
id: string // Redis cache key, 5-minute TTL
aggregator: string
route: string
amount: number // output, base units
usdAmount: number
networkFee: number | string // source gas + bridge gas, USD. 0 when unknown
platformFee: number | string // aggregator/relayer/bridge protocol fees, USD
blazpayFeePercent?: number // 0.001 === 0.1%
blazpayFeeUsd?: number
priceImpact: number
slippage: number
allowanceTo: string
timeEstimate?: number // seconds; omitted for single-chain swaps
}blazpayFeePercent is populated inconsistently, and that is documented in the source
Cross-chain providers that execute through the relayer populate it. Pure single-chain DEX swaps surface it from on-chain at execute time instead. usdAmount is 0 from OKX and Nordstern — which is why bz-backend's price enrichment exists.
Quote
ts
class Quote {
data: any // raw provider response — never sent to a browser
meta: IQuote // the small serialised object
restProps: IRestQuoteProps
getMeta(): IQuote
getTransactionData(): Promise<{ tx, spender, metaData? }>
toJSON(): any // returns `data`
}ts
interface IRestQuoteProps {
srcWalletAddress: string
slippageTolerance: number
fromChain: { id: number; name: string }
toChain: { id: number; name: string }
dstWalletAddress?: string
quotePayload?: any
type?: 'SWAP' | 'BRIDGE'
}getTransactionData() looks the aggregator up by meta.aggregator and delegates. That is why the backend caches all three fields — it must reconstruct a Quote to build the transaction.
IRelayerTxData
ts
interface IRelayerTxData {
tx: { data: string; to: string; value: string; from?: string }
spender: string
amount: number
token: string
isNative: boolean
quote?: any
}
interface ITokenAndWalletInfo {
userAddress: string
decimals: number
chainId: number
}
interface IRelayerRawTxData extends IRelayerTxData, ITokenAndWalletInfo {}The aggregator contract
To add a provider, implement three methods on a class extending Base:
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 — that is what RelayerFactory wraps into a MetaTransaction. A provider that broadcasts itself returns tx: null plus metaData (the Kima pattern).
Base helpers
| Member | Purpose |
|---|---|
senderAddress | EVM wallet |
senderTronNitro | TRON wallet (base58) |
tronFeeLimit | Default 1_000_000_000 |
setSenderAddress(address) | Setter |
getGasPrice(chainId) | OpenOcean v4 gasPrice; sends apikey when configured |
isNativeAddresss(address) | Recognises 0x0…0, 0xeee…eee, Polygon 0x…1010. Triple s |
AGGREGATORS
ts
enum AGGREGATORS {
ONE_INCH = 'one_inch', NITRO = 'nitro',
CHANGE_NOW = 'change_now', OPEN_OCEAN = 'open_ocean',
SYMBIOSIS = 'symbiosis', UNIZEN = 'unizen',
ICECREAM_SWAP = 'icecream_swap', KYBER_SWAP = 'kyber_swap',
LIFI = 'lifi', BUTTER_NETWORK = 'butter_network',
SQUID_ROUTER = 'squid_router', KIMA = 'kima',
NORDSTERN = 'nordstern', ODOS = 'odos',
RELAY = 'relay', SUSHISWAP = 'sushiswap',
ACROSS = 'across', NEAR_1CLICK = 'near_1click',
GAS_ZIP = 'gas_zip', RHINO_FI = 'rhino_fi',
OKX = 'okx', RANGO = 'rango',
DEBRIDGE = 'debridge', XY_FINANCE = 'xy_finance',
HOUDINI = 'houdini', UNISWAP = 'uniswap',
}26 members; 21 registered. NITRO, ICECREAM_SWAP, KIMA and CHANGE_NOW are commented out in TradeManager's constructor with the reason inline. See providers.
Chain constants
| Export | Contents |
|---|---|
ChainName | 36 slugs |
ChainId | Matching numeric ids |
getChainNameById(chainId) | Lookup — returns undefined for Hemi (43111), which is missing from the enums despite having a relayer |
RELAYER_ADDRESSES | chainId → proxy, 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 URLs |
addressZero, addressE | Native sentinels |
relayerJson | The relayer ABI |
assertRelayerDeployed is a safety feature, not an inconvenience
Earlier versions fell back to a default address on unknown chains. That address has no code there, so the transaction succeeds at the EVM level while doing nothing — after the user's tokens were already approved. Hard failure is correct.
Minimal usage
ts
import { TradeManager, configure, relayerAddresses } from 'swap-sdk'
configure({ baseApiUrl: 'https://api.blazpay.com/api/defi' })
const trade = new TradeManager()
const quotes: Quote[] = []
await trade.getQuotes({
fromChain, toChain, fromToken, toToken,
amount: 1.5,
srcWalletAddress: user,
dstWalletAddress: user,
type: 'SWAP',
slippage: 0.5,
onNewQuote: async (q) => { quotes.push(q) },
onLastQuote: () => true,
})
quotes.sort((a, b) => b.meta.amount - a.meta.amount)
const { tx, spender } = await quotes[0].getTransactionData()
// approve relayerAddresses(chainId) for amount + fee, then:
await trade.triggerTransaction(provider, {
tx, spender, amount, token, isNative,
userAddress: user, decimals, chainId,
})Gotchas
| Gotcha | Detail |
|---|---|
| ethers v6 | Top-level exports, bigint, JsonRpcProvider. Both consumers use v5 — expect to rewrite every ethers call when moving code |
Explicit .js extensions | Every relative import ends in .js, even from .ts sources |
prepare builds on install | A broken tsc breaks consumers |
isNativeAddresss | Triple s; do not rename without updating every call site |
MetaTransaction.recipient | Holds the spender, not the end user |
| Errors swallowed per aggregator | Resilient, but a permanently-broken provider disappears silently |
| Hemi missing from the enums | Has a relayer, no slug |
deadline = now + 100 | Seconds. A slow wallet prompt produces "Transaction expired" |