parent
e3eb913b58
commit
79b04d72a3
@ -0,0 +1,191 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from trezor.crypto import base58
|
||||||
|
|
||||||
|
from .transaction import Transaction
|
||||||
|
from .transaction.instructions import (
|
||||||
|
AssociatedTokenAccountProgramCreateInstruction,
|
||||||
|
Instruction,
|
||||||
|
Token2022ProgramTransferCheckedInstruction,
|
||||||
|
TokenProgramTransferCheckedInstruction,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from trezor.messages import SolanaTxAdditionalInfo
|
||||||
|
|
||||||
|
TransferTokenInstruction = (
|
||||||
|
TokenProgramTransferCheckedInstruction
|
||||||
|
| Token2022ProgramTransferCheckedInstruction
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_token_transfer_instructions(
|
||||||
|
instructions: list[Instruction],
|
||||||
|
) -> list[TransferTokenInstruction]:
|
||||||
|
return [
|
||||||
|
instruction
|
||||||
|
for instruction in instructions
|
||||||
|
if TokenProgramTransferCheckedInstruction.is_type_of(instruction)
|
||||||
|
or Token2022ProgramTransferCheckedInstruction.is_type_of(instruction)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_create_associated_token_account_instructions(
|
||||||
|
instructions: list[Instruction],
|
||||||
|
) -> list[AssociatedTokenAccountProgramCreateInstruction]:
|
||||||
|
return [
|
||||||
|
instruction
|
||||||
|
for instruction in instructions
|
||||||
|
if AssociatedTokenAccountProgramCreateInstruction.is_type_of(instruction)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def is_predefined_token_transfer(
|
||||||
|
instructions: list[Instruction],
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Checks that the transaction consists of one or zero create token account instructions
|
||||||
|
and one or more transfer token instructions. Also checks that the token program, token mint
|
||||||
|
and destination in the instructions are the same. I.e. valid instructions can be:
|
||||||
|
|
||||||
|
[transfer]
|
||||||
|
[transfer, *transfer]
|
||||||
|
[create account, transfer]
|
||||||
|
[create account, transfer, *transfer]
|
||||||
|
"""
|
||||||
|
create_token_account_instructions = (
|
||||||
|
get_create_associated_token_account_instructions(instructions)
|
||||||
|
)
|
||||||
|
transfer_token_instructions = get_token_transfer_instructions(instructions)
|
||||||
|
|
||||||
|
if len(create_token_account_instructions) + len(transfer_token_instructions) != len(
|
||||||
|
instructions
|
||||||
|
):
|
||||||
|
# there are also other instructions
|
||||||
|
return False
|
||||||
|
|
||||||
|
if len(create_token_account_instructions) > 1:
|
||||||
|
# there is more than one create token account instruction
|
||||||
|
return False
|
||||||
|
|
||||||
|
if (
|
||||||
|
len(create_token_account_instructions) == 1
|
||||||
|
and instructions[0] != create_token_account_instructions[0]
|
||||||
|
):
|
||||||
|
# create account instruction has to be the first instruction
|
||||||
|
return False
|
||||||
|
|
||||||
|
if len(transfer_token_instructions) == 0:
|
||||||
|
# there are no transfer token instructions
|
||||||
|
return False
|
||||||
|
|
||||||
|
token_program = transfer_token_instructions[0].program_id
|
||||||
|
token_mint = transfer_token_instructions[0].token_mint[0]
|
||||||
|
token_account = transfer_token_instructions[0].destination_account[0]
|
||||||
|
owner = transfer_token_instructions[0].owner[0]
|
||||||
|
|
||||||
|
for transfer_token_instruction in transfer_token_instructions:
|
||||||
|
if (
|
||||||
|
transfer_token_instruction.program_id != token_program
|
||||||
|
or transfer_token_instruction.token_mint[0] != token_mint
|
||||||
|
or transfer_token_instruction.destination_account[0] != token_account
|
||||||
|
or transfer_token_instruction.owner[0] != owner
|
||||||
|
):
|
||||||
|
# there are different token accounts, don't handle as predefined
|
||||||
|
return False
|
||||||
|
|
||||||
|
# at this point there can only be zero or one create token account instructions
|
||||||
|
create_token_account_instruction = (
|
||||||
|
create_token_account_instructions[0]
|
||||||
|
if len(create_token_account_instructions) == 1
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if create_token_account_instruction is not None and (
|
||||||
|
create_token_account_instruction.spl_token[0] != base58.decode(token_program)
|
||||||
|
or create_token_account_instruction.token_mint[0] != token_mint
|
||||||
|
or create_token_account_instruction.associated_token_account[0] != token_account
|
||||||
|
):
|
||||||
|
# there are different token accounts, don't handle as predefined
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def try_confirm_token_transfer_transaction(
|
||||||
|
transaction: Transaction,
|
||||||
|
fee: int,
|
||||||
|
signer_path: list[int],
|
||||||
|
blockhash: bytes,
|
||||||
|
additional_info: SolanaTxAdditionalInfo | None = None,
|
||||||
|
) -> bool:
|
||||||
|
from .layout import confirm_token_transfer
|
||||||
|
from .token_account import try_get_token_account_base_address
|
||||||
|
|
||||||
|
if not is_predefined_token_transfer(
|
||||||
|
transaction.instructions,
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
transfer_token_instructions = get_token_transfer_instructions(
|
||||||
|
transaction.instructions
|
||||||
|
)
|
||||||
|
|
||||||
|
# in is_predefined_token_transfer we made sure that these values are the same
|
||||||
|
# for all the transfer token instructions
|
||||||
|
token_program = base58.decode(transfer_token_instructions[0].program_id)
|
||||||
|
token_mint = transfer_token_instructions[0].token_mint[0]
|
||||||
|
token_account = transfer_token_instructions[0].destination_account[0]
|
||||||
|
|
||||||
|
base_address = (
|
||||||
|
try_get_token_account_base_address(
|
||||||
|
token_account,
|
||||||
|
token_program,
|
||||||
|
token_mint,
|
||||||
|
additional_info.token_accounts_infos,
|
||||||
|
)
|
||||||
|
if additional_info is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
total_token_amount = sum(
|
||||||
|
[
|
||||||
|
transfer_token_instruction.amount
|
||||||
|
for transfer_token_instruction in transfer_token_instructions
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
await confirm_token_transfer(
|
||||||
|
token_account if base_address is None else base_address,
|
||||||
|
token_account,
|
||||||
|
token_mint,
|
||||||
|
total_token_amount,
|
||||||
|
transfer_token_instructions[0].decimals,
|
||||||
|
fee,
|
||||||
|
signer_path,
|
||||||
|
blockhash,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def try_confirm_predefined_transaction(
|
||||||
|
transaction: Transaction,
|
||||||
|
fee: int,
|
||||||
|
signer_path: list[int],
|
||||||
|
blockhash: bytes,
|
||||||
|
additional_info: SolanaTxAdditionalInfo | None = None,
|
||||||
|
) -> bool:
|
||||||
|
from .layout import confirm_system_transfer
|
||||||
|
from .transaction.instructions import SystemProgramTransferInstruction
|
||||||
|
|
||||||
|
instructions = transaction.instructions
|
||||||
|
instructions_count = len(instructions)
|
||||||
|
|
||||||
|
if instructions_count == 1:
|
||||||
|
if SystemProgramTransferInstruction.is_type_of(instructions[0]):
|
||||||
|
await confirm_system_transfer(instructions[0], fee, signer_path, blockhash)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return await try_confirm_token_transfer_transaction(
|
||||||
|
transaction, fee, signer_path, blockhash, additional_info
|
||||||
|
)
|
@ -0,0 +1,65 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from trezor.crypto import base58
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from trezor.messages import SolanaTxTokenAccountInfo
|
||||||
|
|
||||||
|
ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||||
|
|
||||||
|
SEED_CONSTANT = "ProgramDerivedAddress"
|
||||||
|
|
||||||
|
|
||||||
|
def assert_is_associated_token_account(
|
||||||
|
base_address: bytes,
|
||||||
|
token_account_address: bytes,
|
||||||
|
token_program: bytes,
|
||||||
|
token_mint: bytes,
|
||||||
|
) -> None:
|
||||||
|
from trezor.crypto.hashlib import sha256
|
||||||
|
|
||||||
|
# based on the following sources:
|
||||||
|
# https://spl.solana.com/associated-token-account#finding-the-associated-token-account-address
|
||||||
|
# https://github.com/solana-labs/solana/blob/8fbe033eaca693ed8c3e90b19bc3f61b32885e5e/sdk/program/src/pubkey.rs#L495
|
||||||
|
for seed_bump in range(255, 0, -1):
|
||||||
|
seed = (
|
||||||
|
base_address
|
||||||
|
+ token_program
|
||||||
|
+ token_mint
|
||||||
|
+ bytes([seed_bump])
|
||||||
|
+ base58.decode(ASSOCIATED_TOKEN_ACCOUNT_PROGRAM)
|
||||||
|
+ SEED_CONSTANT.encode("utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
|
account = sha256(seed).digest()
|
||||||
|
|
||||||
|
if account == token_account_address:
|
||||||
|
return
|
||||||
|
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
|
def try_get_token_account_base_address(
|
||||||
|
token_account_address: bytes,
|
||||||
|
token_program: bytes,
|
||||||
|
token_mint: bytes,
|
||||||
|
token_accounts_infos: list[SolanaTxTokenAccountInfo],
|
||||||
|
) -> bytes | None:
|
||||||
|
for token_account_info in token_accounts_infos:
|
||||||
|
if (
|
||||||
|
base58.decode(token_account_info.token_account) == token_account_address
|
||||||
|
and base58.decode(token_account_info.token_program) == token_program
|
||||||
|
and base58.decode(token_account_info.token_mint) == token_mint
|
||||||
|
):
|
||||||
|
base_address = base58.decode(token_account_info.base_address)
|
||||||
|
|
||||||
|
assert_is_associated_token_account(
|
||||||
|
base_address,
|
||||||
|
token_account_address,
|
||||||
|
token_program,
|
||||||
|
token_mint,
|
||||||
|
)
|
||||||
|
|
||||||
|
return base_address
|
||||||
|
|
||||||
|
return None
|
@ -0,0 +1,206 @@
|
|||||||
|
from common import *
|
||||||
|
|
||||||
|
from trezor.crypto import base58
|
||||||
|
|
||||||
|
from apps.solana.predefined_transaction import is_predefined_token_transfer
|
||||||
|
from apps.solana.transaction.instruction import Instruction
|
||||||
|
from apps.solana.transaction.instructions import (
|
||||||
|
ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
|
||||||
|
ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID_INS_CREATE,
|
||||||
|
SYSTEM_PROGRAM_ID,
|
||||||
|
SYSTEM_PROGRAM_ID_INS_TRANSFER,
|
||||||
|
TOKEN_2022_PROGRAM_ID,
|
||||||
|
TOKEN_2022_PROGRAM_ID_INS_TRANSFER_CHECKED,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
TOKEN_PROGRAM_ID_INS_TRANSFER_CHECKED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_mock_instruction(
|
||||||
|
program_id: str, instruction_id: int, parsed_data: dict[str, Any]
|
||||||
|
):
|
||||||
|
instruction = Instruction(
|
||||||
|
instruction_data=b"",
|
||||||
|
program_id=program_id,
|
||||||
|
accounts=[],
|
||||||
|
instruction_id=instruction_id,
|
||||||
|
property_templates=[],
|
||||||
|
accounts_template=[],
|
||||||
|
ui_properties=[],
|
||||||
|
ui_name="",
|
||||||
|
is_program_supported=True,
|
||||||
|
is_instruction_supported=True,
|
||||||
|
supports_multisig=False,
|
||||||
|
is_deprecated_warning=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
instruction.parsed_data = parsed_data
|
||||||
|
return instruction
|
||||||
|
|
||||||
|
|
||||||
|
def create_transfer_token_instruction(
|
||||||
|
program_id=TOKEN_PROGRAM_ID,
|
||||||
|
instruction_id=TOKEN_PROGRAM_ID_INS_TRANSFER_CHECKED,
|
||||||
|
token_mint="GHArwcWCuk9WkUG4XKUbt935rKfmBmywbEWyFxdH3mou",
|
||||||
|
destination_account="92YgwqTtTWB7qY92JT6mbL2WCmhAs7LPZL4jLcizNfwx",
|
||||||
|
owner="14CCvQzQzHCVgZM3j9soPnXuJXh1RmCfwLVUcdfbZVBS",
|
||||||
|
):
|
||||||
|
return create_mock_instruction(
|
||||||
|
program_id,
|
||||||
|
instruction_id,
|
||||||
|
{
|
||||||
|
"token_mint": (base58.decode(token_mint),),
|
||||||
|
"destination_account": (base58.decode(destination_account),),
|
||||||
|
"owner": (base58.decode(owner),),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_create_token_account_instruction(
|
||||||
|
token_mint="GHArwcWCuk9WkUG4XKUbt935rKfmBmywbEWyFxdH3mou",
|
||||||
|
associated_token_account="92YgwqTtTWB7qY92JT6mbL2WCmhAs7LPZL4jLcizNfwx",
|
||||||
|
spl_token=TOKEN_PROGRAM_ID,
|
||||||
|
):
|
||||||
|
return create_mock_instruction(
|
||||||
|
ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,
|
||||||
|
ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID_INS_CREATE,
|
||||||
|
{
|
||||||
|
"token_mint": (base58.decode(token_mint),),
|
||||||
|
"associated_token_account": (base58.decode(associated_token_account),),
|
||||||
|
"spl_token": (base58.decode(spl_token),),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
|
||||||
|
class TestSolanaPredefinedTransactions(unittest.TestCase):
|
||||||
|
def test_is_predefined_token_transfer(self):
|
||||||
|
# note: if there are multiple transfer instructions they are the same
|
||||||
|
# in the tests because that's the info the test cares about. In reality
|
||||||
|
# the instructions can differ in the destination account and amount.
|
||||||
|
valid_test_cases = [
|
||||||
|
[create_transfer_token_instruction()],
|
||||||
|
[create_transfer_token_instruction(), create_transfer_token_instruction()],
|
||||||
|
[
|
||||||
|
create_create_token_account_instruction(),
|
||||||
|
create_transfer_token_instruction(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
create_create_token_account_instruction(),
|
||||||
|
create_transfer_token_instruction(),
|
||||||
|
create_transfer_token_instruction(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
create_create_token_account_instruction(
|
||||||
|
spl_token=TOKEN_2022_PROGRAM_ID
|
||||||
|
),
|
||||||
|
create_transfer_token_instruction(
|
||||||
|
program_id=TOKEN_2022_PROGRAM_ID,
|
||||||
|
instruction_id=TOKEN_2022_PROGRAM_ID_INS_TRANSFER_CHECKED,
|
||||||
|
),
|
||||||
|
create_transfer_token_instruction(
|
||||||
|
program_id=TOKEN_2022_PROGRAM_ID,
|
||||||
|
instruction_id=TOKEN_2022_PROGRAM_ID_INS_TRANSFER_CHECKED,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
invalid_test_cases = [
|
||||||
|
# only create account
|
||||||
|
[
|
||||||
|
create_create_token_account_instruction(),
|
||||||
|
],
|
||||||
|
# there are other instructions
|
||||||
|
[
|
||||||
|
create_transfer_token_instruction(),
|
||||||
|
create_mock_instruction(
|
||||||
|
SYSTEM_PROGRAM_ID, SYSTEM_PROGRAM_ID_INS_TRANSFER, {}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
# multiple create account instructions
|
||||||
|
[
|
||||||
|
create_create_token_account_instruction(),
|
||||||
|
create_create_token_account_instruction(),
|
||||||
|
create_transfer_token_instruction(),
|
||||||
|
],
|
||||||
|
# transfer instructions program_id mismatch
|
||||||
|
[
|
||||||
|
create_transfer_token_instruction(
|
||||||
|
program_id=TOKEN_PROGRAM_ID,
|
||||||
|
instruction_id=TOKEN_PROGRAM_ID_INS_TRANSFER_CHECKED,
|
||||||
|
),
|
||||||
|
create_transfer_token_instruction(
|
||||||
|
program_id=TOKEN_2022_PROGRAM_ID,
|
||||||
|
instruction_id=TOKEN_2022_PROGRAM_ID_INS_TRANSFER_CHECKED,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
# transfer instructions token_mint mismatch
|
||||||
|
[
|
||||||
|
create_transfer_token_instruction(
|
||||||
|
token_mint="GHArwcWCuk9WkUG4XKUbt935rKfmBmywbEWyFxdH3mou"
|
||||||
|
),
|
||||||
|
create_transfer_token_instruction(
|
||||||
|
token_mint="GZDphoFQJ9m7uRU7TdS8cVDGFvsiQbcaY3n5mdoQHmDj"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
# transfer instructions destination mismatch
|
||||||
|
[
|
||||||
|
create_transfer_token_instruction(
|
||||||
|
destination_account="92YgwqTtTWB7qY92JT6mbL2WCmhAs7LPZL4jLcizNfwx"
|
||||||
|
),
|
||||||
|
create_transfer_token_instruction(
|
||||||
|
destination_account="74pZnim7gywyschy4MGkW6eZURv1DBXqwHTCqLRk63wz"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
# transfer instructions owner mismatch
|
||||||
|
[
|
||||||
|
create_transfer_token_instruction(
|
||||||
|
owner="14CCvQzQzHCVgZM3j9soPnXuJXh1RmCfwLVUcdfbZVBS"
|
||||||
|
),
|
||||||
|
create_transfer_token_instruction(
|
||||||
|
owner="BVRFH6vt5bNXub6WnnFRgaHFTcbkjBrf7x1troU1izGg"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
# token program mismatch
|
||||||
|
[
|
||||||
|
create_create_token_account_instruction(
|
||||||
|
spl_token=TOKEN_2022_PROGRAM_ID
|
||||||
|
),
|
||||||
|
create_transfer_token_instruction(
|
||||||
|
program_id=TOKEN_PROGRAM_ID,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
# create account token_mint mismatch
|
||||||
|
[
|
||||||
|
create_create_token_account_instruction(
|
||||||
|
token_mint="GZDphoFQJ9m7uRU7TdS8cVDGFvsiQbcaY3n5mdoQHmDj"
|
||||||
|
),
|
||||||
|
create_transfer_token_instruction(
|
||||||
|
token_mint="GHArwcWCuk9WkUG4XKUbt935rKfmBmywbEWyFxdH3mou",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
# create account associated_token_account mismatch
|
||||||
|
[
|
||||||
|
create_create_token_account_instruction(
|
||||||
|
associated_token_account="74pZnim7gywyschy4MGkW6eZURv1DBXqwHTCqLRk63wz"
|
||||||
|
),
|
||||||
|
create_transfer_token_instruction(
|
||||||
|
destination_account="92YgwqTtTWB7qY92JT6mbL2WCmhAs7LPZL4jLcizNfwx",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
# create account is not first
|
||||||
|
[
|
||||||
|
create_transfer_token_instruction(),
|
||||||
|
create_create_token_account_instruction(),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
for instructions in valid_test_cases:
|
||||||
|
self.assertTrue(is_predefined_token_transfer(instructions))
|
||||||
|
|
||||||
|
for instructions in invalid_test_cases:
|
||||||
|
self.assertFalse(is_predefined_token_transfer(instructions))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
Loading…
Reference in new issue