Skip to content

feat: canceled status for withdrawn transfer offers#346

Merged
sadiq1971 merged 5 commits into
mainfrom
feat/canceled-transfer-status
Jul 7, 2026
Merged

feat: canceled status for withdrawn transfer offers#346
sadiq1971 merged 5 commits into
mainfrom
feat/canceled-transfer-status

Conversation

@sadiq1971

@sadiq1971 sadiq1971 commented Jul 6, 2026

Copy link
Copy Markdown
Member

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 completed on any archive event. The ledger stream ran in ACS_DELTA shape, where an archive event doesn't say which choice archived the contract — so an accept and a cancel looked identical.

Fix

  • Stream in LEDGER_EFFECTS shape so archives arrive with the choice that caused them (pkg/cantonsdk/streaming).
  • Map the choice to a terminal status: Acceptcompleted, Withdraw (sender claim-back) → new persisted canceled status, Reject (receiver declined) → new persisted rejected status; unknown choices keep the old behavior (completed + warn log + metric).
  • FinalizeTransfer only updates still-pending rows, so replays can't overwrite a terminal status. No DB migration needed.
  • ?status=canceled and ?status=rejected supported 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: canceled and rejected are separate filter values — query/badge both. The middleware has no reject endpoint, so rejected rows only ever come from third-party wallets declining an offer.

Hardening (self-review follow-ups)

  • acs_delta gate: LEDGER_EFFECTS delivers 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 flagged acs_delta — exactly what the old ACS_DELTA stream delivered — while still reading the choice name.
  • New offer_unknown_archive_choices_total metric: 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.
  • Processor guards against an empty terminal status (defensive).

Testing

  • Unit: choice→status mapping, processor finalize paths, store filter + replay guard.
  • E2E: full api and indexer suites pass on a fresh devstack. TestUSDCx_ClaimBackExpiredOffer_ToExternalParty now asserts the withdrawn offer surfaces as canceled and is absent from the completed list.

Note: rows mis-marked completed before this change can only be repaired by re-indexing from ledger begin.

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.

@gemini-code-assist gemini-code-assist Bot 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.

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-commenter

codecov-commenter commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 54.41176% with 31 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@8a39cc8). Learn more about missing BASE report.

Files with missing lines Patch % Lines
pkg/indexer/engine/processor.go 11.11% 6 Missing and 2 partials ⚠️
pkg/indexer/store/instrumented.go 0.00% 8 Missing ⚠️
pkg/transfer/http.go 0.00% 5 Missing ⚠️
pkg/transfer/service.go 20.00% 2 Missing and 2 partials ⚠️
pkg/cantonsdk/streaming/client.go 71.42% 2 Missing ⚠️
pkg/indexer/service/http.go 0.00% 2 Missing ⚠️
pkg/app/indexer/server.go 0.00% 1 Missing ⚠️
pkg/indexer/store/pg.go 87.50% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #346   +/-   ##
=======================================
  Coverage        ?   31.94%           
=======================================
  Files           ?      154           
  Lines           ?    12249           
  Branches        ?        0           
=======================================
  Hits            ?     3913           
  Misses          ?     8020           
  Partials        ?      316           
Flag Coverage Δ
unittests 31.94% <54.41%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
pkg/cantonsdk/streaming/types.go 0.00% <ø> (ø)
pkg/indexer/client/http.go 71.09% <ø> (ø)
pkg/indexer/engine/decoder.go 74.49% <100.00%> (ø)
pkg/indexer/engine/metrics.go 100.00% <100.00%> (ø)
pkg/indexer/store/metrics.go 0.00% <ø> (ø)
pkg/app/indexer/server.go 0.00% <0.00%> (ø)
pkg/indexer/store/pg.go 68.97% <87.50%> (ø)
pkg/cantonsdk/streaming/client.go 17.18% <71.42%> (ø)
pkg/indexer/service/http.go 63.33% <0.00%> (ø)
pkg/transfer/service.go 72.34% <20.00%> (ø)
... and 3 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sadiq1971 sadiq1971 self-assigned this Jul 6, 2026
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.
@sadiq1971

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot 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.

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.

Comment thread pkg/indexer/engine/decoder.go
sadiq1971 added 2 commits July 7, 2026 15:44
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.
@dhyaniarun1993

Copy link
Copy Markdown
Member

@gemini-code-assist Please review

@gemini-code-assist gemini-code-assist Bot 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.

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.

Comment thread pkg/indexer/engine/decoder.go Outdated
Comment thread pkg/indexer/types.go Outdated
Comment thread pkg/transfer/service.go Outdated
Nil-guard the metrics counter in offerTerminalStatus and mention the
rejected status in the TransferQuery and ListOutgoing doc comments.
@sadiq1971 sadiq1971 merged commit 428e725 into main Jul 7, 2026
4 checks passed
@sadiq1971 sadiq1971 deleted the feat/canceled-transfer-status branch July 7, 2026 16:57
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