diff --git a/src/i18n_messages/en.json b/src/i18n_messages/en.json
index b8151c3e..9007c372 100644
--- a/src/i18n_messages/en.json
+++ b/src/i18n_messages/en.json
@@ -311,6 +311,11 @@
"onchainTransferScreen_feeTierSubtext": "About %{blocks} blocks · up to %{amount} %{currency}",
"onchainTransferScreen_memoFromPayee": "Payment description",
"onchainTransferScreen_networkFee": "Network fee",
+ "onchainTransferScreen_noMintTitle": "No mint to pay from",
+ "onchainTransferScreen_noMintDesc": "You have no mint holding this currency. Add one and top it up before sending Bitcoin.",
+ "onchainTransferScreen_noOnchainMintTitle": "No mint can pay Bitcoin addresses",
+ "onchainTransferScreen_noOnchainMintDesc": "None of your mints holding this balance supports Bitcoin payouts. Add a mint that does, or pay with Lightning instead.",
+ "onchainTransferScreen_mintNoOnchainSupport": "This mint does not support Bitcoin payouts. Select a different mint.",
"onchainTransferScreen_noFeeOptions": "The mint returned no fee options for this payment and it can not be paid.",
"onchainTransferScreen_requestQuote": "Continue",
"onchainTransferScreen_selectFee": "Confirmation speed",
diff --git a/src/i18n_messages/es.json b/src/i18n_messages/es.json
index 4b9ac339..b207f45e 100644
--- a/src/i18n_messages/es.json
+++ b/src/i18n_messages/es.json
@@ -310,6 +310,11 @@
"onchainTransferScreen_feeTierSubtext": "Unos %{blocks} bloques · hasta %{amount} %{currency}",
"onchainTransferScreen_memoFromPayee": "Descripción del pago",
"onchainTransferScreen_networkFee": "Tarifa de red",
+ "onchainTransferScreen_noMintTitle": "No hay ninguna casa de cambio desde la que pagar",
+ "onchainTransferScreen_noMintDesc": "No tienes ninguna casa de cambio con esta moneda. Añade una y recárgala antes de enviar Bitcoin.",
+ "onchainTransferScreen_noOnchainMintTitle": "Ninguna casa de cambio puede pagar a direcciones Bitcoin",
+ "onchainTransferScreen_noOnchainMintDesc": "Ninguna de tus casas de cambio con este saldo admite pagos en Bitcoin. Añade una que lo admita o paga con Lightning.",
+ "onchainTransferScreen_mintNoOnchainSupport": "Esta casa de cambio no admite pagos en Bitcoin. Selecciona otra.",
"onchainTransferScreen_noFeeOptions": "La casa de cambio no devolvió opciones de tarifa para este pago y no se puede pagar.",
"onchainTransferScreen_requestQuote": "Continuar",
"onchainTransferScreen_selectFee": "Velocidad de confirmación",
diff --git a/src/i18n_messages/pt.json b/src/i18n_messages/pt.json
index a1c8e944..4e6d6846 100644
--- a/src/i18n_messages/pt.json
+++ b/src/i18n_messages/pt.json
@@ -311,6 +311,11 @@
"onchainTransferScreen_feeTierSubtext": "Cerca de %{blocks} blocos · até %{amount} %{currency}",
"onchainTransferScreen_memoFromPayee": "Descrição do pagamento",
"onchainTransferScreen_networkFee": "Taxa de rede",
+ "onchainTransferScreen_noMintTitle": "Nenhuma casa da moeda para pagar",
+ "onchainTransferScreen_noMintDesc": "Não tem nenhuma casa da moeda com esta moeda. Adicione uma e carregue-a antes de enviar Bitcoin.",
+ "onchainTransferScreen_noOnchainMintTitle": "Nenhuma casa da moeda pode pagar endereços Bitcoin",
+ "onchainTransferScreen_noOnchainMintDesc": "Nenhuma das suas casas da moeda com este saldo suporta pagamentos em Bitcoin. Adicione uma que suporte, ou pague com Lightning.",
+ "onchainTransferScreen_mintNoOnchainSupport": "Esta casa da moeda não suporta pagamentos em Bitcoin. Selecione outra.",
"onchainTransferScreen_noFeeOptions": "A casa da moeda não devolveu opções de taxa para este pagamento e não pode ser pago.",
"onchainTransferScreen_requestQuote": "Continuar",
"onchainTransferScreen_selectFee": "Velocidade de confirmação",
diff --git a/src/i18n_messages/sk.json b/src/i18n_messages/sk.json
index edf19366..5bf039b0 100644
--- a/src/i18n_messages/sk.json
+++ b/src/i18n_messages/sk.json
@@ -311,6 +311,11 @@
"onchainTransferScreen_feeTierSubtext": "Približne %{blocks} blokov · až do %{amount} %{currency}",
"onchainTransferScreen_memoFromPayee": "Popis platby",
"onchainTransferScreen_networkFee": "Sieťový poplatok",
+ "onchainTransferScreen_noMintTitle": "Žiadna mincovňa, z ktorej platiť",
+ "onchainTransferScreen_noMintDesc": "Nemáte mincovňu s touto menou. Pridajte si ju a dobite ju skôr, než odošlete Bitcoin.",
+ "onchainTransferScreen_noOnchainMintTitle": "Žiadna mincovňa nedokáže platiť na Bitcoin adresy",
+ "onchainTransferScreen_noOnchainMintDesc": "Žiadna z vašich mincovní s týmto zostatkom nepodporuje Bitcoin výplaty. Pridajte mincovňu, ktorá ich podporuje, alebo zaplaťte cez Lightning.",
+ "onchainTransferScreen_mintNoOnchainSupport": "Táto mincovňa nepodporuje Bitcoin výplaty. Vyberte inú mincovňu.",
"onchainTransferScreen_noFeeOptions": "Mincovňa nevrátila žiadne možnosti poplatku pre túto platbu, nedá sa zaplatiť.",
"onchainTransferScreen_requestQuote": "Pokračovať",
"onchainTransferScreen_selectFee": "Rýchlosť potvrdenia",
diff --git a/src/screens/OnchainTransferScreen.tsx b/src/screens/OnchainTransferScreen.tsx
index f582b1ea..26fc7b38 100644
--- a/src/screens/OnchainTransferScreen.tsx
+++ b/src/screens/OnchainTransferScreen.tsx
@@ -69,7 +69,7 @@ import {TranItem} from './TranDetailScreen'
* whatever the user picks — a fabricated index would be rejected by the mint (NUT-30
* requires it). So the CHOICE is simulated; the payment underneath is real.
*/
-const MOCK_FEE_TIERS = true
+const MOCK_FEE_TIERS = false
type Props = StaticScreenProps<{
address: string
@@ -264,7 +264,7 @@ export const OnchainTransferScreen = observer(function OnchainTransferScreen({ro
useEffect(() => {
try {
- const {unit, mintUrl, amountSat, memo: bip21Memo} = route.params
+ const {unit, amountSat, memo: bip21Memo} = route.params
if (!unit) {
throw new AppError(Err.VALIDATION_ERROR, translate('missingMintUnitRouteParamsError'))
}
@@ -280,11 +280,19 @@ export const OnchainTransferScreen = observer(function OnchainTransferScreen({ro
}
if (bip21Memo) setMemo(bip21Memo)
- const balance = mintUrl
- ? proofsStore.getMintBalance(mintUrl)
- : proofsStore.getMintBalancesWithUnit(unit)[0]
-
- if (balance) dispatch({type: 'SET_MINT_BALANCE', balance})
+ // Refresh capabilities for every mint that could conceivably pay this, BEFORE
+ // deciding any of them cannot.
+ //
+ // `supportsMelt` reads cached mintInfo, and a mint whose info we have never
+ // fetched reports `hasUnknownCapabilities` — which we resolve as "assume bolt11",
+ // i.e. NOT onchain. So without this, a mint that genuinely supports onchain melt
+ // would be declared incapable purely because we had not asked it lately. getMint()
+ // is what triggers the stale-info refresh (see WalletStore.refreshMintInfoIfStale);
+ // it is fire-and-forget, and the screen is an observer, so the lists below
+ // recompute when the answers land.
+ for (const balance of proofsStore.getMintBalancesWithUnit(unit)) {
+ void walletStore.getMint(balance.mintUrl)
+ }
if (!isInternetReachable) dispatch({type: 'SET_INFO', message: translate('commonOfflinePretty')})
} catch (e: any) {
@@ -297,6 +305,62 @@ export const OnchainTransferScreen = observer(function OnchainTransferScreen({ro
const handleError = (e: AppError) => dispatch({type: 'SET_ERROR', error: e})
+ /**
+ * The mints that can actually settle this payment: they hold a balance in this unit AND
+ * advertise onchain melt for it.
+ *
+ * Computed rather than assumed. Picking a mint first and discovering it cannot pay a
+ * Bitcoin address only when the mint answers "not found" is how this screen used to
+ * behave, and the user got a bare 404 for a decision the wallet already had the
+ * information to make.
+ */
+ const mintBalancesWithUnit = proofsStore.getMintBalancesWithUnit(unitRef.current)
+
+ const onchainMintBalances = mintBalancesWithUnit.filter(balance => {
+ const mint = mintsStore.findByUrl(balance.mintUrl)
+ return !!mint?.supportsMelt!('onchain', unitRef.current)
+ })
+
+ /**
+ * Why this payment cannot be made, if it cannot — kept as two distinct answers.
+ *
+ * "You have no mint holding this currency" and "none of your mints can send Bitcoin
+ * onchain" are different problems with different fixes, and collapsing them into one
+ * message would send the user off to solve the wrong one.
+ */
+ const blockingReason: 'no-mint' | 'no-onchain-mint' | undefined =
+ mintBalancesWithUnit.length === 0
+ ? 'no-mint'
+ : onchainMintBalances.length === 0
+ ? 'no-onchain-mint'
+ : undefined
+
+ const hasNoOnchainMint = !!blockingReason
+
+ /**
+ * Settle on a mint once we know which ones can pay.
+ *
+ * The mint carried in from the Pay screen is only a suggestion — it is whichever mint
+ * the user happened to be looking at, and nothing about scanning a Bitcoin address says
+ * it can melt onchain. If it cannot, silently fall through to one that can rather than
+ * making the user discover the problem and fix it themselves.
+ */
+ useEffect(() => {
+ if (hasNoOnchainMint) return
+ if (mintBalanceToTransferFrom) {
+ const stillCapable = onchainMintBalances.some(
+ b => b.mintUrl === mintBalanceToTransferFrom.mintUrl,
+ )
+ if (stillCapable) return
+ }
+
+ const preferred =
+ onchainMintBalances.find(b => b.mintUrl === route.params.mintUrl) ??
+ onchainMintBalances[0]
+
+ dispatch({type: 'SET_MINT_BALANCE', balance: preferred})
+ }, [onchainMintBalances.length, mintBalanceToTransferFrom?.mintUrl, hasNoOnchainMint])
+
const selectedMint = mintBalanceToTransferFrom
? mintsStore.findByUrl(mintBalanceToTransferFrom.mintUrl)
: undefined
@@ -333,6 +397,18 @@ export const OnchainTransferScreen = observer(function OnchainTransferScreen({ro
return
}
+ // Last line of defence. The mint selector already refuses an incapable mint and
+ // the screen refuses to render this button when none can pay, but asking a
+ // bolt11-only mint for an onchain melt quote gets a bare "not found" back — a
+ // 404 for a question the wallet had every means to answer itself.
+ if (!selectedMint?.supportsMelt!('onchain', unitRef.current)) {
+ dispatch({
+ type: 'SET_INFO',
+ message: translate('onchainTransferScreen_mintNoOnchainSupport'),
+ })
+ return
+ }
+
const {precision, code: currencyCode} = getCurrency(unitRef.current)
const amountUnit = round(toNumber(amountToTransfer) * precision, 0)
@@ -585,7 +661,7 @@ export const OnchainTransferScreen = observer(function OnchainTransferScreen({ro
}}
selectTextOnFocus={true}
unit={unitRef.current}
- editable={!hasQuote && !isSettled}
+ editable={!hasQuote && !isSettled && !hasNoOnchainMint}
style={{color: amountInputColor}}
/>
@@ -694,7 +770,53 @@ export const OnchainTransferScreen = observer(function OnchainTransferScreen({ro
/>
)}
- {!hasQuote && !isSettled && (
+ {/*
+ * No mint can pay a Bitcoin address.
+ *
+ * Said here, plainly, instead of letting the user set an amount, press
+ * Continue, and receive the mint's bare "not found" — an error about a
+ * question the wallet already had the information to answer. There is no
+ * Continue button at all in this state: nothing the user can type makes it
+ * work, and offering the action anyway is what turns a limitation into a
+ * bug report.
+ */}
+ {hasNoOnchainMint && !isSettled && (
+ <>
+
+ }
+ style={$item}
+ />
+ }
+ />
+
+
+
+
+
+ >
+ )}
+
+ {!hasNoOnchainMint && !hasQuote && !isSettled && (
<>