CC Deposit Flow
CC is Rocky's native token, issued as a Daml contract on Canton Network. This page covers how users top up CC into their Rocky trading account, split into DevNet (current implementation) and MainNet (planned).
Concepts used: Canton Party, Daml Holding contract, Canton Bridge. Read Canton Network first if needed.
1. The shared core: DepositIntent
Regardless of environment, the final "on-chain → credit" step uses the same pipeline:
DepositIntent (Daml contract)
│
▼ canton-bridge consumer observes the Created event
NATS ledger.events.deposit_intent
│
▼ internal-ledger consumes
UPDATE ledger.accounts SET available = available + amount
WHERE user_id = $1 AND asset = 'CC'
DepositIntent is a Rocky-specific Daml template. The user (or their proxy) is the signatory:
template DepositIntent
with
user_party : Party -- signatory
exchange : Party -- observer (Rocky's app-provider)
asset : Text -- "CC", "USDC", ...
amount : Decimal
intent_id : Text -- UUIDv7 for idempotency
where
signatory user_party
observer exchange
...
The two environments differ only in how the DepositIntent is created:
| Step | DevNet | MainNet (planned) |
|---|---|---|
| Initiator | Frontend "top up" button | User wallet transfer |
| Actual transfer | None (dev-tool simulated) | Real Splice Holding transfer |
| Signatory authority | shadow-user-party-id (shared) | The user's own Party |
| Balance check | None | Must hold enough CC |
2. DevNet flow (current)
2.1 User perspective
- Log in to Rocky → click the user pill in the top right to open the account modal
- In the "CC Deposit" section, enter an amount (e.g.
1000) and click "Deposit CC" - ~1–3 seconds later the CC balance refreshes
2.2 API call chain
Browser
POST /api/perp/deposits/cc {user_id, amount}
│
▼
mtc-exchange (Next.js BFF)
/api/perp/deposits/cc/route.ts
└─ injects asset: "CC" then forwards:
POST http://api-gateway/v1/deposits/seed {user_id, asset: "CC", amount}
│
▼
api-gateway (Rust, port 8080)
dev_tools::seed_deposit
└─ gRPC call:
canton_bridge.SubmitDepositIntent({user_id_hex, asset, amount})
│
▼
canton-bridge (Rust, port 50071)
service::submit_deposit_intent
└─ creates DepositIntent contract with shadow_user_party_id as signatory:
Ledger API: CreateCommand(DepositIntent, {user_party, exchange, asset, amount})
│
▼
Canton Participant (DevNet)
Writes contract to Synchronizer, delivers to observer (Rocky app-provider)
│
▼
canton-bridge consumer (subscribed to active contracts stream)
Sees DepositIntent Created event
└─ NATS publish: ledger.events.deposit_intent {user_id, asset, amount, intent_id}
│
▼
internal-ledger (Rust, port 50051)
chain_events::handle_deposit_intent
└─ idempotent INSERT + UPDATE ledger.accounts
SET available = available + amount WHERE user_id = ? AND asset = ?
2.3 Source locations
| File | Role |
|---|---|
mtc-exchange/src/app/api/perp/deposits/cc/route.ts | Frontend BFF, injects asset: "CC" |
rocky-backend/services/api-gateway/src/routes/dev_tools.rs | seed_deposit handler (gated devToolsOnly on the BFF side) |
rocky-backend/services/canton-bridge/src/service.rs | submit_deposit_intent gRPC handler |
rocky-backend/services/canton-bridge/src/ledger/marshal.rs | build_deposit_intent_create Daml command builder |
rocky-backend/services/canton-bridge/src/consumer.rs | active-contracts stream consumer + NATS publisher |
rocky-backend/services/internal-ledger/src/chain_events.rs | NATS consumer + ledger.accounts update |
2.4 DevNet simplifications
- ⚠️ No balance check — users can "deposit" arbitrary amounts. The shadow user party has no real CC holding limit.
- ⚠️ No real transfer — the DepositIntent is created by the backend on the user's behalf; the user never holds any CC themselves.
- ⚠️ No wallet — users don't run their own Splice wallet instance.
- ✅ Pipeline is real — from
DepositIntentcreation throughledger.accountsis the same path that MainNet will use.
2.5 Verifying a DevNet deposit
docker exec rocky-backend-stack-postgres-1 psql -U rocky -d rocky -c \
"SELECT user_id, available FROM ledger.accounts WHERE asset = 'CC' ORDER BY available DESC"
Or just look at the CC balance in the TopNav account modal.
3. MainNet flow (planned, not implemented)
3.1 User perspective
- User already holds CC tokens in their Splice / Canton wallet (from airdrop, OTC, secondary market)
- On Rocky web they click "Deposit CC"
- Rocky shows a prompt: "Transfer X CC from your wallet to this custody address / Party"
- User executes the transfer in their Splice wallet (signs)
- canton-bridge sees the transfer event → after 1–2 block confirmations the balance is credited
- Rocky modal shows "Credited X CC"
3.2 Key differences
| Item | DevNet | MainNet |
|---|---|---|
| Real holder | None | User's own Party |
| Trigger | Backend creates DepositIntent on user's behalf | User wallet initiates |
| Asset source | Out of thin air (dev tool) | User's own CC Holding contract |
| Balance check | None | Daml contract ensures from.amount >= transfer.amount |
| Credit latency | ~1–3s | Depends on Canton Synchronizer block time (typically 2–5s) |
| Failure rollback | Doesn't happen | If wallet is short / signature fails, the transfer never lands on chain |
3.3 MainNet data flow (planned)
User's Splice Wallet
├─ Holding contract 1: Holding(party=alice, asset="CC", amount=1000)
│
▼ alice picks "Transfer to Rocky" in the wallet
Transfer Choice
├─ Enter amount + Rocky platform party id (known constant)
├─ Alice signs with her own key
▼
Canton Synchronizer
├─ Archives the source Holding
├─ Creates new Holding(party=rocky_custody, asset="CC", amount=1000)
├─ Creates DepositIntent(user_party=alice, exchange=rocky, asset="CC", amount=1000)
▼
canton-bridge consumer
└─ Same as DevNet: NATS publish → internal-ledger credits the account
⚠️ MainNet involves Rocky's custody model (funds-custody design), which is still in design. See Canton_Custody_Wallet_Architecture.md at the repo root.
3.4 MainNet open questions
- Wallet UX: how does the user know which party to transfer to? Deep link, or copy/paste?
- Amount precision: CC minimum unit (Daml
Decimaldefaults to 10-decimal precision). How many decimals to show in UI? - Fees: who pays the on-chain transfer fee (gas / synchronizer fee)? Platform absorbs, or user pays?
- Failure handling: transfer landed on chain but canton-bridge is down so credit never happens → retry / support path?
- Limits / KYC: per-tx / per-day caps? KYC required first?
- Cancel / refund: after DepositIntent is created but not yet credited, can the user cancel?
These will be addressed in a separate spec before MainNet migration.
4. Relationship to other assets (USDC, MTC)
Rocky currently supports three assets:
| Asset | Type | How to deposit |
|---|---|---|
| CC | Rocky native token | This document |
| USDC | Splice standard stablecoin | DevNet uses the same seed endpoint; MainNet uses Splice cross-chain bridge |
| MTC | Mining Token Credit | You don't deposit MTC; you earn it via trading, see the MiningReward.Claim flow |
All three share the same ledger.accounts (user_id, asset) storage, but the issuance mechanism differs:
- CC — platform mint (post-MainNet via Rocky's
app-providerparty) - USDC — cross-chain peg
- MTC — on-chain reward contract (see the
MiningRewardtemplate in MTC Mining Reward)
5. Related docs
- Canton Network
- Repo design docs:
Canton_Custody_Wallet_Architecture.md— custody modelparty-model-decision.md— Party model selection (drives shadow vs real party decision for MainNet)canton-privacy-arbitration-design.md— privacy arbitration design