feat: canceled status for withdrawn transfer offers#346
Conversation
A withdrawn (claimed-back) offer was indexed as completed because the ACS_DELTA stream shape strips the archiving choice, making accept and withdraw indistinguishable. Switch the indexer stream to LEDGER_EFFECTS, decode consuming exercised events with their choice name, and map TransferInstruction_Withdraw/Reject to a new persisted canceled status (Accept still completes; unknown choices fall back to completed). FinalizeTransfer only updates still-pending rows so replayed archives cannot overwrite a terminal status. The outgoing list and indexer admin API accept status=canceled; the completed history no longer includes withdrawn offers.
There was a problem hiding this comment.
Code Review
This pull request transitions the Canton ledger stream shape from ACS_DELTA to LEDGER_EFFECTS to capture consuming exercised events. This allows the indexer to determine the terminal status of a TransferOffer (e.g., 'completed' on accept, or 'canceled' on withdraw/reject) based on the specific archiving choice name. The storage layer, HTTP APIs, and tests have been updated to support the new 'canceled' status, replacing the generic CompleteTransfer method with FinalizeTransfer to handle both completed and canceled terminal states. I have no feedback to provide as there are no review comments.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #346 +/- ##
=======================================
Coverage ? 31.94%
=======================================
Files ? 154
Lines ? 12249
Branches ? 0
=======================================
Hits ? 3913
Misses ? 8020
Partials ? 316
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Review follow-ups: - Decode only events flagged acs_delta. LEDGER_EFFECTS delivers witnessed events and transient contracts beyond the old stakeholder view; a witnessed Holding create whose archive is never delivered would leave a permanent phantom balance. The acs_delta flag marks exactly what an ACS_DELTA stream would have delivered, so gating on it preserves the old visibility semantics while keeping the archiving choice name. - Count unrecognized offer-archive choices in a new offer_unknown_archive_choices_total metric (alertable) in addition to the warn log, since each fallback may mislabel an offer as completed. - Guard against an empty terminal status in the processor (defensive). - Document canceled in the OutgoingTransfer status doc comment.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request transitions the indexer from using the ACS_DELTA shape to the LEDGER_EFFECTS shape in the Canton ledger stream. This enables the indexer to capture the specific choice name (such as accept, withdraw, or reject) that archived a TransferOffer, allowing it to distinguish between completed and canceled transfers. The storage layer, HTTP APIs, metrics, and tests have been updated to support this new "canceled" status. I have no additional feedback to provide as no review comments were submitted.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Review follow-up (#346 discussion): withdraw and reject archive an offer for different reasons — the sender claiming it back vs the receiver declining it — and collapsing both into canceled loses that permanently (status is the only persisted record of the archiving choice). TransferInstruction_Withdraw now maps to canceled and TransferInstruction_Reject to a new persisted rejected status. Both filterable via ?status= on the outgoing list and the indexer admin API; claim-back rejects already-rejected offers with a specific message. No migration needed.
Canceled/rejected offers surface in history UIs (dapp Activity), which sort and day-group rows by time; the outgoing payload previously carried only expires_at. Expose the offer's creation ledger time as created_at.
|
@gemini-code-assist Please review |
There was a problem hiding this comment.
Code Review
This pull request transitions the streaming client from using the ACS_DELTA transaction shape to LEDGER_EFFECTS, enabling the indexer to capture the specific consuming choice name when a TransferOffer is archived. This allows the indexer to resolve and persist granular terminal statuses ("completed", "canceled", and "rejected") instead of treating all archives as completed. The changes include updates to the streaming client, event decoders, database store, HTTP APIs, and associated tests. The review feedback is highly constructive, pointing out a potential nil pointer dereference when incrementing metrics in the decoder, and identifying a couple of code comments that need to be updated to document the newly introduced "rejected" status.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Nil-guard the metrics counter in offerTerminalStatus and mention the rejected status in the TransferQuery and ListOutgoing doc comments.
Issue
When a sender cancels (claims back) a USDCx transfer offer, the transfer still shows up as a completed sent transfer in
/api/v2/transfer/completed— as if the recipient had accepted it. Wallets like Loop correctly show the same transfer as Canceled.Cause: the indexer marked an offer
completedon any archive event. The ledger stream ran inACS_DELTAshape, where an archive event doesn't say which choice archived the contract — so an accept and a cancel looked identical.Fix
LEDGER_EFFECTSshape so archives arrive with the choice that caused them (pkg/cantonsdk/streaming).Accept→completed,Withdraw(sender claim-back) → new persistedcanceledstatus,Reject(receiver declined) → new persistedrejectedstatus; unknown choices keep the old behavior (completed+ warn log + metric).FinalizeTransferonly updates still-pending rows, so replays can't overwrite a terminal status. No DB migration needed.?status=canceledand?status=rejectedsupported on the outgoing list (api-server + indexer admin API); claim-back rejects offers already in a terminal state; canceled/rejected offers no longer appear in the completed history.Note for the dapp:
canceledandrejectedare separate filter values — query/badge both. The middleware has no reject endpoint, sorejectedrows only ever come from third-party wallets declining an offer.Hardening (self-review follow-ups)
acs_deltagate:LEDGER_EFFECTSdelivers more than the old stakeholder view (witnessed events, transient contracts), which could leave phantom balances if a witnessed holding create's archive is never delivered. The decoder now only processes events flaggedacs_delta— exactly what the oldACS_DELTAstream delivered — while still reading the choice name.offer_unknown_archive_choices_totalmetric: any offer archived by a choice outside the known mapping (e.g. a registry-side cleanup choice) increments it, so mislabeling is alertable instead of log-only.Testing
apiandindexersuites pass on a fresh devstack.TestUSDCx_ClaimBackExpiredOffer_ToExternalPartynow asserts the withdrawn offer surfaces ascanceledand is absent from the completed list.Note: rows mis-marked
completedbefore this change can only be repaired by re-indexing from ledger begin.