Onchain pay: one card, and honour the payee's BIP21 description

Address and description now share a single card, separated by a rule, instead of
sitting in two cards that read as two unrelated decisions.

The description is READ-ONLY and there is no field for the user to write their
own. A NUT-30 melt request carries {quote, fee_index, inputs, outputs}, and a
Bitcoin transaction has nowhere to put a message either, so nothing the user
typed could ever reach the recipient — and everywhere else in this wallet a memo
IS for the payee, so an editable one here would mean the same word promising
something different on one screen.

What the payee sends is a different thing entirely. BIP21 label/message is their
own text arriving with the request, so the wallet honours it exactly as it
honours a bolt11 invoice's description: shown on the pay screen, saved to
transaction.memo, visible in history. The row is absent when the URI carried
none.

87 tsc (unchanged baseline), 320/320, i18n clean.
This commit is contained in:
minibits-cash
2026-07-14 23:01:02 +02:00
parent e50ef5e1ae
commit 493a5cb0c3
6 changed files with 62 additions and 27 deletions
+1
View File
@@ -309,6 +309,7 @@
"onchainTransferScreen_feeTierBlocks": "About %{blocks} blocks",
"onchainTransferScreen_feeTierReserve": "Up to %{amount} %{currency}",
"onchainTransferScreen_feeTierSubtext": "About %{blocks} blocks · up to %{amount} %{currency}",
"onchainTransferScreen_memoFromPayee": "Payment description",
"onchainTransferScreen_networkFee": "Network fee",
"onchainTransferScreen_noFeeOptions": "The mint returned no fee options for this payment and it can not be paid.",
"onchainTransferScreen_requestQuote": "Continue",
+1
View File
@@ -308,6 +308,7 @@
"onchainTransferScreen_feeTierBlocks": "Unos %{blocks} bloques",
"onchainTransferScreen_feeTierReserve": "Hasta %{amount} %{currency}",
"onchainTransferScreen_feeTierSubtext": "Unos %{blocks} bloques · hasta %{amount} %{currency}",
"onchainTransferScreen_memoFromPayee": "Descripción del pago",
"onchainTransferScreen_networkFee": "Tarifa de red",
"onchainTransferScreen_noFeeOptions": "La casa de cambio no devolvió opciones de tarifa para este pago y no se puede pagar.",
"onchainTransferScreen_requestQuote": "Continuar",
+1
View File
@@ -309,6 +309,7 @@
"onchainTransferScreen_feeTierBlocks": "Cerca de %{blocks} blocos",
"onchainTransferScreen_feeTierReserve": "Até %{amount} %{currency}",
"onchainTransferScreen_feeTierSubtext": "Cerca de %{blocks} blocos · até %{amount} %{currency}",
"onchainTransferScreen_memoFromPayee": "Descrição do pagamento",
"onchainTransferScreen_networkFee": "Taxa de rede",
"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",
+1
View File
@@ -309,6 +309,7 @@
"onchainTransferScreen_feeTierBlocks": "Približne %{blocks} blokov",
"onchainTransferScreen_feeTierReserve": "Až do %{amount} %{currency}",
"onchainTransferScreen_feeTierSubtext": "Približne %{blocks} blokov · až do %{amount} %{currency}",
"onchainTransferScreen_memoFromPayee": "Popis platby",
"onchainTransferScreen_networkFee": "Sieťový poplatok",
"onchainTransferScreen_noFeeOptions": "Mincovňa nevrátila žiadne možnosti poplatku pre túto platbu, nedá sa zaplatiť.",
"onchainTransferScreen_requestQuote": "Pokračovať",
+47 -23
View File
@@ -19,7 +19,7 @@ import {StackActions, StaticScreenProps, useNavigation} from '@react-navigation/
import {TextInput, TextStyle, View, ViewStyle} from 'react-native'
import numbro from 'numbro'
import {MeltQuoteOnchainResponse} from '@cashu/cashu-ts'
import {colors, spacing, typography, useThemeColor} from '../theme'
import {colors, spacing, useThemeColor} from '../theme'
import {
AmountInput,
BottomModal,
@@ -33,7 +33,6 @@ import {
Screen,
Text,
} from '../components'
import {MemoInputCard} from '../components/MemoInputCard'
import {useStores} from '../models'
import {MintBalance} from '../models/Mint'
import {Transaction, TransactionStatus} from '../models/Transaction'
@@ -60,6 +59,7 @@ type Props = StaticScreenProps<{
address: string
/** BIP21 amount hint, in sats. The user may change it. */
amountSat?: number
/** BIP21 label/message: the PAYEE's description. Read-only. */
memo?: string
unit: MintUnit
mintUrl?: string
@@ -255,8 +255,8 @@ export const OnchainTransferScreen = observer(function OnchainTransferScreen({ro
unitRef.current = unit
// BIP21 hints. Both are the payee's suggestion, not a commitment: the user can
// change the amount, and the mint prices the payment from what they confirm.
// The BIP21 amount is the payee's suggestion, not a commitment: the user can
// change it, and the mint prices the payment from what they confirm.
if (amountSat && amountSat > 0) {
setAmountToTransfer(
numbro(amountSat).format({thousandSeparated: true, mantissa: 0}),
@@ -527,36 +527,60 @@ export const OnchainTransferScreen = observer(function OnchainTransferScreen({ro
</View>
<View style={$contentContainer}>
{/*
* Destination, and the payee's description of the payment when the BIP21 URI
* carried one.
*
* The memo is READ-ONLY, and there is no field for the user to write their
* own. A NUT-30 melt request carries {quote, fee_index, inputs, outputs}, and
* a Bitcoin transaction has nowhere to put a message either — so nothing they
* typed could reach anyone. What the payee sent, though, is real: it is their
* text, arriving with the request, and the wallet honours it exactly as it
* honours a bolt11 invoice's description — displayed here, saved on the
* transaction, visible in history.
*/}
{!isSettled && (
<Card
style={$card}
ContentComponent={
<ListItem
tx="onchainTransferScreen_toAddress"
subText={address}
subTextEllipsizeMode="middle"
LeftComponent={
<Icon
containerStyle={$iconContainer}
icon="faBitcoin"
size={spacing.medium}
color={iconColor}
<>
<ListItem
tx="onchainTransferScreen_toAddress"
subText={address}
subTextEllipsizeMode="middle"
LeftComponent={
<Icon
containerStyle={$iconContainer}
icon="faBitcoin"
size={spacing.medium}
color={iconColor}
/>
}
bottomSeparator={!!memo}
style={$item}
/>
{!!memo && (
<ListItem
tx="onchainTransferScreen_memoFromPayee"
subText={memo}
LeftComponent={
<Icon
containerStyle={$iconContainer}
icon="faInfoCircle"
size={spacing.medium}
color={iconColor}
/>
}
style={$item}
/>
}
style={$item}
/>
)}
</>
}
/>
)}
{!hasQuote && !isSettled && (
<>
<MemoInputCard
memo={memo}
setMemo={setMemo}
onMemoDone={() => {}}
disabled={false}
/>
<Text
size="xxs"
tx="onchainTransferScreen_feeHint"
+11 -4
View File
@@ -33,7 +33,14 @@ export type BtcAddressData = {
address: string
/** BIP21 `amount`, in sats. A hint from the payee — the user may change it. */
amountSat?: number
/** BIP21 `label`/`message`, offered as the transaction memo. */
/**
* BIP21 `label`/`message`: the PAYEE's description of what this payment is for.
*
* Not something the user writes — an onchain melt has no field that could carry a
* message out. This is the payee's own text arriving with the request, so the wallet
* honours it: shown read-only and stored on the transaction, exactly as a bolt11
* invoice's description is.
*/
memo?: string
}
@@ -397,10 +404,10 @@ const navigateWithIncomingData = async function (
screen: 'TransferOnchain',
params: {
address: btc.address,
// BIP21 amount and label are HINTS from the payee. They pre-fill the
// screen; the user stays free to change both, and the mint prices the
// payment from what is actually confirmed.
// The BIP21 amount is a HINT: it pre-fills the screen, the user stays free
// to change it, and the mint prices the payment from what they confirm.
amountSat: btc.amountSat,
// The payee's own description, if the URI carried one. Read-only.
memo: btc.memo,
unit,
mintUrl