Skip to main content

Basic swap

import { OneSwap } from '@oneswap/sdk'

const authClient = new OneSwap({ apiKey: 'os_live_...' })

const challenge = await authClient.walletAuth.requestChallenge('alice::12205a8c...')
const signature = await wallet.signMessage(challenge.message)
const verified = await authClient.walletAuth.verifyChallenge({
  partyId: 'alice::12205a8c...',
  nonce: challenge.nonce,
  signature,
  publicKey: wallet.publicKey,
})

const swapClient = new OneSwap({
  apiKey: 'os_live_...',
  walletToken: verified.token,
})

const swap = await swapClient.swaps.create({
  fromToken: 'Amulet',
  toToken: 'USDCx',
  amount: '100',
  walletAddress: 'alice::12205a8c...',
})
In SDK params and responses, use Amulet when you mean CC (Amulet). This returns a SwapIntent with everything you need:
FieldDescription
depositAddressPool swap party that should receive the deposit
swapAddressThe same pool swap party, returned explicitly for clarity
depositReferenceTransfer reference to pass through when the wallet supports Canton reason/reference metadata
expectedOutputEstimated output amount
minOutputMinimum accepted after slippage
outputAddressResolved destination party for the payout
expiresAtIntent expiration timestamp
instructionsStep-by-step transfer instructions
The deposit must be sent from the same authenticated wallet party that created the intent. A different wallet or Canton party will not complete the swap. Swap intents currently remain valid for 24 hours. Use swap.expiresAt from the response instead of hard-coding a shorter timeout.

Direct-party flow

  1. Request a OneSwap wallet-auth challenge for the user’s party ID
  2. Have the user sign the challenge with the same wallet that will deposit
  3. Verify the signature and store the returned wallet token
  4. Create the intent with client.swaps.create(...)
  5. OneSwap returns the pool swap party as depositAddress plus a depositReference
  6. The user sends the input token directly from that same party to depositAddress and passes through the reference when the wallet supports Canton reason/reference metadata
  7. OneSwap detects the deposit and executes the swap
  8. Output returns directly to the same party that made the deposit
walletAddress is a consistency check, not the trust root. The verified wallet token is what authorizes the swap.

Slippage tolerance

Default is 1%. You can set it between 0.1% (0.001) and 50% (0.5):
const swap = await client.swaps.create({
  fromToken: 'Amulet',
  toToken: 'USDCx',
  amount: '100',
  slippageTolerance: 0.005,
  walletAddress: 'alice::12205a8c...',
})
If the actual output falls below minOutput, the swap is refunded.

Maximum price impact

OneSwap enforces a server-side price impact cap (currently 15%). If a quote or swap-intent creation request would move the pool price beyond this limit, the API returns 400 and the SDK surfaces that as ValidationError. If an already-created intent later crosses the same cap during execution, it reaches terminal status price_impact_exceeded and swap.wait() throws PriceImpactError. The cap is returned in quote.maxPriceImpact. To execute large swaps, either split them into smaller amounts or add liquidity to the pool first.

Output routing

The current SDK flow always returns swap output to the same Canton party that sends the deposit. outputAddress remains in the type surface for backward compatibility, but the SDK ignores it and /api/v1/swaps resolves output back to the depositor party. depositReference is recommended operational metadata. Pass it through to the wallet UX when the wallet supports Canton reason/reference fields so fallback matching and observability stay stronger.

Disambiguating duplicate symbols

If the same token symbol exists under multiple admins, use the token list plus admin fields to select the exact pool.
const tokens = await client.tokens.list()

const usdc = tokens.tokens.find(
  (token) => token.name === 'USDCx' && token.admin === 'admin::issuer-a'
)

const quote = await client.quotes.get({
  from: 'USDCx',
  to: 'Amulet',
  amount: '250',
  fromAdmin: usdc?.admin,
  toAdmin: 'DSO',
})

const swap = await client.swaps.create({
  fromToken: 'USDCx',
  toToken: 'Amulet',
  amount: '250',
  walletAddress: 'alice::12205a8c...',
  poolId: quote.poolId,
  fromAdmin: usdc?.admin,
  toAdmin: 'DSO',
})
If you omit those fields for an ambiguous pair, the API returns 409 ambiguous_pool_pair and the SDK throws AmbiguousPoolPairError.

Track swap status

Await completion

const result = await swap.wait()
swap.wait() resolves on completed. It throws typed errors for terminal failures, including ExpiredError when the intent expires or when polling observes a cancelled intent.

Listen to events

swap.on('processing', () => console.log('Deposit detected'))
swap.on('sending_output', () => console.log('Sending payout'))
swap.on('completed', (s) => console.log(`Got ${s.actualOutput}`))
swap.on('slippage_failed', () => console.log('Refunded: price moved'))
swap.on('price_impact_exceeded', () => console.log('Refunded: price impact too high'))
swap.on('insufficient_amount', () => console.log('Refunded: below traffic cost'))
swap.on('output_failed', () => console.log('Output transfer failed, input was refunded'))
swap.on('refund_failed', () => console.log('Refund failed, manual intervention required'))
swap.on('expired', () => console.log('Expired'))

Poll manually

const status = await client.swaps.getStatus(swap.intentId)

Cancel a pending swap

await client.swaps.cancel(swap.intentId)
// or:
await swap.cancel()

Status lifecycle

pending → matched → deposit_received → forwarded → processing → sending_output → completed
        → cancelled
        → slippage_failed
        → price_impact_exceeded
        → insufficient_amount
        → insufficient_liquidity
        → output_failed
        → no_output_holdings
        → failed
        → refund_failed
        → expired
Most validation failures refund the input automatically. Rare operational states such as no_output_holdings, failed, and refund_failed mean the swap reached a terminal state that may require manual investigation.

Quotes

const quote = await client.quotes.get({
  from: 'Amulet',
  to: 'USDCx',
  amount: '100',
  receiverParty: 'alice::12205a8c...',
})
quote.estimatedTrafficCost, quote.trafficFeeInInput, and quote.inputAmountAfterTrafficFee help you account for Canton execution costs before the swap executes.
  • inputTokenAdmin and outputTokenAdmin identify the exact instruments that were priced.
  • maxPriceImpact is the server-enforced price impact cap. Swaps exceeding this are rejected.
  • trafficFeeInInput is the amount deducted from the input token as traffic-cost recovery.
  • outputAmount is quoted from inputAmountAfterTrafficFee, not from the gross deposit amount.
  • receiverParty lets the quote include the same destination-party traffic assumptions used by the live execution path.
  • The traffic fee is separate from the 1% swap fee and is not shared with LPs or SDK developers.

Wallet auth

For browser-based SDK apps, keep the API key and wallet proof separate:
  • The API key identifies your app or developer account.
  • The wallet token identifies the end user who signed the OneSwap challenge.
  • client.swaps.create(...), client.swaps.getStatus(...), and client.track(partyId) require the wallet token.
  • If your app refreshes wallet auth in state, pass walletToken as a function so the SDK reads the current token on each request.