1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-07-23 06:58:13 +00:00

feat(core): Ethereum payment requests UI

This commit is contained in:
Ioan Bizău 2025-07-03 16:22:51 +02:00 committed by Ioan Bizău
parent d03aa07b41
commit 9dd51b8521
6 changed files with 362 additions and 61 deletions

View File

@ -0,0 +1 @@
[T2B1,T3B1,T3T1] Added SLIP-24 swaps.

View File

@ -1,10 +1,9 @@
use crate::{
strutil::TString,
ui::{
component::{paginated::PaginateFull, Bar, Component, Event, EventCtx, Label, Never},
component::{paginated::SinglePage, Bar, Component, Event, EventCtx, Label, Never},
geometry::{Grid, Insets, Rect},
shape::Renderer,
util::Pager,
},
};
@ -20,22 +19,14 @@ impl TradeScreen {
pub fn new(sell_amount: TString<'static>, buy_amount: TString<'static>) -> Self {
Self {
sell_amount: Label::left_aligned(sell_amount, theme::TEXT_WARNING).top_aligned(),
line: Bar::new(theme::GREY_EXTRA_DARK, theme::BG, 1),
line: Bar::new(theme::GREY_EXTRA_DARK, theme::BG, 2),
buy_amount: Label::left_aligned(buy_amount, theme::TEXT_MAIN_GREEN_LIME)
.bottom_aligned(),
}
}
}
impl PaginateFull for TradeScreen {
fn pager(&self) -> Pager {
Pager::new(1).with_current(0)
}
fn change_page(&mut self, _active_page: u16) {
unimplemented!()
}
}
impl SinglePage for TradeScreen {}
impl Component for TradeScreen {
type Msg = Never;

View File

@ -24,6 +24,7 @@ if TYPE_CHECKING:
EthereumNetworkInfo,
EthereumStructMember,
EthereumTokenInfo,
PaymentRequest,
)
@ -107,6 +108,67 @@ async def require_confirm_tx(
)
async def require_confirm_payment_request(
provider_address: str,
verified_payment_req: PaymentRequest,
address_n: list[int],
maximum_fee: str,
fee_info_items: Iterable[tuple[str, str]],
chain_id: int,
network: EthereumNetworkInfo,
token: EthereumTokenInfo | None,
token_address: str,
) -> None:
from trezor.ui.layouts import confirm_ethereum_payment_request
account, account_path = get_account_and_path(address_n)
assert (
verified_payment_req.amount is not None
) # amount is required for non-CoinJoin transactions
total_amount = format_ethereum_amount(verified_payment_req.amount, token, network)
refunds = []
trades = []
for memo in verified_payment_req.memos:
if memo.refund_memo:
refund_account, refund_account_path = get_account_and_path(
memo.refund_memo.address_n
)
assert refund_account is not None
assert refund_account_path is not None
refunds.append(
(memo.refund_memo.address, refund_account, refund_account_path)
)
elif memo.coin_purchase_memo:
coin_purchase_account, coin_purchase_account_path = get_account_and_path(
memo.coin_purchase_memo.address_n
)
assert coin_purchase_account is not None
assert coin_purchase_account_path is not None
trades.append(
(
f"- {total_amount}",
f"+ {memo.coin_purchase_memo.amount}",
memo.coin_purchase_memo.address,
coin_purchase_account,
coin_purchase_account_path,
)
)
await confirm_ethereum_payment_request(
verified_payment_req.recipient_name,
provider_address,
refunds,
trades,
account,
account_path,
f"{network.name} ({chain_id})",
maximum_fee,
fee_info_items,
token_address,
)
async def require_confirm_stake(
addr_bytes: bytes,
value: int,

View File

@ -144,13 +144,24 @@ async def confirm_tx_data(
data_total_len: int,
payment_req_verifier: PaymentRequestVerifier | None,
) -> None:
# function distinguishes between staking / smart contracts / regular transactions
from trezor import TR
from trezor.ui.layouts import ethereum_address_title
from . import tokens
from .layout import (
require_confirm_address,
require_confirm_approve,
require_confirm_other_data,
require_confirm_payment_request,
require_confirm_tx,
require_confirm_unknown_token,
)
# local_cache_attribute
payment_req = msg.payment_req
SC_FUNC_SIG_APPROVE = constants.SC_FUNC_SIG_APPROVE
REVOKE_AMOUNT = constants.SC_FUNC_APPROVE_REVOKE_AMOUNT
if await handle_staking(msg, defs.network, address_bytes, maximum_fee, fee_items):
return
@ -159,7 +170,28 @@ async def confirm_tx_data(
msg, defs, address_bytes
)
if func_sig == constants.SC_FUNC_SIG_APPROVE:
if token is tokens.UNKNOWN_TOKEN:
if func_sig == SC_FUNC_SIG_APPROVE:
if value == REVOKE_AMOUNT:
title = TR.ethereum__approve_intro_title_revoke
else:
title = TR.ethereum__approve_intro_title
else:
title = ethereum_address_title()
await require_confirm_unknown_token(title)
if func_sig != SC_FUNC_SIG_APPROVE:
# For unknown tokens we also show the token address immediately after the warning
# except in the case of the "approve" flow which shows the token address later on!
await require_confirm_address(
address_bytes,
ethereum_address_title(),
TR.ethereum__token_contract,
TR.buttons__continue,
"unknown_token",
TR.ethereum__unknown_contract_address,
)
if func_sig == SC_FUNC_SIG_APPROVE:
assert token
assert token_address
@ -185,29 +217,45 @@ async def confirm_tx_data(
recipient_str = (
address_from_bytes(recipient, defs.network) if recipient else None
)
if payment_req_verifier is not None:
# If a payment_req_verifier is provided, then msg.payment_req must have been set.
assert msg.payment_req is not None
payment_req_verifier.add_output(value, recipient_str or "")
payment_req_verifier.verify()
recipient_str = msg.payment_req.recipient_name
token_address_str = address_from_bytes(address_bytes, defs.network)
is_contract_interaction = token is None and data_total_len > 0
if is_contract_interaction:
await require_confirm_other_data(msg.data_initial_chunk, data_total_len)
if payment_req_verifier is not None:
if is_contract_interaction:
raise DataError("Payment Requests don't support contract interactions")
await require_confirm_tx(
recipient_str,
value,
msg.address_n,
maximum_fee,
fee_items,
defs.network,
token,
is_contract_interaction=is_contract_interaction,
chunkify=bool(msg.chunkify),
)
# If a payment_req_verifier is provided, then msg.payment_req must have been set.
assert payment_req is not None
assert recipient_str is not None
payment_req_verifier.add_output(value, recipient_str or "")
payment_req_verifier.verify()
await require_confirm_payment_request(
recipient_str,
payment_req,
msg.address_n,
maximum_fee,
fee_items,
msg.chain_id,
defs.network,
token,
token_address_str,
)
else:
if is_contract_interaction:
await require_confirm_other_data(msg.data_initial_chunk, data_total_len)
await require_confirm_tx(
recipient_str,
value,
msg.address_n,
maximum_fee,
fee_items,
defs.network,
token,
is_contract_interaction=is_contract_interaction,
chunkify=bool(msg.chunkify),
)
async def handle_staking(
@ -257,12 +305,6 @@ async def _handle_erc20(
definitions: Definitions,
address_bytes: bytes,
) -> tuple[EthereumTokenInfo | None, bytes | None, bytes | None, bytes, int | None]:
from trezor import TR
from trezor.ui.layouts import ethereum_address_title
from . import tokens
from .layout import require_confirm_address, require_confirm_unknown_token
# local_cache_attribute
data_initial_chunk = msg.data_initial_chunk
SC_FUNC_SIG_BYTES = constants.SC_FUNC_SIG_BYTES
@ -270,7 +312,6 @@ async def _handle_erc20(
SC_ARGUMENT_ADDRESS_BYTES = constants.SC_ARGUMENT_ADDRESS_BYTES
SC_FUNC_SIG_APPROVE = constants.SC_FUNC_SIG_APPROVE
SC_FUNC_SIG_TRANSFER = constants.SC_FUNC_SIG_TRANSFER
REVOKE_AMOUNT = constants.SC_FUNC_APPROVE_REVOKE_AMOUNT
token = None
token_address = None
@ -313,27 +354,6 @@ async def _handle_erc20(
token = definitions.get_token(address_bytes)
token_address = address_bytes
if token is tokens.UNKNOWN_TOKEN:
if func_sig == SC_FUNC_SIG_APPROVE:
if value == REVOKE_AMOUNT:
title = TR.ethereum__approve_intro_title_revoke
else:
title = TR.ethereum__approve_intro_title
else:
title = ethereum_address_title()
await require_confirm_unknown_token(title)
if func_sig != SC_FUNC_SIG_APPROVE:
# For unknown tokens we also show the token address immediately after the warning
# except in the case of the "approve" flow which shows the token address later on!
await require_confirm_address(
address_bytes,
ethereum_address_title(),
TR.ethereum__token_contract,
TR.buttons__continue,
"unknown_token",
TR.ethereum__unknown_contract_address,
)
return token, token_address, func_sig, recipient, value

View File

@ -1071,6 +1071,116 @@ if not utils.BITCOIN_ONLY:
br_name="confirm_ethereum_approve",
)
async def confirm_trade(
title: str,
sell_amount: str,
buy_amount: str,
address: str,
account: str,
account_path: str,
token_address: str,
) -> None:
from trezor.ui.layouts.menu import Menu, confirm_with_menu
trade_layout = trezorui_api.confirm_with_info(
title=title,
items=[(sell_amount, True), (buy_amount, True)],
verb=TR.buttons__confirm,
verb_info=TR.buttons__info,
)
menu = Menu.root(
[
create_details(
TR.address__title_receive_address,
[
("", address),
(TR.words__account, account),
(TR.address_details__derivation_path, account_path),
],
),
create_details(TR.ethereum__token_contract, token_address),
]
)
await confirm_with_menu(trade_layout, menu, "confirm_trade")
async def confirm_ethereum_payment_request(
recipient_name: str,
recipient: str,
refunds: Iterable[tuple[str, str, str]],
trades: Iterable[tuple[str, str, str, str, str]],
account: str | None,
account_path: str | None,
chain_id: str,
maximum_fee: str,
fee_info_items: Iterable[tuple[str, str]],
token_address: str,
) -> None:
from trezor.ui.layouts.menu import Menu, confirm_with_menu
main_layout = trezorui_api.confirm_with_info(
title=TR.words__swap,
items=[(TR.words__provider, True), (recipient_name, False)],
verb=TR.buttons__continue,
verb_info=TR.buttons__info,
external_menu=True,
)
menu_items = [create_details(TR.address__title_provider_address, recipient)]
for r_address, r_account, r_account_path in refunds:
menu_items.append(
create_details(
TR.address__title_refund_address,
[
("", r_address),
(TR.words__account, r_account),
(TR.address_details__derivation_path, r_account_path),
],
)
)
menu = Menu.root(menu_items)
await confirm_with_menu(main_layout, menu, "confirm_payment_request")
for sell_amount, buy_amount, t_address, t_account, t_account_path in trades:
await confirm_trade(
f"{TR.words__swap} {TR.words__assets}",
sell_amount,
buy_amount,
t_address,
t_account,
t_account_path,
token_address,
)
account_items = []
if account:
account_items.append((TR.words__account, account))
if account_path:
account_items.append((TR.address_details__derivation_path, account_path))
account_items.append((TR.ethereum__approve_chain_id, chain_id))
summary_layout = trezorui_api.confirm_summary(
amount=None,
amount_label=None,
fee=maximum_fee,
fee_label=TR.words__transaction_fee,
title=TR.words__title_summary,
external_menu=True,
)
summary_menu_items = [
create_details(TR.confirm_total__title_fee, list(fee_info_items)),
create_details(TR.address_details__account_info, account_items),
]
summary_menu = Menu.root(summary_menu_items)
await confirm_with_menu(
summary_layout, summary_menu, br_name="confirm_payment_request"
)
async def confirm_ethereum_staking_tx(
title: str,
intro_question: str,

View File

@ -11,6 +11,7 @@ if TYPE_CHECKING:
from typing import Any, Awaitable, Coroutine, Iterable, NoReturn, Sequence, TypeVar
from ..common import ExceptionType, PropertyType
from ..menu import Details
T = TypeVar("T")
@ -1031,6 +1032,114 @@ if not utils.BITCOIN_ONLY:
TR.confirm_total__title_fee,
)
async def confirm_trade(
title: str,
subtitle: str,
sell_amount: str,
buy_amount: str,
address: str,
account: str,
account_path: str,
token_address: str,
) -> None:
from trezor.ui.layouts.menu import Menu, confirm_with_menu
trade_layout = trezorui_api.confirm_trade(
title=title,
subtitle=subtitle,
sell_amount=sell_amount,
buy_amount=buy_amount,
)
menu = Menu.root(
[
create_details(
TR.address__title_receive_address,
[
("", address),
(TR.words__account, account),
(TR.address_details__derivation_path, account_path),
],
),
create_details(TR.ethereum__token_contract, token_address),
],
TR.send__cancel_sign,
)
await confirm_with_menu(trade_layout, menu, "confirm_trade")
async def confirm_ethereum_payment_request(
recipient_name: str,
recipient: str,
refunds: Iterable[tuple[str, str, str]],
trades: Iterable[tuple[str, str, str, str, str]],
account: str | None,
account_path: str | None,
chain_id: str,
maximum_fee: str,
fee_info_items: Iterable[tuple[str, str]],
token_address: str,
) -> None:
from trezor.ui.layouts.menu import Menu, confirm_with_menu
main_layout = trezorui_api.confirm_value(
title=TR.words__swap,
subtitle=TR.words__provider,
value=recipient_name,
description=None,
verb=TR.instructions__tap_to_continue,
verb_cancel=None,
chunkify=False,
external_menu=True,
)
menu_items = [create_details(TR.address__title_provider_address, recipient)]
for r_address, r_account, r_account_path in refunds:
menu_items.append(
create_details(
TR.address__title_refund_address,
[
("", r_address),
(TR.words__account, r_account),
(TR.address_details__derivation_path, r_account_path),
],
)
)
menu = Menu.root(menu_items, TR.send__cancel_sign)
await confirm_with_menu(main_layout, menu, "confirm_payment_request")
for sell_amount, buy_amount, t_address, t_account, t_account_path in trades:
await confirm_trade(
TR.words__swap,
TR.words__assets,
sell_amount,
buy_amount,
t_address,
t_account,
t_account_path,
token_address,
)
account_items = []
if account:
account_items.append((TR.words__account, account))
if account_path:
account_items.append((TR.address_details__derivation_path, account_path))
account_items.append((TR.ethereum__approve_chain_id, chain_id))
await _confirm_summary(
None,
None,
maximum_fee,
TR.words__transaction_fee,
TR.words__title_summary,
account_items,
fee_info_items,
TR.confirm_total__title_fee,
"confirm_payment_request",
)
async def confirm_ethereum_staking_tx(
title: str,
intro_question: str,
@ -1600,3 +1709,11 @@ def tutorial(br_code: ButtonRequestType = BR_CODE_OTHER) -> Awaitable[None]:
"tutorial",
br_code,
)
def create_details(name: str, value: list[tuple[str, str]] | str) -> Details:
from trezor.ui.layouts.menu import Details
return Details.from_layout(
name, lambda: trezorui_api.show_properties(title=name, value=value)
)