Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
0493cb2
feat: add amulet-vesting Daml package + vendored Splice deps
fernandomg Jun 11, 2026
f55c206
feat(wallet-service): proxy the Splice scan service over JSON-RPC
fernandomg Jun 11, 2026
1e89745
feat(frontend): evolve the vesting dApp into the Amulet app
fernandomg Jun 11, 2026
9bf7547
chore: add amulet-vesting LocalNet bootstrap script
fernandomg Jun 11, 2026
1f321be
feat(frontend): show funder balance and guard over-funding in create
fernandomg Jun 11, 2026
6ec73db
feat: add dev-stack amulet-up/amulet-down subcommands
fernandomg Jun 11, 2026
b697838
style(frontend): give clickable controls a pointer cursor
gabitoesmiapodo Jun 11, 2026
4a63315
feat(frontend): use favicon as the brand mark
gabitoesmiapodo Jun 11, 2026
ab22e92
style(frontend): drop the pink glow blob from the hero KPI card
gabitoesmiapodo Jun 11, 2026
f3c80c3
refactor(frontend): rename roles to Beneficiary and Manager
gabitoesmiapodo Jun 11, 2026
2438e14
style(frontend): bare-icon copy buttons and EVM-style party id shorte…
gabitoesmiapodo Jun 11, 2026
59d432d
feat(frontend): filter dropdown, icon view toggle, relocate create ac…
gabitoesmiapodo Jun 11, 2026
47f24da
style(frontend): drop the brand mark from the empty state
gabitoesmiapodo Jun 11, 2026
b926e0e
feat(frontend): show factory owner in the sidebar instead of the wall…
gabitoesmiapodo Jun 11, 2026
c70269d
feat(frontend): redesign connect screen with party dropdown and refre…
gabitoesmiapodo Jun 11, 2026
2ae3031
refactor(frontend): remove the privacy note
gabitoesmiapodo Jun 11, 2026
b0559c0
feat(frontend): move create action to the form footer, drop disclosur…
gabitoesmiapodo Jun 11, 2026
e7a48df
style(frontend): replace hero KPI gradient with a soft highlight
gabitoesmiapodo Jun 11, 2026
0cdece2
refactor(frontend): rename user-facing grants to escrows
gabitoesmiapodo Jun 11, 2026
4f848c2
style(frontend): drop the choose party heading on the connect screen
gabitoesmiapodo Jun 11, 2026
7ff1554
feat(frontend): unify wallet party switcher with the connect screen l…
gabitoesmiapodo Jun 11, 2026
3bf0db8
fix(frontend): qualify disclosed splice contracts with the live packa…
gabitoesmiapodo Jun 11, 2026
5bc19c0
feat(frontend): circular create-escrow button, drop the sidebar creat…
gabitoesmiapodo Jun 11, 2026
4e2f717
feat(frontend): explain KPIs with Radix tooltips and reorder the toolbar
gabitoesmiapodo Jun 11, 2026
7fd4121
feat(frontend): replace role toggle with Received/Created tabs
gabitoesmiapodo Jun 11, 2026
91a110f
fix(frontend): disclose each splice contract with its own live templa…
gabitoesmiapodo Jun 11, 2026
a9c17b0
chore(frontend): log accept disclosures and submit error for debugging
gabitoesmiapodo Jun 11, 2026
7428c70
feat(frontend): scrollable error toast with copy button
gabitoesmiapodo Jun 12, 2026
9112c09
refactor(frontend): drop the per-party role labels from party pickers
gabitoesmiapodo Jun 12, 2026
d78ba6c
chore(frontend): log proposal and context cids on accept for debugging
gabitoesmiapodo Jun 12, 2026
3490814
feat(frontend): add a contacts picker to the beneficiary field
gabitoesmiapodo Jun 12, 2026
d72e6e2
feat(frontend): filter on the left, view switch above the list
gabitoesmiapodo Jun 12, 2026
1d3b6b7
feat(frontend): modal close button, fixed-height contacts list, white…
gabitoesmiapodo Jun 12, 2026
bc36420
chore(frontend): label which contract is missing on accept failure
gabitoesmiapodo Jun 12, 2026
d243b3a
feat(frontend): add a uniswap-style amount field with cc pill and max
gabitoesmiapodo Jun 12, 2026
d56101a
feat(frontend): refine amount field and schedule layout
gabitoesmiapodo Jun 12, 2026
d94a393
feat(frontend): real CC logo, regroup dashboard controls above the list
gabitoesmiapodo Jun 12, 2026
d3efcf2
feat(frontend): full-width wallet row highlight and grouped amount input
gabitoesmiapodo Jun 12, 2026
38a0973
feat(frontend): add escrows section title, move create top-right, tab…
gabitoesmiapodo Jun 12, 2026
58f19e3
chore(frontend): log proposal input amulets vs proposer's live wallet
gabitoesmiapodo Jun 12, 2026
f07b993
fix(frontend): fail accept early with a clear message when pinned amu…
gabitoesmiapodo Jun 12, 2026
45fdd5b
feat(frontend): fold pending proposals into escrow tabs, remove propo…
gabitoesmiapodo Jun 12, 2026
8ed7d9c
feat(frontend): show cliff/window for pending rows and badge the awai…
gabitoesmiapodo Jun 12, 2026
9c0c051
feat(frontend): drop sidebar, logo in header, factory owner in a footer
gabitoesmiapodo Jun 12, 2026
aa73da1
feat(frontend): mandatory name field in the details card, drop note
gabitoesmiapodo Jun 12, 2026
1430b66
feat(frontend): restore pending card layout, create header with back,…
gabitoesmiapodo Jun 12, 2026
c619f89
feat(frontend): party switch returns to dashboard, smaller footer fac…
gabitoesmiapodo Jun 12, 2026
4d2331e
style(frontend): drop the from/to prefix on the grant card counterparty
gabitoesmiapodo Jun 12, 2026
de2bc65
feat(frontend): copy button next to the party id on grant cards
gabitoesmiapodo Jun 12, 2026
0830c38
style(frontend): shorten accept button label to Accept
gabitoesmiapodo Jun 12, 2026
01dbdbc
feat(frontend): copy button in table, from:/to: prefix on counterparty
gabitoesmiapodo Jun 12, 2026
096912c
fix: select escrow funding coins at accept time instead of pinning at…
gabitoesmiapodo Jun 12, 2026
e7babf2
feat(frontend): spinner during session hydration to avoid connect-scr…
gabitoesmiapodo Jun 12, 2026
cff1a8c
feat(frontend): boring-avatars identicons for parties
gabitoesmiapodo Jun 12, 2026
faa1f06
feat(frontend): factory-owner label in wallet menu, simplify footer
gabitoesmiapodo Jun 12, 2026
044ad92
feat(frontend): move tabs to their own row below the title
gabitoesmiapodo Jun 12, 2026
bdbdff9
feat(frontend): framer-motion animations for modal, toasts, cards, tabs
gabitoesmiapodo Jun 12, 2026
83bf88b
feat(frontend): move ledger status into wallet menu, remove footer
gabitoesmiapodo Jun 12, 2026
0321df6
revert: undo the accept-time coin-selection contract change and deploy
gabitoesmiapodo Jun 12, 2026
d360a50
refactor(frontend): extract shared copyPartyId and status-pill helpers
gabitoesmiapodo Jun 12, 2026
30b012c
fix(frontend): correct claim floor, fund headroom, and secure-context…
gabitoesmiapodo Jun 12, 2026
c66f1b3
fix: accessibility, code-splitting, and scan-proxy scheme handling
gabitoesmiapodo Jun 12, 2026
d3a87c9
refactor(frontend): remove dead icons and unused wallet-status hook
gabitoesmiapodo Jun 12, 2026
282613d
test(frontend): cover claim-floor helpers, shortenParty, and uuid
gabitoesmiapodo Jun 12, 2026
e689ff3
docs(frontend): sync architecture.md and CLAUDE.md with the live backend
gabitoesmiapodo Jun 12, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ node_modules/
dist/
dist-extension/

