Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
c4e76b3
build: vendor the canton-token-forge and vesting DARs
lmcorbalan Sep 4, 2026
0b32a1c
Merge pull request #158 from BootNodeDev/feat/149-vendor-dars
lmcorbalan Sep 4, 2026
99d1213
build: add the canton-token-forge registry service as a dev dependency
lmcorbalan Sep 4, 2026
47406b8
Merge pull request #159 from BootNodeDev/feat/150-registry-dependency
lmcorbalan Sep 4, 2026
10538e7
feat(scripts): seed the DBT instrument admin and config in bootstrap
lmcorbalan Sep 4, 2026
385c1f5
test(scripts): pin the registry env block's exact shape and fix CLAUD…
lmcorbalan Sep 4, 2026
189ef5e
docs: scope the bootstrap bullet's cannot-start-without claim to the …
lmcorbalan Sep 4, 2026
81401da
fix(scripts): preflight the token-forge package and fix bootstrap's m…
lmcorbalan Sep 4, 2026
7e66900
Merge pull request #161 from BootNodeDev/feat/152-bootstrap-instrument
lmcorbalan Sep 4, 2026
e339fda
feat(scripts): deploy the vendored DARs and run the token registry on…
lmcorbalan Sep 4, 2026
186f9e8
fix(scripts): apply the vesting-DAR wording and match the registry's …
lmcorbalan Sep 4, 2026
a90c207
fix(scripts): restart a stale registry, pass bootstrap the participan…
lmcorbalan Sep 4, 2026
8fdfe30
Merge pull request #162 from BootNodeDev/feat/153-dev-stack-registry
lmcorbalan Sep 4, 2026
321a183
fix(scripts): repair the main-module guards and scope the registry re…
lmcorbalan Sep 4, 2026
2ec76fc
Merge pull request #163 from BootNodeDev/feat/154-token-forge-side
lmcorbalan Sep 4, 2026
7bc00c6
feat(frontend): learn the instrument from the registry and move to th…
lmcorbalan Sep 7, 2026
4f55407
fix(frontend): name the registry when it is unreachable and correct t…
lmcorbalan Sep 7, 2026
e58639d
Merge pull request #172 from BootNodeDev/feat/166-registry-client
lmcorbalan Sep 7, 2026
69db6b2
feat(frontend): move the vesting choices onto the instrument config
lmcorbalan Sep 7, 2026
f777054
fix(frontend): repoint the vesting readers at the renamed fields and …
lmcorbalan Sep 7, 2026
1a3a2da
fix(frontend): let a registry failure be retried and normalize its ba…
lmcorbalan Sep 7, 2026
aeb67c8
Merge pull request #174 from BootNodeDev/feat/167-config-commands
lmcorbalan Sep 7, 2026
affadd9
build: bump canton-token-forge to v0.3.0 for the registry CORS support
lmcorbalan Sep 7, 2026
8339b12
feat(frontend): select token holdings instead of splitting, and add t…
lmcorbalan Sep 7, 2026
a57eefa
fix(frontend): refuse a non-positive grant total and repoint the arch…
lmcorbalan Sep 7, 2026
cbfc553
docs(frontend): record what Accept discloses to the receiver under wh…
lmcorbalan Sep 7, 2026
a176513
Merge pull request #176 from BootNodeDev/feat/168-holding-selection
lmcorbalan Sep 7, 2026
164c018
feat(frontend): forward the registry routes from the deployed origin
lmcorbalan Sep 7, 2026
862bd37
fix(frontend): test the registry forwarder and fix its url and error …
lmcorbalan Sep 7, 2026
be5dcff
Merge pull request #177 from BootNodeDev/feat/169-registry-proxy
lmcorbalan Sep 7, 2026
4da0fea
feat(frontend): show the DBT instrument and add the faucet action
lmcorbalan Sep 7, 2026
d5a4f10
fix(frontend): bind the config cid to its disclosure, name the amount…
lmcorbalan Sep 8, 2026
46810dc
Merge pull request #178 from BootNodeDev/feat/170-instrument-faucet
lmcorbalan Sep 8, 2026
7769830
fix(frontend): validate the trimmed registry base and dedupe the disc…
lmcorbalan Sep 8, 2026
f79f985
Merge pull request #183 from BootNodeDev/feat/155-registry-backend
lmcorbalan Sep 8, 2026
9e29d49
build(scripts): deploy only the vendored dars and drop the dpm requir…
lmcorbalan Sep 8, 2026
c8da332
Merge pull request #187 from BootNodeDev/feat/185-dev-stack-no-dpm
lmcorbalan Sep 8, 2026
b0af43e
build: delete the amulet-vesting package and its build
lmcorbalan Sep 8, 2026
e4f5649
Merge pull request #189 from BootNodeDev/feat/184-delete-amulet-package
lmcorbalan Sep 8, 2026
af2cea9
docs: drop the canton coin path from the repository docs
lmcorbalan Sep 8, 2026
9aad464
Merge pull request #190 from BootNodeDev/feat/186-docs-sweep
lmcorbalan Sep 8, 2026
668df58
Merge pull request #191 from BootNodeDev/feat/156-delete-amulet-path
lmcorbalan Sep 8, 2026
61e2c57
feat(dapp): reserve only the granted amount when a grant is created
lmcorbalan Sep 8, 2026
69af401
fix(dapp): repair a missing funding blob on the funder's next view
lmcorbalan Sep 8, 2026
e69083f
fix(dapp): bound the reconcile read and keep one blob per reserved ho…
lmcorbalan Sep 8, 2026
4aeafa4
fix(dapp): count a blobless holding as a miss and spend no budget on …
lmcorbalan Sep 9, 2026
5f02efb
Merge pull request #192 from BootNodeDev/feat/180-reserve-granted-amount
lmcorbalan Sep 9, 2026
0c98f28
feat(dapp): build the proposal cancel and reject commands
lmcorbalan Sep 9, 2026
2daafe0
feat(dapp): end an unaccepted grant from either side and forget what …
lmcorbalan Sep 9, 2026
3f67b16
feat(dapp): wire the proposal cancel and decline through the store
lmcorbalan Sep 9, 2026
f06c2e0
feat(dapp): offer the funder a cancel and the receiver a decline on a…
lmcorbalan Sep 9, 2026
b5dc7c7
docs(dapp): say that an unaccepted grant can now be ended from either…
lmcorbalan Sep 9, 2026
d104519
fix(dapp): pin the ending grant's role and give each pending-grant bu…
lmcorbalan Sep 9, 2026
75fec7e
fix(dapp): survive a browser that cannot record an ended grant, and p…
lmcorbalan Sep 9, 2026
169b360
fix(dapp): stamp the synchronizer on the submission itself and keep a…
lmcorbalan Sep 9, 2026
c47f0b2
fix(dapp): re-arm the close guard under StrictMode and stop a pending…
lmcorbalan Sep 9, 2026
eaa6b0a
Merge pull request #197 from BootNodeDev/feat/181-end-pending-grant
lmcorbalan Sep 9, 2026
fee830e
Merge branch 'main' into feat/157-token-forge-migration
lmcorbalan Sep 9, 2026
7577d0a
Merge pull request #200 from BootNodeDev/chore/199-merge-main
lmcorbalan Sep 9, 2026
6048f4a
fix: stop bootstrap orphaning holdings on restart and close the rest …
lmcorbalan Sep 10, 2026
6f1b1b8
fix(dapp): require the deployed env vars on Vercel alone, not in ever…
lmcorbalan Sep 10, 2026
7518821
fix: match the dApp's operator lookup to bootstrap's stable hint and …
lmcorbalan Sep 10, 2026
f9e9eff
Merge branch 'main' into feat/157-token-forge-migration
lmcorbalan Sep 10, 2026
3cc4beb
Merge branch 'main' into feat/157-token-forge-migration
gabitoesmiapodo Sep 11, 2026
b5eb0a6
docs: keep postcss in knip's ignoreDependencies
gabitoesmiapodo Sep 11, 2026
dd75b09
Merge remote-tracking branch 'origin/main' into feat/157-token-forge-…
gabitoesmiapodo Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,27 @@ CANTON_BACKEND_TOKEN=

