Skip to content

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.0

Bumping 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-only

and 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 aggregator

getQuotes(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

MemberPurpose
senderAddressEVM wallet
senderTronNitroTRON wallet (base58)
tronFeeLimitDefault 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

ExportContents
ChainName36 slugs
ChainIdMatching numeric ids
getChainNameById(chainId)Lookup — returns undefined for Hemi (43111), which is missing from the enums despite having a relayer
RELAYER_ADDRESSESchainId → proxy, 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 URLs
addressZero, addressENative sentinels
relayerJsonThe 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

GotchaDetail
ethers v6Top-level exports, bigint, JsonRpcProvider. Both consumers use v5 — expect to rewrite every ethers call when moving code
Explicit .js extensionsEvery relative import ends in .js, even from .ts sources
prepare builds on installA broken tsc breaks consumers
isNativeAddresssTriple s; do not rename without updating every call site
MetaTransaction.recipientHolds the spender, not the end user
Errors swallowed per aggregatorResilient, but a permanently-broken provider disappears silently
Hemi missing from the enumsHas a relayer, no slug
deadline = now + 100Seconds. A slow wallet prompt produces "Transaction expired"

Release process

swap-sdk releases.

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