1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-02-22 12:32:02 +00:00

feat(core): add Solana staking layouts

This commit is contained in:
Roman Zeyde 2025-02-17 10:35:55 +02:00
parent afb8963a40
commit 6d205a416f
7 changed files with 404 additions and 5 deletions

View File

@ -0,0 +1 @@
Add Solana staking confirmation dialogs.

View File

@ -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),
),
)

View File

@ -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

View File

@ -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(

View File

@ -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,

View File

@ -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,

View File

@ -862,6 +862,46 @@ 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] | None,
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,) if amount_item else ()
summary_items += (fee_item, blockhash_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,