refactor: WalletViewModel payLnurl

This commit is contained in:
2026-06-01 19:48:22 +02:00
parent 40f7aa00ae
commit 5179b24b44
@@ -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<String> {
// 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(
}
}
}
}
}