Skip to content

Fix mint-to-mint transfer reliability#71

Open
zeugmaster wants to merge 6 commits into
releasefrom
fix/transfer-bugs
Open

Fix mint-to-mint transfer reliability#71
zeugmaster wants to merge 6 commits into
releasefrom
fix/transfer-bugs

Conversation

@zeugmaster

Copy link
Copy Markdown
Owner
  • I made sure to choose branch RELEASE as the target for this pull request

Summary

Fixes the reported fund-loss scenario where a transfer's lightning payment succeeded but the ecash issuance at the destination mint failed, leaving funds stranded as a paid-but-unissued mint quote that seed restore cannot recover.

Transfer flow

  • Retry/polling for the issue step: after a successful melt, the destination mint quote is polled until paid (settlement-lag race), then issuance is attempted with retry and exponential backoff (~30s budget). Exhausted retries park the transfer as a resumable pending state instead of a dead-end failure (new shared TransferFlow engine, used by both swap managers).
  • Destination-first resume: "Complete Transfer" now checks the destination mint quote first and can finish issuance without requiring melt data; the melt-quote-pending dead end is gone. Resume decisions are a pure, unit-tested function.
  • Fund-preservation on melt errors: inputs only revert to valid when the mint verifiably reports the quote unpaid. Timeouts/unknown outcomes keep proofs pending and the event resumable; a one-shot meltState recheck catches payments that settled despite a thrown error.

Data model

  • New optional Event.fromMint/toMint relationships set on creation — SwiftData does not preserve to-many array order (proven by test), so positional mints[0]/[1] was unreliable. Existing rows are untouched and resolve via a proofs-orientation fallback. Lightweight migration verified by an on-disk store test.

UI

  • TransferView: live quote-state rows and expiry countdown instead of data-presence checkmarks; the alert modifier was never attached (errors were silently swallowed); "Remove Payment" now verifies the quote is unpaid and checks proof states (NUT-07) before reverting.
  • New "pending" state surfaced in SwapView, BalancerView and RedeemView.

Dependencies

  • CashuSwift and Bolt11 now pinned by version tag (up to next minor: 0.4.1 / 0.1.3) instead of tracking branches.

Localization

  • String catalog fully translated: 56 keys × 11 languages added (ar, bn, de, es, fr, hi, ja, ko, pt-BR, ru, tr).

Test plan

  • 13 new unit tests (TransferFlowTests): error classification, resume decision table, poll/retry backoff behavior, endpoint resolution, on-disk lightweight migration. All passing, plus existing legacy-store migration tests.
  • Manual testing on device against local mints.

🤖 Generated with Claude Code

zeugmaster and others added 6 commits June 10, 2026 10:58
…transfer event helpers

- pollAndIssue: polls the destination mint quote until paid, then issues
  ecash with retry and exponential backoff on transient errors (~30s budget)
- pure resumeAction decision function (destination-first state machine)
- classifyMeltFailure: distinguish definitive-unpaid from unknown outcome
- Event.transferMints orients from/to via the proofs relationship because
  SwiftData does not preserve to-many relationship array order
- persist mint quote expiry into pending transfer events (existing field)
- fix latent index trap in EventList/EventSummary mint labels

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nagers

- issue step now uses TransferFlow.pollAndIssue instead of a single-shot
  mint call; failures park the transfer as resumable pending state
- melt failures only revert inputs to valid when the mint verifiably
  reports the quote unpaid; unknown outcomes (timeouts) keep proofs
  pending and preserve event data instead of wiping blankOutputs/proofs
- melt errors trigger a one-shot meltState recheck: a payment that
  settled despite a thrown error now continues into issuance
- resumeTransfer checks the destination mint quote first and no longer
  requires meltQuote/proofs/blankOutputs for the recovery path
- new .pending state handled in SwapView, BalancerView, RedeemView
- TransferView: live quote status rows and expiry instead of data-
  presence checkmarks, alert view was never attached (errors were
  silently swallowed), removal now verifies unpaid state and per-proof
  NUT-07 state with the mint before reverting

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SwiftData does not preserve to-many relationship array order (proven by
test: [A, B] came back [B, A] after a single save), so the positional
from/to convention on Event.mints is unreliable. New transfer events now
carry unidirectional optional fromMint/toMint relationships, set on
creation and preferred by transferMints. Existing rows stay untouched
and resolve via the proofs-orientation/positional fallback.

Adding optional relationships is a lightweight migration; verified by an
on-disk store test (pre-change schema -> current schema -> reopen) and
the existing legacy-store migration tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CashuSwift: payment-methods branch -> up to next minor from 0.4.1
Bolt11: main branch -> up to next minor from 0.1.3

The branch pins forced a stale Bolt11 revision over cashu-swift's
exact 0.1.3 requirement, breaking the build against Bech32Swift 1.0.1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add translations (ar, bn, de, es, fr, hi, ja, ko, pt-BR, ru, tr) for the
new transfer-flow strings and for previously untranslated keys — 56 keys,
617 translations. Catalog is now fully translated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant