Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.dar binary
31 changes: 24 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ A README may state that a contract exists and link to it. It may not restate it.
| Node | 24 | Exact version pinned via root `.nvmrc`; inherits to every Node subproject. Root and the four Node subprojects all declare `engines.node` at `>=24.15.0`, which is what jsdom 30 requires |
| Container runtime | Docker | Required by the `@bootnodedev/canton-barebones` LocalNet; nothing in this repository builds an image |
| LocalNet | @bootnodedev/canton-barebones | Pinned exact in root devDependencies and reached through `pnpm exec canton-barebones`, so the version is the one in `package.json`. Nothing about its config is committed: `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 which nginx serves no `/api/validator` or `/api/scan`. The Splice checkout and the runtime env land in `.canton-localnet/.generated/` |
| Token registry | @bootnodedev/canton-token-forge | Read-only CIP-56 registry service. A git dependency pinned to `v0.2.0` and reached through `pnpm exec canton-token-forge-registry`. Its `prepare` is what compiles `registry/dist`, so `pnpm-workspace.yaml`'s `allowBuilds` must carry the resolved git specifier or the bin resolves to nothing. `scripts/dev-stack.sh` runs it on 3013, configured entirely from the block `scripts/bootstrap-vesting.mjs` prints |
| Commit linting | commitlint + husky | Enforced via root `.husky/commit-msg` |
| Lint / format | Biome | One root `biome.json` and a single root `@biomejs/biome`; per-project specifics live in `overrides`. No per-subproject Biome install or config. `pnpm lint` = `biome check --error-on-warnings` (warnings fail); standalone SVG assets are excluded |
| Pre-commit | lint-staged | Two passes from `.husky/pre-commit`, because only the first writes: `.lintstagedrc.format.mjs` runs root Biome (`biome check --write`) across `canton-connect/`, `canton-dappbooster/`, `canton-theme/`, `dapp/frontend/` and `scripts/`, then `.lintstagedrc.mjs` runs the read-only gates — the tests, the doc check and the anatomy check — concurrently. One pass would let a reformat land mid-parse |
Expand All @@ -90,14 +91,20 @@ A README may state that a contract exists and link to it. It may not restate it.
| [`canton-dappbooster/`](canton-dappbooster/) | L2 headless UI components for Canton dApps (tsdown-built, zero styling), plus the light/dark/system theme runtime that drives `data-theme`, plus the pure utilities the components are built 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) |
| [`canton-theme/`](canton-theme/) | L3 plain-CSS theme for the kit: `--cnc-*` tokens + prestyled defaults, consumed by importing its CSS. | CSS | n/a (library) |

Two things the loop needs are not subprojects but dependencies. wallet-service ships from
Three things the loop needs are not subprojects but dependencies. wallet-service ships from
[BootNodeDev/canton-wallet-service](https://github.com/BootNodeDev/canton-wallet-service),
arrives as a git dependency pinned to a tag, and `scripts/dev-stack.sh` runs it on
port 3010 through `pnpm exec canton-wallet-service`. The LocalNet ships from
[BootNodeDev/canton-barebones](https://github.com/BootNodeDev/canton-barebones), is a pinned
devDependency whose config `scripts/dev-stack.sh` scaffolds into the gitignored
`.canton-localnet/` and drives there over `pnpm exec`.

The token registry ships from
[BootNodeDev/canton-token-forge](https://github.com/BootNodeDev/canton-token-forge), arrives as a
git dependency pinned to a tag, and `scripts/dev-stack.sh` runs it on port 3013 through
`pnpm exec canton-token-forge-registry`. That repository also builds both DARs under `vendor/`;
`vendor/PROVENANCE.md` records which release each came from.

## Code Style

- All source code in English regardless of conversation language.
Expand Down Expand Up @@ -333,7 +340,13 @@ package, because only `canton-dappbooster` splits markup from styles across a pa

- 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 <subproject>` or by using `pnpm -C <subproject> run <script>`. The root `package.json` is the whole local loop, in order: `mint-token`, `build-dar`, `deploy-dar -- <dar>`, `bootstrap`, `app:dev`. Docs and `dev-stack.sh` use those names, not the underlying commands, so the implementation can move without a doc sweep. There is no `format` script anywhere: `lint:fix` is `biome check --write`, which formats too.
- Run a subproject script either by `cd <subproject>` or by using `pnpm -C <subproject> run <script>`. The root `package.json` is the whole local loop, in order: `mint-token`, `build-dar`, `deploy-dar -- <dar>` for the built DAR and both `vendor/` binaries, `bootstrap`, `app:dev`. Docs and `dev-stack.sh` use those names, not the underlying commands, so the implementation can move without a doc sweep. There is no `format` script anywhere: `lint:fix` is `biome check --write`, which formats too.
- **`bootstrap` and `dev-stack.sh` share one contract: the `KEY=value` block bootstrap prints on
stdout.** Nothing is written to disk, so nothing can go stale, and a manual run gets a block it
can paste into a `.env`. `scripts/bootstrap-vesting.test.mjs` holds the printed keys and
`dev-stack.sh`'s `REGISTRY_ENV_KEYS` together; add a registry variable to one and that test
fails until the other follows. `LEDGER_API_TOKEN` is the one variable outside it, because
bootstrap never sees the bearer.
- **The LocalNet is not in this repository, and neither is its config.** It is
`@bootnodedev/canton-barebones`, a pinned devDependency driven with `start` / `stop` / `reset` in
the directory holding `canton-barebones.config.json`. `up` scaffolds that directory itself through
Expand All @@ -350,10 +363,12 @@ package, because only `canton-dappbooster` splits markup from styles across a pa
directory of its own via `CANTON_LOCALNET_DIR`, not in `.canton-localnet/`.
- `node scripts/add-component.mjs <PascalCaseName>` scaffolds a `canton-dappbooster` component
folder. Not wired into `package.json`: it is an authoring convenience, not part of the loop above.
- `pnpm run bootstrap` creates the vesting operator and its factory, which the
dApp cannot start without. Run it after the DAR is deployed. It writes no file: the dApp reads
both back off the ledger once a wallet connects, so nothing can go stale between the two, and
pointing the wallet at another participant is the whole of switching networks.
- `pnpm run bootstrap` creates the vesting operator and its factory, which the dApp cannot start
without, plus the instrument admin and its `DBT` InstrumentConfig. Run it after both DARs deploy.
It writes no file, printing the admin and instrument as a registry env block instead: the dApp
reads the operator and factory back off the ledger once a wallet connects, so nothing can go
stale between the two, and pointing the wallet at another participant is the whole of switching
networks.
- **One `.env`, at the root.** It is wallet-service's entire configuration, because the service
loads dotenv from the directory it starts in and `pnpm exec` starts it here; it also holds the
signing recipe `scripts/mint-token.mjs` reads and the token `scripts/deploy-dar.sh` sends. Both
Expand Down Expand Up @@ -385,7 +400,9 @@ See [`architecture.md`](architecture.md) for the system shape, subproject layout
- `canton-connect`: `pnpm test` (vitest + jsdom)
- `canton-dappbooster`: `pnpm test` (vitest + jsdom + Testing Library)
- root `scripts/`: covered by the root `pnpm test`, which appends
`node --test "scripts/*.test.mjs"` to the fan-out because `pnpm -r` skips the root package
`node --test "scripts/*.test.mjs"` to the fan-out because `pnpm -r` skips the root package.
`bootstrap-vesting.test.mjs` is the one test that reads a second file: it parses
`dev-stack.sh` for the key list, which is what makes the two-script contract checkable
- Kit components are tested inside `canton-dappbooster` (vitest + jsdom). `dapp/frontend`'s vitest run covers its pure logic wherever that lives; component/DOM behaviour and app+kit integration are out of scope there.
- From the root, `pnpm test` / `pnpm typecheck` / `pnpm build` / `pnpm knip` fan out across every workspace (`pnpm -r --if-present`). CI runs these minus `dapp/daml`'s build, which needs `dpm` and a network fetch of the Splice DARs.
- `pnpm docs:check` (typedoc plus `scripts/docs-check.mjs`) and `pnpm run check:anatomy` do not fan out: both read the two library packages directly, and typedoc has one config over both. `pnpm docs:build` writes the reference site to `typedoc/`.
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,13 @@ pnpm exec canton-barebones start
```bash
pnpm run build-dar
pnpm run deploy-dar -- dapp/daml/.daml/dist/amulet-vesting-0.0.1.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.
The two `vendor/` DARs are committed binaries and need no build; `canton-token-forge` goes
first, because `vesting` data-depends on it. See `vendor/PROVENANCE.md`.

### Wallet service

Expand All @@ -110,6 +114,20 @@ pnpm run bootstrap

**Note:** The step is only needed the first time. Run again if the Daml source changes or if LocalNet is reset.

### 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
LEDGER_API_TOKEN="$CANTON_BACKEND_TOKEN" pnpm exec canton-token-forge-registry
```

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

