Skip to content

feat(token): CIP-0112 V2 token features — identity picker, EventLog history, allocations (DvP), batching - #277

Merged
zheli merged 9 commits into
mainfrom
feat/token-v2-tightening
Jul 25, 2026
Merged

feat(token): CIP-0112 V2 token features — identity picker, EventLog history, allocations (DvP), batching#277
zheli merged 9 commits into
mainfrom
feat/token-v2-tightening

Conversation

@srikanth-bitdynamics

Copy link
Copy Markdown
Collaborator

What

Four CIP-0112 (Token Standard V2) features on the Tokens surface, built on a shared foundation that bundles the new V2 DARs (allocation-v2, transfer-events-v2, util-token-standard-wallet), interface constants, and schema-pinned types. All signatures were pinned from the actual Splice 0.6.12 source (commit 17fd29aa).

  • Identity / act-as picker — top-level role+party switcher on the Tokens screen; role threaded through every token call; GET /api/tokens/identity + dpm localnet token identity.
  • EventLog historyactivity.go gains a V2 EventLog_HoldingsChange-native path (Amulet declares interface instance TransferEventsV2.EventLog at 0.6.12), with the existing netting reconstruction kept as fallback.
  • Allocations / DvPAllocationFactory_Allocate → list / withdraw / cancel; new registry client + orchestration + token allocate/allocations/settle CLI + UI panel. Registry endpoints taken from source.
  • BatchingUtilityV2 — opt-in --atomic transfer+accept via BatchingUtility_ExecuteBatch; sequential (partial-recovery) path stays the default.

CLI↔Web UI parity maintained throughout (shared RunX per verb).

Static verification (green)

  • Backend: go build ./..., go test ./..., make lint (0 issues).
  • Frontend: tsc --noEmit, vitest 221/221, lint clean.
  • internal/ui/dist/index.html placeholder intact.

Live verification (token-standard-v2 alpha) — partial, and why

