Skip to content

feat(token-vault): add the token vault example - #275

Merged
gabitoesmiapodo merged 19 commits into
mainfrom
feat/253-token-vault
Nov 13, 2026
Merged

gabitoesmiapodo merged 19 commits into
mainfrom
feat/253-token-vault

Conversation

@gabitoesmiapodo

@gabitoesmiapodo gabitoesmiapodo commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Closes #253

Adds the token vault from canton-dappbooster-vault as an example, built on the token-vesting example's shell.

Changes

  • New example: deposit USDX for VSH vault shares, redeem VSH for USDX
  • Each deposit or withdraw is a proposal the vault accepts, the user funds and the vault settles
  • A vault operator, started by pnpm dev:token-vault, acts as the participant-hosted vault party
  • The bootstrap uploads the DARs in vendor/ and creates the parties, both instruments and the Vault
  • create-canton-dappbooster offers the token vault example
  • The kit exports readHoldings, which reads any party's holdings, such as the vault's

Deviations

  • useHoldings now skips holdings the party sees but does not own, which an instrument administrator used to get
  • Holding gains contractId, which an allocation names as its input

Acceptance criteria

  • The token vault runs as an example in this repo
  • create-canton-dappbooster can install it
  • A deposit, a withdraw and a cancel work end to end on a LocalNet

Test plan

Automated tests

  1. Run pnpm install
  2. Run pnpm -C example-dapps/token-vault test and pnpm -C canton-dappbooster test
  3. Expect both to pass

Manual verification

  1. Check out this branch and run pnpm dev:token-vault up
  2. Open http://localhost:3012 and connect through Wallet Gateway with client secret unsafe
  3. In the gateway, create a party with the wallet-kernel signing provider and set it as primary
  4. Tap USDX from the account menu and approve it in the gateway tab
  5. Deposit 100 USDX, approve, wait for the vault to accept, then click Fund and approve
  6. Expect the request to settle: USDX goes down by 100, VSH and vault holdings go up by 100

Breaking changes

  • Holding now requires contractId, so code that builds one by hand must add it
  • sumHoldings still takes holdings without it

Checklist

  • Self-reviewed my own diff
  • Tests added or updated
  • Docs updated (if applicable)
  • No unrelated changes bundled in

Screenshots

token-vault-overview.mp4
token-vault-1-vault token-vault-2-request token-vault-3-dark-menu

…ings

Move the holdings read out of useHoldings into src/utils/readHoldings.ts
and export it from the main barrel, so an app can read a party other than
the connected one, such as a vault's.

Each Holding now carries its contractId, which is what a transfer or an
allocation names as its input. The sumHoldings and Holding doc blocks,
doc-fixtures.d.ts and the tests follow.
Add example-dapps/token-vault as @bootnodedev/canton-example-token-vault,
on the same 0.7.0 library ranges as the other examples. The package holds
the manifest, the three tsconfigs, vite.config.ts on port 3012 with its
test block, .env.example, _gitignore, _pnpm-workspace.yaml and the env
schema in src/utils/env.ts.

vendor/ carries the two DARs the vault runs on: canton-token-forge.dar,
identical to token-vesting's, and canton-token-vault.dar.
The dApp deposits USDX for VSH vault shares and redeems shares for USDX
through the connected CIP-0103 wallet, with the Wallet Gateway registered
as a RemoteAdapter.

- backend/deployment.ts finds the vault through the user's rights on the
  templated /v2/users/{user-id}/rights route, matching vault-operator::
  only, and reads both InstrumentConfig rows with their disclosure blobs.
  Package ids come off each row's templateId.
- backend/requests.ts reads the queue through the AllocationRequest
  interface at one offset, scoped to this vault and to the leg the party
  sends.
- backend/LedgerBackend.ts taps, proposes, cancels and allocates, picking
  inputs off a fresh holdings read.
- providers/Backend keeps token-vesting's retry and generation guard;
  providers/Tokens adds the vault's own balance through readHoldings.
- The shell is token-vesting's, renamed to Canton Vault, with the faucet
  as the account menu's Tap item and no wrong-network strip.
- scripts/bootstrap-vault.mjs uploads both vendored DARs, then finds or
  allocates the token-issuer and vault-operator parties through
  /v2/parties, grants the authenticated user CanActAs on each, and
  creates the USDX and VSH InstrumentConfig contracts and the Vault only
  where missing. It warns when what it finds differs from what it would
  create, and writes no file. It runs as the example's bootstrap script.
- scripts/vault-operator.mjs is the vault's backstage signer. It accepts
  proposals, funds a withdraw's payout leg and settles, acting as the
  vault party through the JSON Ledger API. Its pure decisions live in
  scripts/vault-decisions.mjs.
- scripts/ledger-api.mjs is the JSON Ledger API client both share,
  reading CANTON_JSON_API_URL and CANTON_BACKEND_TOKEN from the
  example's .env.
- scripts/operator.sh start|stop runs the operator as stack:up and
  stack:down, with its pid and log in DEV_STACK_RUN_DIR.
- canton-create/src/apps.ts lists token-vault in EXAMPLES, so the menu
  and --example token-vault offer it.
- canton-create/README.md names it in the example list, both --example
  command blocks and the dApps table.
- The root package.json gains dev:token-vault, which runs dev-stack.sh on
  example-dapps/token-vault.
