Docs

Canton Swap API Docs

This page describes how this app integrates with the Canton Swap API, including quote creation, settlement rules, magic addresses, and strongly typed request/response examples.

Base URL

All API requests in this app use:

https://mainnet.rpc.canton.nightly.app
text

Integration Flow

  1. Load token catalog from /nswap/tokens.
  2. Build quote request with fromToken, toToken, amount, and current wallet party ID as recipient.
  3. Fetch quote from /nswap/quote.
  4. Settle using one of the valid routes: swapAddress + memo, or magicAddress without memo when magicAddress is returned.
  5. Track post-submit status via wallet activity until terminal state.
CriticalTransfers sent to swapAddress must include quote memo.

Endpoint: GET /nswap/tokens

HTTP Reference

Fetches available swap tokens and limits.

Response fields:

  • index, instrumentId, name, symbol
  • decimals, displayDecimals, iconUrl
  • price, instrumentAdmin
  • minSwapAmount, maxSwapUsd

UI note: token label Amulet is displayed as CC in the frontend UI layer only.

curl -sS 'https://mainnet.rpc.canton.nightly.app/nswap/tokens'
bash

Function: fetchTokens()

Client Wrapper

Wrapper function used by the app to call GET /nswap/tokens and parse the response.

type ApiToken = {
  index: number;
  instrumentId: string;
  name: string;
  symbol: string;
  decimals: number;
  displayDecimals: number;
  iconUrl: string;
  price: string;
  instrumentAdmin: string;
  minSwapAmount: string;
  maxSwapUsd: string;
};

type TokensResponse = { tokens: ApiToken[] };

async function fetchTokens() {
  const response = await fetch('https://mainnet.rpc.canton.nightly.app/nswap/tokens');
  if (!response.ok) throw new Error('Unable to load tokens');
  const data = (await response.json()) as TokensResponse;
  return data.tokens;
}
typescript

Endpoint: POST /nswap/quote

HTTP Reference

Returns executable swap routing and settlement details for a specific input amount.

Request fields:

  • fromToken, toToken, amount, recipient
  • Optional: slippageTolerance

Response fields:

  • fromToken, toToken, fromAmount, toAmount
  • minOutAmount, rate, priceImpact
  • memo, swapAddress, optional magicAddress
Criticalmemo is required when you settle via swapAddress. Only direct magicAddress flow skips memo.
curl -sS 'https://mainnet.rpc.canton.nightly.app/nswap/quote' \
  -X POST \
  -H 'Content-Type: application/json' \
  -d '{
    "fromToken": "Amulet",
    "toToken": "USDCx",
    "amount": "125",
    "recipient": "alice::1220b1431ef217342db44d516bb9befde802be7d8899637d290895fa58880f19accc",
    "slippageTolerance": 0.5
  }'
bash

Function: fetchQuote()

Client Wrapper

Wrapper function used by the app to call POST /nswap/quote and return typed quote data.

type QuoteRequest = {
  fromToken: string;
  toToken: string;
  amount: string;
  recipient: string;
  slippageTolerance?: number;
};

type SwapQuote = {
  fromToken: string;
  toToken: string;
  fromAmount: string;
  toAmount: string;
  minOutAmount: string;
  rate: string;
  priceImpact: number;
  memo: string;
  swapAddress: string;
  magicAddress?: string;
};

async function fetchQuote(request: QuoteRequest): Promise<SwapQuote> {
  const response = await fetch('https://mainnet.rpc.canton.nightly.app/nswap/quote', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(request)
  });
  if (!response.ok) throw new Error('Unable to fetch quote');
  return (await response.json()) as SwapQuote;
}
typescript

TypeScript Types

Core app-level types used for quote flow and token mapping:

export interface Token {
  index: number;
  instrumentId: string;
  name: string;
  symbol: string;
  decimals: number;
  displayDecimals: number;
  iconUrl: string;
  priceUsd: number;
  minSwapAmount: string;
  maxSwapUsd: string;
  instrumentAdmin?: string;
}

