Canton and Daml primer for DEX builders
This is Step 1 of the
canonical newcomer learning path.
It assumes you understand AMM reserves, x*y=k, swaps, fees, and LP shares,
but have not built a Canton application.
This primer teaches the ledger concepts used by this repository. It is not a complete Daml language course. Before editing Daml, complete Digital Asset’s official Get started with Daml tutorial and basic contracts lesson. Installation comes later in Step 3, Getting started.
By the end, you should be able to answer four questions while reading code:
- What data is a contract carrying?
- Which party can see it and which party must authorize a change?
- Which choice archives or creates contracts?
- Is the code changing DEX state, Token Standard value, or only an off-ledger projection?
The shortest mental model
Section titled “The shortest mental model”Canton is the distributed-ledger system. Daml is the language and ledger model used to define application contracts and their authorized transitions.
flowchart LR User[Trader] --> Wallet[Wallet] Operator[Operator backend] --> API[Participant Ledger API] Wallet --> API API --> Daml[Daml contracts and choices] API <--> Sync[Synchronizer] Daml --> Visible[Per-party visible ledger state]
- A party is the on-ledger identity that authorizes actions. Trader, DEX operator, asset admin, and LP registrar are distinct logical roles and are normally separate parties in production; an explicitly documented local learning setup may let some control roles share one party.
- A participant is the Canton node through which hosted parties read their visible ledger state and submit commands.
- A synchronizer coordinates compatible participant transactions. It does not make every contract globally visible like a public-chain full node.
- A Daml contract is an immutable instance of a template.
- A choice is a permitted transition on a contract. Its controller must authorize the exercise.
- A transaction is atomic: all commands and nested choices commit, or none do.
The frontend does not become a ledger client merely because it can call the operator backend. A self-custodial write crosses the trader’s wallet because only the trader can authorize trader-controlled commands.
Templates become contracts
Section titled “Templates become contracts”A Daml template combines data, visibility, authorization, and operations. A
shortened excerpt of
DexPair.daml illustrates all four:
template DexPair with operator : Party admin : Party baseInstrumentId : Text quoteInstrumentId : Text active : Bool where signatory operator -- authorizes creation; always sees the contract observer admin -- sees the contract; need not authorize creation
choice DexPair_SetActive : ContractId DexPair with newActive : Bool controller operator -- only the operator authorizes this transition do create this with active = newActiveRead it from top to bottom:
DexPairis the schema for one market listing.- A created instance gets a contract ID, often called a
cidin this repo. - The
operatoris the signatory;adminis an observer. DexPair_SetActiveis a consuming choice by default. Exercising it archives the old pair contract and creates a successor with the new flag.
That archive-and-create pattern is how immutable contracts represent state updates. Do not look for a database-style in-place mutation.
Signatory, observer, and controller are different roles
Section titled “Signatory, observer, and controller are different roles”| Role | Question it answers | In the excerpt |
|---|---|---|
| Signatory | Who authorizes contract creation and is a stakeholder? | operator |
| Observer | Which additional stakeholder sees the contract? | admin |
| Controller | Who authorizes this choice exercise? | operator |
Visibility is deliberate. A contract that is visible to the operator is not automatically visible to every trader. Conversely, being able to see a contract does not grant authority to exercise every choice on it.
Commands become one atomic transaction
Section titled “Commands become one atomic transaction”A client submits commands such as “create this template” or “exercise this choice.” Choices can fetch other contracts and exercise nested choices. Canton commits the resulting transaction only if authorization, visibility, preconditions, and contract freshness all hold.
A Daml Script test expresses the submitting authority explicitly:
pairCid <- submit operator $ createCmd DexPair with ...
newPairCid <- submit operator $ exerciseCmd pairCid DexPair_SetActive with newActive = FalseThe important word is operator after submit. Replacing it with an unrelated
trader should fail because the choice controller is the operator. Tests use
this property to document both the happy path and forbidden paths.
Consuming and nonconsuming choices
Section titled “Consuming and nonconsuming choices”- A consuming choice archives the contract it is exercised on. It may
create a successor, as
DexPair_SetActivedoes. - A nonconsuming choice leaves that contract active. It is useful for a stable rules contract that validates an operation without replacing itself.
Do not assume “nonconsuming” means read-only. A nonconsuming choice may still exercise other contracts and create or archive application state inside the same transaction.
Parties are not services or users
Section titled “Parties are not services or users”Keep these three concepts separate:
| Concept | Example in this repo | Meaning |
|---|---|---|
| Human/application user | person using the Trade page | Off-ledger identity and session |
| Daml party | trader, operator, lpRegistrar |
Ledger identity named in contracts and authorization |
| Canton participant | node exposing the Ledger API | Hosts parties, validates/submits commands, and stores their visible ledger state |
A backend credential can submit as a party only when the participant grants
the corresponding ledger rights. Writing actAs: [trader] in a request does
not manufacture trader authority.
Real Canton party IDs normally contain a hint and fingerprint, for example
alice::1220…. Short names such as trader-demo in the browser preview are
seed labels, not production party IDs.
Packages, DARs, and the Ledger API
Section titled “Packages, DARs, and the Ledger API”Daml source is built into a DAR (Daml Archive). A DAR contains one or more compiled packages and their dependencies. A Canton participant must know the packages before it can create those templates or exercise their choices.
This repository separates three representations:
trading/**/*.daml │ dpm build ▼trading/.daml/dist/canton-dex-trading-0.1.4.dar │ upload / vet for the target network ▼Canton participant │ JSON Ledger API ▼services/operator-backenddaml.yamlpins the SDK version and declares DAR dependencies.dpm buildcompiles the package (dpm, the Daml Package Manager, is the SDK’s build-and-test CLI used throughout this repo).- Uploading a DAR makes package code available to a participant; it does not create parties, holdings, pools, or liquidity.
- The backend’s production ledger adapter sends JSON Ledger API commands and reads transaction/contract data visible to its ledger user.
The shortest proof that this package works on a real Canton process is the repository’s DPM sandbox runner. From the repository root, run:
bash scripts/run-dpm-sandbox-proof.shIt starts the Canton sandbox bundled with the pinned SDK, uploads the package closure, and runs a live holdings/allocation/DvP driver. It is intentionally throwaway. Its operator, asset admin, and LP registrar share the bootstrap party, while the LP/trader and swapper are separately allocated so real value moves between counterparties. Read Local Canton from a clean clone before treating that proof as evidence for any broader integration.
The Active Contract Set is current state
Section titled “The Active Contract Set is current state”The Active Contract Set (ACS) is the set of contracts that have been created and not archived, as visible to the querying party. For an AMM, the interesting active contracts include:
Pool: immutable pool configuration;PoolState: aggregate reserves used for pricing;PoolSlice: committed reserve inventory;PoolRules: stable choices for swap validation and execution;- Token Standard
HoldingandAllocationcontracts.
The ACS is not a globally readable SQL table. Results depend on the querying party’s visibility. The backend indexer projects ledger events into a database for API reads, but that database is a derived view, not the authorization or settlement source of truth.
Why a Canton AMM needs Token Standard contracts
Section titled “Why a Canton AMM needs Token Standard contracts”The DEX contracts define market intent and validation. Token Standard V2 contracts represent and move value. This separation is the central design of the repository.
| AMM idea | Daml/Token Standard representation |
|---|---|
| Trader’s balance | one or more Holding contracts for an instrument |
| Permission to use exact funds for a trade | trader-authored Allocation tied to settlement terms |
| Pool reserves used for pricing | PoolState.reserves |
| Pool inventory that backs those reserves | committed allocation slices represented by PoolSlice |
| Atomic input-for-output exchange | SettlementFactory_SettleBatch inside the pool swap transaction |
| LP share | a Token Standard V2 LP instrument held in ordinary Holding contracts |
An allocation is intentionally narrower than an ERC-20 router allowance. It locks identified backing for a particular settlement specification and names the authorized settlement context. The operator can execute a valid settle; it cannot silently rewrite the trader’s signed legs.
One swap, in Canton terms
Section titled “One swap, in Canton terms”For a BTC-to-USDC swap, the flow is:
sequenceDiagram actor T as Trader participant D as dApp participant O as Operator participant W as Wallet participant L as Canton / Daml D->>O: Request quote and Daml-built allocation specification O->>L: Exercise PoolRules_RequestSwap L-->>O: Exact input/output legs bound to a pool snapshot O-->>D: Wallet intent + disclosed context D->>W: Ask trader to authorize allocation W->>L: AllocationFactory_Allocate as trader L-->>D: Trader allocation contract / correlated update D->>O: Settle using that allocation O->>L: PoolRules_Swap as operator L->>L: Validate quote, settle batch, update state and slices atomically L-->>T: Updated visible holdings
There are two authorities because there are two decisions:
- The trader authorizes the exact value locked from the trader’s holdings.
- The operator authorizes execution against the venue’s pool under on-ledger rules.
If the pool changed after the quote, the bound contract IDs are stale and the transaction fails rather than silently repricing the signed trade.
“In memory” means two different things here
Section titled ““In memory” means two different things here”This distinction prevents a common first-day misunderstanding:
| Name used in the repo | Engine | Enforces Daml? | Holds Token Standard value? | Runs Canton? |
|---|---|---|---|---|
Backend InMemoryLedger |
TypeScript map + selected handlers | No | No | No |
| Daml Script runner | Daml ledger engine | Yes | Yes, when the fixture creates real Holdings |
No participant process |
| DPM sandbox proof | Real throwaway Canton process + JSON Ledger API | Yes | Yes | Yes, one local sandbox process |
| Optional DevKit LocalNet / remote testnet | Persistent Canton/Splice services | Yes | Yes | Yes |
The browser preview uses the first row. dpm test uses the second. The default
live proof uses the third. DevKit is only an optional, separately distributed
manager for the fourth row; neither the DEX source nor its DARs depend on it at
runtime. Passing one row is not evidence that the next row is configured.
Map the repository before reading details
Section titled “Map the repository before reading details”app/web/ user interface and wallet handoff │ HTTP ▼services/operator-backend/ orchestration, indexing, matching, ledger adapter │ JSON Ledger API in live mode ▼trading/CantonDex/Dex/ market-state templates and choices │ nested Daml choices ▼trading/CantonDex/Registry/ reference Token Standard holdings and settlementtrading-tests/ drives the bottom two layers directly with Daml Script. The
tests are therefore the best executable contract documentation, but they do
not include the React dApp or HTTP backend.
A first reading exercise
Section titled “A first reading exercise”Open DexPair.daml and answer:
- Which fields define the market and fee schedule?
- Who signs the contract?
- Who observes it?
- Which choices can change it?
- Does each choice mutate the old contract, or create a successor?
Then open the beginning of
PoolWorkflowTests.daml.
Its header explains what the mock-registry fixture proves, what it does not
prove, and which pool scripts to read first. Use the
Daml proof map to find the real-holding proof
for each design claim.
You are ready to continue when…
Section titled “You are ready to continue when…”You can explain these statements in your own words:
- A template is code; a contract is an active instance with a contract ID.
- A party supplies ledger authority; a participant is a node, not an identity.
- A choice describes a legal transition; its controller must authorize it.
- Contract visibility is party-scoped, not globally broadcast.
- The DEX validates market state, while Token Standard factories move value.
- Mock Wallet contract IDs prove a UI handoff only.
- Daml Script can prove contract behavior without proving the HTTP/live-network integration.
Next canonical step: Overview. Keep the Glossary open as a companion reference.