- knip.json gives the example a workspace whose entry is scripts/*.mjs,
  since operator.sh reaches the operator where knip cannot see it.
The bootstrap's comment said a participant answers 409 for a package it
already holds. A LocalNet takes the identical DAR again with a 200, so
the comment now says both, and the 409 branch stays for a participant
that refuses instead.
- README.md: what the example is, running it as numbered steps, how a
  deposit and a withdraw pass between the depositor and the vault
  operator, and configuring it for another network, with its script
  table and the rights the wallet's user needs there.
- CLAUDE.md: the participant-hosted vault party, the vault-operator hint
  the bootstrap, the operator and deployment.ts agree on, the two
  template-id spellings, the InstrumentConfig disclosure read in
  deployment.ts only, the queue read through AllocationRequest, and the
  vendor/ rules. AGENTS.md is the shim.
- architecture.md: the parts, the committed DARs table, the three
  interfaces, discovery, the deposit and withdraw sequence diagrams,
  what keeps the browser's and the operator's views in step, the
  bootstrap, and the operator's own guards.
- The root CLAUDE.md, architecture.md and CONTRIBUTING.md list the
  example beside the others.
The menu the demo shows lists the examples, so record canton-create/demo.svg
again now that it offers Token Vault.
readHoldings and useHoldings returned every Holding the party is a
stakeholder of. An instrument admin sees every holding of its token, so
readHoldings(ledgerApi, vault) reported the whole VSH supply as the
vault's own. Both now keep a view only when its owner is the party read.

sumHoldings takes holdings without a contractId again. It never reads
the field, so requiring it broke callers that build the list by hand.
…legs

queueRows sorted requests by requestId, whose dep- or wd- prefix put
every withdraw above every deposit whatever their age. It now sorts on
the settlement's requestedAt, newest first.

A request leg with no amount became a row asking to fund 0 USDX. It is
now dropped like a leg missing any other field.
findParty returned the first vault-operator:: party in /v2/parties page
order, while deployment.ts and vault-operator.mjs take the last in sort
order. With the hint under two namespaces, the bootstrap could ensure
the Vault for one party while the dApp and the operator used the other.
findParty now collects every local match and takes the last in sort
order.
The operator kept funding payouts and retrying settles for requests past
settleBefore, which the ledger refuses, so a payout locked the vault's
USDX for nothing. openRequests in vault-decisions.mjs drops them.

Each activeContracts call read its own ledger end, so a tick paired
requests from one snapshot with allocations from a later one. The tick
now reads one offset for its proposals, requests and allocations, and
runs discover() again each tick, so a Vault or InstrumentConfig made
again while the operator runs no longer leaves it on archived ids.
- VaultBackend and the store take one propose(kind) in place of
  deposit and withdraw, which only rejoined the kind they split
- the store drops loading, which nothing read, and clears its error
  only when a read succeeds, so a failing poll no longer blinks it
- VaultRequest drops requestId, which nothing read
- the kit's InstrumentId and tokenKey replace five hand-written id
  types and comparisons, and requests.ts builds its proposal filters
  from PROPOSAL_ENTITY
- KIND_TEXT and SENT live in pages/Vault/requestKind.ts for both forms
- the figures refresh keys on the requests alone and skips the first
  read after a load, which repeated reads Tokens had just made
- the unused explorer setting, Button variants and sizes, Modal props,
  theme tokens and styles copied from token-vesting are gone
- ledger-api.mjs gains authenticatedUser, which the bootstrap and the
  operator both read, and the bootstrap creates through submit
- the operator reads the user once, and each tick's four sets in
  parallel before it accepts anything
- vaultHoldings reuses viewOf and keeps only holdings the vault owns,
  as readHoldings does, since the vault sees every VSH holding
- allocationFor drops a requestId check the settlement comparison
  already covers, and the no-op synchronizerId spreads are gone
- the bootstrap resolves both packages at once
@gabitoesmiapodo gabitoesmiapodo self-assigned this Nov 11, 2026
@vercel

vercel Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
components.canton-dappbooster Ready Ready Preview Nov 13, 2026 7:20pm UTC
demo.canton-dappbooster Ready Ready Preview Nov 13, 2026 7:20pm UTC

Request Review

…ss' into feat/253-token-vault

# Conflicts:
#	architecture.md
#	canton-dappbooster/src/hooks/useHoldings.ts
- ACS reads send filter and verbose inside eventFormat; Canton 3.6 refuses them on top;
- the scripts move to POST preferred-packages and submit-and-wait, which 3.6 still serves;
- the tests' fake ledgers refuse the old fields, as a 3.6 participant does;
- ports main's 60b4291, which the example predates.
- the other workspaces moved in #270, after this example was copied;
- the lockfile drops jsdom 30 and the packages only it used.
- vault README and architecture.md: the browser also reads as the vault party and the issuer;
- vault CLAUDE.md and the root one: the operator reads the `InstrumentConfig` too;
- vault README: a late cancel fails rather than the wallet reporting the proposal gone;
- CONTRIBUTING.md: put back the sentence the vault section split;
- name token-vesting where "the token example" now has two candidates.

@fernandomg fernandomg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good

I updated with main and solved conflicts (and applied changes as did in 272 to make it work properly with newers versions), while on it CC found inconsistencies in the docs b72befb

@gabitoesmiapodo
gabitoesmiapodo merged commit d379f1d into main Nov 13, 2026
9 checks passed
@gabitoesmiapodo
gabitoesmiapodo deleted the feat/253-token-vault branch November 13, 2026 19:49
@github-project-automation github-project-automation Bot moved this from In review to Done in Canton - dAppBooster (#390) Nov 13, 2026

This branch was successfully deployed

2 active deployments
Preview – components.canton-dappbooster — 591ed26c Deployed Nov 13, 2026 by vercel[bot]
Preview – demo.canton-dappbooster — 591ed26c Deployed Nov 13, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Add cn dappbooster vault as an example

2 participants