feat: vesting dApp via Carpincho external parties (Splice 0.6.7)#86
Draft
fernandomg wants to merge 150 commits into
Draft
feat: vesting dApp via Carpincho external parties (Splice 0.6.7)#86fernandomg wants to merge 150 commits into
fernandomg wants to merge 150 commits into
Conversation
Build the carpincho-wallet Chrome extension and attach a ready-to-load zip to GitHub releases on publish. Rewrites the manifest version from the release tag so the asset is reproducible from the tagged commit. Closes #26
- pin checkout and setup-node actions to commit SHAs - derive version once from tag, reuse for manifest and artifact name - validate Chrome version rules (segments 0-65535, no leading zeros) - fail when build output is empty before zipping - add concurrency guard per release tag
Run root biome check in the release workflow instead of scoping lint to carpincho-wallet. Bump @types/node to v24 to match .nvmrc. Type the manifest-rewrite plugin's parsed JSON. Add a test asserting the runtime provider version resolves from the root package.json via __APP_VERSION__.
…ase-ci ci: automate carpincho-wallet extension release packaging
Guard the carpincho-only steps (wallet dev server, extension build, mock web app, and the carpincho pkills) on presence of the carpincho-wallet workspace. A custom Canton scaffold can omit carpincho, which strips the wallet:dev and carpincho:build:extension scripts; up, mock-up, and extension previously hard-failed with npm Missing script. They now skip the carpincho steps with an informational log.
Document that has_carpincho presence implies an installable workspace, note the cosmetic pkill guards, refresh the header to mention up steps 4/6 are skipped when absent, and use the Carpincho Wallet product name consistently in skip messages.
fix: make dev-stack resilient when carpincho wallet workspace is absent
docs: standardize README structure across packages
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.
Brings PR #83's CIP-56 / Amulet-preapproval Carpincho and Splice LocalNet wallet-service into feat/vesting-carpincho. Conflicts resolved by: - keeping PR #83's carpincho-wallet and canton-barebones Splice config - keeping this branch's dapp/frontend vesting app and its docs - using PR #83's wallet-service rpc.ts (cip56/preapproval) with the generic scanApi method re-grafted, since the dApp needs it for AmuletRules and OpenMiningRound disclosures the typed cip56 methods do not cover - keeping this branch's scripts/dev-stack.sh amulet-up overlay - taking root README/AGENTS/architecture from PR #83 Checks passing: dapp typecheck, 57 dapp tests, dapp build, wallet-service tsc, 82 wallet-service tests.
The dApp needs the generic scanApi (AmuletRules + OpenMiningRound disclosures) that PR #83's wallet-service dropped. Two fixes so it runs: - scanApi connects to config.splice.scanApiUrl's host:port (host.docker.internal from inside the container) but sends Host: scan.localhost via node:http, since the LocalNet nginx routes the scan vhost on that header and fetch cannot set the forbidden Host header. Overridable with SPLICE_SCAN_VHOST. Verified against the live scan: amulet-rules and open-and-issuing-mining-rounds both return data. - Dockerfile copies dapp/daml/vesting-lite/package.json (the actual workspace), not the non-existent dapp/daml/package.json from PR #83's layout, so the image builds against the merged workspace set.
… the container The scan only serves amulet-rules / mining-rounds / holdings-summary on the scan.localhost nginx vhost (matched by the Host header), but the container reached it as host.docker.internal and got 404s. Map scan.localhost to the host gateway and point SPLICE_SCAN_API_URL at it, so both the grafted scanApi and PR #83's fetch-based cip56 holdings-summary route correctly. Verified: scanApi amulet-rules returns data; cip56.listHoldingSummary no longer 404s.
PR #83's compose pointed the wallet-service at the 29xx participant family and the validator at :2000, neither of which answer on this LocalNet — the JSON API reset and the wallet SDK could not initialize (blocking Carpincho create-account and the cip56/preapproval calls). Point CANTON_*_API_URL at the app-provider participant (JSON :3975, ledger :3901, admin :3902) and the validator/registry at :3903 — the same family the amulet bootstrap uses. All overridable via env. Verified end-to-end against the live stack: ledgerApi, scanApi, cip56.listHoldings, cip56.listHoldingSummary, and amulet.preapproval.status all return data. Note: the wallet-service's CANTON_BACKEND_TOKEN must be a Splice unsafe-auth token (aud https://canton.network.global, HMAC secret "unsafe"); align canton-barebones .env with .env.example so canton:token mints it.
…React 19 Merges origin/feat/vesting-amulet (59 commits) into the carpincho branch. Keeps this branch's carpincho / connect-kit wallet model and takes the remote's UI redesign where it is wallet-agnostic. Conflict resolution: - Wallet layer kept (ours): connect-kit hooks, ConnectScreen landing, AccountMenu, VestingDataProvider; StealthWallet / WalletProvider / WalletControl stay deleted. - AppShell takes the new layout (header+logo, no sidebar, skip link, Suspense for lazy routes) with carpincho gating (connect + lock + no-account). - Dashboard taken from the remote (Received/Created escrow tabs, framer-motion); it keys off the connected party, so it works unchanged with carpincho. - CreateGrant takes the remote UI but keeps a free-text beneficiary id, since carpincho has no party pool to pick from. - wallet-service rpc.ts kept ours (PR #83 cip56 + the scanApi graft). React 19 across the whole workspace (single version via root overrides): - dapp/frontend, connect-kit and carpincho all resolve react/react-dom/@types 19. - connect-kit bumped to 0.2.0 (peer react ^18 || ^19). - JSX.Element -> React.JSX.Element in connect-kit + carpincho (the global JSX namespace is gone in @types/react 19); useBorderTrace ref made nullable. Verified under a single React 19: dapp typecheck + 77 tests + build, connect-kit 7 tests, carpincho tsc.
The dashboard read the ACS only on mount and after the acting party's own actions, so a party's view went stale when the other party acted (e.g. the funder never saw the receiver's withdrawals). useVesting now polls every 5s. refresh() gains a silent mode used by the poll: it skips the loading flag and swallows transient errors so background refreshes never flicker the UI or clobber the last good view on a blip.
On reload with a persisted extension session, AppShell awaited connect() and showed a full-screen spinner until it settled. A stalled Carpincho handshake left reconnecting=true forever, pinning the app on the spinner with no escape. Race the reconnect against an 8s timeout that always clears the flag, so a stuck handshake degrades to the ConnectScreen instead of hanging.
scripts/topup-amulet.mjs <party> <amount> taps the validator faucet to fund the app-provider, then runs a cip56.createTransfer app-provider -> <party> (submit-and-wait as the provider with the disclosed contracts + synchronizerId). The target auto-accepts when it has Amulet preapproval enabled; otherwise the transfer lands as a pending TransferInstruction. Verifies via cip56 holdings.
…ngs for a party Scan only aggregates the validator operator's holdings on LocalNet, so its /v0/holdings/summary returns 200 with no entry for externally-hosted parties. cip56.listHoldingSummary returned that empty result verbatim because the UTXO fallback only fired when the Scan call threw. An empty-but-OK summary now also falls through to the ledger-UTXO path, so wallets reading the summary (e.g. Carpincho's Tokens tab) show the real balance instead of "no token".
bootstrap-amulet.mjs hardcoded rpcUrl :3020 into amulet-parties.json, but the live wallet-service is the :3010 container (canton-barebones compose) the dApp reads through. A bootstrap run would have re-pointed the dApp at the dead :3020.
…command amulet-up previously preflighted an external LocalNet and started a throwaway :3020 wallet-service proxy. canton:up now boots the Splice LocalNet bundle AND the :3010 wallet-service container, so amulet-up just runs canton:up, deploys the amulet-vesting DAR, bootstraps the factory/receiver/funding, serves the dApp on :3012, and builds the extension; amulet-down tears it down via canton:down. Add a `fund <party> <amount>` subcommand + menu entry wrapping topup-amulet.mjs (taps the operator, then transfers to the party — needs Amulet auto-accept on).
`canton builder reset` regenerates the app-provider party fingerprint, so the hardcoded id broke a from-scratch run (PERMISSION_DENIED on the first ACS read). bootstrap-amulet now resolves the party from the participant's app-provider user (/v2/users), and accepts the vesting package id via the PKG env (the DAR's hash changes on a rebuild). Both fall back to the last-known literals.
amulet-up called canton:up, which is pinned to the stale Splice 0.5.18 default and boots a different LocalNet than the 0.6.7 one managed by the external canton builder CLI. It now drives `canton builder start` + `deploy`, brings up the :3010 wallet-service container, derives the vesting package id from the DAR, then runs bootstrap; amulet-down stops the dApp + wallet-service + `canton builder stop` (data preserved). Exports CANTON_DEVREL_DIR so nginx's customs mount resolves.
…tack) The README Quick Start showed the stale base-template path (`canton:up`, quickstart-tally DAR, `wallet:dev`). Replace it with the one-command amulet flow — `./scripts/dev-stack.sh amulet-up` + load extension + `fund` + connect/accept/claim — and add the `canton builder` prerequisite. Note mock-up / vesting-lite as alternatives.
React 19 removed the global JSX namespace; the ConnectKitProvider test still used bare JSX.Element (StatusProbe + Naked), which the pre-push typecheck rejects on a fresh build. Matches the src convention (React.JSX.Element via the global React type).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No related issue.
Migrates the vesting dApp from direct party-dropdown access to external parties via Carpincho +
canton-connect-kit, running on the Splice 0.6.7 LocalNet (managed by thecanton builderCLI). Draft for review.Run it
Prereqs: Docker, the
canton builderCLI, Node ≥24, and in/etc/hosts:127.0.0.1 scan.localhost wallet.localhost sv.localhost.Brings up LocalNet 0.6.7 + wallet-service (
:3010) + the amulet-vesting DAR + bootstrap + dApp (:3012) + the Chrome extension (copied to~/Desktop/dist-extension).Then:
chrome://extensions→ Developer mode → Load unpacked →~/Desktop/dist-extension. Use two profiles (funder + receiver); in each: create a vault → create an account → Tokens → Enable Amulet auto-accept../scripts/dev-stack.sh fund <funder-party-id> 1000Stop:
./scripts/dev-stack.sh amulet-down· full wipe:canton builder reset· menu:./scripts/dev-stack.sh.