mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
fix: wrap NWC response callbacks in a coroutine (408 -> 393 errors)
The NWC response/timeout callbacks are invoked by the relay dispatcher, so they cannot become suspend - the requirement would cascade into LocalCache.consume. Their bodies can open a coroutine though, which is what lets them resolve a Compose string. WalletViewModel went 22 -> 1 on this alone (8 callbacks, plus updateWalletInfo's transform lambda made suspend). Same shape applied to ZapPaymentHandler (3) and V4VPaymentHandler (4) using account.scope, which both already had. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXvKXakvup4inNFfAhhr4L
This commit is contained in:
@@ -48,6 +48,7 @@ import com.vitorpamplona.quartz.podcasts.PodcastValueShare
|
||||
import com.vitorpamplona.quartz.utils.mapNotNullAsync
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
@@ -170,12 +171,16 @@ class V4VPaymentHandler(
|
||||
account.zaps.sendNwcRequest(
|
||||
request = request,
|
||||
onResponse = { response: Response? ->
|
||||
response.nwcFailureDetail(context)?.let { detail ->
|
||||
onError(loadStringRes(Res.string.error_dialog_pay_invoice_error), detail)
|
||||
account.scope.launch {
|
||||
response.nwcFailureDetail(context)?.let { detail ->
|
||||
onError(loadStringRes(Res.string.error_dialog_pay_invoice_error), detail)
|
||||
}
|
||||
}
|
||||
},
|
||||
onTimeout = {
|
||||
onError(loadStringRes(Res.string.error_dialog_pay_invoice_error), nwcTimeoutMessage(context))
|
||||
account.scope.launch {
|
||||
onError(loadStringRes(Res.string.error_dialog_pay_invoice_error), nwcTimeoutMessage(context))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -264,12 +269,16 @@ class V4VPaymentHandler(
|
||||
bolt11 = payable.invoice,
|
||||
zappedNote = zappedNote,
|
||||
onResponse = { response ->
|
||||
response.nwcFailureDetail(context)?.let { detail ->
|
||||
onError(loadStringRes(Res.string.error_dialog_pay_invoice_error), detail)
|
||||
account.scope.launch {
|
||||
response.nwcFailureDetail(context)?.let { detail ->
|
||||
onError(loadStringRes(Res.string.error_dialog_pay_invoice_error), detail)
|
||||
}
|
||||
}
|
||||
},
|
||||
onTimeout = {
|
||||
onError(loadStringRes(Res.string.error_dialog_pay_invoice_error), nwcTimeoutMessage(context))
|
||||
account.scope.launch {
|
||||
onError(loadStringRes(Res.string.error_dialog_pay_invoice_error), nwcTimeoutMessage(context))
|
||||
}
|
||||
},
|
||||
)
|
||||
done++
|
||||
|
||||
@@ -486,21 +486,25 @@ class ZapPaymentHandler(
|
||||
comment = payable.message,
|
||||
),
|
||||
onResponse = { response ->
|
||||
progress.step()
|
||||
response.nwcFailureDetail(context)?.let { detail ->
|
||||
onError(
|
||||
loadStringRes(Res.string.error_dialog_pay_invoice_error),
|
||||
loadStringRes(Res.string.wallet_connect_pay_invoice_error_error, detail),
|
||||
payable.info.user,
|
||||
)
|
||||
account.scope.launch {
|
||||
progress.step()
|
||||
response.nwcFailureDetail(context)?.let { detail ->
|
||||
onError(
|
||||
loadStringRes(Res.string.error_dialog_pay_invoice_error),
|
||||
loadStringRes(Res.string.wallet_connect_pay_invoice_error_error, detail),
|
||||
payable.info.user,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onTimeout = {
|
||||
onError(
|
||||
loadStringRes(Res.string.error_dialog_pay_invoice_error),
|
||||
nwcTimeoutMessage(context),
|
||||
payable.info.user,
|
||||
)
|
||||
account.scope.launch {
|
||||
onError(
|
||||
loadStringRes(Res.string.error_dialog_pay_invoice_error),
|
||||
nwcTimeoutMessage(context),
|
||||
payable.info.user,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -596,9 +600,11 @@ class ZapPaymentHandler(
|
||||
}
|
||||
},
|
||||
onTimeout = {
|
||||
// No response callback will fire, so account for the settlement step here.
|
||||
reportBolt12Error(Res.string.bolt12_payment_failed, nwcTimeoutMessage(context))
|
||||
progress.step()
|
||||
account.scope.launch {
|
||||
// No response callback will fire, so account for the settlement step here.
|
||||
reportBolt12Error(Res.string.bolt12_payment_failed, nwcTimeoutMessage(context))
|
||||
progress.step()
|
||||
}
|
||||
},
|
||||
onProcessed = { progress.step() },
|
||||
)
|
||||
|
||||
+126
-110
@@ -435,21 +435,23 @@ class WalletViewModel : ViewModel() {
|
||||
updateWalletInfo(walletId) { it.copy(isLoading = true, error = null) }
|
||||
try {
|
||||
acc.zaps.sendNwcRequestToWallet(walletUri, GetBalanceMethod.create()) { response ->
|
||||
when (response) {
|
||||
is GetBalanceSuccessResponse -> {
|
||||
val sats = (response.result?.balance ?: 0L) / 1000L
|
||||
updateWalletInfo(walletId) { it.copy(balanceSats = sats, isLoading = false) }
|
||||
}
|
||||
|
||||
is NwcErrorResponse -> {
|
||||
updateWalletInfo(walletId) {
|
||||
it.copy(error = response.error?.message ?: text(Res.string.wallet_balance_request_failed), isLoading = false)
|
||||
viewModelScope.launch {
|
||||
when (response) {
|
||||
is GetBalanceSuccessResponse -> {
|
||||
val sats = (response.result?.balance ?: 0L) / 1000L
|
||||
updateWalletInfo(walletId) { it.copy(balanceSats = sats, isLoading = false) }
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
updateWalletInfo(walletId) {
|
||||
it.copy(error = unreadableResponseError(response), isLoading = false)
|
||||
is NwcErrorResponse -> {
|
||||
updateWalletInfo(walletId) {
|
||||
it.copy(error = response.error?.message ?: text(Res.string.wallet_balance_request_failed), isLoading = false)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
updateWalletInfo(walletId) {
|
||||
it.copy(error = unreadableResponseError(response), isLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -466,12 +468,14 @@ class WalletViewModel : ViewModel() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
acc.zaps.sendNwcRequestToWallet(walletUri, GetInfoMethod.create()) { response ->
|
||||
when (response) {
|
||||
is GetInfoSuccessResponse -> {
|
||||
updateWalletInfo(walletId) { it.copy(alias = response.result?.alias) }
|
||||
}
|
||||
viewModelScope.launch {
|
||||
when (response) {
|
||||
is GetInfoSuccessResponse -> {
|
||||
updateWalletInfo(walletId) { it.copy(alias = response.result?.alias) }
|
||||
}
|
||||
|
||||
else -> {}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
@@ -481,7 +485,7 @@ class WalletViewModel : ViewModel() {
|
||||
|
||||
private suspend fun updateWalletInfo(
|
||||
walletId: String,
|
||||
transform: (WalletInfo) -> WalletInfo,
|
||||
transform: suspend (WalletInfo) -> WalletInfo,
|
||||
) {
|
||||
walletInfoMap.value =
|
||||
walletInfoMap.value.toMutableMap().apply {
|
||||
@@ -508,23 +512,25 @@ class WalletViewModel : ViewModel() {
|
||||
try {
|
||||
requestId =
|
||||
acc.zaps.sendNwcRequestToWallet(walletUri, GetBalanceMethod.create()) { response ->
|
||||
timeoutJob.cancel()
|
||||
when (response) {
|
||||
is GetBalanceSuccessResponse -> {
|
||||
_balanceSats.value = (response.result?.balance ?: 0L) / 1000L
|
||||
updateWalletInfo(walletId) { it.copy(balanceSats = _balanceSats.value) }
|
||||
_error.value = null
|
||||
}
|
||||
viewModelScope.launch {
|
||||
timeoutJob.cancel()
|
||||
when (response) {
|
||||
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 ?: text(Res.string.wallet_balance_request_failed)
|
||||
}
|
||||
is NwcErrorResponse -> {
|
||||
_error.value = response.error?.message ?: text(Res.string.wallet_balance_request_failed)
|
||||
}
|
||||
|
||||
else -> {
|
||||
_error.value = unreadableResponseError(response)
|
||||
else -> {
|
||||
_error.value = unreadableResponseError(response)
|
||||
}
|
||||
}
|
||||
_isLoading.value = false
|
||||
}
|
||||
_isLoading.value = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
timeoutJob.cancel()
|
||||
@@ -541,13 +547,15 @@ class WalletViewModel : ViewModel() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
acc.zaps.sendNwcRequestToWallet(walletUri, GetInfoMethod.create()) { response ->
|
||||
when (response) {
|
||||
is GetInfoSuccessResponse -> {
|
||||
_walletAlias.value = response.result?.alias
|
||||
updateWalletInfo(walletId) { it.copy(alias = response.result?.alias) }
|
||||
}
|
||||
viewModelScope.launch {
|
||||
when (response) {
|
||||
is GetInfoSuccessResponse -> {
|
||||
_walletAlias.value = response.result?.alias
|
||||
updateWalletInfo(walletId) { it.copy(alias = response.result?.alias) }
|
||||
}
|
||||
|
||||
else -> {}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
@@ -576,30 +584,32 @@ class WalletViewModel : ViewModel() {
|
||||
unpaid = false,
|
||||
),
|
||||
) { response ->
|
||||
timeoutJob.cancel()
|
||||
when (response) {
|
||||
is ListTransactionsSuccessResponse -> {
|
||||
val txs = response.result?.transactions ?: emptyList()
|
||||
allTransactions.value = txs
|
||||
val totalCount = response.result?.total_count
|
||||
_hasMoreTransactions.value =
|
||||
if (totalCount != null) {
|
||||
txs.size < totalCount
|
||||
} else {
|
||||
txs.size >= pageSize
|
||||
}
|
||||
_error.value = null
|
||||
}
|
||||
viewModelScope.launch {
|
||||
timeoutJob.cancel()
|
||||
when (response) {
|
||||
is ListTransactionsSuccessResponse -> {
|
||||
val txs = response.result?.transactions ?: emptyList()
|
||||
allTransactions.value = txs
|
||||
val totalCount = response.result?.total_count
|
||||
_hasMoreTransactions.value =
|
||||
if (totalCount != null) {
|
||||
txs.size < totalCount
|
||||
} else {
|
||||
txs.size >= pageSize
|
||||
}
|
||||
_error.value = null
|
||||
}
|
||||
|
||||
is NwcErrorResponse -> {
|
||||
_error.value = response.error?.message ?: text(Res.string.wallet_transactions_load_failed)
|
||||
}
|
||||
is NwcErrorResponse -> {
|
||||
_error.value = response.error?.message ?: text(Res.string.wallet_transactions_load_failed)
|
||||
}
|
||||
|
||||
else -> {
|
||||
_error.value = unreadableResponseError(response)
|
||||
else -> {
|
||||
_error.value = unreadableResponseError(response)
|
||||
}
|
||||
}
|
||||
_isLoading.value = false
|
||||
}
|
||||
_isLoading.value = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
timeoutJob.cancel()
|
||||
@@ -630,30 +640,32 @@ class WalletViewModel : ViewModel() {
|
||||
unpaid = false,
|
||||
),
|
||||
) { response ->
|
||||
timeoutJob.cancel()
|
||||
when (response) {
|
||||
is ListTransactionsSuccessResponse -> {
|
||||
val newTxs = response.result?.transactions ?: emptyList()
|
||||
allTransactions.value += newTxs
|
||||
val totalCount = response.result?.total_count
|
||||
_hasMoreTransactions.value =
|
||||
if (totalCount != null) {
|
||||
allTransactions.value.size < totalCount
|
||||
} else {
|
||||
newTxs.size >= pageSize
|
||||
}
|
||||
_error.value = null
|
||||
}
|
||||
viewModelScope.launch {
|
||||
timeoutJob.cancel()
|
||||
when (response) {
|
||||
is ListTransactionsSuccessResponse -> {
|
||||
val newTxs = response.result?.transactions ?: emptyList()
|
||||
allTransactions.value += newTxs
|
||||
val totalCount = response.result?.total_count
|
||||
_hasMoreTransactions.value =
|
||||
if (totalCount != null) {
|
||||
allTransactions.value.size < totalCount
|
||||
} else {
|
||||
newTxs.size >= pageSize
|
||||
}
|
||||
_error.value = null
|
||||
}
|
||||
|
||||
is NwcErrorResponse -> {
|
||||
_error.value = response.error?.message ?: text(Res.string.wallet_transactions_load_more_failed)
|
||||
}
|
||||
is NwcErrorResponse -> {
|
||||
_error.value = response.error?.message ?: text(Res.string.wallet_transactions_load_more_failed)
|
||||
}
|
||||
|
||||
else -> {
|
||||
_error.value = unreadableResponseError(response)
|
||||
else -> {
|
||||
_error.value = unreadableResponseError(response)
|
||||
}
|
||||
}
|
||||
_isLoadingMore.value = false
|
||||
}
|
||||
_isLoadingMore.value = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
timeoutJob.cancel()
|
||||
@@ -671,24 +683,26 @@ class WalletViewModel : ViewModel() {
|
||||
_sendState.value = SendState.Sending
|
||||
try {
|
||||
acc.zaps.sendNwcRequestToWallet(walletUri, PayInvoiceMethod.create(bolt11)) { response ->
|
||||
when (response) {
|
||||
is PayInvoiceSuccessResponse -> {
|
||||
_sendState.value = SendState.Success(response.result?.preimage)
|
||||
fetchBalance()
|
||||
}
|
||||
viewModelScope.launch {
|
||||
when (response) {
|
||||
is PayInvoiceSuccessResponse -> {
|
||||
_sendState.value = SendState.Success(response.result?.preimage)
|
||||
fetchBalance()
|
||||
}
|
||||
|
||||
is IErrorResponseLike -> {
|
||||
// Both PayInvoiceErrorResponse (method-specific, kept for
|
||||
// back-compat) and NwcErrorResponse (generic) reduce to the
|
||||
// same user-visible "payment failed" message.
|
||||
_sendState.value =
|
||||
SendState.Error(
|
||||
response.errorMessage() ?: text(Res.string.send_payment_failed),
|
||||
)
|
||||
}
|
||||
is IErrorResponseLike -> {
|
||||
// Both PayInvoiceErrorResponse (method-specific, kept for
|
||||
// back-compat) and NwcErrorResponse (generic) reduce to the
|
||||
// same user-visible "payment failed" message.
|
||||
_sendState.value =
|
||||
SendState.Error(
|
||||
response.errorMessage() ?: text(Res.string.send_payment_failed),
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
_sendState.value = SendState.Error(text(Res.string.wallet_connect_unreadable_response_error))
|
||||
else -> {
|
||||
_sendState.value = SendState.Error(text(Res.string.wallet_connect_unreadable_response_error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -715,25 +729,27 @@ class WalletViewModel : ViewModel() {
|
||||
description = description,
|
||||
),
|
||||
) { response ->
|
||||
when (response) {
|
||||
is MakeInvoiceSuccessResponse -> {
|
||||
val invoice = response.result?.invoice
|
||||
if (invoice != null) {
|
||||
_receiveState.value = ReceiveState.Created(invoice, amountSats)
|
||||
} else {
|
||||
_receiveState.value = ReceiveState.Error(text(Res.string.wallet_no_invoice_returned))
|
||||
viewModelScope.launch {
|
||||
when (response) {
|
||||
is MakeInvoiceSuccessResponse -> {
|
||||
val invoice = response.result?.invoice
|
||||
if (invoice != null) {
|
||||
_receiveState.value = ReceiveState.Created(invoice, amountSats)
|
||||
} else {
|
||||
_receiveState.value = ReceiveState.Error(text(Res.string.wallet_no_invoice_returned))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is NwcErrorResponse -> {
|
||||
_receiveState.value =
|
||||
ReceiveState.Error(
|
||||
response.error?.message ?: text(Res.string.wallet_invoice_creation_failed),
|
||||
)
|
||||
}
|
||||
is NwcErrorResponse -> {
|
||||
_receiveState.value =
|
||||
ReceiveState.Error(
|
||||
response.error?.message ?: text(Res.string.wallet_invoice_creation_failed),
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
_receiveState.value = ReceiveState.Error(text(Res.string.wallet_connect_unreadable_response_error))
|
||||
else -> {
|
||||
_receiveState.value = ReceiveState.Error(text(Res.string.wallet_connect_unreadable_response_error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user