Run against a Canton testnet
The DEX runs as two long-lived processes against a Canton participant: the operator backend (configured operator/LP authority, ledger reads, the indexer) and the web app (reads plus wallet-authority commands). This guide points both at a participant whose operator, LP registrar, and asset-admin parties are already allocated. The repository automates package build/upload, registry bootstrap, and optional pair/unfunded-pool creation. It deliberately does not allocate parties or claim to fund a pool: party allocation is participant- specific, and first funding requires an LP-authorized wallet flow.
One invariant throughout: tokens, concrete party ids, and validator-specific package hashes live in the environment, never in the repo.
Prerequisites
Section titled “Prerequisites”- Node.js 24, Java 17, DPM with the SDK pinned by
trading/daml.yaml, and the backend/frontend dependencies installed withnpm ci. - A Canton participant JSON Ledger API URL. For a compact validator setup, its
server-side JWT can
actAsthe operator and LP registrar and read the configured registrars; pool creation and LP settlement require those control roles. Registry bootstrap additionally needsactAsfor each registry admin. In production, prefer separate least-privilege bootstrap and runtime users. - The target network must accept the exact Token Standard V2 package hashes in
vendor/splice/dars/. A production network may require its governance/vetting process before upload. - Operator, LP registrar, and asset-admin parties allocated on the participant.
- The
lpRegistrar‘sRegistry.V2and the asset admins’ registry factory contracts created — the registry bootstrap inscripts/bootstrap-registry.tsdoes this; without the LP registry no pool can allocate a liquidity move.
1. Prepare the ledger
Section titled “1. Prepare the ledger”Copy the backend environment template, fill the participant values, and load it
into the current shell. npm run testnet does not implicitly read .env.
cp services/operator-backend/.env.example services/operator-backend/.env# Edit services/operator-backend/.env. Do not commit it.
set -asource services/operator-backend/.envset +aUse two different high-entropy HTTP API tokens:
export DEX_OPERATOR_API_TOKEN="<short-lived-operator-api-token>"export OPERATOR_ADMIN_TOKEN="<short-lived-admin-api-token>"These are credentials for the DEX HTTP service, not the participant JWT. A
full-mode testnet server refuses to start without both. For an intentional
read-only deployment, set DEX_READ_ONLY=1; every state-changing HTTP route
then returns 401 (the read-only POST /v1/swaps/quote computation remains open).
Build, upload, and bootstrap the on-ledger registries:
bash scripts/deploy-testnet.shExpected final line:
==> Deployment phases completed without a suppressed errorThe script does not allocate parties, start the backend, create a market by default, mint holdings, or fund a pool. Each successful phase mutates the target ledger and is not rolled back if a later phase fails.
Record the assetRegistryCid and lpRegistryCid fields printed by the final
bootstrap complete log. Each reference Registry.V2 implements both factory
interfaces for its own admin, so the two values within a factory pair are the
same registry cid:
export CANTON_ALLOC_FACTORY_CID="<assetRegistryCid>"export CANTON_SETTLE_FACTORY_CID="<assetRegistryCid>"
# Required only when CANTON_LP_REGISTRAR differs from CANTON_ADMIN:export CANTON_LP_ALLOC_FACTORY_CID="<lpRegistryCid>"export CANTON_LP_SETTLE_FACTORY_CID="<lpRegistryCid>"The included server maps the configured asset admin and LP registrar separately. A venue that lists additional third-party admins should replace this two-admin configuration with discovery from each admin’s registry API, as described in Registry integration.
2. Start the operator backend
Section titled “2. Start the operator backend”The backend runs src/testnet-server.ts. Keep the loaded environment in this
terminal. The process reads credentials from the environment and does not write
them to disk.
cd services/operator-backend
npm run testnet| Variable | Required | Purpose |
|---|---|---|
CANTON_LEDGER_URL |
yes | JSON Ledger API base URL of the participant. |
CANTON_LEDGER_TOKEN |
yes | Server-side JWT with the read/actAs rights required by the enabled operator and LP flows. |
CANTON_OPERATOR |
yes | Operator (venue) party id. |
CANTON_LP_REGISTRAR |
yes | LP registrar party id. |
CANTON_ADMIN |
yes | Asset-admin party id. |
DEX_OPERATOR_API_TOKEN |
yes in full mode | Bearer token for every non-admin HTTP write. |
OPERATOR_ADMIN_TOKEN |
yes in full mode | Separate bearer token for /v1/admin/* writes. |
DEX_READ_ONLY |
optional | Set 1 to start without API tokens and reject every state-changing route. |
CANTON_SYNCHRONIZER |
recommended | Synchronizer id, e.g. global-domain::1220.... submit-and-wait requires it on a shared synchronizer. |
CANTON_DEX_PACKAGE_ID |
yes | Template-id prefix. Use the vetted concrete package hash, or #canton-dex-trading only where package-name resolution is acceptable. |
CANTON_NETWORK |
optional | Display label surfaced by /v1/status (default canton:devnet). |
CANTON_ALLOC_FACTORY_CID, CANTON_SETTLE_FACTORY_CID |
yes in full mode | Asset-admin Registry cid, repeated because it implements both interfaces. |
CANTON_LP_ALLOC_FACTORY_CID, CANTON_LP_SETTLE_FACTORY_CID |
yes in full mode when LP registrar differs | LP registrar’s Registry cid, again repeated for both interfaces. |
ALLOWED_ORIGINS |
yes for cross-origin browser access | Exact comma-separated web origins. Unset is default-deny. |
DEX_CALLER_JWT_SECRET, DEX_CALLER_JWT_AUDIENCE |
optional | Bind private reads and trader-subject writes to X-Caller-Token.sub in a multi-user deployment. |
DEX_HOSTED_RFQ_RELAY |
optional, default 0 |
Custodial RFQ create/cancel/accept under hosted trader authority; enabling it requires caller binding and participant rights for those traders. |
The exact variable contract is the header of
testnet-server.ts;
the full list with defaults is
services/operator-backend/.env.example.
Verify the backend before opening a browser:
curl -fsS http://localhost:8080/v1/statusDo not continue unless the response contains "synced":true. HTTP 200 with
synced:false means the most recent participant ledger-end probe failed; check
the URL, participant token, and startup/indexer logs.
3. Start the web app
Section titled “3. Start the web app”The dApp reads its public network/backend settings at build time. A production
build deliberately excludes Mock, Direct Canton, and the operator command
relay, so choose and configure at least one real wallet provider. This
example enables PartyLayer; replace the wallet ids with adapters supported by
your target network. The alternatives are the dApp SDK gateway
(VITE_ENABLE_SDK=1) or WalletConnect (VITE_WC_PROJECT_ID=...).
cd app/web
VITE_API_BASE="http://localhost:8080" \VITE_CANTON_NETWORK_ID="canton:testnet" \VITE_CANTON_SYNCHRONIZER="<synchronizer-id>" \VITE_ENABLE_PARTYLAYER=1 \VITE_PARTYLAYER_NETWORK="canton:testnet" \VITE_PARTYLAYER_WALLET_IDS="console,nightly,send" \VITE_DOCS_URL="https://srikanth-bitdynamics.github.io/Canton-Dex-Reference-Implementation/" \npm run build
npm run previewOpen http://localhost:4173. The backend must allow this exact origin:
export ALLOWED_ORIGINS="http://localhost:4173"Set ALLOWED_ORIGINS before starting (or restart) the backend. The header
should show the configured network, /v1/status should report synced: true,
and Connect Wallet should list the provider you deliberately enabled. If it
lists no production-capable provider, stop—the browser cannot author the
trader allocations required by the flow. The full frontend variable list is
app/web/.env.example.
Authorize protected writes in the validator browser
Section titled “Authorize protected writes in the validator browser”Open Admin → API session credentials and enter short-lived copies of
DEX_OPERATOR_API_TOKEN and OPERATOR_ADMIN_TOKEN. They are stored only in
that tab’s sessionStorage, never in the built JavaScript. Trader settle calls
use the operator token; /v1/admin/* calls use the admin token. If per-caller
binding is enabled, also enter the caller JWT issued for the connected party.
This manual token handoff is for a validator/operator acceptance run. A public
multi-user dApp should obtain scoped, expiring credentials from its authenticated
BFF/session service. Do not distribute the venue’s long-lived shared tokens to
ordinary traders and do not create VITE_* token variables—Vite embeds them in
public assets.
4. Smoke checks
Section titled “4. Smoke checks”curl -s http://localhost:8080/v1/status | python3 -m json.toolcurl -s http://localhost:8080/v1/context | python3 -m json.toolcurl -s http://localhost:8080/v1/pairs | python3 -m json.toolcurl -s http://localhost:8080/v1/pools | python3 -m json.toolExpected:
/v1/statusreturns the configured network and a live slot./v1/contextreturns operator/admin/LP registrar parties and factory CIDs./v1/pairsand/v1/poolsreturn the on-ledger contracts visible to the operator party.
5. Create a pair and an unfunded pool
Section titled “5. Create a pair and an unfunded pool”With the backend still running, use a second terminal that has the same environment loaded:
set -asource services/operator-backend/.envset +a
DEPLOY_SKIP_BUILD=1 \DEPLOY_SKIP_UPLOAD=1 \DEPLOY_SKIP_BOOTSTRAP=1 \DEPLOY_SEED_MARKETS=1 \bash scripts/deploy-testnet.shThis phase first requires /v1/status to succeed. It queries existing pairs and
pools, creates only missing BTC/USDC metadata, and stops on any HTTP failure. It
creates an unfunded pool; it does not fabricate reserves or LP holdings.
Expected checkpoint:
curl -fsS http://localhost:8080/v1/pairscurl -fsS http://localhost:8080/v1/poolsThe pair should be present, and the pool should report an unfunded/zero-reserve state. The first LP must next run the same wallet-authorized request → allocations → settle flow used for later deposits. Exact admin curl alternatives are in Operator Guide.
6. Wallet and HTTP authorization boundaries
Section titled “6. Wallet and HTTP authorization boundaries”Operator/LP-authority calls go through the backend. Trader-authority calls — such as authoring allocations for add/remove liquidity, swaps, and order funding — must go through a wallet or another user-authorized submitter. The arbitrary command relay cannot be enabled in the deployed server.
The RFQ HTTP create/cancel/accept endpoints are a separate custodial exception:
they submit as the RFQ trader and are disabled by default in
testnet-server.ts. A deployment that deliberately enables
DEX_HOSTED_RFQ_RELAY=1 must give its participant user rights for each hosted
trader and configure DEX_CALLER_JWT_SECRET so X-Caller-Token.sub binds every
request to that trader. Its production UI controls also require
VITE_ENABLE_HOSTED_RFQ=1; leaving either side off keeps writes disabled. Do
not describe that mode as self-custodial.
The browser’s follow-up request to the backend is still a protected HTTP write:
it carries the operator API token entered for this tab. That token authorizes
the backend client; it does not replace the wallet’s on-ledger authorization.
When per-caller binding is enabled, X-Caller-Token.sub must also equal the
trader party named by the request. The dApp sends the same token on its scoped
orders, holdings, balances, trades, and RFQ reads; an admin token may bypass the
party comparison for operational inspection.
Reference
Section titled “Reference”PartyLayer wallet live probe
Section titled “PartyLayer wallet live probe”PartyLayer support is integrated into the main web app; no separate probe app is needed. Use this checklist when validating a submit-capable wallet adapter against a live Canton network.
Enable the connector. Set the PartyLayer env vars before building or previewing the frontend:
cd app/web
VITE_ENABLE_PARTYLAYER=1 \VITE_PARTYLAYER_NETWORK="canton:testnet" \VITE_PARTYLAYER_WALLET_IDS="console,nightly,send" \VITE_PARTYLAYER_CONNECT_TIMEOUT_MS=180000 \VITE_API_BASE="http://localhost:8080" \npm run build
npm run previewTo validate a specific adapter, set VITE_PARTYLAYER_WALLET_IDS to just that
adapter id. Optional registry overrides are documented in
app/web/.env.example.
Validate the flow.
- In Admin → API session credentials, configure the short-lived operator token and, when enabled, the connected party’s caller JWT.
- Open the app, click Connect Wallet, and select PartyLayer. Approve the connection in the wallet and confirm the connected party is the party that owns the test holdings.
- Confirm holdings load in Portfolio. The PartyLayer provider reads
holdings through its
ledgerApibridge for the connected party. - Run a small trader-authority action, such as:
- Trade → small pool swap
- Pools → add liquidity or remove liquidity
- Orders → place a prefunded order
- Confirm the wallet approval returns an
updateId. PartyLayer receipts may not include created contract ids directly; the operator backend recovers the createdAllocation,LiquidityAllocationAcceptance, or order-funding evidence by reading the committed transaction tree for thatupdateId. - Confirm the operator settle step completes and the app refreshes holdings, pool reserves, orders, or activity from the backend/indexer.
What to record. For each wallet adapter tested:
- adapter id and network
- connected party
- action submitted
- returned
updateId - whether operator discovery recovered the created contract ids
- final on-ledger result: swap settled, LP add/remove settled, or order funded
If discovery fails, capture the operator backend error and the transaction-tree
lookup response. The usual causes are missing operator visibility on the created
contracts, a wallet receipt without updateId, or a party mismatch between the
connected wallet and the holdings being spent.
Package hash alignment
Section titled “Package hash alignment”If DAR upload or vetting fails with package-version/hash errors, confirm that all
local DARs were built against the same upstream Token Standard package hashes
already accepted by the target network. Rebuild the dependent packages against
the vetted upstream DARs, then rebuild trading and trading-tests.
Running against Amulet assets
Section titled “Running against Amulet assets”If a pair uses Amulet (CC) as an asset, note the Splice 0.6.11+ requirements:
- The validator node (yours or your wallet provider’s) must run a version that
supports the Token Standard V2 APIs, and the Amulet DARs must be at the
V2-capable versions (
amulet0.1.21+,wallet0.1.22+; see the Splice release notes). - Amulet enforces
tokenStandardMaxTTL(default 90 days) on allocations and instructions. See Registry Integration. - Known upstream limitation: the Splice Amulet Wallet UI can only create multiple requested allocations in a single transaction for Amulet allocations. The DEX sidesteps this for its own flows by having one Daml choice author all allocations of a request in a single command.
Where to read next: Deployment · Validator Test Plan · All docs