mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 16:14:40 +00:00
fix(clink): harden payer round-trips + two review findings
From a correctness review of the CLINK branch: - ClinkOfferPayer/ClinkDebitPayer now catch decrypt/parse failures from parseResponse and treat an undecryptable reply as no response (return null) instead of throwing. An uncaught SerializationException/NIP-44 failure escaped launchSigner (which only catches signer exceptions), hanging the UI: the offer card stuck on 'Requesting…', the DVM status stuck, the budget toast never shown, and a split zap silently cancelling sibling payments. - ClinkOfferPreview now renders the active (possibly moved) offer's price/type, not the original pointer's, after an Expired-or-Moved redirect. - WalletViewModel.setDefaultWallet only updates local state when the persist actually succeeds, so the default star can't diverge from the stored value. :amethyst compiles.
This commit is contained in:
@@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
@@ -115,7 +116,15 @@ object ClinkDebitPayer {
|
||||
return try {
|
||||
account.client.publish(request, relays)
|
||||
val response = withTimeoutOrNull(timeoutMs) { reply.await() } ?: return null
|
||||
client.parseResponse(response)
|
||||
// Treat an undecryptable/malformed reply as no usable response rather than
|
||||
// throwing — callers only handle null, and an uncaught decode error would
|
||||
// leave the calling UI hung (spinner stuck, no toast, sibling zaps cancelled).
|
||||
try {
|
||||
client.parseResponse(response)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
} finally {
|
||||
account.client.unsubscribe(subId)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
@@ -82,7 +83,16 @@ object ClinkOfferPayer {
|
||||
return try {
|
||||
account.client.publish(request, relays)
|
||||
val response = withTimeoutOrNull(timeoutMs) { reply.await() } ?: return null
|
||||
client.parseResponse(response)
|
||||
// A reply that can't be decrypted/parsed (corrupt ciphertext, malformed JSON
|
||||
// from a buggy or hostile relay) is treated as no usable response rather than
|
||||
// thrown — callers only handle null, and an uncaught decode error would hang
|
||||
// the UI (the Pay button stuck on "Requesting…").
|
||||
try {
|
||||
client.parseResponse(response)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
null
|
||||
}
|
||||
} finally {
|
||||
account.client.unsubscribe(subId)
|
||||
}
|
||||
|
||||
+4
-2
@@ -145,10 +145,12 @@ fun ClinkOfferPreview(
|
||||
|
||||
// FIXED offers display their preset price; SPONTANEOUS offers (and the default
|
||||
// when the pointer omits a price type) require the payer to enter an amount.
|
||||
val effectiveType = offer.priceType ?: OfferPriceType.SPONTANEOUS
|
||||
// Reflect the pointer actually being charged (which may have changed if the
|
||||
// service redirected us to a replacement noffer via "Expired or Moved").
|
||||
val effectiveType = activeOffer.priceType ?: OfferPriceType.SPONTANEOUS
|
||||
|
||||
if (effectiveType == OfferPriceType.FIXED) {
|
||||
offer.price?.let {
|
||||
activeOffer.price?.let {
|
||||
Text(
|
||||
text = "$it ${stringRes(id = R.string.sats)}",
|
||||
fontSize = 25.sp,
|
||||
|
||||
+5
-2
@@ -287,8 +287,11 @@ class WalletViewModel : ViewModel() {
|
||||
|
||||
fun setDefaultWallet(walletId: String) {
|
||||
val acc = account ?: return
|
||||
acc.settings.setDefaultPaymentSource(walletId)
|
||||
_defaultWalletId.value = walletId
|
||||
// Only reflect the change locally if it actually persisted (the id must exist
|
||||
// in one of the lists); otherwise the star and the stored default would diverge.
|
||||
if (acc.settings.setDefaultPaymentSource(walletId)) {
|
||||
_defaultWalletId.value = walletId
|
||||
}
|
||||
}
|
||||
|
||||
fun removeWallet(walletId: String) {
|
||||
|
||||
Reference in New Issue
Block a user