This document describes how MetaMask, the API Server, the Relayer, and the Canton Ledger work together to enable ERC-20 compatible token operations on Canton Network.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β USER LAYER β
β β
β ββββββββββββββββββββ ββββββββββββββββββββ β
β β π¦ MetaMask β β Native Canton β β
β β (EVM Wallet) β β User / CLI β β
β ββββββββββ¬ββββββββββ ββββββββββ¬ββββββββββ β
β β eth_sendRawTransaction β eth_sendRawTransactionβ
β β eth_call, eth_getBalance β (via /eth endpoint) β
βββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββ
β β
βΌ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MIDDLEWARE LAYER β
β β
β βββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββ β
β β API SERVER β β RELAYER β β
β β (Port 8081) β β (Background Service) β β
β β β β β β
β β β’ /eth - JSON-RPC facade β β β’ Ethereum β Canton β β
β β β’ /register - User signup β β β’ Canton β Ethereum β β
β β β’ /health - Status check β β β’ Event processing β β
β β β β β β
β β Custodial key management: β β Bridges PROMPT token β β
β β Holds Canton keys for all β β between chains β β
β β registered users β β β β
β ββββββββββββββββ¬βββββββββββββββ ββββββββββββββββ¬βββββββββββ β
β β β β
β βββββββββββββββ¬ββββββββββββββββββββββββ β
β β β
β ββββββββββββΌβββββββββββ β
β β PostgreSQL β β
β β (Port 5432) β β
β β β β
β β β’ User registry β β
β β β’ Balance cache β β
β β β’ Transfer state β β
β β β’ Chain offsets β β
β ββββββββββββ¬βββββββββββ β
βββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββ
β
gRPC + OAuth2 β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CANTON LEDGER β
β (Source of Truth) β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β DAML Smart Contracts (CIP-56) β β
β β β β
β β ββββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββ β β
β β β FingerprintMappingβ β CIP56Holding β β TokenMeta β β β
β β β β β β β β β β
β β β Links EVM addr β β Actual token β β DEMO: Native β β β
β β β to Canton Party β β balances per β β PROMPT: Bridged β β β
β β β β β user per token β β β β β
β β ββββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β Ports: 5011 (gRPC), 5013 (HTTP) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β²
β Bridge Events (PROMPT only)
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ETHEREUM (Anvil Local / Sepolia Testnet) β
β β
β ββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββ β
β β Bridge Contract ββββββ PROMPT Token β β
β β β β (ERC-20) β β
β β β’ depositToCanton() β β β β
β β β’ withdrawToEthereum() β β Local: 0x5FbDB231... β β
β β β β β β
β β Local: 0xe7f1725E... β β β β
β ββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββ β
β β
β Port: 8545 (Anvil) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The API Server provides an Ethereum JSON-RPC compatible interface that allows MetaMask and other EVM wallets to interact with Canton tokens.
| Endpoint | Purpose |
|---|---|
/eth |
JSON-RPC facade (eth_call, eth_sendRawTransaction, etc.) |
/register |
User registration with EIP-191 signature |
/health |
Health check endpoint |
Key responsibilities:
- Translate ERC-20 calls to CIP-56 DAML operations
- Manage custodial Canton keys for all registered users
- Cache balances in PostgreSQL for fast queries
- Reconcile database cache with Canton ledger periodically
The Relayer bridges PROMPT tokens between Ethereum and Canton.
Bidirectional processing:
- Ethereum β Canton: Watches for
depositToCanton()events, mints CIP-56 tokens - Canton β Ethereum: Watches for withdrawal requests, releases ERC-20 tokens
Design principles:
- At-least-once delivery with idempotency
- Crash recovery via persisted offsets
- Database-backed deduplication
Serves as a distributed indexer and cache:
- User registry (EVM address β Canton Party mapping)
- Balance cache for fast MetaMask queries
- Transfer state tracking for idempotency
- Chain offsets for crash recovery
The source of truth for all token balances.
DAML Contracts:
FingerprintMapping- Links EVM addresses to Canton partiesCIP56Holding- Actual token balances (one contract per user per token)TokenMeta- Token configuration (DEMO native, PROMPT bridged)
| Token | Type | Virtual Address | Description |
|---|---|---|---|
| DEMO | Native Canton | 0xDE30000000000000000000000000000000000001 |
Created directly on Canton |
| PROMPT | Bridged ERC-20 | 0x5FbDB2315678afecb367f032d93F642f64180aa3 |
Bridged from Ethereum |
User sends tokens to another user via MetaMask.
ββββββββββββ βββββββββββββ ββββββββββββ ββββββββββ
β MetaMask β βAPI Server β βPostgreSQLβ β Canton β
ββββββ¬ββββββ βββββββ¬ββββββ ββββββ¬ββββββ βββββ¬βββββ
β β β β
β eth_sendRawTx β β β
β (ERC-20 transfer) β β
βββββββββββββββββ>β β β
β β β β
β β Decode tx, verify whitelist β
β β Get sender/recipient parties β
β βββββββββββββββββ>β β
β β β β
β β TransferAsUserByFingerprint β
β ββββββββββββββββββββββββββββββββ>β
β β β β
β β β CIP56 Transfer
β β β (exercises choice)
β β β β
β β<βββββββββββββββββββββββββββββββ
β β β β
β β Update balance cache β
β βββββββββββββββββ>β β
β β β β
β tx receipt β β β
β<βββββββββββββββββ β β
β β β β
User deposits PROMPT tokens from Ethereum to Canton.
ββββββββββββ ββββββββββββ βββββββββββ ββββββββββββ ββββββββββ
β User β β Ethereum β β Relayer β βPostgreSQLβ β Canton β
ββββββ¬ββββββ ββββββ¬ββββββ ββββββ¬βββββ ββββββ¬ββββββ βββββ¬βββββ
β β β β β
β depositToCanton() β β β
ββββββββββββββββ>β β β β
β β β β β
β β Emit Deposit event β β
β ββββββββββββββββ>β β β
β β β β β
β β β Check if processed β
β β ββββββββββββββββ>β β
β β β β β
β β β Create pending transfer β
β β ββββββββββββββββ>β β
β β β β β
β β β BridgeMint (CIP56) β
β β βββββββββββββββββββββββββββββββ>β
β β β β β
β β β β Create Holding
β β β β β
β β β<ββββββββββββββββββββββββββββββ
β β β β β
β β β Update status = completed β
β β ββββββββββββββββ>β β
β β β β β
API Server syncs database cache with Canton ledger.
βββββββββββββ ββββββββββββ ββββββββββ
βAPI Server β βPostgreSQLβ β Canton β
βββββββ¬ββββββ ββββββ¬ββββββ βββββ¬βββββ
β β β
β GetAllCIP56Holdings β
ββββββββββββββββββββββββββββββββ>β
β β β
β<βββββββββββββββββββββββββββββββ
β [list of all holdings] β
β β β
β Group by party, sum by token β
β β β
β For each registered user: β
β UpdateBalanceByCantonPartyID β
ββββββββββββββββ>β β
β β β
β Repeat... β β
ββββββββββββββββ>β β
β β β
The Relayer uses a generic Processor pattern for bidirectional event handling:
sequenceDiagram
participant Engine
participant Processor
participant Source
participant Store as Database
participant Destination
Engine->>Processor: Start(offset)
Processor->>Source: StreamEvents(offset)
loop For each event
Source-->>Processor: Event
Processor->>Store: Check if processed
alt Not yet processed
Processor->>Store: CreateTransfer(Pending)
Processor->>Destination: SubmitTransfer(event)
Destination-->>Processor: txHash
Processor->>Store: UpdateStatus(Completed)
end
end
Two processor instances:
- Canton β Ethereum:
CantonSource+EthereumDestination - Ethereum β Canton:
EthereumSource+CantonDestination
All users are allocated as external parties on Canton. This is a key architectural decision that removes the ~200 internal party limit and enables interoperability with wallets like Canton Loop.
| Party Type | Key Management | Submission Method | Limit |
|---|---|---|---|
| Internal | Participant node holds keys | CommandService.SubmitAndWait |
~200 per participant |
| External (used) | API server holds keys custodially | Interactive Submission API | No practical limit |
All token transfers use the Interactive Submission flow:
1. PrepareSubmission β Canton returns transaction hash to sign
2. Sign hash β API server signs with user's custodial secp256k1 key
3. ExecuteSubmission β Canton executes the signed transaction
The API server stores each user's Canton signing key encrypted at rest (AES-256-GCM with CANTON_MASTER_KEY). When a user sends a transfer via /eth, the server decrypts their key, signs the prepared transaction, and submits it.
The middleware connects to Canton via the Daml Ledger gRPC API v2:
Canton Participant Node
β
βββ Ledger API (gRPC, port 5011)
β βββ InteractiveSubmissionService - Prepare/execute (external parties)
β βββ CommandService - Submit transactions (relayer/operator)
β βββ UpdateService - Stream events
β βββ StateService - Query active contracts
β
βββ HTTP API (port 5013) - Health/version checks
Why gRPC (not JSON API):
- First-class, production-grade API
- Streaming with offsets for reliability
- Built-in deduplication via command IDs
- Generated Go stubs from protobuf definitions
Note: gRPC connections through ALBs/proxies require ALPN to be disabled (grpc-go >= 1.67 enforces ALPN by default). The SDK uses
expcreds.NewTLSWithALPNDisabledto handle this.
Canton uses JWT-based authorization via OAuth2:
// JWT claims structure
{
"actAs": ["BridgeOperatorParty"], // Can submit commands
"readAs": ["BridgeOperatorParty"], // Can read events
"exp": 1234567890
}The middleware authenticates via OAuth2 to obtain JWT tokens. User transfers are submitted by the operator party using Interactive Submission with the user's external party key.
- Bidirectional & Independent - Two separate one-way flows that don't depend on each other
- At-Least-Once Delivery - Every event is guaranteed to be processed at least once
- Idempotency - Database-backed deduplication prevents duplicate processing
- Crash Recovery - Persisted offsets allow resuming from exact position after restart
Offsets (Checkpoints):
- Canton:
LedgerOffset(absolute string) - Ethereum:
BlockNumber - Stored in
chain_statetable - Updated only after successful processing
Idempotency:
- Every event has a unique ID
- CantonβEth: Canton Event ID
- EthβCanton: Hash of
(TxHash, LogIndex) - Checked against
transferstable before processing
The API Server runs periodic reconciliation (every 5 minutes):
- Query all
CIP56Holdingcontracts from Canton - Group by party and token
- Update cached balances in PostgreSQL
- Log any stuck transfers for investigation
All users are external parties on Canton, with the API Server custodially holding their secp256k1 signing keys:
- Same elliptic curve as Ethereum (enables future MetaMask Snap trustless signing)
- Keys encrypted at rest with
CANTON_MASTER_KEY(AES-256-GCM) - Stored in PostgreSQL
userstable - Transactions use the Interactive Submission API (PrepareSubmission/ExecuteSubmission)
This enables MetaMask users to interact without Canton tooling and removes the ~200 internal party limit. Trade-off: Users trust the API Server with their Canton keys (mitigated by future MetaMask Snap integration).
- Fast balance queries from PostgreSQL
- Periodic reconciliation ensures consistency
- Canton ledger is always authoritative
Users are identified by keccak256(evmAddress):
- Links EVM identity to Canton party
- Enables cross-chain user lookup
- Stored in
FingerprintMappingDAML contract
Native Canton tokens use synthetic addresses:
- DEMO:
0xDE30000000000000000000000000000000000001 - Allows MetaMask to "import" Canton-native tokens
| Service | Port | Protocol |
|---|---|---|
| API Server | 8081 | HTTP (JSON-RPC) |
| Anvil (Ethereum) | 8545 | HTTP (JSON-RPC) |
| Canton gRPC | 5011 | gRPC |
| Canton HTTP | 5013 | HTTP |
| PostgreSQL | 5432 | PostgreSQL |
| Relayer Metrics | 9090 | HTTP |