refactor: WalletViewModel payLnurl
This commit is contained in:
@@ -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].
|
||||
|
||||
Reference in New Issue
Block a user