### Demo dApp

Start the Vesting demo app.
Expand Down
10 changes: 6 additions & 4 deletions architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
| 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 |
| 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 git dependency pinned to a tag, run on the host by `scripts/dev-stack.sh` |
| 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 + 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 |
| `canton-connect/` | TypeScript + React 19 | wagmi-style hooks wrapping the dapp-sdk facade |
Expand Down Expand Up @@ -69,6 +70,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 @@ -109,14 +111,14 @@ 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 the built DAR and 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
6 changes: 5 additions & 1 deletion knip.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
{
"$schema": "https://unpkg.com/knip@6/schema.json",
"ignoreExportsUsedInFile": true,
"ignoreDependencies": ["@mermaid-js/mermaid-cli", "@bootnodedev/canton-wallet-service"],
"ignoreDependencies": [
"@mermaid-js/mermaid-cli",
"@bootnodedev/canton-token-forge",
"@bootnodedev/canton-wallet-service"
],
"ignoreWorkspaces": ["dapp/daml"],
"workspaces": {
".": {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"devDependencies": {
"@biomejs/biome": "2.5.11",
"@bootnodedev/canton-barebones": "0.3.0",
"@bootnodedev/canton-token-forge": "github:BootNodeDev/canton-token-forge#v0.2.0",
"@bootnodedev/canton-wallet-service": "git+ssh://git@github.com/BootNodeDev/canton-wallet-service.git#v0.1.4",
"@commitlint/cli": "^21.0.1",
"@commitlint/config-conventional": "^21.0.1",
Expand Down
Loading