From 5179b24b44e79aa175569b1ec7885ebda25cce18 Mon Sep 17 00:00:00 2001 From: rasputin Date: Mon, 1 Jun 2026 19:48:22 +0200 Subject: [PATCH] refactor: WalletViewModel payLnurl --- .../gudariwallet/ui/WalletViewModel.kt | 195 +++++++++--------- 1 file changed, 98 insertions(+), 97 deletions(-) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt index 9b87ad5..7cc9dfa 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt @@ -586,7 +586,6 @@ class WalletViewModel( } } - fun payLnurl( rawRes : LnurlScanResponse, lnurl : String, @@ -596,124 +595,53 @@ class WalletViewModel( viewModelScope.launch { _sendState.value = SendState.Paying - // ── 1. Try client-side pay first ────────────────────────────────────── - val directResult = runCatching { - repo.payLnurlDirect(rawRes, amountMsat, comment) - } - - if (directResult.isSuccess) { - val feeSats = runCatching { - repo.getPaymentDetail(directResult.getOrThrow().paymentHash) - .details?.feeSat - }.getOrNull() - _sendState.value = SendState.PaymentSent( - amountSats = amountMsat / WalletConstants.MSAT_PER_SAT, - feeSats = feeSats - ) - refreshBalance() + // ── 1. Initial attempt (direct → proxy) ─────────────────────────────── + val initial = tryPayLnurlWithFallback(rawRes, lnurl, amountMsat, comment, "INITIAL") + if (initial.isSuccess) { + recordPaymentSuccess(initial.getOrThrow(), amountMsat) return@launch } - Log.w(TAG, "LNURL [PAY DIRECT FAIL] ${directResult.exceptionOrNull()?.message} — falling back to server proxy") - - // ── 2. Client-side failed — try server proxy ─────────────────────────── - val firstResult = runCatching { repo.payLnurl(rawRes, lnurl, amountMsat, comment) } - - if (firstResult.isSuccess) { - val feeSats = runCatching { - repo.getPaymentDetail(firstResult.getOrThrow().paymentHash) - .details?.feeSat - }.getOrNull() - _sendState.value = SendState.PaymentSent( - amountSats = amountMsat / WalletConstants.MSAT_PER_SAT, - feeSats = feeSats - ) - refreshBalance() - return@launch - } - - // ── 3. Both failed — check if worth retrying ─────────────────────────── - val firstError = firstResult.exceptionOrNull() - if (isTerminalPayError(firstError)) { - Log.w(TAG, "LNURL [PAY TERMINAL] ${firstError?.message} — not retrying") + // ── 2. Both paths failed — check if retrying is worthwhile ──────────── + val initialError = initial.exceptionOrNull() ?: Exception("Payment failed") + if (isTerminalPayError(initialError)) { + Log.w(TAG, "LNURL [PAY TERMINAL] ${initialError.message} — not retrying") _sendState.value = SendState.Error( - parseApiError(firstError ?: Exception("Payment failed"), "Payment failed") + parseApiError(initialError, "Payment failed") ) return@launch } - Log.w(TAG, "LNURL [PAY RETRY ] ${firstError?.message} — invalidating cache and retrying with fresh scan") + Log.w(TAG, "LNURL [PAY RETRY ] — invalidating cache and retrying with fresh scan") lnurlCache.invalidate(lnurl) - // ── 4. Re-fetch metadata — try client-side first, then proxy ────────── - val freshRaw: LnurlScanResponse = run rescan@{ - // Try client-side rescan - val directRescan = runCatching { repo.scanLnurlDirect(lnurl) } - if (directRescan.isSuccess) { - Log.d(TAG, "LNURL [RESCAN DIRECT] success") - return@rescan directRescan.getOrThrow().rawRes - } + // ── 3. Re-fetch metadata ────────────────────────────────────────────── + val freshRaw = rescanLnurl(lnurl) ?: return@launch // error already set - Log.w(TAG, "LNURL [RESCAN DIRECT FAIL] ${directRescan.exceptionOrNull()?.message} — trying proxy rescan") - val proxyRescan = runCatching { repo.scanLnurl(lnurl) } - if (proxyRescan.isSuccess) { - return@rescan proxyRescan.getOrThrow().rawRes - } - - Log.e(TAG, "LNURL [RESCAN ERR] ${proxyRescan.exceptionOrNull()?.message}") - _sendState.value = SendState.Error( - parseApiError( - proxyRescan.exceptionOrNull() ?: Exception(), - "Payment failed — could not re-fetch address" - ) - ) - return@launch - } val scanProtocolError = lnurlProtocolError(freshRaw) if (scanProtocolError != null) { Log.w(TAG, "LNURL [RESCAN PROTO] $scanProtocolError") _sendState.value = SendState.Error(scanProtocolError) return@launch } - lnurlCache.put(lnurl, freshRaw) - // ── 5. Final attempt — client-side first, then proxy ────────────────── - val finalDirectResult = runCatching { - repo.payLnurlDirect(freshRaw, amountMsat, comment) - } - if (finalDirectResult.isSuccess) { - val feeSats = runCatching { - repo.getPaymentDetail(finalDirectResult.getOrThrow().paymentHash) - .details?.feeSat - }.getOrNull() - _sendState.value = SendState.PaymentSent( - amountSats = amountMsat / WalletConstants.MSAT_PER_SAT, - feeSats = feeSats - ) - refreshBalance() + // ── 4. Final attempt (direct → proxy) with fresh metadata ───────────── + val retry = tryPayLnurlWithFallback(freshRaw, lnurl, amountMsat, comment, "RETRY") + if (retry.isSuccess) { + recordPaymentSuccess(retry.getOrThrow(), amountMsat) return@launch } - runCatching { repo.payLnurl(freshRaw, lnurl, amountMsat, comment) } - .onSuccess { result -> - val feeSats = runCatching { - repo.getPaymentDetail(result.paymentHash).details?.feeSat - }.getOrNull() - _sendState.value = SendState.PaymentSent( - amountSats = amountMsat / WalletConstants.MSAT_PER_SAT, - feeSats = feeSats - ) - refreshBalance() - } - .onFailure { e -> - Log.e(TAG, "LNURL [PAY FINAL ERR] ${e.message}") - _sendState.value = SendState.Error(parseApiError(e, "Payment failed")) - } + // ── 5. Final failure ────────────────────────────────────────────────── + val retryError = retry.exceptionOrNull() ?: Exception("Payment failed") + Log.e(TAG, "LNURL [PAY FINAL ERR] ${retryError.message}") + _sendState.value = SendState.Error( + parseApiError(retryError, "Payment failed") + ) } } - private fun isTerminalPayError(e: Throwable?): Boolean { if (e == null) return false val msg = e.message?.lowercase() ?: return false @@ -902,13 +830,11 @@ class WalletViewModel( _clipboardOfferState.value = ClipboardOfferState.Detected(raw = trimmed, inputType = type) } - fun dismissClipboardOffer() { _clipboardOfferState.value = ClipboardOfferState.None } // ── Private helpers ─────────────────────────────────────────────────────── - private fun buildLnurlReadyState(raw: LnurlScanResponse, lnurl: String): SendState.LnurlReady { return SendState.LnurlReady( callback = raw.callback ?: "", @@ -922,6 +848,81 @@ class WalletViewModel( ) } + /** + * Records a successful payment: fetches the fee, updates send state, + * and refreshes the balance. Centralises the 4 identical success blocks. + */ + private suspend fun recordPaymentSuccess(paymentHash: String, amountMsat: Long) { + val feeSats = runCatching { + repo.getPaymentDetail(paymentHash).details?.feeSat + }.getOrNull() + _sendState.value = SendState.PaymentSent( + amountSats = amountMsat / WalletConstants.MSAT_PER_SAT, + feeSats = feeSats + ) + refreshBalance() + } + + + /** + * Attempts a client-side LNURL payment, then falls back to the server proxy. + * Returns [Result.success] with the payment hash, or [Result.failure] with the + * last exception (from the proxy attempt) so the caller can inspect it without + * a second network round-trip. + */ + private suspend fun tryPayLnurlWithFallback( + rawRes : LnurlScanResponse, + lnurl : String, + amountMsat: Long, + comment : String?, + label : String + ): Result { + // 1. Client-side direct + val direct = runCatching { repo.payLnurlDirect(rawRes, amountMsat, comment) } + if (direct.isSuccess) { + Log.d(TAG, "LNURL [PAY $label DIRECT] success") + return Result.success(direct.getOrThrow().paymentHash) + } + Log.w(TAG, "LNURL [PAY $label DIRECT FAIL] ${direct.exceptionOrNull()?.message} — trying proxy") + + // 2. Server proxy — its exception becomes the Result.failure + val proxy = runCatching { repo.payLnurl(rawRes, lnurl, amountMsat, comment) } + if (proxy.isSuccess) { + Log.d(TAG, "LNURL [PAY $label PROXY] success") + return Result.success(proxy.getOrThrow().paymentHash) + } + Log.w(TAG, "LNURL [PAY $label PROXY FAIL] ${proxy.exceptionOrNull()?.message}") + return Result.failure(proxy.exceptionOrNull() ?: Exception("Payment failed")) + } + + /** + * Re-fetches LNURL metadata: tries client-side first, then server proxy. + * Returns the fresh [LnurlScanResponse], or null if both fail (in which case + * [_sendState] is already set to [SendState.Error]). + */ + private suspend fun rescanLnurl(lnurl: String): LnurlScanResponse? { + val direct = runCatching { repo.scanLnurlDirect(lnurl) } + if (direct.isSuccess) { + Log.d(TAG, "LNURL [RESCAN DIRECT] success") + return direct.getOrThrow().rawRes + } + Log.w(TAG, "LNURL [RESCAN DIRECT FAIL] ${direct.exceptionOrNull()?.message} — trying proxy rescan") + + val proxy = runCatching { repo.scanLnurl(lnurl) } + if (proxy.isSuccess) { + return proxy.getOrThrow().rawRes + } + + Log.e(TAG, "LNURL [RESCAN ERR] ${proxy.exceptionOrNull()?.message}") + _sendState.value = SendState.Error( + parseApiError( + proxy.exceptionOrNull() ?: Exception(), + "Payment failed — could not re-fetch address" + ) + ) + return null + } + /** * Fires background coroutines to resolve node aliases for [payee] and each * pubkey in [routeHints]. @@ -954,4 +955,4 @@ class WalletViewModel( } } } -} +} \ No newline at end of file