Skip to content

feat: evolve the vesting dApp to Amulet on Splice LocalNet#85

Draft
fernandomg wants to merge 65 commits into
feat/vesting-litefrom
feat/vesting-amulet
Draft

feat: evolve the vesting dApp to Amulet on Splice LocalNet#85
fernandomg wants to merge 65 commits into
feat/vesting-litefrom
feat/vesting-amulet

Conversation

@fernandomg

Copy link
Copy Markdown
Member

Summary

No related issue. Stacked on top of feat/vesting-lite — the PR base is that branch, so
this diff is exactly the lite→amulet evolution. Brings the vesting dApp onto real Splice/Amulet:
AmuletBackend becomes the sole backend, every mutating choice carries an explicitly-disclosed
AppTransferContext fetched from the scan service, and grants lock real Canton Coin.

Changes

  • Add the amulet-vesting Daml package + vendored Splice deps
  • wallet-service: proxy the Splice scan service over JSON-RPC (scanApi; uses node:http
    so the Host header routes through nginx — fetch silently drops it)
  • Frontend: replace LiteBackend with AmuletBackend (sole backend). AppTransferContext
    (AmuletRules + the latest opened OpenMiningRound) is fetched from scan and explicitly
    disclosed on every mutating choice; createVesting selects the proposer's Amulet holdings;
    the operator (app-provider) is connectable as the proposer/funder
  • Frontend: restore the "Fund from" section — show the funder's live CC balance and block
    over-funding (availableFunds sums the party's Amulet holdings)
  • Add the re-runnable amulet-vesting LocalNet bootstrap (scripts/bootstrap-amulet.mjs)

Acceptance criteria

  • create → accept → withdraw → cancel → claim works end-to-end on Splice LocalNet
  • AppTransferContext disclosed per mutating choice; proposer input Amulets disclosed at Accept
  • Funder balance shown + over-funding guarded in the create form

Test plan

Automated tests

  • Frontend: npm test (56/56) + npm run typecheck + npm run build + npm run lint — all green
  • wallet-service: npm test (70/70) + npm run lint — green

Manual verification

Verified live against a running Splice LocalNet (SV + app-provider), two ways:

  • A payload-identical headless driver ran the full create→accept→withdraw→cancel→claim cycle.
  • The real frontend/UI drove the same cycle (partial withdraw leaves a >1.0 CC residual that
    cancel→claim consumes); "Fund from" showed the live balance (~235k CC) and blocked over-funding.
    No console errors.

Live-verified protocol facts (now encoded in AmuletBackend):

  • CIP-78 zero-fee holds (createFee/transferFee/lockHolderFee all 0.0)
  • ACS TemplateFilters require package-NAME ids (#name:Module:Entity), not package-ids
  • Accept (submitted by the receiver) must explicitly disclose the proposer's input Amulets
  • The OpenMiningRound must be the highest with opensAt <= now (the top round is often pre-opening)

Breaking changes

None. Additive — LiteBackend is removed in favor of AmuletBackend as the app's sole backend.

Checklist

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

Screenshots

None.

Imported from cc-vesting-contracts main@000c481 (nonconsuming factory) + vendored splice-amulet 0.1.19.
Add a `scanApi` JSON-RPC method that proxies to the Splice scan service, plus
`scanUrl`/`scanHost` config. The proxy uses node:http directly so the Host
header is actually sent — undici/fetch silently drops it (forbidden header),
which broke nginx vhost routing on LocalNet. Disclosure blobs for DSO-signed
AmuletRules/OpenMiningRound come only from scan, so the Amulet backend needs this.
Replace the bare-Canton LiteBackend with AmuletBackend as the sole backend,
targeting amulet-vesting on Splice LocalNet via the wallet-service. Every
mutating choice carries an AppTransferContext (AmuletRules + latest opened
OpenMiningRound) fetched from scan and explicitly disclosed; createVesting
selects proposer Amulet holdings and discloses the factory.

Key correctness points verified against a live LocalNet ledger:
- ACS TemplateFilters must use package-NAME ids (#name:Module:Entity); the
  JSON Ledger API rejects package-id-qualified ids in filters.
- Accept is submitted by the receiver but consumes the proposer's input
  Amulets, so those must be explicitly disclosed (CONTRACT_NOT_FOUND otherwise).
- Pick the highest OpenMiningRound whose opensAt <= now; the top round is
  often pre-opening and aborts the transfer with deadline-not-exceeded.
- The operator (app-provider) is the proposer/funder, so it is connectable.
Re-runnable bootstrap that creates the AmuletVestingFactory, allocates a
receiver party, taps Canton Coin, and writes the deploy-specific
amulet-parties.json (gitignored, like vesting-lite-parties.json).
Restore the "Fund from" section the lite app left hidden (lite grants were
unfunded). Add VestingBackend.availableFunds, implemented in AmuletBackend by
summing the party's Amulet holdings, and surface it on the create form: it shows
the funder's live CC balance and blocks submitting a grant that exceeds it.
fernandomg and others added 23 commits June 11, 2026 23:18
Wrap the Splice LocalNet dApp flow behind dev-stack actions, matching the
existing up/down conventions (port guards, PID/log files, wait helpers).

amulet-up preflights LocalNet (never boots it — that is the external
canton builder tool), runs the idempotent bootstrap-amulet.mjs, starts a
second wallet-service on :3020 pointed at Splice, and serves the dApp on
:3012. amulet-down stops the proxy + dApp via PID files (lsof :3020
fallback) and leaves LocalNet running; it avoids a broad tsx-watch pkill
so a coexisting bare-Canton :3010 instance survives.

Document the new actions in README's dev-stack table and AGENTS.md.
Dedup the clipboard-copy logic triplicated across GrantCard, GrantTable and
WalletControl into lib/clipboard, and the status label/tone mapping shared by
the card and detail views into statusPillLabel/statusPillTone.
… ids

Address review findings on the money path and platform safety:
- ClaimDialog now checks the re-lock floor against the full locked backing
  (unvested + unclaimed) rather than only the vested slice, matching the
  contract; partial grant withdraws no longer pass the UI then abort on-ledger.
- canClaim enables sub-floor dust withdraws when the grant is fully vested (a
  full drain leaves a zero remainder), so small balances are no longer stranded.
- Snap the last milestone cumulative fraction to exactly 1.0 on encode to match
  the contract's exact equality check (the UI validator tolerates 1e-9).
- selectAmuletCids pulls one extra holding past the target as headroom for
  holding-fee decay; document that amuletHoldings reports pre-decay initialAmount.
- viewAs fetches ledger-end once and shares it across the three ACS reads for a
  consistent snapshot and fewer round-trips.
- Replace crypto.randomUUID with a secure-context-safe uuid() fallback.
- shortenParty keeps short fingerprints whole instead of duplicating their ends.
- Drop redundant console.warn diagnostics already carried by thrown errors.
- Modal: wire the visible title/description via aria-labelledby/aria-describedby
  instead of aria-label so the description is announced.
- Route-level lazy loading + a Suspense boundary in AppShell; each page is now its
  own chunk, keeping framer-motion out of the entry bundle.
- Add a skip-to-content link and a main landmark id.
- Escape now closes the wallet, connect, and dashboard-filter dropdowns.
- Label the milestone date/percent inputs for screen readers.
- index.html: Open Graph, Twitter, theme-color, and canonical tags.
- wallet-service scanApi: pick the http/https transport and default port from the
  scanUrl scheme so an https scan host is not sent as cleartext to port 80.
Drop five icon components (Dashboard, History, Inbox, PlusCircle, Wallet) and the
useWalletStatus hook + its result type, none of which are referenced anywhere
after the rewrite.
Add unit tests for the new canClaim/remainderAfter/floorOk re-lock logic, the
previously-untested shortenParty (including the short-fingerprint regression),
and the secure-context uuid fallback paths.
The frontend architecture doc still described the pre-rewrite mocked app
(mockData, Sidebar/RoleToggle, /proposals route, mocked wallet, in-memory ACS).
Rewrite it to match the live structure: the backend/ ledger boundary
(VestingBackend/AmuletBackend over the wallet-service ledgerApi+scanApi),
StealthWallet, the dashboard tabs model, lazy-loaded routes, runtime config from
amulet-parties.json, and the re-lock helpers. Drop the stale "mocked src/wallet"
note in CLAUDE.md.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants