refactor: WalletViewModel payLnurl
This commit is contained in:
@@ -586,7 +586,6 @@ class WalletViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun payLnurl(
|
fun payLnurl(
|
||||||
rawRes : LnurlScanResponse,
|
rawRes : LnurlScanResponse,
|
||||||
lnurl : String,
|
lnurl : String,
|
||||||
@@ -596,123 +595,52 @@ class WalletViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_sendState.value = SendState.Paying
|
_sendState.value = SendState.Paying
|
||||||
|
|
||||||
// ── 1. Try client-side pay first ──────────────────────────────────────
|
// ── 1. Initial attempt (direct → proxy) ───────────────────────────────
|
||||||
val directResult = runCatching {
|
val initial = tryPayLnurlWithFallback(rawRes, lnurl, amountMsat, comment, "INITIAL")
|
||||||
repo.payLnurlDirect(rawRes, amountMsat, comment)
|
if (initial.isSuccess) {
|
||||||
}
|
recordPaymentSuccess(initial.getOrThrow(), amountMsat)
|
||||||
|
|
||||||
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()
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.w(TAG, "LNURL [PAY DIRECT FAIL] ${directResult.exceptionOrNull()?.message} — falling back to server proxy")
|
// ── 2. Both paths failed — check if retrying is worthwhile ────────────
|
||||||
|
val initialError = initial.exceptionOrNull() ?: Exception("Payment failed")
|
||||||
// ── 2. Client-side failed — try server proxy ───────────────────────────
|
if (isTerminalPayError(initialError)) {
|
||||||
val firstResult = runCatching { repo.payLnurl(rawRes, lnurl, amountMsat, comment) }
|
Log.w(TAG, "LNURL [PAY TERMINAL] ${initialError.message} — not retrying")
|
||||||
|
|
||||||
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")
|
|
||||||
_sendState.value = SendState.Error(
|
_sendState.value = SendState.Error(
|
||||||
parseApiError(firstError ?: Exception("Payment failed"), "Payment failed")
|
parseApiError(initialError, "Payment failed")
|
||||||
)
|
)
|
||||||
return@launch
|
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)
|
lnurlCache.invalidate(lnurl)
|
||||||
|
|
||||||
// ── 4. Re-fetch metadata — try client-side first, then proxy ──────────
|
// ── 3. Re-fetch metadata ──────────────────────────────────────────────
|
||||||
val freshRaw: LnurlScanResponse = run rescan@{
|
val freshRaw = rescanLnurl(lnurl) ?: return@launch // error already set
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
val scanProtocolError = lnurlProtocolError(freshRaw)
|
||||||
if (scanProtocolError != null) {
|
if (scanProtocolError != null) {
|
||||||
Log.w(TAG, "LNURL [RESCAN PROTO] $scanProtocolError")
|
Log.w(TAG, "LNURL [RESCAN PROTO] $scanProtocolError")
|
||||||
_sendState.value = SendState.Error(scanProtocolError)
|
_sendState.value = SendState.Error(scanProtocolError)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
lnurlCache.put(lnurl, freshRaw)
|
lnurlCache.put(lnurl, freshRaw)
|
||||||
|
|
||||||
// ── 5. Final attempt — client-side first, then proxy ──────────────────
|
// ── 4. Final attempt (direct → proxy) with fresh metadata ─────────────
|
||||||
val finalDirectResult = runCatching {
|
val retry = tryPayLnurlWithFallback(freshRaw, lnurl, amountMsat, comment, "RETRY")
|
||||||
repo.payLnurlDirect(freshRaw, amountMsat, comment)
|
if (retry.isSuccess) {
|
||||||
}
|
recordPaymentSuccess(retry.getOrThrow(), amountMsat)
|
||||||
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()
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
runCatching { repo.payLnurl(freshRaw, lnurl, amountMsat, comment) }
|
// ── 5. Final failure ──────────────────────────────────────────────────
|
||||||
.onSuccess { result ->
|
val retryError = retry.exceptionOrNull() ?: Exception("Payment failed")
|
||||||
val feeSats = runCatching {
|
Log.e(TAG, "LNURL [PAY FINAL ERR] ${retryError.message}")
|
||||||
repo.getPaymentDetail(result.paymentHash).details?.feeSat
|
_sendState.value = SendState.Error(
|
||||||
}.getOrNull()
|
parseApiError(retryError, "Payment failed")
|
||||||
_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"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun isTerminalPayError(e: Throwable?): Boolean {
|
private fun isTerminalPayError(e: Throwable?): Boolean {
|
||||||
if (e == null) return false
|
if (e == null) return false
|
||||||
@@ -902,13 +830,11 @@ class WalletViewModel(
|
|||||||
_clipboardOfferState.value = ClipboardOfferState.Detected(raw = trimmed, inputType = type)
|
_clipboardOfferState.value = ClipboardOfferState.Detected(raw = trimmed, inputType = type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun dismissClipboardOffer() {
|
fun dismissClipboardOffer() {
|
||||||
_clipboardOfferState.value = ClipboardOfferState.None
|
_clipboardOfferState.value = ClipboardOfferState.None
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Private helpers ───────────────────────────────────────────────────────
|
// ── Private helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun buildLnurlReadyState(raw: LnurlScanResponse, lnurl: String): SendState.LnurlReady {
|
private fun buildLnurlReadyState(raw: LnurlScanResponse, lnurl: String): SendState.LnurlReady {
|
||||||
return SendState.LnurlReady(
|
return SendState.LnurlReady(
|
||||||
callback = raw.callback ?: "",
|
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
|
* Fires background coroutines to resolve node aliases for [payee] and each
|
||||||
* pubkey in [routeHints].
|
* pubkey in [routeHints].
|
||||||
|
|||||||
Reference in New Issue
Block a user