# DAML build cache (per-package + multi-package root)
**/.daml/

# env
.env
.env.local
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Subproject docs must not restate root rules. They should describe only their loc
- `npm run build-dar -- <daml-project>` / `npm run deploy-dar -- <dar>`
- `npm run carpincho:build:extension`
- `npm run app:dev`
- For local-stack convenience, [`scripts/dev-stack.sh`](scripts/dev-stack.sh) wraps the shortcuts above behind an interactive menu (run with no args) or direct subcommands (`install`, `docker-up`, `up`, `down`, `docker-down`, `mock-up`, `mock-down`, `extension`, `status`). The `npm` scripts remain canonical; the helper just orchestrates them. See [`README.md`](README.md).
- For local-stack convenience, [`scripts/dev-stack.sh`](scripts/dev-stack.sh) wraps the shortcuts above behind an interactive menu (run with no args) or direct subcommands (`install`, `docker-up`, `up`, `down`, `amulet-up`, `amulet-down`, `docker-down`, `mock-up`, `mock-down`, `extension`, `status`). The `amulet-*` pair drives the Splice LocalNet stack (`:3020` proxy + dApp); LocalNet itself stays external (`canton builder`). The `npm` scripts remain canonical; the helper just orchestrates them. See [`README.md`](README.md).
- Local ports are intentionally assigned in the `3010+` range (see table above). Do not change them without updating every subproject's defaults.
- Treat the single root `package-lock.json` as authoritative. Do not regenerate it as part of unrelated changes, and do not reintroduce per-package lockfiles.
- The root `package.json` pins `@canton-network/dapp-sdk` to `1.1.0` via `overrides`: consumers declare `^1.1.0`, but `1.2.0` is intentionally held back. npm 11 does not persist `overrides` into `package-lock.json`, so the pin is enforced by the override on every relock and by the resolved `1.1.0` entry in the lock on every plain install. Do not bump it without testing the dApp flow against the newer SDK.
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ Or call an action directly:
| Docker down | `docker-down` | Quit Docker Desktop (macOS only). |
| Stack up | `up` | Bring up containers, build + deploy the DAR, start the wallet (3011) and dApp (3012) dev servers, build the extension. |
| Stack down | `down` | Stop the dev servers and tear down the containers. |
| Amulet up | `amulet-up` | Splice LocalNet path: bootstrap parties/factory/funding, start the `:3020` wallet-service proxy, serve the dApp (3012). Assumes LocalNet is already booted. |
| Amulet down | `amulet-down` | Stop the amulet proxy + dApp dev server (LocalNet is left running). |
| Wallet up | `mock-up` | Start the mocked wallet-service (3010) + Carpincho web app (3011) with no Docker. |
| Wallet down | `mock-down` | Stop the mocked wallet-service + Carpincho web app only. |
| Build extension | `extension` | Build the Chrome extension and copy it to `~/Desktop/dist-extension`. |
Expand All @@ -87,6 +89,7 @@ Or call an action directly:
Notes:

