End-to-end instructions to get CantonStake running locally, from zero to a working demo with real Polygon Amoy transactions and on-ledger FeaturedAppActivityMarkers.
Estimated time: 3–4 hours for the first run. 15 minutes to restart a clean environment afterwards.
Before you start, make sure you have:
- Docker Desktop with at least 8 GB memory allocated (CN Quickstart needs it). Open Docker Desktop → Settings → Resources and set Memory to 8 GB.
- Node.js 20+ and npm
- Git
- MetaMask browser extension (or any WalletConnect-compatible wallet)
- Access to the CN Quickstart GitHub repo. If you get 404s, email Digital Asset — access is gated and requires a sponsoring Super Validator contact.
- A throwaway Ethereum wallet with a few 0.1+ POL on Amoy. Get testnet POL from https://faucets.chain.link/polygon-amoy or the Polygon Discord faucet bot. Do not use a wallet that holds real funds.
Check them:
docker --version # 20.x or higher
docker compose version # v2
node --version # v20.x
npm --version
git --versionPick a parent directory. We'll put cn-quickstart and cantonstake as siblings.
mkdir ~/canton-work && cd ~/canton-work
git clone https://github.com/digital-asset/cn-quickstart.git
# Then drop the cantonstake/ directory here (extract the zip/tar you received)
ls
# cn-quickstart cantonstakeThis gets you a full local Canton network: an App Provider participant, an App User participant, a Super Validator (synchronizer), a Canton Coin wallet, and the Splice support services.
cd ~/canton-work/cn-quickstart/quickstart
direnv allow # If you use direnv; otherwise skip
docker login # Required for Digital Asset artifact access
make install-daml-sdk # Slow — installs Daml SDK. 5–10 minutes.
make setupWhen make setup prompts:
- Enable OAuth2: No (simpler for the hackathon; you can turn on later)
- Enable Observability: No (saves RAM)
- Enable TEST MODE: No
- Party hint: leave blank (uses default)
Then:
make build # 3–5 minutes
make start # Starts all the containers. 2–3 minutes until everything is green.In a second terminal, watch the logs:
cd ~/canton-work/cn-quickstart/quickstart
make capture-logs
# ctrl-c to stop — logs are in quickstart/logs/Verify it's up:
make status
# All services should be "Up" and "healthy"Also open these in your browser:
- http://wallet.localhost:2000 — the Canton Coin wallet (for self-featuring later)
- http://localhost:2975/v2/state/ledger-end — the JSON Ledger API health check for the App Provider. You should see JSON back.
- The licensing demo at http://localhost:3000 proves the full stack is wired up. (We will replace this UI with CantonStake's later, so stop the demo frontend before running ours:
docker stop quickstart-frontend-1— name may differ; check withdocker ps.)
Memory tip: If containers flake out, bump Docker Desktop memory to 10 GB and run
make clean-allthen start over.
This is the EVM side — a real on-chain contract on a real testnet.
Go to https://faucets.chain.link/polygon-amoy, connect the throwaway wallet, claim 0.01 POL. Repeat a few times if needed — you want about 0.1 POL total for deploying + funding the reward pool.
cd ~/canton-work/cantonstake/evm
cp .env.example .env
# Edit .env — paste your deployer private key:
# DEPLOYER_PRIVATE_KEY=0xabc123...
# (Optional) POLYGONSCAN_API_KEY=... for verification laternpm install
npm run compile
npm run deploy:amoyYou should see:
Deploying with: 0xYourWallet
Balance: 0.15 POL
1. Deploying MockValidatorShare...
MockValidatorShare deployed at: 0xABCDEF1234...
2. Funding reward pool (0.05 POL)...
Funded: 0xTransactionHash
3. Done. Verify with:
npm run verify:amoy:deployed
Copy the MockValidatorShare deployed at: address. You'll need it in three places:
backend/.envasMOCK_VALIDATOR_SHARE_ADDRESSfrontend/.env.localasNEXT_PUBLIC_MOCK_VALIDATOR_SHARE- Root
.env(for docker compose) asMOCK_VALIDATOR_SHARE_ADDRESS
npm run verify:amoy:deployed
# or: npm run verify:amoy -- 0xABCDEF1234...If the contract needs more mock reward liquidity later:
FUND_AMOUNT_POL=0.05 npm run fund:amoyOpen https://amoy.polygonscan.com/address/0xABCDEF1234... — you should see the contract creation transaction and the 0.05 POL balance.
The Daml contracts depend on splice-api-featured-app-v1.dar to create FeaturedAppActivityMarker contracts. LocalNet bundles it, but we need to copy it into our package's dependencies folder.
cd ~/canton-work/cantonstake/daml/CantonStake
mkdir -p .daml/dars
# LocalNet stores Splice DARs inside its splice container.
# Copy the one we need out:
docker cp $(docker ps -qf "name=splice-app-provider"):/app/dars/splice-api-featured-app-v1.dar ./.daml/dars/
# Verify:
ls -la .daml/dars/
# splice-api-featured-app-v1.darIf the container name differs, find it with docker ps | grep splice and adjust the cp command. If the file is in a different path inside the container, try /app/resources/dars/ or daml/dars/.
daml build
# Builds .daml/dist/cantonstake-0.0.1.dardaml test
# Should pass: testHappyPath, testCannotUnbondFromPendingIf tests fail, the state machine logic has a regression — fix before moving on.
In a new terminal, open the Canton Console:
cd ~/canton-work/cn-quickstart/quickstart
make canton-consoleInside the console:
val darPath = "/shared/cantonstake-0.0.1.dar"
participants.app_provider.dars.upload(darPath)You'll need to make the DAR available to the container first. The simplest way:
# From outside Canton Console, in a new terminal:
docker cp ~/canton-work/cantonstake/daml/CantonStake/.daml/dist/cantonstake-0.0.1.dar \
$(docker ps -qf "name=canton"):/tmp/cantonstake-0.0.1.darThen in Canton Console:
participants.app_provider.dars.upload("/tmp/cantonstake-0.0.1.dar")Still in Canton Console:
val csHint = "CantonStake"
val cs = participants.app_provider.parties.enable(csHint)
println(s"App Provider party: ${cs.toProtoPrimitive}")
val aliceHint = "Alice"
val alice = participants.app_provider.parties.enable(aliceHint)
println(s"Delegator party: ${alice.toProtoPrimitive}")Copy both party IDs. You'll paste them into the env files. They look like CantonStake::1220a0db3761....
daml damlc inspect-dar ~/canton-work/cantonstake/daml/CantonStake/.daml/dist/cantonstake-0.0.1.dar | head -10You'll see something like:
DAR archive contains the following packages:
cantonstake-0.0.1-a1b2c3d4... "a1b2c3d4..."
The hash after the version is your package id. Update backend/src/canton.ts:
export const TEMPLATES = {
StakingRequest: "a1b2c3d4...:CantonStake.Staking:StakingRequest",
StakingPosition: "a1b2c3d4...:CantonStake.Staking:StakingPosition",
} as const;(Alternatively, keep the #cantonstake: prefix which uses package name resolution — works if you have only one version of the package uploaded.)
On LocalNet you can self-feature — no governance vote required. This gives your app party a FeaturedAppRight contract, which is what lets FeaturedAppActivityMarker actually convert into AppRewardCoupon.
Go to http://wallet.localhost:2000
Log in as CantonStake (the party you allocated). If prompted to tap for CC, do so — you'll need a small balance.
Look for a "Self-feature" or "Request Featured Status" option in the wallet UI. Tap it.
If the option is missing: the wallet version may differ. The alternative is to create the
FeaturedAppRightcontract directly via the Canton Console:// In Canton Console, as the DSO party // (see https://docs.sync.global/app_dev/daml_api/index.html for the full flow)Or via a direct gRPC/JSON call. See the Featured App How-To for details.
After self-featuring, the wallet UI shows the new FeaturedAppRight contract. Copy its contract id (starts with 00...). This goes into backend/.env as FEATURED_APP_RIGHT_CID.
Alternatively query it from Canton Console:
participants.app_provider.ledger_api.state.acs.of_party(cs)
.filter(_.templateId.contains("FeaturedAppRight"))
.foreach(c => println(c.contractId.coid))cd ~/canton-work/cantonstake/backend
cp .env.example .envEdit .env:
PORT=4000
LOG_LEVEL=info
AMOY_RPC_URL=https://rpc-amoy.polygon.technology
MOCK_VALIDATOR_SHARE_ADDRESS=0xABCDEF1234... # From step 3c
CANTON_JSON_API_URL=http://localhost:2975 # LocalNet App Provider
CANTON_APP_PROVIDER_PARTY=CantonStake::1220a0db... # From step 4e
CANTON_AUTH_TOKEN= # Empty — OAuth disabled
FEATURED_APP_RIGHT_CID=00abc123... # From step 5c
DEMO_MODE=true # Enables manual reward round trigger
Test it:
npm install
npm run devYou should see:
[orchestrator] watching 0xABCDEF1234... on Amoy
cantonstake backend listening on :4000
orchestrator running
Ping the health endpoint:
curl http://localhost:4000/api/health
# {"status":"ok","cantonJsonApi":"...","validatorShare":"...","featuredAppRight":"configured","time":"..."}If featuredAppRight says missing, revisit step 5. If there are 500 errors about the JSON API, check your CANTON_APP_PROVIDER_PARTY and that LocalNet is still running.
cd ~/canton-work/cantonstake/frontend
cp .env.example .env.localEdit .env.local:
NEXT_PUBLIC_BACKEND_URL=http://localhost:4000
NEXT_PUBLIC_MOCK_VALIDATOR_SHARE=0xABCDEF1234... # From step 3c
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= # Optional
NEXT_PUBLIC_MOCK_LOOP_PARTY_ID=Alice::1220... # Hosted delegator party for mock Loop
Install and run:
npm install
npm run devOpen http://localhost:3000. You should see the landing page.
In MetaMask:
- Networks → Add network → Add network manually
- Name: Polygon Amoy
- RPC URL:
https://rpc-amoy.polygon.technology - Chain ID:
80002 - Currency Symbol:
POL - Block Explorer:
https://amoy.polygonscan.com
Make sure the wallet you use for staking is funded with ~0.1 POL from the faucet. Use the same wallet you're tracking as Alice on the Canton side conceptually — in this MVP the mapping is just by EVM address, not cryptographic identity.
- Open http://localhost:3000
- Connect MetaMask (Amoy)
- Go to Stake → enter
0.5, click Stake now - Watch the execution trace:
- 01 Create StakingRequest → done (green dot)
- 02 buyVoucher → MetaMask popup → sign → running → done
- 03 Orchestrator catches ShareMinted → running (backend terminal shows
[ShareMinted] user=0x... tx=0x...) - 04 StakingRequest_Accept → done (backend terminal shows
-> accepted. tx=...)
- Go to Positions → table shows one Bonded row, markers: 1
- Click Unbond → MetaMask popup → sign → after confirmation, row becomes Unbonding, markers: 2,
ready atshows ~60 seconds from now - Wait 60–75 seconds (backend release checker runs every 15s). Row becomes Released.
- Go to Rewards → markers emitted: 2, bonded POL: 0 (position released), estimated CC: shown
If all five trace rows go green without manual intervention, the demo works end-to-end.
In a new terminal:
cd ~/canton-work/cn-quickstart/quickstart
make shell # Opens Daml ShellInside Daml Shell:
active CantonStake.Staking:StakingPosition
active Splice.Api.FeaturedAppRightV1:FeaturedAppActivityMarker
You'll see your actual contracts, the markers that were emitted, and the beneficiary splits — all on the ledger.
Allocate 10 GB to Docker Desktop. Run make clean-all, then make start.
OAuth is enabled on LocalNet but you set CANTON_AUTH_TOKEN=. Either disable OAuth at make setup time (redo setup), or get a bearer token from the Keycloak admin UI at http://localhost:8080.
Your delegator EVM address on the frontend doesn't match the MetaMask account that signed. Or the amount is different (e.g. 0.5 vs 0.50). The matcher compares exact decimal strings. Normalize on the frontend side to parseFloat(amount).toString() if needed.
The FeaturedAppRight contract isn't active, or its provider party doesn't match the appProvider party on the StakingPosition. Double-check you self-featured the same party you're using as CANTON_APP_PROVIDER_PARTY.
The reward pool in MockValidatorShare is low. Send more POL to the contract address (it has a receive() function).
Public Amoy RPCs rate-limit. Get a free dedicated RPC from Alchemy, Infura, or QuickNode and replace AMOY_RPC_URL in both evm/.env and backend/.env.
Step 4a didn't copy the DAR. Check the running Splice container:
docker ps | grep splice
docker exec -it <splice-container-name> ls /app/darsAdjust the docker cp command to the correct path.
Template ID mismatch. Rerun daml damlc inspect-dar and update backend/src/canton.ts TEMPLATES with the actual package id.
# Stop CantonStake
cd ~/canton-work/cantonstake
docker compose down
# Stop LocalNet
cd ~/canton-work/cn-quickstart/quickstart
make stop
# Full reset:
make clean-allThe 3-minute script in the README is the sharp version. The full 10-minute walkthrough adds:
- Show the Daml source code on Staking.daml — point at the marker emission and the beneficiary split. "This is the entire trust story. Four lines of Daml."
- Show
backend/src/orchestrator.ts— point at the viem event watcher. "73 lines of TypeScript. The orchestrator is minimal on purpose." - Show
MockValidatorShare.sol— point at the event signatures. "Matches Polygon's real interface. This flow is a drop-in swap for mainnet on Ethereum." - In Daml Shell:
active Splice.Api.FeaturedAppRightV1:FeaturedAppActivityMarker— "these are real ledger contracts. Super Validator automation converts them into reward coupons."
- Replace MockValidatorShare with real mainnet integration. Deploy the frontend + backend against Ethereum mainnet where Polygon's real
StakeManagerandValidatorSharecontracts live. - Loop SDK integration for wallet-level party management. Once Loop supports custom DAR (or as a pure CC wallet connector for the rewards page only).
- Multi-chain support. Cosmos (ATOM), Polkadot (DOT), Moonbeam (GLMR). Each adds a new backend connector but reuses the same Daml state machine.
- Featured App mainnet application. Submit via https://canton.foundation/featured-app-request/ — takes weeks of 2/3 Super Validator vote but converts the DevNet demo into real CC revenue.
- Auto-compound premium tier. Daml choice that reinvests claimed rewards into a new StakingPosition, emitting an additional marker.
- Ledger hardware support. Add
@ledgerhq/connect-kitas a wagmi connector.