mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 00:37:41 +00:00
Merge pull request #3032 from vitorpamplona/claude/jolly-cray-6vKga
Surface NIP-47 wallet response errors to user
This commit is contained in:
@@ -2099,7 +2099,15 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
wasVerified: Boolean,
|
||||
): Boolean {
|
||||
val requestId = event.requestId()
|
||||
val pending = paymentTracker.onResponseReceived(requestId) ?: return false
|
||||
val pending =
|
||||
paymentTracker.onResponseReceived(requestId) ?: run {
|
||||
Log.w(
|
||||
"LocalCache",
|
||||
"NWC response ${event.id} from ${event.pubKey} references request e=$requestId but no pending request is registered. " +
|
||||
"The response was either delivered after timeout, the user holds a stale subscription, or the wallet service set the wrong e tag.",
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
val zappedNote = pending.zappedNote
|
||||
val responseCallback = pending.onResponse
|
||||
|
||||
+8
-2
@@ -152,7 +152,11 @@ class NwcSignerState(
|
||||
|
||||
val assembler = nwcFilterAssembler()
|
||||
|
||||
assembler.subscribe(filter)
|
||||
// Synchronous flush so the REQ frame is queued on the WebSocket before
|
||||
// the EVENT is published. Without this, the bundler may delay REQ up
|
||||
// to 500ms, and the wallet service's ephemeral kind 23195 reply can
|
||||
// be missed.
|
||||
assembler.subscribeAndFlush(filter)
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
delay(60000)
|
||||
@@ -189,7 +193,9 @@ class NwcSignerState(
|
||||
|
||||
val assembler = nwcFilterAssembler()
|
||||
|
||||
assembler.subscribe(filter)
|
||||
// Synchronous flush so the REQ frame is queued before the EVENT.
|
||||
// See sendNwcRequestToWallet above for the rationale.
|
||||
assembler.subscribeAndFlush(filter)
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
delay(60000) // waits 1 minute to complete payment.
|
||||
|
||||
+12
@@ -48,5 +48,17 @@ class NWCPaymentFilterAssembler(
|
||||
|
||||
override fun invalidateKeys() = invalidateFilters()
|
||||
|
||||
/**
|
||||
* Synchronously sends the REQ frame to the relay, bypassing the 500ms
|
||||
* BundledUpdate debounce. Used for NIP-47 RPC where the response is an
|
||||
* ephemeral event (kind 23195) and the subscription must be active on the
|
||||
* relay before we publish the request event — otherwise the relay drops
|
||||
* the response with no replay.
|
||||
*/
|
||||
fun subscribeAndFlush(query: NWCPaymentQueryState) {
|
||||
subscribe(query)
|
||||
group.forEach { it.forceInvalidate() }
|
||||
}
|
||||
|
||||
override fun destroy() = group.forEach { it.destroy() }
|
||||
}
|
||||
|
||||
+28
-4
@@ -39,6 +39,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -172,6 +173,19 @@ class WalletViewModel : ViewModel() {
|
||||
private val _receiveState = MutableStateFlow<ReceiveState>(ReceiveState.Idle)
|
||||
val receiveState = _receiveState.asStateFlow()
|
||||
|
||||
// A null `Response` means the wallet service replied but the payload could
|
||||
// not be decrypted (wrong key, unexpected format). Any other unexpected
|
||||
// subtype means the response shape didn't match any known NIP-47 result.
|
||||
// Both used to be swallowed silently — now we surface them so the user
|
||||
// can distinguish "wallet never answered" from "wallet answered with
|
||||
// something we can't read".
|
||||
private fun unreadableResponseError(response: Response?): String =
|
||||
if (response == null) {
|
||||
"Could not decrypt the wallet's reply — the wallet may be using a different key"
|
||||
} else {
|
||||
"Wallet returned an unrecognized reply for ${response.resultType}"
|
||||
}
|
||||
|
||||
private fun launchTimeout(onTimeout: () -> Unit): Job =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
delay(NWC_TIMEOUT_MS)
|
||||
@@ -315,7 +329,9 @@ class WalletViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
else -> {
|
||||
updateWalletInfo(walletId) { it.copy(isLoading = false) }
|
||||
updateWalletInfo(walletId) {
|
||||
it.copy(error = unreadableResponseError(response), isLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -376,13 +392,16 @@ class WalletViewModel : ViewModel() {
|
||||
is GetBalanceSuccessResponse -> {
|
||||
_balanceSats.value = (response.result?.balance ?: 0L) / 1000L
|
||||
updateWalletInfo(walletId) { it.copy(balanceSats = _balanceSats.value) }
|
||||
_error.value = null
|
||||
}
|
||||
|
||||
is NwcErrorResponse -> {
|
||||
_error.value = response.error?.message ?: "Balance request failed"
|
||||
}
|
||||
|
||||
else -> {}
|
||||
else -> {
|
||||
_error.value = unreadableResponseError(response)
|
||||
}
|
||||
}
|
||||
_isLoading.value = false
|
||||
}
|
||||
@@ -444,13 +463,16 @@ class WalletViewModel : ViewModel() {
|
||||
} else {
|
||||
txs.size >= pageSize
|
||||
}
|
||||
_error.value = null
|
||||
}
|
||||
|
||||
is NwcErrorResponse -> {
|
||||
_error.value = response.error?.message ?: "Failed to load transactions"
|
||||
}
|
||||
|
||||
else -> {}
|
||||
else -> {
|
||||
_error.value = unreadableResponseError(response)
|
||||
}
|
||||
}
|
||||
_isLoading.value = false
|
||||
}
|
||||
@@ -498,7 +520,9 @@ class WalletViewModel : ViewModel() {
|
||||
_error.value = response.error?.message ?: "Failed to load more transactions"
|
||||
}
|
||||
|
||||
else -> {}
|
||||
else -> {
|
||||
_error.value = unreadableResponseError(response)
|
||||
}
|
||||
}
|
||||
_isLoadingMore.value = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user