Brought up a live token-standard-v2 instance and exercised the branch binary:

  • ✅ Instance comes up; token identity works live; read/balance path works live.
  • ⚠️ Fuller create→mint→transfer→allocate e2e is blocked by a PRE-EXISTING issue, not this change: the mint OfferMint → TransferInstruction_Accept sequence (instrument_v2.go:289, byte-identical to main) fails against the pinned alpha with DAML_FAILURE: unavailable action TIA_Accept. It reproduces on main (unit tests use a fake ledger, so it isn't caught there).

Because the underlying mint is what fails, the V2-on-ledger paths (batching, and allocations that need holdings) can't be proven end-to-end on this alpha yet. They are statically sound and source-grounded; live proof waits on the mint fix.

Issues discovered during local testing (separate follow-ups)

  1. Mint vs. current alphaTIA_Accept state-machine mismatch (above). Pre-existing.
  2. Container healthcheck false-negativehealth-check.sh uses wget --spider (HEAD); readyz returns 405 for HEAD / 200 for GET, so the container never flips healthy and status shows "syncing" indefinitely.
  3. No port-collision pre-flight for auto-allocated ports — a leftover instance holding a port yields a cryptic mid-startup Docker failure + partial-state instance instead of a clean fail-fast (explicit --port-base mode already pre-checks).

Known in-code TODOs

  • TODO(BIT-ALLOC-SETTLE) — allocation settlement is executor-driven (no registry settle context); RunSettle resolves the settlement-factory context but the FinalizedAllocation batch wants a live instance.
  • Batching TokenStandardAction/HoldingMap wire-shapes are built from confirmed 0.6.12 signatures, pending live-ledger verification.

@srikanth-bitdynamics

Copy link
Copy Markdown
Collaborator Author

Session update — token-page tightening (pushed through 494e8ce)

All fixes below were verified live on a 0.6.12 LocalNet (instance tok612), CLI and Web UI.

Discovery / Holdings matrix

  • Created-but-unminted instruments now list — discovery was holdings-only, so a token you created but hadn't minted was invisible while the ledger was live. Now shows with zero supply until first mint.
  • Matrix: dropped phantom duplicate rows (stale bare-alias CanActAs/CanReadAs grants surfaced non-party strings) → 12 → 8 real parties.
  • Matrix: filter-by-token — All tokens + per-token chips; narrows the grid to one token's column; seeds from the Instruments selection.

Actions

  • Self-mint (issuer == receiver) now rejected with an actionable error — for a self-custodial receiver that equals the admin, the test-token state machine auto-advances past accept, so no Token is ever created; mint to a distinct party instead.
  • Sequential transfer+accept verified end-to-end (holder1 → demo-holder, balances conserved).

Web UI

  • Copy party-id in the Parties panel + party pickers.
  • Activity feed newest-first (already shared-sorted) + "Load more" pagination.

Batching (--atomic)

  • Wire shapes corrected and live-proven past COMMAND_PREPROCESSING: HoldingMap record, bare AnyContractId ChoiceCall.cid, instrument-keyed holdings.
  • Known limitation — EXPERIMENTAL: BatchingUtility_ExecuteBatch on the current test-token/wallet DARs does not rebind the accept leg to the TransferInstruction the transfer leg creates in the same batch (forward reference), so it fails at interpretation and nothing commits. Flag / handler / api.ts marked experimental; it now returns a clear actionable error and points to the sequential path; TODO(BIT-NNN) marks the divergence. Sequential transfer+accept is the supported default.

go test ./... + make lint + frontend build all green; internal/ui/dist placeholder preserved.

…types

Foundation for CIP-0112 (Token Standard V2): auto-bundle the
allocation-v2, transfer-events-v2, and util-token-standard-wallet DARs
alongside the test-token DAR, add the V2 interface/choice name
constants, and define the schema-pinned API types shared by the CLI
--json output and the Web UI REST/SSE payloads so the two surfaces
cannot drift.
…tching

Four CIP-0112 capabilities on both the CLI and the Web UI Tokens screen,
over one shared orchestration layer:
- identity/act-as picker: operate as app-user / app-provider / sv, with
  the role threaded through every token call.
- EventLog transaction history: per-instrument activity reconstructed
  from the V2 EventLog interface, newest-first.
- allocations / DvP: allocate, list, settle, withdraw, cancel via the V2
  AllocationFactory.
- opt-in atomic transfer+accept batching via BatchingUtilityV2.
Fixes found by exercising create -> mint -> transfer -> allocate on a
live LocalNet, plus Tokens-screen UX. Each lands on both the CLI and the
Web UI:
- authorize V2 ops: grant read-any + per-issuer act-as, resolve issuer
  aliases on create, onboard freshly-allocated parties to the synchronizer.
- self-custodial receiver account so V2 mint accept works; reject
  self-mint (issuer == receiver) with an actionable error.
- correct the allocation wire shapes to V2 (drop expectedAdmin;
  SettlementInfo and TransferLegSide v1 -> v2).
- list created-but-unminted instruments (discovery was holdings-only).
- drop phantom bare-alias parties from the readable set and matrix.
- Holdings matrix filter-by-token; copy party ids; paginate the Activity
  feed newest-first.
- --atomic batching: wire shapes corrected, but closed as experimental
  (ExecuteBatch does not rebind the accept leg to the transfer leg's
  intra-batch instruction) -- it fails with a clear error; sequential
  transfer+accept is the supported default.
Open-source hygiene pass over the token subsystem: cut running commentary
and code-restating comments (~1000 fewer comment lines) while keeping the
load-bearing wire-shape and Daml-quirk rationale, and remove internal
ticket tags and notes. Comment-only -- no behavior change.

@zheli zheli left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The V2 foundation is well structured, but this revision has several blocking issues to address. Generic ledger dialing introduces universal execute-as authority, allocation withdraw/cancel omit a required Daml argument, settlement is exposed but unimplemented, and the Web UI does not preserve role or atomic parity. EventLog history also duplicates paired reports and can omit newer activity. The new commands and flag additionally require proposal-deviation documentation.

Comment thread internal/localnet/token/ledger.go
Comment thread internal/localnet/token/run_allocation.go Outdated
Comment thread internal/localnet/token/exercise_allocation.go Outdated
Comment thread frontend/src/screens/TokensScreen.tsx Outdated
Comment thread frontend/src/screens/TokensScreen.tsx Outdated
Comment thread internal/localnet/token/activity_eventlog.go
Comment thread internal/localnet/token/activity_eventlog.go
Comment thread internal/localnet/token/activity.go
Comment thread internal/cli/localnet/token/allocations.go
Comment thread internal/cli/localnet/token/token.go
@zheli

zheli commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Let's create a new doc page for V2 and add instructions on how to run a localnet with support there. It seems multiple steps are required.

@zheli

zheli commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Manual verification: standalone canton-devkit, live tokens-v2 instance

Built bin/canton-devkit from this branch and exercised all 14 token verbs against a live instance (localnet up --version token-standard-v2 --profile tokens-v2, came up running).

Summary

Command Result
identity ✅ Pass (text/json, --role, invalid-role rejection) — no instance needed
party new / party ls ✅ Pass
create ✅ Pass (on-ledger V2 instrument)
mint ✅ Pass — the TIA_Accept blocker in the PR description did not reproduce on this image
balances / balance / summary ✅ Pass
activity ✅ Pass (V2 EventLog path)
faucet ✅ Pass
transfer (+ --no-wait) / transfer accept ✅ Pass
transfer --auto-accept ✅ Pass
transfer --auto-accept --atomic ✅ Pass as designed — fails cleanly with the documented experimental message, no partial commit
burn (+ --yes) ✅ Pass
allocations (list/withdraw/cancel) ✅ Pass on invalid input (clean 400s); list correctly empty
allocate ❌ Fails
settle ❌ Fails
demo ❌ Fails

New issues found (not mentioned in the PR description)

1. allocateAllocationFactory_Allocate rejects custom instruments

exercise AllocationFactory_Allocate: ... DAML_FAILURE: ... AssertionFailed:
The requirement 'Instrument-id must match the factory' was not met.

Reproduced with an instrument created via token create (not a network-default instrument). Looks like the registry's allocation factory doesn't resolve issuer-created instruments the way allocate assumes.

2. settleGetSettlementFactory uses the wrong HTTP verb
internal/canton/registry/allocation_v2.go:157-163 issues a GET to the settlement-factory path:

func (c *Client) GetSettlementFactory(ctx context.Context, path string) (*SettlementFactoryResponse, error) {
	var out SettlementFactoryResponse
	if err := c.doJSON(ctx, "GET", path, nil, &out); err != nil {

Against the live registry this returns HTTP 405: supported methods: POST. This is more fundamental than the documented "not yet proven — batch assembly needs a live V2 instance" TODO in RunSettle — the code never even reaches that TODO because the endpoint call itself fails first.

3. demo — always self-mints, so it always fails on a V2 instance
internal/localnet/token/demo.go:111-122 (runDemoV2) mints the initial supply To: issuer.PartyID:

step("Minting %s %s to the issuer…", opts.InitialSupply, opts.Symbol)
if err := demoMint(ctx, out, MintOptions{
    ...
    To:         issuer.PartyID,
    ...

This is exactly the self-mint case the guard added in this same PR ("reject self-mint (issuer == receiver) with an actionable error") rejects:

demo: mint supply: cannot self-mint: the test token cannot mint to the issuer's own party (...) — mint to a distinct party

token demo will fail every time on a tokens-v2 instance until it mints to a distinct holder (or seeds a holder first and mints there).

Minor inconsistency

allocations (list) marks --endpoint as a hard cobra-required flag, unlike its sibling verbs (transfer/allocate/settle/demo), which instead print the ErrNeedsV2LocalNet remediation at runtime when --endpoint is omitted. Worth aligning for consistent UX.


Happy to share the exact command transcript if useful.

The V2 demo minted the initial supply to the issuer party, which the
self-mint guard added in this branch rejects (issuer == receiver) — so
token demo failed every time on a tokens-v2 instance. Mint the supply to
a distinct holder party instead, which both satisfies the guard and lands
a transferable balance in one step, dropping the redundant issuer->holder
faucet leg. The holder is now intrinsic to the V2 demo, so --seed-holder
is removed.
@zheli

zheli commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Fix for the demo self-mint finding — verified live on token-standard-v2

Addressed the demo always-self-mints issue in 9449111 (fix(token): demo mints supply to holder, not the issuer).

Fix: runDemoV2 now allocates the demo-holder party before minting and mints the initial supply straight to the holder instead of the issuer. That satisfies the self-mint guard added in this branch and lands a transferable balance in one step, so the redundant issuer→holder faucet leg is dropped. Since the V2 demo always seeds a holder now, the --seed-holder flag (and its seed_holder API field) were removed as dead config. Updated CLI/handler/api.ts + demo unit tests accordingly.

Verified end-to-end — fresh bin/canton-devkit from this branch against a live localnet up --version token-standard-v2 --profile tokens-v2 instance (ready in ~1m18s):

Check Result
token demo --format json ✅ mints 1000000 DEMO to demo-holder (mint: offeredmint: accepted), seeded:true — previously failed 100% with cannot self-mint
token balances demo-holder = 1000000.0, demo-issuer = · (supply committed to the holder)
self-mint negative control ✅ explicit mint --to <issuer> still rejected with cannot self-mint … mint to a distinct party (guard intact)

Note on the self-mint guard / --endpoint: the guard lives in the live path (runMintLive), which RunMint only dispatches to when --endpoint is set. Without --endpoint, RunMint never reaches the guard and instead returns the generic ErrUnsupportedOnInstrument ("this instrument doesn't implement the V2 standard's mint/burn surface…"). So to actually exercise the self-mint guard from the CLI you must pass --endpoint (demo does this via auto-discovery). Worth aligning if the guard should surface regardless of endpoint.

@zheli

zheli commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Follow-up (deferred): token --role defaults to app-user, but app-provider is the node we control in prod

While fixing the demo self-mint bug I dug into the --role handling and found an inconsistency worth tracking separately (not in scope for this PR):

What --role actually controls. It selects both the participant node to dial (state.Ports["participant_ledger_<role>"], ledger.go:64) and the JWT / act-as identity (state.Credentials[role], ledger.go:286). Because mint looks up the issuer's TokenRules on the dialed participant, the whole lifecycle — party newcreatemint — must share one role, or mint fails with no on-ledger TokenRules for issuer (instrument_v2.go:110-113). So the role default is a whole-workflow decision, not a per-command one.

The inconsistency.

  • Every token command (CLI --role flag, action-layer roleOrDefault / inline defaults, UI roleFromQuery, frontend DEFAULT_ROLE) defaults to app-user.
  • The JWT / identity dashboard already defaults to app-provider (internal/ui/handlers/auth_test.go:86-111, "default Role = app-provider").
  • There is no comment anywhere justifying app-user over app-provider; the only note (actions.go:149-151) treats it as an arbitrary fallback participant.

In a prod-like setup app-provider is the validator node an operator actually controls, so it's the natural place to host issuers and run mints. Flipping the default to app-provider likely matches real usage and would also align the token surfaces with the identity dashboard.

Why it's deferred (broad blast radius). A correct flip must change the default in lockstep across:

  • roleOrDefault (party.go:267), the four action-layer inlines (mint/burn/transfer/accept), the allocation/workspace/plan inlines, identity.DefaultRole, the two ledger.go fallbacks;
  • the ~17 CLI --role flag defaults;
  • UI roleFromQuery (tokens.go:707) + frontend DEFAULT_ROLE (api.ts:1690);
  • plus test churn (many tests assert the app-user default).

It also changes behavior for existing on-ledger state: instances whose parties were allocated on the app-user node would appear empty after a flip unless --role app-user is passed.

TODO(BIT-NNN): decide on app-provider as the default role for token operations and flip it consistently across all surfaces (or document explicitly why app-user stays). Tracking here so we don't lose it.

mint and burn only took the live path when --endpoint was passed
explicitly; otherwise they fell through to ErrUnsupportedOnInstrument,
which also meant the self-mint guard (inside runMintLive) never ran. A
self-mint attempt without --endpoint therefore surfaced a misleading
'instrument doesn't implement mint/burn' error instead of the actionable
guard message. transfer/accept similarly returned ErrNeedsV2LocalNet on a
live instance when --endpoint was omitted.

Resolve the endpoint from the instance's captured participant_ledger_<role>
port (as RunBalance already does) in the shared action layer, so both the
CLI and Web UI behave consistently and these commands work flag-free on a
running LocalNet. Explicit --endpoint still wins; instance-down still falls
back to the correct remediation. Align the CLI --endpoint help text with
balances/balance.
@zheli

zheli commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Live verification — --endpoint auto-resolution (commit f0c7eea)

Rebuilt bin/canton-devkit at f0c7eea (canton-devkit dev (f0c7eea)) and exercised it against the live token-standard-v2 instance demo-v2fix (● running).

The --endpoint issue this addresses

mint/burn previously only took the live path when --endpoint was passed explicitly; otherwise they fell through to ErrUnsupportedOnInstrument. That also meant the self-mint guard (which lives inside runMintLive) never ran — so a self-mint attempt without --endpoint surfaced a misleading "instrument doesn't implement mint/burn" error instead of the actionable guard. transfer/transfer accept similarly returned ErrNeedsV2LocalNet on a live instance when --endpoint was omitted.

Fix (f0c7eea): RunMint/RunBurn/RunTransfer/RunAccept now auto-resolve the endpoint from the instance's captured participant_ledger_<role> port (the same idiom RunBalance already uses), in the shared action layer so the CLI and Web UI stay consistent. Explicit --endpoint still wins; instance-down still falls back to the correct remediation. CLI --endpoint help text aligned with balances/balance.

Results (all commands run without --endpoint)

Command Result
token mint --to <issuer> --amount 1 (self-mint) ✅ Now rejected with the actionable guard: cannot self-mint: the test token cannot mint to the issuer's own party (…) — mint to a distinct party. Previously returned the misleading ErrUnsupportedOnInstrument.
token mint --to <demo-holder> --amount 7 mint: offeredmint: accepted — flag-free live mint commits end-to-end.
token balances demo-holder holds 1000012 DEMO (1,000,000 demo supply + 5 + 7 from flag-free mints); demo-issuer holds nothing.
### self-mint WITHOUT --endpoint (expect guard):
cannot self-mint: the test token cannot mint to the issuer's own party (demo-issuer::1220…0e82) — mint to a distinct party
exit=1

### mint to distinct holder WITHOUT --endpoint (expect accepted):
mint: offered: {"amount":"7","offer_cid":"000ca432…56b51","to":"demo-holder::1220…0e82"}
mint: accepted: {"amount":"7","instrument":"DEMO","to":"demo-holder::1220…0e82"}
exit=0

### balances (no --endpoint):
PARTY                             AMULET            DEMO
──────────────────────────────────────────────────────────────────────
app_user_demo_v2fix-localparty-1  23600.1600000000  ·
demo-holder                       ·                 1000012.0000000000
demo-issuer                       ·                 ·
Σ total                           23600.1600000000  1000012.0000000000
exit=0

go build + go vet clean; go test ./internal/localnet/token/... ./internal/ui/handlers/... ./internal/cli/localnet/token/...485 passed (includes new auto-resolution tests: self-mint hits the guard flag-free, distinct receiver reaches the live dial, no-captured-port keeps the registry fallback, transfer auto-resolves).

Scope note: this covers mint/burn/transfer/transfer accept. Kept the app-user role default; the app-provider role-default alignment is tracked as a separate follow-up.

zheli added 2 commits July 24, 2026 22:24
`token allocate` posted the scan registry's allocation-factory endpoint,
which returns the network-default (DSO/Amulet) factory, then exercised
AllocationFactory_Allocate against it. For an issuer-created instrument
(token create) that fails on-ledger with

  AssertionFailed: The requirement 'Instrument-id must match the factory'

because the DAML impl asserts allocation.admin == tokenRules.admin and
the DSO factory's admin is not the issuer.

The issuer's own on-ledger TokenRules contract IS the V2 AllocationFactory
(interface instance V2.AllocationFactory for TokenRules), with admin ==
the instrument admin — exactly as it is the TransferFactory the on-ledger
transfer path already uses. Resolve TokenRules via findTokenRules(admin)
and exercise AllocationFactory_Allocate against it, building the choice
context locally (TokenRules + authorizer AccountConfig cids) and acting as
the authorizer's account parties plus the admin. The authorizer Account is
taken from the picked holdings' own account so it satisfies the impl's
inputHolding.account == allocation.authorizer check (self-custodial and
provider-scoped).

Verified live on a token-standard-v2 LocalNet: the previously-failing
allocate now finalizes an Allocation (committed and non-committed), which
`token allocations` lists. Scope limited to allocate; withdraw/cancel/
settle unchanged.
Allocation_Withdraw / Allocation_Cancel were malformed: the choice
argument omitted the required `actors : [Party]` controller field
(AllocationV2.daml), and the choice context was fetched from the scan
registry. The TestToken impl reads the local test-token context from
extraArgs (unlockTokenAllocationV2 -> getEventLogFromContext,
applyAllocationTransitions -> extractAccountConfigMap), so the registry
blob was rejected — the same root cause as the allocate fix.

Rework runAllocationAction to mirror the allocate factory path: fetch the
target Allocation's view to resolve the admin / authorizer account /
executors, resolve the issuer's on-ledger TokenRules (event-log +
AccountConfig source), build the local choice context, and exercise with
the controller `actors` the state machine requires (AccountConfig.daml):
withdraw -> the authorizer's account parties, cancel -> the executors.
Act as those actors plus the admin (co-signs the unlocked holdings).

Extend the Allocation view walker to surface the authorizer account
provider/id and the settlement executors. Drop the now-unused registry
withdraw/cancel choice-context paths.

Live-verified on a token-standard-v2 LocalNet: fresh allocate -> withdraw
and allocate -> cancel each consume the Allocation; a committed
allocation correctly refuses early withdrawal.
@zheli

zheli commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Pushed two more allocation-flow fixes addressing the review (2860d2e, df311c6).

1. allocate — exercise the issuer's on-ledger AllocationFactory (2860d2e)

The scan registry returns the network-default (DSO) allocation factory, but an issuer-created test-token instrument requires exercising against the issuer's own on-ledger TokenRules contract — which is the V2 AllocationFactory (interface instance V2.AllocationFactory for TokenRules, admin == instrument admin). The DAML impl asserts allocation.admin == tokenRules.admin, so the DSO factory rejected issuer instruments with "Instrument-id must match the factory".

RunAllocate now resolves findTokenRules(admin), builds the choice context locally (TokenRules event-log + authorizer AccountConfig cids) instead of the registry blob, builds the authorizer Account from the picked holdings' own account (the impl asserts inputHolding.account == allocation.authorizer), sets the factory actors, and acts as the authorizer's account parties + admin. Mirrors the on-ledger transfer path.

2. withdraw / cancel — same root cause + the missing actors arg (df311c6)

Both were malformed two ways:

  • The choice argument omitted the required actors : [Party] field (AllocationV2.damlAllocation_Withdraw/_Cancel are with { actors; extraArgs }, controller actors), so submissions were unauthorized. This was the arg flagged in review.
  • The choice context was fetched from the scan registry, but the TestToken impl reads the local test-token context from extraArgs (unlockTokenAllocationV2 -> getEventLogFromContext, applyAllocationTransitions -> extractAccountConfigMap) — same on-ledger context as allocate.

runAllocationAction now fetches the target Allocation's view to resolve the admin / authorizer account / executors, resolves the on-ledger TokenRules, builds the local context, and exercises with the controller actors the state machine requires (AccountConfig.daml): withdraw → the authorizer's account parties, cancel → the settlement executors. Acts as those actors + admin (co-signs the unlocked holdings). Removed the now-unused registry withdraw/cancel choice-context paths.

settle is intentionally left as-is for a follow-up — it's not a one-line bug fix but a real feature (reconstruct the FinalizedAllocation batch + transferLegs, exercise SettlementFactory_SettleBatch on the on-ledger factory, and it only settles meaningfully with a two-sided DvP). Its GetSettlementFactory GET→POST bug and batch assembly stay behind the existing explicit not-proven error.

Verification: go build ./... + go test ./... (1592 pass, +2 new unit tests pinning the { actors; extraArgs } wire shape and the extended view walker); golangci-lint 0 issues. Live-verified on a token-standard-v2 LocalNet: fresh allocate → withdraw and allocate → cancel each consume the Allocation and drop it from token allocations; a committed allocation correctly refuses early withdrawal (cannot-withdraw-committed-allocation).

…e, atomic flag

Review fixes for the token V2 tightening PR:

- activity: keep the newest maxActivityScan events via a sliding ring
  buffer instead of breaking at the first cap (which returned the oldest
  slice). Applies to both the EventLog and netting paths.
- activity: dedup paired sender/receiver EventLog_HoldingsChange exercises
  by (updateID, sorted transferLegIds) so a transfer is not double-counted.
- activity: fall through to netting when the EventLog path fails in a
  fallback-safe way (stream open / malformed event); only a cancelled or
  expired context aborts.
- allocations: emit the shared types.AllocationsResponse from both the CLI
  JSON and the Web UI handler so the two surfaces cannot drift.
- allocations: disable the settle action (CLI, HTTP route, Web UI) until
  SettlementFactory_SettleBatch is functional on LocalNet; keep
  withdraw/cancel.
- ui: thread the active role through the allocations/transfer/allocate
  flows; add an experimental atomic-transfer control gated on auto-accept.
- docs: record the identity/allocations commands, the experimental
  --atomic flag, and the deferred settle verb in changes-from-proposal.md.

Adds tests for newest-first truncation (both paths), paired-side dedup,
and EventLog→netting fallback on stream error vs. context cancellation.
@zheli

zheli commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Addressed the review comments from #277 (review). Pushed as 2dd2d12.

Activity feed

  • Newest-first under the cap (review comment): replaced the "break at maxActivityScan" logic (which returned the oldest slice, since the Updates stream flows oldest→newest) with a fixed-capacity ring buffer that retains the newest maxActivityScan records and sets truncated once older ones are evicted. Applied to both the EventLog and netting paths.
  • Dedup paired sides (review comment): a transfer surfaces as two EventLog_HoldingsChange exercises (one per account side) sharing the same updateID + transferLegId. They're now collapsed by (updateID, sorted legIDs) so the movement isn't double-counted.
  • Fallback on error (review comment): the EventLog path now falls through to netting on fallback-safe errors (stream open / malformed event); only a cancelled/expired context aborts (netting would hit the same deadline).

DvP allocations

  • Shared response type (review comment): both token allocations --format json and GET /api/tokens/allocations now emit types.AllocationsResponse, pinned in the schema tests, so the CLI and Web UI shapes can't drift.
  • Settle disabled (review comment): the settle verb is removed from the CLI, the HTTP route, and the Web UI until SettlementFactory_SettleBatch is functional on LocalNet. Withdraw/cancel stay. The factory plumbing remains and is still unit-tested.

Web UI

  • Role threading (review comment): the active role is now passed through the allocations list, allocate, withdraw, and cancel flows.
  • Atomic transfer (review comment): added an experimental atomic-settlement control, disabled unless auto-accept is on, with an explicit warning that it's not yet supported on current Splice.

Docs (review comment): changes-from-proposal.md now records the token identity / token allocations (+ withdraw/cancel) commands, the experimental --atomic flag, and the deferred settle verb.

The execute-grants finding (review comment) and the actors finding (review comment, already threaded in this worktree) were left as-is per discussion.

Verification: go build ./..., go test ./... (1595 passing), and the frontend tsc --noEmit && vite build all green. Added tests cover newest-first truncation on both paths, paired-side dedup, and the EventLog→netting fallback (stream error vs. context cancellation).

@zheli
zheli merged commit 1793a42 into main Jul 25, 2026
25 of 29 checks passed
@zheli
zheli deleted the feat/token-v2-tightening branch July 25, 2026 09:56
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