Web SDK for Stellar swaps against uswap-server. It aggregates six swap sources — four Stellar-native providers plus NEAR and Axelar ITS for cross-chain routes — behind one API and runs execution client-side:
| Provider | Execution | minBuyAmount |
Third-party recipient |
|---|---|---|---|
| STELLARBROKER | interactive WebSocket session | null (no on-chain floor) |
no |
| SOROSWAP | signed transaction | enforced | yes |
| AQUARIUS | signed transaction | enforced | no |
| STELLAR_DEX | signed transaction | enforced | yes |
| NEAR (cross-chain) | deposit to address | enforced | yes |
| AXELAR_ITS (cross-chain) | signed transaction | enforced (1:1 bridge) | yes |
The SDK implements the StellarBroker-first waterfall. Pricing, committing, and tracking go
through uswap-server (/v2/rate, /v2/swap, /v2/track), but execution is client-side and does
not always go through the server:
- Soroswap / Aquarius / Stellar DEX — the SDK signs the server-built envelope and submits it to Horizon itself.
- StellarBroker — the SDK talks to the broker directly.
/v2/swaponly hands back the session parameters (assets, amount, slippage, partner key); the SDK then opens its own WebSocket towss://api.stellar.broker/wsand runs the trade — uswap-server never proxies the session. (SB's REST quote endpoint is likewise public/unauthenticated; the SDK still sources quotes through/v2/rateso all four providers compare under one waterfall.)
Every StellarBroker transaction is built by the broker, so the SDK runs a full signing-security pipeline (shape validation, cryptographic trader-signature detection, per-tx debit budget, classic fee-bumps, and Soroban two-phase auth-entry signing) before signing each one.
It mirrors the finished iOS reference implementation and the contract in
docs/STELLAR_WEB_SDK_GUIDE.md and uswap-server/API.md.
npm install stellar-web-sdk @stellar/stellar-sdkRuntime requirements: fetch (Node 18+/browser) and, for StellarBroker sessions, WebSocket
(Node 22+/browser). Both can be injected via config for older runtimes.
import { StellarSwapSDK, keypairSigner } from 'stellar-web-sdk'
const sdk = new StellarSwapSDK({
apiBaseUrl: 'https://swap-dev.unstoppable.money/api', // uswap-server base (parent of /v2)
apiKey: process.env.USWAP_API_KEY!, // SDK-specific key — ask esen
// horizonUrl defaults to https://horizon.stellar.org; networkPassphrase defaults to PUBLIC
})
const trader = 'GTRADER…'
const signer = keypairSigner(process.env.STELLAR_SECRET!) // or your own StellarSigner (see below)
// 1) Quote — fans out to the right providers and applies the SB-first waterfall.
const quote = await sdk.quote({
sellAsset: 'XLM.XLM',
buyAsset: 'XLM.USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN',
sellAmount: '100',
slippage: 1, // PERCENT (1 = 1%)
sourceAddress: trader, // destination defaults to source
})
if (!quote.route) throw new Error('no route')
// 2) Trustline gate — buying a classic asset the recipient doesn't trust is rejected on-chain.
const trust = await sdk.checkTrustline(trader, quote.route.buyAsset)
if (trust.required) {
await sdk.activateTrustline(signer, quote.route.buyAsset) // submits a changeTrust, then re-quote
}
// 3) Commit against the picked provider (creates the order + returns execution + uuid).
const route = await sdk.commit({
sellAsset: 'XLM.XLM',
buyAsset: quote.route.buyAsset,
sellAmount: '100',
slippage: 1,
sourceAddress: trader,
provider: quote.provider!, // carry the waterfall's pick so price matches
})
// 4) Execute (dispatches on execution.method) and track — one call.
const { execution, track } = await sdk.executeAndTrack(route, signer, {
callbacks: {
onQuote: (q) => console.log('live SB quote', q.estimatedBuyingAmount),
onProgress: (p) => console.log('progress', p),
},
})
console.log('tracking hash', execution.inboundTxHash, 'status', track?.status)
// 5) Poll to completion.
const final = await sdk.pollTrack(route.uuid, execution.inboundTxHash, {
onUpdate: (s) => console.log(s.status),
})React bindings live at the stellar-web-sdk/react entry point (react >= 18 is an optional peer
dependency). Everything there is 'use client' — the SDK holds live fetch/WebSocket handles, so
mount it under a client boundary. The core entry (stellar-web-sdk) touches no browser globals at
import time, so it's safe to import in React Server Components / the Next.js App Router.
Wrap your tree once, then drive the lifecycle with hooks:
'use client'
import { StellarSwapProvider, useQuote, useExecuteSwap } from 'stellar-web-sdk/react'
import { keypairSigner } from 'stellar-web-sdk'
// Give the provider a STABLE config (module const / useMemo) or a prebuilt `sdk` instance.
const config = { apiBaseUrl: '/api/uswap', apiKey: '…' }
function App() {
return (
<StellarSwapProvider config={config}>
<Swap />
</StellarSwapProvider>
)
}
function Swap() {
const q = useQuote() // { quote, data, error, isLoading, reset }
const swap = useExecuteSwap() // commit → execute → track, + live broker state
return (
<>
<button onClick={() => q.quote({ sellAsset: 'native', buyAsset: 'USDC:GA5Z…', sellAmount: '10', slippage: 1, sourceAddress: 'GTRADER…' })}>
Quote
</button>
<button
disabled={!q.data?.provider || swap.isLoading}
onClick={() => swap.swap({
sellAsset: 'native', buyAsset: 'USDC:GA5Z…', sellAmount: '10', slippage: 1,
sourceAddress: 'GTRADER…', provider: q.data!.provider!, signer: keypairSigner('S…'),
})}
>
{swap.isLoading ? swap.status : 'Swap'}
</button>
{/* broker routes stream live state: swap.brokerPhase / swap.brokerQuote / swap.brokerProgress */}
</>
)
}Hooks: useStellarSwap() (the SDK from context), useQuote(), useExecuteSwap(), and
useTrackStatus(uuid, hash?, opts?) for standalone polling. useQuote/useExecuteSwap drop
superseded/aborted results, so rapid re-quoting never flashes a stale price.
Keep your API key server-side. A
NEXT_PUBLIC_key ships to the browser. In production pointapiBaseUrlat a Next.js route handler that injectsX-API-Keyand proxies uswap-server (StellarBroker WebSocket sessions still run directly from the browser). A runnable App Router example — including that proxy pattern — is inexamples/nextjs.
Build the React entry with npm run build:all (or build:react); the default npm run build only
emits the framework-agnostic core, so it stays green without React installed.
Runnable examples of the full swap flow live in examples/: vanilla JS (core SDK,
no framework), React (these hooks on a plain esbuild bundle), and Next.js (App Router,
including the key-proxy pattern).
sdk.quote() implements the policy from the guide exactly:
/v2/rateacross the eligible providers.- If a
STELLARBROKERroute exists → pick it, even when a fallback shows a higher number (SB's number is an estimate and it usually wins after execution — the grant's agreed policy). - Else → the fallback with the greatest
expectedBuyAmount. - Nothing → no route.
If destinationAddress differs from sourceAddress, the fan-out is automatically restricted to the
recipient-capable providers (SOROSWAP, STELLAR_DEX) — SB and AQUARIUS settle on the trader's own
account.
quote()/commit()/execute() handle every path in one flow, routed automatically from the
assets:
- Stellar-native pair (both assets Stellar) → the four Stellar in-chain providers (SB-first waterfall).
- Axelar ITS pair — the same token bridged Stellar ↔ Ethereum (
XLM.XLM↔ETH.XLM-0x…,SHX↔SHX) → the AXELAR_ITS provider (a signed Stellar tx, so the normalexecute()signs & submits it). - any other cross-chain pair → the NEAR provider (1Click), deposit-to-address.
You don't choose up front — read QuoteResult.crossChain. Cross-chain assets are CHAIN.TICKER-ADDRESS
identifiers from each provider's catalog (crossChainTokens('NEAR' | 'AXELAR_ITS')).
// (optional) discover cross-chain assets — never hand-build identifiers.
const tokens = await sdk.crossChainTokens('NEAR') // [{ identifier: 'ETH.ETH', decimals: 18, … }]
// 1) One quote — auto-routes. `sellAsset`/`buyAsset` are Stellar (XLM.XLM) or cross-chain (ETH.USDC-0x…).
const q = await sdk.quote({
sellAsset: 'XLM.XLM',
buyAsset: 'ETH.USDC-0XA0B86991C6218B36C1D19D4A2E9EB0CE3606EB48', // cross-chain here → NEAR
sellAmount: '500',
slippage: 1, // PERCENT
sourceAddress: 'GTRADER…',
destinationAddress: '0x…', // destination-chain address (cross-chain / third-party)
})
q.crossChain // true → routed via NEAR; false → Stellar in-chain
// 2) Commit the picked provider (refundAddress defaults to sourceAddress for cross-chain).
const route = await sdk.commit({ ...quoteParams, provider: q.provider! })
// 3) Execute — dispatches on execution.method. Stellar origin + signer signs & submits the deposit;
// any other origin returns the deposit instruction to send yourself.
const exec = await sdk.execute(route, signer)
if (exec.method === 'transfer' && !exec.submitted) {
// exec.deposit = { chain, depositAddress, amount, asset, attachment: { type, value } }
// send it from your wallet, then sdk.track(route.uuid, hash)
} else {
await sdk.track(route.uuid, exec.inboundTxHash) // then pollTrack to completion
}Cross-chain routes carry execution.method === 'transfer' (see TransferExecution). crossChainTokens()
lists cross-chain assets and depositFor(route) returns the deposit without submitting.
Key custody stays with the caller. The SDK only ever asks for a raw ed25519 signature over specific bytes, which is enough to implement classic fee-bumps and Soroban two-phase auth signing:
export interface StellarSigner {
readonly publicKey: string // G…
sign(data: Uint8Array): Promise<Uint8Array> | Uint8Array // raw ed25519 signature (64 bytes)
}keypairSigner(secret) wraps an in-memory S… seed. For a browser wallet or hardware device,
implement the two members with your device's raw-signing primitive.
Execution is a direct connection: from the committed route's session parameters the SDK opens
wss://api.stellar.broker/ws?partner=<key> and drives the trade against the broker itself
(uswap-server is not in this path). The broker builds every transaction and submits it — the client
only signs. Because the SDK is signing broker-authored transactions over a direct link, the signing
pipeline (SigningPipeline) is the client's only defense; it runs on every tx message, in
order:
- Shape — each op is an
InvokeHostFunctionor a path payment that pays the trader (swap leg) or a trader/unset-sourced strict-send fee leg. Anything else is refused. - Trader-signature detection — a tx already carrying the trader's signature is the Soroban fee-bump round-trip. Detected by cryptographic verification, never by the 4-byte hint or signature-presence (SB's channel accounts pre-sign classic txs).
- Per-tx debit budget — worst-case trader spend in the selling asset must be
≤ sellingAmount × 1.02, and at most 5 distinct debiting txs per session. This is per-tx + tx-count only, never cumulative — SB rebuilds retries on different channel accounts, so a cumulative ceiling would kill legitimate retries. - Sign — classic: fee-bump with
feeSource = trader; Soroban first pass: sign each auth entry (signatureExpirationLedger = maxLedger + 1) + the inner tx, no fee-bump (the server round-trips it); Soroban second pass: only wrap + sign the fee-bump.
On any failure after a signature, the session still returns the last signed fee-bump hash so the
swap can be tracked — a partial fill may already have moved value. executeAndTrack reports it
before rethrowing.
new StellarSwapSDK(config)—apiBaseUrl,apiKey, optionalhorizonUrl,networkPassphrase,brokerWsUrl,fetch,WebSocket,requestTimeoutMs.quote(params)→{ route?, provider?, crossChain, allRoutes, providerErrors }crossChainTokens(provider?)→ the cross-chain asset catalog ('NEAR'default, or'AXELAR_ITS')checkTrustline(recipient, buyAsset)/activateTrustline(signer, asset, limit?)commit(params)→CommittedRoute(hasexecution+uuid)previewSignedTransaction(route)→ fee + enforced minimum for a confirm screenexecute(route, signer?, opts?)→ExecutionResult(signer optional only for a non-Stellar-origin cross-chain route, which returnsresult.depositinstead of submitting)depositFor(route)→ the cross-chain deposit instruction without submittingexecuteAndTrack(route, signer, opts?)→{ execution, track? }track(uuid, inboundTxHash?)/pollTrack(uuid, inboundTxHash, opts?)
Advanced building blocks are also exported: UswapClient, TrustlineManager, HorizonClient,
SignedTransactionExecutor, TransferExecutor, StellarBrokerSession, SigningPipeline, the
waterfall helpers, and all asset/amount utilities.
wss://is required explicitly for the broker (browsers auto-upgradehttps://WS URLs; nothing else does).- Every
pingis answered withpong{uid}, including while waiting forconnected/quote. - Asset codes are case-sensitive end to end — never normalized.
/v2slippageis a percent; the broker'sslippageToleranceis a fraction — the server-provided execution params are used verbatim, never converted.- Amounts are truncated (not rounded) to the 7-dp stroop grid.
destinationAddressis sent to/v2/swapeven when it equals the source.
src/
index.ts public entry — re-exports the SDK surface
StellarSwapSDK.ts top-level orchestrator (quote → commit → execute → track)
core/ dependency-light primitives
types.ts wire types for the /v2 contract
errors.ts StellarSwapError + error codes
config.ts config resolution / defaults
amounts.ts stroop math (7-dp truncation)
assets.ts asset parsing, SB/Horizon forms, SAC derivation
signer.ts StellarSigner interface + keypair signer + sig helpers
waterfall.ts SB-first route selection policy
client/
UswapClient.ts typed REST wrapper for /v2/rate, /v2/swap, /v2/track, …
stellar/ on-chain interaction
horizon.ts Horizon submit + account reads
trustline.ts trustline detection + changeTrust
execution/ client-native execution engines
signedTransaction.ts sign + submit a server-built envelope
stellarBroker/
messages.ts WebSocket protocol message shapes
SigningPipeline.ts the security pipeline (shape/debit/2-phase signing)
StellarBrokerSession.ts session driver (connect → quote → trade → settle)
docs/ reference: STELLAR_WEB_SDK_GUIDE.md, STELLAR_SWAP_KIT.md, provider API docs
demo/ browser test harness (see demo/README.md)
test/ dependency-free test suite (test/*.test.mjs)
npm install
npm run build # tsc → dist/
npm test # builds, then runs the dependency-free test suite (test/*.test.mjs)
npm run typecheck
npm run demo # build + serve the browser demo (see demo/README.md)The tests exercise the security pipeline against real XDR: classic swap-leg fee-bumps, the Soroban two-phase flow, debit-budget and shape rejections, and the full WebSocket session against a mock broker.