feat(core): use new SLIP-24 UI for Bitcoin
What changed, and why it matters
This commit updates the on-screen user interface shown when approving Bitcoin payment requests on a Trezor device. It replaces an older screen that merely asked whether to show details with a new screen that actually displays the payment request details (recipient, address, memos, refunds, trades, account path). There is no indication of a security bug or vulnerability fix in the change itself or the commit message.
No security action required. Treat as a normal feature/UI refactor review.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change refactors Bitcoin signing flow to use a new SLIP-24 payment-request UI (layouts.confirm_payment_request) instead of the previous should_show_payment_request_details helper. It threads the current output address (txo.address) and transaction info (tx_info) into Approver.add_payment_request(), removes the show_payment_req_details flag, and always renders the richer confirmation screen. The diff is a pure UI/UX feature addition; no cryptographic, authorization, or input-validation logic is weakened.
Changed components
core/src/apps/bitcoin/sign_tx/approvers.pycore/src/apps/bitcoin/sign_tx/bitcoin.pycore/src/apps/bitcoin/sign_tx/helpers.pycore/src/apps/bitcoin/sign_tx/layout.pyInspect captured patch +103 / −34
diff --git a/core/.changelog.d/5450.added b/core/.changelog.d/5450.added
new file mode 100644
index 00000000..46d0b379
--- /dev/null
+++ b/core/.changelog.d/5450.added
@@ -0,0 +1 @@
+New payment requests UI for BTC.
diff --git a/core/src/apps/bitcoin/sign_tx/approvers.py b/core/src/apps/bitcoin/sign_tx/approvers.py
index dea5751f..83f2b615 100644
--- a/core/src/apps/bitcoin/sign_tx/approvers.py
+++ b/core/src/apps/bitcoin/sign_tx/approvers.py
@@ -35,7 +35,6 @@ class Approver:
self.coin = coin
self.weight = tx_weight.TxWeightCalculator()
self.payment_req_verifier: PaymentRequestVerifier | None = None
- self.show_payment_req_details = False
# amounts in the current transaction
self.total_in = 0 # sum of input amounts
@@ -90,20 +89,23 @@ class Approver:
self.total_out += txo.amount
async def add_payment_request(
- self, msg: PaymentRequest, keychain: Keychain
+ self,
+ payment_request: PaymentRequest,
+ keychain: Keychain,
+ _tx_info: TxInfo | None,
+ _txo: TxOutput,
) -> None:
from apps.common.payment_request import PaymentRequestVerifier
self.finish_payment_request()
self.payment_req_verifier = PaymentRequestVerifier(
- msg, self.coin.slip44, keychain
+ payment_request, self.coin.slip44, keychain
)
def finish_payment_request(self) -> None:
if self.payment_req_verifier:
self.payment_req_verifier.verify()
self.payment_req_verifier = None
- self.show_payment_req_details = False
async def add_change_output(self, txo: TxOutput, script_pubkey: bytes) -> None:
await self._add_output(txo, script_pubkey)
@@ -229,7 +231,7 @@ class BasicApprover(Approver):
raise ProcessError(
"Adding new OP_RETURN outputs in replacement transactions is not supported."
)
- elif txo.payment_req_index is None or self.show_payment_req_details:
+ elif txo.payment_req_index is None:
source_path = (
tx_info.change_detector.wallet_path.get_path() if tx_info else None
)
@@ -246,16 +248,29 @@ class BasicApprover(Approver):
self.external_output_index += 1
async def add_payment_request(
- self, msg: PaymentRequest, keychain: Keychain
+ self,
+ payment_request: PaymentRequest,
+ keychain: Keychain,
+ tx_info: TxInfo | None,
+ txo: TxOutput,
) -> None:
- await super().add_payment_request(msg, keychain)
- if msg.amount is None:
+ await super().add_payment_request(payment_request, keychain, tx_info, txo)
+ if payment_request.amount is None:
raise DataError("Missing payment request amount.")
- result = await helpers.should_show_payment_request_details(
- msg, self.coin, self.amount_unit
+ source_path = (
+ tx_info.change_detector.wallet_path.get_path() if tx_info else None
+ )
+
+ assert txo.address
+
+ await helpers.show_payment_request_details(
+ txo.address,
+ payment_request,
+ self.coin,
+ self.amount_unit,
+ source_path,
)
- self.show_payment_req_details = result is True
async def approve_orig_txids(
self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]
diff --git a/core/src/apps/bitcoin/sign_tx/bitcoin.py b/core/src/apps/bitcoin/sign_tx/bitcoin.py
index 920d503b..83efd253 100644
--- a/core/src/apps/bitcoin/sign_tx/bitcoin.py
+++ b/core/src/apps/bitcoin/sign_tx/bitcoin.py
@@ -523,7 +523,9 @@ class Bitcoin:
tx_ack_payment_req = await helpers.request_payment_req(
self.tx_req, payment_req_index
)
- await approver.add_payment_request(tx_ack_payment_req, self.keychain)
+ await approver.add_payment_request(
+ tx_ack_payment_req, self.keychain, self.tx_info, txo
+ )
self.payment_req_index = payment_req_index
if self.tx_info.output_is_change(txo):
diff --git a/core/src/apps/bitcoin/sign_tx/helpers.py b/core/src/apps/bitcoin/sign_tx/helpers.py
index a0eda706..35a29b46 100644
--- a/core/src/apps/bitcoin/sign_tx/helpers.py
+++ b/core/src/apps/bitcoin/sign_tx/helpers.py
@@ -90,17 +90,25 @@ class UiConfirmDecredSSTXSubmission(UiConfirm):
class UiConfirmPaymentRequest(UiConfirm):
def __init__(
self,
+ provider_address: str,
payment_req: PaymentRequest,
coin: CoinInfo,
amount_unit: AmountUnit,
+ address_n: Bip32Path | None,
) -> None:
+ self.provider_address = provider_address
self.payment_req = payment_req
self.amount_unit = amount_unit
self.coin = coin
+ self.address_n = address_n
- def confirm_dialog(self) -> Awaitable[bool]:
- return layout.should_show_payment_request_details(
- self.payment_req, self.coin, self.amount_unit
+ def confirm_dialog(self) -> Awaitable[None]:
+ return layout.show_payment_request_details(
+ self.provider_address,
+ self.payment_req,
+ self.coin,
+ self.amount_unit,
+ self.address_n,
)
__eq__ = utils.obj_eq
@@ -266,8 +274,8 @@ def confirm_decred_sstx_submission(output: TxOutput, coin: CoinInfo, amount_unit
return (yield UiConfirmDecredSSTXSubmission(output, coin, amount_unit)) # type: ignore [awaitable-return-type]
-def should_show_payment_request_details(payment_req: PaymentRequest, coin: CoinInfo, amount_unit: AmountUnit) -> Awaitable[bool]: # type: ignore [awaitable-return-type]
- return (yield UiConfirmPaymentRequest(payment_req, coin, amount_unit)) # type: ignore [awaitable-return-type]
+def show_payment_request_details(provider_address: str, payment_req: PaymentRequest, coin: CoinInfo, amount_unit: AmountUnit, address_n: Bip32Path | None) -> Awaitable[bool]: # type: ignore [awaitable-return-type]
+ return (yield UiConfirmPaymentRequest(provider_address, payment_req, coin, amount_unit, address_n)) # type: ignore [awaitable-return-type]
def confirm_replacement(description: str, txid: bytes) -> Awaitable[Any]: # type: ignore [awaitable-return-type]
diff --git a/core/src/apps/bitcoin/sign_tx/layout.py b/core/src/apps/bitcoin/sign_tx/layout.py
index 61e75772..778d3a69 100644
--- a/core/src/apps/bitcoin/sign_tx/layout.py
+++ b/core/src/apps/bitcoin/sign_tx/layout.py
@@ -151,30 +151,73 @@ async def confirm_decred_sstx_submission(
)
-async def should_show_payment_request_details(
- msg: PaymentRequest,
+async def show_payment_request_details(
+ provider_address: str,
+ payment_request: PaymentRequest,
coin: CoinInfo,
amount_unit: AmountUnit,
-) -> bool:
+ address_n: Bip32Path | None,
+) -> None:
from trezor import wire
- memo_texts: list[str] = []
- for m in msg.memos:
- if m.text_memo is not None:
- memo_texts.append(m.text_memo.text)
- elif m.refund_memo is not None:
- pass
- elif m.coin_purchase_memo is not None:
- memo_texts.append(f"{TR.words__buying} {m.coin_purchase_memo.amount}.")
+ assert payment_request.amount is not None # required for non-CoinJoin
+ total_amount = format_coin_amount(payment_request.amount, coin, amount_unit)
+
+ texts = []
+ refunds = []
+ trades = []
+ for memo in payment_request.memos:
+ if memo.text_memo is not None:
+ texts.append((None, memo.text_memo.text))
+ elif memo.text_details_memo is not None:
+ texts.append((memo.text_details_memo.title, memo.text_details_memo.text))
+ elif memo.refund_memo:
+ refund_address_n = memo.refund_memo.address_n
+ refund_account = account_label(coin, refund_address_n)
+ refund_account_path = (
+ address_n_to_str(refund_address_n) if refund_address_n else None
+ )
+ refunds.append(
+ (memo.refund_memo.address, refund_account, refund_account_path)
+ )
+ elif memo.coin_purchase_memo:
+ coin_purchase_address_n = memo.coin_purchase_memo.address_n
+ coin_purchase_account = account_label(coin, coin_purchase_address_n)
+ coin_purchase_account_path = (
+ address_n_to_str(coin_purchase_address_n)
+ if coin_purchase_address_n
+ else None
+ )
+ trades.append(
+ (
+ f"-\u00A0{total_amount}",
+ f"+\u00A0{memo.coin_purchase_memo.amount}",
+ memo.coin_purchase_memo.address,
+ coin_purchase_account,
+ coin_purchase_account_path,
+ )
+ )
else:
raise wire.DataError("Unrecognized memo type in payment request memo.")
- assert msg.amount is not None
-
- return await layouts.should_show_payment_request_details(
- msg.recipient_name,
- format_coin_amount(msg.amount, coin, amount_unit),
- memo_texts,
+ account = account_label(coin, address_n)
+ account_path = address_n_to_str(address_n) if address_n else None
+ account_items = []
+ if account:
+ account_items.append((TR.words__account, account))
+ if account_path:
+ account_items.append((TR.address_details__derivation_path, account_path))
+
+ await layouts.confirm_payment_request(
+ payment_request.recipient_name,
+ provider_address,
+ texts,
+ refunds,
+ trades,
+ account_items,
+ None,
+ None,
+ None,
)
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.