diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt index 9218037079..902ca796d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt @@ -1179,6 +1179,7 @@ class CashuWalletState( targetMintUrl: String, sats: Long, onProgress: ((Float) -> Unit)? = null, + onFundsMoved: () -> Unit = {}, ): RebalanceCompleted { check(started) { "CashuWalletState.start() not called" } require(sats > 0) { "Amount must be positive" } @@ -1207,6 +1208,11 @@ class CashuWalletState( // 3. Pay the destination invoice by melting at the source. onProgress?.invoke(0.5f) meltToLightning(sourceMintUrl, meltQuote) + // Funds have now LEFT the source. Signal the caller immediately so a + // failure in the steps below (slow confirmation, completeMint error) + // never causes a retry to melt a second time — the destination quote is + // paid and resumable via the pending-quote banner instead. + onFundsMoved() // 4. The melt paid the invoice; confirm + issue proofs at the target. // The melt settled synchronously, but a healthy mint can still lag a diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/ReloadMintViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/ReloadMintViewModel.kt index 7de2acdc35..477adc4af4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/ReloadMintViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/ReloadMintViewModel.kt @@ -309,11 +309,16 @@ class ReloadMintViewModel : ViewModel() { ) { val st = state ?: return setStatus(ReloadStatus.Working("Moving funds", 0.1f)) - 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 + st.rebalance( + sourceMintUrl = sourceMint, + targetMintUrl = targetMint, + sats = moveSats, + onProgress = { p -> setStatus(ReloadStatus.Working("Moving funds", p.coerceIn(0.1f, 0.9f))) }, + // Checkpoint the instant funds leave the source — not after the whole + // rebalance returns. A failure between the melt and the issue/send steps + // must never let a retry melt again. + onFundsMoved = { toppedUp = true }, + ) awaitTargetFunded(targetMint, sendSats) sendNutzapAndFinish(note, sendSats) } @@ -362,10 +367,12 @@ class ReloadMintViewModel : ViewModel() { setStatus(ReloadStatus.Failed("Invoice not paid yet — you can finish it later from the pending quote banner")) return } + // The invoice is paid — money has left the wallet. Checkpoint now, before + // issuing, so a failure in completeMintFromLightning never lets a retry + // mint+pay a second time (the paid quote is resumable from the banner). + toppedUp = true 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) }