RPC_URL=http://localhost:3010/rpc

# The token registry's own options, which dev-stack.sh passes through to it when set here.
# CORS_ORIGINS defaults to exactly http://localhost:3012, so the dApp opened on any other
# spelling of that address (127.0.0.1, a published port, a LAN name) needs it listed here.
# Quoted, and no space after the comma: dev-stack.sh sources this file as a shell script,
# where an unquoted space starts a second word and aborts `up` before anything starts.
#CORS_ORIGINS='http://localhost:3012,http://127.0.0.1:3012'

###############################################################################
# Frontend
###############################################################################
VITE_EXPLORER_URL=http://scan.localhost:4000
VITE_WALLET_RPC_URL=http://localhost:3010/rpc
VITE_REGISTRY_URL=http://localhost:3013

###############################################################################
# Vercel
###############################################################################
#WALLET_SERVICE_RPC_URL=http://localhost:3010/rpc
# Both are set on the deployed project rather than in this file, and neither can be localhost
# there: the bundle asks its own origin, and only the function names the registry.
#VITE_REGISTRY_URL=/api/registry
#REGISTRY_URL=https://registry.example
# Set by the build command in dapp/frontend/vercel.json, and by any other host that serves this
# bundle: it is what makes the two VITE_ keys above required instead of falling back to localhost,
# which an https page blocks as mixed content. Never set it for a local or CI build.
#DEPLOYED_BUILD=1
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.dar binary
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
- run: pnpm -r --filter '!./dapp/daml' run --if-present build
- run: pnpm build
- run: pnpm knip
- run: pnpm docs:check
- run: pnpm run check:anatomy
Expand Down
56 changes: 34 additions & 22 deletions CLAUDE.md

Large diffs are not rendered by default.

38 changes: 31 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ Local Canton development stack.
- Node 24 (>=24.15.0)
- pnpm 11.22.0
- Docker
- dpm (DAML SDK 3.4.11)

## Initial setup

Expand Down Expand Up @@ -81,17 +80,20 @@ pnpm exec canton-barebones start
- The first run pulls ~10 GB. If `start` exits 1 during splice migrations, run it again.
- Splice can take a few minutes to start.

### DAR build and deploy
### DAR deploy

`deploy-dar` requires LocalNet up and running.

```bash
pnpm run build-dar
# use the appropriate version for amulet-vesting-*.dar
pnpm run deploy-dar -- dapp/daml/.daml/dist/amulet-vesting-*.dar
pnpm run deploy-dar -- vendor/canton-token-forge.dar
pnpm run deploy-dar -- vendor/vesting.dar
```

**Note:** The step is only needed the first time. Run again if the Daml source changes or if LocalNet is reset.
**Note:** Safe to re-run. It reuses the operator, factory and instrument it finds on the ledger and
creates only what is missing, so the holdings and grants of an earlier run survive a `down` and `up`.
A LocalNet reset drops the parties with the ledger, so the next run creates them again. Both DARs are
committed binaries and need no build, so no DAML SDK is involved; `canton-token-forge` goes
first, because `vesting` data-depends on it. See `vendor/PROVENANCE.md`.

### Wallet service