- Docker lifecycle is managed separately from the stack: `up` and `down` assume Docker is already running and never start or quit it. Start/quit Docker with `docker-up` / `docker-down`, the Docker app, or your own CLI.
- The `amulet-*` actions target the Splice LocalNet stack (ports 3020/3975/4000), not bare Canton. LocalNet is run by the external `canton builder` tool — the script preflights it but never boots or stops it. Boot it first with `canton builder start --validators app-provider` then `canton builder deploy canton-barebones/dars/amulet-vesting-0.0.1.dar`.
- `up` requires Docker running and `dpm` on `PATH` (for the DAR build). It fails fast with a clear message if the daemon is not reachable.
- Background dev-server PIDs and logs live under `${TMPDIR:-/tmp}/cn-dev-stack/`.
- The two Docker actions are macOS only; on other platforms they warn and no-op, while every other action runs unchanged.
Expand Down
Binary file added canton-barebones/dars/amulet-vesting-0.0.1.dar
Binary file not shown.
4 changes: 4 additions & 0 deletions canton-barebones/wallet-service/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export interface WalletServiceConfig {
jsonApiUrl: string
ledgerApiUrl: string
adminApiUrl: string
scanUrl: string
scanHost: string
backendUserId: string
backendToken?: string
tokenSource: TokenSource
Expand Down Expand Up @@ -86,6 +88,8 @@ export const loadConfig = (): WalletServiceConfig => {
jsonApiUrl: optional('CANTON_JSON_API_URL') ?? 'http://localhost:3013',
ledgerApiUrl: optional('CANTON_LEDGER_API_URL') ?? 'grpc://localhost:3014',
adminApiUrl: optional('CANTON_ADMIN_API_URL') ?? 'grpc://localhost:3015',
scanUrl: optional('CANTON_SCAN_URL') ?? 'http://localhost:4000',
scanHost: optional('CANTON_SCAN_HOST') ?? 'scan.localhost',
backendUserId,
backendToken: resolved.token,
tokenSource: resolved.source,
Expand Down
69 changes: 69 additions & 0 deletions canton-barebones/wallet-service/src/rpc.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as http from 'node:http'
import * as https from 'node:https'
import { SDK } from '@canton-network/wallet-sdk'
import type { WalletServiceConfig } from './config.ts'
import type {
Expand All @@ -11,6 +13,7 @@ import type {
LedgerApiRequest,
Network,
Provider,
ScanApiRequest,
StatusEvent,
Wallet,
} from './types.ts'
Expand Down Expand Up @@ -347,6 +350,67 @@ export const createRpc = (config: WalletServiceConfig): Rpc => {
return parsed
}

// Transparent proxy to the Splice scan service. Returns raw parsed JSON from
// the scan service with no Authorization header — the scan service is public.
// Uses node:http directly so the Host header is actually sent; undici/fetch
// silently drops Host (it's a forbidden header), which breaks nginx vhost routing.
const scanApi = async (params: unknown): Promise<unknown> => {
const p = objectParam<ScanApiRequest>(params, 'scanApi')
if (typeof p.resource !== 'string' || p.resource.length === 0) {
throw new InvalidParams('resource is required')
}
const method = (p.requestMethod ?? 'post').toUpperCase()
const payload =
method !== 'GET' && method !== 'HEAD' && p.body !== undefined
? JSON.stringify(p.body)
: undefined
const target = new URL(config.canton.scanUrl.replace(/\/$/, '') + p.resource)
// node:http(s) directly (not fetch) so the Host header survives for nginx vhost
// routing. Pick the transport + default port from the URL scheme so an https
// scanUrl is not silently sent as cleartext to port 80.
const isHttps = target.protocol === 'https:'
const transport = isHttps ? https : http
return new Promise<unknown>((resolve, reject) => {
const req = transport.request(
{
hostname: target.hostname,
port: target.port || (isHttps ? 443 : 80),
path: target.pathname + target.search,
method,
headers: {
'content-type': 'application/json',
host: config.canton.scanHost,
...(payload !== undefined ? { 'content-length': Buffer.byteLength(payload) } : {}),
},
},
(res) => {
const chunks: Buffer[] = []
res.on('data', (chunk: Buffer) => chunks.push(chunk))
res.on('end', () => {
const text = Buffer.concat(chunks).toString('utf8')
const contentType = res.headers['content-type'] ?? ''
const isJson = text.length > 0 && contentType.includes('json')
const parsed: unknown = isJson ? safeJsonParse(text) : text
const status = res.statusCode ?? 0
if (status < 200 || status >= 300) {
const detail = typeof parsed === 'string' ? parsed : JSON.stringify(parsed)
reject(new Error(`Scan API ${method} ${p.resource} → HTTP ${status}: ${detail}`))
} else {
resolve(parsed)
}
})
res.on('error', reject)
},
)
req.setTimeout(15_000, () => req.destroy(new Error('Scan API timed out')))
req.on('error', reject)
if (payload !== undefined) {
req.write(payload)
}
req.end()
})
}

// Reads the backend user's CanActAs rights (what the bootstrap grants), optionally
// narrowed to a hint prefix so a long-lived dev ledger's stale grants don't leak in.
const listAccounts = async (): Promise<Wallet[]> => {
Expand Down Expand Up @@ -417,6 +481,8 @@ export const createRpc = (config: WalletServiceConfig): Rpc => {
return rpcResult(id, await executePrepared(request.params))
case 'ledgerApi':
return rpcResult(id, await ledgerApi(request.params))
case 'scanApi':
return rpcResult(id, await scanApi(request.params))
case 'prepareExecute':
case 'prepareExecuteAndWait':
case 'signMessage':
Expand Down Expand Up @@ -458,6 +524,7 @@ export const createRpc = (config: WalletServiceConfig): Rpc => {
'listAccounts',
'getPrimaryAccount',
'ledgerApi',
'scanApi',
'prepareTransaction',
'executePrepared',
],
Expand All @@ -469,6 +536,8 @@ export const createRpc = (config: WalletServiceConfig): Rpc => {
jsonApiUrl: config.canton.jsonApiUrl,
ledgerApiUrl: config.canton.ledgerApiUrl,
adminApiUrl: config.canton.adminApiUrl,
scanUrl: config.canton.scanUrl,
scanHost: config.canton.scanHost,
backendUserId: config.canton.backendUserId,
hasBackendToken: config.canton.backendToken !== undefined,
},
Expand Down
6 changes: 6 additions & 0 deletions canton-barebones/wallet-service/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ export type LedgerApiRequest = {

export type LedgerApiResult = Record<string, unknown>

export type ScanApiRequest = {
resource: string
requestMethod?: 'get' | 'post'
body?: unknown
}

export type SignMessageRequest = { message: string }
export type SignMessageResult = { signature: string }

Expand Down
54 changes: 54 additions & 0 deletions canton-barebones/wallet-service/test/rpc.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { strict as assert } from 'node:assert'
import * as http from 'node:http'
import { describe, it } from 'node:test'
import { createRpc, errorData, errorMessage } from '../src/rpc.ts'
import type { JsonRpcResponse } from '../src/types.ts'
Expand All @@ -18,6 +19,8 @@ const baseConfig = () => ({
jsonApiUrl: 'http://localhost:3013',
ledgerApiUrl: 'grpc://localhost:3014',
adminApiUrl: 'grpc://localhost:3015',
scanUrl: 'http://localhost:4000',
scanHost: 'scan.localhost',
backendUserId: 'wallet-service',
backendToken: undefined as string | undefined,
tokenSource: 'none' as const,
Expand Down Expand Up @@ -287,6 +290,57 @@ describe('party methods are off the dapp-api surface', () => {
})
})

describe('scanApi pass-through', () => {
it('forwards to scanUrl + resource with Host header and returns parsed body', async () => {
const responseBody = { rounds: [{ number: 1 }] }
let capturedHost: string | undefined
let capturedAuth: string | undefined

const server = http.createServer((req, res) => {
capturedHost = req.headers.host
capturedAuth = req.headers.authorization
res.writeHead(200, { 'content-type': 'application/json' })
res.end(JSON.stringify(responseBody))
})

await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
const { port } = server.address() as { port: number }

const config = baseConfig()
config.canton.scanUrl = `http://127.0.0.1:${port}`

try {
const rpc = createRpc(config)
const res = (await rpc.handle({
jsonrpc: '2.0',
id: 1,
method: 'scanApi',
params: { resource: '/v0/domains/global-domain/rounds/open', requestMethod: 'get' },
})) as JsonRpcResponse
assert.ok('result' in res, `expected result, got: ${JSON.stringify(res)}`)
assert.deepEqual(res.result, responseBody)
assert.equal(capturedHost, 'scan.localhost')
assert.equal(capturedAuth, undefined)
} finally {
await new Promise<void>((resolve, reject) =>
server.close((err) => (err ? reject(err) : resolve())),
)
}
})

it('returns -32602 when resource is missing', async () => {
const rpc = createRpc(baseConfig())
const res = (await rpc.handle({
jsonrpc: '2.0',
id: 1,
method: 'scanApi',
params: { requestMethod: 'get' },
})) as JsonRpcResponse
assert.ok('error' in res)
assert.equal(res.error.code, -32602)
})
})

describe('ledgerApi pass-through', () => {
it('accepts requestMethod=get', async () => {
const rpc = createRpc(baseConfig())
Expand Down
1 change: 1 addition & 0 deletions dapp/daml/amulet-vesting/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.daml/
21 changes: 21 additions & 0 deletions dapp/daml/amulet-vesting/daml.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# for config file options, refer to
# https://docs.digitalasset.com/build/3.4/dpm/configuration.html#project-configuration
# SDK pinned to 3.4.11 / LF target 2.1 to match the vendored Splice deps
# (splice-amulet et al. are built with sdk 3.4.11 + --target=2.1). Keeping the
# same SDK/LF as the deps lets the amulet-vesting-test package data-depend on this
# DAR directly instead of recompiling the source at a different target.
sdk-version: 3.4.11
name: amulet-vesting
source: daml
version: 0.0.1
dependencies:
- daml-prim
- daml-stdlib
data-dependencies:
# paths are relative to this package dir (dapp/daml/amulet-vesting/); ../../ is dapp/ (the vendor root in this repo)
- ../../deps/splice-daml/dars/splice-amulet-0.1.19.dar
- ../../deps/splice-daml/dars/splice-api-token-holding-v1-1.0.0.dar
- ../../deps/splice-daml/dars/splice-api-token-metadata-v1-1.0.0.dar
- ../../deps/splice-daml/dars/splice-api-token-transfer-instruction-v1-1.0.0.dar
build-options:
- --target=2.1
Loading