fix: send payment shows as failed even when succeeded

This commit is contained in:
2026-06-11 20:34:00 +02:00
parent a2754a7168
commit 78eb034327
4 changed files with 62 additions and 44 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-05-31T17:55:52.851171Z"> <DropdownSelection timestamp="2026-06-11T09:22:11.580091Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=48090DLAQ00119" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=48090DLAQ00119" />
+7 -1
View File
@@ -27,6 +27,7 @@
# looked up by name at runtime via SerializersModule. # looked up by name at runtime via SerializersModule.
# ────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────
-keepattributes *Annotation*, InnerClasses -keepattributes *Annotation*, InnerClasses
-keep class kotlinx.serialization.SerializersKt { *; }
-dontnote kotlinx.serialization.** -dontnote kotlinx.serialization.**
-dontwarn kotlinx.serialization.** -dontwarn kotlinx.serialization.**
@@ -51,10 +52,13 @@
-keep,allowobfuscation,allowshrinking interface retrofit2.Call -keep,allowobfuscation,allowshrinking interface retrofit2.Call
-keep,allowobfuscation,allowshrinking class retrofit2.Response -keep,allowobfuscation,allowshrinking class retrofit2.Response
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
-keep,allowobfuscation,allowshrinking class * extends kotlin.coroutines.CoroutineContext$Element { *; }
# ────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────
# OkHttp 5.x / Okio # OkHttp 5.x / Okio
# ────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────
-keep class okhttp3.internal.publicsuffix.PublicSuffixDatabase { *; }
-keepnames class okhttp3.** { *; }
-dontwarn okhttp3.** -dontwarn okhttp3.**
-dontwarn okio.** -dontwarn okio.**
-dontwarn javax.annotation.** -dontwarn javax.annotation.**
@@ -101,8 +105,9 @@
# ────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────
# Room 2.7.1 # Room 2.7.1
# ────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────
-keep class * extends androidx.room.RoomDatabase
-keep @androidx.room.Entity class * -keep @androidx.room.Entity class *
-keep @androidx.room.Dao interface *
-keep class * extends androidx.room.RoomDatabase { *; }
-dontwarn androidx.room.** -dontwarn androidx.room.**
# ────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────
@@ -124,6 +129,7 @@
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} -keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-dontwarn kotlinx.coroutines.** -dontwarn kotlinx.coroutines.**
-dontwarn kotlinx.coroutines.debug.*
# ────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────
# Lyricist 1.8.0 # Lyricist 1.8.0
@@ -35,9 +35,9 @@ data class CreateInvoiceRequest(
@Serializable @Serializable
data class CreateInvoiceResponse( data class CreateInvoiceResponse(
@SerialName("payment_hash") val paymentHash: String, @SerialName("payment_hash") val paymentHash: String = "",
@SerialName("payment_request") val paymentRequest: String, @SerialName("payment_request") val paymentRequest: String = "",
@SerialName("expiry") val expiry: String? @SerialName("expiry") val expiry: String? = null
) )
// ── Pay invoice ─────────────────────────────────────────────────────────────── // ── Pay invoice ───────────────────────────────────────────────────────────────
@@ -49,22 +49,22 @@ data class PayInvoiceRequest(
@Serializable @Serializable
data class PayInvoiceResponse( data class PayInvoiceResponse(
@SerialName("payment_hash") val paymentHash: String, @SerialName("payment_hash") val paymentHash: String = "",
@SerialName("checking_id") val checkingId: String @SerialName("checking_id") val checkingId: String = ""
) )
// ── Check payment ───────────────────────────────────────────────────────────── // ── Check payment ─────────────────────────────────────────────────────────────
@Serializable @Serializable
data class PaymentStatusResponse( data class PaymentStatusResponse(
val paid: Boolean, val paid: Boolean = false,
val details: PaymentDetails? = null val details: PaymentDetails? = null
) )
@Serializable @Serializable
data class PaymentDetails( data class PaymentDetails(
val amount: Long?, val amount: Long? = null,
val fee: Long?, val fee: Long? = null,
val memo: String? val memo: String? = null
) )
// ── Decode BOLT-11 ──────────────────────────────────────────────────────────── // ── Decode BOLT-11 ────────────────────────────────────────────────────────────
@@ -84,7 +84,7 @@ data class DecodeInvoiceResponse(
@Serializable @Serializable
data class RouteHintHop( data class RouteHintHop(
@SerialName("public_key") val publicKey: String, @SerialName("public_key") val publicKey: String = "",
@SerialName("short_channel_id") val shortChannelId: String? = null, @SerialName("short_channel_id") val shortChannelId: String? = null,
@SerialName("base_fee") val baseFeeMsat: Long = 0, @SerialName("base_fee") val baseFeeMsat: Long = 0,
@SerialName("ppm_fee") val ppmFee: Long = 0, @SerialName("ppm_fee") val ppmFee: Long = 0,
@@ -94,7 +94,7 @@ data class RouteHintHop(
// ── LNURL scan ──────────────────────────────────────────────────────────────── // ── LNURL scan ────────────────────────────────────────────────────────────────
@Serializable @Serializable
data class LnurlScanResponse( data class LnurlScanResponse(
val tag: String?, val tag: String? = null,
// ── payRequest fields ───────────────────────────────────────────────────── // ── payRequest fields ─────────────────────────────────────────────────────
val callback: String? = null, val callback: String? = null,
@SerialName("minSendable") val minSendable: Long? = null, @SerialName("minSendable") val minSendable: Long? = null,
@@ -121,7 +121,7 @@ data class LnurlScanResponse(
// ── LNURL-pay callback response (LUD-06) ───────────────────────────────────── // ── LNURL-pay callback response (LUD-06) ─────────────────────────────────────
@Serializable @Serializable
data class LnurlCallbackResponse( data class LnurlCallbackResponse(
val pr: String, val pr: String = "",
val routes: List<JsonElement>? = null, val routes: List<JsonElement>? = null,
@SerialName("successAction") val successAction: SuccessAction? = null, @SerialName("successAction") val successAction: SuccessAction? = null,
val status: String? = null, val status: String? = null,
@@ -131,12 +131,12 @@ data class LnurlCallbackResponse(
// ── LNURL pay ───────────────────────────────────────────────────────────────── // ── LNURL pay ─────────────────────────────────────────────────────────────────
@Serializable @Serializable
data class LnurlPayRes( data class LnurlPayRes(
val tag: String?, val tag: String? = null,
val callback: String?, val callback: String? = null,
@SerialName("minSendable") val minSendable: Long?, @SerialName("minSendable") val minSendable: Long? = null,
@SerialName("maxSendable") val maxSendable: Long?, @SerialName("maxSendable") val maxSendable: Long? = null,
val metadata: String?, val metadata: String? = null,
@SerialName("commentAllowed") val commentAllowed: Int?, @SerialName("commentAllowed") val commentAllowed: Int? = null,
@SerialName("allowsNostr") val allowsNostr: Boolean? = null, @SerialName("allowsNostr") val allowsNostr: Boolean? = null,
@SerialName("nostrPubkey") val nostrPubkey: String? = null @SerialName("nostrPubkey") val nostrPubkey: String? = null
) )
@@ -151,25 +151,25 @@ data class PayLnurlRequest(
@Serializable @Serializable
data class PayLnurlResponse( data class PayLnurlResponse(
@SerialName("payment_hash") val paymentHash: String, @SerialName("payment_hash") val paymentHash: String = "",
@SerialName("checking_id") val checkingId: String @SerialName("checking_id") val checkingId: String = ""
) )
@Serializable @Serializable
data class WithdrawCallbackResponse( data class WithdrawCallbackResponse(
val status: String, val status: String = "",
val reason: String? = null val reason: String? = null
) )
// ── LNURLp pay link (lightning address) ────────────────────────────────────── // ── LNURLp pay link (lightning address) ──────────────────────────────────────
@Serializable @Serializable
data class LnurlpLink( data class LnurlpLink(
val id: String, val id: String = "",
val wallet: String, val wallet: String = "",
val username: String?, // the part before the @ in the lightning address val username: String? = null,
val description: String?, val description: String? = null,
@Serializable(with = LongAsDoubleSerializer::class) val min: Long, @Serializable(with = LongAsDoubleSerializer::class) val min: Long = 0L,
@Serializable(with = LongAsDoubleSerializer::class) val max: Long @Serializable(with = LongAsDoubleSerializer::class) val max: Long = 0L
) )
// ── Payment history ─────────────────────────────────────────────────────────── // ── Payment history ───────────────────────────────────────────────────────────
@@ -303,6 +303,7 @@ object SuccessActionSerializer : KSerializer<SuccessAction?> {
} }
} }
@Serializable
data class PaymentDetailResponse( data class PaymentDetailResponse(
val paid: Boolean, val paid: Boolean,
val details: PaymentRecord? val details: PaymentRecord?
@@ -311,30 +312,30 @@ data class PaymentDetailResponse(
// ── WebSocket payment message ───────────────────────────────────────────────── // ── WebSocket payment message ─────────────────────────────────────────────────
@Serializable @Serializable
data class WsPaymentMessage( data class WsPaymentMessage(
@SerialName("wallet_balance") val walletBalance: Long?, @SerialName("wallet_balance") val walletBalance: Long? = null,
val payment: WsPayment? val payment: WsPayment? = null
) )
@Serializable @Serializable
data class WsPayment( data class WsPayment(
@SerialName("payment_hash") val paymentHash: String, @SerialName("payment_hash") val paymentHash: String = "",
@SerialName("checking_id") val checkingId: String, @SerialName("checking_id") val checkingId: String = "",
@SerialName("amount") val amount: Long, @SerialName("amount") val amount: Long = 0L,
@SerialName("fee") val fee: Long, @SerialName("fee") val fee: Long = 0L,
@SerialName("memo") val memo: String?, @SerialName("memo") val memo: String? = null,
@SerialName("time") val time: String, @SerialName("time") val time: String = "",
@SerialName("status") val status: String, @SerialName("status") val status: String = "",
@SerialName("bolt11") val bolt11: String?, @SerialName("bolt11") val bolt11: String? = null,
@SerialName("pending") val pending: Boolean, @SerialName("pending") val pending: Boolean = false,
@SerialName("preimage") val preimage: String?, @SerialName("preimage") val preimage: String? = null,
@SerialName("extra") val extra: PaymentExtra? @SerialName("extra") val extra: PaymentExtra? = null
) )
// ── Fiat rate ───────────────────────────────────────────────────────────────── // ── Fiat rate ─────────────────────────────────────────────────────────────────
@Serializable @Serializable
data class FiatRateResponse( data class FiatRateResponse(
val rate: Double, // sat to fiat rate val rate: Double = 0.0, // sat to fiat rate
val price: Double // BTC price in the requested fiat currency val price: Double = 0.0 // BTC price in the requested fiat currency
) )
// ── History sync ────────────────────────────────────────────────────────────── // ── History sync ──────────────────────────────────────────────────────────────
@@ -156,6 +156,17 @@ class SendViewModel(
_sendState.value = SendState.Error(strings.paymentFailed) _sendState.value = SendState.Error(strings.paymentFailed)
} }
} }
// hardening against a null detail
detail == null -> {
// Detail fetch failed entirely — treat as pending and poll once
Timber.w("SEND [DETAIL NULL] polling as fallback")
when (val result = poller.poll(paymentHash)) {
is PaymentPoller.PollResult.Settled ->
emitPaymentSent(paymentHash, amountSats, result.feeSats, bolt11, memo, pubkey, alias)
else ->
_sendState.value = SendState.Error(strings.paymentFailed)
}
}
// ── Failed or unknown // ── Failed or unknown
else -> { else -> {