Skip to main content

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:

StepDevNetMainNet (planned)
InitiatorFrontend "top up" buttonUser wallet transfer
Actual transferNone (dev-tool simulated)Real Splice Holding transfer
Signatory authorityshadow-user-party-id (shared)The user's own Party
Balance checkNoneMust hold enough CC

2. DevNet flow (current)

2.1 User perspective

  1. Log in to Rocky → click the user pill in the top right to open the account modal
  2. In the "CC Deposit" section, enter an amount (e.g. 1000) and click "Deposit CC"
  3. ~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

FileRole
mtc-exchange/src/app/api/perp/deposits/cc/route.tsFrontend BFF, injects asset: "CC"
rocky-backend/services/api-gateway/src/routes/dev_tools.rsseed_deposit handler (gated devToolsOnly on the BFF side)
rocky-backend/services/canton-bridge/src/service.rssubmit_deposit_intent gRPC handler
rocky-backend/services/canton-bridge/src/ledger/marshal.rsbuild_deposit_intent_create Daml command builder
rocky-backend/services/canton-bridge/src/consumer.rsactive-contracts stream consumer + NATS publisher
rocky-backend/services/internal-ledger/src/chain_events.rsNATS 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 DepositIntent creation through ledger.accounts is 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

  1. User already holds CC tokens in their Splice / Canton wallet (from airdrop, OTC, secondary market)
  2. On Rocky web they click "Deposit CC"
  3. Rocky shows a prompt: "Transfer X CC from your wallet to this custody address / Party"
  4. User executes the transfer in their Splice wallet (signs)
  5. canton-bridge sees the transfer event → after 1–2 block confirmations the balance is credited
  6. Rocky modal shows "Credited X CC"

3.2 Key differences

ItemDevNetMainNet
Real holderNoneUser's own Party
TriggerBackend creates DepositIntent on user's behalfUser wallet initiates
Asset sourceOut of thin air (dev tool)User's own CC Holding contract
Balance checkNoneDaml contract ensures from.amount >= transfer.amount
Credit latency~1–3sDepends on Canton Synchronizer block time (typically 2–5s)
Failure rollbackDoesn't happenIf 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 Decimal defaults 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:

AssetTypeHow to deposit
CCRocky native tokenThis document
USDCSplice standard stablecoinDevNet uses the same seed endpoint; MainNet uses Splice cross-chain bridge
MTCMining Token CreditYou 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-provider party)
  • USDC — cross-chain peg
  • MTC — on-chain reward contract (see the MiningReward template in MTC Mining Reward)

  • Canton Network
  • Repo design docs:
    • Canton_Custody_Wallet_Architecture.md — custody model
    • party-model-decision.md — Party model selection (drives shadow vs real party decision for MainNet)
    • canton-privacy-arbitration-design.md — privacy arbitration design