mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 19:28:26 +00:00
Guard onchain pay when no mint can settle it
Pasting a Bitcoin address with only bolt11 mints installed let the user set an
amount, press Continue, and receive the mint's bare "not found" — a 404 for a
question the wallet already had the information to answer.
Three gaps, all in a row:
- The mint was chosen before any capability check. The mount effect took
route.mintUrl or simply the first mint with a balance, and MintBalanceSelector
(which does gate on supportsMelt('onchain')) only appears AFTER a quote exists —
so there was no way to reach a capable mint before the failing call. Selection
now happens over the mints that can actually settle, and the mint carried in
from the Pay screen is treated as the suggestion it is: it is whichever mint the
user happened to be looking at, and nothing about scanning a Bitcoin address
says it can melt onchain.
- Capabilities could be stale, which would have made this guard WORSE than none.
supportsMelt reads cached mintInfo, and a mint whose info was never fetched
reports hasUnknownCapabilities, which resolves to "assume bolt11" — i.e. not
onchain. A capable mint would have been declared incapable purely because we had
not asked it lately. The screen now triggers the stale-info refresh (getMint())
for every candidate before deciding any of them cannot pay.
- requestQuote never checked at all. It does now, as a last line of defence.
When nothing can pay, the screen says so and offers no Continue button: nothing
the user can type makes it work, and offering the action anyway is what turns a
limitation into a bug report. "No mint holds this currency" and "no mint can send
Bitcoin onchain" stay separate messages — different problems, different fixes, and
one message for both would send the user to solve the wrong one.
87 tsc (unchanged baseline), 335/335, i18n clean.
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}}
|
||||
/>
|
||||
</View>
|
||||
@@ -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 && (
|
||||
<>
|
||||
<Card
|
||||
style={$card}
|
||||
ContentComponent={
|
||||
<ListItem
|
||||
tx={
|
||||
blockingReason === 'no-mint'
|
||||
? 'onchainTransferScreen_noMintTitle'
|
||||
: 'onchainTransferScreen_noOnchainMintTitle'
|
||||
}
|
||||
subTx={
|
||||
blockingReason === 'no-mint'
|
||||
? 'onchainTransferScreen_noMintDesc'
|
||||
: 'onchainTransferScreen_noOnchainMintDesc'
|
||||
}
|
||||
LeftComponent={
|
||||
<Icon
|
||||
containerStyle={$iconContainer}
|
||||
icon="faTriangleExclamation"
|
||||
size={spacing.medium}
|
||||
color={colors.palette.accent300}
|
||||
/>
|
||||
}
|
||||
style={$item}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<View style={$bottomContainer}>
|
||||
<View style={$buttonContainer}>
|
||||
<Button preset="secondary" tx="commonClose" onPress={gotoWallet} />
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!hasNoOnchainMint && !hasQuote && !isSettled && (
|
||||
<>
|
||||
<Text
|
||||
size="xxs"
|
||||
|
||||
Reference in New Issue
Block a user