mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 00:37:41 +00:00
fix(topup): never re-move funds on retry; wait for proofs before zapping
Bug: a top-up that moved funds but then failed at the nutzap (the freshly minted proofs hadn't landed in local state yet → "No proofs available") left a Failed state; tapping "Try again" re-ran the WHOLE pipeline and moved the funds a second time — two transfers for one zap. - Add a `toppedUp` checkpoint set the moment funds land at the target (after rebalance / completeMintFromLightning). confirm()/retry now skips the move entirely once topped up and only (re)sends the zap — funds can never move twice. - awaitTargetFunded(): briefly poll the target balance after topping up so the follow-up nutzap sees the new proofs and succeeds on the first try instead of needing a manual retry. Best-effort with a timeout; the checkpoint guarantees no double-move even if it falls through. https://claude.ai/code/session_01HNE2z7CSYZ2G8KwC5fziJn
This commit is contained in:
+33
-2
@@ -128,6 +128,11 @@ class ReloadMintViewModel : ViewModel() {
|
||||
* scope, not this VM's). */
|
||||
private var job: Job? = null
|
||||
|
||||
/** Set once the target mint has actually been topped up, so a retry after a
|
||||
* later failure (e.g. the send) re-sends only — it must never move funds
|
||||
* again. Without this, "Try again" re-ran the whole pipeline and double-spent. */
|
||||
private var toppedUp = false
|
||||
|
||||
private var recipient: String? = null
|
||||
|
||||
private val _uiState = MutableStateFlow(ReloadUiState())
|
||||
@@ -278,8 +283,9 @@ class ReloadMintViewModel : ViewModel() {
|
||||
vm.launchSigner {
|
||||
try {
|
||||
when {
|
||||
// The target already covers the send — no top-up, just zap.
|
||||
moveSats <= 0L -> sendNutzapAndFinish(note, s.sendSats)
|
||||
// Already topped up (or the target already covers the send) —
|
||||
// never move funds again on retry, just (re)send the zap.
|
||||
toppedUp || moveSats <= 0L -> sendNutzapAndFinish(note, s.sendSats)
|
||||
source is ReloadSource.Mint -> rebalanceThenZap(source.mintUrl, s.selectedTarget, moveSats, note, s.sendSats)
|
||||
source is ReloadSource.LightningWallet ->
|
||||
reloadFromLightningThenZap(s.selectedTarget, moveSats, walletUriFor(source.walletId), note, s.sendSats)
|
||||
@@ -306,6 +312,9 @@ class ReloadMintViewModel : ViewModel() {
|
||||
st.rebalance(sourceMint, targetMint, moveSats) { p ->
|
||||
setStatus(ReloadStatus.Working("Moving funds", p.coerceIn(0.1f, 0.9f)))
|
||||
}
|
||||
// Funds have moved — checkpoint so a later failure never re-moves them.
|
||||
toppedUp = true
|
||||
awaitTargetFunded(targetMint, sendSats)
|
||||
sendNutzapAndFinish(note, sendSats)
|
||||
}
|
||||
|
||||
@@ -355,9 +364,31 @@ class ReloadMintViewModel : ViewModel() {
|
||||
}
|
||||
setStatus(ReloadStatus.Working("Issuing ecash", 0.85f))
|
||||
ops.completeMintFromLightning(targetMint, flow.quoteEvent, moveSats)
|
||||
// Ecash minted at the target — checkpoint so a later failure never re-mints.
|
||||
toppedUp = true
|
||||
awaitTargetFunded(targetMint, sendSats)
|
||||
sendNutzapAndFinish(note, sendSats)
|
||||
}
|
||||
|
||||
/**
|
||||
* Freshly moved/minted proofs reach [CashuWalletState] asynchronously (the
|
||||
* kind:7375 token event round-trips before [peekMintBalances] reflects it).
|
||||
* Give the target a moment to show the balance so the nutzap that follows
|
||||
* doesn't fail with "No proofs available" on the first try. Best-effort: if it
|
||||
* doesn't land in time the send still proceeds (and fails recoverably without
|
||||
* re-moving funds, thanks to the [toppedUp] checkpoint).
|
||||
*/
|
||||
private suspend fun awaitTargetFunded(
|
||||
targetMint: String,
|
||||
sats: Long,
|
||||
) {
|
||||
val st = state ?: return
|
||||
repeat(12) {
|
||||
if ((st.peekMintBalances()[targetMint] ?: 0L) >= sats) return
|
||||
delay(500)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the nutzap and only report [ReloadStatus.Done] once it actually succeeds.
|
||||
* Suspends on the real send (throwing on failure) instead of the fire-and-forget
|
||||
|
||||
Reference in New Issue
Block a user