Expand All @@ -109,7 +111,29 @@ Needs both LocalNet and wallet-service up and running.
pnpm run bootstrap
```

**Note:** The step is only needed the first time. Run again if the Daml source changes or if LocalNet is reset.
**Note:** Safe to re-run. It reuses the operator, factory and instrument it finds on the ledger and
creates only what is missing, so the holdings and grants of an earlier run survive a `down` and `up`.
A LocalNet reset drops the parties with the ledger, so the next run creates them again. A ledger
bootstrapped before this step became idempotent carries `vesting-operator-<stamp>` parties that no
run adopts; reset it once and the holdings from those runs are gone with it.

### Token registry

Needs the registry env block `bootstrap` printed above, plus `CANTON_BACKEND_TOKEN` from `.env` as the bearer.

```bash
source .env
# paste the block bootstrap printed, skipping its LEDGER_API_TOKEN placeholder line, then:
export LEDGER_API_URL ADMIN_PARTY INSTRUMENT_CONFIG_TEMPLATE_ID PREAPPROVAL_TEMPLATE_ID \
LOCKED_TOKEN_TEMPLATE_ID TRANSFER_INSTRUCTION_TEMPLATE_ID ALLOCATION_TEMPLATE_ID PORT
DOTENV_CONFIG_PATH=/dev/null LEDGER_API_TOKEN="$CANTON_BACKEND_TOKEN" pnpm exec canton-token-forge-registry
```

`DOTENV_CONFIG_PATH=/dev/null` is not optional: the registry loads dotenv from the directory it
starts in, which here is the repo root, so without it the whole of `.env` is read into the registry
process, `CANTON_AUTH_SECRET` included.

**Note:** `./scripts/dev-stack.sh up` automates this step, reading the block back out of its own bootstrap log.

### Demo dApp

Expand Down
36 changes: 19 additions & 17 deletions architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
| Subproject | Stack | Purpose |
| --- | --- | --- |
| LocalNet (external: [BootNodeDev/canton-barebones](https://github.com/BootNodeDev/canton-barebones)) | Node CLI over Docker Compose + the official Splice LocalNet bundle | Starts `sv + app-user`. A pinned devDependency, scaffolded by `dev-stack.sh` into the gitignored `.canton-localnet/` |
| `scripts/` | Bash + Node | The local loop: `dev-stack.sh`, the Splice dep fetch, the DAR build and upload, the token mint, the vesting bootstrap |
| `scripts/` | Bash + Node | The local loop: `dev-stack.sh`, the DAR upload, the token mint, the vesting bootstrap |
| `kit/` | Node + JSON | The tooling that reads the three libraries' source: the doc, anatomy and version gates, the release bump, and the typedoc configs. Grouped so a consumer scaffold deletes it whole; see [`CLAUDE.md`](CLAUDE.md) |
| wallet-service (external: [BootNodeDev/canton-wallet-service](https://github.com/BootNodeDev/canton-wallet-service)) | Node 24 + Express 5 + TypeScript + `@canton-network/wallet-sdk` | Bridge the wallet uses for external-party onboarding and participant JSON API calls. A root devDependency installed from npm, run on the host by `scripts/dev-stack.sh` |
| `dapp/frontend/` | Vite + React + Ark UI + Tailwind v4 + zustand + react-router | Canton Coin **vesting** dApp; every read and write goes through the connected CIP-0103 wallet via `canton-connect` |
| `dapp/daml/` | DAML | `amulet-vesting` DAR: the vesting factory, proposal, contract and residual-claim templates, escrowing real Canton Coin as a Splice `LockedAmulet`. Vendored from [BootNodeDev/cc-vesting-contracts](https://github.com/BootNodeDev/cc-vesting-contracts); its Splice data-dependencies are fetched, not committed |
| token registry (external: [BootNodeDev/canton-token-forge](https://github.com/BootNodeDev/canton-token-forge)) | Node + Express + TypeScript | Read-only CIP-56 registry over the `canton-token-forge` package: serves instrument metadata and the transfer-factory choice context. A git dependency pinned to a tag, run on the host by `scripts/dev-stack.sh` |
| `dapp/frontend/` | Vite + React + Ark UI + lucide-react + Tailwind v4 + zustand + react-router | `DBT` **vesting** dApp; every read and write goes through the connected CIP-0103 wallet via `canton-connect`, and the `InstrumentConfig` every write carries comes from the token registry |
| `canton-connect/` | TypeScript + React 19 | wagmi-style hooks wrapping the dapp-sdk facade |
| `canton-dappbooster/` | TypeScript + React 19 + tsdown | L2 headless UI components, zero styling, plus the theme runtime and the pure utilities under the components, exact-decimal amounts included |
| `canton-theme/` | CSS | L3 plain-CSS theme: `--cnc-*` tokens + prestyled defaults |
Expand All @@ -28,24 +28,26 @@ flowchart TD
au["Splice app-user<br/>JSON API http://localhost:2975"]
sv["Splice sv<br/>DSO / synchronizer side"]
scan["Scan<br/>http://scan.localhost:4000"]
dar["amulet-vesting DAR"]
dar["vendored DARs<br/>canton-token-forge + vesting"]
reg["canton-token-forge registry<br/>http://localhost:3013"]

fe -->|"AmuletRules + open mining round, off amulet.tap"| ws
fe <-->|"CIP-0103 provider: reads, writes, session"| wallet
wallet -->|"onboarding, prepare/execute, JSON API"| ws
fe -->|"instrument + InstrumentConfig disclosure"| reg
reg -->|"CANTON_BACKEND_TOKEN"| au
ws -->|"CANTON_BACKEND_TOKEN"| au
ws -->|"AmuletRules, mining rounds"| scan
au <--> sv
dar --> au
```

