Skip to content

feat: vesting dApp via Carpincho external parties (Splice 0.6.7)#86

Draft
fernandomg wants to merge 150 commits into
feat/vesting-litefrom
feat/vesting-carpincho
Draft

feat: vesting dApp via Carpincho external parties (Splice 0.6.7)#86
fernandomg wants to merge 150 commits into
feat/vesting-litefrom
feat/vesting-carpincho

Conversation

@fernandomg

Copy link
Copy Markdown
Member

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 the canton builder CLI). Draft for review.

Run it

Prereqs: Docker, the canton builder CLI, Node ≥24, and in /etc/hosts:
127.0.0.1 scan.localhost wallet.localhost sv.localhost.

./scripts/dev-stack.sh amulet-up

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:

  • Load the walletchrome://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.
  • Fund the funder./scripts/dev-stack.sh fund <funder-party-id> 1000
  • Run the flowhttp://localhost:3012ConnectCreate escrow (to the receiver's party id) → switch to the receiver profile → AcceptClaim.

Stop: ./scripts/dev-stack.sh amulet-down · full wipe: canton builder reset · menu: ./scripts/dev-stack.sh.

gabitoesmiapodo and others added 30 commits June 8, 2026 16:09
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
gabitoesmiapodo and others added 28 commits June 11, 2026 22:49
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).
@fernandomg fernandomg changed the base branch from feat/vesting-lite to main June 12, 2026 14:01
@fernandomg fernandomg changed the base branch from main to feat/vesting-lite June 12, 2026 14:02
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.

3 participants