diff --git a/common/protob/messages-cardano.proto b/common/protob/messages-cardano.proto index a4f57ba9dd..20a605225b 100644 --- a/common/protob/messages-cardano.proto +++ b/common/protob/messages-cardano.proto @@ -536,6 +536,8 @@ message CardanoSignMessage { required CardanoDerivationType derivation_type = 3; required uint32 network_id = 4; // network id - mainnet or testnet optional CardanoAddressType address_type = 5; // one of the CardanoAddressType + optional uint32 protocol_magic = 6; // network's protocol magic - needed for Byron addresses on testnets + } /** diff --git a/common/protob/messages-stellar.proto b/common/protob/messages-stellar.proto index 8970a33002..293d48e880 100644 --- a/common/protob/messages-stellar.proto +++ b/common/protob/messages-stellar.proto @@ -59,6 +59,7 @@ message StellarSignTx { optional uint64 memo_id = 12; // 8-byte uint64 optional bytes memo_hash = 13; // 32 bytes representing a hash required uint32 num_operations = 14; // number of operations in this transaction + optional uint32 soroban_data_size = 60[default=0]; // soroban transaction // https://github.com/stellar/stellar-core/blob/02d26858069de7c0eefe065056fb0a19bf72ea56/src/xdr/Stellar-transaction.x#L506-L513 enum StellarMemoType { @@ -85,6 +86,7 @@ message StellarSignTx { * @next StellarAccountMergeOp * @next StellarManageDataOp * @next StellarBumpSequenceOp + * @next StellarInvokeHostFunctionOp */ message StellarTxOpRequest { } @@ -268,6 +270,44 @@ message StellarBumpSequenceOp { required uint64 bump_to = 2; // new sequence number } +/** + * Request: ask device to confirm this operation type + * @next StellarSorobanDataRequest + * @next StellarSignedTx + */ +message StellarInvokeHostFunctionOp { + optional string source_account = 1; // (optional) source account address + required string contract_address = 2; // contract id string + required string function_name = 3; // invoked contract function name (SCSymbol, max 32 bytes) + required uint32 call_args_xdr_size = 4; // the total size of call args xdr + required bytes call_args_xdr_initial_chunk = 5; // invokecontract call args xdr bytes + required uint32 soroban_auth_xdr_size = 6; + required bytes soroban_auth_xdr_initial_chunk = 7; // soroban authorization entries xdr +} + +/** + * Response: device is ready for client to send the soroban data + * @next StellarSorobanDataAck + */ +message StellarSorobanDataRequest { + required StellarRequestType type = 1; + required uint32 data_length = 2; // Number of bytes being requested (<= 1024) + + enum StellarRequestType { + CALL = 0; + AUTH = 1; + EXT = 2; + } +} + +/** + * Request: ask device to confirm + * @next StellarSignedTx + */ +message StellarSorobanDataAck { + required bytes data_chunk_xdr = 1; // the soroban data in xdr format +} + /** * Response: signature for transaction * @end diff --git a/common/protob/messages.proto b/common/protob/messages.proto index 0984ddca16..d153309370 100644 --- a/common/protob/messages.proto +++ b/common/protob/messages.proto @@ -296,6 +296,9 @@ enum MessageType { MessageType_StellarManageBuyOfferOp = 222 [(wire_in) = true]; MessageType_StellarPathPaymentStrictSendOp = 223 [(wire_in) = true]; MessageType_StellarSignedTx = 230 [(wire_out) = true]; + MessageType_StellarInvokeHostFunctionOp = 260 [(wire_in) = true]; + MessageType_StellarSorobanDataRequest = 261 [(wire_out) = true]; + MessageType_StellarSorobanDataAck = 262 [(wire_in) = true]; // Cardano // dropped Sign/VerifyMessage ids 300-302 diff --git a/core/embed/firmware/version.h b/core/embed/firmware/version.h index 2ebaaaabf0..6a9213e6a5 100644 --- a/core/embed/firmware/version.h +++ b/core/embed/firmware/version.h @@ -9,7 +9,7 @@ #define FIX_VERSION_BUILD VERSION_BUILD #define ONEKEY_VERSION_MAJOR 4 -#define ONEKEY_VERSION_MINOR 19 +#define ONEKEY_VERSION_MINOR 20 #define ONEKEY_VERSION_PATCH 0 #define ONEKEY_VERSION_BUILD 0 diff --git a/core/src/all_modules.py b/core/src/all_modules.py index ab62fd6888..cd9ea4804b 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -1129,6 +1129,8 @@ import trezor.enums.StellarAssetType trezor.enums.StellarMemoType import trezor.enums.StellarMemoType + trezor.enums.StellarRequestType + import trezor.enums.StellarRequestType trezor.enums.StellarSignerType import trezor.enums.StellarSignerType trezor.enums.TezosBallotType diff --git a/core/src/apps/base.py b/core/src/apps/base.py index 2b720f88d1..a30115eba8 100644 --- a/core/src/apps/base.py +++ b/core/src/apps/base.py @@ -822,7 +822,9 @@ def boot() -> None: ) workflow_handlers.register(MessageType.UnLockDevice, handle_UnLockDevice) - reload_settings_from_storage() + reload_settings_from_storage( + timeout_ms=10 * 1000 if utils.is_rest_by_usb_lock() else None + ) from trezor.lvglui.scrs import fingerprints if config.is_unlocked() and fingerprints.is_unlocked(): diff --git a/core/src/apps/cardano/sign_message.py b/core/src/apps/cardano/sign_message.py index e5f7251141..d39daf2b22 100644 --- a/core/src/apps/cardano/sign_message.py +++ b/core/src/apps/cardano/sign_message.py @@ -8,7 +8,6 @@ from . import seed from .addresses import assert_params_cond -from .helpers.paths import SCHEMA_STAKING_ANY_ACCOUNT if TYPE_CHECKING: from trezor.wire import Context @@ -22,7 +21,8 @@ async def sign_message( from trezor.messages import CardanoMessageSignature, CardanoAddressParametersType from trezor.enums import CardanoAddressType from apps.common import paths - from .helpers.paths import SCHEMA_MINT, SCHEMA_PAYMENT + from .helpers.paths import SCHEMA_PAYMENT, SCHEMA_STAKING_ANY_ACCOUNT + from trezor.crypto.curve import ed25519 from trezor import wire from .helpers import network_ids, protocol_magics @@ -38,10 +38,10 @@ async def sign_message( msg.address_n, True, # path must match the PUBKEY schema - (SCHEMA_PAYMENT.match(msg.address_n) or SCHEMA_MINT.match(msg.address_n)), + SCHEMA_PAYMENT.match(msg.address_n), ) - if msg.network_id != network_ids.MAINNET: - raise wire.ProcessError("Invalid Networ ID") + if msg.protocol_magic is None and (msg.network_id != network_ids.MAINNET): + raise wire.ProcessError("Invalid Network id, need protocol magic provide") address_type = msg.address_type if msg.address_type else CardanoAddressType.BASE address_n = msg.address_n @@ -70,7 +70,7 @@ async def sign_message( script_payment_hash=None, script_staking_hash=None, ), - protocol_magics.MAINNET, + protocol_magics.MAINNET if msg.protocol_magic is None else msg.protocol_magic, msg.network_id, ) address = addresses.encode_human_readable(address_bytes) diff --git a/core/src/apps/ethereum/onekey/sign_typed_data.py b/core/src/apps/ethereum/onekey/sign_typed_data.py index a612270019..974084ce89 100644 --- a/core/src/apps/ethereum/onekey/sign_typed_data.py +++ b/core/src/apps/ethereum/onekey/sign_typed_data.py @@ -593,4 +593,5 @@ async def confirm_domain(ctx: Context, typed_data_envelope: TypedDataEnvelope) - eip712_domain[member.name] = value from ..layout import confirm_domain - await confirm_domain(ctx, eip712_domain) + if eip712_domain: + await confirm_domain(ctx, eip712_domain) diff --git a/core/src/apps/solana/sign_tx.py b/core/src/apps/solana/sign_tx.py index 48f784a520..f1fb79e4f9 100644 --- a/core/src/apps/solana/sign_tx.py +++ b/core/src/apps/solana/sign_tx.py @@ -67,7 +67,8 @@ async def sign_tx( print( f"Invalid signer used: {PublicKey(fee_payer.get())} != {PublicKey(signer_pub_key_bytes)}" ) - raise wire.DataError("Invalid signer used") + else: + raise wire.DataError("Invalid signer used") else: if PublicKey(signer_pub_key_bytes) not in accounts_keys[:sigs_count]: raise wire.DataError("Invalid transaction params") diff --git a/core/src/apps/solana/spl/spl_token_program.py b/core/src/apps/solana/spl/spl_token_program.py index 8eee613da2..da4b695c55 100644 --- a/core/src/apps/solana/spl/spl_token_program.py +++ b/core/src/apps/solana/spl/spl_token_program.py @@ -437,7 +437,7 @@ async def parse(ctx: wire.Context, accounts: list[PublicKey], data: bytes) -> No from ..constents import SPL_TOKEN_PROGRAM_ID owner_address = None - if hasattr(ctx, "extra"): + if ctx.extra is not None: owner_address = try_get_token_account_owner_address( params.dest.get(), SPL_TOKEN_PROGRAM_ID.get(), diff --git a/core/src/apps/stellar/consts.py b/core/src/apps/stellar/consts.py index 89b5d279c1..2e9d069dd0 100644 --- a/core/src/apps/stellar/consts.py +++ b/core/src/apps/stellar/consts.py @@ -20,6 +20,7 @@ StellarPathPaymentStrictSendOp, StellarPaymentOp, StellarSetOptionsOp, + StellarInvokeHostFunctionOp, ) StellarMessageType = ( @@ -36,6 +37,7 @@ | StellarPathPaymentStrictSendOp | StellarPaymentOp | StellarSetOptionsOp + | StellarInvokeHostFunctionOp ) @@ -57,6 +59,7 @@ MessageType.StellarPathPaymentStrictSendOp: 13, MessageType.StellarPaymentOp: 1, MessageType.StellarSetOptionsOp: 5, + MessageType.StellarInvokeHostFunctionOp: 24, } op_wire_types = [ @@ -73,6 +76,7 @@ MessageType.StellarPathPaymentStrictSendOp, MessageType.StellarPaymentOp, MessageType.StellarSetOptionsOp, + MessageType.StellarInvokeHostFunctionOp, ] # https://github.com/stellar/go/blob/e0ffe19f58879d3c31e2976b97a5bf10e13a337b/xdr/xdr_generated.go#L584 @@ -93,6 +97,12 @@ FLAG_AUTH_REVOCABLE = const(2) FLAG_AUTH_IMMUTABLE = const(4) FLAGS_MAX_SIZE = const(7) +STELLAR_KEY_TYPE_ED25519 = const(0) +STELLAR_KEY_TYPE_CONTRACT = const(1) +STELLAR_STRKEY_VERSION_CONTRACT = const(0x10) +STELLAR_STRKEY_VERSION_ED25519_PUBLIC_KEY = const(0x30) +STELLAR_HOST_FUNCTION_TYPE_INVOKE_CONTRACT = const(0) +STELLAR_TX_EXT_SOROBAN = const(1) def get_op_code(msg: protobuf.MessageType) -> int: diff --git a/core/src/apps/stellar/helpers.py b/core/src/apps/stellar/helpers.py index fe2033695b..4bdc616464 100644 --- a/core/src/apps/stellar/helpers.py +++ b/core/src/apps/stellar/helpers.py @@ -3,15 +3,35 @@ from trezor.crypto import base32 from trezor.wire import ProcessError +from . import consts + + +class InvokeHostFunctionOpSummary: + def __init__( + self, + contract_address: str, + function_name: str, + call_args_hash: bytes, + soroban_auth_hash: bytes, + soroban_tx_ext_hash: bytes, + ) -> None: + self.contract_address = contract_address + self.function_name = function_name + self.call_args_hash = call_args_hash + self.soroban_auth_hash = soroban_auth_hash + self.soroban_tx_ext_hash = soroban_tx_ext_hash + def public_key_from_address(address: str) -> bytes: - """Extracts public key from an address - Stellar address is in format: - <1-byte version> <32-bytes ed25519 public key> <2-bytes CRC-16 checksum> - """ - b = base32.decode(address) - _crc16_checksum_verify(b[:-2], b[-2:]) - return b[1:-2] + return _raw_payload_from_address( + address, version=consts.STELLAR_STRKEY_VERSION_ED25519_PUBLIC_KEY + ) + + +def contract_id_from_address(c_address: str) -> bytes: + return _raw_payload_from_address( + c_address, version=consts.STELLAR_STRKEY_VERSION_CONTRACT + ) def address_from_public_key(pubkey: bytes) -> str: @@ -48,3 +68,15 @@ def _crc16_checksum(data: bytes) -> bytes: crc ^= polynomial return ustruct.pack(" bytes: + """Extracts raw payload from an address + Stellar address is in format: + <1-byte version> <32-bytes raw payload> <2-bytes CRC-16 checksum> + """ + b = base32.decode(address) + if b[0] != version: + raise ProcessError("Invalid address version") + _crc16_checksum_verify(b[:-2], b[-2:]) + return b[1:-2] diff --git a/core/src/apps/stellar/operations/__init__.py b/core/src/apps/stellar/operations/__init__.py index 60315aa886..187d81b52e 100644 --- a/core/src/apps/stellar/operations/__init__.py +++ b/core/src/apps/stellar/operations/__init__.py @@ -9,8 +9,11 @@ async def process_operation( - ctx: Context, w: Writer, op: consts.StellarMessageType + ctx: Context, w: Writer, op: consts.StellarMessageType, soroban_data_size: int = 0 ) -> None: + if soroban_data_size > 0: + if not serialize.StellarInvokeHostFunctionOp.is_type_of(op): + raise ValueError("Stellar: unexpected operation for soroban transaction") if op.source_account: await layout.confirm_source_account(ctx, op.source_account) serialize.write_account(w, op.source_account) @@ -54,5 +57,10 @@ async def process_operation( elif serialize.StellarSetOptionsOp.is_type_of(op): await layout.confirm_set_options_op(ctx, op) serialize.write_set_options_op(w, op) + elif serialize.StellarInvokeHostFunctionOp.is_type_of(op): + summary = await serialize.write_invoke_host_function_op( + ctx, w, op, soroban_data_size + ) + await layout.confirm_invoke_host_function_op(ctx, summary) else: raise ValueError("Unknown operation") diff --git a/core/src/apps/stellar/operations/layout.py b/core/src/apps/stellar/operations/layout.py index 02bb25d445..ce686978bb 100644 --- a/core/src/apps/stellar/operations/layout.py +++ b/core/src/apps/stellar/operations/layout.py @@ -29,6 +29,7 @@ from trezor.wire import DataError, ProcessError from .. import consts, helpers +from ..helpers import InvokeHostFunctionOpSummary from ..layout import format_amount, format_asset if TYPE_CHECKING: @@ -343,3 +344,23 @@ async def confirm_asset_issuer(ctx: Context, asset: StellarAsset) -> None: description=f"{asset.code} issuer:", br_type="confirm_asset_issuer", ) + + +async def confirm_invoke_host_function_op( + ctx: Context, summary: InvokeHostFunctionOpSummary +) -> None: + from trezor.lvglui.i18n import gettext as _, keys as i18n_keys + + await confirm_properties( + ctx, + "op_invoke_host_function", + "Invoke Contract", + props=( + ("Contract", summary.contract_address), + ("Function", summary.function_name), + ("Args Hash", summary.call_args_hash), + ("Auths Hash", summary.soroban_auth_hash), + ("Ext Hash", summary.soroban_tx_ext_hash), + ), + warning_banner_text=_(i18n_keys.SECURITY__SOLANA_RAW_SIGNING_TX_WARNING), + ) diff --git a/core/src/apps/stellar/operations/serialize.py b/core/src/apps/stellar/operations/serialize.py index 517c85ca1a..ca82404e72 100644 --- a/core/src/apps/stellar/operations/serialize.py +++ b/core/src/apps/stellar/operations/serialize.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING -from trezor.enums import StellarAssetType +from trezor.crypto import hashlib +from trezor.enums import StellarAssetType, StellarRequestType from trezor.messages import ( StellarAccountMergeOp, StellarAllowTrustOp, @@ -9,6 +10,7 @@ StellarChangeTrustOp, StellarCreateAccountOp, StellarCreatePassiveSellOfferOp, + StellarInvokeHostFunctionOp, StellarManageBuyOfferOp, StellarManageDataOp, StellarManageSellOfferOp, @@ -16,10 +18,14 @@ StellarPathPaymentStrictSendOp, StellarPaymentOp, StellarSetOptionsOp, + StellarSorobanDataAck, + StellarSorobanDataRequest, ) -from trezor.wire import DataError, ProcessError +from trezor.utils import HashWriter +from trezor.wire import Context, DataError, ProcessError -from .. import writers +from .. import consts, writers +from ..helpers import InvokeHostFunctionOpSummary if TYPE_CHECKING: from trezor.utils import Writer @@ -165,6 +171,100 @@ def write_set_options_op(w: Writer, msg: StellarSetOptionsOp) -> None: writers.write_uint32(w, msg.signer_weight) +async def write_invoke_host_function_op( + ctx: Context, w: Writer, msg: StellarInvokeHostFunctionOp, soroban_data_size: int +) -> InvokeHostFunctionOpSummary: + writers.write_uint32(w, consts.STELLAR_HOST_FUNCTION_TYPE_INVOKE_CONTRACT) + writers.write_contract_id(w, msg.contract_address) + if not msg.function_name: + raise DataError("Stellar: Contract function name can not be empty") + if len(msg.call_args_xdr_initial_chunk) == 0: + raise DataError("Stellar: Contract function args can not be empty") + if len(msg.soroban_auth_xdr_initial_chunk) == 0: + raise DataError("Stellar: Contract authorization can not be empty") + writers.write_string(w, msg.function_name) + + call_args_hash_writer = HashWriter(hashlib.sha256()) + await _write_soroban_xdr( + ctx, + w, + StellarRequestType.CALL, + msg.call_args_xdr_size, + msg.call_args_xdr_initial_chunk, + call_args_hash_writer, + ) + + soroban_auth_hash_writer = HashWriter(hashlib.sha256()) + await _write_soroban_xdr( + ctx, + w, + StellarRequestType.AUTH, + msg.soroban_auth_xdr_size, + msg.soroban_auth_xdr_initial_chunk, + soroban_auth_hash_writer, + ) + + soroban_tx_ext_hash_writer = HashWriter(hashlib.sha256()) + writers.write_uint32(w, consts.STELLAR_TX_EXT_SOROBAN) + await _write_soroban_xdr( + ctx, + w, + StellarRequestType.EXT, + soroban_data_size, + b"", + soroban_tx_ext_hash_writer, + ) + + return InvokeHostFunctionOpSummary( + msg.contract_address, + msg.function_name, + call_args_hash_writer.get_digest(), + soroban_auth_hash_writer.get_digest(), + soroban_tx_ext_hash_writer.get_digest(), + ) + + +async def _write_soroban_xdr( + ctx: Context, + w: Writer, + request_type: StellarRequestType, + total_size: int, + initial_chunk: bytes, + hash_writer: HashWriter, +) -> None: + if len(initial_chunk) > total_size: + raise DataError("Stellar: soroban chunk exceeds declared size") + if initial_chunk: + writers.write_bytes_unchecked(w, initial_chunk) + hash_writer.write(initial_chunk) + received = len(initial_chunk) + + while received < total_size: + ack = await _send_chunk_request(ctx, request_type, total_size - received) + chunk = ack.data_chunk_xdr + if not chunk: + raise DataError("Stellar: empty soroban chunk") + received += len(chunk) + if received > total_size: + raise DataError("Stellar: soroban chunk exceeds declared size") + writers.write_bytes_unchecked(w, chunk) + hash_writer.write(chunk) + + if received != total_size: + raise DataError("Stellar: soroban chunk size mismatch") + + +async def _send_chunk_request( + ctx: Context, request_type: StellarRequestType, data_left: int +) -> StellarSorobanDataAck: + if data_left <= 0: + raise ValueError("Invalid soroban chunk request size") + req = StellarSorobanDataRequest( + type=request_type, data_length=data_left if data_left <= 1024 else 1024 + ) + return await ctx.call(req, StellarSorobanDataAck) + + def _write_set_options_int(w: Writer, value: int | None) -> None: if value is None: writers.write_bool(w, False) diff --git a/core/src/apps/stellar/sign_tx.py b/core/src/apps/stellar/sign_tx.py index 156567cc95..aba8c9e7ef 100644 --- a/core/src/apps/stellar/sign_tx.py +++ b/core/src/apps/stellar/sign_tx.py @@ -7,6 +7,7 @@ from trezor.lvglui.scrs import lv from trezor.messages import StellarSignedTx, StellarSignTx, StellarTxOpRequest from trezor.ui.layouts import confirm_final +from trezor.utils import HashWriter from trezor.wire import DataError, ProcessError from apps.common import paths, seed @@ -33,26 +34,36 @@ async def sign_tx( if msg.num_operations == 0: raise ProcessError("Stellar: At least one operation is required") - - w = bytearray() + is_soroban_tx = msg.soroban_data_size > 0 + if is_soroban_tx: + if msg.num_operations != 1: + raise ProcessError( + "Stellar: Soroban transaction must have exactly one operation" + ) + if msg.memo_type != StellarMemoType.NONE: + raise ProcessError("Stellar: Soroban transaction must have no memo") + w = HashWriter(sha256()) ctx.primary_color, ctx.icon_path = lv.color_hex(PRIMARY_COLOR), ICON await _init(ctx, w, pubkey, msg) await _timebounds(ctx, w, msg.timebounds_start, msg.timebounds_end) await _memo(ctx, w, msg) - await _operations(ctx, w, msg.num_operations) - await _final(ctx, w, msg) + await _operations(ctx, w, msg.num_operations, msg.soroban_data_size) + await _final(ctx, w, msg, is_soroban_tx) # sign - digest = sha256(w).digest() + digest = w.get_digest() signature = ed25519.sign(node.private_key(), digest) await confirm_final(ctx, "XLM") # Add the public key for verification that the right account was used for signing return StellarSignedTx(public_key=pubkey, signature=signature) -async def _final(ctx: Context, w: Writer, msg: StellarSignTx) -> None: - # 4 null bytes representing a (currently unused) empty union - writers.write_uint32(w, 0) +async def _final( + ctx: Context, w: Writer, msg: StellarSignTx, is_soroban_tx: bool = False +) -> None: + if not is_soroban_tx: + # 4 null bytes representing a (currently unused) empty unioin for non-soroban legacy transactions + writers.write_uint32(w, 0) # final confirm await layout.require_confirm_final(ctx, msg.fee, msg.num_operations) @@ -85,15 +96,19 @@ async def _timebounds(ctx: Context, w: Writer, start: int, end: int) -> None: writers.write_uint64(w, end) -async def _operations(ctx: Context, w: Writer, num_operations: int) -> None: +async def _operations( + ctx: Context, w: Writer, num_operations: int, soroban_data_size: int = 0 +) -> None: writers.write_uint32(w, num_operations) for _ in range(num_operations): op = await ctx.call_any(StellarTxOpRequest(), *consts.op_wire_types) - await process_operation(ctx, w, op) # type: ignore [Argument of type "MessageType" cannot be assigned to parameter "op" of type "StellarMessageType" in function "process_operation"] + await process_operation(ctx, w, op, soroban_data_size) # type: ignore [Argument of type "MessageType" cannot be assigned to parameter "op" of type "StellarMessageType" in function "process_operation"] async def _memo(ctx: Context, w: Writer, msg: StellarSignTx) -> None: writers.write_uint32(w, msg.memo_type) + if msg.soroban_data_size > 0: + return if msg.memo_type == StellarMemoType.NONE: # nothing is serialized memo_confirm_text = "" diff --git a/core/src/apps/stellar/writers.py b/core/src/apps/stellar/writers.py index f57538eba0..2809e46222 100644 --- a/core/src/apps/stellar/writers.py +++ b/core/src/apps/stellar/writers.py @@ -7,7 +7,8 @@ write_uint64_be, ) -from .helpers import public_key_from_address +from . import consts +from .helpers import contract_id_from_address, public_key_from_address write_uint32 = write_uint32_be write_uint64 = write_uint64_be @@ -40,6 +41,14 @@ def write_bool(w: Writer, val: bool) -> None: def write_pubkey(w: Writer, address: str) -> None: - # first 4 bytes of an address are the type, there's only one type (0) - write_uint32(w, 0) + write_uint32(w, consts.STELLAR_KEY_TYPE_ED25519) write_bytes_fixed(w, public_key_from_address(address), 32) + + +def write_contract_id(w: Writer, contract_address: str) -> None: + write_uint32(w, consts.STELLAR_KEY_TYPE_CONTRACT) + write_bytes_fixed( + w, + contract_id_from_address(contract_address), + 32, + ) diff --git a/core/src/trezor/enums/MessageType.py b/core/src/trezor/enums/MessageType.py index 6a600d5c2c..6f41ddb4b4 100644 --- a/core/src/trezor/enums/MessageType.py +++ b/core/src/trezor/enums/MessageType.py @@ -196,6 +196,9 @@ StellarManageBuyOfferOp = 222 StellarPathPaymentStrictSendOp = 223 StellarSignedTx = 230 + StellarInvokeHostFunctionOp = 260 + StellarSorobanDataRequest = 261 + StellarSorobanDataAck = 262 CardanoGetPublicKey = 305 CardanoPublicKey = 306 CardanoGetAddress = 307 diff --git a/core/src/trezor/enums/StellarRequestType.py b/core/src/trezor/enums/StellarRequestType.py new file mode 100644 index 0000000000..d7202d004a --- /dev/null +++ b/core/src/trezor/enums/StellarRequestType.py @@ -0,0 +1,7 @@ +# Automatically generated by pb2py +# fmt: off +# isort:skip_file + +CALL = 0 +AUTH = 1 +EXT = 2 diff --git a/core/src/trezor/enums/__init__.py b/core/src/trezor/enums/__init__.py index 0de1a0b579..30caf210cb 100644 --- a/core/src/trezor/enums/__init__.py +++ b/core/src/trezor/enums/__init__.py @@ -203,6 +203,9 @@ class MessageType(IntEnum): StellarManageBuyOfferOp = 222 StellarPathPaymentStrictSendOp = 223 StellarSignedTx = 230 + StellarInvokeHostFunctionOp = 260 + StellarSorobanDataRequest = 261 + StellarSorobanDataAck = 262 CardanoGetPublicKey = 305 CardanoPublicKey = 306 CardanoGetAddress = 307 @@ -762,6 +765,11 @@ class StellarSignerType(IntEnum): PRE_AUTH = 1 HASH = 2 + class StellarRequestType(IntEnum): + CALL = 0 + AUTH = 1 + EXT = 2 + class TezosContractType(IntEnum): Implicit = 0 Originated = 1 diff --git a/core/src/trezor/lvglui/i18n/keys.py b/core/src/trezor/lvglui/i18n/keys.py index 87d43dc9a2..8aedccb041 100644 --- a/core/src/trezor/lvglui/i18n/keys.py +++ b/core/src/trezor/lvglui/i18n/keys.py @@ -2165,7 +2165,7 @@ OVERVIEW = 982 # Contract address CONTRACT_ADDRESS = 983 -# Token address +# Token Address TOKEN_ADDRESS = 984 # Approve to APPROVE_PROVIDER = 985 @@ -2246,7 +2246,7 @@ FIELDS_REVOKE_ON_NETWORK = 1019 # Understand BUTTON_UNDERSTAND = 1020 -# 3 failed attempts. Slide to continue. You do not have Passphrase turned on. +# 3 failed tries. Slide to continue. You do not have Passphrase turned on. CONTENT__STR_FAILED_TRIES_SLIDE_TO_CONTINUE_DISABLE_PASSPHRASE = 1021 # Disabling the Passphrase will prevent you from using the hidden wallet PIN t # o unlock your device. @@ -2367,4 +2367,8 @@ BUTTON__OPEN = 1071 # Set Brightness TITLE__SET_BRIGHTNESS = 1072 +# Select Address +TITLE_SELECT_ADDRESS = 1073 +# Go To Address +TITLE_GO_TO_ADDRESS = 1074 # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/de.py b/core/src/trezor/lvglui/i18n/locales/de.py index 8eafc22b82..5ceb9d3700 100644 --- a/core/src/trezor/lvglui/i18n/locales/de.py +++ b/core/src/trezor/lvglui/i18n/locales/de.py @@ -1073,5 +1073,7 @@ "Anwenden", "Öffnen", "Helligkeit einstellen", + "Adresse auswählen", + "Gehe zu Adresse", ] # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/en.py b/core/src/trezor/lvglui/i18n/locales/en.py index 08fedd32cd..e56dc8d62c 100644 --- a/core/src/trezor/lvglui/i18n/locales/en.py +++ b/core/src/trezor/lvglui/i18n/locales/en.py @@ -984,7 +984,7 @@ "Approve {token} for {name}", "Overview", "Contract address", - "Token address", + "Token Address", "Approve to", "Approve unlimited {token} for {name}", "This action grants the contract unlimited access to this asset. Trust the dApp before proceeding.", @@ -1021,7 +1021,7 @@ "This authorization delegates authority to a smart contract not included on the whitelist.", "Revoke on Network", "Understand", - "3 failed attempts. Slide to continue. You do not have Passphrase turned on.", + "3 failed tries. Slide to continue. You do not have Passphrase turned on.", "Disabling the Passphrase will prevent you from using the hidden wallet PIN to unlock your device.", "Touch", "Home Screen", @@ -1073,5 +1073,7 @@ "Apply", "Open", "Set Brightness", + "Select Address", + "Go To Address", ] # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/es.py b/core/src/trezor/lvglui/i18n/locales/es.py index 1e1503156e..b3f9d879be 100644 --- a/core/src/trezor/lvglui/i18n/locales/es.py +++ b/core/src/trezor/lvglui/i18n/locales/es.py @@ -1073,5 +1073,7 @@ "Aplicar", "Abrir", "Establecer brillo", + "Seleccione la dirección", + "Ir a la dirección", ] # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/fr.py b/core/src/trezor/lvglui/i18n/locales/fr.py index 72caf68468..6b1d4a382e 100644 --- a/core/src/trezor/lvglui/i18n/locales/fr.py +++ b/core/src/trezor/lvglui/i18n/locales/fr.py @@ -1073,5 +1073,7 @@ "Appliquer", "Ouvrir", "Régler la luminosité", + "Sélectionner l'adresse", + "Aller à l'adresse", ] # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/it.py b/core/src/trezor/lvglui/i18n/locales/it.py index 94c1a1e657..cf48a52afb 100644 --- a/core/src/trezor/lvglui/i18n/locales/it.py +++ b/core/src/trezor/lvglui/i18n/locales/it.py @@ -1073,5 +1073,7 @@ "Fare domanda a", "Apri", "Imposta luminosità", + "Seleziona l'indirizzo", + "Vai all'indirizzo", ] # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/ja.py b/core/src/trezor/lvglui/i18n/locales/ja.py index 64c80e4c06..eba8bb2bb8 100644 --- a/core/src/trezor/lvglui/i18n/locales/ja.py +++ b/core/src/trezor/lvglui/i18n/locales/ja.py @@ -1073,5 +1073,7 @@ "適用する", "開く", "明るさを設定する", + "住所を選択", + "アドレスへ移動", ] # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/ko.py b/core/src/trezor/lvglui/i18n/locales/ko.py index 5fe8741295..eff6d697b7 100644 --- a/core/src/trezor/lvglui/i18n/locales/ko.py +++ b/core/src/trezor/lvglui/i18n/locales/ko.py @@ -1073,5 +1073,7 @@ "적용하다", "열기", "밝기 설정", + "주소를 선택하세요", + "주소로 이동", ] # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/pt_br.py b/core/src/trezor/lvglui/i18n/locales/pt_br.py index bc3645de72..af7a274572 100644 --- a/core/src/trezor/lvglui/i18n/locales/pt_br.py +++ b/core/src/trezor/lvglui/i18n/locales/pt_br.py @@ -1073,5 +1073,7 @@ "Aplicar", "Abrir", "Definir Brilho", + "Selecione o endereço", + "Ir para o endereço", ] # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/ru.py b/core/src/trezor/lvglui/i18n/locales/ru.py index 9089a8a39c..17e39aec3e 100644 --- a/core/src/trezor/lvglui/i18n/locales/ru.py +++ b/core/src/trezor/lvglui/i18n/locales/ru.py @@ -1073,5 +1073,7 @@ "Применять", "Открыть", "Установить яркость", + "Выберите адрес", + "Перейти к адресу", ] # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/zh_cn.py b/core/src/trezor/lvglui/i18n/locales/zh_cn.py index e18940eabd..c3af72d708 100644 --- a/core/src/trezor/lvglui/i18n/locales/zh_cn.py +++ b/core/src/trezor/lvglui/i18n/locales/zh_cn.py @@ -1073,5 +1073,7 @@ "应用", "开启", "设置亮度", + "选择地址", + "前往地址", ] # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/zh_hk.py b/core/src/trezor/lvglui/i18n/locales/zh_hk.py index 4de5afb47f..e5364a1f59 100644 --- a/core/src/trezor/lvglui/i18n/locales/zh_hk.py +++ b/core/src/trezor/lvglui/i18n/locales/zh_hk.py @@ -1073,5 +1073,7 @@ "应用", "開啟", "設定亮度", + "選擇地址", + "前往地址", ] # fmt: on diff --git a/core/src/trezor/lvglui/scrs/address.py b/core/src/trezor/lvglui/scrs/address.py index bbed5aa3ba..fe4d0bace8 100644 --- a/core/src/trezor/lvglui/scrs/address.py +++ b/core/src/trezor/lvglui/scrs/address.py @@ -25,6 +25,7 @@ def __init__(self): self.current_handler = None self.current_chain_info = None self.current_index = 0 + self.btc_address_index = 0 self.btc_script_type = None self.use_standard_derivation = True self.user_interaction = None @@ -61,6 +62,7 @@ def _prepare_btc_message(self): self.addr_type = "Taproot" pos = self.current_chain_info["index_pos"] path[pos] += self.current_index + path[-1] += self.btc_address_index msg_class = getattr(messages, self.current_chain_info["msg_class"]) return msg_class( @@ -327,7 +329,6 @@ async def generate_address(self, name, index=0, ctx=wire.DUMMY_CONTEXT): state = self.STATE.INIT while state not in (self.STATE.FINISH, self.STATE.ERROR): - # pyright: off if state == self.STATE.INIT: chain_info = self.get_chain_info(name) @@ -370,6 +371,7 @@ async def generate_address(self, name, index=0, ctx=wire.DUMMY_CONTEXT): address=address, network=self.current_chain_info["name"], addr_type=self.addr_type, + address_index=self.btc_address_index, account_name=f" Account #{self.current_index + 1}", ) state = self.STATE.HANDLE_RESPONSE @@ -390,6 +392,12 @@ async def generate_address(self, name, index=0, ctx=wire.DUMMY_CONTEXT): ): self.btc_script_type = value state = self.STATE.SHOW_ADDRESS + elif ( + return_type + == ADDRESS_OFFLINE_RETURN_TYPE.BTC_ADDRESS_INDEX_CHANGED + ): + self.btc_address_index = value + state = self.STATE.SHOW_ADDRESS elif self.user_interaction == ADDRESS_OFFLINE_RETURN_TYPE.DONE: state = self.STATE.FINISH else: @@ -406,6 +414,7 @@ def cleanup(self): self.user_interaction = None self.addr_type = None self.current_index = 0 + self.btc_address_index = 0 self.btc_script_type = None self.use_standard_derivation = True self.addr_type = None diff --git a/core/src/trezor/lvglui/scrs/components/listitem.py b/core/src/trezor/lvglui/scrs/components/listitem.py index ffb24afa1b..03de21aa81 100644 --- a/core/src/trezor/lvglui/scrs/components/listitem.py +++ b/core/src/trezor/lvglui/scrs/components/listitem.py @@ -195,7 +195,7 @@ def __init__( class CardHeader(lv.obj): - def __init__(self, parent, title, icon): + def __init__(self, parent, title, icon, clickable=False): super().__init__(parent) self.remove_style_all() self.set_size(456, 63) @@ -217,6 +217,11 @@ def __init__(self, parent, title, icon): self.icon.set_src(icon) self.icon.set_size(32, 32) self.icon.align(lv.ALIGN.TOP_LEFT, 0, 0) + if clickable: + self.right_arrow = lv.img(self) + self.right_arrow.set_src("A:/res/arrow-right.png") + self.right_arrow.set_size(32, 32) + self.right_arrow.align(lv.ALIGN.TOP_RIGHT, 0, 0) self.label = lv.label(self) self.label.set_text(title) self.label.set_size(360, lv.SIZE.CONTENT) diff --git a/core/src/trezor/lvglui/scrs/homescreen.py b/core/src/trezor/lvglui/scrs/homescreen.py index d13fd71d9a..462f7ab8f7 100644 --- a/core/src/trezor/lvglui/scrs/homescreen.py +++ b/core/src/trezor/lvglui/scrs/homescreen.py @@ -53,6 +53,7 @@ create_top_mask, refresh_preview_device_labels, ) +from .template import IndexSelectionScreen from .widgets.style import StyleWrapper _attach_to_pin_task_running = False @@ -1928,7 +1929,7 @@ def init_ui(self): align=lv.ALIGN.TOP_RIGHT, ) - # Account button + # Account indicator self.index_btn = ListItemBtn( self.content_area, f" Account #{self.current_index + 1}", @@ -1950,16 +1951,15 @@ def init_ui(self): # Initialize variables self.chains = chains_brief_info() - self.visible_chains_count = 8 + # self.visible_chains_count = 8 - self.is_expanded = False + # self.is_expanded = False self.chain_buttons = [] - self.created_count = 0 + # self.created_count = 0 self.current_page = 0 self.items_per_page = 5 self.max_pages = 0 - self.chain_buttons = [] for _i in range(self.items_per_page): btn = ListItemBtn( self.container, @@ -1968,23 +1968,11 @@ def init_ui(self): min_height=87, pad_ver=5, ) - self.chain_buttons.append(btn) btn.add_flag(lv.obj.FLAG.HIDDEN) + self.chain_buttons.append(btn) self._create_visible_chain_buttons() self.max_pages = (len(self.chains) - 1) // self.items_per_page - - if self.max_pages > 0: - self.next_btn = NormalButton(self, "") - self.next_btn.set_size(224, 98) - self.next_btn.align(lv.ALIGN.BOTTOM_RIGHT, -12, -8) - self.next_btn.set_style_bg_img_src("A:/res/arrow-right-2.png", 0) - - self.back_btn = NormalButton(self, "") - self.back_btn.set_size(224, 98) - self.back_btn.align(lv.ALIGN.BOTTOM_LEFT, 12, -8) - self.back_btn.set_style_bg_img_src("A:/res/arrow-left-2.png", 0) - self.disable_style = ( StyleWrapper() .bg_color(lv_colors.ONEKEY_BLACK_5) @@ -1997,16 +1985,22 @@ def init_ui(self): .bg_opa(lv.OPA.COVER) .radius(98) ) - - self._create_visible_chain_buttons() - if self.max_pages > 0: + self.next_btn = NormalButton(self, "") + self.next_btn.set_size(224, 98) + self.next_btn.align(lv.ALIGN.BOTTOM_RIGHT, -12, -8) + self.next_btn.set_style_bg_img_src("A:/res/arrow-right-2.png", 0) + + self.back_btn = NormalButton(self, "") + self.back_btn.set_size(224, 98) + self.back_btn.align(lv.ALIGN.BOTTOM_LEFT, 12, -8) + self.back_btn.set_style_bg_img_src("A:/res/arrow-left-2.png", 0) self.update_page_buttons() - self.next_btn.add_style(self.enable_style, 0) + # self.next_btn.add_style(self.enable_style, 0) self.animations_next = [] self.animations_prev = [] - self.list_items = self.chain_buttons + # self.list_items = self.chain_buttons if storage_device.is_animation_enabled(): self.animate_list_items() @@ -2084,7 +2078,9 @@ def prev_page(self): self.update_page_buttons() def on_index_click(self, event): - IndexSelectionScreen(self) + IndexSelectionScreen( + self, _(i18n_keys.TITLE__SELECT_ACCOUNT), self.current_index + ) def on_chain_click(self, event, name): if utils.lcd_resume(): @@ -2092,8 +2088,9 @@ def on_chain_click(self, event, name): workflow.spawn(self.addr_manager.generate_address(name, self.current_index)) - def update_index_btn_text(self): - self.index_btn.label_left.set_text(f"Account #{self.current_index + 1}") + def update_index(self, index): + self.current_index = index + self.index_btn.label_left.set_text(f"Account #{index + 1}") def eventhandler(self, event_obj): event = event_obj.code @@ -2120,227 +2117,6 @@ def eventhandler(self, event_obj): elif hasattr(self, "next_btn") and target == self.next_btn: self.next_page() - async def _handle_passphrase_change(self, coro): - await coro - self.init_ui() - - -class IndexSelectionScreen(AnimScreen): - def __init__(self, prev_scr=None): - if not hasattr(self, "_init"): - self._init = True - super().__init__( - prev_scr, title=_(i18n_keys.TITLE__SELECT_ACCOUNT), nav_back=True - ) - - from .components.navigation import Navigation - - # # navi - self.nav_opt = Navigation( - self.content_area, - nav_btn_align=lv.ALIGN.RIGHT_MID, - btn_bg_img="A:/res/general.png", - align=lv.ALIGN.TOP_RIGHT, - ) - - self.container = ContainerFlexCol(self.content_area, self.title, padding_row=2) - - self.max_account = 1000000000 - self.max_page = (self.max_account - 1) // 5 - - self.current_account = self.prev_scr.current_index + 1 - self.current_page = (self.current_account - 1) // 5 - - # account select btn - self.account_btns = [] - for _i in range(5): - btn = ListItemBtn( - self.container, - "", - has_next=False, - use_transition=False, - ) - btn.add_check_img() - self.account_btns.append(btn) - self.update_account_buttons() - - self.next_btn = NormalButton(self, "") - self.next_btn.set_size(224, 98) - self.next_btn.align(lv.ALIGN.BOTTOM_RIGHT, -12, -8) - self.next_btn.set_style_bg_img_src("A:/res/arrow-right-2.png", 0) - - self.back_btn = NormalButton(self, "") - self.back_btn.set_size(224, 98) - self.back_btn.align(lv.ALIGN.BOTTOM_LEFT, 12, -8) - self.back_btn.set_style_bg_img_src("A:/res/arrow-left-2.png", 0) - - self.disable_style = ( - StyleWrapper() - .bg_color(lv_colors.ONEKEY_BLACK_5) - .bg_img_recolor(lv_colors.ONEKEY_GRAY_1) - .bg_img_recolor_opa(lv.OPA.COVER) - ) - self.enable_style = ( - StyleWrapper().bg_color(lv_colors.ONEKEY_BLACK_3).bg_opa(lv.OPA.COVER) - ) - - self.update_page_buttons() - self.next_btn.add_style(self.enable_style, 0) - self.back_btn.add_style(self.enable_style, 0) - - self.animations_next = [] - self.animations_prev = [] - if storage_device.is_animation_enabled(): - self.animate_list_items() - - def animate_list_items(self): - def create_move_cb_container(obj, item_index): - def cb(value): - obj.set_style_translate_x(value, 0) - obj.invalidate() - - return cb - - container_move_anim = Anim( - 50, - 0, - create_move_cb_container(self.container, 0), - time=150, - delay=0, - path_cb=lv.anim_t.path_ease_out, - ) - - container_move_back_anim = Anim( - -50, - 0, - create_move_cb_container(self.container, 0), - time=150, - delay=0, - path_cb=lv.anim_t.path_ease_out, - ) - - self.animations_next.append(container_move_anim) - container_move_anim.start() - - self.animations_prev.append(container_move_back_anim) - - def get_page_start(self): - return (self.current_page * 5) + 1 - - def update_account_buttons(self): - page_start = self.get_page_start() - for i, btn in enumerate(self.account_btns): - account_num = page_start + i - btn.label_left.set_text(f"Account #{account_num}") - - if account_num == self.current_account: - btn.set_checked() - else: - btn.set_uncheck() - - def enable_page_buttons(self, btn): - btn.add_flag(lv.btn.FLAG.CLICKABLE) - btn.remove_style(self.disable_style, 0) - btn.add_style(self.enable_style, 0) - - def disable_page_buttons(self, btn): - btn.clear_flag(lv.btn.FLAG.CLICKABLE) - btn.remove_style(self.enable_style, 0) - btn.add_style(self.disable_style, 0) - - def update_page_buttons(self): - if self.current_page == 0: - self.disable_page_buttons(self.back_btn) - if not self.next_btn.has_flag(lv.btn.FLAG.CLICKABLE): - self.enable_page_buttons(self.next_btn) - - elif self.current_page == self.max_page: - self.disable_page_buttons(self.next_btn) - if not self.back_btn.has_flag(lv.btn.FLAG.CLICKABLE): - self.enable_page_buttons(self.back_btn) - - else: - if not self.next_btn.has_flag(lv.btn.FLAG.CLICKABLE): - self.enable_page_buttons(self.next_btn) - if not self.back_btn.has_flag(lv.btn.FLAG.CLICKABLE): - self.enable_page_buttons(self.back_btn) - gc.collect() - - def next_page(self): - self.current_page += 1 - self.update_account_buttons() - self.update_page_buttons() - for anim in self.animations_next: - anim.start() - - def prev_page(self): - if self.current_page > 0: - self.current_page -= 1 - self.update_account_buttons() - self.update_page_buttons() - for anim in self.animations_prev: - anim.start() - - def eventhandler(self, event_obj): - event = event_obj.code - target = event_obj.get_target() - if event == lv.EVENT.CLICKED: - if utils.lcd_resume(): - return - - if isinstance(target, lv.imgbtn): - if target == self.nav_back.nav_btn: - if self.prev_scr is not None: - self.load_screen(self.prev_scr, destroy_self=True) - elif target == self.nav_opt.nav_btn: - workflow.spawn(self.type_account_index()) - else: - if target == self.back_btn: - self.prev_page() - elif target == self.next_btn: - self.next_page() - else: - for i, btn in enumerate(self.account_btns): - if target == btn: - for other_btn in self.account_btns: - other_btn.set_uncheck() - - btn.set_checked() - - self.current_account = self.get_page_start() + i - self.prev_scr.current_index = self.current_account - 1 - self.prev_scr.update_index_btn_text() - break - - async def type_account_index(self): - from trezor.lvglui.scrs.pinscreen import InputNum - - result = None - while True: - numscreen = InputNum( - title=_(i18n_keys.TITLE__SET_INITIAL_ACCOUNT), - subtitle=_(i18n_keys.TITLE__SET_INITIAL_ACCOUNT_ERROR) - if result is not None - else "", - is_pin=False, - ) - result = await numscreen.request() - - if not result: # user cancelled - return - - account_num = int(result) - if 1 <= account_num <= self.max_account: - break - - self.current_account = account_num - self.current_page = (account_num - 1) // 5 - self.prev_scr.current_index = account_num - 1 - self.prev_scr.update_index_btn_text() - - self.update_account_buttons() - self.update_page_buttons() - class SettingsScreen(AnimScreen): @classmethod diff --git a/core/src/trezor/lvglui/scrs/template.py b/core/src/trezor/lvglui/scrs/template.py index 2fb0b07f84..026b821fdd 100644 --- a/core/src/trezor/lvglui/scrs/template.py +++ b/core/src/trezor/lvglui/scrs/template.py @@ -30,8 +30,10 @@ RawDataItem, ) from .components.container import ContainerFlexCol +from .components.label import ScreenTitle from .components.listitem import CardHeader, CardItem, DisplayItem from .components.qrcode import QRCode +from .components.signatureinfo import OverviewComponent from .widgets.style import StyleWrapper @@ -258,6 +260,241 @@ class ADDRESS_OFFLINE_RETURN_TYPE: DONE = 0 COMMON_DRI_CONFIG_CHANGED = 1 BTC_DRI_CONFIG_CHANGED = 2 + BTC_ADDRESS_INDEX_CHANGED = 3 + + +class IndexSelectionScreen(FullSizeWindow): + SELECTION_TYPE_ACCOUNT = 0 + SELECTION_TYPE_ADDRESS = 1 + selection_type_text_mapping = { + SELECTION_TYPE_ACCOUNT: ("Account", _(i18n_keys.TITLE__SET_INITIAL_ACCOUNT)), + SELECTION_TYPE_ADDRESS: ("Address", _(i18n_keys.TITLE_GO_TO_ADDRESS)), + } + + def __init__(self, parent, title, current_index, sel_type: int = 0): + super().__init__( + title="", + subtitle=None, + anim_dir=0, + ) + self.title = ScreenTitle(self, None, (), title, 63) + import storage.device as storage_device + from trezor import workflow + from .components.navigation import Navigation + + self._workflow = workflow + self.add_nav_back() + # # navi + self.nav_opt = Navigation( + self, + nav_btn_align=lv.ALIGN.RIGHT_MID, + btn_bg_img="A:/res/general.png", + align=lv.ALIGN.TOP_RIGHT, + ) + self.parent = parent + self.container = ContainerFlexCol( + self.content_area, None, padding_row=2, no_align=True + ) + self.container.align(lv.ALIGN.TOP_MID, 0, 32) + self.max_account = 1000000000 + self.max_page = (self.max_account - 1) // 5 + + self.current_account = current_index + 1 + self.current_page = (self.current_account - 1) // 5 + self.button_label = self.selection_type_text_mapping[sel_type][0] + self.sel_type = sel_type + # account select btn + self.account_btns = [] + for _i in range(5): + btn = ListItemBtn( + self.container, + "", + has_next=False, + use_transition=False, + ) + btn.add_check_img() + self.account_btns.append(btn) + self.update_account_buttons() + + self.next_btn = NormalButton(self, "") + self.next_btn.set_size(224, 98) + self.next_btn.align(lv.ALIGN.BOTTOM_RIGHT, -12, -8) + self.next_btn.set_style_bg_img_src("A:/res/arrow-right-2.png", 0) + + self.back_btn = NormalButton(self, "") + self.back_btn.set_size(224, 98) + self.back_btn.align(lv.ALIGN.BOTTOM_LEFT, 12, -8) + self.back_btn.set_style_bg_img_src("A:/res/arrow-left-2.png", 0) + + self.disable_style = ( + StyleWrapper() + .bg_color(lv_colors.ONEKEY_BLACK_5) + .bg_img_recolor(lv_colors.ONEKEY_GRAY_1) + .bg_img_recolor_opa(lv.OPA.COVER) + ) + self.enable_style = ( + StyleWrapper().bg_color(lv_colors.ONEKEY_BLACK_3).bg_opa(lv.OPA.COVER) + ) + + self.update_page_buttons() + self.next_btn.add_style(self.enable_style, 0) + self.back_btn.add_style(self.enable_style, 0) + + self.animations_next = [] + self.animations_prev = [] + if storage_device.is_animation_enabled(): + self.animate_list_items() + + def animate_list_items(self): + def create_move_cb_container(obj, item_index): + def cb(value): + obj.set_style_translate_x(value, 0) + obj.invalidate() + + return cb + + from .components.anim import Anim + + container_move_anim = Anim( + 50, + 0, + create_move_cb_container(self.container, 0), + time=150, + delay=0, + path_cb=lv.anim_t.path_ease_out, + ) + + container_move_back_anim = Anim( + -50, + 0, + create_move_cb_container(self.container, 0), + time=150, + delay=0, + path_cb=lv.anim_t.path_ease_out, + ) + + self.animations_next.append(container_move_anim) + container_move_anim.start() + + self.animations_prev.append(container_move_back_anim) + + def get_page_start(self): + return (self.current_page * 5) + 1 + + def update_account_buttons(self): + page_start = self.get_page_start() + for i, btn in enumerate(self.account_btns): + account_num = page_start + i + btn.label_left.set_text(f"{self.button_label} #{account_num}") + + if account_num == self.current_account: + btn.set_checked() + else: + btn.set_uncheck() + + def enable_page_buttons(self, btn): + btn.add_flag(lv.btn.FLAG.CLICKABLE) + btn.remove_style(self.disable_style, 0) + btn.add_style(self.enable_style, 0) + + def disable_page_buttons(self, btn): + btn.clear_flag(lv.btn.FLAG.CLICKABLE) + btn.remove_style(self.enable_style, 0) + btn.add_style(self.disable_style, 0) + + def update_page_buttons(self): + if self.current_page == 0: + self.disable_page_buttons(self.back_btn) + if not self.next_btn.has_flag(lv.btn.FLAG.CLICKABLE): + self.enable_page_buttons(self.next_btn) + + elif self.current_page == self.max_page: + self.disable_page_buttons(self.next_btn) + if not self.back_btn.has_flag(lv.btn.FLAG.CLICKABLE): + self.enable_page_buttons(self.back_btn) + + else: + if not self.next_btn.has_flag(lv.btn.FLAG.CLICKABLE): + self.enable_page_buttons(self.next_btn) + if not self.back_btn.has_flag(lv.btn.FLAG.CLICKABLE): + self.enable_page_buttons(self.back_btn) + gc.collect() + + def next_page(self): + self.current_page += 1 + self.update_account_buttons() + self.update_page_buttons() + for anim in self.animations_next: + anim.start() + + def prev_page(self): + if self.current_page > 0: + self.current_page -= 1 + self.update_account_buttons() + self.update_page_buttons() + for anim in self.animations_prev: + anim.start() + + def eventhandler(self, event_obj): + event = event_obj.code + target = event_obj.get_target() + if event == lv.EVENT.CLICKED: + if utils.lcd_resume(): + return + + if isinstance(target, lv.imgbtn): + if target == self.nav_back.nav_btn: + if hasattr(self.parent, "on_index_changed"): + self.parent.on_index_changed(self.current_account - 1) + self.destroy(200) + elif target == self.nav_opt.nav_btn: + self._workflow.spawn(self.type_account_index()) + else: + if target == self.back_btn: + self.prev_page() + elif target == self.next_btn: + self.next_page() + else: + for i, btn in enumerate(self.account_btns): + if target == btn: + for other_btn in self.account_btns: + other_btn.set_uncheck() + + btn.set_checked() + + self.current_account = self.get_page_start() + i + self.parent.update_index(self.current_account - 1) + break + + async def type_account_index(self): + from trezor.lvglui.scrs.pinscreen import InputNum + + result = None + while True: + numscreen = InputNum( + title=self.selection_type_text_mapping[self.sel_type][1], + subtitle=( + _(i18n_keys.TITLE__SET_INITIAL_ACCOUNT_ERROR) + if result is not None + else "" + ), + is_pin=False, + ) + result = await numscreen.request() + + if not result: # user cancelled + return + + account_num = int(result) + if 1 <= account_num <= self.max_account: + break + + self.current_account = account_num + self.current_page = (account_num - 1) // 5 + self.parent.update_index(account_num - 1) + + self.update_account_buttons() + self.update_page_buttons() class AddressOffline(FullSizeWindow): @@ -280,6 +517,7 @@ def __init__( network: str = "", prev_scr=None, account_name: str = "", + address_index: int = 0, ): super().__init__( title, @@ -301,6 +539,7 @@ def __init__( self.network = network self.prev_scr = prev_scr self.account_name = account_name + self.address_index = address_index if primary_color: self.title.add_style(StyleWrapper().text_color(primary_color), 0) self.qr_first = qr_first @@ -323,7 +562,7 @@ def show_address(self, evm_chain_id: int | None = None): if self.network in ("Bitcoin", "Ethereum", "Solana", "Litecoin", "Kaspa"): self.derive_btn = ListItemBtn( self.content_area, - self.addr_type, + self.addr_type or "", left_img_src="A:/res/branches.png", has_next=True, ) @@ -347,11 +586,18 @@ def show_address(self, evm_chain_id: int | None = None): pos=(0, 40), padding_row=0, ) + card_clickable = self.network in ("Bitcoin",) self.item_group_header = CardHeader( self.group_address, - self.account_name, + f" Address #{self.address_index + 1}" + if card_clickable + else self.account_name, "A:/res/group-icon-wallet.png", + clickable=card_clickable, ) + if card_clickable: + self.item_group_header.add_flag(lv.obj.FLAG.EVENT_BUBBLE) + self.card_clickable = card_clickable self.item_group_body = DisplayItem( self.group_address, None, @@ -427,6 +673,18 @@ def on_derive_config_changed(self, new_type): raise ValueError(f"Unsupported network: {self.network}") self.destroy(50) + def on_index_changed(self, new_index: int): + if new_index == self.address_index: + return + self.channel.publish( + (ADDRESS_OFFLINE_RETURN_TYPE.BTC_ADDRESS_INDEX_CHANGED, new_index) + ) + self.destroy(50) + + def update_index(self, index): + self.current_index = index + self.item_group_header.label.set_text(f"Address #{index + 1}") + def eventhandler(self, event_obj): code = event_obj.code target = event_obj.get_target() @@ -440,6 +698,10 @@ def eventhandler(self, event_obj): elif target == self.btn_yes: self.destroy(50) self.channel.publish(ADDRESS_OFFLINE_RETURN_TYPE.DONE) + elif self.card_clickable and target == self.item_group_header: + IndexSelectionScreen( + self, _(i18n_keys.TITLE_SELECT_ADDRESS), self.address_index, 1 + ) elif hasattr(self, "derive_btn") and target == self.derive_btn: title = _(i18n_keys.TITLE__SELECT_DERIVATION_PATH) if self.network == "Bitcoin": @@ -716,9 +978,6 @@ def __init__( sub_icon_path=icon_path, ) - from .components.signatureinfo import OverviewComponent - from .components.banner import Banner - if banner_key: self.banner = Banner( self.content_area, @@ -1446,10 +1705,10 @@ def __init__( from .components.signatureinfo import ( AmountComponent, + DataComponent, DirectionComponent, FeeComponent, MoreInfoComponent, - DataComponent, ) # 1. Amount Component(optional) @@ -1532,9 +1791,6 @@ def __init__( self.title.set_style_text_font(font_GeistSemiBold48, 0) self.primary_color = primary_color - from .components.signatureinfo import OverviewComponent - from .components.banner import Banner - if is_unlimited: self.banner = Banner( self.content_area, @@ -1616,7 +1872,6 @@ def __init__( FeeComponent, MoreInfoComponent, ) - from .components.banner import Banner if is_unlimited: self.banner = Banner( @@ -2469,7 +2724,13 @@ def __init__(self, title: str, identity: str, subtitle: str | None, primary_colo class ConfirmProperties(FullSizeWindow): - def __init__(self, title: str, properties: list[tuple[str, str]], primary_color): + def __init__( + self, + title: str, + properties: list[tuple[str, str]], + primary_color, + warning_banner_text: str | None = None, + ): super().__init__( title, None, @@ -2477,8 +2738,18 @@ def __init__(self, title: str, properties: list[tuple[str, str]], primary_color) _(i18n_keys.BUTTON__CANCEL), primary_color=primary_color, ) + if warning_banner_text: + self.warning_banner = Banner( + self.content_area, + 2, + warning_banner_text, + ) + self.warning_banner.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 40) self.container = ContainerFlexCol( - self.content_area, self.title, pos=(0, 40), padding_row=0 + self.content_area, + self.title if not warning_banner_text else self.warning_banner, + pos=(0, 40), + padding_row=0, ) self.container.add_dummy() for key, value in properties: @@ -5583,7 +5854,7 @@ def __init__( self.item_group_header = CardHeader( self.group, step[0], - f"A:/res/group-icon-num-{i+1}.png", + f"A:/res/group-icon-num-{i + 1}.png", ) self.item_group_body = DisplayItem( self.group, @@ -6105,8 +6376,8 @@ def __init__(self): _(i18n_keys.BUTTON__APPLY), _(i18n_keys.BUTTON__CANCEL), ) - from trezor.ui import style import storage.device as storage_device + from trezor.ui import style current_brightness = storage_device.get_brightness() slider = lv.slider(self.content_area) diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index 5c8fab0541..ecf9aa7dc4 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -65,6 +65,7 @@ def __getattr__(name: str) -> Any: from trezor.enums import SolanaOffChainMessageVersion # noqa: F401 from trezor.enums import StellarAssetType # noqa: F401 from trezor.enums import StellarMemoType # noqa: F401 + from trezor.enums import StellarRequestType # noqa: F401 from trezor.enums import StellarSignerType # noqa: F401 from trezor.enums import TezosBallotType # noqa: F401 from trezor.enums import TezosContractType # noqa: F401 @@ -2430,6 +2431,7 @@ class CardanoSignMessage(protobuf.MessageType): derivation_type: "CardanoDerivationType" network_id: "int" address_type: "CardanoAddressType | None" + protocol_magic: "int | None" def __init__( self, @@ -2439,6 +2441,7 @@ def __init__( network_id: "int", address_n: "list[int] | None" = None, address_type: "CardanoAddressType | None" = None, + protocol_magic: "int | None" = None, ) -> None: pass @@ -8471,6 +8474,7 @@ class StellarSignTx(protobuf.MessageType): memo_id: "int | None" memo_hash: "bytes | None" num_operations: "int" + soroban_data_size: "int" def __init__( self, @@ -8487,6 +8491,7 @@ def __init__( memo_text: "str | None" = None, memo_id: "int | None" = None, memo_hash: "bytes | None" = None, + soroban_data_size: "int | None" = None, ) -> None: pass @@ -8792,6 +8797,62 @@ def __init__( def is_type_of(cls, msg: Any) -> TypeGuard["StellarBumpSequenceOp"]: return isinstance(msg, cls) + class StellarInvokeHostFunctionOp(protobuf.MessageType): + source_account: "str | None" + contract_address: "str" + function_name: "str" + call_args_xdr_size: "int" + call_args_xdr_initial_chunk: "bytes" + soroban_auth_xdr_size: "int" + soroban_auth_xdr_initial_chunk: "bytes" + + def __init__( + self, + *, + contract_address: "str", + function_name: "str", + call_args_xdr_size: "int", + call_args_xdr_initial_chunk: "bytes", + soroban_auth_xdr_size: "int", + soroban_auth_xdr_initial_chunk: "bytes", + source_account: "str | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["StellarInvokeHostFunctionOp"]: + return isinstance(msg, cls) + + class StellarSorobanDataRequest(protobuf.MessageType): + type: "StellarRequestType" + data_length: "int" + + def __init__( + self, + *, + type: "StellarRequestType", + data_length: "int", + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["StellarSorobanDataRequest"]: + return isinstance(msg, cls) + + class StellarSorobanDataAck(protobuf.MessageType): + data_chunk_xdr: "bytes" + + def __init__( + self, + *, + data_chunk_xdr: "bytes", + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["StellarSorobanDataAck"]: + return isinstance(msg, cls) + class StellarSignedTx(protobuf.MessageType): public_key: "bytes" signature: "bytes" diff --git a/core/src/trezor/uart.py b/core/src/trezor/uart.py index b5c2c9db09..527f1799f8 100644 --- a/core/src/trezor/uart.py +++ b/core/src/trezor/uart.py @@ -182,6 +182,7 @@ async def handle_usb_state(): while True: try: utils.USB_STATE_CHANGED = False + utils.unmark_rest_by_usb_lock() usb_state = loop.wait(io.USB_STATE) state, enable = await usb_state if enable is not None and utils.is_usb_enabled(): @@ -226,6 +227,7 @@ async def handle_usb_state(): else: config.lock() await safe_reloop() + utils.mark_rest_by_usb_lock() await workflow.spawn(utils.internal_reloop()) elif not usb_auto_lock and not state: await safe_reloop(ack=False) diff --git a/core/src/trezor/ui/layouts/lvgl/__init__.py b/core/src/trezor/ui/layouts/lvgl/__init__.py index 27e5fafdeb..b9994f71e7 100644 --- a/core/src/trezor/ui/layouts/lvgl/__init__.py +++ b/core/src/trezor/ui/layouts/lvgl/__init__.py @@ -380,6 +380,7 @@ async def show_address_offline( addr_type: str | None = None, prev_scr=None, account_name: str = "", + address_index: int = 0, ) -> None: is_multisig = len(xpubs) > 0 from trezor.lvglui.scrs.template import AddressOffline @@ -416,6 +417,7 @@ async def show_address_offline( network=network, prev_scr=prev_scr, account_name=account_name, + address_index=address_index, ), "show_address", ButtonRequestType.Address, @@ -1024,6 +1026,7 @@ async def confirm_properties( icon_color: int = ui.GREEN, # TODO cleanup @ redesign hold: bool = False, br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput, + warning_banner_text: str | None = None, ) -> None: para = [] from trezor.lvglui.scrs.template import ConfirmProperties @@ -1034,7 +1037,7 @@ async def confirm_properties( para.append((key, val)) elif isinstance(val, bytes): para.append((key, hexlify(val).decode())) - screen = ConfirmProperties(title, para, ctx.primary_color) + screen = ConfirmProperties(title, para, ctx.primary_color, warning_banner_text) await raise_if_cancelled(interact(ctx, screen, br_type, br_code)) diff --git a/core/src/trezor/utils.py b/core/src/trezor/utils.py index 692d513656..bc8a9ab893 100644 --- a/core/src/trezor/utils.py +++ b/core/src/trezor/utils.py @@ -88,6 +88,7 @@ def board_version() -> str: CHARGE_ENABLE: bool | None = None CHARGING = False USB_STATE_CHANGED = False +_REST_AFTER_USB_LOCK = False RESTART_MAIN_LOOP = False _WIRE_BUSY = False _PENDING_SLEEP_AFTER_CANCEL = False @@ -387,6 +388,22 @@ def mark_collecting_fingerprint_done(): _COLLECTING_FINGERPRINT = False +def is_rest_by_usb_lock(): + return _REST_AFTER_USB_LOCK + + +def mark_rest_by_usb_lock(): + global _REST_AFTER_USB_LOCK + + _REST_AFTER_USB_LOCK = True + + +def unmark_rest_by_usb_lock(): + global _REST_AFTER_USB_LOCK + + _REST_AFTER_USB_LOCK = False + + def set_backup_none(): global _CURRENT_BACKUP_METHOD _CURRENT_BACKUP_METHOD = BACKUP_METHOD_NONE diff --git a/python/src/trezorlib/cli/stellar.py b/python/src/trezorlib/cli/stellar.py index d9c9fb6a4b..129d8069f3 100644 --- a/python/src/trezorlib/cli/stellar.py +++ b/python/src/trezorlib/cli/stellar.py @@ -105,7 +105,8 @@ def sign_transaction( sys.exit(1) address_n = tools.parse_path(address) - tx, operations = stellar.from_envelope(envelope) - resp = stellar.sign_tx(client, tx, operations, address_n, network_passphrase) - + tx, operations, soroban_data_xdr = stellar.from_envelope(envelope) + resp = stellar.sign_tx( + client, tx, operations, address_n, soroban_data_xdr, network_passphrase + ) return base64.b64encode(resp.signature) diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 593c4f5808..6819cf93ab 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -212,6 +212,9 @@ class MessageType(IntEnum): StellarManageBuyOfferOp = 222 StellarPathPaymentStrictSendOp = 223 StellarSignedTx = 230 + StellarInvokeHostFunctionOp = 260 + StellarSorobanDataRequest = 261 + StellarSorobanDataAck = 262 CardanoGetPublicKey = 305 CardanoPublicKey = 306 CardanoGetAddress = 307 @@ -824,6 +827,12 @@ class StellarSignerType(IntEnum): HASH = 2 +class StellarRequestType(IntEnum): + CALL = 0 + AUTH = 1 + EXT = 2 + + class TezosContractType(IntEnum): Implicit = 0 Originated = 1 @@ -3809,6 +3818,7 @@ class CardanoSignMessage(protobuf.MessageType): 3: protobuf.Field("derivation_type", "CardanoDerivationType", repeated=False, required=True), 4: protobuf.Field("network_id", "uint32", repeated=False, required=True), 5: protobuf.Field("address_type", "CardanoAddressType", repeated=False, required=False), + 6: protobuf.Field("protocol_magic", "uint32", repeated=False, required=False), } def __init__( @@ -3819,12 +3829,14 @@ def __init__( network_id: "int", address_n: Optional[Sequence["int"]] = None, address_type: Optional["CardanoAddressType"] = None, + protocol_magic: Optional["int"] = None, ) -> None: self.address_n: Sequence["int"] = address_n if address_n is not None else [] self.message = message self.derivation_type = derivation_type self.network_id = network_id self.address_type = address_type + self.protocol_magic = protocol_magic class CardanoMessageSignature(protobuf.MessageType): @@ -10574,6 +10586,7 @@ class StellarSignTx(protobuf.MessageType): 12: protobuf.Field("memo_id", "uint64", repeated=False, required=False), 13: protobuf.Field("memo_hash", "bytes", repeated=False, required=False), 14: protobuf.Field("num_operations", "uint32", repeated=False, required=True), + 60: protobuf.Field("soroban_data_size", "uint32", repeated=False, required=False), } def __init__( @@ -10591,6 +10604,7 @@ def __init__( memo_text: Optional["str"] = None, memo_id: Optional["int"] = None, memo_hash: Optional["bytes"] = None, + soroban_data_size: Optional["int"] = 0, ) -> None: self.address_n: Sequence["int"] = address_n if address_n is not None else [] self.network_passphrase = network_passphrase @@ -10604,6 +10618,7 @@ def __init__( self.memo_text = memo_text self.memo_id = memo_id self.memo_hash = memo_hash + self.soroban_data_size = soroban_data_size class StellarTxOpRequest(protobuf.MessageType): @@ -10957,6 +10972,69 @@ def __init__( self.source_account = source_account +class StellarInvokeHostFunctionOp(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 260 + FIELDS = { + 1: protobuf.Field("source_account", "string", repeated=False, required=False), + 2: protobuf.Field("contract_address", "string", repeated=False, required=True), + 3: protobuf.Field("function_name", "string", repeated=False, required=True), + 4: protobuf.Field("call_args_xdr_size", "uint32", repeated=False, required=True), + 5: protobuf.Field("call_args_xdr_initial_chunk", "bytes", repeated=False, required=True), + 6: protobuf.Field("soroban_auth_xdr_size", "uint32", repeated=False, required=True), + 7: protobuf.Field("soroban_auth_xdr_initial_chunk", "bytes", repeated=False, required=True), + } + + def __init__( + self, + *, + contract_address: "str", + function_name: "str", + call_args_xdr_size: "int", + call_args_xdr_initial_chunk: "bytes", + soroban_auth_xdr_size: "int", + soroban_auth_xdr_initial_chunk: "bytes", + source_account: Optional["str"] = None, + ) -> None: + self.contract_address = contract_address + self.function_name = function_name + self.call_args_xdr_size = call_args_xdr_size + self.call_args_xdr_initial_chunk = call_args_xdr_initial_chunk + self.soroban_auth_xdr_size = soroban_auth_xdr_size + self.soroban_auth_xdr_initial_chunk = soroban_auth_xdr_initial_chunk + self.source_account = source_account + + +class StellarSorobanDataRequest(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 261 + FIELDS = { + 1: protobuf.Field("type", "StellarRequestType", repeated=False, required=True), + 2: protobuf.Field("data_length", "uint32", repeated=False, required=True), + } + + def __init__( + self, + *, + type: "StellarRequestType", + data_length: "int", + ) -> None: + self.type = type + self.data_length = data_length + + +class StellarSorobanDataAck(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 262 + FIELDS = { + 1: protobuf.Field("data_chunk_xdr", "bytes", repeated=False, required=True), + } + + def __init__( + self, + *, + data_chunk_xdr: "bytes", + ) -> None: + self.data_chunk_xdr = data_chunk_xdr + + class StellarSignedTx(protobuf.MessageType): MESSAGE_WIRE_TYPE = 230 FIELDS = { diff --git a/python/src/trezorlib/stellar.py b/python/src/trezorlib/stellar.py index 1cb0c99b8a..47f3b89038 100644 --- a/python/src/trezorlib/stellar.py +++ b/python/src/trezorlib/stellar.py @@ -14,8 +14,12 @@ # You should have received a copy of the License along with this library. # If not, see . +from copy import deepcopy from decimal import Decimal from typing import TYPE_CHECKING, List, Tuple, Union +from stellar_sdk import operation +from stellar_sdk.xdr import SorobanTransactionData +from xdrlib3 import Packer from . import exceptions, messages from .tools import expect @@ -68,6 +72,8 @@ Network, ManageBuyOffer, MuxedAccount, + InvokeHostFunction, + StrKey, ) from stellar_sdk.xdr.signer_key_type import SignerKeyType @@ -83,7 +89,7 @@ def from_envelope( envelope: "TransactionEnvelope", -) -> Tuple[messages.StellarSignTx, List["StellarMessageType"]]: +) -> Tuple[messages.StellarSignTx, List["StellarMessageType"], bytes]: """Parses transaction envelope into a map with the following keys: tx - a StellarSignTx describing the transaction header operations - an array of protobuf message objects for each operation @@ -92,7 +98,7 @@ def from_envelope( raise RuntimeError("Stellar SDK not available") parsed_tx = envelope.transaction - if parsed_tx.time_bounds is None: + if parsed_tx.preconditions.time_bounds is None: raise ValueError("Timebounds are mandatory") memo_type = messages.StellarMemoType.NONE @@ -118,22 +124,24 @@ def from_envelope( raise ValueError("Unsupported memo type") _raise_if_account_muxed_id_exists(parsed_tx.source) + soroban_data_xdr = parsed_tx.soroban_data.to_xdr_bytes() tx = messages.StellarSignTx( source_account=parsed_tx.source.account_id, fee=parsed_tx.fee, sequence_number=parsed_tx.sequence, - timebounds_start=parsed_tx.time_bounds.min_time, - timebounds_end=parsed_tx.time_bounds.max_time, + timebounds_start=parsed_tx.preconditions.time_bounds.min_time, + timebounds_end=parsed_tx.preconditions.time_bounds.max_time, memo_type=memo_type, memo_text=memo_text, memo_id=memo_id, memo_hash=memo_hash, num_operations=len(parsed_tx.operations), network_passphrase=envelope.network_passphrase, + soroban_data_size=len(soroban_data_xdr), ) operations = [_read_operation(op) for op in parsed_tx.operations] - return tx, operations + return tx, operations, soroban_data_xdr def _read_operation(op: "Operation") -> "StellarMessageType": @@ -143,6 +151,31 @@ def _read_operation(op: "Operation") -> "StellarMessageType": source_account = op.source.account_id else: source_account = None + if isinstance(op, InvokeHostFunction): + args = op.host_function.invoke_contract.args + packer = Packer() + packer.pack_uint(len(args)) + for args_item in args: + args_item.pack(packer) + args = packer.get_buffer() + + auths = op.auth + packer.reset() + packer.pack_uint(len(auths)) + for auth_item in auths: + auth_item.pack(packer) + auths = packer.get_buffer() + return messages.StellarInvokeHostFunctionOp( + source_account=source_account, + contract_address=StrKey.encode_contract( + op.host_function.invoke_contract.contract_address.contract_id.contract_id.hash + ), + function_name=op.host_function.invoke_contract.function_name.sc_symbol.decode(), + call_args_xdr_size=len(args), + call_args_xdr_initial_chunk=args, + soroban_auth_xdr_size=len(auths), + soroban_auth_xdr_initial_chunk=auths, + ) if isinstance(op, CreateAccount): return messages.StellarCreateAccountOp( source_account=source_account, @@ -336,6 +369,7 @@ def sign_tx( tx: messages.StellarSignTx, operations: List["StellarMessageType"], address_n: "Address", + soroban_transaction_data_xdr: bytes, network_passphrase: str = DEFAULT_NETWORK_PASSPHRASE, ) -> messages.StellarSignedTx: tx.network_passphrase = network_passphrase @@ -349,9 +383,47 @@ def sign_tx( # 4. Send operations one by one until all operations have been sent. If there are more operations to sign, the device will send a StellarTxOpRequest message # 5. The final message received will be StellarSignedTx which is returned from this method resp = client.call(tx) + soroban_operation = None try: + initial_chunk_size = 1024 while isinstance(resp, messages.StellarTxOpRequest): - resp = client.call(operations.pop(0)) + _opreation = operations.pop(0) + if isinstance(_opreation, messages.StellarInvokeHostFunctionOp): + soroban_operation = deepcopy(_opreation) + _opreation.call_args_xdr_initial_chunk = ( + _opreation.call_args_xdr_initial_chunk[:initial_chunk_size] + ) + _opreation.soroban_auth_xdr_initial_chunk = ( + _opreation.soroban_auth_xdr_initial_chunk[:initial_chunk_size] + ) + resp = client.call(_opreation) + if soroban_operation is not None: + offset_call = initial_chunk_size + offset_auth = initial_chunk_size + offset_ext = 0 + while isinstance(resp, messages.StellarSorobanDataRequest): + if resp.type == messages.StellarRequestType.CALL: + data_chunk = soroban_operation.call_args_xdr_initial_chunk[ + offset_call : offset_call + resp.data_length + ] + offset_call += resp.data_length + elif resp.type == messages.StellarRequestType.AUTH: + data_chunk = soroban_operation.soroban_auth_xdr_initial_chunk[ + offset_auth : offset_auth + resp.data_length + ] + offset_auth += resp.data_length + elif resp.type == messages.StellarRequestType.EXT: + data_chunk = soroban_transaction_data_xdr[ + offset_ext : offset_ext + resp.data_length + ] + offset_ext += resp.data_length + else: + raise exceptions.TrezorException( + "Invalid Stellar data request type." + ) + resp = client.call( + messages.StellarSorobanDataAck(data_chunk_xdr=data_chunk) + ) except IndexError: # pop from empty list raise exceptions.TrezorException(