1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-07-23 23:18:16 +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::{ use crate::{
strutil::TString, strutil::TString,
ui::{ ui::{
component::{paginated::PaginateFull, Bar, Component, Event, EventCtx, Label, Never}, component::{paginated::SinglePage, Bar, Component, Event, EventCtx, Label, Never},
geometry::{Grid, Insets, Rect}, geometry::{Grid, Insets, Rect},
shape::Renderer, shape::Renderer,
util::Pager,
}, },
}; };
@ -20,22 +19,14 @@ impl TradeScreen {
pub fn new(sell_amount: TString<'static>, buy_amount: TString<'static>) -> Self { pub fn new(sell_amount: TString<'static>, buy_amount: TString<'static>) -> Self {
Self { Self {
sell_amount: Label::left_aligned(sell_amount, theme::TEXT_WARNING).top_aligned(), 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) buy_amount: Label::left_aligned(buy_amount, theme::TEXT_MAIN_GREEN_LIME)
.bottom_aligned(), .bottom_aligned(),
} }
} }
} }
impl PaginateFull for TradeScreen { impl SinglePage for TradeScreen {}
fn pager(&self) -> Pager {
Pager::new(1).with_current(0)
}
fn change_page(&mut self, _active_page: u16) {
unimplemented!()
}
}
impl Component for TradeScreen { impl Component for TradeScreen {
type Msg = Never; type Msg = Never;

View File

@ -24,6 +24,7 @@ if TYPE_CHECKING:
EthereumNetworkInfo, EthereumNetworkInfo,
EthereumStructMember, EthereumStructMember,
EthereumTokenInfo, 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( async def require_confirm_stake(
addr_bytes: bytes, addr_bytes: bytes,
value: int, value: int,

View File

@ -144,13 +144,24 @@ async def confirm_tx_data(
data_total_len: int, data_total_len: int,
payment_req_verifier: PaymentRequestVerifier | None, payment_req_verifier: PaymentRequestVerifier | None,
) -> 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 ( from .layout import (
require_confirm_address,
require_confirm_approve, require_confirm_approve,
require_confirm_other_data, require_confirm_other_data,
require_confirm_payment_request,
require_confirm_tx, 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): if await handle_staking(msg, defs.network, address_bytes, maximum_fee, fee_items):
return return
@ -159,7 +170,28 @@ async def confirm_tx_data(
msg, defs, address_bytes 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
assert token_address assert token_address
@ -185,29 +217,45 @@ async def confirm_tx_data(
recipient_str = ( recipient_str = (
address_from_bytes(recipient, defs.network) if recipient else None address_from_bytes(recipient, defs.network) if recipient else None
) )
if payment_req_verifier is not None: token_address_str = address_from_bytes(address_bytes, defs.network)
# 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
is_contract_interaction = token is None and data_total_len > 0 is_contract_interaction = token is None and data_total_len > 0
if is_contract_interaction: if payment_req_verifier is not None:
await require_confirm_other_data(msg.data_initial_chunk, data_total_len) if is_contract_interaction:
raise DataError("Payment Requests don't support contract interactions")
await require_confirm_tx( # If a payment_req_verifier is provided, then msg.payment_req must have been set.
recipient_str, assert payment_req is not None
value, assert recipient_str is not None
msg.address_n, payment_req_verifier.add_output(value, recipient_str or "")
maximum_fee, payment_req_verifier.verify()
fee_items, await require_confirm_payment_request(
defs.network, recipient_str,
token, payment_req,
is_contract_interaction=is_contract_interaction, msg.address_n,
chunkify=bool(msg.chunkify), 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( async def handle_staking(
@ -257,12 +305,6 @@ async def _handle_erc20(
definitions: Definitions, definitions: Definitions,
address_bytes: bytes, address_bytes: bytes,
) -> tuple[EthereumTokenInfo | None, bytes | None, bytes | None, bytes, int | None]: ) -> 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 # local_cache_attribute
data_initial_chunk = msg.data_initial_chunk data_initial_chunk = msg.data_initial_chunk
SC_FUNC_SIG_BYTES = constants.SC_FUNC_SIG_BYTES 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_ARGUMENT_ADDRESS_BYTES = constants.SC_ARGUMENT_ADDRESS_BYTES
SC_FUNC_SIG_APPROVE = constants.SC_FUNC_SIG_APPROVE SC_FUNC_SIG_APPROVE = constants.SC_FUNC_SIG_APPROVE
SC_FUNC_SIG_TRANSFER = constants.SC_FUNC_SIG_TRANSFER SC_FUNC_SIG_TRANSFER = constants.SC_FUNC_SIG_TRANSFER
REVOKE_AMOUNT = constants.SC_FUNC_APPROVE_REVOKE_AMOUNT
token = None token = None
token_address = None token_address = None
@ -313,27 +354,6 @@ async def _handle_erc20(
token = definitions.get_token(address_bytes) token = definitions.get_token(address_bytes)
token_address = 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 return token, token_address, func_sig, recipient, value

View File

@ -1071,6 +1071,116 @@ if not utils.BITCOIN_ONLY:
br_name="confirm_ethereum_approve", 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( async def confirm_ethereum_staking_tx(
title: str, title: str,
intro_question: str, intro_question: str,

View File

@ -11,6 +11,7 @@ if TYPE_CHECKING:
from typing import Any, Awaitable, Coroutine, Iterable, NoReturn, Sequence, TypeVar from typing import Any, Awaitable, Coroutine, Iterable, NoReturn, Sequence, TypeVar
from ..common import ExceptionType, PropertyType from ..common import ExceptionType, PropertyType
from ..menu import Details
T = TypeVar("T") T = TypeVar("T")
@ -1031,6 +1032,114 @@ if not utils.BITCOIN_ONLY:
TR.confirm_total__title_fee, 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( async def confirm_ethereum_staking_tx(
title: str, title: str,
intro_question: str, intro_question: str,
@ -1600,3 +1709,11 @@ def tutorial(br_code: ButtonRequestType = BR_CODE_OTHER) -> Awaitable[None]:
"tutorial", "tutorial",
br_code, 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)
)