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.appIntegration Flow
- Load token catalog from
/nswap/tokens. - Build quote request with
fromToken,toToken,amount, and current wallet party ID asrecipient. - Fetch quote from
/nswap/quote. - Settle using one of the valid routes:
swapAddress + memo, ormagicAddress without memowhenmagicAddressis returned. - Track post-submit status via wallet activity until terminal state.
swapAddress must include quote memo.Endpoint: GET /nswap/tokens
HTTP Reference
Fetches available swap tokens and limits.
Response fields:
index,instrumentId,name,symboldecimals,displayDecimals,iconUrlprice,instrumentAdminminSwapAmount,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'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;
}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,toAmountminOutAmount,rate,priceImpactmemo,swapAddress, optionalmagicAddress
memo 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
}'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 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;
}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.
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 || ''
}
});
}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:
magicAddresspresent: eitherrecipient = magicAddresswith no memo, orrecipient = swapAddresswith quotememomagicAddressmissing: userecipient = swapAddresswith quotememo
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:
- Try JSON error body
message. - Fallback to plain response text.
- 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}`;
}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 usesinstrumentAdmin + instrumentIdpair for instrument selection. - Re-fetch quote if connected wallet changes before submit.
- Address fields can be long. Keep UI containers scrollable or truncation-safe.
priceImpactis numeric in the quote payload, while most amount fields are strings.