> `dapp/frontend` hosts the Canton Coin vesting dApp. Every ledger read and every submission goes
> through the wallet over CIP-0103, so the dApp only ever acts as the connected account and each
> write is signed by the account's own key. One call is not a ledger path: an Amulet-moving choice
> takes the current `AmuletRules` and open mining round as an argument, and no connected party is a
> stakeholder of either, so the dApp asks wallet-service's `amulet.tap` — a pure builder that
> submits nothing — and keeps the two disclosures its answer carries. Deployed, that one call goes
> through the app's own `/api/rpc` function, which forwards it and refuses every other method.
> `dapp/frontend` hosts the vesting dApp. Every ledger read and every submission goes through the
> wallet over CIP-0103, so the dApp only ever acts as the connected account and each write is signed
> by the account's own key. One call is not a ledger path: every choice that moves a holding takes
> the instrument's `InstrumentConfig` as an argument, and no connected party is a stakeholder of it,
> so the dApp asks the token registry's CIP-56 transfer-factory route and keeps the disclosure its
> answer carries. Deployed, those calls go through the app's own `/api/registry` function, which
> forwards three read-only routes and refuses the rest.

`app-user` is the primary local validator from the official Splice LocalNet
bundle. It is not a product user. `sv` provides the Super Validator / DSO side
Expand All @@ -70,6 +72,7 @@ State boundaries:
| wallet-service | `http://localhost:3010` | wallet bridge for onboarding and JSON API calls |
| CIP-0103 browser wallet | `http://localhost:3011` | browser wallet UI/provider, run from its own repo |
| dApp frontend | `http://localhost:3012` | example dApp |
| canton-token-forge registry | `http://localhost:3013` | CIP-56 registry, run from the env block `bootstrap` prints |
| app-user Wallet UI | `http://wallet.localhost:2000` | optional official Splice wallet UI |
| app-user Ledger API | `grpc://localhost:2901` | SDK/tools |
| app-user Admin API | `grpc://localhost:2902` | wallet-service/tools |
Expand Down Expand Up @@ -110,14 +113,13 @@ with the same script, configured manually in its LocalNet settings.

| Command | What it does |
| --- | --- |
| `./scripts/dev-stack.sh up` | the whole local loop: LocalNet, DAR, wallet-service on 3010, bootstrap, dApp dev server |
| `./scripts/dev-stack.sh down` | stop wallet-service and the dApp dev server, stop the LocalNet |
| `./scripts/dev-stack.sh up` | the whole local loop: LocalNet, DARs, wallet-service on 3010, bootstrap, token registry on 3013, dApp dev server |
| `./scripts/dev-stack.sh down` | stop the dApp dev server, the token registry and wallet-service, stop the LocalNet |
| `pnpm exec canton-barebones start` / `stop` / `reset` / `status` | the LocalNet itself, run from `.canton-localnet/` |
| `node scripts/localnet-config.mjs <dir>` | scaffold that directory and apply the flags nginx needs |
| `pnpm run mint-token` | generate a LocalNet dev JWT, offline |
| `pnpm run build-dar` | fetch the Splice deps, then compile the DAR with `dpm` |
| `pnpm run deploy-dar -- <dar>` | upload DAR to app-user JSON API |
| `pnpm run bootstrap` | create the vesting operator and its factory |
| `pnpm run deploy-dar -- <dar>` | upload DAR to app-user JSON API; called for both `vendor/` binaries |
| `pnpm run bootstrap` | create the vesting operator and its factory, the instrument admin and its `DBT` InstrumentConfig, and print the registry env block |
| `pnpm run app:dev` | start the dApp frontend |

`dev-stack.sh` shells out to the LocalNet tool in the directory passed as its second argument
Expand Down
12 changes: 10 additions & 2 deletions canton-dappbooster/src/utils/readInstruments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,15 @@ interface Page {
nextPageToken: string | undefined
}

// A registry that accepts the socket and never answers would otherwise leave the caller's own
// `await` pending for good, with nothing to render and no error to show for it.
const REQUEST_TIMEOUT_MS = 15_000

const get = async (url: string): Promise<unknown> => {
const response = await fetch(url, { headers: { accept: 'application/json' } })
const response = await fetch(url, {
headers: { accept: 'application/json' },
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
})
if (!response.ok) {
throw new Error(`${url} answered ${response.status}`)
}
Expand Down Expand Up @@ -63,7 +70,8 @@ const toPage = (value: unknown): Page => {
* Reads a registry's instrument metadata, following its pages up to a limit of 100, so the answer
* is the registry's whole catalogue and a registry that will not stop paging cannot hang the caller.
*
* @throws where either request answers anything but 200, or the reply is not JSON.
* @throws where either request answers anything but 200, does not answer within 15s, or replies
* with something that is not JSON.
*
* @example
* const instruments = await readInstruments('https://registry.example/api')
Expand Down
2 changes: 0 additions & 2 deletions dapp/daml/.gitignore

This file was deleted.

Loading