diff --git a/core/.changelog.d/4560.added b/core/.changelog.d/4560.added new file mode 100644 index 0000000000..ad0afc82c5 --- /dev/null +++ b/core/.changelog.d/4560.added @@ -0,0 +1 @@ +Add Solana staking confirmation dialogs. diff --git a/core/src/apps/solana/layout.py b/core/src/apps/solana/layout.py index e4d44b09e4..8b5040f3bb 100644 --- a/core/src/apps/solana/layout.py +++ b/core/src/apps/solana/layout.py @@ -5,10 +5,13 @@ from trezor.crypto import base58 from trezor.enums import ButtonRequestType from trezor.strings import format_amount from trezor.ui.layouts import ( + confirm_address, confirm_metadata, confirm_properties, confirm_solana_tx, confirm_value, + show_danger, + show_warning, ) from apps.common.paths import address_n_to_str @@ -46,6 +49,10 @@ def _get_address_reference_props( ) +def _blockhash_item(blockhash: bytes) -> tuple[str, str]: + return (f"{TR.words__blockhash}:", base58.encode(blockhash)) + + async def confirm_instruction( instruction: Instruction, instructions_count: int, @@ -345,21 +352,151 @@ async def confirm_custom_transaction( fee_title=f"{TR.solana__expected_fee}:", items=( (f"{TR.words__account}:", _format_path(signer_path)), - (f"{TR.words__blockhash}:", base58.encode(blockhash)), + _blockhash_item(blockhash), ), ) +def _fee_details(fee: Fee) -> tuple[tuple[str, str], ...]: + return ( + (TR.solana__base_fee, f"{format_amount(fee.base, 9)} SOL"), + (TR.solana__priority_fee, f"{format_amount(fee.priority, 9)} SOL"), + ) + + +def _fee_summary(fee: Fee) -> tuple[str, str]: + return (f"{TR.solana__expected_fee}:", f"{format_amount(fee.total, 9)} SOL") + + +async def confirm_stake_withdrawer(withdrawer_account: bytes) -> None: + await show_danger( + title=TR.words__important, + content=TR.solana__stake_withdrawal_warning, + verb_cancel=TR.words__cancel_and_exit, + br_name="confirm_stake_warning", + ) + await confirm_address( + title="Withdraw authority address", + address=base58.encode(withdrawer_account), + br_name="confirm_stake_warning_address", + ) + + +async def confirm_claim_recipient(recipient_account: bytes) -> None: + await show_warning( + content=TR.solana__claim_recipient_warning, + br_name="confirm_claim_warning", + ) + await confirm_address( + title=TR.address_details__title_receive_address, + address=base58.encode(recipient_account), + br_name="confirm_claim_warning_address", + ) + + +async def confirm_stake_transaction( + fee: Fee, + signer_path: list[int], + blockhash: bytes, + create: Instruction, + delegate: Instruction, +) -> None: + from trezor.ui.layouts import confirm_solana_staking_tx + + KNOWN_ACCOUNTS = { + "9QU2QSxhb24FUX3Tu2FpczXjpK3VYrvRudywSZaM29mF": "Everstake", + } + + vote_account = base58.encode(delegate.vote_account[0]) + vote_account = KNOWN_ACCOUNTS.get(vote_account, vote_account) + + await confirm_solana_staking_tx( + title=TR.solana__stake, + message=TR.solana__stake_question, + account=_format_path(signer_path), + account_path=address_n_to_str(signer_path), + vote_account=vote_account, + stake_item=( + TR.solana__stake_account, + base58.encode(delegate.initialized_stake_account[0]), + ), + amount_item=( + f"{TR.words__amount}:", + f"{format_amount(create.lamports, 9)} SOL", + ), + fee_item=_fee_summary(fee), + fee_details=_fee_details(fee), + blockhash_item=_blockhash_item(blockhash), + br_name="confirm_stake_transaction", + ) + + +async def confirm_unstake_transaction( + fee: Fee, + signer_path: list[int], + blockhash: bytes, + deactivate: Instruction, +) -> None: + from trezor.ui.layouts import confirm_solana_staking_tx + + await confirm_solana_staking_tx( + title=TR.solana__unstake, + message=TR.solana__unstake_question, + account=_format_path(signer_path), + account_path=address_n_to_str(signer_path), + vote_account="", + stake_item=( + TR.solana__stake_account, + base58.encode(deactivate.delegated_stake_account[0]), + ), + amount_item=("", ""), + fee_item=_fee_summary(fee), + fee_details=_fee_details(fee), + blockhash_item=_blockhash_item(blockhash), + br_name="confirm_unstake_transaction", + ) + + +async def confirm_claim_transaction( + fee: Fee, + signer_path: list[int], + blockhash: bytes, + withdraw: Instruction, +) -> None: + from trezor.ui.layouts import confirm_solana_staking_tx + + await confirm_solana_staking_tx( + title=TR.solana__claim, + message=TR.solana__claim_question, + account=_format_path(signer_path), + account_path=address_n_to_str(signer_path), + vote_account="", + stake_item=( + TR.solana__stake_account, + base58.encode(withdraw.stake_account[0]), + ), + amount_item=( + f"{TR.words__amount}:", + f"{format_amount(withdraw.lamports, 9)} SOL", + ), + fee_item=_fee_summary(fee), + fee_details=_fee_details(fee), + blockhash_item=_blockhash_item(blockhash), + br_name="confirm_claim_transaction", + ) + + async def confirm_transaction( signer_path: list[int], blockhash: bytes, fee: Fee ) -> None: + (fee_title, fee_value) = _fee_summary(fee) await confirm_solana_tx( amount="", amount_title="", - fee=f"{format_amount(fee.total, 9)} SOL", - fee_title=f"{TR.solana__expected_fee}:", + fee=fee_value, + fee_title=fee_title, items=( (f"{TR.words__account}:", _format_path(signer_path)), - (f"{TR.words__blockhash}:", base58.encode(blockhash)), + _blockhash_item(blockhash), ), ) diff --git a/core/src/apps/solana/predefined_transaction.py b/core/src/apps/solana/predefined_transaction.py index f3d7bf595f..8af16bdb22 100644 --- a/core/src/apps/solana/predefined_transaction.py +++ b/core/src/apps/solana/predefined_transaction.py @@ -4,6 +4,7 @@ from trezor.crypto import base58 from .transaction import Transaction from .transaction.instructions import ( + _SYSTEM_PROGRAM_ID, AssociatedTokenAccountProgramCreateInstruction, Instruction, Token2022ProgramTransferCheckedInstruction, @@ -11,6 +12,8 @@ from .transaction.instructions import ( ) if TYPE_CHECKING: + from typing import Type + from trezor.messages import SolanaTxAdditionalInfo from .transaction import Fee @@ -173,6 +176,7 @@ async def try_confirm_predefined_transaction( transaction: Transaction, fee: Fee, signer_path: list[int], + signer_public_key: bytes, blockhash: bytes, additional_info: SolanaTxAdditionalInfo | None = None, ) -> bool: @@ -191,6 +195,117 @@ async def try_confirm_predefined_transaction( await confirm_system_transfer(instructions[0], fee, signer_path, blockhash) return True + if await try_confirm_staking_transaction( + transaction, + fee, + signer_path, + signer_public_key, + blockhash, + # TODO: do we need to confirm `additional_info`? + ): + return True + return await try_confirm_token_transfer_transaction( transaction, fee, signer_path, blockhash, additional_info ) + + +async def try_confirm_staking_transaction( + transaction: Transaction, + fee: Fee, + signer_path: list[int], + signer_public_key: bytes, + blockhash: bytes, +) -> bool: + from .transaction.instructions import ( + ComputeBudgetProgramSetComputeUnitPriceInstruction, + StakeProgramDeactivateInstruction, + StakeProgramDelegateStakeInstruction, + StakeProgramInitializeInstruction, + StakeProgramWithdrawInstruction, + SystemProgramCreateAccountWithSeedInstruction, + ) + + instructions = transaction.instructions + + def _match_instructions(*expected_types: Type[Instruction]) -> bool: + if len(instructions) != len(expected_types): + return False + return all( + expected_type.is_type_of(instruction) + for instruction, expected_type in zip(instructions, expected_types) + ) + + if _match_instructions( + ComputeBudgetProgramSetComputeUnitPriceInstruction, + SystemProgramCreateAccountWithSeedInstruction, + StakeProgramInitializeInstruction, + StakeProgramDelegateStakeInstruction, + ): + from .layout import confirm_stake_transaction, confirm_stake_withdrawer + + _budget, create, init, delegate = instructions + if signer_public_key != create.funding_account[0]: + return False + if signer_public_key != create.base: + return False + if signer_public_key != init.withdrawer: + await confirm_stake_withdrawer(init.withdrawer) + if signer_public_key != init.staker: + return False + if signer_public_key != delegate.stake_authority[0]: + return False + + if base58.encode(init.custodian) != _SYSTEM_PROGRAM_ID: + return False + + stake_account = create.created_account[0] + if stake_account != init.uninitialized_stake_account[0]: + return False + if stake_account != delegate.initialized_stake_account[0]: + return False + + await confirm_stake_transaction( + fee=fee, + signer_path=signer_path, + blockhash=blockhash, + create=create, + delegate=delegate, + ) + return True + + if _match_instructions( + ComputeBudgetProgramSetComputeUnitPriceInstruction, + StakeProgramDeactivateInstruction, + ): + from .layout import confirm_unstake_transaction + + _budget, deactivate = instructions + if signer_public_key != deactivate.stake_authority[0]: + return False + + await confirm_unstake_transaction( + fee=fee, signer_path=signer_path, blockhash=blockhash, deactivate=deactivate + ) + return True + + if _match_instructions( + ComputeBudgetProgramSetComputeUnitPriceInstruction, + StakeProgramWithdrawInstruction, + ): + from .layout import confirm_claim_recipient, confirm_claim_transaction + + _budget, withdraw = instructions + if signer_public_key != withdraw.withdrawal_authority[0]: + return False + if signer_public_key != withdraw.recipient_account[0]: + await confirm_claim_recipient(withdraw.recipient_account[0]) + + await confirm_claim_transaction( + fee=fee, signer_path=signer_path, blockhash=blockhash, withdraw=withdraw + ) + + return True + + # not a staking transaction + return False diff --git a/core/src/apps/solana/sign_tx.py b/core/src/apps/solana/sign_tx.py index 39d9ed34ac..8251125442 100644 --- a/core/src/apps/solana/sign_tx.py +++ b/core/src/apps/solana/sign_tx.py @@ -59,7 +59,12 @@ async def sign_tx( fee = transaction.calculate_fee() if not await try_confirm_predefined_transaction( - transaction, fee, address_n, transaction.blockhash, msg.additional_info + transaction, + fee, + address_n, + signer_public_key, + transaction.blockhash, + msg.additional_info, ): await confirm_instructions(address_n, signer_public_key, transaction) await confirm_transaction( diff --git a/core/src/trezor/ui/layouts/bolt/__init__.py b/core/src/trezor/ui/layouts/bolt/__init__.py index f628ed94ab..61bba8ac68 100644 --- a/core/src/trezor/ui/layouts/bolt/__init__.py +++ b/core/src/trezor/ui/layouts/bolt/__init__.py @@ -950,6 +950,57 @@ if not utils.BITCOIN_ONLY: br_code=br_code, ) + async def confirm_solana_staking_tx( + title: str, + message: str, + account: str, + account_path: str, + vote_account: str, + stake_item: tuple[str, str], + amount_item: tuple[str, str], + fee_item: tuple[str, str], + fee_details: Iterable[tuple[str, str]], + blockhash_item: tuple[str, str], + br_name: str, + ) -> None: + (amount_label, amount) = amount_item + (fee_label, fee) = fee_item + + confirm_layout = trezorui_api.confirm_value( + title=title, + description=message, + extra=f"{TR.solana__stake_provider}:" if vote_account else None, + value=vote_account, + verb=TR.buttons__continue, + info=True, + ) + + info_layout = trezorui_api.show_info_with_cancel( + title=title, + items=( + (f"{TR.words__account}:", account), + (f"{TR.address_details__derivation_path}:", account_path), + stake_item, + blockhash_item, + ), + horizontal=True, + ) + + await with_info(confirm_layout, info_layout, br_name, ButtonRequestType.SignTx) + + await _confirm_summary( + amount=amount, + amount_label=amount_label, + fee=fee, + fee_label=fee_label, + account_items=None, + title=title, + extra_title=TR.confirm_total__title_fee, + extra_items=fee_details, + br_name=br_name, + br_code=ButtonRequestType.SignTx, + ) + def confirm_cardano_tx( amount: str, fee: str, diff --git a/core/src/trezor/ui/layouts/caesar/__init__.py b/core/src/trezor/ui/layouts/caesar/__init__.py index b8c51fa5fc..c97819fd8f 100644 --- a/core/src/trezor/ui/layouts/caesar/__init__.py +++ b/core/src/trezor/ui/layouts/caesar/__init__.py @@ -926,6 +926,56 @@ if not utils.BITCOIN_ONLY: br_code=br_code, ) + async def confirm_solana_staking_tx( + title: str, + message: str, + account: str, + account_path: str, + vote_account: str, + stake_item: tuple[str, str], + amount_item: tuple[str, str], + fee_item: tuple[str, str], + fee_details: Iterable[tuple[str, str]], + blockhash_item: tuple[str, str], + br_name: str, + ) -> None: + (amount_label, amount) = amount_item + (fee_label, fee) = fee_item + + items = ( + (f"{TR.words__account}:", account), + (f"{TR.address_details__derivation_path}:", account_path), + stake_item, + blockhash_item, + ) + await raise_if_not_confirmed( + trezorui_api.confirm_summary( + amount="", + amount_label=message, + fee=vote_account, + fee_label=f"{TR.solana__stake_provider}:" if vote_account else "", + extra_title=title, + extra_items=items, + ), + br_name, + ButtonRequestType.SignTx, + ) + + await raise_if_not_confirmed( + trezorui_api.confirm_summary( + amount=amount, + amount_label=amount_label, + fee=fee, + fee_label=fee_label, + account_items=None, + title=title, + extra_title=TR.confirm_total__title_fee, + extra_items=fee_details, + ), + br_name, + ButtonRequestType.SignTx, + ) + def confirm_cardano_tx( amount: str, fee: str, diff --git a/core/src/trezor/ui/layouts/delizia/__init__.py b/core/src/trezor/ui/layouts/delizia/__init__.py index 48c200caeb..20efee8380 100644 --- a/core/src/trezor/ui/layouts/delizia/__init__.py +++ b/core/src/trezor/ui/layouts/delizia/__init__.py @@ -862,6 +862,45 @@ if not utils.BITCOIN_ONLY: br_code=br_code, ) + async def confirm_solana_staking_tx( + title: str, + message: str, + account: str, + account_path: str, + vote_account: str, + stake_item: tuple[str, str], + amount_item: tuple[str, str], + fee_item: tuple[str, str], + fee_details: Iterable[tuple[str, str]], + blockhash_item: tuple[str, str], + br_name: str, + ) -> None: + (address_title, address) = stake_item + summary_items = (amount_item, fee_item) + await raise_if_not_confirmed( + trezorui_api.flow_confirm_output( + title=title, + subtitle=None, + message=message, + amount=None, + chunkify=False, + text_mono=False, + account=account, + account_path=account_path, + br_code=ButtonRequestType.SignTx, + br_name=br_name, + address=address, + address_title=address_title, + fee_items=fee_details, + summary_title=title, + summary_items=summary_items, + summary_br_name="confirm_solana_staking_tx_total", + summary_br_code=ButtonRequestType.SignTx, + cancel_text=TR.buttons__cancel, + ), + br_name=None, + ) + def confirm_cardano_tx( amount: str, fee: str,