mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
Merge pull request #3188 from davotoula/feat/onchain-failure-translations
Localize onchain zap send UI and failure messages
This commit is contained in:
@@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.model.nip51Lists.peopleList.PeopleList
|
||||
import com.vitorpamplona.amethyst.commons.model.nip56Reports.ReportAction
|
||||
import com.vitorpamplona.amethyst.commons.model.nip72Communities.CommunityListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.TrustProviderListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendError
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendResult
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendStage
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSender
|
||||
@@ -287,6 +288,8 @@ import kotlin.coroutines.cancellation.CancellationException
|
||||
import com.vitorpamplona.quartz.experimental.nip95.header.thumbhash as nip95thumbhash
|
||||
import com.vitorpamplona.quartz.experimental.profileGallery.thumbhash as galleryThumbhash
|
||||
|
||||
private const val ONCHAIN_BACKEND_NOT_CONFIGURED = "Bitcoin chain backend is not configured"
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@Stable
|
||||
class Account(
|
||||
@@ -889,6 +892,13 @@ class Account(
|
||||
return zapRequest
|
||||
}
|
||||
|
||||
private fun onchainBackendNotConfigured() =
|
||||
OnchainZapSendResult.Failure(
|
||||
OnchainZapSendStage.LOADING_UTXOS,
|
||||
OnchainZapSendError.BACKEND_NOT_CONFIGURED,
|
||||
ONCHAIN_BACKEND_NOT_CONFIGURED,
|
||||
)
|
||||
|
||||
/**
|
||||
* Send a NIP-BC onchain zap: build a Bitcoin transaction paying the recipient's
|
||||
* derived Taproot address, sign it, broadcast it, and publish the kind:8333
|
||||
@@ -904,10 +914,7 @@ class Account(
|
||||
): OnchainZapSendResult {
|
||||
val backend =
|
||||
cache.onchainBackend
|
||||
?: return OnchainZapSendResult.Failure(
|
||||
OnchainZapSendStage.LOADING_UTXOS,
|
||||
"Bitcoin chain backend is not configured",
|
||||
)
|
||||
?: return onchainBackendNotConfigured()
|
||||
return OnchainZapSender.send(
|
||||
backend = backend,
|
||||
signer = signer,
|
||||
@@ -932,10 +939,7 @@ class Account(
|
||||
): OnchainZapSendResult {
|
||||
val backend =
|
||||
cache.onchainBackend
|
||||
?: return OnchainZapSendResult.Failure(
|
||||
OnchainZapSendStage.LOADING_UTXOS,
|
||||
"Bitcoin chain backend is not configured",
|
||||
)
|
||||
?: return onchainBackendNotConfigured()
|
||||
return OnchainZapSender.sendToAddress(
|
||||
backend = backend,
|
||||
signer = signer,
|
||||
@@ -959,10 +963,7 @@ class Account(
|
||||
): OnchainZapSendResult {
|
||||
val backend =
|
||||
cache.onchainBackend
|
||||
?: return OnchainZapSendResult.Failure(
|
||||
OnchainZapSendStage.LOADING_UTXOS,
|
||||
"Bitcoin chain backend is not configured",
|
||||
)
|
||||
?: return onchainBackendNotConfigured()
|
||||
return OnchainZapSender.sendSplit(
|
||||
backend = backend,
|
||||
signer = signer,
|
||||
|
||||
+5
-3
@@ -733,6 +733,8 @@ private fun SectionLabel(text: String) {
|
||||
// Previews — fake data only; no account or network required.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private const val PREVIEW_MESSAGE_LABEL = "Add a public message"
|
||||
|
||||
private val previewMethods =
|
||||
persistentListOf(
|
||||
PaymentMethodUi(ProfilePaymentMethod.LIGHTNING, copyValue = "alice@walletofsatoshi.com"),
|
||||
@@ -799,7 +801,7 @@ private fun SendPaymentLightningPreview() {
|
||||
message = "Thank you for all your work!",
|
||||
onMessageChange = {},
|
||||
showMessageField = true,
|
||||
messageLabel = "Add a public message",
|
||||
messageLabel = PREVIEW_MESSAGE_LABEL,
|
||||
zapTypes = previewZapTypes,
|
||||
selectedZapType = LnZapEvent.ZapType.PUBLIC,
|
||||
onZapTypeChange = {},
|
||||
@@ -871,7 +873,7 @@ private fun SendPaymentInProgressPreview() {
|
||||
message = "",
|
||||
onMessageChange = {},
|
||||
showMessageField = true,
|
||||
messageLabel = "Add a public message",
|
||||
messageLabel = PREVIEW_MESSAGE_LABEL,
|
||||
zapTypes = null,
|
||||
selectedZapType = LnZapEvent.ZapType.PUBLIC,
|
||||
onZapTypeChange = {},
|
||||
@@ -907,7 +909,7 @@ private fun SendPaymentSuccessPreview() {
|
||||
message = "",
|
||||
onMessageChange = {},
|
||||
showMessageField = true,
|
||||
messageLabel = "Add a public message",
|
||||
messageLabel = PREVIEW_MESSAGE_LABEL,
|
||||
zapTypes = previewZapTypes,
|
||||
selectedZapType = LnZapEvent.ZapType.PUBLIC,
|
||||
onZapTypeChange = {},
|
||||
|
||||
+13
-3
@@ -66,6 +66,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.FeeTier
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.rateFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.technicalDetail
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.userMessage
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.experimental.clink.common.SatRange
|
||||
import com.vitorpamplona.quartz.experimental.clink.offers.OfferErrorCode
|
||||
@@ -525,7 +527,10 @@ private fun SendPaymentLoaded(
|
||||
successTitle,
|
||||
stringRes(context, R.string.send_payment_onchain_txid, result.txid.shortenMiddle()),
|
||||
)
|
||||
is OnchainZapSendResult.Failure -> PaymentFlowStage.Failure(result.message)
|
||||
is OnchainZapSendResult.Failure ->
|
||||
PaymentFlowStage.Failure(
|
||||
listOfNotNull(result.userMessage(context), result.technicalDetail()).joinToString("\n"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -744,9 +749,14 @@ private fun OnchainFeeSection(
|
||||
label = {
|
||||
Text(
|
||||
if (rate != null) {
|
||||
"${tier.label} · ${"%.1f".format(rate)} sat/vB · ${tier.etaLabel}"
|
||||
stringRes(
|
||||
R.string.onchain_send_fee_tier_label_rate_eta,
|
||||
stringRes(tier.labelRes),
|
||||
"%.1f".format(rate),
|
||||
stringRes(tier.etaLabelRes),
|
||||
)
|
||||
} else {
|
||||
tier.label
|
||||
stringRes(tier.labelRes)
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendError
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendResult
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendStage
|
||||
import com.vitorpamplona.amethyst.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
/**
|
||||
* Maps the machine-readable [OnchainZapSendResult.Failure] coming out of the
|
||||
* KMP sender (which only carries English text) to localized UI strings.
|
||||
*/
|
||||
fun OnchainZapSendResult.Failure.userMessage(context: Context): String {
|
||||
val total = totalReceipts
|
||||
return if (error == OnchainZapSendError.RECEIPT_PUBLISH_FAILED && total != null) {
|
||||
pluralStringRes(
|
||||
context,
|
||||
R.plurals.onchain_send_error_publish_receipt_partial,
|
||||
total,
|
||||
publishedReceiptEventIds.size,
|
||||
total,
|
||||
)
|
||||
} else {
|
||||
stringRes(context, error.messageRes())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Untranslated diagnostic detail worth showing under the localized headline.
|
||||
* Which errors carry a human-readable cause is a fact about the sender,
|
||||
* declared on [OnchainZapSendError.causeIsUserFacing].
|
||||
*/
|
||||
fun OnchainZapSendResult.Failure.technicalDetail(): String? = if (error.causeIsUserFacing) cause?.message else null
|
||||
|
||||
@StringRes
|
||||
private fun OnchainZapSendError.messageRes(): Int =
|
||||
when (this) {
|
||||
OnchainZapSendError.BACKEND_NOT_CONFIGURED -> R.string.onchain_send_error_backend_not_configured
|
||||
OnchainZapSendError.LOAD_UTXOS_FAILED -> R.string.onchain_send_error_load_utxos
|
||||
OnchainZapSendError.BUILD_FAILED -> R.string.onchain_send_error_build
|
||||
OnchainZapSendError.RECIPIENT_BELOW_DUST -> R.string.onchain_send_error_recipient_dust
|
||||
OnchainZapSendError.SIGN_FAILED -> R.string.onchain_send_error_sign
|
||||
OnchainZapSendError.BROADCAST_FAILED -> R.string.onchain_send_error_broadcast
|
||||
OnchainZapSendError.RECEIPT_PUBLISH_FAILED -> R.string.onchain_send_error_publish_receipt
|
||||
}
|
||||
|
||||
@StringRes
|
||||
fun OnchainZapSendStage.labelRes(): Int =
|
||||
when (this) {
|
||||
OnchainZapSendStage.LOADING_UTXOS -> R.string.onchain_stage_loading_utxos
|
||||
OnchainZapSendStage.BUILDING -> R.string.onchain_stage_building
|
||||
OnchainZapSendStage.SIGNING -> R.string.onchain_stage_signing
|
||||
OnchainZapSendStage.BROADCASTING -> R.string.onchain_stage_broadcasting
|
||||
OnchainZapSendStage.PUBLISHING -> R.string.onchain_stage_publishing
|
||||
}
|
||||
+65
-42
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -61,14 +62,18 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.onchain.DustRecipientException
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendError
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendResult
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendStage
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapShare
|
||||
@@ -84,6 +89,7 @@ import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSugge
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.bitcoinColor
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -103,12 +109,12 @@ import java.text.NumberFormat
|
||||
|
||||
/** Shared with the profile Send Payment screen's on-chain rail. */
|
||||
internal enum class FeeTier(
|
||||
val label: String,
|
||||
val etaLabel: String,
|
||||
@param:StringRes val labelRes: Int,
|
||||
@param:StringRes val etaLabelRes: Int,
|
||||
) {
|
||||
SLOW("Slow", "~1 hr"),
|
||||
NORMAL("Normal", "~30 min"),
|
||||
FAST("Fast", "~10 min"),
|
||||
SLOW(R.string.onchain_send_fee_slow, R.string.onchain_send_fee_eta_slow),
|
||||
NORMAL(R.string.onchain_send_fee_normal, R.string.onchain_send_fee_eta_normal),
|
||||
FAST(R.string.onchain_send_fee_fast, R.string.onchain_send_fee_eta_fast),
|
||||
}
|
||||
|
||||
internal fun FeeEstimates.rateFor(tier: FeeTier): Double =
|
||||
@@ -290,7 +296,7 @@ fun OnchainZapSendDialog(
|
||||
) {
|
||||
SuccessBody(r)
|
||||
}
|
||||
DoneButton(label = "Done", onClick = onDismiss)
|
||||
DoneButton(label = stringRes(R.string.onchain_send_done), onClick = onDismiss)
|
||||
}
|
||||
|
||||
is OnchainZapSendResult.Failure -> {
|
||||
@@ -303,7 +309,7 @@ fun OnchainZapSendDialog(
|
||||
) {
|
||||
FailureBody(r)
|
||||
}
|
||||
DoneButton(label = "Close", onClick = onDismiss)
|
||||
DoneButton(label = stringRes(R.string.onchain_send_close), onClick = onDismiss)
|
||||
}
|
||||
|
||||
null -> {
|
||||
@@ -356,7 +362,7 @@ fun OnchainZapSendDialog(
|
||||
TextButton(
|
||||
onClick = { useSplits = true },
|
||||
) {
|
||||
Text("Use this note's ${onchainSplits.size}-way zap split")
|
||||
Text(pluralStringResource(R.plurals.onchain_send_use_note_split, onchainSplits.size, onchainSplits.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -375,7 +381,7 @@ fun OnchainZapSendDialog(
|
||||
OutlinedTextField(
|
||||
value = comment,
|
||||
onValueChange = { comment = it },
|
||||
label = { Text("Comment (optional)") },
|
||||
label = { Text(stringRes(R.string.onchain_send_comment_label)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
@@ -413,7 +419,9 @@ fun OnchainZapSendDialog(
|
||||
result =
|
||||
OnchainZapSendResult.Failure(
|
||||
stage = OnchainZapSendStage.BUILDING,
|
||||
error = OnchainZapSendError.RECIPIENT_BELOW_DUST,
|
||||
message = e.message ?: "A recipient share is below dust",
|
||||
cause = e,
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
@@ -471,7 +479,7 @@ private fun Header(onClose: () -> Unit) {
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "Send onchain zap",
|
||||
text = stringRes(R.string.onchain_send_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier =
|
||||
@@ -482,7 +490,7 @@ private fun Header(onClose: () -> Unit) {
|
||||
IconButton(onClick = onClose) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Close,
|
||||
contentDescription = "Close",
|
||||
contentDescription = stringRes(R.string.onchain_send_close),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
@@ -500,7 +508,7 @@ private fun RecipientSection(
|
||||
searchInput: String,
|
||||
onSearchChange: (String) -> Unit,
|
||||
) {
|
||||
SectionLabel("To")
|
||||
SectionLabel(stringRes(R.string.onchain_send_to))
|
||||
|
||||
if (recipientPubKey != null) {
|
||||
Surface(
|
||||
@@ -519,7 +527,7 @@ private fun RecipientSection(
|
||||
nav = EmptyNav(),
|
||||
)
|
||||
Text(
|
||||
text = "Post author",
|
||||
text = stringRes(R.string.onchain_send_post_author),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(start = 12.dp),
|
||||
@@ -539,8 +547,8 @@ private fun RecipientSection(
|
||||
OutlinedTextField(
|
||||
value = searchInput,
|
||||
onValueChange = onSearchChange,
|
||||
label = { Text("Recipient") },
|
||||
placeholder = { Text("name, NIP-05 or npub") },
|
||||
label = { Text(stringRes(R.string.onchain_send_recipient)) },
|
||||
placeholder = { Text(stringRes(R.string.onchain_send_recipient_placeholder)) },
|
||||
singleLine = true,
|
||||
isError =
|
||||
searchInput.isNotBlank() &&
|
||||
@@ -611,7 +619,7 @@ private fun SelectedRecipientChip(
|
||||
IconButton(onClick = onClear) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Close,
|
||||
contentDescription = "Change recipient",
|
||||
contentDescription = stringRes(R.string.onchain_send_change_recipient),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
@@ -627,7 +635,7 @@ private fun AmountSection(
|
||||
presetAmounts: List<Long>,
|
||||
belowMinimum: Boolean,
|
||||
) {
|
||||
SectionLabel("Amount")
|
||||
SectionLabel(stringRes(R.string.onchain_send_amount))
|
||||
|
||||
if (presetAmounts.isNotEmpty()) {
|
||||
FlowRow(
|
||||
@@ -651,7 +659,7 @@ private fun AmountSection(
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
placeholder = { Text("0") },
|
||||
suffix = { Text("sats", color = MaterialTheme.colorScheme.onSurfaceVariant) },
|
||||
suffix = { Text(stringRes(R.string.onchain_send_sats_suffix), color = MaterialTheme.colorScheme.onSurfaceVariant) },
|
||||
isError = belowMinimum,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
@@ -659,9 +667,7 @@ private fun AmountSection(
|
||||
if (belowMinimum) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text =
|
||||
"Minimum on-chain zap is ${NumberFormat.getNumberInstance().format(MIN_ONCHAIN_ZAP_SATS)} sats — " +
|
||||
"smaller amounts are eaten by miner fees. Use a Lightning zap instead.",
|
||||
text = stringRes(R.string.onchain_send_min_warning, NumberFormat.getNumberInstance().format(MIN_ONCHAIN_ZAP_SATS)),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
@@ -675,7 +681,7 @@ private fun FeeSection(
|
||||
onFeeTierChange: (FeeTier) -> Unit,
|
||||
fees: FeeEstimates?,
|
||||
) {
|
||||
SectionLabel("Priority")
|
||||
SectionLabel(stringRes(R.string.onchain_send_priority))
|
||||
|
||||
FlowRow(
|
||||
modifier =
|
||||
@@ -700,12 +706,17 @@ private fun FeeSection(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = tier.label,
|
||||
text = stringRes(tier.labelRes),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Text(
|
||||
text = if (rate != null) "${formatRate(rate)} sat/vB · ${tier.etaLabel}" else tier.etaLabel,
|
||||
text =
|
||||
if (rate != null) {
|
||||
stringRes(R.string.onchain_send_fee_rate_eta, formatRate(rate), stringRes(tier.etaLabelRes))
|
||||
} else {
|
||||
stringRes(tier.etaLabelRes)
|
||||
},
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
@@ -717,7 +728,7 @@ private fun FeeSection(
|
||||
if (fees == null) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Loading fee estimates…",
|
||||
text = stringRes(R.string.onchain_send_loading_fees),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -760,9 +771,10 @@ private fun SendButton(
|
||||
Text(
|
||||
text =
|
||||
when {
|
||||
sats != null && splitWays > 1 -> "Send $sats sats, $splitWays ways"
|
||||
sats != null -> "Send $sats sats"
|
||||
else -> "Send"
|
||||
sats != null && splitWays > 1 ->
|
||||
pluralStringResource(R.plurals.onchain_send_button_amount_split, splitWays, sats, splitWays)
|
||||
sats != null -> stringRes(R.string.onchain_send_button_amount, sats)
|
||||
else -> stringRes(R.string.send)
|
||||
},
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
@@ -777,7 +789,7 @@ private fun SplitsRecipientSection(
|
||||
onDisable: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
SectionLabel("Splits among ${splits.size} recipients")
|
||||
SectionLabel(pluralStringResource(R.plurals.onchain_send_splits_label, splits.size, splits.size))
|
||||
|
||||
val totalWeight = splits.sumOf { it.second }
|
||||
// Index the preview by pubkey once — the splits list scan would otherwise
|
||||
@@ -833,11 +845,9 @@ private fun SplitsRecipientSection(
|
||||
|
||||
if (skippedLnSplits.isNotEmpty()) {
|
||||
Spacer(Modifier.height(6.dp))
|
||||
val word = if (skippedLnSplits.size == 1) "recipient" else "recipients"
|
||||
val skipped = skippedLnSplits.size
|
||||
Text(
|
||||
text =
|
||||
"Skipping ${skippedLnSplits.size} Lightning-address-only $word — " +
|
||||
"on-chain needs a Nostr pubkey to derive the address.",
|
||||
text = pluralStringResource(R.plurals.onchain_send_splits_skipped, skipped, skipped),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -845,7 +855,7 @@ private fun SplitsRecipientSection(
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
TextButton(onClick = onDisable) {
|
||||
Text("Don't split — pay one recipient instead")
|
||||
Text(stringRes(R.string.onchain_send_dont_split))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -895,7 +905,7 @@ private fun SendingState() {
|
||||
color = MaterialTheme.colorScheme.bitcoinColor,
|
||||
)
|
||||
Text(
|
||||
text = "Building, signing and broadcasting…",
|
||||
text = stringRes(R.string.onchain_send_sending),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
@@ -914,15 +924,21 @@ private fun SuccessBody(result: OnchainZapSendResult.Success) {
|
||||
)
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(
|
||||
text = "Onchain zap sent",
|
||||
text = stringRes(R.string.onchain_send_success),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
ResultRow("Transaction", result.txid)
|
||||
ResultRow("Fee", "${NumberFormat.getNumberInstance().format(result.feeSats)} sats")
|
||||
ResultRow(stringRes(R.string.onchain_send_result_transaction), result.txid)
|
||||
ResultRow(
|
||||
stringRes(R.string.onchain_send_result_fee),
|
||||
stringRes(R.string.onchain_send_sats_amount, NumberFormat.getNumberInstance().format(result.feeSats)),
|
||||
)
|
||||
if (result.changeSats > 0) {
|
||||
ResultRow("Change", "${NumberFormat.getNumberInstance().format(result.changeSats)} sats")
|
||||
ResultRow(
|
||||
stringRes(R.string.onchain_send_result_change),
|
||||
stringRes(R.string.onchain_send_sats_amount, NumberFormat.getNumberInstance().format(result.changeSats)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -931,19 +947,26 @@ private fun SuccessBody(result: OnchainZapSendResult.Success) {
|
||||
private fun FailureBody(result: OnchainZapSendResult.Failure) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text = result.message,
|
||||
text = result.userMessage(LocalContext.current),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
result.technicalDetail()?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
result.broadcastTxid?.let {
|
||||
Text(
|
||||
text = "Payment was broadcast (tx $it) but the receipt was not published.",
|
||||
text = stringRes(R.string.onchain_send_broadcast_no_receipt, it),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "Failed at: ${result.stage.name.lowercase().replace('_', ' ')}",
|
||||
text = stringRes(R.string.onchain_send_failed_at, stringRes(result.stage.labelRes())),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
@@ -2219,6 +2219,73 @@
|
||||
<string name="wallet_onchain_public_dialog_body">Your Taproot address is derived from your Nostr public key, so anyone who knows your npub can see this wallet\'s balance and transaction history on the blockchain.\n\nTo preserve your privacy, fund and drain this wallet from and to non-private accounts, like exchanges. Never mix these funds with your cold wallets, and treat them as money you can lose, since anyone in control of your nsec can spend it.</string>
|
||||
<string name="wallet_onchain_public_dialog_confirm">Got it</string>
|
||||
<string name="wallet_onchain_copy_dialog_confirm">Copy address</string>
|
||||
|
||||
<!-- Onchain zap send dialog -->
|
||||
<string name="onchain_send_title">Send onchain zap</string>
|
||||
<string name="onchain_send_close">Close</string>
|
||||
<string name="onchain_send_to">To</string>
|
||||
<string name="onchain_send_post_author">Post author</string>
|
||||
<string name="onchain_send_recipient">Recipient</string>
|
||||
<string name="onchain_send_recipient_placeholder">name, NIP-05 or npub</string>
|
||||
<string name="onchain_send_change_recipient">Change recipient</string>
|
||||
<string name="onchain_send_amount">Amount</string>
|
||||
<string name="onchain_send_sats_suffix">sats</string>
|
||||
<string name="onchain_send_min_warning">Minimum on-chain zap is %1$s sats — smaller amounts are eaten by miner fees. Use a Lightning zap instead.</string>
|
||||
<string name="onchain_send_comment_label">Comment (optional)</string>
|
||||
<string name="onchain_send_priority">Priority</string>
|
||||
<string name="onchain_send_fee_slow">Slow</string>
|
||||
<string name="onchain_send_fee_normal">Normal</string>
|
||||
<string name="onchain_send_fee_fast">Fast</string>
|
||||
<string name="onchain_send_fee_eta_slow">~1 hr</string>
|
||||
<string name="onchain_send_fee_eta_normal">~30 min</string>
|
||||
<string name="onchain_send_fee_eta_fast">~10 min</string>
|
||||
<string name="onchain_send_fee_rate_eta">%1$s sat/vB · %2$s</string>
|
||||
<string name="onchain_send_fee_tier_label_rate_eta">%1$s · %2$s sat/vB · %3$s</string>
|
||||
<string name="onchain_send_loading_fees">Loading fee estimates…</string>
|
||||
<string name="onchain_send_button_amount">Send %1$s sats</string>
|
||||
<plurals name="onchain_send_button_amount_split">
|
||||
<item quantity="one">Send %1$s sats, %2$d way</item>
|
||||
<item quantity="other">Send %1$s sats, %2$d ways</item>
|
||||
</plurals>
|
||||
<plurals name="onchain_send_use_note_split">
|
||||
<item quantity="one">Use this note\'s %1$d-way zap split</item>
|
||||
<item quantity="other">Use this note\'s %1$d-way zap split</item>
|
||||
</plurals>
|
||||
<plurals name="onchain_send_splits_label">
|
||||
<item quantity="one">Splits among %1$d recipient</item>
|
||||
<item quantity="other">Splits among %1$d recipients</item>
|
||||
</plurals>
|
||||
<plurals name="onchain_send_splits_skipped">
|
||||
<item quantity="one">Skipping %1$d Lightning-address-only recipient — on-chain needs a Nostr pubkey to derive the address.</item>
|
||||
<item quantity="other">Skipping %1$d Lightning-address-only recipients — on-chain needs a Nostr pubkey to derive the address.</item>
|
||||
</plurals>
|
||||
<string name="onchain_send_dont_split">Don\'t split — pay one recipient instead</string>
|
||||
<string name="onchain_send_sending">Building, signing and broadcasting…</string>
|
||||
<string name="onchain_send_success">Onchain zap sent</string>
|
||||
<string name="onchain_send_result_transaction">Transaction</string>
|
||||
<string name="onchain_send_result_fee">Fee</string>
|
||||
<string name="onchain_send_result_change">Change</string>
|
||||
<string name="onchain_send_sats_amount">%1$s sats</string>
|
||||
<string name="onchain_send_done">Done</string>
|
||||
<string name="onchain_send_failed_at">Failed at: %1$s</string>
|
||||
<string name="onchain_send_broadcast_no_receipt">Payment was broadcast (tx %1$s) but the receipt was not published.</string>
|
||||
<string name="onchain_stage_loading_utxos">loading balance</string>
|
||||
<string name="onchain_stage_building">building the transaction</string>
|
||||
<string name="onchain_stage_signing">signing</string>
|
||||
<string name="onchain_stage_broadcasting">broadcasting</string>
|
||||
<string name="onchain_stage_publishing">publishing the receipt</string>
|
||||
<string name="onchain_send_error_backend_not_configured">Bitcoin chain backend is not configured</string>
|
||||
<string name="onchain_send_error_load_utxos">Could not load your Bitcoin balance</string>
|
||||
<string name="onchain_send_error_build">Could not build the transaction</string>
|
||||
<string name="onchain_send_error_recipient_dust">A recipient share is below dust</string>
|
||||
<string name="onchain_send_error_sign">Could not sign the transaction</string>
|
||||
<string name="onchain_send_error_broadcast">Could not broadcast the transaction</string>
|
||||
<string name="onchain_send_error_publish_receipt">Payment sent, but the zap receipt could not be published</string>
|
||||
<plurals name="onchain_send_error_publish_receipt_partial">
|
||||
<item quantity="one">Payment sent (%1$d of %2$d receipt published), but the next receipt could not be published</item>
|
||||
<item quantity="other">Payment sent (%1$d of %2$d receipts published), but the next receipt could not be published</item>
|
||||
</plurals>
|
||||
|
||||
<string name="route_security_filters">Security Filters</string>
|
||||
<string name="route_import_follows">Import Follows</string>
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Id
|
||||
*/
|
||||
object OfferCommands {
|
||||
private const val MAX_FOLLOW_HOPS = 3
|
||||
private const val ERR_NOT_A_NOFFER = "not a valid noffer pointer"
|
||||
|
||||
suspend fun dispatch(
|
||||
dataDir: DataDir,
|
||||
@@ -113,7 +114,7 @@ object OfferCommands {
|
||||
val args = Args(rest)
|
||||
val offer =
|
||||
ClinkPointerParser.parse(args.positional(0, "noffer").trim()) as? NOffer
|
||||
?: return Output.error("bad_args", "not a valid noffer pointer")
|
||||
?: return Output.error("bad_args", ERR_NOT_A_NOFFER)
|
||||
|
||||
Output.emit(
|
||||
mapOf(
|
||||
@@ -139,7 +140,7 @@ object OfferCommands {
|
||||
|
||||
var offer =
|
||||
ClinkPointerParser.parse(args.positional(0, "noffer").trim()) as? NOffer
|
||||
?: return Output.error("bad_args", "not a valid noffer pointer")
|
||||
?: return Output.error("bad_args", ERR_NOT_A_NOFFER)
|
||||
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
@@ -209,7 +210,7 @@ object OfferCommands {
|
||||
|
||||
val offer =
|
||||
ClinkPointerParser.parse(args.positional(0, "noffer").trim()) as? NOffer
|
||||
?: return Output.error("bad_args", "not a valid noffer pointer")
|
||||
?: return Output.error("bad_args", ERR_NOT_A_NOFFER)
|
||||
val withFlag =
|
||||
args.flag("with")
|
||||
?: return Output.error("bad_args", "offer pay needs --with <ndebit> to settle the fetched invoice")
|
||||
|
||||
+62
-13
@@ -55,6 +55,43 @@ enum class OnchainZapSendStage {
|
||||
PUBLISHING,
|
||||
}
|
||||
|
||||
/**
|
||||
* Machine-readable reason for an [OnchainZapSendResult.Failure], so front ends
|
||||
* can map the failure to a localized message. The English
|
||||
* [OnchainZapSendResult.Failure.message] stays available for logs, tests, and
|
||||
* the CLI.
|
||||
*/
|
||||
enum class OnchainZapSendError(
|
||||
/**
|
||||
* Whether [OnchainZapSendResult.Failure.cause] carries a message crafted
|
||||
* for humans (insufficient funds, signer tampering, which share is below
|
||||
* dust) as opposed to low-level exception text. UIs may surface the cause
|
||||
* message as a diagnostic detail when this is true.
|
||||
*/
|
||||
val causeIsUserFacing: Boolean,
|
||||
) {
|
||||
/** No [OnchainBackend] is configured for the account. */
|
||||
BACKEND_NOT_CONFIGURED(causeIsUserFacing = false),
|
||||
|
||||
/** The chain backend could not return the sender's UTXOs. */
|
||||
LOAD_UTXOS_FAILED(causeIsUserFacing = false),
|
||||
|
||||
/** Coin selection / PSBT assembly failed (e.g. insufficient funds). */
|
||||
BUILD_FAILED(causeIsUserFacing = true),
|
||||
|
||||
/** A recipient's split share is below the dust threshold. */
|
||||
RECIPIENT_BELOW_DUST(causeIsUserFacing = true),
|
||||
|
||||
/** Signing, signature verification, or finalization failed. */
|
||||
SIGN_FAILED(causeIsUserFacing = true),
|
||||
|
||||
/** The signed transaction could not be broadcast. */
|
||||
BROADCAST_FAILED(causeIsUserFacing = false),
|
||||
|
||||
/** The payment broadcast, but a kind:8333 receipt could not be published. */
|
||||
RECEIPT_PUBLISH_FAILED(causeIsUserFacing = false),
|
||||
}
|
||||
|
||||
/** Outcome of an [OnchainZapSender.send] attempt. */
|
||||
sealed interface OnchainZapSendResult {
|
||||
/**
|
||||
@@ -85,6 +122,9 @@ sealed interface OnchainZapSendResult {
|
||||
*/
|
||||
data class Failure(
|
||||
val stage: OnchainZapSendStage,
|
||||
/** Machine-readable reason — map this to a localized message in UIs. */
|
||||
val error: OnchainZapSendError,
|
||||
/** English description for logs, tests, and the CLI. */
|
||||
val message: String,
|
||||
val cause: Throwable? = null,
|
||||
/** Non-null when the payment was broadcast but a later stage failed. */
|
||||
@@ -95,6 +135,11 @@ sealed interface OnchainZapSendResult {
|
||||
* publishes that fail on the first receipt.
|
||||
*/
|
||||
val publishedReceiptEventIds: List<HexKey> = emptyList(),
|
||||
/**
|
||||
* How many receipts a split zap intended to publish. Null when the
|
||||
* failure did not happen while publishing split receipts.
|
||||
*/
|
||||
val totalReceipts: Int? = null,
|
||||
) : OnchainZapSendResult
|
||||
}
|
||||
|
||||
@@ -139,7 +184,7 @@ object OnchainZapSender {
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
return fail(OnchainZapSendStage.LOADING_UTXOS, "Could not load your Bitcoin balance", e)
|
||||
return fail(OnchainZapSendStage.LOADING_UTXOS, OnchainZapSendError.LOAD_UTXOS_FAILED, "Could not load your Bitcoin balance", e)
|
||||
}
|
||||
|
||||
// 2. Coin-select and assemble the unsigned PSBT.
|
||||
@@ -155,7 +200,7 @@ object OnchainZapSender {
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
return fail(OnchainZapSendStage.BUILDING, e.message ?: "Could not build the transaction", e)
|
||||
return fail(OnchainZapSendStage.BUILDING, OnchainZapSendError.BUILD_FAILED, e.message ?: "Could not build the transaction", e)
|
||||
}
|
||||
|
||||
// 3. Sign, verify the signer didn't tamper, and finalize.
|
||||
@@ -165,7 +210,7 @@ object OnchainZapSender {
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
return fail(OnchainZapSendStage.SIGNING, e.message ?: "Could not sign the transaction", e)
|
||||
return fail(OnchainZapSendStage.SIGNING, OnchainZapSendError.SIGN_FAILED, e.message ?: "Could not sign the transaction", e)
|
||||
}
|
||||
|
||||
// 4. Broadcast.
|
||||
@@ -175,7 +220,7 @@ object OnchainZapSender {
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
return fail(OnchainZapSendStage.BROADCASTING, "Could not broadcast the transaction", e)
|
||||
return fail(OnchainZapSendStage.BROADCASTING, OnchainZapSendError.BROADCAST_FAILED, "Could not broadcast the transaction", e)
|
||||
}
|
||||
|
||||
// 5. Publish the kind:8333 receipt. The payment is already on-chain at
|
||||
@@ -194,6 +239,7 @@ object OnchainZapSender {
|
||||
} catch (e: Throwable) {
|
||||
return OnchainZapSendResult.Failure(
|
||||
stage = OnchainZapSendStage.PUBLISHING,
|
||||
error = OnchainZapSendError.RECEIPT_PUBLISH_FAILED,
|
||||
message = "Payment sent, but the zap receipt could not be published",
|
||||
cause = e,
|
||||
broadcastTxid = txid,
|
||||
@@ -241,7 +287,7 @@ object OnchainZapSender {
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
return fail(OnchainZapSendStage.LOADING_UTXOS, "Could not load your Bitcoin balance", e)
|
||||
return fail(OnchainZapSendStage.LOADING_UTXOS, OnchainZapSendError.LOAD_UTXOS_FAILED, "Could not load your Bitcoin balance", e)
|
||||
}
|
||||
|
||||
// 2. Coin-select and assemble the multi-output PSBT.
|
||||
@@ -256,7 +302,7 @@ object OnchainZapSender {
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
return fail(OnchainZapSendStage.BUILDING, e.message ?: "Could not build the transaction", e)
|
||||
return fail(OnchainZapSendStage.BUILDING, OnchainZapSendError.BUILD_FAILED, e.message ?: "Could not build the transaction", e)
|
||||
}
|
||||
|
||||
// 3. Sign, verify, and finalize. Same fund-safety contract as [send].
|
||||
@@ -266,7 +312,7 @@ object OnchainZapSender {
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
return fail(OnchainZapSendStage.SIGNING, e.message ?: "Could not sign the transaction", e)
|
||||
return fail(OnchainZapSendStage.SIGNING, OnchainZapSendError.SIGN_FAILED, e.message ?: "Could not sign the transaction", e)
|
||||
}
|
||||
|
||||
// 4. Broadcast.
|
||||
@@ -276,7 +322,7 @@ object OnchainZapSender {
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
return fail(OnchainZapSendStage.BROADCASTING, "Could not broadcast the transaction", e)
|
||||
return fail(OnchainZapSendStage.BROADCASTING, OnchainZapSendError.BROADCAST_FAILED, "Could not broadcast the transaction", e)
|
||||
}
|
||||
|
||||
// 5. Publish one receipt per recipient. If any one fails, surface the
|
||||
@@ -297,12 +343,14 @@ object OnchainZapSender {
|
||||
} catch (e: Throwable) {
|
||||
return OnchainZapSendResult.Failure(
|
||||
stage = OnchainZapSendStage.PUBLISHING,
|
||||
error = OnchainZapSendError.RECEIPT_PUBLISH_FAILED,
|
||||
message =
|
||||
"Payment sent (${publishedIds.size} of ${recipients.size} receipts published), " +
|
||||
"but the next receipt could not be published",
|
||||
cause = e,
|
||||
broadcastTxid = txid,
|
||||
publishedReceiptEventIds = publishedIds.toList(),
|
||||
totalReceipts = recipients.size,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -341,7 +389,7 @@ object OnchainZapSender {
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
return fail(OnchainZapSendStage.LOADING_UTXOS, "Could not load your Bitcoin balance", e)
|
||||
return fail(OnchainZapSendStage.LOADING_UTXOS, OnchainZapSendError.LOAD_UTXOS_FAILED, "Could not load your Bitcoin balance", e)
|
||||
}
|
||||
|
||||
// 2. Decode the destination address and assemble the unsigned PSBT.
|
||||
@@ -356,7 +404,7 @@ object OnchainZapSender {
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
return fail(OnchainZapSendStage.BUILDING, e.message ?: "Could not build the transaction", e)
|
||||
return fail(OnchainZapSendStage.BUILDING, OnchainZapSendError.BUILD_FAILED, e.message ?: "Could not build the transaction", e)
|
||||
}
|
||||
|
||||
// 3. Sign, verify, and finalize. Same fund-safety contract as [send].
|
||||
@@ -366,7 +414,7 @@ object OnchainZapSender {
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
return fail(OnchainZapSendStage.SIGNING, e.message ?: "Could not sign the transaction", e)
|
||||
return fail(OnchainZapSendStage.SIGNING, OnchainZapSendError.SIGN_FAILED, e.message ?: "Could not sign the transaction", e)
|
||||
}
|
||||
|
||||
// 4. Broadcast. No receipt stage: an address send has nothing to publish.
|
||||
@@ -376,7 +424,7 @@ object OnchainZapSender {
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
return fail(OnchainZapSendStage.BROADCASTING, "Could not broadcast the transaction", e)
|
||||
return fail(OnchainZapSendStage.BROADCASTING, OnchainZapSendError.BROADCAST_FAILED, "Could not broadcast the transaction", e)
|
||||
}
|
||||
|
||||
return OnchainZapSendResult.Success(
|
||||
@@ -426,7 +474,8 @@ object OnchainZapSender {
|
||||
|
||||
private fun fail(
|
||||
stage: OnchainZapSendStage,
|
||||
error: OnchainZapSendError,
|
||||
message: String,
|
||||
cause: Throwable? = null,
|
||||
) = OnchainZapSendResult.Failure(stage, message, cause)
|
||||
) = OnchainZapSendResult.Failure(stage, error, message, cause)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user