export type QuoteRequest = {
  fromToken: string;
  toToken: string;
  amount: string;
  recipient: string;
  slippageTolerance?: number;
};

export interface SwapQuote {
  fromToken: string;
  toToken: string;
  fromAmount: string;
  toAmount: string;
  minOutAmount: string;
  rate: string;
  priceImpact: number;
  memo: string;
  swapAddress: string;
  magicAddress?: string;
}
typescript

Execute a Swap from Quote

You can settle the quote in two valid ways: recipient = swapAddress + memo, or recipient = magicAddress without memo.

If magicAddress exists, both routes are valid. You can still use swapAddress with quote memo, or choose direct deposit via magicAddress without memo.

CriticalIf recipient is swapAddress, you HAVE TO include quote memo.

Current app behavior: when magicAddress is present, this UI prefers the direct-deposit path by default.

Wallet consistency rule in current implementation: non-magic execution requires the quoted recipient to match the currently connected wallet party ID.

type Transfer = (
  receiverPartyId: string,
  amount: string,
  options?: {
    memo?: string;
    instrumentId?: { admin: string; id: string };
  }
) => Promise<{ success: boolean; error?: string }>;

async function executeQuote(params: {
  quote: {
    fromAmount: string;
    memo: string;
    swapAddress: string;
    magicAddress?: string;
  };
  fromToken: { instrumentId: string; instrumentAdmin?: string };
  connectedPartyId: string;
  quotedRecipient: string;
  transfer: Transfer;
  preferMagicAddress?: boolean;
}) {
  const {
    quote,
    fromToken,
    connectedPartyId,
    quotedRecipient,
    transfer,
    preferMagicAddress = true
  } = params;
  const useMagicAddress = Boolean(quote.magicAddress) && preferMagicAddress;
  const recipient = useMagicAddress ? quote.magicAddress! : quote.swapAddress;

  if (!useMagicAddress && quotedRecipient !== connectedPartyId) {
    throw new Error('Recipient changed. Refresh quote before swap.');
  }

  return transfer(recipient, quote.fromAmount, {
    memo: useMagicAddress ? undefined : quote.memo,
    instrumentId: {
      id: fromToken.instrumentId,
      admin: fromToken.instrumentAdmin || ''
    }
  });
}
typescript

Magic Address Explained

magicAddress is a direct-deposit recipient returned by quote API for eligible routes. When present, transfer can be submitted without memo, but standard swapAddress + memo routing is still valid.

Use this decision logic:

  • magicAddress present: either recipient = magicAddress with no memo, or recipient = swapAddress with quote memo
  • magicAddress missing: use recipient = swapAddress with quote memo
CriticalRule of thumb: swapAddress => memo required. magicAddress => no memo.

The UI exposes this as Direct deposit - no memo and provides copy-to-clipboard for the address.

Error Handling

Recommended fallback order for failed responses:

  1. Try JSON error body message.
  2. Fallback to plain response text.
  3. Fallback to HTTP status-based generic message.
async function readErrorMessage(response: Response): Promise<string> {
  try {
    const data = await response.json();
    if (data && typeof data.message === 'string') return data.message;
  } catch {}

  try {
    const text = await response.text();
    if (text) return text;
  } catch {}

  return `Request failed with status ${response.status}`;
}
typescript

Practical Notes / Gotchas

  • Keep all amount fields as strings (amount, fromAmount, toAmount, minOutAmount) to avoid float precision loss.
  • Quote requests use token instrumentId. Transfer path still uses instrumentAdmin + instrumentId pair for instrument selection.
  • Re-fetch quote if connected wallet changes before submit.
  • Address fields can be long. Keep UI containers scrollable or truncation-safe.
  • priceImpact is numeric in the quote payload, while most amount fields are strings.