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:
parent
d03aa07b41
commit
9dd51b8521
1
core/.changelog.d/4951.added
Normal file
1
core/.changelog.d/4951.added
Normal file
@ -0,0 +1 @@
|
||||
[T2B1,T3B1,T3T1] Added SLIP-24 swaps.
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user