diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..93e81659
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+*.dar binary
diff --git a/.gitignore b/.gitignore
index 34dab813..14d2be45 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,7 @@ typedoc/
.env
.env.local
.env.fivenorth
+.env.registry
# typescript
*.tsbuildinfo
diff --git a/.lintstagedrc.mjs b/.lintstagedrc.mjs
index 0e702fcf..a696326d 100644
--- a/.lintstagedrc.mjs
+++ b/.lintstagedrc.mjs
@@ -1,9 +1,13 @@
+const examples = (files) => [...new Set(files.map((file) => file.match(/example-dapps\/[^/]+/)?.[0]))]
+
export default {
'canton-connect/src/**/*.{ts,tsx,js,jsx}': () => 'pnpm -C canton-connect test',
'canton-dappbooster/src/**/*.{ts,tsx,js,jsx}': () => 'pnpm -C canton-dappbooster test',
'example-dapps/*/src/**/*.{ts,tsx,js,jsx}': (files) =>
- [...new Set(files.map((file) => file.match(/example-dapps\/[^/]+/)?.[0]))].map(
- (example) => `pnpm -C ${example} test`,
+ examples(files).map((example) => `pnpm -C ${example} test`),
+ 'example-dapps/*/{api,scripts}/**/*.{ts,mjs,sh}': (files) =>
+ examples(files).map(
+ (example) => `pnpm -C ${example} exec vitest run --passWithNoTests api scripts`,
),
'canton-create/**/*.{ts,tsx,mjs,sh,json}': () => 'pnpm -C canton-create test',
'{canton-create/scaffold/*/scripts,scripts}/*.{mjs,sh}': () =>
diff --git a/CLAUDE.md b/CLAUDE.md
index cbe2bd9d..747b1883 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -11,6 +11,7 @@ Each subproject can layer its own `CLAUDE.md` for stack-specific deltas:
- [`canton-dappbooster/CLAUDE.md`](canton-dappbooster/CLAUDE.md): L2 component authoring and file layout
- [`canton-theme/CLAUDE.md`](canton-theme/CLAUDE.md): the L3 `--cnc-*` token naming convention
- [`example-dapps/amulet-vesting/CLAUDE.md`](example-dapps/amulet-vesting/CLAUDE.md): the vesting example's layout and naming deltas. Its seams are in [`example-dapps/amulet-vesting/architecture.md`](example-dapps/amulet-vesting/architecture.md), and its Daml in `daml/README.md`
+- [`example-dapps/token-vesting/CLAUDE.md`](example-dapps/token-vesting/CLAUDE.md): the token vesting example, the same layout and naming deltas. Its seams are in [`example-dapps/token-vesting/architecture.md`](example-dapps/token-vesting/architecture.md), and its vendored DARs in `vendor/PROVENANCE.md`
The dApp connects through any CIP-0103 wallet; no wallet lives in this monorepo.
@@ -40,6 +41,7 @@ Current distribution:
| `canton-connect/` | yes | shim | yes | yes, plus `architecture/` | Public hook API, the machine-owned lifecycle, the picker/adapter seams; pages for the connection machine and the popup close guard. |
| `canton-create/` | yes | shim | yes | yes | The `create-canton-dappbooster` installer. `CLAUDE.md` carries the flag rule, the scaffold rules and the demo recording, and `architecture.md` how a run and the scaffold layers fit together. A project reads the `README.md` files in `scaffold/`: `starter/`'s whole, and `daml-tooling/`'s and `localnet/`'s as sections appended to the app's. |
| `example-dapps/amulet-vesting/` | yes | shim | yes | yes | Canton Coin vesting example, published on its own. `CLAUDE.md` carries the page-owns-its-components layout and the naming rules an agent would otherwise get wrong, and architecture.md its internal seams. Its single Daml package (`amulet-vesting`) sits in `daml/` with a `README.md` of its own. `PROVENANCE.md` and `daml/PROVENANCE.md` record the vendored sources. |
+| `example-dapps/token-vesting/` | yes | shim | yes | yes | Token vesting example, published on its own. It started as a copy of the Amulet one, so its `CLAUDE.md` carries the same deltas plus the `api/` rule. Its two Daml packages are binaries in `vendor/`, and `vendor/PROVENANCE.md` records their releases and checksums. |
| `canton-dappbooster/` | yes | shim | yes | yes | L2 headless components; `CLAUDE.md` carries the folder-per-component layout an agent would otherwise get wrong, architecture.md the authoring seam (anatomy contract, L2/L3 split, Zag boundary). |
| `canton-theme/` | yes | shim | yes | no | Plain-CSS theme (L3); README covers the two CSS exports, `CLAUDE.md` the `--cnc-*` naming convention an agent adding a token would otherwise invent. |
@@ -62,7 +64,7 @@ A README may state that a contract exists and link to it. It may not restate it.
|----------|-----------|-------|
| Languages | TypeScript, Daml, Bash | TypeScript across the JS subprojects; Daml in each app's `daml/`; Bash and Node for the root `scripts/` and the scaffold's |
| Package manager | pnpm workspaces | Single root `pnpm-lock.yaml`; one root `pnpm install` links every workspace. Workspace layout, `linkWorkspacePackages` and `allowBuilds` live in `pnpm-workspace.yaml`. Root `package.json` orchestrates scripts via `pnpm -C
` |
-| Node | 24 | Exact version pinned via root `.nvmrc`; inherits to every Node package. Root, `canton-connect`, `canton-dappbooster` and `example-dapps/amulet-vesting` declare `engines.node` at `>=24.15.0`, which is what `jsdom` 30 requires. `canton-create` declares `>=22.12.0` |
+| Node | 24 | Exact version pinned via root `.nvmrc`; inherits to every Node package. Root, `canton-connect`, `canton-dappbooster` and both examples declare `engines.node` at `>=24.15.0`, which is what `jsdom` 30 requires. `canton-create` declares `>=22.12.0` |
| Container runtime | Docker | Required by the `@bootnodedev/canton-barebones` LocalNet; nothing in this repository builds an image |
| Wallet | `@canton-network/wallet-gateway-remote` | The CIP-0103 wallet the dApp connects to, from the Splice wallet kernel. Pinned exact in the root `devDependencies`, run by `pnpm run wallet-gateway` on port 3030, configured by the committed `canton-create/scaffold/localnet/wallet-gateway.config.json`, the file every project with a local network gets. `example-dapps/amulet-vesting` registers it as a `RemoteAdapter` in `additionalAdapters`, which is what lets a session survive a reload |
| LocalNet | `@bootnodedev/canton-barebones` | Pinned exact in `canton-create/scaffold/localnet/package.json`, the only manifest that declares it. `publicHoistPattern` in `pnpm-workspace.yaml` links it and its command into the root `node_modules`, so `pnpm exec canton-barebones` and the scaffold's scripts reach that one copy from the root. The repo commits none of its config. `canton-create/scaffold/localnet/scripts/localnet-config.mjs` scaffolds the gitignored `.canton-localnet/` from the tool's own template and turns on `validators.appUser.ui` and `sv.scanUI`. Without them, nginx serves no `/api/validator` or `/api/scan`. The Splice checkout and the runtime env land in `.canton-localnet/.generated/` |
@@ -86,6 +88,7 @@ A README may state that a contract exists and link to it. It may not restate it.
| Path | Purpose | Stack | Port |
|------|---------|-------|------|
| [`example-dapps/amulet-vesting/`](example-dapps/amulet-vesting/) | Canton Coin vesting dApp, an example `create-canton-dappbooster` scaffolds, published as `@bootnodedev/canton-example-amulet-vesting`. Every read and write goes through the connected CIP-0103 wallet via `canton-connect`; the operator's factory, the `AmuletRules` and the open mining round all arrive by explicit disclosure. Its `amulet-vesting` Daml model in `daml/` covers the factory, the proposal, the contract and the residual claim, and holds Canton Coin in escrow as a Splice `LockedAmulet`. It is vendored from [cc-vesting-contracts](https://github.com/BootNodeDev/cc-vesting-contracts), where its scenarios stay. Imported from `cn-dappbooster@feat/vesting-lite` (see its `PROVENANCE.md`). | Vite + React + Ark UI + lucide-react + Tailwind v4 + `zustand` + react-router + Daml | 3012 |
+| [`example-dapps/token-vesting/`](example-dapps/token-vesting/) | The same vesting dApp over a `DBT` instrument from [canton-token-forge](https://github.com/BootNodeDev/canton-token-forge), published as `@bootnodedev/canton-example-token-vesting`. A canton-token-forge registry serves the instrument and the one `InstrumentConfig` each write discloses, so no Splice reference data reaches a write. Its `vesting` and `canton-token-forge` DARs ship as binaries in `vendor/`, so it needs no `dpm`. Its bootstrap uploads them and writes the registry's settings to the gitignored `.env.registry`. `scripts/registry.sh` runs the registry, from the registry's git dependency. | Vite + React + Ark UI + lucide-react + Tailwind v4 + `zustand` + react-router | 3012, registry 3013 |
| [`canton-create/`](canton-create/) | The `create-canton-dappbooster` installer, published on its own. [`canton-create/architecture.md`](canton-create/architecture.md) covers how it builds a project. | TypeScript + Ink + React 19 + `tsdown` + `vitest` | n/a (command-line tool) |
| [`canton-connect/`](canton-connect/) | wagmi-style React hooks wrapping the `dapp-sdk` facade; the SDK owns discovery, the picker, the session and the transports | TypeScript + React 19 + `xstate` 5 + Biome | n/a (library) |
| [`canton-dappbooster/`](canton-dappbooster/) | L2 headless UI components for Canton dApps, built with `tsdown` and carrying no styling. It also holds the light/dark/system theme runtime that drives `data-theme`, and the pure utilities the components build on, the exact-decimal amount ones included. Styling lives in `canton-theme`. `src/index.ts` is the public API. `src/connect.ts` is the `/connect` sub-path, holding the components that read the wallet session so the main barrel stays free of the Canton SDK. | TypeScript + React 19 + `tsdown` + `vitest` + Biome | n/a (library) |
@@ -357,7 +360,7 @@ its `CLAUDE.md` owns that contract. Nothing below differs.
- Use **pnpm** only (never npm or yarn).
- This is a pnpm workspaces monorepo: one `pnpm install` from the repo root installs and links every package. There is no per-package install step.
-- Run a subproject script either by `cd ` or by using `pnpm -C run
+