diff --git a/common/defs/ethereum/chains b/common/defs/ethereum/chains index 3fb1fb982c..fa90d06165 160000 --- a/common/defs/ethereum/chains +++ b/common/defs/ethereum/chains @@ -1 +1 @@ -Subproject commit 3fb1fb982c09a310163d0b0953e73a121ab61401 +Subproject commit fa90d06165cf20bf1086546e10a2a76bbf056010 diff --git a/common/defs/evm_tokens/1.json b/common/defs/evm_tokens/1.json index 2e3351c51b..578956e573 100644 --- a/common/defs/evm_tokens/1.json +++ b/common/defs/evm_tokens/1.json @@ -157,6 +157,32 @@ "__v": 0, "marketCap": "6126893790.915463" }, + { + "_id": "62e8a3d28edb7c3c91a5586b", + "name": "Polygon", + "symbol": "POL", + "address": "0x455e53CBB86018Ac2B8092FdCd39d8444aFFC3F6", + "decimals": 18, + "logoURI": "https://common.onekey-asset.com/token/evm-1/0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0.jpg", + "impl": "evm", + "status": "LISTED", + "verified": true, + "security": false, + "addToIndex": false, + "chainId": "1", + "source": [ + "sushi", + "Uniswap Labs Default", + "Coingecko" + ], + "checked": true, + "coingeckoId": "matic-network", + "swftId": "MATIC", + "createdAt": "2022-08-02T04:10:58.228Z", + "updatedAt": "2022-11-16T13:02:51.357Z", + "__v": 0, + "marketCap": "6126893790.915463" + }, { "_id": "62e8a3d28edb7c3c91a5585f", "name": "Wrapped BTC", @@ -1263,6 +1289,31 @@ "updatedAt": "2022-09-16T16:46:49.461Z", "__v": 0, "marketCap": "192347985.13471806" + }, + { + "_id": "", + "name": "HashKey Platform Token", + "symbol": "HSK", + "address": "0xe7c6bf469e97eeb0bfb74c8dbff5bd47d4c1c98a", + "decimals": 18, + "logoURI": "", + "impl": "evm", + "status": "LISTED", + "verified": true, + "security": false, + "addToIndex": false, + "chainId": "1", + "source": [ + "sushi", + "Coingecko" + ], + "checked": true, + "coingeckoId": "", + "swftId": "", + "createdAt": "", + "updatedAt": "", + "__v": 0, + "marketCap": "" } ] } diff --git a/common/defs/evm_tokens/177.json b/common/defs/evm_tokens/177.json new file mode 100644 index 0000000000..1b9542dea3 --- /dev/null +++ b/common/defs/evm_tokens/177.json @@ -0,0 +1,30 @@ +{ + "name": "HashKey Chain", + "chain": "HashKey Chain", + "tokens": [ + { + "name": "Wrapped HSK", + "symbol": "WHSK", + "address": "0xB210D2120d57b758EE163cFfb43e73728c471Cf1", + "decimals": 18 + }, + { + "name": "Wrapped Ether", + "symbol": "WETH", + "address": "0xefd4bC9afD210517803f293ABABd701CaeeCdfd0", + "decimals": 18 + }, + { + "name": "Tether USD", + "symbol": "USDT", + "address": "0xF1B50eD67A9e2CC94Ad3c477779E2d4cBfFf9029", + "decimals": 6 + }, + { + "name": "Wrapped BTC", + "symbol": "WBTC", + "address": "0x6119CA49a79f5825C8B345F8d7aC36B272565b14", + "decimals": 8 + } + ] +} diff --git a/common/defs/evm_tokens/9798.json b/common/defs/evm_tokens/9798.json new file mode 100644 index 0000000000..70dd512cda --- /dev/null +++ b/common/defs/evm_tokens/9798.json @@ -0,0 +1,69 @@ +{ + "name": "Data Trade Chain", + "chain": "Data Trade Chain", + "tokens": [ + { + "id": "837d169e-6f33-535a-988c-8b2eb31dd0bd", + "name": "HLT", + "symbol": "HLT", + "address": "0xE52a736828c782C2a4A345bBE8052aed010fc82D", + "decimals": 2 + }, + { + "id": "e2c7ceec-289d-5508-8c3d-ab33d36d6ecd", + "name": "BV", + "symbol": "BV", + "address": "0x8E79850C50E525eB6Ba63e601E7b41888A1c9102", + "decimals": 2 + }, + { + "id": "ce5883e4-213b-55cb-b49c-2df48e5dea8e", + "name": "dUSDT", + "symbol": "dUSDT", + "address": "0x36E6504c968f5C2A310B6AF7B97BC22cdd3402cc", + "decimals": 6 + }, + { + "id": "79cab3e2-b87b-53a3-9b5b-c120b9287749", + "name": "dBTC", + "symbol": "dBTC", + "address": "0xE895c577D747bB5dbBc1F06cb44d6067680bE4be", + "decimals": 8 + }, + { + "id": "3f348ef3-1ced-5c1f-b25b-dbde27a12ab1", + "name": "dETH", + "symbol": "dETH", + "address": "0x8B7160C1E9fDb689A060Ff0919E84915B0dFa04a", + "decimals": 18 + }, + { + "id": "ad4ea990-8cfe-5372-8102-a9aa7574a65c", + "name": "DOS", + "symbol": "DOS", + "address": "0x745C11Fb4783Bd00A88a0B99420262f409FA8Bb8", + "decimals": 2 + }, + { + "id": "0128716b-1bc9-542b-a322-51d4e3be9270", + "name": "CNV", + "symbol": "CNV", + "address": "0x899f0B9d67DD1B833fdaa90c8b09ea616d0e9E98", + "decimals": 2 + }, + { + "id": "7f37f71c-aee4-5876-9781-61eb5ede3e61", + "name": "FEC", + "symbol": "FEC", + "address": "0xb88ad767B416197e62939dEc207431b561A9383B", + "decimals": 4 + }, + { + "id": "1e46303c-dcc6-5899-8b44-84dd2f348644", + "name": "STC08375", + "symbol": "STC08375", + "address": "0x6d885b0B37C62Be0c72Ecd6a61Af2bfFf681419e", + "decimals": 0 + } + ] +} diff --git a/common/defs/support.json b/common/defs/support.json index de13ae03dc..100a8634ed 100644 --- a/common/defs/support.json +++ b/common/defs/support.json @@ -2131,7 +2131,7 @@ "eth:JOYS": "1.9.5", "eth:KAR": "1.9.5", "eth:KCS": "1.10.3", - "eth:KLAY": "1.9.5", + "eth:KAIA": "1.9.5", "eth:KTO:2559": "1.10.3", "eth:L1": "1.10.5", "eth:L99": "1.10.4", @@ -2252,7 +2252,7 @@ "eth:tGOR:420": "1.9.5", "eth:tGOR:5": "1.9.5", "eth:tKAL": "1.9.4", - "eth:tKLAY": "1.9.5", + "eth:tKAIA": "1.9.5", "eth:tKOR": "1.9.5", "eth:tKOT": "1.9.4", "eth:tKOV": "1.6.2", @@ -4286,6 +4286,7 @@ "erc20:eth:eUSD": "2.0.7", "erc20:eth:eosDAC": "2.0.7", "erc20:eth:fstETHDAI": "2.3.1", + "erc20:eth:HSK": "4.10.0", "erc20:eth:iBAT": "2.3.0", "erc20:eth:iBNB": "2.3.0", "erc20:eth:iBTC": "2.3.0", @@ -4697,7 +4698,7 @@ "eth:BTA": "2.4.3", "eth:BTCIX": "2.4.4", "eth:BTM": "2.4.4", - "eth:BTT": "2.4.3", + "eth:BTT:199": "2.4.3", "eth:CATE": "2.4.2", "eth:CCP": "2.4.4", "eth:CELO": "2.3.7", @@ -4710,6 +4711,7 @@ "eth:DAX": "2.4.2", "eth:DEV": "2.4.3", "eth:DIODE": "2.3.7", + "eth:DTT": "4.10.0", "eth:DTH": "2.4.4", "eth:DWU": "2.4.2", "eth:DYNO": "2.4.4", @@ -4744,7 +4746,7 @@ "eth:EWT": "2.3.5", "eth:EXP": "2.0.7", "eth:FETH": "2.4.2", - "eth:FIL": "4.3.0", + "eth:FIL:314": "4.3.0", "eth:FIN": "2.3.5", "eth:FLR": "2.3.7", "eth:FSN": "2.4.2", @@ -4758,9 +4760,10 @@ "eth:GT": "2.4.2", "eth:GooD": "2.4.2", "eth:HAIC": "2.4.2", - "eth:HO": "2.4.4", + "eth:HO:1280": "2.4.4", "eth:HOP": "2.4.4", "eth:HPB": "2.1.1", + "eth:HSK": "4.10.0", "eth:HT": "2.3.7", "eth:ILT": "2.3.5", "eth:IORA": "2.4.4", @@ -4770,14 +4773,14 @@ "eth:JOYS": "2.3.7", "eth:KAR": "2.3.7", "eth:KCS": "2.4.2", - "eth:KLAY": "2.3.7", + "eth:KAIA": "2.3.7", "eth:KTO:2559": "2.4.2", - "eth:L1": "2.4.4", + "eth:L1:29": "2.4.4", "eth:L99": "2.4.3", "eth:LA": "2.4.4", "eth:LISINSKI": "2.3.5", "eth:MATH": "2.3.7", - "eth:MATIC": "2.3.5", + "eth:POL": "2.3.5", "eth:META": "2.1.1", "eth:METIS": "2.4.4", "eth:MINTME": "2.4.2", @@ -4786,7 +4789,7 @@ "eth:MOLE": "2.4.4", "eth:MOVR": "2.4.2", "eth:MTR": "2.3.7", - "eth:MTT": "2.4.2", + "eth:MTT:16000": "2.4.2", "eth:MUSIC": "2.0.8", "eth:NEON:245022934": "2.4.3", "eth:NEW": "2.3.7", @@ -4803,7 +4806,7 @@ "eth:ONE:1666600003": "2.3.7", "eth:ONG": "2.4.2", "eth:OY": "2.4.4", - "eth:PALM": "2.4.3", + "eth:PALM:11297108109": "2.4.3", "eth:PETH": "2.4.2", "eth:PHT": "2.3.5", "eth:PHX": "2.4.4", @@ -4812,7 +4815,7 @@ "eth:PLS": "2.4.3", "eth:POA": "2.3.5", "eth:POLIS": "2.4.4", - "eth:POP": "2.4.3", + "eth:POP:1213": "2.4.3", "eth:PRB": "2.4.4", "eth:QKC:100000": "2.4.2", "eth:QKC:100001": "2.4.2", @@ -4832,6 +4835,7 @@ "eth:ROSE": "2.4.4", "eth:RPG": "2.4.4", "eth:RUPX": "2.3.7", + "eth:S": "4.10.0", "eth:SDN": "2.4.3", "eth:SETM": "2.4.4", "eth:SGB": "2.4.2", @@ -4843,7 +4847,7 @@ "eth:SPOA": "2.4.4", "eth:SRN": "2.4.3", "eth:Seele": "2.4.4", - "eth:TAO": "2.3.5", + "eth:TAO:558": "2.3.5", "eth:TBG": "2.3.7", "eth:TCH": "2.3.5", "eth:TCLO": "2.3.5", @@ -4851,7 +4855,7 @@ "eth:TETC": "2.3.5", "eth:TFI": "2.3.7", "eth:TLOS:40": "2.4.2", - "eth:TOMO:88": "2.4.4", + "eth:VIC": "2.4.4", "eth:TOYS": "2.3.7", "eth:TPEP": "2.3.5", "eth:TSF": "2.3.5", @@ -4896,14 +4900,14 @@ "eth:tETH:5": "4.3.0", "eth:tETH:11155111": "4.9.1", "eth:tKAL": "2.3.5", - "eth:tKLAY": "2.3.7", + "eth:tKAIA": "2.3.7", "eth:tKOR": "2.3.7", "eth:tKOT": "2.3.5", "eth:tKOV": "2.0.7", "eth:tMATH": "2.3.7", - "eth:tMATIC": "2.3.5", + "eth:tMATIC:80001": "2.3.5", "eth:tMETC": "2.3.5", - "eth:tNEW": "2.3.7", + "eth:tNEW:1007": "2.3.7", "eth:tNRG": "2.3.5", "eth:tPHT": "2.3.5", "eth:tRBTC": "2.0.7", diff --git a/common/protob/messages-bitcoin.proto b/common/protob/messages-bitcoin.proto index 772fec1860..76267963ea 100644 --- a/common/protob/messages-bitcoin.proto +++ b/common/protob/messages-bitcoin.proto @@ -154,6 +154,7 @@ message SignMessage { optional string coin_name = 3 [default='Bitcoin']; // coin to use for signing optional InputScriptType script_type = 4 [default=SPENDADDRESS]; // used to distinguish between various address formats (non-segwit, segwit, etc.) optional bool no_script_type = 5; // don't include script type information in the recovery byte of the signature, same as in Bitcoin Core + optional bool is_bip322_simple = 10[default=false]; // use BIP-322 simple format for signing } /** @@ -621,3 +622,21 @@ message AuthorizeCoinJoin { optional AmountUnit amount_unit = 8 [default=BITCOIN]; // show amounts in } +/** + * Request: Ask device to sign a taproot transaction + * @start + * @next SignedPsbt + * @next Failure + */ + message SignPsbt { + required bytes psbt = 1; // PSBT to be signed + optional string coin_name = 2[default='Bitcoin']; +} + +/** + * Response: Contains the signed PSBT + * @end + */ +message SignedPsbt { + required bytes psbt = 1; // PSBT to be finalized +} diff --git a/common/protob/messages-cardano.proto b/common/protob/messages-cardano.proto index c19dc6383d..5843aa892f 100644 --- a/common/protob/messages-cardano.proto +++ b/common/protob/messages-cardano.proto @@ -56,6 +56,16 @@ enum CardanoCertificateType { STAKE_DEREGISTRATION = 1; STAKE_DELEGATION = 2; STAKE_POOL_REGISTRATION = 3; + STAKE_REGISTRATION_CONWAY = 7; + STAKE_DEREGISTRATION_CONWAY = 8; + VOTE_DELEGATION = 9; +} + +enum CardanoDRepType { + KEY_HASH = 0; + SCRIPT_HASH = 1; + ABSTAIN = 2; + NO_CONFIDENCE = 3; } enum CardanoPoolRelayType { @@ -66,10 +76,10 @@ enum CardanoPoolRelayType { enum CardanoTxAuxiliaryDataSupplementType { NONE = 0; - GOVERNANCE_REGISTRATION_SIGNATURE = 1; + CVOTE_REGISTRATION_SIGNATURE = 1; } -enum CardanoGovernanceRegistrationFormat { +enum CardanoCVoteRegistrationFormat { CIP15 = 0; CIP36 = 1; } @@ -164,6 +174,7 @@ message CardanoGetAddress { required uint32 network_id = 4; // network id - mainnet or testnet required CardanoAddressParametersType address_parameters = 5; // parameters used to derive the address required CardanoDerivationType derivation_type = 6; + optional bool chunkify = 7; // display the address in chunks of 4 characters } /** @@ -223,6 +234,8 @@ message CardanoSignTxInit { optional bool has_collateral_return = 19 [default=false]; optional uint64 total_collateral = 20; optional uint32 reference_inputs_count = 21 [default=0]; + optional bool chunkify = 22; // display the address in chunks of 4 characters + optional bool tag_cbor_sets = 23 [default=false]; // use tag 258 for sets in cbor } /** @@ -333,6 +346,16 @@ message CardanoPoolParametersType { required uint32 relays_count = 12; // number of pool relays } +/** + * DRep delegation parameters + * @embed +*/ +message CardanoDRep { + required CardanoDRepType type = 1; // drep type + optional bytes key_hash = 2; // drep key hash + optional bytes script_hash = 3; // drep script hash +} + /** * Request: Transaction certificate data * @next CardanoTxItemAck @@ -344,6 +367,8 @@ message CardanoTxCertificate { optional CardanoPoolParametersType pool_parameters = 4; // used for stake pool registration certificate optional bytes script_hash = 5; // stake credential script hash optional bytes key_hash = 6; // stake credential key hash + optional uint64 deposit = 7; // used for stake key registration certificate + optional CardanoDRep drep = 8; // used for vote delegation certificate } /** @@ -360,22 +385,23 @@ message CardanoTxWithdrawal { /** * @embed */ -message CardanoGovernanceRegistrationDelegation { - required bytes voting_public_key = 1; +message CardanoCVoteRegistrationDelegation { + required bytes vote_public_key = 1; required uint32 weight = 2; } /** * @embed */ -message CardanoGovernanceRegistrationParametersType { - optional bytes voting_public_key = 1; +message CardanoCVoteRegistrationParametersType { + optional bytes vote_public_key = 1; // mutually exclusive with delegations repeated uint32 staking_path = 2; - required CardanoAddressParametersType reward_address_parameters = 3; + optional CardanoAddressParametersType payment_address_parameters = 3; // mutually exclusive with payment_address required uint64 nonce = 4; - optional CardanoGovernanceRegistrationFormat format = 5 [default=CIP15]; - repeated CardanoGovernanceRegistrationDelegation delegations = 6; // mutually exclusive with voting_public_key; max 32 delegations + optional CardanoCVoteRegistrationFormat format = 5 [default=CIP15]; + repeated CardanoCVoteRegistrationDelegation delegations = 6; // mutually exclusive with vote_public_key; max 32 delegations optional uint64 voting_purpose = 7; + optional string payment_address = 8; // mutually exclusive with payment_address_parameters } /** @@ -384,7 +410,7 @@ message CardanoGovernanceRegistrationParametersType { * @next CardanoTxAuxiliaryDataSupplement */ message CardanoTxAuxiliaryData { - optional CardanoGovernanceRegistrationParametersType governance_registration_parameters = 1; + optional CardanoCVoteRegistrationParametersType cvote_registration_parameters = 1; optional bytes hash = 2; } @@ -452,7 +478,7 @@ message CardanoTxItemAck { message CardanoTxAuxiliaryDataSupplement { required CardanoTxAuxiliaryDataSupplementType type = 1; optional bytes auxiliary_data_hash = 2; - optional bytes governance_signature = 3; + optional bytes cvote_registration_signature = 3; } /** @@ -504,7 +530,7 @@ message CardanoSignTxFinished { * @next CardanoMessageSignature * @next Failure */ - message CardanoSignMessage { +message CardanoSignMessage { repeated uint32 address_n = 1; required bytes message = 2; required CardanoDerivationType derivation_type = 3; diff --git a/common/protob/messages-solana.proto b/common/protob/messages-solana.proto index fe32083fe4..5068fdaeb9 100644 --- a/common/protob/messages-solana.proto +++ b/common/protob/messages-solana.proto @@ -5,6 +5,21 @@ package hw.trezor.messages.solana; option java_package = "com.satoshilabs.trezor.lib.protobuf"; option java_outer_classname = "TrezorMessageSolana"; +/** + * Message version for Solana SignMessage + */ +enum SolanaOffChainMessageVersion { + MESSAGE_VERSION_0 = 0; +} + +/** + * Message format for Solana SignMessage + */ +enum SolanaOffChainMessageFormat { + V0_RESTRICTED_ASCII = 0; + V0_LIMITED_UTF8 = 1; +} + /** * Request: Address at the specified index * @start @@ -40,3 +55,37 @@ message SolanaSignTx { message SolanaSignedTx { required bytes signature = 1; // the signature of the raw transaction } + +/** + * Request: ask device to sign a off-chain message + * @start + * @next SolanaMessageSignature + * @next Failure + */ +message SolanaSignOffChainMessage { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + required bytes message = 2; // the message to sign + optional SolanaOffChainMessageVersion message_version = 3[default = MESSAGE_VERSION_0]; + optional SolanaOffChainMessageFormat message_format = 4[default = V0_RESTRICTED_ASCII]; + optional bytes application_domain = 5; // application domain must be 32 bytes +} + +/** + * Request: ask device to sign arbitrary message except valid solana transaction + * @start + * @next SolanaMessageSignature + * @next Failure + */ +message SolanaSignUnsafeMessage { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + required bytes message = 2; // the message to sign +} + +/** + * Response: signature for message signing + * @end + */ +message SolanaMessageSignature { + required bytes signature = 1; // the signature of the message + optional bytes public_key = 2; // the public key of the signer +} diff --git a/common/protob/messages.proto b/common/protob/messages.proto index 1f0261091f..54097aeb05 100644 --- a/common/protob/messages.proto +++ b/common/protob/messages.proto @@ -156,6 +156,8 @@ enum MessageType { MessageType_GetOwnershipProof = 49 [(bitcoin_only) = true, (wire_in) = true]; MessageType_OwnershipProof = 50 [(bitcoin_only) = true, (wire_out) = true]; MessageType_AuthorizeCoinJoin = 51 [(bitcoin_only) = true, (wire_in) = true]; + MessageType_SignPsbt = 10052 [(bitcoin_only) = true, (wire_in) = true]; + MessageType_SignedPsbt = 10053 [(bitcoin_only) = true, (wire_out) = true]; // Crypto MessageType_CipherKeyValue = 23 [(bitcoin_only) = true, (wire_in) = true]; @@ -455,6 +457,9 @@ enum MessageType { MessageType_SolanaAddress = 10101 [(wire_out) = true]; MessageType_SolanaSignTx = 10102 [(wire_in) = true]; MessageType_SolanaSignedTx = 10103 [(wire_out) = true]; + MessageType_SolanaSignOffChainMessage = 10104 [(wire_in) = true]; + MessageType_SolanaMessageSignature = 10105 [(wire_out) = true]; + MessageType_SolanaSignUnsafeMessage = 10106 [(wire_in) = true]; // Cosmos MessageType_CosmosGetAddress = 10800 [(wire_in) = true]; diff --git a/common/tools/coin_info.py b/common/tools/coin_info.py index d427c2e06d..553e3a4658 100755 --- a/common/tools/coin_info.py +++ b/common/tools/coin_info.py @@ -378,6 +378,8 @@ def _load_btc_coins() -> Coins: 61: "0x328332", 128: "0x01943F", 137: "0x8247E5", + 146: "0xFFFFFF", + 177: "0xFFFFFF", 250: "0x1969FF", 288: "0xCCFF00", 314: "0x0090FF", @@ -397,6 +399,7 @@ def _load_btc_coins() -> Coins: } EVM_ICON_NAME_OVERRIDE = { 10: "oeth", + 137: "matic", 288: "boba", 314: "filecoin", 324: "zksync-era", diff --git a/core/embed/bootloader/bootui.c b/core/embed/bootloader/bootui.c index fa1b473c1c..ae4745102c 100644 --- a/core/embed/bootloader/bootui.c +++ b/core/embed/bootloader/bootui.c @@ -60,29 +60,43 @@ extern secbool load_vendor_header_keys(const uint8_t *const data, #define BACKLIGHT_NORMAL 150 -#define COLOR_BL_BG COLOR_BLACK // background -#define COLOR_BL_FG COLOR_WHITE // foreground -#define COLOR_BL_FAIL RGB16(0xFF, 0x00, 0x00) // red -#define COLOR_BL_DANGER RGB16(0xFF, 0x11, 0x00) // onekey red -#define COLOR_BL_DONE RGB16(0x00, 0xFF, 0x33) // green -#define COLOR_BL_PROCESS RGB16(0x4A, 0x90, 0xE2) // blue -#define COLOR_BL_GRAY RGB16(0x99, 0x99, 0x99) // gray -#define COLOR_BL_DARK RGB16(0x2D, 0x2D, 0x2D) // gray -#define COLOR_BL_ICON RGB16(0x33, 0x33, 0x33) // gray -#define COLOR_BL_TAGVALUE RGB16(0xB4, 0xB4, 0xB4) // -#define COLOR_BL_SUBTITLE RGB16(0xD2, 0xD2, 0xD2) // - +#define COLOR_BL_BG COLOR_BLACK // background +#define COLOR_BL_FG COLOR_WHITE // foreground +#define COLOR_BL_FAIL RGB16(0xFF, 0x00, 0x00) // red +#define COLOR_BL_DANGER RGB16(0xFF, 0x11, 0x00) // onekey red +#define COLOR_BL_DONE RGB16(0x00, 0xFF, 0x33) // green +#define COLOR_BL_PROCESS RGB16(0x4A, 0x90, 0xE2) // blue +#define COLOR_BL_GRAY RGB16(0x99, 0x99, 0x99) // gray +#define COLOR_BL_DARK RGB16(0x2D, 0x2D, 0x2D) // gray +#define COLOR_BL_ICON RGB16(0x33, 0x33, 0x33) // gray +#define COLOR_BL_TAGVALUE RGB16(0xB4, 0xB4, 0xB4) // +#define COLOR_BL_SUBTITLE RGB16(0xD2, 0xD2, 0xD2) // +#define COLOR_BL_FG_INFO_ICON RGB16(0x96, 0x96, 0x96) // info icon foreground + +#define STATUS_BAR_HEIGHT 44 +#define LOGO_SIZE 128 +#define LOGO_OFFSET_X ((DISPLAY_RESX - LOGO_SIZE) / 2) +#define LOGO_OFFSET_Y (STATUS_BAR_HEIGHT + 92) + +#define INFO_ICON_SIZE 48 +#define INFO_ICON_OFFSET_X (DISPLAY_RESX - 20 - INFO_ICON_SIZE) +#define INFO_ICON_OFFSET_Y (STATUS_BAR_HEIGHT + 16) +#define CLOCK_AREA_EXPAND 10 +#define TITLE_OFFSET_Y (STATUS_BAR_HEIGHT + 274) +#define SUBTITLE_OFFSET_Y (STATUS_BAR_HEIGHT + 332) #define COLOR_WELCOME_BG COLOR_WHITE // welcome background #define COLOR_WELCOME_FG COLOR_BLACK // welcome foreground - +#define BUTTON_RADIUS 4 // common shared functions static void ui_confirm_cancel_buttons(void) { - display_bar_radius(9, 184, 108, 50, COLOR_BL_FAIL, COLOR_BL_BG, 4); + display_bar_radius(9, 184, 108, 50, COLOR_BL_FAIL, COLOR_BL_BG, + BUTTON_RADIUS); display_icon(9 + (108 - 16) / 2, 184 + (50 - 16) / 2, 16, 16, toi_icon_cancel + 12, sizeof(toi_icon_cancel) - 12, COLOR_BL_BG, COLOR_BL_FAIL); - display_bar_radius(123, 184, 108, 50, COLOR_BL_DONE, COLOR_BL_BG, 4); + display_bar_radius(123, 184, 108, 50, COLOR_BL_DONE, COLOR_BL_BG, + BUTTON_RADIUS); display_icon(123 + (108 - 19) / 2, 184 + (50 - 16) / 2, 20, 16, toi_icon_confirm + 12, sizeof(toi_icon_confirm) - 12, COLOR_BL_BG, COLOR_BL_DONE); @@ -103,9 +117,11 @@ static uint16_t boot_background; static bool ble_name_show = false; static int ui_bootloader_page_current = 0; -void ui_logo_center(void) { - display_image(203, 56, 74, 74, toi_icon_onekey_74x74 + 12, - sizeof(toi_icon_onekey_74x74) - 12); +int get_ui_bootloader_page_current(void) { return ui_bootloader_page_current; } + +void ui_logo_onekey(void) { + display_image(LOGO_OFFSET_X, LOGO_OFFSET_Y, LOGO_SIZE, LOGO_SIZE, + toi_icon_onekey + 12, sizeof(toi_icon_onekey) - 12); } void ui_screen_boot(const vendor_header *const vhdr, @@ -167,7 +183,7 @@ void ui_screen_boot_wait(int wait_seconds) { mini_snprintf(wait_str, sizeof(wait_str), "Starting in %d s", wait_seconds); display_bar(0, DISPLAY_RESY - 5 - 20, DISPLAY_RESX, 5 + 20, boot_background); #if PRODUCTION_MODEL == 'H' - ui_title_update(); + ui_statusbar_update(); display_bar(0, 600, DISPLAY_RESX, 100, boot_background); display_text_center(DISPLAY_RESX / 2, 655, wait_str, -1, FONT_NORMAL, COLOR_BL_FG, boot_background); @@ -273,7 +289,8 @@ void ui_screen_firmware_fingerprint(const image_header *const hdr) { FONT_NORMAL, COLOR_BL_FG, COLOR_BL_BG); } - display_bar_radius(9, 184, 222, 50, COLOR_BL_DONE, COLOR_BL_BG, 4); + display_bar_radius(9, 184, 222, 50, COLOR_BL_DONE, COLOR_BL_BG, + BUTTON_RADIUS); display_icon(9 + (222 - 19) / 2, 184 + (50 - 16) / 2, 20, 16, toi_icon_confirm + 12, sizeof(toi_icon_confirm) - 12, COLOR_BL_BG, COLOR_BL_DONE); @@ -319,8 +336,8 @@ void ui_screen_install_confirm_newvendor_or_downgrade_wipe( } display_clear(); - ui_title_update(); - ui_logo_center(); + ui_statusbar_update(); + ui_logo_onekey(); display_text_center( MAX_DISPLAY_RESX / 2, 190, @@ -380,15 +397,15 @@ void ui_screen_install_confirm_newvendor_or_downgrade_wipe( } void ui_screen_progress_bar_prepare(char *title, char *notes) { - ui_title_update(); - ui_logo_center(); + ui_statusbar_update(); + ui_logo_onekey(); ui_screen_progress_bar_update(title, notes, -1); } void ui_screen_progress_bar_update(char *title, char *notes, int progress) { if (title != NULL) - display_text_center(DISPLAY_RESX / 2, 180, title, -1, FONT_PJKS_BOLD_38, - COLOR_BL_FG, COLOR_BL_BG); + display_text_center(DISPLAY_RESX / 2, TITLE_OFFSET_Y, title, -1, + FONT_PJKS_BOLD_38, COLOR_BL_FG, COLOR_BL_BG); if ((progress >= 0) || (progress <= 100)) { display_bar(60, 740, 360, 12, COLOR_WHITE); @@ -411,9 +428,9 @@ void ui_screen_progress_bar_update(char *title, char *notes, int progress) { void ui_screen_install_start(void) { #if PRODUCTION_MODEL == 'H' - ui_title_update(); - ui_logo_center(); - display_text_center(DISPLAY_RESX / 2, 180, "Installing", -1, + ui_statusbar_update(); + ui_logo_onekey(); + display_text_center(DISPLAY_RESX / 2, SUBTITLE_OFFSET_Y, "Installing", -1, FONT_PJKS_BOLD_38, COLOR_BL_FG, COLOR_BL_BG); #else display_clear(); @@ -433,9 +450,9 @@ void ui_screen_install_start(void) { void ui_screen_install_progress_erase(int pos, int len) { #if PRODUCTION_MODEL == 'H' - ui_title_update(); - ui_logo_center(); - display_text_center(DISPLAY_RESX / 2, 180, "Installing", -1, + ui_statusbar_update(); + ui_logo_onekey(); + display_text_center(DISPLAY_RESX / 2, SUBTITLE_OFFSET_Y, "Installing", -1, FONT_PJKS_BOLD_38, COLOR_BL_FG, COLOR_BL_BG); #endif display_loader(250 * pos / len, false, -20, COLOR_BL_PROCESS, COLOR_BL_BG, @@ -449,9 +466,9 @@ void ui_screen_install_progress_erase(int pos, int len) { void ui_screen_install_progress_upload(int pos) { #if PRODUCTION_MODEL == 'H' - ui_title_update(); - ui_logo_center(); - display_text_center(DISPLAY_RESX / 2, 180, "Installing", -1, + ui_statusbar_update(); + ui_logo_onekey(); + display_text_center(DISPLAY_RESX / 2, SUBTITLE_OFFSET_Y, "Installing", -1, FONT_PJKS_BOLD_38, COLOR_BL_FG, COLOR_BL_BG); #endif display_loader(pos, false, -20, COLOR_BL_PROCESS, COLOR_BL_BG, @@ -485,14 +502,16 @@ void ui_screen_wipe_confirm(void) { void ui_screen_wipe(void) { #if PRODUCTION_MODEL == 'H' display_clear(); - ui_title_update(); - ui_logo_center(); - display_text_center(DISPLAY_RESX / 2, 190, "Wipe Device", -1, + ui_statusbar_update(); + ui_logo_onekey(); + display_text_center(DISPLAY_RESX / 2, SUBTITLE_OFFSET_Y, "Wipe Device", -1, FONT_PJKS_BOLD_38, COLOR_BL_FG, COLOR_BL_BG); - display_text_center(DISPLAY_RESX / 2, 240, "Do you want to wipe the device?", - -1, FONT_NORMAL, COLOR_BL_SUBTITLE, COLOR_BL_BG); - display_text_center(DISPLAY_RESX / 2, 268, "Recovery phrase will be erased", - -1, FONT_NORMAL, COLOR_BL_SUBTITLE, COLOR_BL_BG); + display_text_center(DISPLAY_RESX / 2, SUBTITLE_OFFSET_Y + 50, + "Do you want to wipe the device?", -1, FONT_NORMAL, + COLOR_BL_SUBTITLE, COLOR_BL_BG); + display_text_center(DISPLAY_RESX / 2, SUBTITLE_OFFSET_Y + 78, + "Recovery phrase will be erased", -1, FONT_NORMAL, + COLOR_BL_SUBTITLE, COLOR_BL_BG); #else display_clear(); #endif @@ -527,7 +546,7 @@ void ui_screen_done(int restart_seconds, secbool full_redraw) { if (sectrue == full_redraw) { #if PRODUCTION_MODEL == 'H' display_clear(); - ui_title_update(); + ui_statusbar_update(); display_image(203, 56, 74, 74, toi_icon_onekey_74x74 + 12, sizeof(toi_icon_onekey_74x74) - 12); #else @@ -558,8 +577,8 @@ void ui_screen_done(int restart_seconds, secbool full_redraw) { void ui_screen_fail(void) { #if PRODUCTION_MODEL == 'H' display_bar(0, DISPLAY_RESY / 2, DISPLAY_RESX, DISPLAY_RESY, COLOR_BL_BG); - ui_title_update(); - ui_logo_center(); + ui_statusbar_update(); + ui_logo_onekey(); #else display_clear(); #endif @@ -642,6 +661,13 @@ int ui_input_poll(int zones, bool poll) { y < 694 + 98) { return INPUT_CONFIRM; } + // clicked on Info icon + if ((zones & INPUT_INFO) && x >= INFO_ICON_OFFSET_X - CLOCK_AREA_EXPAND && + x < INFO_ICON_OFFSET_X + INFO_ICON_SIZE + CLOCK_AREA_EXPAND && + y > INFO_ICON_OFFSET_Y - CLOCK_AREA_EXPAND && + y < INFO_ICON_OFFSET_Y + INFO_ICON_SIZE + CLOCK_AREA_EXPAND) { + return INPUT_INFO; + } // clicked on next button if ((zones & INPUT_NEXT) && x >= 8 && x < 8 + 464 && y > 694 && y < 694 + 98) { @@ -668,7 +694,7 @@ int ui_input_poll(int zones, bool poll) { return 0; } -void ui_title_update(void) { +void ui_statusbar_update(void) { char battery_str[8] = {0}; uint32_t len = 0; uint32_t offset_x = 8; @@ -740,19 +766,21 @@ void ui_title_update(void) { } void ui_wipe_confirm(const image_header *const hdr) { - ui_title_update(); - ui_logo_center(); + ui_statusbar_update(); + ui_logo_onekey(); // if (hdr && (hdr->onekey_version != 0)) { // const char *ver_str = format_ver("v%d.%d.%d", (hdr->onekey_version)); // display_text_center(DISPLAY_RESX / 2, 246, ver_str, -1, FONT_NORMAL, // COLOR_BL_GRAY, COLOR_BL_BG); // } - display_text_center(DISPLAY_RESX / 2, 190, "Wipe Device", -1, + display_text_center(DISPLAY_RESX / 2, TITLE_OFFSET_Y, "Wipe Device", -1, FONT_PJKS_BOLD_38, COLOR_BL_FG, COLOR_BL_BG); - display_text_center(DISPLAY_RESX / 2, 240, "Do you want to wipe the device?", - -1, FONT_NORMAL, COLOR_BL_SUBTITLE, COLOR_BL_BG); - display_text_center(DISPLAY_RESX / 2, 268, "Recovery phrase will be erased", - -1, FONT_NORMAL, COLOR_BL_SUBTITLE, COLOR_BL_BG); + display_text_center(DISPLAY_RESX / 2, SUBTITLE_OFFSET_Y + 50, + "Do you want to wipe the device?", -1, FONT_NORMAL, + COLOR_BL_SUBTITLE, COLOR_BL_BG); + display_text_center(DISPLAY_RESX / 2, SUBTITLE_OFFSET_Y + 78, + "Recovery phrase will be erased", -1, FONT_NORMAL, + COLOR_BL_SUBTITLE, COLOR_BL_BG); display_bar(8, 694, 231, 98, COLOR_BL_ICON); display_text_center(DISPLAY_RESX / 4, 755, "Cancel", -1, FONT_PJKS_BOLD_26, COLOR_BL_FG, COLOR_BL_ICON); @@ -764,21 +792,22 @@ void ui_wipe_confirm(const image_header *const hdr) { void ui_install_confirm(image_header *current_hdr, const image_header *const new_hdr) { if ((current_hdr == NULL) || (new_hdr == NULL)) return; - ui_title_update(); - ui_logo_center(); - display_text_center(DISPLAY_RESX / 2, 190, "System Update", -1, + ui_statusbar_update(); + ui_logo_onekey(); + display_text_center(DISPLAY_RESX / 2, TITLE_OFFSET_Y, "System Update", -1, FONT_PJKS_BOLD_38, COLOR_BL_FG, COLOR_BL_BG); - display_text_center(DISPLAY_RESX / 2, 240, "Install firmware by OneKey?", -1, - FONT_NORMAL, COLOR_BL_SUBTITLE, COLOR_BL_BG); + display_text_center(DISPLAY_RESX / 2, SUBTITLE_OFFSET_Y, + "Install firmware by OneKey?", -1, FONT_NORMAL, + COLOR_BL_SUBTITLE, COLOR_BL_BG); const char *ver_str = format_ver("%d.%d.%d", current_hdr->onekey_version); - display_text_right(DISPLAY_RESX / 2 - 25, 320, ver_str, -1, FONT_NORMAL, - COLOR_BL_SUBTITLE, COLOR_BL_BG); + display_text_right(DISPLAY_RESX / 2 - 25, SUBTITLE_OFFSET_Y + 90, ver_str, -1, + FONT_NORMAL, COLOR_BL_SUBTITLE, COLOR_BL_BG); ver_str = format_ver("%d.%d.%d", new_hdr->onekey_version); - display_text(DISPLAY_RESX / 2 + 25, 320, ver_str, -1, FONT_NORMAL, - COLOR_BL_SUBTITLE, COLOR_BL_BG); + display_text(DISPLAY_RESX / 2 + 25, SUBTITLE_OFFSET_Y + 90, ver_str, -1, + FONT_NORMAL, COLOR_BL_SUBTITLE, COLOR_BL_BG); - display_image(231, 303, 17, 14, toi_icon_arrow_right + 12, + display_image(231, SUBTITLE_OFFSET_Y + 70, 17, 14, toi_icon_arrow_right + 12, sizeof(toi_icon_arrow_right) - 12); display_bar(8, 694, 231, 98, COLOR_BL_ICON); @@ -791,17 +820,17 @@ void ui_install_confirm(image_header *current_hdr, void ui_install_ble_confirm(void) { char str[128] = {0}; - ui_title_update(); - ui_logo_center(); - display_text_center(DISPLAY_RESX / 2, 190, "Bluetooth Update", -1, + ui_statusbar_update(); + ui_logo_onekey(); + display_text_center(DISPLAY_RESX / 2, TITLE_OFFSET_Y, "Bluetooth Update", -1, FONT_PJKS_BOLD_38, COLOR_BL_FG, COLOR_BL_BG); - display_text_center(DISPLAY_RESX / 2, 240, + display_text_center(DISPLAY_RESX / 2, SUBTITLE_OFFSET_Y, "A new bluetooth firmware is avaliable! The", -1, FONT_NORMAL, COLOR_BL_SUBTITLE, COLOR_BL_BG); strcat(str, "current version is "); strcat(str, ble_get_ver()); - display_text_center(DISPLAY_RESX / 2, 268, str, -1, FONT_NORMAL, - COLOR_BL_SUBTITLE, COLOR_BL_BG); + display_text_center(DISPLAY_RESX / 2, SUBTITLE_OFFSET_Y + 30, str, -1, + FONT_NORMAL, COLOR_BL_SUBTITLE, COLOR_BL_BG); display_bar(8, 694, 231, 98, COLOR_BL_ICON); display_text_center(DISPLAY_RESX / 4, 755, "Cancel", -1, FONT_PJKS_BOLD_26, @@ -814,27 +843,30 @@ void ui_install_ble_confirm(void) { void ui_bootloader_first(const image_header *const hdr) { ui_bootloader_page_current = 0; - ui_title_update(); - ui_logo_center(); - display_text_center(DISPLAY_RESX / 2, 190, "Update Mode", -1, + ui_statusbar_update(); + // info icon 48 * 48 - the entry point of the bootloader details + display_icon(INFO_ICON_OFFSET_X, INFO_ICON_OFFSET_Y, INFO_ICON_SIZE, + INFO_ICON_SIZE, toi_ok_icon_info + 12, + sizeof(toi_ok_icon_info) - 12, COLOR_BL_FG_INFO_ICON, + COLOR_BL_BG); + ui_logo_onekey(); + display_text_center(DISPLAY_RESX / 2, TITLE_OFFSET_Y, "Update Mode", -1, FONT_PJKS_BOLD_38, COLOR_BL_FG, COLOR_BL_BG); if (ble_name_state()) { char *ble_name; ble_name = ble_get_name(); - display_text_center(DISPLAY_RESX / 2, 240, ble_name, -1, FONT_NORMAL, - COLOR_BL_SUBTITLE, COLOR_BL_BG); + display_text_center(DISPLAY_RESX / 2, SUBTITLE_OFFSET_Y, ble_name, -1, + FONT_NORMAL, COLOR_BL_SUBTITLE, COLOR_BL_BG); ble_name_show = true; } + display_text_center(DISPLAY_RESX / 2, DISPLAY_RESY - 92, "SafeOS", -1, + FONT_PJKS_BOLD_38, COLOR_BL_FG, COLOR_BL_BG); if (hdr) { const char *ver_str = format_ver("%d.%d.%d", (hdr->onekey_version)); - display_text_center(DISPLAY_RESX / 2, DISPLAY_RESY - 125, ver_str, -1, + display_text_center(DISPLAY_RESX / 2, DISPLAY_RESY - 50, ver_str, -1, FONT_NORMAL, COLOR_BL_SUBTITLE, COLOR_BL_BG); } - - display_bar(8, 694, 464, 98, COLOR_BL_ICON); - display_text_center(DISPLAY_RESX / 2, 755, "View Details", -1, - FONT_PJKS_BOLD_26, COLOR_BL_FG, COLOR_BL_ICON); } void ui_bootloader_view_details(const image_header *const hdr) { @@ -843,7 +875,7 @@ void ui_bootloader_view_details(const image_header *const hdr) { int offset_x = 8, offset_y = 90, offset_seg = 44, offset_line = 30; const char *ver_str = NULL; - ui_title_update(); + ui_statusbar_update(); display_text(offset_x, offset_y, "Model", -1, FONT_PJKS_BOLD_26, COLOR_BL_FG, COLOR_BL_BG); offset_y += offset_line; @@ -929,7 +961,7 @@ void ui_bootloader_view_details(const image_header *const hdr) { void ui_bootloader_restart_confirm(void) { ui_bootloader_page_current = 4; - ui_title_update(); + ui_statusbar_update(); int title_offset_y = 90; int title_offset_x = 12; display_text(title_offset_x, title_offset_y, "Restart Device?", -1, @@ -977,8 +1009,8 @@ void ui_bootloader_page_switch(const image_header *const hdr) { static uint32_t click = 0, click_pre = 0, click_now = 0; if (ui_bootloader_page_current == 0) { - response = ui_input_poll(INPUT_NEXT, false); - if (INPUT_NEXT == response) { + response = ui_input_poll(INPUT_INFO, false); + if (INPUT_INFO == response) { display_clear(); ui_bootloader_view_details(hdr); } diff --git a/core/embed/bootloader/bootui.h b/core/embed/bootloader/bootui.h index ff745ec859..c3ee0edb96 100644 --- a/core/embed/bootloader/bootui.h +++ b/core/embed/bootloader/bootui.h @@ -84,7 +84,7 @@ enum BAT_LEVEL { // clang-format on -void ui_title_update(void); +void ui_statusbar_update(void); int ui_user_input(int zones); int ui_input_poll(int zones, bool poll); void ui_bootloader_first(const image_header* const hdr); @@ -96,5 +96,5 @@ void ui_install_ble_confirm(void); void ui_install_progress(image_header* current_hdr, const image_header* const new_hdr); void ui_bootloader_page_switch(const image_header* const hdr); - +int get_ui_bootloader_page_current(void); #endif diff --git a/core/embed/bootloader/icon_onekey.h b/core/embed/bootloader/icon_onekey.h index 01ad447522..15c3955a83 100644 --- a/core/embed/bootloader/icon_onekey.h +++ b/core/embed/bootloader/icon_onekey.h @@ -175,3 +175,13 @@ static const uint8_t toi_icon_arrow_right[] = { 0x63, 0x60, 0x40, 0x06, 0x5E, 0x99, 0x7B, 0xAF, 0xEF, 0xBD, 0xEE, 0x95, 0xC9, 0x80, 0x07, 0x64, 0xFB, 0x5E, 0x9B, 0x75, 0x6D, 0x16, 0xB5, 0xD4, 0x44, 0x9D, 0x26, 0xAC, 0x26, 0xDB, 0x97, 0x38, 0x35, 0x2D, 0x02, 0x20, 0x16, 0x6E, 0xE8, 0xC4, 0x41, 0x48, 0x85, 0xE1, 0x32, 0x72, 0x7D, 0x34, 0xD0, 0xF2, 0x51, 0xAF, 0x31, 0xE5, 0x01, }; +static const uint8_t toi_ok_icon_info[] = { + // magic + 'T', 'O', 'I', 'g', + // width (16-bit), height (16-bit) + 0x30, 0x00, 0x30, 0x00, + // compressed data length (32-bit) + 0x69, 0x01, 0x00, 0x00, + // compressed data + 0xbd, 0x52, 0x3b, 0x4e, 0xc3, 0x40, 0x10, 0xb5, 0x2d, 0x3e, 0x96, 0x80, 0x68, 0x0f, 0x80, 0x22, 0x37, 0x74, 0x20, 0x2a, 0x68, 0x41, 0xf4, 0x44, 0x08, 0x6a, 0x63, 0x24, 0x7a, 0xe0, 0x06, 0xe1, 0x02, 0x90, 0x0b, 0x80, 0xb8, 0x02, 0x37, 0x00, 0x4a, 0xaa, 0x80, 0x14, 0x24, 0xa8, 0x96, 0x14, 0x46, 0xe9, 0x2c, 0x84, 0x20, 0x41, 0xc4, 0x7e, 0xec, 0x7a, 0x76, 0x6c, 0xaf, 0xa1, 0xce, 0x34, 0xef, 0xed, 0xdb, 0xdd, 0x99, 0xd9, 0xd9, 0xe7, 0x38, 0x13, 0x8a, 0xb5, 0xcb, 0x24, 0xbb, 0x3f, 0xac, 0xab, 0xee, 0x11, 0xf2, 0x78, 0x0e, 0x6c, 0xb9, 0x0d, 0x13, 0x23, 0x6b, 0x63, 0x1f, 0x45, 0x3c, 0x55, 0xe4, 0x59, 0xb5, 0x1e, 0x6c, 0x09, 0x77, 0xe9, 0x5c, 0x91, 0x93, 0x32, 0xcb, 0x8d, 0x3a, 0x26, 0x72, 0x1a, 0x02, 0x63, 0xc1, 0xfa, 0x3c, 0x10, 0x33, 0x8f, 0x80, 0x6b, 0xe6, 0x6d, 0xa4, 0x45, 0x35, 0x57, 0x62, 0x68, 0xe8, 0x34, 0x70, 0x57, 0xd6, 0x5a, 0x04, 0x76, 0x88, 0xad, 0x20, 0x15, 0xa5, 0xee, 0x49, 0xf4, 0x89, 0x75, 0xf0, 0x9e, 0x63, 0xab, 0x97, 0x43, 0xd3, 0x24, 0xf2, 0xf8, 0xa2, 0xcc, 0x38, 0x6d, 0x7e, 0xdd, 0x47, 0x4a, 0xf7, 0x12, 0x83, 0x12, 0x07, 0x1a, 0x16, 0xf0, 0x49, 0xeb, 0x30, 0xe6, 0x56, 0x5f, 0x35, 0x6c, 0x10, 0x94, 0xd1, 0xc4, 0x87, 0x86, 0x63, 0x9c, 0xda, 0xba, 0x4f, 0x09, 0xae, 0x4c, 0xd9, 0x16, 0x7a, 0xfc, 0x9e, 0x6f, 0x0d, 0x5d, 0x04, 0xa6, 0x5c, 0x66, 0x5e, 0x40, 0x8d, 0x24, 0xd4, 0x96, 0x42, 0x98, 0x51, 0xd0, 0x01, 0x5e, 0x86, 0x4c, 0x8c, 0x52, 0x5b, 0x16, 0x84, 0xf3, 0x38, 0xb5, 0x3c, 0xd2, 0xd4, 0x2d, 0x74, 0x0f, 0x63, 0xea, 0x73, 0xd3, 0xd6, 0xa7, 0xa8, 0xcf, 0x88, 0xdf, 0xc5, 0xba, 0x8f, 0x2f, 0x9a, 0x43, 0xdf, 0xd6, 0x1b, 0x34, 0x87, 0x06, 0xcf, 0x8d, 0xf5, 0x08, 0x0f, 0x1a, 0x66, 0x78, 0xce, 0xac, 0x4b, 0x4a, 0xec, 0xf2, 0xbf, 0x98, 0xf9, 0xf3, 0xbf, 0x28, 0x3b, 0x50, 0x81, 0x6d, 0x9a, 0xff, 0x2a, 0x46, 0x3c, 0xef, 0x34, 0xb0, 0xfe, 0xfd, 0xf1, 0x3f, 0x9f, 0x2c, 0x83, 0xdf, 0xa3, 0x7d, 0x25, 0x2a, 0xc7, 0xd9, 0x57, 0xce, 0x5c, 0xc5, 0x87, 0xca, 0xd9, 0xb7, 0x85, 0xf7, 0xba, 0x30, 0x7f, 0xe5, 0xaa, 0x61, 0xff, 0x08, 0xdb, 0xe7, 0xbb, 0x7f, 0x7c, 0xae, 0x5a, 0x44, 0x11, 0x71, 0xd5, 0x02, 0x5e, 0x87, 0xe5, 0xa1, 0xb0, 0xbc, 0xe1, 0x9d, 0x91, 0xfc, 0x66, 0xcb, 0x2a, 0xd6, 0x2f, 0x92, 0xec, 0x65, 0xcf, 0x99, 0x54, 0xfc, 0x02, +}; diff --git a/core/embed/bootloader/main.c b/core/embed/bootloader/main.c index 449beb9e1d..c01ee0fca3 100644 --- a/core/embed/bootloader/main.c +++ b/core/embed/bootloader/main.c @@ -344,11 +344,13 @@ static secbool bootloader_usb_loop(const vendor_header* const vhdr, else if (ble_power_button_state() == 2) // long press { // give a way to go back to bootloader home page - ble_power_button_state_clear(); - ui_progress_bar_visible_clear(); - ui_fadeout(); - ui_bootloader_first(NULL); - ui_fadein(); + if (get_ui_bootloader_page_current() != 0) { + ble_power_button_state_clear(); + ui_progress_bar_visible_clear(); + ui_fadeout(); + ui_bootloader_first(NULL); + ui_fadein(); + } memzero(buf, USB_PACKET_SIZE); continue; } @@ -357,7 +359,7 @@ static secbool bootloader_usb_loop(const vendor_header* const vhdr, ui_bootloader_page_switch(hdr); static uint32_t tickstart = 0; if ((HAL_GetTick() - tickstart) >= 1000) { - ui_title_update(); + ui_statusbar_update(); tickstart = HAL_GetTick(); } continue; @@ -470,7 +472,21 @@ static secbool bootloader_usb_loop(const vendor_header* const vhdr, process_msg_Reboot(USB_IFACE_NUM, msg_size, buf); break; case MSG_NAME_TO_ID(FirmwareUpdateEmmc): // FirmwareUpdateEmmc - process_msg_FirmwareUpdateEmmc(USB_IFACE_NUM, msg_size, buf); + r = process_msg_FirmwareUpdateEmmc(USB_IFACE_NUM, msg_size, buf); + if (r < 0 && r != -4) { // error + ui_fadeout(); + ui_screen_fail(); + ui_fadein(); + while (!touch_click()) { + hal_delay(10); + } + bluetooth_reset(); + // make sure we have latest bluetooth status (and wait for bluetooth + // become ready) + ble_refresh_dev_info(); + reboot_to_boot(); + return secfalse; // shutdown + } break; case MSG_NAME_TO_ID(EmmcFixPermission): // EmmcFixPermission process_msg_EmmcFixPermission(USB_IFACE_NUM, msg_size, buf); diff --git a/core/embed/firmware/version.h b/core/embed/firmware/version.h index 1e73e2ca0f..44767e49fd 100644 --- a/core/embed/firmware/version.h +++ b/core/embed/firmware/version.h @@ -9,8 +9,8 @@ #define FIX_VERSION_BUILD 99 #define ONEKEY_VERSION_MAJOR 4 -#define ONEKEY_VERSION_MINOR 9 -#define ONEKEY_VERSION_PATCH 1 +#define ONEKEY_VERSION_MINOR 10 +#define ONEKEY_VERSION_PATCH 0 #define ONEKEY_VERSION_BUILD 0 -#define ONEKEY_VERSION "4.9.1" +#define ONEKEY_VERSION "4.10.0" diff --git a/core/src/all_modules.py b/core/src/all_modules.py index d5d025d4b5..b36b1ce0b1 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -127,6 +127,10 @@ import trezor.enums.SafetyCheckLevel trezor.enums.SdProtectOperationType import trezor.enums.SdProtectOperationType +trezor.enums.SolanaOffChainMessageFormat +import trezor.enums.SolanaOffChainMessageFormat +trezor.enums.SolanaOffChainMessageVersion +import trezor.enums.SolanaOffChainMessageVersion trezor.enums.TonWalletVersion import trezor.enums.TonWalletVersion trezor.enums.TonWorkChain @@ -167,6 +171,8 @@ import trezor.lvglui.i18n.locales.ja trezor.lvglui.i18n.locales.ko import trezor.lvglui.i18n.locales.ko +trezor.lvglui.i18n.locales.pt_br +import trezor.lvglui.i18n.locales.pt_br trezor.lvglui.i18n.locales.ru import trezor.lvglui.i18n.locales.ru trezor.lvglui.i18n.locales.zh_cn @@ -433,6 +439,8 @@ import apps.bitcoin.authorization apps.bitcoin.authorize_coinjoin import apps.bitcoin.authorize_coinjoin +apps.bitcoin.bip322_simple +import apps.bitcoin.bip322_simple apps.bitcoin.common import apps.bitcoin.common apps.bitcoin.get_address @@ -449,6 +457,18 @@ import apps.bitcoin.multisig apps.bitcoin.ownership import apps.bitcoin.ownership +apps.bitcoin.psbt +import apps.bitcoin.psbt +apps.bitcoin.psbt.key +import apps.bitcoin.psbt.key +apps.bitcoin.psbt.psbt +import apps.bitcoin.psbt.psbt +apps.bitcoin.psbt.script +import apps.bitcoin.psbt.script +apps.bitcoin.psbt.serialize +import apps.bitcoin.psbt.serialize +apps.bitcoin.psbt.tx +import apps.bitcoin.psbt.tx apps.bitcoin.readers import apps.bitcoin.readers apps.bitcoin.scripts @@ -457,6 +477,8 @@ import apps.bitcoin.scripts_decred apps.bitcoin.sign_message import apps.bitcoin.sign_message +apps.bitcoin.sign_taproot +import apps.bitcoin.sign_taproot apps.bitcoin.sign_tx import apps.bitcoin.sign_tx apps.bitcoin.sign_tx.approvers @@ -731,8 +753,12 @@ import apps.solana.message apps.solana.publickey import apps.solana.publickey +apps.solana.sign_offchain_message +import apps.solana.sign_offchain_message apps.solana.sign_tx import apps.solana.sign_tx +apps.solana.sign_unsafe_message +import apps.solana.sign_unsafe_message apps.solana.spl._layouts import apps.solana.spl._layouts apps.solana.spl._layouts.token_instructions @@ -875,12 +901,14 @@ import trezor.enums.BinanceTimeInForce trezor.enums.CardanoAddressType import trezor.enums.CardanoAddressType + trezor.enums.CardanoCVoteRegistrationFormat + import trezor.enums.CardanoCVoteRegistrationFormat trezor.enums.CardanoCertificateType import trezor.enums.CardanoCertificateType + trezor.enums.CardanoDRepType + import trezor.enums.CardanoDRepType trezor.enums.CardanoDerivationType import trezor.enums.CardanoDerivationType - trezor.enums.CardanoGovernanceRegistrationFormat - import trezor.enums.CardanoGovernanceRegistrationFormat trezor.enums.CardanoNativeScriptHashDisplayFormat import trezor.enums.CardanoNativeScriptHashDisplayFormat trezor.enums.CardanoNativeScriptType diff --git a/core/src/apps/bitcoin/bip322_simple.py b/core/src/apps/bitcoin/bip322_simple.py new file mode 100644 index 0000000000..8156ddc098 --- /dev/null +++ b/core/src/apps/bitcoin/bip322_simple.py @@ -0,0 +1,134 @@ +from . import writers +from .common import create_hashwriter, tagged_hashwriter +from .scripts import write_output_script_p2pkh + +UTXO = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +TAG = b"BIP0322-signed-message" + + +def create_to_spend(message: bytes, script_pub: bytes) -> bytes: + tag_hash_writer = tagged_hashwriter(TAG) + tag_hash_writer.write(message) + message_hash = tag_hash_writer.get_digest() + script_sig = b"\x00\x20" + message_hash + h_tx = create_hashwriter() + # nVersion + writers.write_uint32(h_tx, 0) + # inputs + writers.write_compact_size(h_tx, 1) + writers.write_bytes_reversed(h_tx, UTXO, writers.TX_HASH_SIZE) + writers.write_uint32(h_tx, 0xFFFFFFFF) + writers.write_bytes_prefixed(h_tx, script_sig) + writers.write_uint32(h_tx, 0) + # outputs + writers.write_compact_size(h_tx, 1) + writers.write_uint64(h_tx, 0) + writers.write_bytes_prefixed(h_tx, script_pub) + # nLockTime + writers.write_uint32(h_tx, 0) + + return writers.get_tx_hash(h_tx, double=True, reverse=True) + + +def sighash_bip341(message: bytes, script_pub: bytes) -> bytes: + tx_id = create_to_spend(message, script_pub) + h_sigmsg = tagged_hashwriter(b"TapSighash") + h_prevouts = create_hashwriter() + h_amounts = create_hashwriter() + h_scriptpubkeys = create_hashwriter() + h_sequences = create_hashwriter() + h_outputs = create_hashwriter() + # sighash epoch 0 + writers.write_uint8(h_sigmsg, 0) + + # nHashType + writers.write_uint8(h_sigmsg, 0) + + # nVersion + writers.write_uint32(h_sigmsg, 0) + + # nLockTime + writers.write_uint32(h_sigmsg, 0) + + # sha_prevouts + writers.write_bytes_reversed(h_prevouts, tx_id, writers.TX_HASH_SIZE) + writers.write_uint32(h_prevouts, 0) + writers.write_bytes_fixed(h_sigmsg, h_prevouts.get_digest(), writers.TX_HASH_SIZE) + + # sha_amounts + writers.write_uint64(h_amounts, 0) + writers.write_bytes_fixed(h_sigmsg, h_amounts.get_digest(), writers.TX_HASH_SIZE) + + # sha_scriptpubkeys + writers.write_bytes_prefixed(h_scriptpubkeys, script_pub) + writers.write_bytes_fixed( + h_sigmsg, h_scriptpubkeys.get_digest(), writers.TX_HASH_SIZE + ) + + # sha_sequences + writers.write_uint32(h_sequences, 0) + writers.write_bytes_fixed(h_sigmsg, h_sequences.get_digest(), writers.TX_HASH_SIZE) + + # sha_outputs + writers.write_uint64(h_outputs, 0) + writers.write_bytes_prefixed(h_outputs, b"\x6a") + writers.write_bytes_fixed(h_sigmsg, h_outputs.get_digest(), writers.TX_HASH_SIZE) + + # spend_type 0 (no tapscript message extension, no annex) + writers.write_uint8(h_sigmsg, 0) + + # input_index + writers.write_uint32(h_sigmsg, 0) + + return h_sigmsg.get_digest() + + +def sighash_bip143( + message: bytes, script_pub: bytes, pubkeyhash: bytes, sign_hash_double: bool = True +) -> bytes: + tx_id = create_to_spend(message, script_pub) + h_preimage = create_hashwriter() + h_prevouts = create_hashwriter() + h_sequences = create_hashwriter() + h_outputs = create_hashwriter() + + # nVersion + writers.write_uint32(h_preimage, 0) + + # hashPrevouts + writers.write_bytes_reversed(h_prevouts, tx_id, writers.TX_HASH_SIZE) + writers.write_uint32(h_prevouts, 0) + prevouts_hash = writers.get_tx_hash(h_prevouts, double=sign_hash_double) + writers.write_bytes_fixed(h_preimage, prevouts_hash, writers.TX_HASH_SIZE) + + # hashSequence + writers.write_uint32(h_sequences, 0) + sequence_hash = writers.get_tx_hash(h_sequences, double=sign_hash_double) + writers.write_bytes_fixed(h_preimage, sequence_hash, writers.TX_HASH_SIZE) + + # outpoint + writers.write_bytes_reversed(h_preimage, tx_id, writers.TX_HASH_SIZE) + writers.write_uint32(h_preimage, 0) + + # scriptCode + write_output_script_p2pkh(h_preimage, pubkeyhash, prefixed=True) + + # amount + writers.write_uint64(h_preimage, 0) + + # nSequence + writers.write_uint32(h_preimage, 0) + + # hashOutputs + writers.write_uint64(h_outputs, 0) + writers.write_bytes_prefixed(h_outputs, b"\x6a") + outputs_hash = writers.get_tx_hash(h_outputs, double=sign_hash_double) + writers.write_bytes_fixed(h_preimage, outputs_hash, writers.TX_HASH_SIZE) + + # nLockTime + writers.write_uint32(h_preimage, 0) + + # nHashType + writers.write_uint32(h_preimage, 1) + + return writers.get_tx_hash(h_preimage, double=sign_hash_double) diff --git a/core/src/apps/bitcoin/common.py b/core/src/apps/bitcoin/common.py index d343dcd6bd..01d96fb48c 100644 --- a/core/src/apps/bitcoin/common.py +++ b/core/src/apps/bitcoin/common.py @@ -187,6 +187,10 @@ def tagged_hashwriter(tag: bytes) -> HashWriter: return HashWriter(ctx) +def create_hashwriter() -> HashWriter: + return HashWriter(sha256()) + + def format_fee_rate( fee_rate: float, coin: CoinInfo, include_shortcut: bool = False ) -> str: diff --git a/core/src/apps/bitcoin/psbt/__init__.py b/core/src/apps/bitcoin/psbt/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/src/apps/bitcoin/psbt/key.py b/core/src/apps/bitcoin/psbt/key.py new file mode 100644 index 0000000000..5510e6148c --- /dev/null +++ b/core/src/apps/bitcoin/psbt/key.py @@ -0,0 +1,186 @@ +import binascii +import ustruct as struct +from micropython import const + +from trezor.crypto import base58 + +HARDENED_FLAG = const(0x8000_0000) + + +def H_(x: int) -> int: + return x | HARDENED_FLAG + + +def is_hardened(i: int) -> bool: + return i & HARDENED_FLAG != 0 + + +class ExtendedPubKey: + """ + A BIP 32 extended public key. + """ + + MAINNET_PUBLIC = b"\x04\x88\xB2\x1E" + TESTNET_PUBLIC = b"\x04\x35\x87\xCF" + + def __init__( + self, + version: bytes, + depth: int, + parent_fingerprint: bytes, + child_num: int, + chaincode: bytes, + pubkey: bytes, + ) -> None: + self.version: bytes = version + self.is_testnet: bool = version == ExtendedPubKey.TESTNET_PUBLIC + self.depth: int = depth + self.parent_fingerprint: bytes = parent_fingerprint + self.child_num: int = child_num + self.chaincode: bytes = chaincode + self.pubkey: bytes = pubkey + + @classmethod + def deserialize(cls, xpub: str) -> "ExtendedPubKey": + data = base58.decode_check(xpub) + return cls.from_bytes(data) + + @classmethod + def from_bytes(cls, data: bytes) -> "ExtendedPubKey": + version = data[0:4] + if version not in [ + ExtendedPubKey.MAINNET_PUBLIC, + ExtendedPubKey.TESTNET_PUBLIC, + ]: + raise Exception(f"Extended key magic of {version.hex()} is invalid") + depth = data[4] + parent_fingerprint = data[5:9] + child_num = struct.unpack(">I", data[9:13])[0] + chaincode = data[13:45] + pubkey = data[45:78] + return cls(version, depth, parent_fingerprint, child_num, chaincode, pubkey) + + def serialize(self) -> bytes: + return ( + self.version + + struct.pack("B", self.depth) + + self.parent_fingerprint + + struct.pack(">I", self.child_num) + + self.chaincode + + self.pubkey + ) + + +class KeyOriginInfo: + """ + Object representing the origin of a key. + """ + + def __init__(self, fingerprint: bytes, path: list[int]) -> None: + """ + :param fingerprint: The 4 byte BIP 32 fingerprint of a parent key from which this key is derived from + :param path: The derivation path to reach this key from the key at ``fingerprint`` + """ + self.fingerprint: bytes = fingerprint + self.path: list[int] = path + + @classmethod + def deserialize(cls, s: bytes) -> "KeyOriginInfo": + """ + Deserialize a serialized KeyOriginInfo. + They will be serialized in the same way that PSBTs serialize derivation paths + """ + fingerprint = s[0:4] + s = s[4:] + path = list(struct.unpack("<" + "I" * (len(s) // 4), s)) + return cls(fingerprint, path) + + def serialize(self) -> bytes: + """ + Serializes the KeyOriginInfo in the same way that derivation paths are stored in PSBTs + """ + r = self.fingerprint + r += struct.pack("<" + "I" * len(self.path), *self.path) + return r + + def _path_string(self, hardened_char: str = "h") -> str: + s = "" + for i in self.path: + hardened = is_hardened(i) + i &= ~HARDENED_FLAG + s += "/" + str(i) + if hardened: + s += hardened_char + return s + + def to_string(self, hardened_char: str = "h") -> str: + """ + Return the KeyOriginInfo as a string in the form ///... + This is the same way that KeyOriginInfo is shown in descriptors + """ + s = binascii.hexlify(self.fingerprint).decode() + s += self._path_string(hardened_char) + return s + + @classmethod + def from_string(cls, s: str) -> "KeyOriginInfo": + """ + Create a KeyOriginInfo from the string + + :param s: The string to parse + """ + s = s.lower() + entries = s.split("/") + fingerprint = binascii.unhexlify(s[0:8]) + path: list[int] = [] + if len(entries) > 1: + path = parse_path(s[9:]) + return cls(fingerprint, path) + + def get_derivation_path(self) -> str: + """ + Return the string for just the path + """ + return "m" + self._path_string() + + def get_full_int_list(self) -> list[int]: + """ + Return a list of ints representing this KeyOriginInfo. + The first int is the fingerprint, followed by the path + """ + xfp = [struct.unpack(" list[int]: + """ + Convert BIP32 path string to list of uint32 integers with hardened flags. + Several conventions are supported to set the hardened flag: -1, 1', 1h + + e.g.: "0/1h/1" -> [0, 0x80000001, 1] + + :param nstr: path string + :return: list of integers + """ + if not nstr: + return [] + + n = nstr.split("/") + + # m/a/b/c => a/b/c + if n[0] == "m": + n = n[1:] + + def str_to_harden(x: str) -> int: + if x.startswith("-"): + return H_(abs(int(x))) + elif x.endswith(("h", "'")): + return H_(int(x[:-1])) + else: + return int(x) + + try: + return [str_to_harden(x) for x in n] + except Exception: + raise ValueError("Invalid BIP32 path", nstr) diff --git a/core/src/apps/bitcoin/psbt/psbt.py b/core/src/apps/bitcoin/psbt/psbt.py new file mode 100644 index 0000000000..2404d68d5b --- /dev/null +++ b/core/src/apps/bitcoin/psbt/psbt.py @@ -0,0 +1,1241 @@ +import ustruct as struct +from typing import TYPE_CHECKING + +from trezor.utils import BufferReader + +from .key import KeyOriginInfo +from .serialize import ( + deser_compact_size, + deser_string, + ser_compact_size, + ser_string, + ser_uint256, + uint256_from_str, +) +from .tx import COutPoint, CTransaction, CTxIn, CTxInWitness, CTxOut + +if TYPE_CHECKING: + from .serialize import Readable + from typing import Dict, Tuple, List, Set + + +def deserialize_HDKeypath( + f: Readable, + key: bytes, + hd_keypaths: Dict[bytes, KeyOriginInfo], + expected_sizes: List[int], +) -> None: + """ + :meta private: + + Deserialize a serialized PSBT public key and keypath key-value pair. + + :param f: The byte stream to read the value from. + :param key: The bytes of the key of the key-value pair. + :param hd_keypaths: Dictionary of public key bytes to their. + :param expected_sizes: List of key lengths expected for the keypair being deserialized. + """ + if len(key) not in expected_sizes: + raise Exception( + f"Size of key was not the expected size for the type partial signature pubkey. Length: {len(key)}" + ) + pubkey = key[1:] + if pubkey in hd_keypaths: + raise Exception( + "Duplicate key, input partial signature for pubkey already provided" + ) + + hd_keypaths[pubkey] = KeyOriginInfo.deserialize(deser_string(f)) + + +def serialize_HDKeypath(hd_keypaths: Dict[bytes, KeyOriginInfo], type: bytes) -> bytes: + """ + :meta private: + + Serialize a public key to :class:`~hwilib.key.KeyOriginInfo` mapping as a PSBT key-value pair. + + :param hd_keypaths: The mapping of public key to keypath + :param type: The PSBT type bytes to use + :returns: The serialized keypaths + """ + r = b"" + for pubkey, path in sorted(hd_keypaths.items()): + r += ser_string(type + pubkey) + packed = path.serialize() + r += ser_string(packed) + return r + + +class PartiallySignedInput: + """ + An object for a PSBT input map. + """ + + PSBT_IN_NON_WITNESS_UTXO = 0x00 + PSBT_IN_WITNESS_UTXO = 0x01 + PSBT_IN_PARTIAL_SIG = 0x02 + PSBT_IN_SIGHASH_TYPE = 0x03 + PSBT_IN_REDEEM_SCRIPT = 0x04 + PSBT_IN_WITNESS_SCRIPT = 0x05 + PSBT_IN_BIP32_DERIVATION = 0x06 + PSBT_IN_FINAL_SCRIPTSIG = 0x07 + PSBT_IN_FINAL_SCRIPTWITNESS = 0x08 + PSBT_IN_PREVIOUS_TXID = 0x0E + PSBT_IN_OUTPUT_INDEX = 0x0F + PSBT_IN_SEQUENCE = 0x10 + PSBT_IN_REQUIRED_TIME_LOCKTIME = 0x11 + PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = 0x12 + PSBT_IN_TAP_KEY_SIG = 0x13 + PSBT_IN_TAP_SCRIPT_SIG = 0x14 + PSBT_IN_TAP_LEAF_SCRIPT = 0x15 + PSBT_IN_TAP_BIP32_DERIVATION = 0x16 + PSBT_IN_TAP_INTERNAL_KEY = 0x17 + PSBT_IN_TAP_MERKLE_ROOT = 0x18 + + def __init__(self, version: int) -> None: + self.non_witness_utxo: CTransaction | None = None + self.witness_utxo: CTxOut | None = None + self.partial_sigs: Dict[bytes, bytes] = {} + self.sighash: int | None = None + self.redeem_script = b"" + self.witness_script = b"" + self.hd_keypaths: Dict[bytes, KeyOriginInfo] = {} + self.final_script_sig = b"" + self.final_script_witness = CTxInWitness() + self.prev_txid = b"" + self.prev_out: int | None = None + self.sequence: int | None = None + self.time_locktime: int | None = None + self.height_locktime: int | None = None + self.tap_key_sig = b"" + self.tap_script_sigs: Dict[Tuple[bytes, bytes], bytes] = {} + self.tap_scripts: Dict[Tuple[bytes, int], Set[bytes]] = {} + self.tap_bip32_paths: Dict[bytes, Tuple[Set[bytes], KeyOriginInfo]] = {} + self.tap_internal_key = b"" + self.tap_merkle_root = b"" + self.unknown: Dict[bytes, bytes] = {} + + self.version: int = version + + def set_null(self) -> None: + """ + Clear all values in this PSBT input map. + """ + self.non_witness_utxo = None + self.witness_utxo = None + self.partial_sigs.clear() + self.sighash = None + self.redeem_script = b"" + self.witness_script = b"" + self.hd_keypaths.clear() + self.final_script_sig = b"" + self.final_script_witness = CTxInWitness() + self.tap_key_sig = b"" + self.tap_script_sigs.clear() + self.tap_scripts.clear() + self.tap_bip32_paths.clear() + self.tap_internal_key = b"" + self.tap_merkle_root = b"" + self.prev_txid = b"" + self.prev_out = None + self.sequence = None + self.time_locktime = None + self.height_locktime = None + self.unknown.clear() + + def deserialize(self, f: Readable) -> None: + """ + Deserialize a serialized PSBT input. + + :param f: A byte stream containing the serialized PSBT input + """ + key_lookup: Set[bytes] = set() + + while True: + # read the key + try: + key = deser_string(f) + except Exception: + break + + # Check for separator + if len(key) == 0: + break + + # First byte of key is the type + key_type = deser_compact_size(BufferReader(key)) + + if key_type == PartiallySignedInput.PSBT_IN_NON_WITNESS_UTXO: + if key in key_lookup: + raise Exception( + "Duplicate Key, input non witness utxo already provided" + ) + elif len(key) != 1: + raise Exception("non witness utxo key is more than one byte type") + self.non_witness_utxo = CTransaction() + utxo_bytes = BufferReader(deser_string(f)) + self.non_witness_utxo.deserialize(utxo_bytes) + self.non_witness_utxo.rehash() + elif key_type == PartiallySignedInput.PSBT_IN_WITNESS_UTXO: + if key in key_lookup: + raise Exception( + "Duplicate Key, input witness utxo already provided" + ) + elif len(key) != 1: + raise Exception("witness utxo key is more than one byte type") + self.witness_utxo = CTxOut() + tx_out_bytes = BufferReader(deser_string(f)) + self.witness_utxo.deserialize(tx_out_bytes) + elif key_type == PartiallySignedInput.PSBT_IN_PARTIAL_SIG: + if len(key) != 34 and len(key) != 66: + raise Exception( + "Size of key was not the expected size for the type partial signature pubkey" + ) + pubkey = key[1:] + if pubkey in self.partial_sigs: + raise Exception( + "Duplicate key, input partial signature for pubkey already provided" + ) + + sig = deser_string(f) + self.partial_sigs[pubkey] = sig + elif key_type == PartiallySignedInput.PSBT_IN_SIGHASH_TYPE: + if key in key_lookup: + raise Exception( + "Duplicate key, input sighash type already provided" + ) + elif len(key) != 1: + raise Exception("sighash key is more than one byte type") + sighash_bytes = deser_string(f) + self.sighash = struct.unpack(" 65: + raise Exception( + "Input Taproot key path signature is longer than 65 bytes" + ) + elif key_type == PartiallySignedInput.PSBT_IN_TAP_SCRIPT_SIG: + if key in key_lookup: + raise Exception( + "Duplicate key, input Taproot script signature already provided" + ) + elif len(key) != 65: + raise Exception( + "Input Taproot script signature key is not 65 bytes" + ) + xonly = key[1:33] + script_hash = key[33:65] + sig = deser_string(f) + if len(sig) < 64: + raise Exception( + "Input Taproot script path signature is shorter than 64 bytes" + ) + elif len(sig) > 65: + raise Exception( + "Input Taproot script path signature is longer than 65 bytes" + ) + self.tap_script_sigs[(xonly, script_hash)] = sig + elif key_type == PartiallySignedInput.PSBT_IN_TAP_LEAF_SCRIPT: + if key in key_lookup: + raise Exception( + "Duplicate key, input Taproot leaf script already provided" + ) + elif len(key) < 34: + raise Exception( + "Input Taproot leaf script key is not at least 34 bytes" + ) + elif (len(key) - 2) % 32 != 0: + raise Exception( + "Input Taproot leaf script key's control block is not valid" + ) + script = deser_string(f) + if len(script) == 0: + raise Exception("Input Taproot leaf script cannot be empty") + leaf_script = (script[:-1], int(script[-1])) + if leaf_script not in self.tap_scripts: + self.tap_scripts[leaf_script] = set() + self.tap_scripts[(script[:-1], int(script[-1]))].add(key[1:]) + elif key_type == PartiallySignedInput.PSBT_IN_TAP_BIP32_DERIVATION: + if key in key_lookup: + raise Exception( + "Duplicate key, input Taproot BIP 32 keypath already provided" + ) + elif len(key) != 33: + raise Exception("Input Taproot BIP 32 keypath key is not 33 bytes") + xonly = key[1:33] + value = deser_string(f) + vs = BufferReader(value) + num_hashes = deser_compact_size(vs) + leaf_hashes = set() + for _ in range(0, num_hashes): + leaf_hashes.add(vs.read(32)) + self.tap_bip32_paths[xonly] = ( + leaf_hashes, + KeyOriginInfo.deserialize(vs.read()), + ) + elif key_type == PartiallySignedInput.PSBT_IN_TAP_INTERNAL_KEY: + if key in key_lookup: + raise Exception( + "Duplicate key, input Taproot internal key already provided" + ) + elif len(key) != 1: + raise Exception( + "Input Taproot internal key key is more than one byte type" + ) + self.tap_internal_key = deser_string(f) + if len(self.tap_internal_key) != 32: + raise Exception("Input Taproot internal key is not 32 bytes") + elif key_type == PartiallySignedInput.PSBT_IN_TAP_MERKLE_ROOT: + if key in key_lookup: + raise Exception( + "Duplicate key, input Taproot merkle root already provided" + ) + elif len(key) != 1: + raise Exception( + "Input Taproot merkle root key is more than one byte type" + ) + self.tap_merkle_root = deser_string(f) + if len(self.tap_merkle_root) != 32: + raise Exception("Input Taproot merkle root is not 32 bytes") + else: + if key in self.unknown: + raise Exception( + "Duplicate key, key for unknown value already provided" + ) + unknown_bytes = deser_string(f) + self.unknown[key] = unknown_bytes + + key_lookup.add(key) + + # Make sure required PSBTv2 fields are present + if self.version >= 2: + if len(self.prev_txid) == 0: + raise Exception("Previous TXID is required in PSBTv2") + if self.prev_out is None: + raise Exception("Previous output's index is required in PSBTv2") + + def serialize(self) -> bytes: + """ + Serialize this PSBT input + + :returns: The serialized PSBT input + """ + r = b"" + + if self.non_witness_utxo: + r += ser_string( + ser_compact_size(PartiallySignedInput.PSBT_IN_NON_WITNESS_UTXO) + ) + tx = self.non_witness_utxo.serialize_with_witness() + r += ser_string(tx) + + if self.witness_utxo: + r += ser_string(ser_compact_size(PartiallySignedInput.PSBT_IN_WITNESS_UTXO)) + tx = self.witness_utxo.serialize() + r += ser_string(tx) + + if len(self.final_script_sig) == 0 and self.final_script_witness.is_null(): + for pubkey, sig in sorted(self.partial_sigs.items()): + r += ser_string( + ser_compact_size(PartiallySignedInput.PSBT_IN_PARTIAL_SIG) + pubkey + ) + r += ser_string(sig) + + if self.sighash is not None: + r += ser_string( + ser_compact_size(PartiallySignedInput.PSBT_IN_SIGHASH_TYPE) + ) + r += ser_string(struct.pack("= 2: + if len(self.prev_txid) != 0: + r += ser_string( + ser_compact_size(PartiallySignedInput.PSBT_IN_PREVIOUS_TXID) + ) + r += ser_string(self.prev_txid) + + if self.prev_out is not None: + r += ser_string( + ser_compact_size(PartiallySignedInput.PSBT_IN_OUTPUT_INDEX) + ) + r += ser_string(struct.pack(" None: + self.redeem_script = b"" + self.witness_script = b"" + self.hd_keypaths: Dict[bytes, KeyOriginInfo] = {} + self.amount: int | None = None + self.script = b"" + self.tap_internal_key = b"" + self.tap_tree = b"" + self.tap_bip32_paths: Dict[bytes, Tuple[Set[bytes], KeyOriginInfo]] = {} + self.unknown: Dict[bytes, bytes] = {} + + self.version: int = version + + def set_null(self) -> None: + """ + Clear this PSBT output map + """ + self.redeem_script = b"" + self.witness_script = b"" + self.hd_keypaths.clear() + self.tap_internal_key = b"" + self.tap_tree = b"" + self.tap_bip32_paths.clear() + self.amount = None + self.script = b"" + self.unknown.clear() + + def deserialize(self, f: Readable) -> None: + """ + Deserialize a serialized PSBT output map + + :param f: A byte stream containing the serialized PSBT output + """ + key_lookup: Set[bytes] = set() + + while True: + # read the key + try: + key = deser_string(f) + except Exception: + break + + # Check for separator + if len(key) == 0: + break + + # First byte of key is the type + key_type = deser_compact_size(BufferReader(key)) + + if key_type == PartiallySignedOutput.PSBT_OUT_REDEEM_SCRIPT: + if key in key_lookup: + raise Exception( + "Duplicate key, output redeemScript already provided" + ) + elif len(key) != 1: + raise Exception( + "Output redeemScript key is more than one byte type" + ) + self.redeem_script = deser_string(f) + elif key_type == PartiallySignedOutput.PSBT_OUT_WITNESS_SCRIPT: + if key in key_lookup: + raise Exception( + "Duplicate key, output witnessScript already provided" + ) + elif len(key) != 1: + raise Exception( + "Output witnessScript key is more than one byte type" + ) + self.witness_script = deser_string(f) + elif key_type == PartiallySignedOutput.PSBT_OUT_BIP32_DERIVATION: + deserialize_HDKeypath(f, key, self.hd_keypaths, [34, 66]) + elif key_type == PartiallySignedOutput.PSBT_OUT_AMOUNT: + if key in key_lookup: + raise Exception("Duplicate key, output amount already provided") + elif len(key) != 1: + raise Exception("Output amount key is more than one byte type") + v = deser_string(f) + if len(v) != 8: + raise Exception("Output amount is not 8 bytes") + self.amount = struct.unpack("= 2: + if self.amount is None: + raise Exception("PSBT_OUTPUT_AMOUNT is required in PSBTv2") + if len(self.script) == 0: + raise Exception("PSBT_OUTPUT_SCRIPT is required in PSBTv2") + + def serialize(self) -> bytes: + """ + Serialize this PSBT output + + :returns: The serialized PSBT output + """ + r = b"" + if len(self.redeem_script) != 0: + r += ser_string( + ser_compact_size(PartiallySignedOutput.PSBT_OUT_REDEEM_SCRIPT) + ) + r += ser_string(self.redeem_script) + + if len(self.witness_script) != 0: + r += ser_string( + ser_compact_size(PartiallySignedOutput.PSBT_OUT_WITNESS_SCRIPT) + ) + r += ser_string(self.witness_script) + + r += serialize_HDKeypath( + self.hd_keypaths, + ser_compact_size(PartiallySignedOutput.PSBT_OUT_BIP32_DERIVATION), + ) + + if self.version >= 2: + if self.amount is not None: + r += ser_string(ser_compact_size(PartiallySignedOutput.PSBT_OUT_AMOUNT)) + r += ser_string(struct.pack(" CTxOut: + """ + Creates a CTxOut for this output + + :returns: The CTxOut + """ + assert self.amount is not None + assert len(self.script) != 0 + return CTxOut(self.amount, self.script) + + +class PSBT: + """ + A class representing a PSBT + """ + + PSBT_GLOBAL_UNSIGNED_TX = 0x00 + PSBT_GLOBAL_XPUB = 0x01 + PSBT_GLOBAL_TX_VERSION = 0x02 + PSBT_GLOBAL_FALLBACK_LOCKTIME = 0x03 + PSBT_GLOBAL_INPUT_COUNT = 0x04 + PSBT_GLOBAL_OUTPUT_COUNT = 0x05 + PSBT_GLOBAL_TX_MODIFIABLE = 0x06 + PSBT_GLOBAL_VERSION = 0xFB + + def __init__(self, tx: CTransaction | None = None) -> None: + """ + :param tx: A Bitcoin transaction that specifies the inputs and outputs to use + """ + if tx: + self.tx = tx + else: + self.tx = CTransaction() + self.inputs: List[PartiallySignedInput] = [] + self.outputs: List[PartiallySignedOutput] = [] + self.unknown: Dict[bytes, bytes] = {} + self.xpub: Dict[bytes, KeyOriginInfo] = {} + self.tx_version: int | None = None + self.fallback_locktime: int | None = None + self.tx_modifiable: int | None = None + + # Assume version 0 PSBT + self.version = 0 + self.explicit_version = False + + def deserialize(self, psbt: bytes) -> None: + + f = BufferReader(psbt) + end = len(psbt) + + # Read the magic bytes + magic = f.read(5) + if magic != b"psbt\xff": + raise Exception("invalid magic") + + key_lookup: Set[bytes] = set() + + input_count = None + output_count = None + + # Read loop + while True: + # read the key + try: + key = deser_string(f) + except Exception: + break + + # Check for separator + if len(key) == 0: + break + + # First byte of key is the type + key_type = deser_compact_size(BufferReader(key)) + + # Do stuff based on type + if key_type == PSBT.PSBT_GLOBAL_UNSIGNED_TX: + # Checks for correctness + if key in key_lookup: + raise Exception("Duplicate key, unsigned tx already provided") + elif len(key) > 1: + raise Exception("Global unsigned tx key is more than one byte type") + + # read in value + tx_bytes = BufferReader(deser_string(f)) + self.tx.deserialize(tx_bytes) + + # Make sure that all scriptSigs and scriptWitnesses are empty + for txin in self.tx.vin: + if len(txin.scriptSig) != 0 or not self.tx.wit.is_null(): + raise Exception( + "Unsigned tx does not have empty scriptSigs and scriptWitnesses" + ) + elif key_type == PSBT.PSBT_GLOBAL_XPUB: + deserialize_HDKeypath(f, key, self.xpub, [79]) + elif key_type == PSBT.PSBT_GLOBAL_TX_VERSION: + if key in key_lookup: + raise Exception( + "Duplicate key, global transaction version is already provided" + ) + elif len(key) > 1: + raise Exception( + "Global transaction version key is more than one byte type" + ) + v = deser_string(f) + if len(v) != 4: + raise Exception("Global transaction version is not 4 bytes") + self.tx_version = struct.unpack(" 1: + raise Exception( + "Global fallback locktime key is more than one byte type" + ) + v = deser_string(f) + if len(v) != 4: + raise Exception("Global fallback locktime is not 4 bytes") + self.fallback_locktime = struct.unpack(" 1: + raise Exception("Global input count key is more than one byte type") + _ = deser_compact_size(f) # Value length, we can ignore this + input_count = deser_compact_size(f) + elif key_type == PSBT.PSBT_GLOBAL_OUTPUT_COUNT: + if key in key_lookup: + raise Exception( + "Duplicate key, global output count is already provided" + ) + elif len(key) > 1: + raise Exception( + "Global output count key is more than one byte type" + ) + _ = deser_compact_size(f) # Value length, we can ignore this + output_count = deser_compact_size(f) + elif key_type == PSBT.PSBT_GLOBAL_TX_MODIFIABLE: + if key in key_lookup: + raise Exception( + "Duplicate key, global tx modifiable flags is already provided" + ) + elif len(key) > 1: + raise Exception( + "Global tx modifiable flags key is more than one byte type" + ) + v = deser_string(f) + if len(v) != 1: + raise Exception("Global tx modifiable flags is not 1 bytes") + self.tx_modifiable = struct.unpack(" 1: + raise Exception( + "Global PSBT version key is more than one byte type" + ) + v = deser_string(f) + if len(v) != 4: + raise Exception("Global PSBT version is not 4 bytes") + self.version = struct.unpack("= 2: + # Tx version, input, and output counts are required + if self.tx_version is None: + raise Exception("PSBT_GLOBAL_TX_VERSION is required in PSBTv2") + if input_count is None: + raise Exception("PSBT_GLOBAL_INPUT_COUNT is required in PSBTv2") + if output_count is None: + raise Exception("PSBT_GLOBAL_OUTPUT_COUNT is required in PSBTv2") + # Unsigned tx is disallowed + if not self.tx.is_null(): + raise Exception("PSBT_GLOBAL_UNSIGNED_TX is not allowed in PSBTv2") + + # Read input data + if input_count is None: + input_count = len(self.tx.vin) + for i in range(input_count): + if f.tell() == end: + break + psbt_in = PartiallySignedInput(self.version) + psbt_in.deserialize(f) + self.inputs.append(psbt_in) + + if self.version >= 2: + prev_txid = psbt_in.prev_txid + else: + prev_txid = ser_uint256(self.tx.vin[i].prevout.hash) + + if psbt_in.non_witness_utxo: + psbt_in.non_witness_utxo.rehash() + if psbt_in.non_witness_utxo.hash != prev_txid: + raise Exception("Non-witness UTXO does not match outpoint hash") + + if len(self.inputs) != input_count: + raise Exception( + "Inputs provided does not match the number of inputs in transaction" + ) + + # Read output data + if output_count is None: + output_count = len(self.tx.vout) + for i in range(output_count): + if f.tell() == end: + break + output = PartiallySignedOutput(self.version) + output.deserialize(f) + self.outputs.append(output) + + if len(self.outputs) != output_count: + raise Exception( + "Outputs provided does not match the number of outputs in transaction" + ) + + self.cache_unsigned_tx_pieces() + + def serialize(self) -> bytes: + r = b"" + + # magic bytes + r += b"psbt\xff" + + if self.version == 0: + # unsigned tx flag + r += ser_string(ser_compact_size(PSBT.PSBT_GLOBAL_UNSIGNED_TX)) + + # write serialized tx + tx = self.tx.serialize_with_witness() + r += ser_compact_size(len(tx)) + r += tx + + # write xpubs + r += serialize_HDKeypath(self.xpub, ser_compact_size(PSBT.PSBT_GLOBAL_XPUB)) + + if self.version >= 2: + assert self.tx_version is not None + r += ser_string(ser_compact_size(PSBT.PSBT_GLOBAL_TX_VERSION)) + r += ser_string(struct.pack(" 0 or self.explicit_version: + r += ser_string(ser_compact_size(PSBT.PSBT_GLOBAL_VERSION)) + r += ser_string(struct.pack(" None: + """ + If this PSBT is v0, then the global unsigned transaction will be used to fill in the PSBTv2 + fields so that all users of the PSBT classes can use the same PSBTv2 interface regardless + of PSBT version. + + Does nothing if the PSBT is already v2. + """ + # To make things easier, we split up the global transaction + # and use the PSBTv2 fields for PSBTv0 + if self.tx is not None: + self.setup_from_tx(self.tx) + + def setup_from_tx(self, tx: CTransaction): + """ + Fills in the PSBTv2 fields for this PSBT given a transaction + + :param tx: The CTransaction to fill from + """ + self.tx_version = tx.nVersion + self.fallback_locktime = tx.nLockTime + + for i, txin in enumerate(tx.vin): + psbt_in = self.inputs[i] + + psbt_in.prev_txid = ser_uint256(txin.prevout.hash) + psbt_in.prev_out = txin.prevout.n + psbt_in.sequence = txin.nSequence + + for i, txout in enumerate(tx.vout): + psbt_out = self.outputs[i] + + psbt_out.amount = txout.nValue + psbt_out.script = txout.scriptPubKey + + def compute_lock_time(self) -> int: + """ + Computes the lock time for this transaction + + :returns: The lock time + """ + time_lock: int | None = 0 + height_lock: int | None = 0 + + for psbt_in in self.inputs: + if psbt_in.time_locktime is not None and psbt_in.height_locktime is None: + height_lock = None + if time_lock is None: + raise Exception("Cannot require both time and height locktimes") + elif psbt_in.time_locktime is None and psbt_in.height_locktime is not None: + time_lock = None + if height_lock is None: + raise Exception("Cannot require both time and height locktimes") + + if psbt_in.time_locktime is not None and time_lock is not None: + time_lock = max(time_lock, psbt_in.time_locktime) + if psbt_in.height_locktime is not None and height_lock is not None: + height_lock = max(height_lock, psbt_in.height_locktime) + + if height_lock is not None and height_lock > 0: + return height_lock + if time_lock is not None and time_lock > 0: + return time_lock + if self.fallback_locktime is not None: + return self.fallback_locktime + return 0 + + def lock_time_disabled(self) -> bool: + """ + Checks if the lock time is disabled + """ + return all( + sequence == 0xFFFFFFFF + for sequence in [psbt_in.sequence for psbt_in in self.inputs] + ) + + def get_unsigned_tx(self) -> CTransaction: + """ + Get the unsigned transaction represented by this PSBT + + :return: A CTransaction + """ + if not self.tx.is_null(): + return self.tx + + assert self.tx_version is not None + + tx = CTransaction() + tx.nVersion = self.tx_version + self.nLockTime = self.compute_lock_time() + + for psbt_in in self.inputs: + assert psbt_in.prev_txid is not None + assert psbt_in.prev_out is not None + assert psbt_in.sequence is not None + + txin = CTxIn( + COutPoint(uint256_from_str(psbt_in.prev_txid), psbt_in.prev_out), + b"", + psbt_in.sequence, + ) + tx.vin.append(txin) + + for psbt_out in self.outputs: + assert psbt_out.amount is not None + + txout = CTxOut(psbt_out.amount, psbt_out.script) + tx.vout.append(txout) + + tx.rehash() + return tx + + def _convert_version(self, version) -> None: + self.version = version + for psbt_in in self.inputs: + psbt_in.version = version + for psbt_out in self.outputs: + psbt_out.version = version + + def convert_to_v2(self) -> None: + """ + Sets this PSBT to version 2 + """ + self._convert_version(2) + + def convert_to_v0(self) -> None: + """ + Sets this PSBT to version 0 + """ + self._convert_version(0) + self.tx = self.get_unsigned_tx() + self.explicit_version = False diff --git a/core/src/apps/bitcoin/psbt/script.py b/core/src/apps/bitcoin/psbt/script.py new file mode 100644 index 0000000000..cfbd0bc616 --- /dev/null +++ b/core/src/apps/bitcoin/psbt/script.py @@ -0,0 +1,168 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Tuple + + pass + + +def is_opreturn(script: bytes) -> bool: + """ + Determine whether a script is an OP_RETURN output script. + + :param script: The script + :returns: Whether the script is an OP_RETURN output script + """ + return script[0] == 0x6A + + +def is_p2sh(script: bytes) -> bool: + """ + Determine whether a script is a P2SH output script. + + :param script: The script + :returns: Whether the script is a P2SH output script + """ + return ( + len(script) == 23 + and script[0] == 0xA9 + and script[1] == 0x14 + and script[22] == 0x87 + ) + + +def is_p2pkh(script: bytes) -> bool: + """ + Determine whether a script is a P2PKH output script. + + :param script: The script + :returns: Whether the script is a P2PKH output script + """ + return ( + len(script) == 25 + and script[0] == 0x76 + and script[1] == 0xA9 + and script[2] == 0x14 + and script[23] == 0x88 + and script[24] == 0xAC + ) + + +def is_p2pk(script: bytes) -> bool: + """ + Determine whether a script is a P2PK output script. + + :param script: The script + :returns: Whether the script is a P2PK output script + """ + return ( + (len(script) == 35 or len(script) == 67) + and (script[0] == 0x21 or script[0] == 0x41) + and script[-1] == 0xAC + ) + + +def is_witness(script: bytes) -> Tuple[bool, int, bytes]: + """ + Determine whether a script is a segwit output script. + If so, also returns the witness version and witness program. + + :param script: The script + :returns: A tuple of a bool indicating whether the script is a segwit output script, + an int representing the witness version, + and the bytes of the witness program. + """ + if len(script) < 4 or len(script) > 42: + return (False, 0, b"") + + if script[0] != 0 and (script[0] < 81 or script[0] > 96): + return (False, 0, b"") + + if script[1] + 2 == len(script): + return (True, script[0] - 0x50 if script[0] else 0, script[2:]) + + return (False, 0, b"") + + +def is_p2wpkh(script: bytes) -> bool: + """ + Determine whether a script is a P2WPKH output script. + + :param script: The script + :returns: Whether the script is a P2WPKH output script + """ + is_wit, wit_ver, wit_prog = is_witness(script) + if not is_wit: + return False + elif wit_ver != 0: + return False + return len(wit_prog) == 20 + + +def is_p2wsh(script: bytes) -> bool: + """ + Determine whether a script is a P2WSH output script. + + :param script: The script + :returns: Whether the script is a P2WSH output script + """ + is_wit, wit_ver, wit_prog = is_witness(script) + if not is_wit: + return False + elif wit_ver != 0: + return False + return len(wit_prog) == 32 + + +def is_p2tr(script: bytes) -> bool: + """ + Determine whether a script is a P2TR output script. + + :param script: The script + :returns: Whether the script is a P2TR output script + """ + is_wit, wit_ver, wit_prog = is_witness(script) + if not is_wit: + return False + elif wit_ver != 1: + return False + return len(wit_prog) == 32 + + +# Only handles up to 15 of 15. Returns None if this script is not a +# multisig script. Returns (m, pubkeys) otherwise. +def parse_multisig(script: bytes) -> Tuple[int, list[bytes]] | None: + """ + Determine whether a script is a multisig script. If so, determine the parameters of that multisig. + + :param script: The script + :returns: ``None`` if the script is not multisig. + If multisig, returns a tuple of the number of signers required, + and a sequence of public key bytes. + """ + # Get m + m = script[0] - 80 + if m < 1 or m > 15: + return None + + # Get pubkeys + pubkeys = [] + offset = 1 + while True: + pubkey_len = script[offset] + if pubkey_len != 33: + break + offset += 1 + pubkeys.append(script[offset : offset + 33]) + offset += 33 + + # Check things at the end + n = script[offset] - 80 + if n != len(pubkeys): + return None + offset += 1 + op_cms = script[offset] + if op_cms != 174: + return None + + return (m, pubkeys) diff --git a/core/src/apps/bitcoin/psbt/serialize.py b/core/src/apps/bitcoin/psbt/serialize.py new file mode 100644 index 0000000000..55299315a0 --- /dev/null +++ b/core/src/apps/bitcoin/psbt/serialize.py @@ -0,0 +1,247 @@ +import ustruct as struct +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import List, Protocol, TypeVar, Callable, Sequence + + class Readable(Protocol): + def read(self, length: int | None = -1) -> bytes: + ... + + class Deserializable(Protocol): + def deserialize(self, f: Readable) -> None: + ... + + class Serializable(Protocol): + def serialize(self) -> bytes: + ... + + D = TypeVar("D", bound=Deserializable) + + +# Serialization/deserialization tools +def ser_compact_size(size: int) -> bytes: + """ + Serialize an integer using Bitcoin's compact size unsigned integer serialization. + + :param size: The int to serialize + :returns: The int serialized as a compact size unsigned integer + """ + r = b"" + if size < 253: + r = struct.pack("B", size) + elif size < 0x10000: + r = struct.pack(" int: + """ + Deserialize a compact size unsigned integer from the beginning of the byte stream. + + :param s: The byte stream + :returns: The integer that was serialized + """ + nit: int = struct.unpack(" bytes: + """ + Deserialize a variable length byte string serialized with Bitcoin's variable length string serialization from a byte stream. + + :param s: The byte stream + :returns: The byte string that was serialized + """ + nit = deser_compact_size(s) + return s.read(nit) + + +def ser_string(s: bytes) -> bytes: + """ + Serialize a byte string with Bitcoin's variable length string serialization. + + :param s: The byte string to be serialized + :returns: The serialized byte string + """ + return ser_compact_size(len(s)) + s + + +def deser_uint256(s: Readable) -> int: + """ + Deserialize a 256 bit integer serialized with Bitcoin's 256 bit integer serialization from a byte stream. + + :param s: The byte stream. + :returns: The integer that was serialized + """ + r = 0 + for i in range(8): + t = struct.unpack(" bytes: + """ + Serialize a 256 bit integer with Bitcoin's 256 bit integer serialization. + + :param u: The integer to serialize + :returns: The serialized 256 bit integer + """ + rs = b"" + for _ in range(8): + rs += struct.pack(">= 32 + return rs + + +def uint256_from_str(s: bytes) -> int: + """ + Deserialize a 256 bit integer serialized with Bitcoin's 256 bit integer serialization from a byte string. + + :param s: The byte string + :returns: The integer that was serialized + """ + r = 0 + t = struct.unpack(" List[D]: + """ + Deserialize a vector of objects with Bitcoin's object vector serialization from a byte stream. + + :param s: The byte stream + :param c: The class of object to deserialize for each object in the vector + :returns: A list of objects that were serialized + """ + nit = deser_compact_size(s) + r = [] + for _ in range(nit): + t = c() + t.deserialize(s) + r.append(t) + return r + + +def ser_vector(v: Sequence[Serializable]) -> bytes: + """ + Serialize a vector of objects with Bitcoin's object vector serialzation. + + :param v: The list of objects to serialize + :returns: The serialized objects + """ + r = ser_compact_size(len(v)) + for i in v: + r += i.serialize() + return r + + +def deser_string_vector(s: Readable) -> List[bytes]: + """ + Deserialize a vector of byte strings from a byte stream. + + :param f: The byte stream + :returns: The list of byte strings that were serialized + """ + nit = deser_compact_size(s) + r = [] + for _ in range(nit): + t = deser_string(s) + r.append(t) + return r + + +def ser_string_vector(v: List[bytes]) -> bytes: + """ + Serialize a list of byte strings as a vector of byte strings. + + :param v: The list of byte strings to serialize + :returns: The serialized list of byte strings + """ + r = ser_compact_size(len(v)) + for sv in v: + r += ser_string(sv) + return r + + +def ser_sig_der(r: bytes, s: bytes) -> bytes: + """ + Serialize the ``r`` and ``s`` values of an ECDSA signature using DER. + + :param r: The ``r`` value bytes + :param s: The ``s`` value bytes + :returns: The DER encoded signature + """ + sig = b"\x30" + + # Make r and s as short as possible + ri = 0 + for b in r: + if b == 0: + ri += 1 + else: + break + r = r[ri:] + si = 0 + for b in s: + if b == 0: + si += 1 + else: + break + s = s[si:] + + # Make positive of neg + first = r[0] + if first & (1 << 7) != 0: + r = b"\x00" + r + first = s[0] + if first & (1 << 7) != 0: + s = b"\x00" + s + + # Write total length + total_len = len(r) + len(s) + 4 + sig += struct.pack("B", total_len) + + # write r + sig += b"\x02" + sig += struct.pack("B", len(r)) + sig += r + + # write s + sig += b"\x02" + sig += struct.pack("B", len(s)) + sig += s + + sig += b"\x01" + return sig + + +def ser_sig_compact(r: bytes, s: bytes, recid: bytes) -> bytes: + """ + Serialize the ``r`` and ``s`` values of an ECDSA signature using the compact signature serialization scheme. + + :param r: The ``r`` value bytes + :param s: The ``s`` value bytes + :returns: The compact signature + """ + rec = struct.unpack("B", recid)[0] + prefix = struct.pack("B", 27 + 4 + rec) + + sig = b"" + sig += prefix + sig += r + s + + return sig diff --git a/core/src/apps/bitcoin/psbt/tx.py b/core/src/apps/bitcoin/psbt/tx.py new file mode 100644 index 0000000000..21d75ffdf9 --- /dev/null +++ b/core/src/apps/bitcoin/psbt/tx.py @@ -0,0 +1,295 @@ +import ustruct as struct +from typing import TYPE_CHECKING + +from .script import is_opreturn, is_p2pk, is_p2pkh, is_p2sh, is_p2wsh, is_witness +from .serialize import ( + deser_string, + deser_string_vector, + deser_uint256, + deser_vector, + ser_string, + ser_string_vector, + ser_uint256, + ser_vector, + uint256_from_str, +) + +if TYPE_CHECKING: + from .serialize import Readable + + +# Objects that map to bitcoind objects, which can be serialized/deserialized + +MSG_WITNESS_FLAG = 1 << 30 + + +def sha256d(s: bytes) -> bytes: + """ + Double SHA-256 hash of the input bytes. + """ + from trezor.crypto import hashlib + + return hashlib.sha256(hashlib.sha256(s).digest()).digest() + + +class COutPoint: + def __init__(self, hash: int = 0, n: int = 0xFFFFFFFF): + self.hash = hash + self.n = n + + def deserialize(self, f: Readable) -> None: + self.hash = deser_uint256(f) + self.n = struct.unpack(" bytes: + r = b"" + r += ser_uint256(self.hash) + r += struct.pack(" str: + return f"COutPoint(hash={self.hash:064x}, n={self.n:i})" + + +class CTxIn: + def __init__( + self, + outpoint: COutPoint | None = None, + scriptSig: bytes = b"", + nSequence: int = 0, + ): + if outpoint is None: + self.prevout = COutPoint() + else: + self.prevout = outpoint + self.scriptSig = scriptSig + self.nSequence = nSequence + + def deserialize(self, f: Readable) -> None: + self.prevout = COutPoint() + self.prevout.deserialize(f) + self.scriptSig = deser_string(f) + self.nSequence = struct.unpack(" bytes: + r = b"" + r += self.prevout.serialize() + r += ser_string(self.scriptSig) + r += struct.pack(" str: + import binascii + + return f"CTxIn(prevout={repr(self.prevout)}, scriptSig={binascii.hexlify(self.scriptSig).decode()}, nSequence={self.nSequence:i})" + + +class CTxOut: + def __init__(self, nValue: int = 0, scriptPubKey: bytes = b""): + self.nValue = nValue + self.scriptPubKey = scriptPubKey + + def deserialize(self, f: Readable) -> None: + self.nValue = struct.unpack(" bytes: + r = b"" + r += struct.pack(" bool: + return is_opreturn(self.scriptPubKey) + + def is_p2sh(self) -> bool: + return is_p2sh(self.scriptPubKey) + + def is_p2wsh(self) -> bool: + return is_p2wsh(self.scriptPubKey) + + def is_p2pkh(self) -> bool: + return is_p2pkh(self.scriptPubKey) + + def is_p2pk(self) -> bool: + return is_p2pk(self.scriptPubKey) + + def is_witness(self) -> tuple[bool, int, bytes]: + return is_witness(self.scriptPubKey) + + if __debug__: + + def __repr__(self) -> str: + import binascii + + return f"CTxOut(nValue={self.nValue // 100_000_000:i}.{self.nValue % 100_000_000:08i} scriptPubKey={binascii.hexlify(self.scriptPubKey).decode()})" + + +class CScriptWitness: + def __init__(self) -> None: + # stack is a vector of strings + self.stack: list[bytes] = [] + + if __debug__: + + def __repr__(self) -> str: + import binascii + + return f"CScriptWitness({(';'.join([binascii.hexlify(x).decode() for x in self.stack]))})" + + def is_null(self) -> bool: + if self.stack: + return False + return True + + +class CTxInWitness: + def __init__(self) -> None: + self.scriptWitness = CScriptWitness() + + def deserialize(self, f: Readable) -> None: + self.scriptWitness.stack = deser_string_vector(f) + + def serialize(self) -> bytes: + return ser_string_vector(self.scriptWitness.stack) + + if __debug__: + + def __repr__(self) -> str: + return repr(self.scriptWitness) + + def is_null(self) -> bool: + return self.scriptWitness.is_null() + + +class CTxWitness: + def __init__(self) -> None: + self.vtxinwit: list[CTxInWitness] = [] + + def deserialize(self, f: Readable) -> None: + for i in range(len(self.vtxinwit)): + self.vtxinwit[i].deserialize(f) + + def serialize(self) -> bytes: + r = b"" + # This is different than the usual vector serialization -- + # we omit the length of the vector, which is required to be + # the same length as the transaction's vin vector. + for x in self.vtxinwit: + r += x.serialize() + return r + + if __debug__: + + def __repr__(self) -> str: + return f"CTxWitness({(';'.join([repr(x) for x in self.vtxinwit]))})" + + def is_null(self) -> bool: + for x in self.vtxinwit: + if not x.is_null(): + return False + return True + + +class CTransaction: + def __init__(self, tx: "CTransaction" | None = None) -> None: + if tx is None: + self.nVersion = 1 + self.vin: list[CTxIn] = [] + self.vout: list[CTxOut] = [] + self.wit = CTxWitness() + self.nLockTime = 0 + self.sha256: int | None = None + self.hash: bytes | None = None + else: + self.nVersion = tx.nVersion + self.vin = tx.vin[:] + self.vout = tx.vout[:] + self.nLockTime = tx.nLockTime + self.sha256 = tx.sha256 + self.hash = tx.hash + self.wit = tx.wit + + def deserialize(self, f: Readable) -> None: + self.nVersion = struct.unpack(" bytes: + r = b"" + r += struct.pack(" bytes: + flags = 0 + if not self.wit.is_null(): + flags |= 1 + r = b"" + r += struct.pack(" bytes: + return self.serialize_without_witness() + + # Recalculate the txid (transaction hash without witness) + def rehash(self) -> None: + self.sha256 = None + self.calc_sha256() + + # We will only cache the serialization without witness in + # self.sha256 and self.hash -- those are expected to be the txid. + def calc_sha256(self, with_witness: bool = False) -> int | None: + if with_witness: + # Don't cache the result, just return it + return uint256_from_str(sha256d(self.serialize_with_witness())) + + if self.sha256 is None: + self.sha256 = uint256_from_str(sha256d(self.serialize_without_witness())) + self.hash = sha256d(self.serialize()) + return None + + def is_null(self) -> bool: + return len(self.vin) == 0 and len(self.vout) == 0 + + if __debug__: + + def __repr__(self) -> str: + return f"CTransaction(nVersion={self.nVersion:i} vin={repr(self.vin)} vout={repr(self.vout)} wit={repr(self.wit)} nLockTime={self.nLockTime:i})" diff --git a/core/src/apps/bitcoin/sign_message.py b/core/src/apps/bitcoin/sign_message.py index 7a521daad5..21663a86fd 100644 --- a/core/src/apps/bitcoin/sign_message.py +++ b/core/src/apps/bitcoin/sign_message.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING -from trezor import wire -from trezor.crypto.curve import secp256k1 +from trezor import utils, wire +from trezor.crypto.curve import bip340, secp256k1 from trezor.enums import InputScriptType from trezor.lvglui.scrs import lv from trezor.messages import MessageSignature @@ -13,6 +13,7 @@ from .addresses import address_short, get_address from .keychain import validate_path_against_script_type, with_keychain +from .scripts import output_script_native_segwit if TYPE_CHECKING: from trezor.messages import SignMessage @@ -28,6 +29,7 @@ async def sign_message( message = msg.message address_n = msg.address_n script_type = msg.script_type or InputScriptType.SPENDADDRESS + no_script_type = msg.no_script_type validate_message(message) await validate_path( ctx, keychain, address_n, validate_path_against_script_type(coin, msg) @@ -39,30 +41,66 @@ async def sign_message( lv.color_hex(coin.primary_color), f"A:/res/{coin.icon}", ) + is_standard = (not no_script_type) or script_type == InputScriptType.SPENDADDRESS await confirm_signverify( ctx, coin.coin_name, decode_message(message), address_short(coin, address), verify=False, + is_standard=is_standard, ) - + if not is_standard: + script_type = InputScriptType.SPENDADDRESS seckey = node.private_key() + if msg.is_bip322_simple: + if script_type == InputScriptType.SPENDWITNESS: + from .bip322_simple import sighash_bip143 + from .common import ecdsa_hash_pubkey, SigHashType, ecdsa_sign + from .scripts import write_witness_p2wpkh - digest = message_digest(coin, message) - signature = secp256k1.sign(seckey, digest) + pubkey_hash = ecdsa_hash_pubkey(node.public_key(), coin) + script_pub = output_script_native_segwit(0, pubkey_hash) + sighash = sighash_bip143( + message, script_pub, pubkey_hash, coin.sign_hash_double + ) + signature = ecdsa_sign(node, sighash) + witness = utils.empty_bytearray( + 1 + 1 + len(signature) + 1 + len(node.public_key()) + 1 + ) + write_witness_p2wpkh( + witness, signature, node.public_key(), SigHashType.SIGHASH_ALL + ) + signature = witness + elif script_type == InputScriptType.SPENDTAPROOT: + from .bip322_simple import sighash_bip341 + from .common import bip340_sign + from .scripts import write_witness_p2tr + from .common import ecdsa_hash_pubkey, SigHashType - if script_type == InputScriptType.SPENDADDRESS: - script_type_info = 0 - elif script_type == InputScriptType.SPENDP2SHWITNESS: - script_type_info = 4 - elif script_type == InputScriptType.SPENDWITNESS: - script_type_info = 8 + output_pubkey = bip340.tweak_public_key(node.public_key()[1:]) + script_pub = output_script_native_segwit(1, output_pubkey) + sighash = sighash_bip341(message, script_pub) + signature = bip340_sign(node, sighash) + witness = utils.empty_bytearray(1 + 1 + len(signature)) + write_witness_p2tr(witness, signature, SigHashType.SIGHASH_ALL_TAPROOT) + signature = witness + else: + raise wire.ProcessError("Unsupported script type") else: - raise wire.ProcessError("Unsupported script type") + digest = message_digest(coin, message) + signature = secp256k1.sign(seckey, digest) + if script_type == InputScriptType.SPENDADDRESS: + script_type_info = 0 + elif script_type == InputScriptType.SPENDP2SHWITNESS: + script_type_info = 4 + elif script_type == InputScriptType.SPENDWITNESS: + script_type_info = 8 + else: + raise wire.ProcessError("Unsupported script type") - # Add script type information to the recovery byte. - if script_type_info != 0 and not msg.no_script_type: - signature = bytes([signature[0] + script_type_info]) + signature[1:] + # Add script type information to the recovery byte. + if script_type_info != 0 and not no_script_type: + signature = bytes([signature[0] + script_type_info]) + signature[1:] return MessageSignature(address=address, signature=signature) diff --git a/core/src/apps/bitcoin/sign_taproot.py b/core/src/apps/bitcoin/sign_taproot.py new file mode 100644 index 0000000000..af66d1f222 --- /dev/null +++ b/core/src/apps/bitcoin/sign_taproot.py @@ -0,0 +1,227 @@ +from typing import TYPE_CHECKING + +from trezor import wire +from trezor.crypto import base58 +from trezor.enums import AmountUnit, OutputScriptType +from trezor.lvglui.scrs import lv +from trezor.messages import SignedPsbt, SignTx, TxInput, TxOutput + +from apps.common import address_type + +from .common import ( + SigHashType, + bip340_sign, + bip340_sign_internal, + encode_bech32_address, + tagged_hashwriter, +) +from .keychain import with_keychain +from .psbt.psbt import PSBT +from .psbt.script import is_witness +from .psbt.serialize import ser_string +from .sign_tx import layout +from .sign_tx.sig_hasher import BitcoinSigHasher + +# from .sign_tx import tx_weight + +if TYPE_CHECKING: + from apps.common.coininfo import CoinInfo + from apps.common.keychain import Keychain + from trezor.messages import SignPsbt + + +@with_keychain +async def sign_taproot( + ctx: wire.Context, msg: SignPsbt, keychain: Keychain, coin: CoinInfo +) -> SignedPsbt: + if not msg.psbt: + raise wire.DataError("Missing psbt") + try: + psbt = PSBT() + psbt.deserialize(msg.psbt) + except Exception as e: + if __debug__: + import sys + + sys.print_exception(e) # type: ignore["print_exception" is not a known member of module] + raise wire.DataError("Invalid psbt") + + sig_hasher = BitcoinSigHasher() + # weight = tx_weight.TxWeightCalculator() + total_in = 0 + total_out = 0 + change_out = 0 + master_fp = keychain.root_fingerprint().to_bytes(4, "big") + found_ours = False + for i, input in enumerate(psbt.inputs): + assert input.prev_txid is not None + assert input.prev_out is not None + assert input.sequence is not None + + if input.non_witness_utxo is not None: + # TODO: check if non-witness UTXO is presigned + raise wire.DataError("Non-witness UTXO is not allowed") + if input.witness_utxo is None: + raise wire.DataError("Missing required witness UTXO") + if list(input.tap_bip32_paths.items()): + found_ours = True + + scriptPub = input.witness_utxo.scriptPubKey + amount = input.witness_utxo.nValue + is_wit, wit_ver, _ = is_witness(scriptPub) + + assert is_wit and wit_ver == 1, "Only taproot input is allowed" + total_in += amount + for key, (_, origin) in input.tap_bip32_paths.items(): + if origin.fingerprint != master_fp: + if __debug__: + print( + f"Key fingerprint {origin.fingerprint} does not match master key {master_fp}" + ) + raise wire.DataError("Wallet mismatch") + node = keychain.derive(origin.path) + intend_key = node.public_key()[1:] + assert intend_key == key, "Invalid key" + if not input.tap_scripts: + assert key == input.tap_internal_key, "Invalid internal key" + else: + script, _ = list(input.tap_scripts.keys())[0] + assert key in script, "Invalid script" + + sig_hasher.add_input( + txi=TxInput( + prev_hash=bytes(reversed(input.prev_txid)), + prev_index=input.prev_out, + sequence=input.sequence, + amount=amount, + ), + script_pubkey=scriptPub, + ) + if not found_ours: + raise wire.DataError("Invalid PSBT, no tap_bip32_paths present") + ctx.primary_color, ctx.icon_path = ( + lv.color_hex(coin.primary_color), + f"A:/res/{coin.icon}", + ) + for i, output in enumerate(psbt.outputs): + is_change_out = False + op_return_data = None + out = output.get_txout() + total_out += out.nValue + wit, ver, prog = out.is_witness() + out_address = None + if wit: + assert coin.bech32_prefix is not None + out_address = encode_bech32_address(coin.bech32_prefix, ver, prog) + elif out.is_p2pkh(): + out_address = base58.encode_check( + address_type.tobytes(coin.address_type) + out.scriptPubKey[3:23], + coin.b58_hash, + ) + elif out.is_p2sh(): + out_address = base58.encode_check( + address_type.tobytes(coin.address_type_p2sh) + out.scriptPubKey[2:22], + coin.b58_hash, + ) + elif out.is_opreturn(): + assert out.nValue == 0, "OpReturn output should have 0 value" + op_return_data = out.scriptPubKey[2:] + else: + raise Exception("Invalid output type") + + if not wit or (wit and ver == 0): + for _, keypath in output.hd_keypaths.items(): + if keypath.fingerprint != master_fp: + if __debug__: + print( + f"Key fingerprint {keypath.fingerprint} does not match master key {master_fp}" + ) + else: + raise wire.DataError( + "Master fingerprint does not match master key" + ) + change_out += out.nValue + is_change_out = True + elif wit and ver == 1: + for key, (_, origin) in output.tap_bip32_paths.items(): + if not ( + key == output.tap_internal_key and origin.fingerprint == master_fp + ): + raise wire.DataError( + "Invalid parameters, only key path change is allowed" + ) + change_out += out.nValue + is_change_out = True + sig_hasher.add_output( + txo=TxOutput( + amount=out.nValue, + ), + script_pubkey=out.scriptPubKey, + ) + + if not is_change_out: + # display the the output + await layout.confirm_output( + ctx, + TxOutput( + amount=out.nValue, + address=out_address, + op_return_data=op_return_data if op_return_data else None, + script_type=OutputScriptType.PAYTOOPRETURN + if op_return_data + else None, + ), + coin, + AmountUnit.BITCOIN, + ) + if total_in <= total_out: + raise wire.DataError("Insufficient funds") + tx_locktime = psbt.compute_lock_time() + + if tx_locktime > 0: + await layout.confirm_nondefault_locktime( + ctx, tx_locktime, lock_time_disabled=psbt.lock_time_disabled() + ) + fee = total_in - total_out + spending = total_in - change_out + await layout.confirm_total(ctx, spending, fee, 0, coin, AmountUnit.BITCOIN) + + from trezor.ui.layouts import confirm_final + + await confirm_final(ctx, coin.coin_name) + + for i, input in enumerate(psbt.inputs): + for key, (_, origin) in input.tap_bip32_paths.items(): + if not input.tap_scripts: + script_path_spending = False + leaf_hash = None + else: + script_path_spending = True + script, leaf_version = list(input.tap_scripts.keys())[0] + leaf_hash_writer = tagged_hashwriter(b"TapLeaf") + leaf_hash_writer.extend(bytes([leaf_version]) + ser_string(script)) + leaf_hash = leaf_hash_writer.get_digest() + + node = keychain.derive(origin.path) + sigmsg_digest = sig_hasher.hash341( + i, + SignTx( + outputs_count=0, + inputs_count=0, + version=psbt.tx_version, + lock_time=tx_locktime, + ), + SigHashType.SIGHASH_ALL_TAPROOT, + leaf_hash=leaf_hash, + ) + if not script_path_spending: + signature = bip340_sign(node, sigmsg_digest) + else: + signature = bip340_sign_internal(node, sigmsg_digest) + if not script_path_spending: + input.tap_key_sig = signature + else: + assert leaf_hash is not None + input.tap_script_sigs[(key, leaf_hash)] = signature + + return SignedPsbt(psbt=psbt.serialize()) diff --git a/core/src/apps/bitcoin/sign_tx/sig_hasher.py b/core/src/apps/bitcoin/sign_tx/sig_hasher.py index bdb8d3daae..a3f7b5b8e9 100644 --- a/core/src/apps/bitcoin/sign_tx/sig_hasher.py +++ b/core/src/apps/bitcoin/sign_tx/sig_hasher.py @@ -126,6 +126,7 @@ def hash341( i: int, tx: SignTx | PrevTx, sighash_type: SigHashType, + leaf_hash: bytes | None = None, # for script path ) -> bytes: h_sigmsg = tagged_hashwriter(b"TapSighash") @@ -167,11 +168,23 @@ def hash341( ) # spend_type 0 (no tapscript message extension, no annex) - writers.write_uint8(h_sigmsg, 0) + spend_type = 0 + if leaf_hash is not None: + # script path spending, no annex + spend_type = 2 + writers.write_uint8(h_sigmsg, spend_type) # input_index writers.write_uint32(h_sigmsg, i) + if leaf_hash is not None: + # leaf hash + writers.write_bytes_fixed(h_sigmsg, leaf_hash, writers.TX_HASH_SIZE) + # key version + writers.write_uint8(h_sigmsg, 0) + # codesep_pos (signed int32, default -1) + writers.write_bytes_unchecked(h_sigmsg, b"\xff\xff\xff\xff") + return h_sigmsg.get_digest() def hash_zip244( diff --git a/core/src/apps/cardano/README.md b/core/src/apps/cardano/README.md index 01b07cb67f..25ac786a91 100644 --- a/core/src/apps/cardano/README.md +++ b/core/src/apps/cardano/README.md @@ -91,7 +91,7 @@ Since Alonzo era, network id may be included as an item in the transaction body. ## Key types -In Shelley two types of keys are used. Payment key and staking key. Payment keys are derived from _m/1852'/1815'/x/[0,1]/y_ paths and are used for holding/transferring funds. Staking keys are derived from _m/1852'/1815'/x/2/0_ paths, thus there is only one staking key per account. They are used for staking operations - certificates, withdrawals. Shelley addresses are built from the combination of hashes of these keys. +In Shelley two types of keys are used. Payment key and staking key. Payment keys are derived from _m/1852'/1815'/x/[0,1]/y_ paths and are used for holding/transferring funds. Staking keys are derived from _m/1852'/1815'/x/2/y_ paths (in the past, the only allowed value of `y` was `0`). They are used for staking operations - certificates, withdrawals. Shelley addresses are built from the combination of hashes of these keys. [Multi-sig paths (1854')](https://cips.cardano.org/cips/cip1854/) are used to generate keys which should be used in native scripts and also to sign multi-sig transactions. [Minting paths (1855')](https://cips.cardano.org/cips/cip1855/) are used for creating minting policies and for witnessing minting transactions. @@ -291,9 +291,9 @@ Each transaction may contain auxiliary data. Auxiliary data format can be found Auxiliary data can be sent to Trezor as a hash or as an object with parameters. The hash will be included in the transaction body as is and will be shown to the user. -The only object currently supported is governance voting key registration (currently, this is used only by Catalyst, but there may be other governance use cases in the future). To be in compliance with the CDDL and other Cardano tools, governance voting key registration object is being wrapped in a tuple and an empty tuple follows it. The empty tuple represents `auxiliary_scripts` which are not yet supported on Trezor and are thus always empty. Byron addresses are not supported as governance reward addresses. The governance registration signature is returned in the form of `CardanoTxAuxiliaryDataSupplement` which also contains the auxiliary data hash calculated by Trezor. +The only object currently supported is CIP-15/CIP-36 vote key registration (currently, this is used only by Catalyst, but there may be other voting use cases in the future). To be in compliance with the CDDL and other Cardano tools, vote key registration object is being wrapped in a tuple and an empty tuple follows it. The empty tuple represents `auxiliary_scripts` which are not yet supported on Trezor and are thus always empty. Byron addresses are not supported as the addresses to receive rewards. The registration signature is returned in the form of `CardanoTxAuxiliaryDataSupplement` which also contains the auxiliary data hash calculated by Trezor. -[Governance Registration Transaction Metadata Format](https://cips.cardano.org/cips/cip36/) +[CIP-36 Vote Registration Transaction Metadata Format](https://cips.cardano.org/cips/cip36/) ### Native scripts diff --git a/core/src/apps/cardano/addresses.py b/core/src/apps/cardano/addresses.py index f5ac5288aa..e237b2ec42 100644 --- a/core/src/apps/cardano/addresses.py +++ b/core/src/apps/cardano/addresses.py @@ -1,13 +1,22 @@ -from typing import Any +from micropython import const +from typing import TYPE_CHECKING -from trezor import messages, wire +from trezor import wire from trezor.crypto import base58 from trezor.enums import CardanoAddressType -from . import byron_addresses, seed -from .helpers import ADDRESS_KEY_HASH_SIZE, SCRIPT_HASH_SIZE, bech32, network_ids +from . import byron_addresses +from .helpers import bech32 from .helpers.paths import SCHEMA_STAKING_ANY_ACCOUNT -from .helpers.utils import get_public_key_hash, variable_length_encode +from .helpers.utils import get_public_key_hash + +if TYPE_CHECKING: + from typing import Any + + from trezor import messages + + from .seed import Keychain + ADDRESS_TYPES_SHELLEY = ( CardanoAddressType.BASE, @@ -37,8 +46,10 @@ CardanoAddressType.ENTERPRISE_SCRIPT, ) -MIN_ADDRESS_BYTES_LENGTH = 29 -MAX_ADDRESS_BYTES_LENGTH = 65 +ADDRESS_TYPES_PAYMENT = ADDRESS_TYPES_PAYMENT_KEY + ADDRESS_TYPES_PAYMENT_SCRIPT + +_MIN_ADDRESS_BYTES_LENGTH = const(29) +_MAX_ADDRESS_BYTES_LENGTH = const(65) def assert_params_cond(condition: bool) -> None: @@ -49,52 +60,58 @@ def assert_params_cond(condition: bool) -> None: def validate_address_parameters( parameters: messages.CardanoAddressParametersType, ) -> None: - _validate_address_parameters_structure(parameters) + from . import seed - if parameters.address_type == CardanoAddressType.BYRON: - assert_params_cond(seed.is_byron_path(parameters.address_n)) - - elif parameters.address_type == CardanoAddressType.BASE: - assert_params_cond(seed.is_shelley_path(parameters.address_n)) + _validate_address_parameters_structure(parameters) + address_type = parameters.address_type # local_cache_attribute + address_n = parameters.address_n # local_cache_attribute + address_n_staking = parameters.address_n_staking # local_cache_attribute + script_payment_hash = parameters.script_payment_hash # local_cache_attribute + is_shelley_path = seed.is_shelley_path # local_cache_attribute + CAT = CardanoAddressType # local_cache_global + + if address_type == CAT.BYRON: + assert_params_cond(seed.is_byron_path(address_n)) + + elif address_type == CAT.BASE: + assert_params_cond(is_shelley_path(address_n)) _validate_base_address_staking_info( - parameters.address_n_staking, parameters.staking_key_hash + address_n_staking, parameters.staking_key_hash ) - elif parameters.address_type == CardanoAddressType.BASE_SCRIPT_KEY: - _validate_script_hash(parameters.script_payment_hash) + elif address_type == CAT.BASE_SCRIPT_KEY: + _validate_script_hash(script_payment_hash) _validate_base_address_staking_info( - parameters.address_n_staking, parameters.staking_key_hash + address_n_staking, parameters.staking_key_hash ) - elif parameters.address_type == CardanoAddressType.BASE_KEY_SCRIPT: - assert_params_cond(seed.is_shelley_path(parameters.address_n)) + elif address_type == CAT.BASE_KEY_SCRIPT: + assert_params_cond(is_shelley_path(address_n)) _validate_script_hash(parameters.script_staking_hash) - elif parameters.address_type == CardanoAddressType.BASE_SCRIPT_SCRIPT: - _validate_script_hash(parameters.script_payment_hash) + elif address_type == CAT.BASE_SCRIPT_SCRIPT: + _validate_script_hash(script_payment_hash) _validate_script_hash(parameters.script_staking_hash) - elif parameters.address_type == CardanoAddressType.POINTER: - assert_params_cond(seed.is_shelley_path(parameters.address_n)) + elif address_type == CAT.POINTER: + assert_params_cond(is_shelley_path(address_n)) assert_params_cond(parameters.certificate_pointer is not None) - elif parameters.address_type == CardanoAddressType.POINTER_SCRIPT: - _validate_script_hash(parameters.script_payment_hash) + elif address_type == CAT.POINTER_SCRIPT: + _validate_script_hash(script_payment_hash) assert_params_cond(parameters.certificate_pointer is not None) - elif parameters.address_type == CardanoAddressType.ENTERPRISE: - assert_params_cond(seed.is_shelley_path(parameters.address_n)) + elif address_type == CAT.ENTERPRISE: + assert_params_cond(is_shelley_path(address_n)) - elif parameters.address_type == CardanoAddressType.ENTERPRISE_SCRIPT: - _validate_script_hash(parameters.script_payment_hash) + elif address_type == CAT.ENTERPRISE_SCRIPT: + _validate_script_hash(script_payment_hash) - elif parameters.address_type == CardanoAddressType.REWARD: - assert_params_cond(seed.is_shelley_path(parameters.address_n_staking)) - assert_params_cond( - SCHEMA_STAKING_ANY_ACCOUNT.match(parameters.address_n_staking) - ) + elif address_type == CAT.REWARD: + assert_params_cond(is_shelley_path(address_n_staking)) + assert_params_cond(SCHEMA_STAKING_ANY_ACCOUNT.match(address_n_staking)) - elif parameters.address_type == CardanoAddressType.REWARD_SCRIPT: + elif address_type == CAT.REWARD_SCRIPT: _validate_script_hash(parameters.script_staking_hash) else: @@ -104,75 +121,76 @@ def validate_address_parameters( def _validate_address_parameters_structure( parameters: messages.CardanoAddressParametersType, ) -> None: - address_n = parameters.address_n - address_n_staking = parameters.address_n_staking - staking_key_hash = parameters.staking_key_hash - certificate_pointer = parameters.certificate_pointer - script_payment_hash = parameters.script_payment_hash - script_staking_hash = parameters.script_staking_hash + address_n = parameters.address_n # local_cache_attribute + address_n_staking = parameters.address_n_staking # local_cache_attribute + staking_key_hash = parameters.staking_key_hash # local_cache_attribute + certificate_pointer = parameters.certificate_pointer # local_cache_attribute + script_payment_hash = parameters.script_payment_hash # local_cache_attribute + script_staking_hash = parameters.script_staking_hash # local_cache_attribute + CAT = CardanoAddressType # local_cache_global fields_to_be_empty: dict[CardanoAddressType, tuple[Any, ...]] = { - CardanoAddressType.BASE: ( + CAT.BASE: ( certificate_pointer, script_payment_hash, script_staking_hash, ), - CardanoAddressType.BASE_KEY_SCRIPT: ( + CAT.BASE_KEY_SCRIPT: ( address_n_staking, certificate_pointer, script_payment_hash, ), - CardanoAddressType.BASE_SCRIPT_KEY: ( + CAT.BASE_SCRIPT_KEY: ( address_n, certificate_pointer, script_staking_hash, ), - CardanoAddressType.BASE_SCRIPT_SCRIPT: ( + CAT.BASE_SCRIPT_SCRIPT: ( address_n, address_n_staking, certificate_pointer, ), - CardanoAddressType.POINTER: ( + CAT.POINTER: ( address_n_staking, staking_key_hash, script_payment_hash, script_staking_hash, ), - CardanoAddressType.POINTER_SCRIPT: ( + CAT.POINTER_SCRIPT: ( address_n, address_n_staking, staking_key_hash, script_staking_hash, ), - CardanoAddressType.ENTERPRISE: ( + CAT.ENTERPRISE: ( address_n_staking, staking_key_hash, certificate_pointer, script_payment_hash, script_staking_hash, ), - CardanoAddressType.ENTERPRISE_SCRIPT: ( + CAT.ENTERPRISE_SCRIPT: ( address_n, address_n_staking, staking_key_hash, certificate_pointer, script_staking_hash, ), - CardanoAddressType.BYRON: ( + CAT.BYRON: ( address_n_staking, staking_key_hash, certificate_pointer, script_payment_hash, script_staking_hash, ), - CardanoAddressType.REWARD: ( + CAT.REWARD: ( address_n, staking_key_hash, certificate_pointer, script_payment_hash, script_staking_hash, ), - CardanoAddressType.REWARD_SCRIPT: ( + CAT.REWARD_SCRIPT: ( address_n, address_n_staking, staking_key_hash, @@ -189,6 +207,8 @@ def _validate_base_address_staking_info( staking_path: list[int], staking_key_hash: bytes | None, ) -> None: + from .helpers import ADDRESS_KEY_HASH_SIZE + assert_params_cond(not (staking_key_hash and staking_path)) if staking_key_hash: @@ -200,6 +220,8 @@ def _validate_base_address_staking_info( def _validate_script_hash(script_hash: bytes | None) -> None: + from .helpers import SCRIPT_HASH_SIZE + assert_params_cond(script_hash is not None and len(script_hash) == SCRIPT_HASH_SIZE) @@ -213,6 +235,13 @@ def validate_output_address_parameters( assert_params_cond(parameters.address_type in ADDRESS_TYPES_PAYMENT_KEY) +def validate_cvote_payment_address_parameters( + parameters: messages.CardanoAddressParametersType, +) -> None: + validate_address_parameters(parameters) + assert_params_cond(parameters.address_type in ADDRESS_TYPES_SHELLEY) + + def assert_cond(condition: bool) -> None: if not condition: raise wire.ProcessError("Invalid address") @@ -232,7 +261,22 @@ def _validate_and_get_type(address: str, protocol_magic: int, network_id: int) - if address_type == CardanoAddressType.BYRON: byron_addresses.validate(address_bytes, protocol_magic) elif address_type in ADDRESS_TYPES_SHELLEY: - _validate_shelley_address(address, address_bytes, network_id) + # _validate_shelley_address + + # _validate_size + assert_cond( + _MIN_ADDRESS_BYTES_LENGTH <= len(address_bytes) <= _MAX_ADDRESS_BYTES_LENGTH + ) + + # _validate_bech32_hrp + valid_hrp = _get_bech32_hrp(address_type, network_id) + # get_hrp + bech32_hrp = address.rsplit(bech32.HRP_SEPARATOR, 1)[0] + assert_cond(valid_hrp == bech32_hrp) + + # _validate_network_id + if _get_network_id(address_bytes) != network_id: + raise wire.ProcessError("Output address network mismatch") else: raise wire.ProcessError("Invalid address") @@ -254,6 +298,13 @@ def validate_reward_address(address: str, protocol_magic: int, network_id: int) ) +def validate_cvote_payment_address( + address: str, protocol_magic: int, network_id: int +) -> None: + address_type = _validate_and_get_type(address, protocol_magic, network_id) + assert_cond(address_type in ADDRESS_TYPES_SHELLEY) + + def get_bytes_unsafe(address: str) -> bytes: try: address_bytes = bech32.decode_unsafe(address) @@ -270,32 +321,9 @@ def get_type(address: bytes) -> CardanoAddressType: return address[0] >> 4 # type: ignore [int-into-enum] -def _validate_shelley_address( - address_str: str, address_bytes: bytes, network_id: int -) -> None: - address_type = get_type(address_bytes) - - _validate_size(address_bytes) - _validate_bech32_hrp(address_str, address_type, network_id) - _validate_network_id(address_bytes, network_id) - - -def _validate_size(address_bytes: bytes) -> None: - assert_cond( - MIN_ADDRESS_BYTES_LENGTH <= len(address_bytes) <= MAX_ADDRESS_BYTES_LENGTH - ) - - -def _validate_bech32_hrp( - address_str: str, address_type: CardanoAddressType, network_id: int -) -> None: - valid_hrp = _get_bech32_hrp(address_type, network_id) - bech32_hrp = bech32.get_hrp(address_str) - - assert_cond(valid_hrp == bech32_hrp) - - def _get_bech32_hrp(address_type: CardanoAddressType, network_id: int) -> str: + from .helpers import bech32, network_ids + if address_type == CardanoAddressType.BYRON: # Byron address uses base58 encoding raise ValueError @@ -312,17 +340,12 @@ def _get_bech32_hrp(address_type: CardanoAddressType, network_id: int) -> str: return bech32.HRP_TESTNET_ADDRESS -def _validate_network_id(address: bytes, network_id: int) -> None: - if _get_network_id(address) != network_id: - raise wire.ProcessError("Output address network mismatch") - - def _get_network_id(address: bytes) -> int: return address[0] & 0x0F def derive_human_readable( - keychain: seed.Keychain, + keychain: Keychain, parameters: messages.CardanoAddressParametersType, protocol_magic: int, network_id: int, @@ -343,7 +366,7 @@ def encode_human_readable(address_bytes: bytes) -> str: def derive_bytes( - keychain: seed.Keychain, + keychain: Keychain, parameters: messages.CardanoAddressParametersType, protocol_magic: int, network_id: int, @@ -359,11 +382,13 @@ def derive_bytes( def _derive_shelley_address( - keychain: seed.Keychain, + keychain: Keychain, parameters: messages.CardanoAddressParametersType, network_id: int, ) -> bytes: - header = _create_header(parameters.address_type, network_id) + # _create_header + header_int = parameters.address_type << 4 | network_id + header = header_int.to_bytes(1, "little") payment_part = _get_payment_part(keychain, parameters) staking_part = _get_staking_part(keychain, parameters) @@ -371,13 +396,8 @@ def _derive_shelley_address( return header + payment_part + staking_part -def _create_header(address_type: CardanoAddressType, network_id: int) -> bytes: - header: int = address_type << 4 | network_id - return header.to_bytes(1, "little") - - def _get_payment_part( - keychain: seed.Keychain, parameters: messages.CardanoAddressParametersType + keychain: Keychain, parameters: messages.CardanoAddressParametersType ) -> bytes: if parameters.address_n: return get_public_key_hash(keychain, parameters.address_n) @@ -388,8 +408,10 @@ def _get_payment_part( def _get_staking_part( - keychain: seed.Keychain, parameters: messages.CardanoAddressParametersType + keychain: Keychain, parameters: messages.CardanoAddressParametersType ) -> bytes: + from .helpers.utils import variable_length_encode + if parameters.staking_key_hash: return parameters.staking_key_hash elif parameters.address_n_staking: @@ -397,16 +419,11 @@ def _get_staking_part( elif parameters.script_staking_hash: return parameters.script_staking_hash elif parameters.certificate_pointer: - return _encode_certificate_pointer(parameters.certificate_pointer) + # _encode_certificate_pointer + pointer = parameters.certificate_pointer + block_index_encoded = variable_length_encode(pointer.block_index) + tx_index_encoded = variable_length_encode(pointer.tx_index) + certificate_index_encoded = variable_length_encode(pointer.certificate_index) + return bytes(block_index_encoded + tx_index_encoded + certificate_index_encoded) else: return bytes() - - -def _encode_certificate_pointer( - pointer: messages.CardanoBlockchainPointerType, -) -> bytes: - block_index_encoded = variable_length_encode(pointer.block_index) - tx_index_encoded = variable_length_encode(pointer.tx_index) - certificate_index_encoded = variable_length_encode(pointer.certificate_index) - - return bytes(block_index_encoded + tx_index_encoded + certificate_index_encoded) diff --git a/core/src/apps/cardano/auxiliary_data.py b/core/src/apps/cardano/auxiliary_data.py index 54001a9b62..2155490cca 100644 --- a/core/src/apps/cardano/auxiliary_data.py +++ b/core/src/apps/cardano/auxiliary_data.py @@ -1,41 +1,32 @@ +from micropython import const from typing import TYPE_CHECKING from trezor import messages, wire from trezor.crypto import hashlib -from trezor.crypto.curve import ed25519 -from trezor.enums import ( - CardanoAddressType, - CardanoGovernanceRegistrationFormat, - CardanoTxAuxiliaryDataSupplementType, -) +from trezor.enums import CardanoAddressType, CardanoCVoteRegistrationFormat from apps.common import cbor from . import addresses, layout -from .helpers import bech32 from .helpers.paths import SCHEMA_STAKING_ANY_ACCOUNT from .helpers.utils import derive_public_key if TYPE_CHECKING: Delegations = list[tuple[bytes, int]] - GovernanceRegistrationPayload = dict[int, Delegations | bytes | int] - SignedGovernanceRegistrationPayload = tuple[GovernanceRegistrationPayload, bytes] - GovernanceRegistrationSignature = dict[int, bytes] - GovernanceRegistration = dict[ - int, GovernanceRegistrationPayload | GovernanceRegistrationSignature - ] + CVoteRegistrationPayload = dict[int, Delegations | bytes | int] + SignedCVoteRegistrationPayload = tuple[CVoteRegistrationPayload, bytes] from . import seed -AUXILIARY_DATA_HASH_SIZE = 32 -GOVERNANCE_VOTING_PUBLIC_KEY_LENGTH = 32 -GOVERNANCE_REGISTRATION_HASH_SIZE = 32 +_AUXILIARY_DATA_HASH_SIZE = const(32) +_CVOTE_PUBLIC_KEY_LENGTH = const(32) +_CVOTE_REGISTRATION_HASH_SIZE = const(32) -METADATA_KEY_GOVERNANCE_REGISTRATION = 61284 -METADATA_KEY_GOVERNANCE_REGISTRATION_SIGNATURE = 61285 +_METADATA_KEY_CVOTE_REGISTRATION = const(61284) +_METADATA_KEY_CVOTE_REGISTRATION_SIGNATURE = const(61285) -MAX_DELEGATION_COUNT = 32 -DEFAULT_VOTING_PURPOSE = 0 +_MAX_DELEGATION_COUNT = const(32) +_DEFAULT_VOTING_PURPOSE = const(0) def assert_cond(condition: bool) -> None: @@ -43,65 +34,79 @@ def assert_cond(condition: bool) -> None: raise wire.ProcessError("Invalid auxiliary data") -def validate(auxiliary_data: messages.CardanoTxAuxiliaryData) -> None: +def validate( + auxiliary_data: messages.CardanoTxAuxiliaryData, + protocol_magic: int, + network_id: int, +) -> None: fields_provided = 0 if auxiliary_data.hash: fields_provided += 1 - _validate_hash(auxiliary_data.hash) - if auxiliary_data.governance_registration_parameters: + # _validate_hash + assert_cond(len(auxiliary_data.hash) == _AUXILIARY_DATA_HASH_SIZE) + if auxiliary_data.cvote_registration_parameters: fields_provided += 1 - _validate_governance_registration_parameters( - auxiliary_data.governance_registration_parameters + _validate_cvote_registration_parameters( + auxiliary_data.cvote_registration_parameters, + protocol_magic, + network_id, ) assert_cond(fields_provided == 1) -def _validate_hash(auxiliary_data_hash: bytes) -> None: - assert_cond(len(auxiliary_data_hash) == AUXILIARY_DATA_HASH_SIZE) - - -def _validate_governance_registration_parameters( - parameters: messages.CardanoGovernanceRegistrationParametersType, +def _validate_cvote_registration_parameters( + parameters: messages.CardanoCVoteRegistrationParametersType, + protocol_magic: int, + network_id: int, ) -> None: - voting_key_fields_provided = 0 - if parameters.voting_public_key is not None: - voting_key_fields_provided += 1 - _validate_voting_public_key(parameters.voting_public_key) + vote_key_fields_provided = 0 + if parameters.vote_public_key is not None: + vote_key_fields_provided += 1 + _validate_vote_public_key(parameters.vote_public_key) if parameters.delegations: - voting_key_fields_provided += 1 - assert_cond(parameters.format == CardanoGovernanceRegistrationFormat.CIP36) + vote_key_fields_provided += 1 + assert_cond(parameters.format == CardanoCVoteRegistrationFormat.CIP36) _validate_delegations(parameters.delegations) - assert_cond(voting_key_fields_provided == 1) + assert_cond(vote_key_fields_provided == 1) assert_cond(SCHEMA_STAKING_ANY_ACCOUNT.match(parameters.staking_path)) - address_parameters = parameters.reward_address_parameters - assert_cond(address_parameters.address_type != CardanoAddressType.BYRON) - addresses.validate_address_parameters(address_parameters) + payment_address_fields_provided = 0 + if parameters.payment_address is not None: + payment_address_fields_provided += 1 + addresses.validate_cvote_payment_address( + parameters.payment_address, protocol_magic, network_id + ) + if parameters.payment_address_parameters: + payment_address_fields_provided += 1 + addresses.validate_cvote_payment_address_parameters( + parameters.payment_address_parameters + ) + assert_cond(payment_address_fields_provided == 1) if parameters.voting_purpose is not None: - assert_cond(parameters.format == CardanoGovernanceRegistrationFormat.CIP36) + assert_cond(parameters.format == CardanoCVoteRegistrationFormat.CIP36) -def _validate_voting_public_key(key: bytes) -> None: - assert_cond(len(key) == GOVERNANCE_VOTING_PUBLIC_KEY_LENGTH) +def _validate_vote_public_key(key: bytes) -> None: + assert_cond(len(key) == _CVOTE_PUBLIC_KEY_LENGTH) def _validate_delegations( - delegations: list[messages.CardanoGovernanceDelegation], + delegations: list[messages.CardanoCVoteDelegation], ) -> None: - assert_cond(len(delegations) <= MAX_DELEGATION_COUNT) + assert_cond(len(delegations) <= _MAX_DELEGATION_COUNT) for delegation in delegations: - _validate_voting_public_key(delegation.voting_public_key) + _validate_vote_public_key(delegation.vote_public_key) def _get_voting_purpose_to_serialize( - parameters: messages.CardanoGovernanceRegistrationParametersType, + parameters: messages.CardanoCVoteRegistrationParametersType, ) -> int | None: - if parameters.format == CardanoGovernanceRegistrationFormat.CIP15: + if parameters.format == CardanoCVoteRegistrationFormat.CIP15: return None if parameters.voting_purpose is None: - return DEFAULT_VOTING_PURPOSE + return _DEFAULT_VOTING_PURPOSE return parameters.voting_purpose @@ -109,13 +114,13 @@ async def show( ctx: wire.Context, keychain: seed.Keychain, auxiliary_data_hash: bytes, - parameters: messages.CardanoGovernanceRegistrationParametersType | None, + parameters: messages.CardanoCVoteRegistrationParametersType | None, protocol_magic: int, network_id: int, should_show_details: bool, ) -> None: if parameters: - await _show_governance_registration( + await _show_cvote_registration( ctx, keychain, parameters, @@ -128,44 +133,69 @@ async def show( await layout.show_auxiliary_data_hash(ctx, auxiliary_data_hash) -async def _show_governance_registration( +def _should_show_payment_warning(address_type: CardanoAddressType) -> bool: + # For cvote payment addresses that are actually REWARD addresses, we show a warning that the + # address is not eligible for rewards. https://github.com/cardano-foundation/CIPs/pull/373 + # However, the registration is otherwise valid, so we allow such addresses since we don't + # want to prevent the user from voting just because they use an outdated SW wallet. + return address_type not in addresses.ADDRESS_TYPES_PAYMENT + + +async def _show_cvote_registration( ctx: wire.Context, keychain: seed.Keychain, - parameters: messages.CardanoGovernanceRegistrationParametersType, + parameters: messages.CardanoCVoteRegistrationParametersType, protocol_magic: int, network_id: int, should_show_details: bool, ) -> None: + from .helpers import bech32 + from .helpers.credential import Credential, should_show_credentials + for delegation in parameters.delegations: encoded_public_key = bech32.encode( - bech32.HRP_GOVERNANCE_PUBLIC_KEY, delegation.voting_public_key + bech32.HRP_CVOTE_PUBLIC_KEY, delegation.vote_public_key ) - await layout.confirm_governance_registration_delegation( + await layout.confirm_cvote_registration_delegation( ctx, encoded_public_key, delegation.weight ) + if parameters.payment_address: + show_payment_warning = _should_show_payment_warning( + addresses.get_type(addresses.get_bytes_unsafe(parameters.payment_address)) + ) + await layout.confirm_cvote_registration_payment_address( + ctx, parameters.payment_address, show_payment_warning + ) + else: + address_parameters = parameters.payment_address_parameters + assert address_parameters # _validate_cvote_registration_parameters + show_both_credentials = should_show_credentials(address_parameters) + show_payment_warning = _should_show_payment_warning( + address_parameters.address_type + ) + await layout.show_cvote_registration_payment_credentials( + ctx, + Credential.payment_credential(address_parameters), + Credential.stake_credential(address_parameters), + show_both_credentials, + show_payment_warning, + ) + encoded_public_key: str | None = None - if parameters.voting_public_key: + if parameters.vote_public_key: encoded_public_key = bech32.encode( - bech32.HRP_GOVERNANCE_PUBLIC_KEY, parameters.voting_public_key + bech32.HRP_CVOTE_PUBLIC_KEY, parameters.vote_public_key ) - reward_address = addresses.derive_human_readable( - keychain, - parameters.reward_address_parameters, - protocol_magic, - network_id, - ) - voting_purpose: int | None = ( _get_voting_purpose_to_serialize(parameters) if should_show_details else None ) - await layout.confirm_governance_registration( + await layout.confirm_cvote_registration( ctx, encoded_public_key, parameters.staking_path, - reward_address, parameters.nonce, voting_purpose, ) @@ -177,20 +207,22 @@ def get_hash_and_supplement( protocol_magic: int, network_id: int, ) -> tuple[bytes, messages.CardanoTxAuxiliaryDataSupplement]: - if parameters := auxiliary_data.governance_registration_parameters: + from trezor.enums import CardanoTxAuxiliaryDataSupplementType + + if parameters := auxiliary_data.cvote_registration_parameters: ( - governance_registration_payload, - governance_signature, - ) = _get_signed_governance_registration_payload( + cvote_registration_payload, + cvote_registration_signature, + ) = _get_signed_cvote_registration_payload( keychain, parameters, protocol_magic, network_id ) - auxiliary_data_hash = _get_governance_registration_hash( - governance_registration_payload, governance_signature + auxiliary_data_hash = _get_cvote_registration_hash( + cvote_registration_payload, cvote_registration_signature ) auxiliary_data_supplement = messages.CardanoTxAuxiliaryDataSupplement( - type=CardanoTxAuxiliaryDataSupplementType.GOVERNANCE_REGISTRATION_SIGNATURE, + type=CardanoTxAuxiliaryDataSupplementType.CVOTE_REGISTRATION_SIGNATURE, auxiliary_data_hash=auxiliary_data_hash, - governance_signature=governance_signature, + cvote_registration_signature=cvote_registration_signature, ) return auxiliary_data_hash, auxiliary_data_supplement else: @@ -200,67 +232,74 @@ def get_hash_and_supplement( ) -def _get_governance_registration_hash( - governance_registration_payload: GovernanceRegistrationPayload, - governance_registration_payload_signature: bytes, +def _get_cvote_registration_hash( + cvote_registration_payload: CVoteRegistrationPayload, + cvote_registration_payload_signature: bytes, ) -> bytes: - cborized_governance_registration = _cborize_governance_registration( - governance_registration_payload, - governance_registration_payload_signature, - ) - return _get_hash(cbor.encode(_wrap_metadata(cborized_governance_registration))) - - -def _cborize_governance_registration( - governance_registration_payload: GovernanceRegistrationPayload, - governance_registration_payload_signature: bytes, -) -> GovernanceRegistration: - governance_registration_signature = {1: governance_registration_payload_signature} - - return { - METADATA_KEY_GOVERNANCE_REGISTRATION: governance_registration_payload, - METADATA_KEY_GOVERNANCE_REGISTRATION_SIGNATURE: governance_registration_signature, + # _cborize_catalyst_registration + cvote_registration_signature = {1: cvote_registration_payload_signature} + cborized_catalyst_registration = { + _METADATA_KEY_CVOTE_REGISTRATION: cvote_registration_payload, + _METADATA_KEY_CVOTE_REGISTRATION_SIGNATURE: cvote_registration_signature, } + # _get_hash + # _wrap_metadata + # A new structure of metadata is used after Cardano Mary era. The metadata + # is wrapped in a tuple and auxiliary_scripts may follow it. Cardano + # tooling uses this new format of "wrapped" metadata even if no + # auxiliary_scripts are included. So we do the same here. + # https://github.com/input-output-hk/cardano-ledger-specs/blob/f7deb22be14d31b535f56edc3ca542c548244c67/shelley-ma/shelley-ma-test/cddl-files/shelley-ma.cddl#L212 + metadata = (cborized_catalyst_registration, ()) + auxiliary_data = cbor.encode(metadata) + return hashlib.blake2b( + data=auxiliary_data, outlen=_AUXILIARY_DATA_HASH_SIZE + ).digest() + -def _get_signed_governance_registration_payload( +def _get_signed_cvote_registration_payload( keychain: seed.Keychain, - parameters: messages.CardanoGovernanceRegistrationParametersType, + parameters: messages.CardanoCVoteRegistrationParametersType, protocol_magic: int, network_id: int, -) -> SignedGovernanceRegistrationPayload: +) -> SignedCVoteRegistrationPayload: delegations_or_key: Delegations | bytes if len(parameters.delegations) > 0: delegations_or_key = [ - (delegation.voting_public_key, delegation.weight) + (delegation.vote_public_key, delegation.weight) for delegation in parameters.delegations ] - elif parameters.voting_public_key: - delegations_or_key = parameters.voting_public_key + elif parameters.vote_public_key: + delegations_or_key = parameters.vote_public_key else: - raise RuntimeError # should not be reached - _validate_governance_registration_parameters + raise RuntimeError # should not be reached - _validate_cvote_registration_parameters staking_key = derive_public_key(keychain, parameters.staking_path) - reward_address = addresses.derive_bytes( - keychain, - parameters.reward_address_parameters, - protocol_magic, - network_id, - ) + if parameters.payment_address: + payment_address = addresses.get_bytes_unsafe(parameters.payment_address) + else: + address_parameters = parameters.payment_address_parameters + assert address_parameters # _validate_cvote_registration_parameters + payment_address = addresses.derive_bytes( + keychain, + address_parameters, + protocol_magic, + network_id, + ) voting_purpose = _get_voting_purpose_to_serialize(parameters) - payload: GovernanceRegistrationPayload = { + payload: CVoteRegistrationPayload = { 1: delegations_or_key, 2: staking_key, - 3: reward_address, + 3: payment_address, 4: parameters.nonce, } if voting_purpose is not None: payload[5] = voting_purpose - signature = _create_governance_registration_payload_signature( + signature = _create_cvote_registration_payload_signature( keychain, payload, parameters.staking_path, @@ -269,40 +308,24 @@ def _get_signed_governance_registration_payload( return payload, signature -def _create_governance_registration_payload_signature( +def _create_cvote_registration_payload_signature( keychain: seed.Keychain, - governance_registration_payload: GovernanceRegistrationPayload, + cvote_registration_payload: CVoteRegistrationPayload, path: list[int], ) -> bytes: + from trezor.crypto.curve import ed25519 + node = keychain.derive(path) - encoded_governance_registration = cbor.encode( - {METADATA_KEY_GOVERNANCE_REGISTRATION: governance_registration_payload} + encoded_cvote_registration = cbor.encode( + {_METADATA_KEY_CVOTE_REGISTRATION: cvote_registration_payload} ) - governance_registration_hash = hashlib.blake2b( - data=encoded_governance_registration, - outlen=GOVERNANCE_REGISTRATION_HASH_SIZE, + cvote_registration_hash = hashlib.blake2b( + data=encoded_cvote_registration, + outlen=_CVOTE_REGISTRATION_HASH_SIZE, ).digest() return ed25519.sign_ext( - node.private_key(), node.private_key_ext(), governance_registration_hash + node.private_key(), node.private_key_ext(), cvote_registration_hash ) - - -def _wrap_metadata(metadata: dict) -> tuple[dict, tuple]: - """ - A new structure of metadata is used after Cardano Mary era. The metadata - is wrapped in a tuple and auxiliary_scripts may follow it. Cardano - tooling uses this new format of "wrapped" metadata even if no - auxiliary_scripts are included. So we do the same here. - - https://github.com/input-output-hk/cardano-ledger-specs/blob/f7deb22be14d31b535f56edc3ca542c548244c67/shelley-ma/shelley-ma-test/cddl-files/shelley-ma.cddl#L212 - """ - return metadata, () - - -def _get_hash(auxiliary_data: bytes) -> bytes: - return hashlib.blake2b( - data=auxiliary_data, outlen=AUXILIARY_DATA_HASH_SIZE - ).digest() diff --git a/core/src/apps/cardano/byron_addresses.py b/core/src/apps/cardano/byron_addresses.py index 7b1f818bc0..f956024e00 100644 --- a/core/src/apps/cardano/byron_addresses.py +++ b/core/src/apps/cardano/byron_addresses.py @@ -1,17 +1,17 @@ +from micropython import const from typing import TYPE_CHECKING -from trezor import log, wire -from trezor.crypto import crc, hashlib +from trezor.crypto import crc +from trezor.wire import ProcessError from apps.common import cbor from .helpers import protocol_magics -from .helpers.utils import derive_public_key if TYPE_CHECKING: from . import seed -PROTOCOL_MAGIC_KEY = 2 +_PROTOCOL_MAGIC_KEY = const(2) """ @@ -22,31 +22,28 @@ """ -def _encode_raw(address_data_encoded: bytes) -> bytes: - return cbor.encode( - [cbor.Tagged(24, address_data_encoded), crc.crc32(address_data_encoded)] - ) - - def derive(keychain: seed.Keychain, path: list, protocol_magic: int) -> bytes: - address_attributes = get_address_attributes(protocol_magic) - - address_root = _get_address_root(keychain, path, address_attributes) - address_type = 0 - address_data = [address_root, address_attributes, address_type] - address_data_encoded = cbor.encode(address_data) - - return _encode_raw(address_data_encoded) + from .helpers.utils import derive_public_key - -def get_address_attributes(protocol_magic: int) -> dict: + # get_address_attributes # protocol magic is included in Byron addresses only on testnets if protocol_magics.is_mainnet(protocol_magic): address_attributes = {} else: - address_attributes = {PROTOCOL_MAGIC_KEY: cbor.encode(protocol_magic)} + address_attributes = {_PROTOCOL_MAGIC_KEY: cbor.encode(protocol_magic)} + + # _get_address_root + extpubkey = derive_public_key(keychain, path, extended=True) + address_root = _address_hash([0, [0, extpubkey], address_attributes]) + + address_type = 0 + address_data = [address_root, address_attributes, address_type] + address_data_encoded = cbor.encode(address_data) - return address_attributes + # _encode_raw + return cbor.encode( + [cbor.Tagged(24, address_data_encoded), crc.crc32(address_data_encoded)] + ) def validate(address: bytes, protocol_magic: int) -> None: @@ -54,27 +51,38 @@ def validate(address: bytes, protocol_magic: int) -> None: _validate_protocol_magic(address_data_encoded, protocol_magic) +def _address_hash(data: list) -> bytes: + from trezor.crypto import hashlib + + cbor_data = cbor.encode(data) + sha_data_hash = hashlib.sha3_256(cbor_data).digest() + res = hashlib.blake2b(data=sha_data_hash, outlen=28).digest() + return res + + def _decode_raw(address: bytes) -> bytes: + from trezor import log + try: address_unpacked = cbor.decode(address) except ValueError as e: if __debug__: log.exception(__name__, e) - raise wire.ProcessError("Invalid address") + raise ProcessError("Invalid address") if not isinstance(address_unpacked, list) or len(address_unpacked) != 2: - raise wire.ProcessError("Invalid address") + raise ProcessError("Invalid address") address_data_encoded = address_unpacked[0] if not isinstance(address_data_encoded, bytes): - raise wire.ProcessError("Invalid address") + raise ProcessError("Invalid address") address_crc = address_unpacked[1] if not isinstance(address_crc, int): - raise wire.ProcessError("Invalid address") + raise ProcessError("Invalid address") if address_crc != crc.crc32(address_data_encoded): - raise wire.ProcessError("Invalid address") + raise ProcessError("Invalid address") return address_data_encoded @@ -87,35 +95,21 @@ def _validate_protocol_magic(address_data_encoded: bytes, protocol_magic: int) - """ address_data = cbor.decode(address_data_encoded) if not isinstance(address_data, list) or len(address_data) < 2: - raise wire.ProcessError("Invalid address") + raise ProcessError("Invalid address") attributes = address_data[1] if protocol_magics.is_mainnet(protocol_magic): - if PROTOCOL_MAGIC_KEY in attributes: - raise wire.ProcessError("Output address network mismatch") + if _PROTOCOL_MAGIC_KEY in attributes: + raise ProcessError("Output address network mismatch") else: # testnet - if len(attributes) == 0 or PROTOCOL_MAGIC_KEY not in attributes: - raise wire.ProcessError("Output address network mismatch") + if len(attributes) == 0 or _PROTOCOL_MAGIC_KEY not in attributes: + raise ProcessError("Output address network mismatch") - protocol_magic_cbor = attributes[PROTOCOL_MAGIC_KEY] + protocol_magic_cbor = attributes[_PROTOCOL_MAGIC_KEY] address_protocol_magic = cbor.decode(protocol_magic_cbor) if not isinstance(address_protocol_magic, int): - raise wire.ProcessError("Invalid address") + raise ProcessError("Invalid address") if address_protocol_magic != protocol_magic: - raise wire.ProcessError("Output address network mismatch") - - -def _address_hash(data: list) -> bytes: - cbor_data = cbor.encode(data) - sha_data_hash = hashlib.sha3_256(cbor_data).digest() - res = hashlib.blake2b(data=sha_data_hash, outlen=28).digest() - return res - - -def _get_address_root( - keychain: seed.Keychain, path: list[int], address_attributes: dict -) -> bytes: - extpubkey = derive_public_key(keychain, path, extended=True) - return _address_hash([0, [0, extpubkey], address_attributes]) + raise ProcessError("Output address network mismatch") diff --git a/core/src/apps/cardano/certificates.py b/core/src/apps/cardano/certificates.py index f404602598..762dea4329 100644 --- a/core/src/apps/cardano/certificates.py +++ b/core/src/apps/cardano/certificates.py @@ -1,32 +1,30 @@ +from micropython import const from typing import TYPE_CHECKING -from trezor import wire -from trezor.enums import CardanoCertificateType, CardanoPoolRelayType - -from apps.common import cbor +from trezor.enums import CardanoCertificateType, CardanoDRepType, CardanoPoolRelayType +from trezor.wire import ProcessError from . import addresses -from .helpers import ADDRESS_KEY_HASH_SIZE, LOVELACE_MAX_SUPPLY -from .helpers.paths import SCHEMA_STAKING_ANY_ACCOUNT -from .helpers.utils import get_public_key_hash, validate_stake_credential +from .helpers.utils import get_public_key_hash if TYPE_CHECKING: from typing import Any from trezor import messages + from apps.common.cbor import CborSequence from . import seed from .helpers.account_path_check import AccountPathChecker -POOL_HASH_SIZE = 28 -VRF_KEY_HASH_SIZE = 32 -POOL_METADATA_HASH_SIZE = 32 -IPV4_ADDRESS_SIZE = 4 -IPV6_ADDRESS_SIZE = 16 +_POOL_HASH_SIZE = const(28) +_VRF_KEY_HASH_SIZE = const(32) +_POOL_METADATA_HASH_SIZE = const(32) +_IPV4_ADDRESS_SIZE = const(4) +_IPV6_ADDRESS_SIZE = const(16) -MAX_URL_LENGTH = 64 -MAX_PORT_NUMBER = 65535 +_MAX_URL_LENGTH = const(128) +_MAX_PORT_NUMBER = const(65535) def validate( @@ -35,75 +33,130 @@ def validate( network_id: int, account_path_checker: AccountPathChecker, ) -> None: + from .helpers.utils import validate_stake_credential + _validate_structure(certificate) + CCT = CardanoCertificateType # local_cache_global + if certificate.type in ( - CardanoCertificateType.STAKE_DELEGATION, - CardanoCertificateType.STAKE_REGISTRATION, - CardanoCertificateType.STAKE_DEREGISTRATION, + CCT.STAKE_DELEGATION, + CCT.STAKE_REGISTRATION, + CCT.STAKE_DEREGISTRATION, + ): + validate_stake_credential( + certificate.path, + certificate.script_hash, + certificate.key_hash, + ProcessError("Invalid certificate"), + ) + + if certificate.type in ( + CCT.STAKE_REGISTRATION_CONWAY, + CCT.STAKE_DEREGISTRATION_CONWAY, ): + if certificate.deposit is None: + raise ProcessError("Invalid certificate") validate_stake_credential( certificate.path, certificate.script_hash, certificate.key_hash, - wire.ProcessError("Invalid certificate"), + ProcessError("Invalid certificate"), ) - if certificate.type == CardanoCertificateType.STAKE_DELEGATION: - if not certificate.pool or len(certificate.pool) != POOL_HASH_SIZE: - raise wire.ProcessError("Invalid certificate") + if certificate.type == CCT.STAKE_DELEGATION: + if not certificate.pool or len(certificate.pool) != _POOL_HASH_SIZE: + raise ProcessError("Invalid certificate") - if certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION: + if certificate.type == CCT.STAKE_POOL_REGISTRATION: if certificate.pool_parameters is None: - raise wire.ProcessError("Invalid certificate") + raise ProcessError("Invalid certificate") _validate_pool_parameters( certificate.pool_parameters, protocol_magic, network_id ) + if certificate.type == CCT.VOTE_DELEGATION: + if certificate.drep is None: + raise ProcessError("Invalid certificate") + validate_drep( + certificate.drep, + ProcessError("Invalid certificate"), + ) + validate_stake_credential( + certificate.path, + certificate.script_hash, + certificate.key_hash, + ProcessError("Invalid certificate"), + ) + account_path_checker.add_certificate(certificate) def _validate_structure(certificate: messages.CardanoTxCertificate) -> None: - pool = certificate.pool - pool_parameters = certificate.pool_parameters + pool = certificate.pool # local_cache_attribute + pool_parameters = certificate.pool_parameters # local_cache_attribute + deposit = certificate.deposit + drep = certificate.drep + CCT = CardanoCertificateType # local_cache_global fields_to_be_empty: dict[CardanoCertificateType, tuple[Any, ...]] = { - CardanoCertificateType.STAKE_REGISTRATION: (pool, pool_parameters), - CardanoCertificateType.STAKE_DELEGATION: (pool_parameters,), - CardanoCertificateType.STAKE_DEREGISTRATION: (pool, pool_parameters), - CardanoCertificateType.STAKE_POOL_REGISTRATION: ( + CCT.STAKE_REGISTRATION: (pool, pool_parameters, deposit, drep), + CCT.STAKE_REGISTRATION_CONWAY: (pool, pool_parameters, drep), + CCT.STAKE_DELEGATION: (pool_parameters, deposit, drep), + CCT.STAKE_DEREGISTRATION: (pool, pool_parameters, deposit, drep), + CCT.STAKE_DEREGISTRATION_CONWAY: (pool, pool_parameters, drep), + CCT.STAKE_POOL_REGISTRATION: ( certificate.path, certificate.script_hash, certificate.key_hash, pool, + deposit, + drep, ), + CCT.VOTE_DELEGATION: (pool, pool_parameters, deposit), } if certificate.type not in fields_to_be_empty or any( fields_to_be_empty[certificate.type] ): - raise wire.ProcessError("Invalid certificate") + raise ProcessError("Invalid certificate") def cborize( keychain: seed.Keychain, certificate: messages.CardanoTxCertificate ) -> CborSequence: - if certificate.type in ( + cert_type = certificate.type # local_cache_attribute + + if cert_type in ( CardanoCertificateType.STAKE_REGISTRATION, CardanoCertificateType.STAKE_DEREGISTRATION, ): return ( - certificate.type, + cert_type, + cborize_stake_credential( + keychain, + certificate.path, + certificate.script_hash, + certificate.key_hash, + ), + ) + elif cert_type in ( + CardanoCertificateType.STAKE_REGISTRATION_CONWAY, + CardanoCertificateType.STAKE_DEREGISTRATION_CONWAY, + ): + return ( + cert_type, cborize_stake_credential( keychain, certificate.path, certificate.script_hash, certificate.key_hash, ), + certificate.deposit, ) - elif certificate.type == CardanoCertificateType.STAKE_DELEGATION: + elif cert_type == CardanoCertificateType.STAKE_DELEGATION: return ( - certificate.type, + cert_type, cborize_stake_credential( keychain, certificate.path, @@ -112,6 +165,18 @@ def cborize( ), certificate.pool, ) + elif cert_type == CardanoCertificateType.VOTE_DELEGATION: + assert certificate.drep is not None + return ( + cert_type, + cborize_stake_credential( + keychain, + certificate.path, + certificate.script_hash, + certificate.key_hash, + ), + cborize_drep(certificate.drep), + ) else: raise RuntimeError # should be unreachable @@ -135,6 +200,8 @@ def cborize_stake_credential( def cborize_pool_registration_init( certificate: messages.CardanoTxCertificate, ) -> CborSequence: + from apps.common import cbor + assert certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION pool_parameters = certificate.pool_parameters @@ -161,7 +228,7 @@ def cborize_pool_registration_init( def assert_cond(condition: bool) -> None: if not condition: - raise wire.ProcessError("Invalid certificate") + raise ProcessError("Invalid certificate") def _validate_pool_parameters( @@ -169,8 +236,10 @@ def _validate_pool_parameters( protocol_magic: int, network_id: int, ) -> None: - assert_cond(len(pool_parameters.pool_id) == POOL_HASH_SIZE) - assert_cond(len(pool_parameters.vrf_key_hash) == VRF_KEY_HASH_SIZE) + from .helpers import LOVELACE_MAX_SUPPLY + + assert_cond(len(pool_parameters.pool_id) == _POOL_HASH_SIZE) + assert_cond(len(pool_parameters.vrf_key_hash) == _VRF_KEY_HASH_SIZE) assert_cond(0 <= pool_parameters.pledge <= LOVELACE_MAX_SUPPLY) assert_cond(0 <= pool_parameters.cost <= LOVELACE_MAX_SUPPLY) assert_cond(pool_parameters.margin_numerator >= 0) @@ -181,14 +250,46 @@ def _validate_pool_parameters( addresses.validate_reward_address( pool_parameters.reward_account, protocol_magic, network_id ) + pool_metadata = pool_parameters.metadata # local_cache_attribute + if pool_metadata: + # _validate_pool_metadata + assert_cond(len(pool_metadata.url) <= _MAX_URL_LENGTH) + assert_cond(len(pool_metadata.hash) == _POOL_METADATA_HASH_SIZE) + assert_cond(all((32 <= ord(c) < 127) for c in pool_metadata.url)) + - if pool_parameters.metadata: - _validate_pool_metadata(pool_parameters.metadata) +def validate_drep( + drep: messages.CardanoDRep, + error: ProcessError, +) -> None: + from .helpers import ADDRESS_KEY_HASH_SIZE, SCRIPT_HASH_SIZE + + drep_type = drep.type + script_hash = drep.script_hash + key_hash = drep.key_hash + + if drep_type == CardanoDRepType.KEY_HASH: + if script_hash or not key_hash or len(key_hash) != ADDRESS_KEY_HASH_SIZE: + raise error + elif drep_type == CardanoDRepType.SCRIPT_HASH: + if key_hash or not script_hash or len(script_hash) != SCRIPT_HASH_SIZE: + raise error + elif drep_type in ( + CardanoDRepType.ABSTAIN, + CardanoDRepType.NO_CONFIDENCE, + ): + if script_hash or key_hash: + raise error + else: + raise RuntimeError # should be unreachable def validate_pool_owner( owner: messages.CardanoPoolOwner, account_path_checker: AccountPathChecker ) -> None: + from .helpers import ADDRESS_KEY_HASH_SIZE + from .helpers.paths import SCHEMA_STAKING_ANY_ACCOUNT + assert_cond( owner.staking_key_hash is not None or owner.staking_key_path is not None ) @@ -201,38 +302,40 @@ def validate_pool_owner( def validate_pool_relay(pool_relay: messages.CardanoPoolRelayParameters) -> None: + port = pool_relay.port # local_cache_attribute + host_name = pool_relay.host_name # local_cache_attribute + if pool_relay.type == CardanoPoolRelayType.SINGLE_HOST_IP: assert_cond( pool_relay.ipv4_address is not None or pool_relay.ipv6_address is not None ) if pool_relay.ipv4_address is not None: - assert_cond(len(pool_relay.ipv4_address) == IPV4_ADDRESS_SIZE) + assert_cond(len(pool_relay.ipv4_address) == _IPV4_ADDRESS_SIZE) if pool_relay.ipv6_address is not None: - assert_cond(len(pool_relay.ipv6_address) == IPV6_ADDRESS_SIZE) - assert_cond( - pool_relay.port is not None and 0 <= pool_relay.port <= MAX_PORT_NUMBER - ) + assert_cond(len(pool_relay.ipv6_address) == _IPV6_ADDRESS_SIZE) + assert_cond(port is not None and 0 <= port <= _MAX_PORT_NUMBER) elif pool_relay.type == CardanoPoolRelayType.SINGLE_HOST_NAME: - assert_cond( - pool_relay.host_name is not None - and len(pool_relay.host_name) <= MAX_URL_LENGTH - ) - assert_cond( - pool_relay.port is not None and 0 <= pool_relay.port <= MAX_PORT_NUMBER - ) + assert_cond(host_name is not None and len(host_name) <= _MAX_URL_LENGTH) + assert_cond(port is not None and 0 <= port <= _MAX_PORT_NUMBER) elif pool_relay.type == CardanoPoolRelayType.MULTIPLE_HOST_NAME: - assert_cond( - pool_relay.host_name is not None - and len(pool_relay.host_name) <= MAX_URL_LENGTH - ) + assert_cond(host_name is not None and len(host_name) <= _MAX_URL_LENGTH) else: raise RuntimeError # should be unreachable -def _validate_pool_metadata(pool_metadata: messages.CardanoPoolMetadataType) -> None: - assert_cond(len(pool_metadata.url) <= MAX_URL_LENGTH) - assert_cond(len(pool_metadata.hash) == POOL_METADATA_HASH_SIZE) - assert_cond(all((32 <= ord(c) < 127) for c in pool_metadata.url)) +def cborize_drep(drep: messages.CardanoDRep) -> tuple[int, bytes] | tuple[int]: + if drep.type == CardanoDRepType.KEY_HASH: + assert drep.key_hash is not None + return 0, drep.key_hash + elif drep.type == CardanoDRepType.SCRIPT_HASH: + assert drep.script_hash is not None + return 1, drep.script_hash + elif drep.type == CardanoDRepType.ABSTAIN: + return (2,) + elif drep.type == CardanoDRepType.NO_CONFIDENCE: + return (3,) + else: + raise RuntimeError # should be unreachable def cborize_pool_owner( @@ -251,7 +354,7 @@ def _cborize_ipv6_address(ipv6_address: bytes | None) -> bytes | None: return None # ipv6 addresses are serialized to CBOR as uint_32[4] little endian - assert len(ipv6_address) == IPV6_ADDRESS_SIZE + assert len(ipv6_address) == _IPV6_ADDRESS_SIZE result = b"" for i in range(0, 4): @@ -263,22 +366,24 @@ def _cborize_ipv6_address(ipv6_address: bytes | None) -> bytes | None: def cborize_pool_relay( pool_relay: messages.CardanoPoolRelayParameters, ) -> CborSequence: - if pool_relay.type == CardanoPoolRelayType.SINGLE_HOST_IP: + relay_type = pool_relay.type # local_cache_attribute + + if relay_type == CardanoPoolRelayType.SINGLE_HOST_IP: return ( - pool_relay.type, + relay_type, pool_relay.port, pool_relay.ipv4_address, _cborize_ipv6_address(pool_relay.ipv6_address), ) - elif pool_relay.type == CardanoPoolRelayType.SINGLE_HOST_NAME: + elif relay_type == CardanoPoolRelayType.SINGLE_HOST_NAME: return ( - pool_relay.type, + relay_type, pool_relay.port, pool_relay.host_name, ) - elif pool_relay.type == CardanoPoolRelayType.MULTIPLE_HOST_NAME: + elif relay_type == CardanoPoolRelayType.MULTIPLE_HOST_NAME: return ( - pool_relay.type, + relay_type, pool_relay.host_name, ) else: diff --git a/core/src/apps/cardano/get_address.py b/core/src/apps/cardano/get_address.py index 44d8bc0e09..da9ffa60d9 100644 --- a/core/src/apps/cardano/get_address.py +++ b/core/src/apps/cardano/get_address.py @@ -1,17 +1,32 @@ -from trezor import log, messages, wire +from typing import TYPE_CHECKING -from . import addresses, seed -from .helpers.credential import Credential, should_show_credentials -from .helpers.utils import validate_network_info -from .layout import show_cardano_address, show_credentials +from trezor import wire +from trezor.messages import CardanoAddress + +from . import seed + +if __debug__: + from trezor import log + +if TYPE_CHECKING: + from trezor.messages import CardanoGetAddress @seed.with_keychain async def get_address( - ctx: wire.Context, msg: messages.CardanoGetAddress, keychain: seed.Keychain -) -> messages.CardanoAddress: + ctx: wire.Context, msg: CardanoGetAddress, keychain: seed.Keychain +) -> CardanoAddress: + + from . import addresses + from .helpers.credential import Credential, should_show_credentials + from .helpers.utils import validate_network_info + from .layout import show_cardano_address, show_credentials + + address_parameters = msg.address_parameters # local_cache_attribute + validate_network_info(msg.network_id, msg.protocol_magic) - addresses.validate_address_parameters(msg.address_parameters) + addresses.validate_address_parameters(address_parameters) + from trezor.lvglui.scrs import lv from . import ICON, PRIMARY_COLOR @@ -19,7 +34,7 @@ async def get_address( try: address = addresses.derive_human_readable( - keychain, msg.address_parameters, msg.protocol_magic, msg.network_id + keychain, address_parameters, msg.protocol_magic, msg.network_id ) except ValueError as e: if __debug__: @@ -27,22 +42,19 @@ async def get_address( raise wire.ProcessError("Deriving address failed") if msg.show_display: - await _display_address(ctx, msg.address_parameters, address, msg.protocol_magic) - - return messages.CardanoAddress(address=address) - - -async def _display_address( - ctx: wire.Context, - address_parameters: messages.CardanoAddressParametersType, - address: str, - protocol_magic: int, -) -> None: - if should_show_credentials(address_parameters): - await show_credentials( + # _display_address + if should_show_credentials(address_parameters): + await show_credentials( + ctx, + Credential.payment_credential(address_parameters), + Credential.stake_credential(address_parameters), + ) + await show_cardano_address( ctx, - Credential.payment_credential(address_parameters), - Credential.stake_credential(address_parameters), + address_parameters, + address, + msg.protocol_magic, + chunkify=bool(msg.chunkify), ) - await show_cardano_address(ctx, address_parameters, address, protocol_magic) + return CardanoAddress(address=address) diff --git a/core/src/apps/cardano/get_native_script_hash.py b/core/src/apps/cardano/get_native_script_hash.py index e5f4130676..403e0005a1 100644 --- a/core/src/apps/cardano/get_native_script_hash.py +++ b/core/src/apps/cardano/get_native_script_hash.py @@ -1,13 +1,22 @@ -from trezor import messages, wire -from trezor.enums import CardanoNativeScriptHashDisplayFormat +from typing import TYPE_CHECKING -from . import layout, native_script, seed +from trezor.messages import CardanoNativeScriptHash + +from . import seed + +if TYPE_CHECKING: + from trezor.messages import CardanoGetNativeScriptHash + from trezor import wire @seed.with_keychain async def get_native_script_hash( - ctx: wire.Context, msg: messages.CardanoGetNativeScriptHash, keychain: seed.Keychain -) -> messages.CardanoNativeScriptHash: + ctx: wire.Context, msg: CardanoGetNativeScriptHash, keychain: seed.Keychain +) -> CardanoNativeScriptHash: + from trezor.enums import CardanoNativeScriptHashDisplayFormat + + from . import layout, native_script + native_script.validate_native_script(msg.script) script_hash = native_script.get_native_script_hash(keychain, msg.script) @@ -16,4 +25,4 @@ async def get_native_script_hash( await layout.show_native_script(ctx, msg.script) await layout.show_script_hash(ctx, script_hash, msg.display_format) - return messages.CardanoNativeScriptHash(script_hash=script_hash) + return CardanoNativeScriptHash(script_hash=script_hash) diff --git a/core/src/apps/cardano/get_public_key.py b/core/src/apps/cardano/get_public_key.py index 66f6a22df1..343aab19c0 100644 --- a/core/src/apps/cardano/get_public_key.py +++ b/core/src/apps/cardano/get_public_key.py @@ -1,27 +1,43 @@ +from typing import TYPE_CHECKING from ubinascii import hexlify -from trezor import log, messages, wire -from trezor.ui.layouts import show_pubkey - -from apps.common import paths +from trezor import wire +from trezor.messages import CardanoPublicKey from . import seed -from .helpers.paths import SCHEMA_MINT, SCHEMA_PUBKEY -from .helpers.utils import derive_public_key + +if __debug__: + from trezor import log + +if TYPE_CHECKING: + from trezor.messages import CardanoGetPublicKey @seed.with_keychain async def get_public_key( - ctx: wire.Context, msg: messages.CardanoGetPublicKey, keychain: seed.Keychain -) -> messages.CardanoPublicKey: - address_n = msg.address_n + ctx: wire.Context, msg: CardanoGetPublicKey, keychain: seed.Keychain +) -> CardanoPublicKey: + + from trezor.ui.layouts import show_pubkey + + from apps.common import paths + + from .helpers.paths import SCHEMA_MINT, SCHEMA_PUBKEY + + address_n = msg.address_n # local_cache_attribute + await paths.validate_path( ctx, keychain, address_n, + True, # path must match the PUBKEY schema - SCHEMA_PUBKEY.match(address_n) or SCHEMA_MINT.match(address_n), + (SCHEMA_PUBKEY.match(address_n) or SCHEMA_MINT.match(address_n)), ) + from . import ICON, PRIMARY_COLOR + from trezor.lvglui.scrs import lv + + ctx.primary_color, ctx.icon_path = lv.color_hex(PRIMARY_COLOR), ICON try: key = _get_public_key(keychain, address_n) @@ -31,25 +47,31 @@ async def get_public_key( raise wire.ProcessError("Deriving public key failed") if msg.show_display: - await show_pubkey(ctx, hexlify(key.node.public_key).decode()) + from apps.common.paths import address_n_to_str + + path = address_n_to_str(address_n) + await show_pubkey(ctx, key.xpub, path=path, network="Cardano") return key def _get_public_key( keychain: seed.Keychain, derivation_path: list[int] -) -> messages.CardanoPublicKey: - node = keychain.derive(derivation_path) +) -> CardanoPublicKey: + from trezor.messages import HDNodeType + + from .helpers.utils import derive_public_key - public_key = hexlify(derive_public_key(keychain, derivation_path)).decode() - chain_code = hexlify(node.chain_code()).decode() - xpub_key = public_key + chain_code + node = keychain.derive(derivation_path) + public_key = derive_public_key(keychain, derivation_path) + chain_code = node.chain_code() + xpub_key = hexlify(public_key + chain_code).decode() - node_type = messages.HDNodeType( + node_type = HDNodeType( depth=node.depth(), child_num=node.child_num(), fingerprint=node.fingerprint(), - chain_code=node.chain_code(), - public_key=derive_public_key(keychain, derivation_path), + chain_code=chain_code, + public_key=public_key, ) - return messages.CardanoPublicKey(node=node_type, xpub=xpub_key) + return CardanoPublicKey(node=node_type, xpub=xpub_key) diff --git a/core/src/apps/cardano/helpers/account_path_check.py b/core/src/apps/cardano/helpers/account_path_check.py index e6f62dda00..4b85ad2367 100644 --- a/core/src/apps/cardano/helpers/account_path_check.py +++ b/core/src/apps/cardano/helpers/account_path_check.py @@ -1,19 +1,16 @@ from typing import TYPE_CHECKING -from trezor import wire +from trezor.wire import ProcessError -from ...common.paths import HARDENED from .. import seed -from .paths import ACCOUNT_PATH_INDEX, ACCOUNT_PATH_LENGTH -from .utils import to_account_path if TYPE_CHECKING: from trezor.messages import ( CardanoPoolOwner, CardanoTxCertificate, CardanoTxOutput, - CardanoTxWitnessRequest, CardanoTxWithdrawal, + CardanoTxWitnessRequest, ) @@ -29,7 +26,9 @@ class AccountPathChecker: def __init__(self) -> None: self.account_path: object | list[int] = self.UNDEFINED - def _add(self, path: list[int], error: wire.ProcessError) -> None: + def _add(self, path: list[int], error: ProcessError) -> None: + from .utils import to_account_path + # multi-sig and minting paths are always shown and thus don't need to be checked if seed.is_multisig_path(path) or seed.is_minting_path(path): return @@ -51,10 +50,15 @@ def _is_byron_and_shelley_equivalent(self, account_path: list[int]) -> bool: from the user. This way the user can be sure that the funds are being moved between the user's accounts without being bothered by more screens. """ - assert isinstance(self.account_path, list) + from ...common.paths import HARDENED + from .paths import ACCOUNT_PATH_INDEX, ACCOUNT_PATH_LENGTH + + self_account_path = self.account_path # local_cache_attribute + + assert isinstance(self_account_path, list) is_control_path_byron_or_shelley = seed.is_byron_path( - self.account_path - ) or seed.is_shelley_path(self.account_path) + self_account_path + ) or seed.is_shelley_path(self_account_path) is_new_path_byron_or_shelley = seed.is_byron_path( account_path @@ -63,9 +67,9 @@ def _is_byron_and_shelley_equivalent(self, account_path: list[int]) -> bool: return ( is_control_path_byron_or_shelley and is_new_path_byron_or_shelley - and len(self.account_path) == ACCOUNT_PATH_LENGTH + and len(self_account_path) == ACCOUNT_PATH_LENGTH and len(account_path) == ACCOUNT_PATH_LENGTH - and self.account_path[ACCOUNT_PATH_INDEX] == 0 | HARDENED + and self_account_path[ACCOUNT_PATH_INDEX] == 0 | HARDENED and account_path[ACCOUNT_PATH_INDEX] == 0 | HARDENED ) @@ -76,27 +80,25 @@ def add_output(self, output: CardanoTxOutput) -> None: if not output.address_parameters.address_n: return - self._add( - output.address_parameters.address_n, wire.ProcessError("Invalid output") - ) + self._add(output.address_parameters.address_n, ProcessError("Invalid output")) def add_certificate(self, certificate: CardanoTxCertificate) -> None: if not certificate.path: return - self._add(certificate.path, wire.ProcessError("Invalid certificate")) + self._add(certificate.path, ProcessError("Invalid certificate")) def add_pool_owner(self, pool_owner: CardanoPoolOwner) -> None: if not pool_owner.staking_key_path: return - self._add(pool_owner.staking_key_path, wire.ProcessError("Invalid certificate")) + self._add(pool_owner.staking_key_path, ProcessError("Invalid certificate")) def add_withdrawal(self, withdrawal: CardanoTxWithdrawal) -> None: if not withdrawal.path: return - self._add(withdrawal.path, wire.ProcessError("Invalid withdrawal")) + self._add(withdrawal.path, ProcessError("Invalid withdrawal")) def add_witness_request(self, witness_request: CardanoTxWitnessRequest) -> None: - self._add(witness_request.path, wire.ProcessError("Invalid witness request")) + self._add(witness_request.path, ProcessError("Invalid witness request")) diff --git a/core/src/apps/cardano/helpers/bech32.py b/core/src/apps/cardano/helpers/bech32.py index 82bda2e565..90369d1e91 100644 --- a/core/src/apps/cardano/helpers/bech32.py +++ b/core/src/apps/cardano/helpers/bech32.py @@ -7,7 +7,7 @@ HRP_TESTNET_ADDRESS = "addr_test" HRP_REWARD_ADDRESS = "stake" HRP_TESTNET_REWARD_ADDRESS = "stake_test" -HRP_GOVERNANCE_PUBLIC_KEY = "gov_vk" +HRP_CVOTE_PUBLIC_KEY = "cvote_vk" HRP_SCRIPT_HASH = "script" HRP_KEY_HASH = "addr_vkh" HRP_SHARED_KEY_HASH = "addr_shared_vkh" @@ -15,6 +15,8 @@ HRP_REQUIRED_SIGNER_KEY_HASH = "req_signer_vkh" HRP_OUTPUT_DATUM_HASH = "datum" HRP_SCRIPT_DATA_HASH = "script_data" +HRP_DREP_KEY_HASH = "drep" +HRP_DREP_SCRIPT_HASH = "drep_script" def encode(hrp: str, data: bytes) -> str: @@ -27,19 +29,13 @@ def decode_unsafe(bech: str) -> bytes: return _decode(hrp, bech) -def get_hrp(bech: str) -> str: - return bech.rsplit(HRP_SEPARATOR, 1)[0] - - def _decode(hrp: str, bech: str) -> bytes: decoded_hrp, data, spec = bech32.bech32_decode(bech, 130) if decoded_hrp != hrp: raise ValueError if spec != bech32.Encoding.BECH32: raise ValueError - - if data is not None: - decoded = bech32.convertbits(data, 5, 8, False) - else: + if data is None: raise ValueError + decoded = bech32.convertbits(data, 5, 8, False) return bytes(decoded) diff --git a/core/src/apps/cardano/helpers/credential.py b/core/src/apps/cardano/helpers/credential.py index f59c139776..af6f804dc0 100644 --- a/core/src/apps/cardano/helpers/credential.py +++ b/core/src/apps/cardano/helpers/credential.py @@ -2,10 +2,7 @@ from trezor.enums import CardanoAddressType -from ...common.paths import address_n_to_str -from . import bech32 -from .paths import CHAIN_STAKING_KEY, SCHEMA_PAYMENT, SCHEMA_STAKING -from .utils import to_account_path +from .paths import SCHEMA_PAYMENT if TYPE_CHECKING: from trezor import messages @@ -56,7 +53,9 @@ def __init__( def payment_credential( cls, address_params: messages.CardanoAddressParametersType ) -> "Credential": - address_type = address_params.address_type + address_type = address_params.address_type # local_cache_attribute + CAT = CardanoAddressType # local_cache_global + credential = cls( type_name=CREDENTIAL_TYPE_PAYMENT, address_type=address_type, @@ -67,26 +66,26 @@ def payment_credential( ) if address_type in ( - CardanoAddressType.BASE, - CardanoAddressType.BASE_KEY_SCRIPT, - CardanoAddressType.POINTER, - CardanoAddressType.ENTERPRISE, - CardanoAddressType.BYRON, + CAT.BASE, + CAT.BASE_KEY_SCRIPT, + CAT.POINTER, + CAT.ENTERPRISE, + CAT.BYRON, ): if not SCHEMA_PAYMENT.match(address_params.address_n): credential.is_unusual_path = True elif address_type in ( - CardanoAddressType.BASE_SCRIPT_KEY, - CardanoAddressType.BASE_SCRIPT_SCRIPT, - CardanoAddressType.POINTER_SCRIPT, - CardanoAddressType.ENTERPRISE_SCRIPT, + CAT.BASE_SCRIPT_KEY, + CAT.BASE_SCRIPT_SCRIPT, + CAT.POINTER_SCRIPT, + CAT.ENTERPRISE_SCRIPT, ): credential.is_other_warning = True elif address_type in ( - CardanoAddressType.REWARD, - CardanoAddressType.REWARD_SCRIPT, + CAT.REWARD, + CAT.REWARD_SCRIPT, ): credential.is_reward = True @@ -99,55 +98,58 @@ def payment_credential( def stake_credential( cls, address_params: messages.CardanoAddressParametersType ) -> "Credential": - address_type = address_params.address_type + from .paths import SCHEMA_STAKING + + address_n_staking = address_params.address_n_staking # local_cache_attribute + address_type = address_params.address_type # local_cache_attribute + CAT = CardanoAddressType # local_cache_global + credential = cls( type_name=CREDENTIAL_TYPE_STAKE, address_type=address_type, - path=address_params.address_n_staking, + path=address_n_staking, key_hash=address_params.staking_key_hash, script_hash=address_params.script_staking_hash, pointer=address_params.certificate_pointer, ) - if address_type == CardanoAddressType.BASE: + if address_type == CAT.BASE: if address_params.staking_key_hash: credential.is_other_warning = True else: - if not SCHEMA_STAKING.match(address_params.address_n_staking): + if not SCHEMA_STAKING.match(address_n_staking): credential.is_unusual_path = True if not _do_base_address_credentials_match( address_params.address_n, - address_params.address_n_staking, + address_n_staking, ): credential.is_mismatch = True - elif address_type == CardanoAddressType.BASE_SCRIPT_KEY: - if address_params.address_n_staking and not SCHEMA_STAKING.match( - address_params.address_n_staking - ): + elif address_type == CAT.BASE_SCRIPT_KEY: + if address_n_staking and not SCHEMA_STAKING.match(address_n_staking): credential.is_unusual_path = True elif address_type in ( - CardanoAddressType.POINTER, - CardanoAddressType.POINTER_SCRIPT, + CAT.POINTER, + CAT.POINTER_SCRIPT, ): credential.is_other_warning = True - elif address_type == CardanoAddressType.REWARD: - if not SCHEMA_STAKING.match(address_params.address_n_staking): + elif address_type == CAT.REWARD: + if not SCHEMA_STAKING.match(address_n_staking): credential.is_unusual_path = True elif address_type in ( - CardanoAddressType.BASE_KEY_SCRIPT, - CardanoAddressType.BASE_SCRIPT_SCRIPT, - CardanoAddressType.REWARD_SCRIPT, + CAT.BASE_KEY_SCRIPT, + CAT.BASE_SCRIPT_SCRIPT, + CAT.REWARD_SCRIPT, ): credential.is_other_warning = True elif address_type in ( - CardanoAddressType.ENTERPRISE, - CardanoAddressType.ENTERPRISE_SCRIPT, - CardanoAddressType.BYRON, + CAT.ENTERPRISE, + CAT.ENTERPRISE_SCRIPT, + CAT.BYRON, ): credential.is_no_staking = True @@ -156,17 +158,6 @@ def stake_credential( return credential - def should_warn(self) -> bool: - return any( - ( - self.is_reward, - self.is_no_staking, - self.is_mismatch, - self.is_unusual_path, - self.is_other_warning, - ) - ) - def is_set(self) -> bool: return any((self.path, self.key_hash, self.script_hash, self.pointer)) @@ -183,6 +174,11 @@ def get_title(self) -> str: return "" def format(self) -> list[PropertyType]: + from ...common.paths import address_n_to_str + from . import bech32 + + pointer = self.pointer # local_cache_attribute + if self.path: return [(None, address_n_to_str(self.path))] elif self.key_hash: @@ -194,11 +190,11 @@ def format(self) -> list[PropertyType]: return [(None, bech32.encode(hrp, self.key_hash))] elif self.script_hash: return [(None, bech32.encode(bech32.HRP_SCRIPT_HASH, self.script_hash))] - elif self.pointer: + elif pointer: return [ - (f"Block: {self.pointer.block_index}", None), - (f"Transaction: {self.pointer.tx_index}", None), - (f"Certificate: {self.pointer.certificate_index}", None), + (f"Block: {pointer.block_index}", None), + (f"Transaction: {pointer.tx_index}", None), + (f"Certificate: {pointer.certificate_index}", None), ] else: return [] @@ -221,8 +217,10 @@ def _do_base_address_credentials_match( address_n: list[int], address_n_staking: list[int], ) -> bool: - return address_n_staking == _path_to_staking_path(address_n) - + from .paths import CHAIN_STAKING_KEY + from .utils import to_account_path -def _path_to_staking_path(path: list[int]) -> list[int]: - return to_account_path(path) + [CHAIN_STAKING_KEY, 0] + # Note: This checks that the account matches and the staking path address_index is 0. + # (Even though other values are allowed, we want to display them to the user.) + path_to_staking_path = to_account_path(address_n) + [CHAIN_STAKING_KEY, 0] + return address_n_staking == path_to_staking_path diff --git a/core/src/apps/cardano/helpers/hash_builder_collection.py b/core/src/apps/cardano/helpers/hash_builder_collection.py index c846ed488a..22b6d238e1 100644 --- a/core/src/apps/cardano/helpers/hash_builder_collection.py +++ b/core/src/apps/cardano/helpers/hash_builder_collection.py @@ -4,6 +4,7 @@ if TYPE_CHECKING: from typing import Any, Generic, TypeVar + from trezor import wire from trezor.utils import HashContext @@ -22,7 +23,7 @@ def __init__(self, size: int) -> None: self.size = size self.remaining = size self.hash_fn: HashContext | None = None - self.parent: "HashBuilderCollection" | None = None + self.parent: "HashBuilderCollection | None" = None self.has_unfinished_child = False def start(self, hash_fn: HashContext) -> "HashBuilderCollection": @@ -89,6 +90,19 @@ def _header_bytes(self) -> bytes: return cbor.create_array_header(self.size) +class HashBuilderSet(HashBuilderList, Generic[T]): + def __init__(self, size: int, *, tagged: bool) -> None: + super().__init__(size) + self.tagged = tagged + + def _header_bytes(self) -> bytes: + return ( + cbor.create_tagged_set_header(self.size) + if self.tagged + else cbor.create_array_header(self.size) + ) + + class HashBuilderDict(HashBuilderCollection, Generic[K, V]): key_order_error: wire.ProcessError previous_encoded_key: bytes diff --git a/core/src/apps/cardano/helpers/paths.py b/core/src/apps/cardano/helpers/paths.py index a895c690fe..2725cb1f93 100644 --- a/core/src/apps/cardano/helpers/paths.py +++ b/core/src/apps/cardano/helpers/paths.py @@ -1,6 +1,6 @@ from micropython import const -from apps.common.paths import HARDENED, PathSchema +from apps.common.paths import HARDENED, PathSchema, unharden # noqa: F401 _SLIP44_ID = const(1815) @@ -14,21 +14,14 @@ # minting has a specific schema for key derivation - see CIP-1855 SCHEMA_MINT = PathSchema.parse(f"m/1855'/coin_type'/[0-{HARDENED - 1}]'", _SLIP44_ID) SCHEMA_PAYMENT = PathSchema.parse("m/[44,1852]'/coin_type'/account'/[0,1]/address_index", _SLIP44_ID) -# staking is only allowed on Shelley paths with suffix /2/0 -SCHEMA_STAKING = PathSchema.parse("m/1852'/coin_type'/account'/2/0", _SLIP44_ID) -SCHEMA_STAKING_ANY_ACCOUNT = PathSchema.parse(f"m/1852'/coin_type'/[0-{HARDENED - 1}]'/2/0", _SLIP44_ID) + +SCHEMA_STAKING = PathSchema.parse("m/1852'/coin_type'/account'/2/address_index", _SLIP44_ID) +SCHEMA_STAKING_ANY_ACCOUNT = PathSchema.parse(f"m/1852'/coin_type'/[0-{HARDENED - 1}]'/2/address_index", _SLIP44_ID) # fmt: on ACCOUNT_PATH_INDEX = const(2) ACCOUNT_PATH_LENGTH = const(3) CHAIN_STAKING_KEY = const(2) -CHANGE_OUTPUT_PATH_NAME = "Change output path" -CHANGE_OUTPUT_STAKING_PATH_NAME = "Change output staking path" -CERTIFICATE_PATH_NAME = "Certificate path" -POOL_OWNER_STAKING_PATH_NAME = "Pool owner staking path" -WITNESS_PATH_NAME = "Witness path" - - -def unharden(item: int) -> int: - return item ^ (item & HARDENED) +ADDRESS_INDEX_PATH_INDEX = const(4) +RECOMMENDED_ADDRESS_INDEX = const(0) # https://cips.cardano.org/cips/cip11/ diff --git a/core/src/apps/cardano/helpers/protocol_magics.py b/core/src/apps/cardano/helpers/protocol_magics.py index e0a30fc9bc..513c73b986 100644 --- a/core/src/apps/cardano/helpers/protocol_magics.py +++ b/core/src/apps/cardano/helpers/protocol_magics.py @@ -1,8 +1,10 @@ +from micropython import const + # https://book.world.dev.cardano.org/environments.html -MAINNET = 764824073 -TESTNET_PREPROD = 1 -TESTNET_PREVIEW = 2 -TESTNET_LEGACY = 1097911063 +MAINNET = const(764824073) +TESTNET_PREPROD = const(1) +TESTNET_PREVIEW = const(2) +TESTNET_LEGACY = const(1097911063) NAMES = { MAINNET: "Mainnet", diff --git a/core/src/apps/cardano/helpers/utils.py b/core/src/apps/cardano/helpers/utils.py index c2f51d4686..69229ecf3e 100644 --- a/core/src/apps/cardano/helpers/utils.py +++ b/core/src/apps/cardano/helpers/utils.py @@ -1,21 +1,13 @@ from typing import TYPE_CHECKING -from trezor import wire from trezor.crypto import hashlib -from apps.common.seed import remove_ed25519_prefix - -from . import ( - ADDRESS_KEY_HASH_SIZE, - SCRIPT_HASH_SIZE, - bech32, - network_ids, - protocol_magics, -) -from .paths import ACCOUNT_PATH_INDEX, SCHEMA_STAKING_ANY_ACCOUNT, unharden +from . import ADDRESS_KEY_HASH_SIZE, bech32 +from .paths import ACCOUNT_PATH_INDEX if TYPE_CHECKING: from .. import seed + from trezor import wire def variable_length_encode(number: int) -> bytes: @@ -40,6 +32,8 @@ def to_account_path(path: list[int]) -> list[int]: def format_account_number(path: list[int]) -> str: + from .paths import unharden + if len(path) <= ACCOUNT_PATH_INDEX: raise ValueError("Path is too short.") @@ -76,6 +70,8 @@ def get_public_key_hash(keychain: seed.Keychain, path: list[int]) -> bytes: def derive_public_key( keychain: seed.Keychain, path: list[int], extended: bool = False ) -> bytes: + from apps.common.seed import remove_ed25519_prefix + node = keychain.derive(path) public_key = remove_ed25519_prefix(node.public_key()) return public_key if not extended else public_key + node.chain_code() @@ -87,6 +83,9 @@ def validate_stake_credential( key_hash: bytes | None, error: wire.ProcessError, ) -> None: + from . import SCRIPT_HASH_SIZE + from .paths import SCHEMA_STAKING_ANY_ACCOUNT + if sum(bool(k) for k in (path, script_hash, key_hash)) != 1: raise error @@ -104,6 +103,10 @@ def validate_network_info(network_id: int, protocol_magic: int) -> None: belong to the mainnet or that both belong to a testnet. We don't need to check for consistency between various testnets (at least for now). """ + from trezor import wire + + from . import network_ids, protocol_magics + is_mainnet_network_id = network_ids.is_mainnet(network_id) is_mainnet_protocol_magic = protocol_magics.is_mainnet(protocol_magic) diff --git a/core/src/apps/cardano/layout.py b/core/src/apps/cardano/layout.py index 5c9eb12b19..50d0c61f2c 100644 --- a/core/src/apps/cardano/layout.py +++ b/core/src/apps/cardano/layout.py @@ -1,28 +1,22 @@ -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING -from trezor import messages, ui +from trezor import messages from trezor.enums import ( ButtonRequestType, CardanoAddressType, CardanoCertificateType, + CardanoDRepType, + CardanoNativeScriptHashDisplayFormat, CardanoNativeScriptType, ) from trezor.lvglui.i18n import gettext as _, keys as i18n_keys from trezor.strings import format_amount -from trezor.ui.layouts import ( - confirm_blob, - confirm_metadata, - confirm_output, - confirm_path_warning, - confirm_properties, - confirm_text, - should_show_more, - show_address, -) +from trezor.ui import layouts +from trezor.ui.layouts import confirm_metadata, confirm_output, confirm_properties from apps.common.paths import address_n_to_str -from . import addresses, seed +from . import addresses from .helpers import bech32, protocol_magics from .helpers.utils import ( format_account_number, @@ -32,15 +26,13 @@ ) if TYPE_CHECKING: - from trezor import wire + from typing import Literal - from trezor.wire import Context - from trezor.enums import CardanoNativeScriptHashDisplayFormat from trezor.ui.layouts.lvgl import PropertyType from .helpers.credential import Credential from .seed import Keychain - + from trezor import wire ADDRESS_TYPE_NAMES = { CardanoAddressType.BYRON: "Legacy", @@ -67,15 +59,20 @@ CERTIFICATE_TYPE_NAMES = { CardanoCertificateType.STAKE_REGISTRATION: "Stake key registration", + CardanoCertificateType.STAKE_REGISTRATION_CONWAY: "Stake key registration", CardanoCertificateType.STAKE_DEREGISTRATION: "Stake key deregistration", + CardanoCertificateType.STAKE_DEREGISTRATION_CONWAY: "Stake key deregistration", CardanoCertificateType.STAKE_DELEGATION: "Stake delegation", CardanoCertificateType.STAKE_POOL_REGISTRATION: "Stakepool registration", + CardanoCertificateType.VOTE_DELEGATION: "Vote delegation", } TITLE = _(i18n_keys.TITLE__CONFIRM_TRANSACTION) BRT_Other = ButtonRequestType.Other # global_import_cache +CVOTE_REWARD_ELIGIBILITY_WARNING = "Reward eligibility warning" + def format_coin_amount(amount: int, network_id: int) -> str: from .helpers import network_ids @@ -158,11 +155,10 @@ async def show_native_script( async def show_script_hash( - ctx: Context, + ctx: wire.Context, script_hash: bytes, display_format: CardanoNativeScriptHashDisplayFormat, ) -> None: - from trezor.enums import CardanoNativeScriptHashDisplayFormat assert display_format in ( CardanoNativeScriptHashDisplayFormat.BECH32, @@ -178,7 +174,7 @@ async def show_script_hash( br_code=BRT_Other, ) elif display_format == CardanoNativeScriptHashDisplayFormat.POLICY_ID: - await confirm_blob( + await layouts.confirm_blob( ctx, "verify_script", "Verify script", @@ -188,22 +184,23 @@ async def show_script_hash( ) -async def show_tx_init(ctx: Context, title: str) -> bool: - should_show_details = await should_show_more( - ctx, - TITLE, - ( - ( - ui.BOLD, - title, - ), - (ui.NORMAL, "Choose level of details:"), - ), - "Show All", - br_type="Show Simple", - ) +async def show_tx_init(ctx: wire.Context, title: str) -> bool: + # should_show_details = await should_show_more( + # ctx, + # TITLE, + # ( + # ( + # ui.BOLD, + # title, + # ), + # (ui.NORMAL, "Choose level of details:"), + # ), + # "Show All", + # br_type="Show Simple", + # ) - return should_show_details + # return should_show_details + return True async def confirm_input(ctx: wire.Context, input: messages.CardanoTxInput) -> None: @@ -220,11 +217,12 @@ async def confirm_input(ctx: wire.Context, input: messages.CardanoTxInput) -> No async def confirm_sending( - ctx: Context, + ctx: wire.Context, ada_amount: int, to: str, output_type: Literal["address", "change", "collateral-return"], network_id: int, + chunkify: bool = False, ) -> None: if output_type not in ("address", "change", "collateral-return"): raise RuntimeError # should be unreachable @@ -262,7 +260,7 @@ async def confirm_sending_token( ) -async def confirm_datum_hash(ctx: Context, datum_hash: bytes) -> None: +async def confirm_datum_hash(ctx: wire.Context, datum_hash: bytes) -> None: await confirm_properties( ctx, "confirm_datum_hash", @@ -330,41 +328,66 @@ async def show_credentials( stake_credential: Credential, ) -> None: intro_text = "Address" - await _show_credential(ctx, payment_credential, intro_text, is_output=False) - await _show_credential(ctx, stake_credential, intro_text, is_output=False) + await _show_credential(ctx, payment_credential, intro_text, purpose="address") + await _show_credential(ctx, stake_credential, intro_text, purpose="address") async def show_change_output_credentials( - ctx: Context, + ctx: wire.Context, payment_credential: Credential, stake_credential: Credential, ) -> None: intro_text = "The following address is a change address. Its" - await _show_credential(ctx, payment_credential, intro_text, is_output=True) - await _show_credential(ctx, stake_credential, intro_text, is_output=True) + await _show_credential(ctx, payment_credential, intro_text, purpose="output") + await _show_credential(ctx, stake_credential, intro_text, purpose="output") async def show_device_owned_output_credentials( - ctx: Context, + ctx: wire.Context, payment_credential: Credential, stake_credential: Credential, show_both_credentials: bool, ) -> None: intro_text = "The following address is owned by this device. Its" - await _show_credential(ctx, payment_credential, intro_text, is_output=True) + await _show_credential(ctx, payment_credential, intro_text, purpose="output") if show_both_credentials: - await _show_credential(ctx, stake_credential, intro_text, is_output=True) + await _show_credential(ctx, stake_credential, intro_text, purpose="output") + + +async def show_cvote_registration_payment_credentials( + ctx: wire.Context, + payment_credential: Credential, + stake_credential: Credential, + show_both_credentials: bool, + show_payment_warning: bool, +) -> None: + intro_text = "Registration Payment" + await _show_credential( + ctx, payment_credential, intro_text, purpose="cvote_reg_payment_address" + ) + if show_both_credentials or show_payment_warning: + extra_text = CVOTE_REWARD_ELIGIBILITY_WARNING if show_payment_warning else None + await _show_credential( + ctx, + stake_credential, + intro_text, + purpose="cvote_reg_payment_address", + extra_text=extra_text, + ) async def _show_credential( - ctx: Context, + ctx: wire.Context, credential: Credential, intro_text: str, - is_output: bool, + purpose: Literal["address", "output", "cvote_reg_payment_address"], + extra_text: str | None = None, ) -> None: - title = ( - TITLE if is_output else f"{ADDRESS_TYPE_NAMES[credential.address_type]} address" - ) + title = { + "address": f"{ADDRESS_TYPE_NAMES[credential.address_type]} address", + "output": TITLE, + "cvote_reg_payment_address": TITLE, + }[purpose] props: list[PropertyType] = [] append = props.append # local_cache_attribute @@ -374,6 +397,7 @@ async def _show_credential( # show some of the "props". if credential.is_set(): credential_title = credential.get_title() + # TODO: handle translation append( ( f"{intro_text} {credential.type_name} credential is a {credential_title}:", @@ -386,7 +410,8 @@ async def _show_credential( append((None, "Path is unusual.")) if credential.is_mismatch: append((None, "Credential doesn't match payment credential.")) - if credential.is_reward: + if credential.is_reward and purpose != "cvote_reg_payment_address": + # for cvote registrations, this is handled by extra_text at the end append(("Address is a reward address.", None)) if credential.is_no_staking: append( @@ -396,21 +421,25 @@ async def _show_credential( ) ) - await confirm_properties( - ctx, - "confirm_credential", - title, - props, - br_code=BRT_Other, - ) + if extra_text: + append((extra_text, None)) + + if len(props) > 0: + await confirm_properties( + ctx, + "confirm_credential", + title, + props, + br_code=BRT_Other, + ) -async def warn_path(ctx: Context, path: list[int], title: str) -> None: - await confirm_path_warning(ctx, address_n_to_str(path), path_type=title) +async def warn_path(ctx: wire.Context, path: list[int], title: str) -> None: + await layouts.confirm_path_warning(ctx, address_n_to_str(path), path_type=title) async def warn_tx_output_contains_tokens( - ctx: Context, is_collateral_return: bool = False + ctx: wire.Context, is_collateral_return: bool = False ) -> None: content = ( "The collateral return\noutput contains tokens." @@ -426,7 +455,7 @@ async def warn_tx_output_contains_tokens( ) -async def warn_tx_contains_mint(ctx: Context) -> None: +async def warn_tx_contains_mint(ctx: wire.Context) -> None: await confirm_metadata( ctx, "confirm_tokens", @@ -436,7 +465,7 @@ async def warn_tx_contains_mint(ctx: Context) -> None: ) -async def warn_tx_output_no_datum(ctx: Context) -> None: +async def warn_tx_output_no_datum(ctx: wire.Context) -> None: await confirm_metadata( ctx, "confirm_no_datum_hash", @@ -446,7 +475,7 @@ async def warn_tx_output_no_datum(ctx: Context) -> None: ) -async def warn_no_script_data_hash(ctx: Context) -> None: +async def warn_no_script_data_hash(ctx: wire.Context) -> None: await confirm_metadata( ctx, "confirm_no_script_data_hash", @@ -477,9 +506,11 @@ async def warn_unknown_total_collateral(ctx: wire.Context) -> None: async def confirm_witness_request( - ctx: Context, + ctx: wire.Context, witness_path: list[int], ) -> None: + from . import seed + if seed.is_multisig_path(witness_path): path_title = "multi-sig path" elif seed.is_minting_path(witness_path): @@ -487,7 +518,7 @@ async def confirm_witness_request( else: path_title = "path" - await confirm_text( + await layouts.confirm_text( ctx, "confirm_total", TITLE, @@ -539,7 +570,9 @@ async def confirm_tx( async def confirm_certificate( - ctx: wire.Context, certificate: messages.CardanoTxCertificate + ctx: wire.Context, + certificate: messages.CardanoTxCertificate, + network_id: int, ) -> None: # stake pool registration requires custom confirmation logic not covered # in this call @@ -552,7 +585,7 @@ async def confirm_certificate( elif certificate.type == CardanoCertificateType.STAKE_DELEGATION: transaction_type_value = _(i18n_keys.LIST_VALUE__STAKE_DELEGATION) else: - transaction_type_value = _(i18n_keys.LIST_VALUE__STAKEPOOL_REGISTRATION) + transaction_type_value = CERTIFICATE_TYPE_NAMES[certificate.type] props: list[PropertyType] = [ (_(i18n_keys.LIST_KEY__TRANSACTION_TYPE__COLON), transaction_type_value), @@ -569,6 +602,21 @@ async def confirm_certificate( format_stake_pool_id(certificate.pool), ) ) + elif certificate.type in ( + CardanoCertificateType.STAKE_REGISTRATION_CONWAY, + CardanoCertificateType.STAKE_DEREGISTRATION_CONWAY, + ): + assert certificate.deposit is not None # validate_certificate + props.append( + ( + _(i18n_keys.LIST_VALUE__DEPOSIT) + ":", + format_coin_amount(certificate.deposit, network_id), + ) + ) + + elif certificate.type == CardanoCertificateType.VOTE_DELEGATION: + assert certificate.drep is not None # validate_certificate + props.append(_format_drep(certificate.drep)) await confirm_properties( ctx, @@ -687,7 +735,7 @@ async def confirm_stake_pool_metadata( async def confirm_stake_pool_registration_final( - ctx: Context, + ctx: wire.Context, protocol_magic: int, ttl: int | None, validity_interval_start: int | None, @@ -726,7 +774,12 @@ async def confirm_withdrawal( ) ) - props.append(("Amount:", format_coin_amount(withdrawal.amount, network_id))) + props.append( + ( + _(i18n_keys.LIST_KEY__AMOUNT__COLON), + format_coin_amount(withdrawal.amount, network_id), + ) + ) await confirm_properties( ctx, @@ -740,12 +793,19 @@ async def confirm_withdrawal( def _format_stake_credential( path: list[int], script_hash: bytes | None, key_hash: bytes | None ) -> tuple[str, str]: - from .helpers.utils import to_account_path + from .helpers.paths import ADDRESS_INDEX_PATH_INDEX, RECOMMENDED_ADDRESS_INDEX if path: + account_number = format_account_number(path) + address_index = path[ADDRESS_INDEX_PATH_INDEX] + if address_index == RECOMMENDED_ADDRESS_INDEX: + return ( + f"for account {account_number}:", + address_n_to_str(path), + ) return ( - _(i18n_keys.LIST_KEY__ACCOUNT__COLON), - address_n_to_str(to_account_path(path)), + f"for account {account_number} and index {address_index}:", + address_n_to_str(path), ) elif key_hash: return ("for key hash:", bech32.encode(bech32.HRP_STAKE_KEY_HASH, key_hash)) @@ -756,13 +816,35 @@ def _format_stake_credential( raise ValueError -async def confirm_governance_registration_delegation( +def _format_drep(drep: messages.CardanoDRep) -> tuple[str, str]: + if drep.type == CardanoDRepType.KEY_HASH: + assert drep.key_hash is not None # validate_drep + return ( + "Delegating to key hash:", + bech32.encode(bech32.HRP_DREP_KEY_HASH, drep.key_hash), + ) + elif drep.type == CardanoDRepType.SCRIPT_HASH: + assert drep.script_hash is not None # validate_drep + return ( + "Delegating to script:", + bech32.encode(bech32.HRP_DREP_SCRIPT_HASH, drep.script_hash), + ) + elif drep.type == CardanoDRepType.ABSTAIN: + return ("Delegating to:", "Always Abstain") + elif drep.type == CardanoDRepType.NO_CONFIDENCE: + return ("Delegating to:", "Always No Confidence") + else: + # should be unreachable unless there's a bug in validation + raise ValueError + + +async def confirm_cvote_registration_delegation( ctx: wire.Context, public_key: str, weight: int, ) -> None: props: list[PropertyType] = [ - ("Governance voting key registration", None), + ("Vote key registration (CIP-36)", None), ("Delegating to:", public_key), ] if weight is not None: @@ -770,31 +852,49 @@ async def confirm_governance_registration_delegation( await confirm_properties( ctx, - "confirm_governance_registration_delegation", + "confirm_cvote_registration_delegation", + title=TITLE, + props=props, + br_code=ButtonRequestType.Other, + ) + + +async def confirm_cvote_registration_payment_address( + ctx: wire.Context, + payment_address: str, + should_show_payment_warning: bool, +) -> None: + props = [ + ("Vote key registration (CIP-36)", None), + ("Rewards go to:", payment_address), + ] + if should_show_payment_warning: + props.append((CVOTE_REWARD_ELIGIBILITY_WARNING, None)) + await confirm_properties( + ctx, + "confirm_cvote_registration_payment_address", title=TITLE, props=props, br_code=ButtonRequestType.Other, ) -async def confirm_governance_registration( +async def confirm_cvote_registration( ctx: wire.Context, - public_key: str | None, + vote_public_key: str | None, staking_path: list[int], - reward_address: str, nonce: int, voting_purpose: int | None, ) -> None: - props: list[PropertyType] = [("Governance voting key registration", None)] - if public_key is not None: - props.append(("Voting public key:", public_key)) + props: list[PropertyType] = [("Vote key registration (CIP-36)", None)] + if vote_public_key is not None: + props.append(("Vote public key:", vote_public_key)) props.extend( [ ( - f"Staking key for account {format_account_number(staking_path)}:", + f"Staking key for account: {format_account_number(staking_path)}:", address_n_to_str(staking_path), ), - ("Rewards go to:", reward_address), ("Nonce:", str(nonce)), ] ) @@ -802,20 +902,22 @@ async def confirm_governance_registration( props.append( ( "Voting purpose:", - "Catalyst" if voting_purpose == 0 else f"{voting_purpose} (other)", + ("Catalyst" if voting_purpose == 0 else f"{voting_purpose} (Other)"), ) ) await confirm_properties( ctx, - "confirm_governance_registration", + "confirm_cvote_registration", title=TITLE, props=props, br_code=ButtonRequestType.Other, ) -async def show_auxiliary_data_hash(ctx: Context, auxiliary_data_hash: bytes) -> None: +async def show_auxiliary_data_hash( + ctx: wire.Context, auxiliary_data_hash: bytes +) -> None: await confirm_properties( ctx, "confirm_auxiliary_data", @@ -842,7 +944,9 @@ async def confirm_token_minting( ), ), ( - "Amount minted:" if token.mint_amount >= 0 else "Amount burned:", + "Amount minted (decimals unknown):" + if token.mint_amount >= 0 + else "Amount burned (decimals unknown):", format_amount(token.mint_amount, 0), ), ), @@ -860,7 +964,7 @@ async def warn_tx_network_unverifiable(ctx: wire.Context) -> None: ) -async def confirm_script_data_hash(ctx: Context, script_data_hash: bytes) -> None: +async def confirm_script_data_hash(ctx: wire.Context, script_data_hash: bytes) -> None: await confirm_properties( ctx, "confirm_script_data_hash", @@ -931,6 +1035,7 @@ async def show_cardano_address( address_parameters: messages.CardanoAddressParametersType, address: str, protocol_magic: int, + chunkify: bool, ) -> None: CAT = CardanoAddressType # local_cache_global @@ -939,8 +1044,7 @@ async def show_cardano_address( network_name = protocol_magics.to_ui_string(protocol_magic) title = f"{ADDRESS_TYPE_NAMES[address_parameters.address_type]} address" - address_extra = None - title_qr = title + path = None if address_parameters.address_type in ( CAT.BYRON, CAT.BASE, @@ -950,17 +1054,14 @@ async def show_cardano_address( CAT.REWARD, ): if address_parameters.address_n: - address_extra = address_n_to_str(address_parameters.address_n) - title_qr = address_n_to_str(address_parameters.address_n) + path = address_n_to_str(address_parameters.address_n) elif address_parameters.address_n_staking: - address_extra = address_n_to_str(address_parameters.address_n_staking) - title_qr = address_n_to_str(address_parameters.address_n_staking) + path = address_n_to_str(address_parameters.address_n_staking) - await show_address( + await layouts.show_address( ctx, address, network=network_name, - address_n=address_extra, - title_qr=title_qr, + address_n=path, addr_type=title, ) diff --git a/core/src/apps/cardano/native_script.py b/core/src/apps/cardano/native_script.py index ad4d39bf2b..815601716b 100644 --- a/core/src/apps/cardano/native_script.py +++ b/core/src/apps/cardano/native_script.py @@ -1,105 +1,111 @@ from typing import TYPE_CHECKING -from trezor import messages, wire -from trezor.crypto import hashlib from trezor.enums import CardanoNativeScriptType - -from apps.common import cbor +from trezor.wire import ProcessError from . import seed -from .helpers import ADDRESS_KEY_HASH_SIZE, SCRIPT_HASH_SIZE -from .helpers.paths import SCHEMA_MINT -from .helpers.utils import get_public_key_hash if TYPE_CHECKING: from typing import Any + from trezor import messages + from apps.common.cbor import CborSequence def validate_native_script(script: messages.CardanoNativeScript | None) -> None: - INVALID_NATIVE_SCRIPT = wire.ProcessError("Invalid native script") + from .helpers import ADDRESS_KEY_HASH_SIZE + from .helpers.paths import SCHEMA_MINT + + INVALID_NATIVE_SCRIPT = ProcessError("Invalid native script") if not script: raise INVALID_NATIVE_SCRIPT _validate_native_script_structure(script) + script_type = script.type # local_cache_attribute + key_path = script.key_path # local_cache_attribute + scripts = script.scripts # local_cache_attribute + CNST = CardanoNativeScriptType # local_cache_global - if script.type == CardanoNativeScriptType.PUB_KEY: - if script.key_hash and script.key_path: + if script_type == CNST.PUB_KEY: + if script.key_hash and key_path: raise INVALID_NATIVE_SCRIPT if script.key_hash: if len(script.key_hash) != ADDRESS_KEY_HASH_SIZE: raise INVALID_NATIVE_SCRIPT - elif script.key_path: - is_minting = SCHEMA_MINT.match(script.key_path) - if not seed.is_multisig_path(script.key_path) and not is_minting: + elif key_path: + is_minting = SCHEMA_MINT.match(key_path) + if not seed.is_multisig_path(key_path) and not is_minting: raise INVALID_NATIVE_SCRIPT else: raise INVALID_NATIVE_SCRIPT - elif script.type == CardanoNativeScriptType.ALL: - for sub_script in script.scripts: + elif script_type == CNST.ALL: + for sub_script in scripts: validate_native_script(sub_script) - elif script.type == CardanoNativeScriptType.ANY: - for sub_script in script.scripts: + elif script_type == CNST.ANY: + for sub_script in scripts: validate_native_script(sub_script) - elif script.type == CardanoNativeScriptType.N_OF_K: + elif script_type == CNST.N_OF_K: if script.required_signatures_count is None: raise INVALID_NATIVE_SCRIPT - if script.required_signatures_count > len(script.scripts): + if script.required_signatures_count > len(scripts): raise INVALID_NATIVE_SCRIPT - for sub_script in script.scripts: + for sub_script in scripts: validate_native_script(sub_script) - elif script.type == CardanoNativeScriptType.INVALID_BEFORE: + elif script_type == CNST.INVALID_BEFORE: if script.invalid_before is None: raise INVALID_NATIVE_SCRIPT - elif script.type == CardanoNativeScriptType.INVALID_HEREAFTER: + elif script_type == CNST.INVALID_HEREAFTER: if script.invalid_hereafter is None: raise INVALID_NATIVE_SCRIPT def _validate_native_script_structure(script: messages.CardanoNativeScript) -> None: - key_hash = script.key_hash - key_path = script.key_path - scripts = script.scripts - required_signatures_count = script.required_signatures_count - invalid_before = script.invalid_before - invalid_hereafter = script.invalid_hereafter + key_hash = script.key_hash # local_cache_attribute + key_path = script.key_path # local_cache_attribute + scripts = script.scripts # local_cache_attribute + required_signatures_count = ( + script.required_signatures_count + ) # local_cache_attribute + invalid_before = script.invalid_before # local_cache_attribute + invalid_hereafter = script.invalid_hereafter # local_cache_attribute + CNST = CardanoNativeScriptType # local_cache_global fields_to_be_empty: dict[CardanoNativeScriptType, tuple[Any, ...]] = { - CardanoNativeScriptType.PUB_KEY: ( + CNST.PUB_KEY: ( scripts, required_signatures_count, invalid_before, invalid_hereafter, ), - CardanoNativeScriptType.ALL: ( + CNST.ALL: ( key_hash, key_path, required_signatures_count, invalid_before, invalid_hereafter, ), - CardanoNativeScriptType.ANY: ( + CNST.ANY: ( key_hash, key_path, required_signatures_count, invalid_before, invalid_hereafter, ), - CardanoNativeScriptType.N_OF_K: ( + CNST.N_OF_K: ( key_hash, key_path, invalid_before, invalid_hereafter, ), - CardanoNativeScriptType.INVALID_BEFORE: ( + CNST.INVALID_BEFORE: ( key_hash, key_path, required_signatures_count, invalid_hereafter, ), - CardanoNativeScriptType.INVALID_HEREAFTER: ( + CNST.INVALID_HEREAFTER: ( key_hash, key_path, required_signatures_count, @@ -108,12 +114,18 @@ def _validate_native_script_structure(script: messages.CardanoNativeScript) -> N } if script.type not in fields_to_be_empty or any(fields_to_be_empty[script.type]): - raise wire.ProcessError("Invalid native script") + raise ProcessError("Invalid native script") def get_native_script_hash( keychain: seed.Keychain, script: messages.CardanoNativeScript ) -> bytes: + from trezor.crypto import hashlib + + from apps.common import cbor + + from .helpers import SCRIPT_HASH_SIZE + script_cbor = cbor.encode(cborize_native_script(keychain, script)) prefixed_script_cbor = b"\00" + script_cbor return hashlib.blake2b(data=prefixed_script_cbor, outlen=SCRIPT_HASH_SIZE).digest() @@ -122,29 +134,34 @@ def get_native_script_hash( def cborize_native_script( keychain: seed.Keychain, script: messages.CardanoNativeScript ) -> CborSequence: + from .helpers.utils import get_public_key_hash + + script_type = script.type # local_cache_attribute + CNST = CardanoNativeScriptType # local_cache_global + script_content: CborSequence - if script.type == CardanoNativeScriptType.PUB_KEY: + if script_type == CNST.PUB_KEY: if script.key_hash: script_content = (script.key_hash,) elif script.key_path: script_content = (get_public_key_hash(keychain, script.key_path),) else: - raise wire.ProcessError("Invalid native script") - elif script.type == CardanoNativeScriptType.ALL: + raise ProcessError("Invalid native script") + elif script_type == CNST.ALL: script_content = ( tuple( cborize_native_script(keychain, sub_script) for sub_script in script.scripts ), ) - elif script.type == CardanoNativeScriptType.ANY: + elif script_type == CNST.ANY: script_content = ( tuple( cborize_native_script(keychain, sub_script) for sub_script in script.scripts ), ) - elif script.type == CardanoNativeScriptType.N_OF_K: + elif script_type == CNST.N_OF_K: script_content = ( script.required_signatures_count, tuple( @@ -152,11 +169,11 @@ def cborize_native_script( for sub_script in script.scripts ), ) - elif script.type == CardanoNativeScriptType.INVALID_BEFORE: + elif script_type == CNST.INVALID_BEFORE: script_content = (script.invalid_before,) - elif script.type == CardanoNativeScriptType.INVALID_HEREAFTER: + elif script_type == CNST.INVALID_HEREAFTER: script_content = (script.invalid_hereafter,) else: raise RuntimeError # should be unreachable - return (script.type,) + script_content + return (script_type,) + script_content diff --git a/core/src/apps/cardano/seed.py b/core/src/apps/cardano/seed.py index d4bdf563f3..21c20d6631 100644 --- a/core/src/apps/cardano/seed.py +++ b/core/src/apps/cardano/seed.py @@ -8,16 +8,16 @@ from apps.common import mnemonic from apps.common.seed import derive_and_store_roots, get_seed -from .helpers import paths +from .helpers.paths import BYRON_ROOT, MINTING_ROOT, MULTISIG_ROOT, SHELLEY_ROOT if TYPE_CHECKING: - from typing import Callable, Awaitable, TypeVar - - from apps.common.paths import Bip32Path - from apps.common.keychain import MsgOut, Handler + from typing import Awaitable, Callable, TypeVar from trezor import messages + from apps.common.keychain import Handler, MsgOut + from apps.common.paths import Bip32Path + CardanoMessages = ( messages.CardanoGetAddress | messages.CardanoGetPublicKey @@ -37,10 +37,10 @@ class Keychain: """ def __init__(self, root: bip32.HDNode) -> None: - self.byron_root = self._derive_path(root, paths.BYRON_ROOT) - self.shelley_root = self._derive_path(root, paths.SHELLEY_ROOT) - self.multisig_root = self._derive_path(root, paths.MULTISIG_ROOT) - self.minting_root = self._derive_path(root, paths.MINTING_ROOT) + self.byron_root = self._derive_path(root, BYRON_ROOT) + self.shelley_root = self._derive_path(root, SHELLEY_ROOT) + self.multisig_root = self._derive_path(root, MULTISIG_ROOT) + self.minting_root = self._derive_path(root, MINTING_ROOT) root.__del__() @staticmethod @@ -50,7 +50,7 @@ def _derive_path(root: bip32.HDNode, path: Bip32Path) -> bip32.HDNode: node.derive_path(path) return node - def verify_path(self, path: Bip32Path) -> None: + def verify_path(self, path: Bip32Path, _force_strict: bool = True) -> None: if not self.is_in_keychain(path): raise wire.DataError("Forbidden key path") @@ -80,11 +80,11 @@ def derive(self, node_path: Bip32Path) -> bip32.HDNode: # this is true now, so for simplicity we don't branch on path type assert ( - len(paths.BYRON_ROOT) == len(paths.SHELLEY_ROOT) - and len(paths.MULTISIG_ROOT) == len(paths.SHELLEY_ROOT) - and len(paths.MINTING_ROOT) == len(paths.SHELLEY_ROOT) + len(BYRON_ROOT) == len(SHELLEY_ROOT) + and len(MULTISIG_ROOT) == len(SHELLEY_ROOT) + and len(MINTING_ROOT) == len(SHELLEY_ROOT) ) - suffix = node_path[len(paths.SHELLEY_ROOT) :] + suffix = node_path[len(SHELLEY_ROOT) :] # derive child node from the root return self._derive_path(path_root, suffix) @@ -95,19 +95,19 @@ def derive(self, node_path: Bip32Path) -> bip32.HDNode: def is_byron_path(path: Bip32Path) -> bool: - return path[: len(paths.BYRON_ROOT)] == paths.BYRON_ROOT + return path[: len(BYRON_ROOT)] == BYRON_ROOT def is_shelley_path(path: Bip32Path) -> bool: - return path[: len(paths.SHELLEY_ROOT)] == paths.SHELLEY_ROOT + return path[: len(SHELLEY_ROOT)] == SHELLEY_ROOT def is_multisig_path(path: Bip32Path) -> bool: - return path[: len(paths.MULTISIG_ROOT)] == paths.MULTISIG_ROOT + return path[: len(MULTISIG_ROOT)] == MULTISIG_ROOT def is_minting_path(path: Bip32Path) -> bool: - return path[: len(paths.MINTING_ROOT)] == paths.MINTING_ROOT + return path[: len(MINTING_ROOT)] == MINTING_ROOT def derive_and_store_secrets(passphrase: str) -> None: @@ -168,7 +168,7 @@ async def _get_keychain_bip39( return Keychain(root) -async def get_keychain( +async def _get_keychain( ctx: wire.Context, derivation_type: CardanoDerivationType ) -> Keychain: if mnemonic.is_bip39(): @@ -181,7 +181,7 @@ async def get_keychain( def with_keychain(func: HandlerWithKeychain[MsgIn, MsgOut]) -> Handler[MsgIn, MsgOut]: async def wrapper(ctx: wire.Context, msg: MsgIn) -> MsgOut: - keychain = await get_keychain(ctx, msg.derivation_type) + keychain = await _get_keychain(ctx, msg.derivation_type) return await func(ctx, msg, keychain) return wrapper diff --git a/core/src/apps/cardano/sign_message.py b/core/src/apps/cardano/sign_message.py index a8328b84b7..9248883126 100644 --- a/core/src/apps/cardano/sign_message.py +++ b/core/src/apps/cardano/sign_message.py @@ -3,6 +3,7 @@ from trezor.ui.layouts import confirm_signverify from apps.common import cbor +from apps.common.seed import remove_ed25519_prefix from apps.common.signverify import decode_message from . import seed @@ -35,8 +36,9 @@ async def sign_message( ctx, keychain, msg.address_n, + True, # path must match the PUBKEY schema - SCHEMA_PUBKEY.match(msg.address_n) or SCHEMA_MINT.match(msg.address_n), + (SCHEMA_PUBKEY.match(msg.address_n) or SCHEMA_MINT.match(msg.address_n)), ) if msg.network_id != network_ids.MAINNET: raise wire.ProcessError("Invalid Networ ID") @@ -78,7 +80,7 @@ async def sign_message( # verification_key node = keychain.derive(msg.address_n) - verification_key = ed25519.publickey(node.private_key()) + verification_key = remove_ed25519_prefix(node.public_key()) # Sign1Message # msg = Sign1Message( # phdr={ diff --git a/core/src/apps/cardano/sign_tx/__init__.py b/core/src/apps/cardano/sign_tx/__init__.py index 5581526868..4f40a15051 100644 --- a/core/src/apps/cardano/sign_tx/__init__.py +++ b/core/src/apps/cardano/sign_tx/__init__.py @@ -1,35 +1,46 @@ -from typing import Type +from typing import TYPE_CHECKING -from trezor import log, messages, wire -from trezor.enums import CardanoTxSigningMode +from trezor import wire from .. import seed -from .signer import Signer + +if __debug__: + from trezor import log +if TYPE_CHECKING: + from typing import Type + + from trezor.messages import CardanoSignTxFinished, CardanoSignTxInit @seed.with_keychain async def sign_tx( - ctx: wire.Context, msg: messages.CardanoSignTxInit, keychain: seed.Keychain -) -> messages.CardanoSignTxFinished: - signer_type: Type[Signer] + ctx: wire.Context, msg: CardanoSignTxInit, keychain: seed.Keychain +) -> CardanoSignTxFinished: + from trezor.enums import CardanoTxSigningMode + from trezor.messages import CardanoSignTxFinished + + from .signer import Signer + + signing_mode = msg.signing_mode # local_cache_attribute from trezor.lvglui.scrs import lv from .. import ICON, PRIMARY_COLOR ctx.primary_color, ctx.icon_path = lv.color_hex(PRIMARY_COLOR), ICON - if msg.signing_mode == CardanoTxSigningMode.ORDINARY_TRANSACTION: + signer_type: Type[Signer] + if signing_mode == CardanoTxSigningMode.ORDINARY_TRANSACTION: from .ordinary_signer import OrdinarySigner signer_type = OrdinarySigner - elif msg.signing_mode == CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER: + elif signing_mode == CardanoTxSigningMode.POOL_REGISTRATION_AS_OWNER: from .pool_owner_signer import PoolOwnerSigner signer_type = PoolOwnerSigner - elif msg.signing_mode == CardanoTxSigningMode.MULTISIG_TRANSACTION: + elif signing_mode == CardanoTxSigningMode.MULTISIG_TRANSACTION: from .multisig_signer import MultisigSigner signer_type = MultisigSigner - elif msg.signing_mode == CardanoTxSigningMode.PLUTUS_TRANSACTION: + elif signing_mode == CardanoTxSigningMode.PLUTUS_TRANSACTION: from .plutus_signer import PlutusSigner signer_type = PlutusSigner @@ -45,4 +56,4 @@ async def sign_tx( log.exception(__name__, e) raise wire.ProcessError("Signing failed") - return messages.CardanoSignTxFinished() + return CardanoSignTxFinished() diff --git a/core/src/apps/cardano/sign_tx/multisig_signer.py b/core/src/apps/cardano/sign_tx/multisig_signer.py index 2cd56f63ff..b57104e317 100644 --- a/core/src/apps/cardano/sign_tx/multisig_signer.py +++ b/core/src/apps/cardano/sign_tx/multisig_signer.py @@ -1,10 +1,12 @@ -from trezor import messages, wire -from trezor.enums import CardanoCertificateType +from typing import TYPE_CHECKING + +from trezor.wire import ProcessError -from .. import layout, seed -from ..helpers.paths import SCHEMA_MINT from .signer import Signer +if TYPE_CHECKING: + from trezor import messages + class MultisigSigner(Signer): """ @@ -14,23 +16,30 @@ class MultisigSigner(Signer): SIGNING_MODE_TITLE = "Confirming a multisig transaction" def _validate_tx_init(self) -> None: + msg = self.msg # local_cache_attribute + _assert_tx_init_cond = self._assert_tx_init_cond # local_cache_attribute + super()._validate_tx_init() - self._assert_tx_init_cond(self.msg.collateral_inputs_count == 0) - self._assert_tx_init_cond(not self.msg.has_collateral_return) - self._assert_tx_init_cond(self.msg.total_collateral is None) - self._assert_tx_init_cond(self.msg.reference_inputs_count == 0) + _assert_tx_init_cond(msg.collateral_inputs_count == 0) + _assert_tx_init_cond(not msg.has_collateral_return) + _assert_tx_init_cond(msg.total_collateral is None) + _assert_tx_init_cond(msg.reference_inputs_count == 0) async def _confirm_tx(self, tx_hash: bytes) -> None: + from .. import layout + + msg = self.msg # local_cache_attribute + # super() omitted intentionally is_network_id_verifiable = self._is_network_id_verifiable() await layout.confirm_tx( self.ctx, - self.msg.fee, - self.msg.network_id, - self.msg.protocol_magic, - self.msg.ttl, - self.msg.validity_interval_start, - self.msg.total_collateral, + msg.fee, + msg.network_id, + msg.protocol_magic, + msg.ttl, + msg.validity_interval_start, + msg.total_collateral, is_network_id_verifiable, tx_hash=None, ) @@ -38,23 +47,28 @@ async def _confirm_tx(self, tx_hash: bytes) -> None: def _validate_output(self, output: messages.CardanoTxOutput) -> None: super()._validate_output(output) if output.address_parameters is not None: - raise wire.ProcessError("Invalid output") + raise ProcessError("Invalid output") def _validate_certificate(self, certificate: messages.CardanoTxCertificate) -> None: + from trezor.enums import CardanoCertificateType + super()._validate_certificate(certificate) if certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION: - raise wire.ProcessError("Invalid certificate") + raise ProcessError("Invalid certificate") if certificate.path or certificate.key_hash: - raise wire.ProcessError("Invalid certificate") + raise ProcessError("Invalid certificate") def _validate_withdrawal(self, withdrawal: messages.CardanoTxWithdrawal) -> None: super()._validate_withdrawal(withdrawal) if withdrawal.path or withdrawal.key_hash: - raise wire.ProcessError("Invalid withdrawal") + raise ProcessError("Invalid withdrawal") def _validate_witness_request( self, witness_request: messages.CardanoTxWitnessRequest ) -> None: + from .. import seed + from ..helpers.paths import SCHEMA_MINT + super()._validate_witness_request(witness_request) is_minting = SCHEMA_MINT.match(witness_request.path) tx_has_token_minting = self.msg.minting_asset_groups_count > 0 @@ -63,4 +77,4 @@ def _validate_witness_request( seed.is_multisig_path(witness_request.path) or (is_minting and tx_has_token_minting) ): - raise wire.ProcessError("Invalid witness request") + raise ProcessError("Invalid witness request") diff --git a/core/src/apps/cardano/sign_tx/ordinary_signer.py b/core/src/apps/cardano/sign_tx/ordinary_signer.py index 6ecbd22462..d7a3cc17d0 100644 --- a/core/src/apps/cardano/sign_tx/ordinary_signer.py +++ b/core/src/apps/cardano/sign_tx/ordinary_signer.py @@ -1,15 +1,14 @@ -from trezor import messages, wire -from trezor.enums import CardanoCertificateType - -from .. import layout, seed -from ..helpers.paths import ( - SCHEMA_MINT, - SCHEMA_PAYMENT, - SCHEMA_STAKING, - WITNESS_PATH_NAME, -) +from typing import TYPE_CHECKING + +from trezor.wire import ProcessError + +from .. import layout +from ..helpers.paths import SCHEMA_MINT from .signer import Signer +if TYPE_CHECKING: + from trezor import messages + class OrdinarySigner(Signer): """ @@ -20,42 +19,51 @@ class OrdinarySigner(Signer): SIGNING_MODE_TITLE = "Confirming a transaction" def _validate_tx_init(self) -> None: + msg = self.msg # local_cache_attribute + _assert_tx_init_cond = self._assert_tx_init_cond # local_cache_attribute + super()._validate_tx_init() - self._assert_tx_init_cond(self.msg.collateral_inputs_count == 0) - self._assert_tx_init_cond(not self.msg.has_collateral_return) - self._assert_tx_init_cond(self.msg.total_collateral is None) - self._assert_tx_init_cond(self.msg.reference_inputs_count == 0) + _assert_tx_init_cond(msg.collateral_inputs_count == 0) + _assert_tx_init_cond(not msg.has_collateral_return) + _assert_tx_init_cond(msg.total_collateral is None) + _assert_tx_init_cond(msg.reference_inputs_count == 0) async def _confirm_tx(self, tx_hash: bytes) -> None: + msg = self.msg # local_cache_attribute + # super() omitted intentionally is_network_id_verifiable = self._is_network_id_verifiable() await layout.confirm_tx( self.ctx, - self.msg.fee, - self.msg.network_id, - self.msg.protocol_magic, - self.msg.ttl, - self.msg.validity_interval_start, - self.msg.total_collateral, + msg.fee, + msg.network_id, + msg.protocol_magic, + msg.ttl, + msg.validity_interval_start, + msg.total_collateral, is_network_id_verifiable, tx_hash=None, ) def _validate_certificate(self, certificate: messages.CardanoTxCertificate) -> None: + from trezor.enums import CardanoCertificateType + super()._validate_certificate(certificate) if certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION: - raise wire.ProcessError("Invalid certificate") + raise ProcessError("Invalid certificate") if certificate.script_hash or certificate.key_hash: - raise wire.ProcessError("Invalid certificate") + raise ProcessError("Invalid certificate") def _validate_withdrawal(self, withdrawal: messages.CardanoTxWithdrawal) -> None: super()._validate_withdrawal(withdrawal) if withdrawal.script_hash or withdrawal.key_hash: - raise wire.ProcessError("Invalid withdrawal") + raise ProcessError("Invalid withdrawal") def _validate_witness_request( self, witness_request: messages.CardanoTxWitnessRequest ) -> None: + from .. import seed + super()._validate_witness_request(witness_request) is_minting = SCHEMA_MINT.match(witness_request.path) tx_has_token_minting = self.msg.minting_asset_groups_count > 0 @@ -65,9 +73,11 @@ def _validate_witness_request( or seed.is_shelley_path(witness_request.path) or (is_minting and tx_has_token_minting) ): - raise wire.ProcessError("Invalid witness request") + raise ProcessError("Invalid witness request") async def _show_witness_request(self, witness_path: list[int]) -> None: + from ..helpers.paths import SCHEMA_PAYMENT, SCHEMA_STAKING + # super() omitted intentionally # We only allow payment, staking or minting paths. # If the path is an unusual payment or staking path, we either fail or show the @@ -80,7 +90,7 @@ async def _show_witness_request(self, witness_path: list[int]) -> None: if is_minting: await layout.confirm_witness_request(self.ctx, witness_path) elif not is_payment and not is_staking: - await self._fail_or_warn_path(witness_path, WITNESS_PATH_NAME) + await self._fail_or_warn_path(witness_path, "Witness path") else: await self._show_if_showing_details( layout.confirm_witness_request(self.ctx, witness_path) diff --git a/core/src/apps/cardano/sign_tx/plutus_signer.py b/core/src/apps/cardano/sign_tx/plutus_signer.py index 0c03ea6cf1..486ffb5ae3 100644 --- a/core/src/apps/cardano/sign_tx/plutus_signer.py +++ b/core/src/apps/cardano/sign_tx/plutus_signer.py @@ -1,11 +1,13 @@ -from trezor import messages, wire -from trezor.enums import CardanoCertificateType +from typing import TYPE_CHECKING -from .. import layout, seed -from ..helpers.credential import Credential, should_show_credentials -from ..helpers.paths import SCHEMA_MINT +from trezor import wire + +from .. import layout from .signer import Signer +if TYPE_CHECKING: + from trezor import messages + class PlutusSigner(Signer): """ @@ -28,6 +30,8 @@ async def _show_tx_init(self) -> None: await layout.warn_unknown_total_collateral(self.ctx) async def _confirm_tx(self, tx_hash: bytes) -> None: + msg = self.msg # local_cache_attribute + # super() omitted intentionally # We display tx hash so that experienced users can compare it to the tx hash # computed by a trusted device (in case the tx contains many items which are @@ -35,22 +39,16 @@ async def _confirm_tx(self, tx_hash: bytes) -> None: is_network_id_verifiable = self._is_network_id_verifiable() await layout.confirm_tx( self.ctx, - self.msg.fee, - self.msg.network_id, - self.msg.protocol_magic, - self.msg.ttl, - self.msg.validity_interval_start, - self.msg.total_collateral, + msg.fee, + msg.network_id, + msg.protocol_magic, + msg.ttl, + msg.validity_interval_start, + msg.total_collateral, is_network_id_verifiable, tx_hash, ) - def _should_show_tx_hash(self) -> bool: - # super() omitted intentionally - # Plutus txs tend to contain a lot of opaque data, some users might - # want to verify only the tx hash. - return True - async def _show_input(self, input: messages.CardanoTxInput) -> None: # super() omitted intentionally # The inputs are not interchangeable (because of datums), so we must show them. @@ -59,6 +57,8 @@ async def _show_input(self, input: messages.CardanoTxInput) -> None: async def _show_output_credentials( self, address_parameters: messages.CardanoAddressParametersType ) -> None: + from ..helpers.credential import Credential, should_show_credentials + # In ordinary txs, change outputs with matching payment and staking paths can be # hidden, but we need to show them in Plutus txs because of the script # evaluation. We at least hide the staking path if it matches the payment path. @@ -86,6 +86,8 @@ def _is_change_output(self, output: messages.CardanoTxOutput) -> bool: return False def _validate_certificate(self, certificate: messages.CardanoTxCertificate) -> None: + from trezor.enums import CardanoCertificateType + super()._validate_certificate(certificate) if certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION: raise wire.ProcessError("Invalid certificate") @@ -93,6 +95,9 @@ def _validate_certificate(self, certificate: messages.CardanoTxCertificate) -> N def _validate_witness_request( self, witness_request: messages.CardanoTxWitnessRequest ) -> None: + from .. import seed + from ..helpers.paths import SCHEMA_MINT + super()._validate_witness_request(witness_request) is_minting = SCHEMA_MINT.match(witness_request.path) diff --git a/core/src/apps/cardano/sign_tx/pool_owner_signer.py b/core/src/apps/cardano/sign_tx/pool_owner_signer.py index 8008a226e8..628d8f3d15 100644 --- a/core/src/apps/cardano/sign_tx/pool_owner_signer.py +++ b/core/src/apps/cardano/sign_tx/pool_owner_signer.py @@ -1,10 +1,12 @@ -from trezor import messages, wire -from trezor.enums import CardanoCertificateType +from typing import TYPE_CHECKING + +from trezor.wire import ProcessError -from .. import layout -from ..helpers.paths import SCHEMA_STAKING_ANY_ACCOUNT from .signer import Signer +if TYPE_CHECKING: + from trezor import messages + class PoolOwnerSigner(Signer): """ @@ -22,18 +24,25 @@ class PoolOwnerSigner(Signer): SIGNING_MODE_TITLE = "Confirming pool registration as owner" def _validate_tx_init(self) -> None: + msg = self.msg # local_cache_attribute + super()._validate_tx_init() - self._assert_tx_init_cond(self.msg.certificates_count == 1) - self._assert_tx_init_cond(self.msg.withdrawals_count == 0) - self._assert_tx_init_cond(self.msg.minting_asset_groups_count == 0) - self._assert_tx_init_cond(self.msg.script_data_hash is None) - self._assert_tx_init_cond(self.msg.collateral_inputs_count == 0) - self._assert_tx_init_cond(self.msg.required_signers_count == 0) - self._assert_tx_init_cond(not self.msg.has_collateral_return) - self._assert_tx_init_cond(self.msg.total_collateral is None) - self._assert_tx_init_cond(self.msg.reference_inputs_count == 0) + for condition in ( + msg.certificates_count == 1, + msg.withdrawals_count == 0, + msg.minting_asset_groups_count == 0, + msg.script_data_hash is None, + msg.collateral_inputs_count == 0, + msg.required_signers_count == 0, + not msg.has_collateral_return, + msg.total_collateral is None, + msg.reference_inputs_count == 0, + ): + self._assert_tx_init_cond(condition) async def _confirm_tx(self, tx_hash: bytes) -> None: + from .. import layout + # super() omitted intentionally await layout.confirm_stake_pool_registration_final( self.ctx, @@ -50,7 +59,7 @@ def _validate_output(self, output: messages.CardanoTxOutput) -> None: or output.inline_datum_size > 0 or output.reference_script_size > 0 ): - raise wire.ProcessError("Invalid output") + raise ProcessError("Invalid output") def _should_show_output(self, output: messages.CardanoTxOutput) -> bool: # super() omitted intentionally @@ -58,17 +67,24 @@ def _should_show_output(self, output: messages.CardanoTxOutput) -> bool: return False def _validate_certificate(self, certificate: messages.CardanoTxCertificate) -> None: + from trezor.enums import CardanoCertificateType + super()._validate_certificate(certificate) if certificate.type != CardanoCertificateType.STAKE_POOL_REGISTRATION: - raise wire.ProcessError("Invalid certificate") + raise ProcessError("Invalid certificate") def _validate_witness_request( self, witness_request: messages.CardanoTxWitnessRequest ) -> None: + from ..helpers.paths import SCHEMA_STAKING_ANY_ACCOUNT + super()._validate_witness_request(witness_request) - if not SCHEMA_STAKING_ANY_ACCOUNT.match(witness_request.path): - raise wire.ProcessError( - "Stakepool registration transaction can only contain staking witnesses" + if not ( + SCHEMA_STAKING_ANY_ACCOUNT.match(witness_request.path) + and witness_request.path == self.pool_owner_path + ): + raise ProcessError( + "Stakepool registration transaction can only contain the pool owner witness request" ) def _is_network_id_verifiable(self) -> bool: diff --git a/core/src/apps/cardano/sign_tx/signer.py b/core/src/apps/cardano/sign_tx/signer.py index 48675b46fc..8363528703 100644 --- a/core/src/apps/cardano/sign_tx/signer.py +++ b/core/src/apps/cardano/sign_tx/signer.py @@ -1,87 +1,72 @@ from micropython import const from typing import TYPE_CHECKING -from trezor import messages, wire -from trezor.crypto import hashlib -from trezor.crypto.curve import ed25519 +from trezor import messages from trezor.enums import ( - CardanoAddressType, CardanoCertificateType, CardanoTxOutputSerializationFormat, CardanoTxWitnessType, ) +from trezor.messages import CardanoTxItemAck, CardanoTxOutput from trezor.ui.layouts.lvgl import confirm_final +from trezor.wire import DataError, ProcessError -from apps.common import cbor, safety_checks +from apps.common import safety_checks -from .. import addresses, auxiliary_data, certificates, layout, seed -from ..helpers import ( - ADDRESS_KEY_HASH_SIZE, - INPUT_PREV_HASH_SIZE, - LOVELACE_MAX_SUPPLY, - OUTPUT_DATUM_HASH_SIZE, - SCRIPT_DATA_HASH_SIZE, -) -from ..helpers.account_path_check import AccountPathChecker -from ..helpers.credential import Credential, should_show_credentials +from .. import addresses, certificates, layout, seed +from ..helpers import INPUT_PREV_HASH_SIZE, LOVELACE_MAX_SUPPLY +from ..helpers.credential import Credential from ..helpers.hash_builder_collection import ( HashBuilderDict, - HashBuilderEmbeddedCBOR, HashBuilderList, + HashBuilderSet, ) -from ..helpers.paths import ( - CERTIFICATE_PATH_NAME, - CHANGE_OUTPUT_PATH_NAME, - CHANGE_OUTPUT_STAKING_PATH_NAME, - POOL_OWNER_STAKING_PATH_NAME, - SCHEMA_STAKING, -) -from ..helpers.utils import ( - derive_public_key, - get_public_key_hash, - validate_network_info, - validate_stake_credential, -) +from ..helpers.paths import SCHEMA_STAKING +from ..helpers.utils import derive_public_key if TYPE_CHECKING: from typing import Any, Awaitable, ClassVar + from trezor import wire + from trezor.enums import CardanoAddressType + + from apps.common import cbor from apps.common.paths import PathSchema - CardanoTxResponseType = ( - messages.CardanoTxItemAck | messages.CardanoTxWitnessResponse - ) + from ..helpers.hash_builder_collection import HashBuilderEmbeddedCBOR + + CardanoTxResponseType = CardanoTxItemAck | messages.CardanoTxWitnessResponse -MINTING_POLICY_ID_LENGTH = 28 -MAX_ASSET_NAME_LENGTH = 32 +_MINTING_POLICY_ID_LENGTH = const(28) +_MAX_ASSET_NAME_LENGTH = const(32) -TX_BODY_KEY_INPUTS = const(0) -TX_BODY_KEY_OUTPUTS = const(1) -TX_BODY_KEY_FEE = const(2) -TX_BODY_KEY_TTL = const(3) -TX_BODY_KEY_CERTIFICATES = const(4) -TX_BODY_KEY_WITHDRAWALS = const(5) -TX_BODY_KEY_AUXILIARY_DATA = const(7) -TX_BODY_KEY_VALIDITY_INTERVAL_START = const(8) -TX_BODY_KEY_MINT = const(9) -TX_BODY_KEY_SCRIPT_DATA_HASH = const(11) -TX_BODY_KEY_COLLATERAL_INPUTS = const(13) -TX_BODY_KEY_REQUIRED_SIGNERS = const(14) -TX_BODY_KEY_NETWORK_ID = const(15) -TX_BODY_KEY_COLLATERAL_RETURN = const(16) -TX_BODY_KEY_TOTAL_COLLATERAL = const(17) -TX_BODY_KEY_REFERENCE_INPUTS = const(18) +_TX_BODY_KEY_INPUTS = const(0) +_TX_BODY_KEY_OUTPUTS = const(1) +_TX_BODY_KEY_FEE = const(2) +_TX_BODY_KEY_TTL = const(3) +_TX_BODY_KEY_CERTIFICATES = const(4) +_TX_BODY_KEY_WITHDRAWALS = const(5) +_TX_BODY_KEY_AUXILIARY_DATA = const(7) +_TX_BODY_KEY_VALIDITY_INTERVAL_START = const(8) +_TX_BODY_KEY_MINT = const(9) +_TX_BODY_KEY_SCRIPT_DATA_HASH = const(11) +_TX_BODY_KEY_COLLATERAL_INPUTS = const(13) +_TX_BODY_KEY_REQUIRED_SIGNERS = const(14) +_TX_BODY_KEY_NETWORK_ID = const(15) +_TX_BODY_KEY_COLLATERAL_RETURN = const(16) +_TX_BODY_KEY_TOTAL_COLLATERAL = const(17) +_TX_BODY_KEY_REFERENCE_INPUTS = const(18) -BABBAGE_OUTPUT_KEY_ADDRESS = const(0) -BABBAGE_OUTPUT_KEY_AMOUNT = const(1) -BABBAGE_OUTPUT_KEY_DATUM_OPTION = const(2) -BABBAGE_OUTPUT_KEY_REFERENCE_SCRIPT = const(3) +_BABBAGE_OUTPUT_KEY_ADDRESS = const(0) +_BABBAGE_OUTPUT_KEY_AMOUNT = const(1) +_BABBAGE_OUTPUT_KEY_DATUM_OPTION = const(2) +_BABBAGE_OUTPUT_KEY_REFERENCE_SCRIPT = const(3) -DATUM_OPTION_KEY_HASH = const(0) -DATUM_OPTION_KEY_INLINE = const(1) +_DATUM_OPTION_KEY_HASH = const(0) +_DATUM_OPTION_KEY_INLINE = const(1) -POOL_REGISTRATION_CERTIFICATE_ITEMS_COUNT = 10 +_POOL_REGISTRATION_CERTIFICATE_ITEMS_COUNT = const(10) -MAX_CHUNK_SIZE = 1024 +_MAX_CHUNK_SIZE = const(1024) class Signer: @@ -101,12 +86,17 @@ def __init__( msg: messages.CardanoSignTxInit, keychain: seed.Keychain, ) -> None: + from ..helpers.account_path_check import AccountPathChecker + self.ctx = ctx self.msg = msg self.keychain = keychain self.account_path_checker = AccountPathChecker() + # There should be at most one pool owner given as a path. + self.pool_owner_path = None + # Inputs, outputs and fee are mandatory, count the number of optional fields present. tx_dict_items_count = 3 + sum( ( @@ -126,12 +116,14 @@ def __init__( ) ) self.tx_dict: HashBuilderDict[int, Any] = HashBuilderDict( - tx_dict_items_count, wire.ProcessError("Invalid tx signing request") + tx_dict_items_count, ProcessError("Invalid tx signing request") ) self.should_show_details = False async def sign(self) -> None: + from trezor.crypto import hashlib + hash_fn = hashlib.blake2b(outlen=32) self.tx_dict.start(hash_fn) with self.tx_dict: @@ -141,7 +133,6 @@ async def sign(self) -> None: await self._confirm_tx(tx_hash) response_after_witness_requests = await self._process_witness_requests(tx_hash) - await confirm_final(self.ctx, "Cardano") await self.ctx.call(response_after_witness_requests, messages.CardanoTxHostAck) await self.ctx.call( messages.CardanoTxBodyHash(tx_hash=tx_hash), messages.CardanoTxHostAck @@ -152,96 +143,100 @@ async def sign(self) -> None: async def _processs_tx_init(self) -> None: self._validate_tx_init() await self._show_tx_init() + msg = self.msg # local_cache_attribute + add = self.tx_dict.add # local_cache_attribute + HBL = HashBuilderList # local_cache_global + HBS = HashBuilderSet # local_cache_global - inputs_list: HashBuilderList[tuple[bytes, int]] = HashBuilderList( - self.msg.inputs_count + inputs_set: HashBuilderSet[tuple[bytes, int]] = HBS( + msg.inputs_count, tagged=self.msg.tag_cbor_sets ) - with self.tx_dict.add(TX_BODY_KEY_INPUTS, inputs_list): - await self._process_inputs(inputs_list) + with add(_TX_BODY_KEY_INPUTS, inputs_set): + await self._process_inputs(inputs_set) - outputs_list: HashBuilderList = HashBuilderList(self.msg.outputs_count) - with self.tx_dict.add(TX_BODY_KEY_OUTPUTS, outputs_list): + outputs_list: HashBuilderList = HBL(msg.outputs_count) + with add(_TX_BODY_KEY_OUTPUTS, outputs_list): await self._process_outputs(outputs_list) - self.tx_dict.add(TX_BODY_KEY_FEE, self.msg.fee) + add(_TX_BODY_KEY_FEE, msg.fee) - if self.msg.ttl is not None: - self.tx_dict.add(TX_BODY_KEY_TTL, self.msg.ttl) + if msg.ttl is not None: + add(_TX_BODY_KEY_TTL, msg.ttl) - if self.msg.certificates_count > 0: - certificates_list: HashBuilderList = HashBuilderList( - self.msg.certificates_count + if msg.certificates_count > 0: + certificates_set: HashBuilderSet = HBS( + msg.certificates_count, tagged=self.msg.tag_cbor_sets ) - with self.tx_dict.add(TX_BODY_KEY_CERTIFICATES, certificates_list): - await self._process_certificates(certificates_list) + with add(_TX_BODY_KEY_CERTIFICATES, certificates_set): + await self._process_certificates(certificates_set) - if self.msg.withdrawals_count > 0: + if msg.withdrawals_count > 0: withdrawals_dict: HashBuilderDict[bytes, int] = HashBuilderDict( - self.msg.withdrawals_count, wire.ProcessError("Invalid withdrawal") + msg.withdrawals_count, ProcessError("Invalid withdrawal") ) - with self.tx_dict.add(TX_BODY_KEY_WITHDRAWALS, withdrawals_dict): + with add(_TX_BODY_KEY_WITHDRAWALS, withdrawals_dict): await self._process_withdrawals(withdrawals_dict) - if self.msg.has_auxiliary_data: + if msg.has_auxiliary_data: await self._process_auxiliary_data() - if self.msg.validity_interval_start is not None: - self.tx_dict.add( - TX_BODY_KEY_VALIDITY_INTERVAL_START, self.msg.validity_interval_start - ) + if msg.validity_interval_start is not None: + add(_TX_BODY_KEY_VALIDITY_INTERVAL_START, msg.validity_interval_start) - if self.msg.minting_asset_groups_count > 0: + if msg.minting_asset_groups_count > 0: minting_dict: HashBuilderDict[bytes, HashBuilderDict] = HashBuilderDict( - self.msg.minting_asset_groups_count, - wire.ProcessError("Invalid mint token bundle"), + msg.minting_asset_groups_count, + ProcessError("Invalid mint token bundle"), ) - with self.tx_dict.add(TX_BODY_KEY_MINT, minting_dict): + with add(_TX_BODY_KEY_MINT, minting_dict): await self._process_minting(minting_dict) - if self.msg.script_data_hash is not None: + if msg.script_data_hash is not None: await self._process_script_data_hash() - if self.msg.collateral_inputs_count > 0: - collateral_inputs_list: HashBuilderList[ - tuple[bytes, int] - ] = HashBuilderList(self.msg.collateral_inputs_count) - with self.tx_dict.add( - TX_BODY_KEY_COLLATERAL_INPUTS, collateral_inputs_list - ): - await self._process_collateral_inputs(collateral_inputs_list) + if msg.collateral_inputs_count > 0: + collateral_inputs_set: HashBuilderSet[tuple[bytes, int]] = HBS( + msg.collateral_inputs_count, tagged=self.msg.tag_cbor_sets + ) + with add(_TX_BODY_KEY_COLLATERAL_INPUTS, collateral_inputs_set): + await self._process_collateral_inputs(collateral_inputs_set) - if self.msg.required_signers_count > 0: - required_signers_list: HashBuilderList[bytes] = HashBuilderList( - self.msg.required_signers_count + if msg.required_signers_count > 0: + required_signers_set: HashBuilderSet[bytes] = HBS( + msg.required_signers_count, tagged=self.msg.tag_cbor_sets ) - with self.tx_dict.add(TX_BODY_KEY_REQUIRED_SIGNERS, required_signers_list): - await self._process_required_signers(required_signers_list) + with add(_TX_BODY_KEY_REQUIRED_SIGNERS, required_signers_set): + await self._process_required_signers(required_signers_set) - if self.msg.include_network_id: - self.tx_dict.add(TX_BODY_KEY_NETWORK_ID, self.msg.network_id) + if msg.include_network_id: + add(_TX_BODY_KEY_NETWORK_ID, msg.network_id) - if self.msg.has_collateral_return: + if msg.has_collateral_return: await self._process_collateral_return() - if self.msg.total_collateral is not None: - self.tx_dict.add(TX_BODY_KEY_TOTAL_COLLATERAL, self.msg.total_collateral) + if msg.total_collateral is not None: + add(_TX_BODY_KEY_TOTAL_COLLATERAL, msg.total_collateral) - if self.msg.reference_inputs_count > 0: - reference_inputs_list: HashBuilderList[tuple[bytes, int]] = HashBuilderList( - self.msg.reference_inputs_count + if msg.reference_inputs_count > 0: + reference_inputs_set: HashBuilderSet[tuple[bytes, int]] = HBS( + msg.reference_inputs_count, tagged=self.msg.tag_cbor_sets ) - with self.tx_dict.add(TX_BODY_KEY_REFERENCE_INPUTS, reference_inputs_list): - await self._process_reference_inputs(reference_inputs_list) + with add(_TX_BODY_KEY_REFERENCE_INPUTS, reference_inputs_set): + await self._process_reference_inputs(reference_inputs_set) def _validate_tx_init(self) -> None: - if self.msg.fee > LOVELACE_MAX_SUPPLY: - raise wire.ProcessError("Fee is out of range!") + from ..helpers.utils import validate_network_info + + msg = self.msg # local_cache_attribute + + if msg.fee > LOVELACE_MAX_SUPPLY: + raise ProcessError("Fee is out of range!") if ( - self.msg.total_collateral is not None - and self.msg.total_collateral > LOVELACE_MAX_SUPPLY + msg.total_collateral is not None + and msg.total_collateral > LOVELACE_MAX_SUPPLY ): - raise wire.ProcessError("Total collateral is out of range!") - validate_network_info(self.msg.network_id, self.msg.protocol_magic) + raise ProcessError("Total collateral is out of range!") + validate_network_info(msg.network_id, msg.protocol_magic) async def _show_tx_init(self) -> None: self.should_show_details = await layout.show_tx_init( @@ -255,10 +250,6 @@ async def _confirm_tx(self, tx_hash: bytes) -> None: # Final signing confirmation is handled separately in each signing mode. raise NotImplementedError - def _should_show_tx_hash(self) -> bool: - # By default we display tx hash only if showing details - return self.should_show_details - # inputs async def _process_inputs( @@ -266,7 +257,7 @@ async def _process_inputs( ) -> None: for _ in range(self.msg.inputs_count): input: messages.CardanoTxInput = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoTxInput + CardanoTxItemAck(), messages.CardanoTxInput ) self._validate_input(input) await self._show_input(input) @@ -274,7 +265,7 @@ async def _process_inputs( def _validate_input(self, input: messages.CardanoTxInput) -> None: if len(input.prev_hash) != INPUT_PREV_HASH_SIZE: - raise wire.ProcessError("Invalid input") + raise ProcessError("Invalid input") async def _show_input(self, input: messages.CardanoTxInput) -> None: # We never show the inputs, except for Plutus txs. @@ -285,18 +276,18 @@ async def _show_input(self, input: messages.CardanoTxInput) -> None: async def _process_outputs(self, outputs_list: HashBuilderList) -> None: total_amount = 0 for _ in range(self.msg.outputs_count): - output: messages.CardanoTxOutput = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoTxOutput + output: CardanoTxOutput = await self.ctx.call( + CardanoTxItemAck(), CardanoTxOutput ) await self._process_output(outputs_list, output) total_amount += output.amount if total_amount > LOVELACE_MAX_SUPPLY: - raise wire.ProcessError("Total transaction amount is out of range!") + raise ProcessError("Total transaction amount is out of range!") async def _process_output( - self, outputs_list: HashBuilderList, output: messages.CardanoTxOutput + self, outputs_list: HashBuilderList, output: CardanoTxOutput ) -> None: self._validate_output(output) should_show = self._should_show_output(output) @@ -316,49 +307,53 @@ async def _process_output( await self._process_legacy_output(output_list, output, should_show) elif output.format == CardanoTxOutputSerializationFormat.MAP_BABBAGE: output_dict: HashBuilderDict[int, Any] = HashBuilderDict( - output_items_count, wire.ProcessError("Invalid output") + output_items_count, ProcessError("Invalid output") ) with outputs_list.append(output_dict): await self._process_babbage_output(output_dict, output, should_show) else: raise RuntimeError # should be unreachable - def _validate_output(self, output: messages.CardanoTxOutput) -> None: - if output.address_parameters is not None and output.address is not None: - raise wire.ProcessError("Invalid output") + def _validate_output(self, output: CardanoTxOutput) -> None: + from ..helpers import OUTPUT_DATUM_HASH_SIZE - if output.address_parameters is not None: - addresses.validate_output_address_parameters(output.address_parameters) - self._fail_if_strict_and_unusual(output.address_parameters) + address_parameters = output.address_parameters # local_cache_attribute + + if address_parameters is not None and output.address is not None: + raise ProcessError("Invalid output") + + if address_parameters is not None: + addresses.validate_output_address_parameters(address_parameters) + self._fail_if_strict_and_unusual(address_parameters) elif output.address is not None: addresses.validate_output_address( output.address, self.msg.protocol_magic, self.msg.network_id ) else: - raise wire.ProcessError("Invalid output") + raise ProcessError("Invalid output") # datum hash if output.datum_hash is not None: if len(output.datum_hash) != OUTPUT_DATUM_HASH_SIZE: - raise wire.ProcessError("Invalid output datum hash") + raise ProcessError("Invalid output datum hash") # inline datum if output.inline_datum_size > 0: if output.format != CardanoTxOutputSerializationFormat.MAP_BABBAGE: - raise wire.ProcessError("Invalid output") + raise ProcessError("Invalid output") # datum hash and inline datum are mutually exclusive if output.datum_hash is not None and output.inline_datum_size > 0: - raise wire.ProcessError("Invalid output") + raise ProcessError("Invalid output") # reference script if output.reference_script_size > 0: if output.format != CardanoTxOutputSerializationFormat.MAP_BABBAGE: - raise wire.ProcessError("Invalid output") + raise ProcessError("Invalid output") self.account_path_checker.add_output(output) - async def _show_output_init(self, output: messages.CardanoTxOutput) -> None: + async def _show_output_init(self, output: CardanoTxOutput) -> None: address_type = self._get_output_address_type(output) if ( output.datum_hash is None @@ -388,6 +383,7 @@ async def _show_output_init(self, output: messages.CardanoTxOutput) -> None: address, "change" if self._is_change_output(output) else "address", self.msg.network_id, + chunkify=bool(self.msg.chunkify), ) async def _show_output_credentials( @@ -399,7 +395,7 @@ async def _show_output_credentials( Credential.stake_credential(address_parameters), ) - def _should_show_output(self, output: messages.CardanoTxOutput) -> bool: + def _should_show_output(self, output: CardanoTxOutput) -> bool: """ Determines whether the output should be shown. Extracted from _show_output because of readability. @@ -425,12 +421,14 @@ def _should_show_output(self, output: messages.CardanoTxOutput) -> bool: return True - def _is_change_output(self, output: messages.CardanoTxOutput) -> bool: + def _is_change_output(self, output: CardanoTxOutput) -> bool: """Used only to determine what message to show to the user when confirming sending.""" return output.address_parameters is not None - def _is_simple_change_output(self, output: messages.CardanoTxOutput) -> bool: + def _is_simple_change_output(self, output: CardanoTxOutput) -> bool: """Used to determine whether an output is a change output with ordinary credentials.""" + from ..helpers.credential import should_show_credentials + return output.address_parameters is not None and not should_show_credentials( output.address_parameters ) @@ -438,7 +436,7 @@ def _is_simple_change_output(self, output: messages.CardanoTxOutput) -> bool: async def _process_legacy_output( self, output_list: HashBuilderList, - output: messages.CardanoTxOutput, + output: CardanoTxOutput, should_show: bool, ) -> None: address = self._get_output_address(output) @@ -463,23 +461,27 @@ async def _process_legacy_output( async def _process_babbage_output( self, output_dict: HashBuilderDict[int, Any], - output: messages.CardanoTxOutput, + output: CardanoTxOutput, should_show: bool, ) -> None: """ This output format corresponds to the post-Alonzo format in CDDL. Note that it is to be used also for outputs with no Plutus elements. """ + from ..helpers.hash_builder_collection import HashBuilderEmbeddedCBOR + + add = output_dict.add # local_cache_attribute + address = self._get_output_address(output) - output_dict.add(BABBAGE_OUTPUT_KEY_ADDRESS, address) + add(_BABBAGE_OUTPUT_KEY_ADDRESS, address) if output.asset_groups_count == 0: # Only amount is added to the dict. - output_dict.add(BABBAGE_OUTPUT_KEY_AMOUNT, output.amount) + add(_BABBAGE_OUTPUT_KEY_AMOUNT, output.amount) else: # [amount, asset_groups] is added to the dict. output_value_list: HashBuilderList = HashBuilderList(2) - with output_dict.add(BABBAGE_OUTPUT_KEY_AMOUNT, output_value_list): + with add(_BABBAGE_OUTPUT_KEY_AMOUNT, output_value_list): await self._process_output_value(output_value_list, output, should_show) if output.datum_hash is not None: @@ -487,14 +489,14 @@ async def _process_babbage_output( await self._show_if_showing_details( layout.confirm_datum_hash(self.ctx, output.datum_hash) ) - output_dict.add( - BABBAGE_OUTPUT_KEY_DATUM_OPTION, - (DATUM_OPTION_KEY_HASH, output.datum_hash), + add( + _BABBAGE_OUTPUT_KEY_DATUM_OPTION, + (_DATUM_OPTION_KEY_HASH, output.datum_hash), ) elif output.inline_datum_size > 0: inline_datum_list: HashBuilderList = HashBuilderList(2) - with output_dict.add(BABBAGE_OUTPUT_KEY_DATUM_OPTION, inline_datum_list): - inline_datum_list.append(DATUM_OPTION_KEY_INLINE) + with add(_BABBAGE_OUTPUT_KEY_DATUM_OPTION, inline_datum_list): + inline_datum_list.append(_DATUM_OPTION_KEY_INLINE) inline_datum_cbor: HashBuilderEmbeddedCBOR = HashBuilderEmbeddedCBOR( output.inline_datum_size ) @@ -507,9 +509,7 @@ async def _process_babbage_output( reference_script_cbor: HashBuilderEmbeddedCBOR = HashBuilderEmbeddedCBOR( output.reference_script_size ) - with output_dict.add( - BABBAGE_OUTPUT_KEY_REFERENCE_SCRIPT, reference_script_cbor - ): + with add(_BABBAGE_OUTPUT_KEY_REFERENCE_SCRIPT, reference_script_cbor): await self._process_reference_script( reference_script_cbor, output.reference_script_size, should_show ) @@ -517,7 +517,7 @@ async def _process_babbage_output( async def _process_output_value( self, output_value_list: HashBuilderList, - output: messages.CardanoTxOutput, + output: CardanoTxOutput, should_show_tokens: bool, ) -> None: """Should be used only when the output contains tokens.""" @@ -529,7 +529,7 @@ async def _process_output_value( bytes, HashBuilderDict[bytes, int] ] = HashBuilderDict( output.asset_groups_count, - wire.ProcessError("Invalid token bundle in output"), + ProcessError("Invalid token bundle in output"), ) with output_value_list.append(asset_groups_dict): await self._process_asset_groups( @@ -548,13 +548,13 @@ async def _process_asset_groups( ) -> None: for _ in range(asset_groups_count): asset_group: messages.CardanoAssetGroup = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoAssetGroup + CardanoTxItemAck(), messages.CardanoAssetGroup ) self._validate_asset_group(asset_group) tokens: HashBuilderDict[bytes, int] = HashBuilderDict( asset_group.tokens_count, - wire.ProcessError("Invalid token bundle in output"), + ProcessError("Invalid token bundle in output"), ) with asset_groups_dict.add(asset_group.policy_id, tokens): await self._process_tokens( @@ -568,12 +568,12 @@ def _validate_asset_group( self, asset_group: messages.CardanoAssetGroup, is_mint: bool = False ) -> None: INVALID_TOKEN_BUNDLE = ( - wire.ProcessError("Invalid mint token bundle") + ProcessError("Invalid mint token bundle") if is_mint - else wire.ProcessError("Invalid token bundle in output") + else ProcessError("Invalid token bundle in output") ) - if len(asset_group.policy_id) != MINTING_POLICY_ID_LENGTH: + if len(asset_group.policy_id) != _MINTING_POLICY_ID_LENGTH: raise INVALID_TOKEN_BUNDLE if asset_group.tokens_count == 0: raise INVALID_TOKEN_BUNDLE @@ -589,7 +589,7 @@ async def _process_tokens( ) -> None: for _ in range(tokens_count): token: messages.CardanoToken = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoToken + CardanoTxItemAck(), messages.CardanoToken ) self._validate_token(token) if should_show_tokens: @@ -602,9 +602,9 @@ def _validate_token( self, token: messages.CardanoToken, is_mint: bool = False ) -> None: INVALID_TOKEN_BUNDLE = ( - wire.ProcessError("Invalid mint token bundle") + ProcessError("Invalid mint token bundle") if is_mint - else wire.ProcessError("Invalid token bundle in output") + else ProcessError("Invalid token bundle in output") ) if is_mint: @@ -614,7 +614,7 @@ def _validate_token( if token.amount is None or token.mint_amount is not None: raise INVALID_TOKEN_BUNDLE - if len(token.asset_name_bytes) > MAX_ASSET_NAME_LENGTH: + if len(token.asset_name_bytes) > _MAX_ASSET_NAME_LENGTH: raise INVALID_TOKEN_BUNDLE # inline datum @@ -630,13 +630,13 @@ async def _process_inline_datum( chunks_count = self._get_chunks_count(inline_datum_size) for chunk_number in range(chunks_count): chunk: messages.CardanoTxInlineDatumChunk = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoTxInlineDatumChunk + CardanoTxItemAck(), messages.CardanoTxInlineDatumChunk ) self._validate_chunk( chunk.data, chunk_number, chunks_count, - wire.ProcessError("Invalid inline datum chunk"), + ProcessError("Invalid inline datum chunk"), ) if chunk_number == 0 and should_show: await self._show_if_showing_details( @@ -657,13 +657,13 @@ async def _process_reference_script( chunks_count = self._get_chunks_count(reference_script_size) for chunk_number in range(chunks_count): chunk: messages.CardanoTxReferenceScriptChunk = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoTxReferenceScriptChunk + CardanoTxItemAck(), messages.CardanoTxReferenceScriptChunk ) self._validate_chunk( chunk.data, chunk_number, chunks_count, - wire.ProcessError("Invalid reference script chunk"), + ProcessError("Invalid reference script chunk"), ) if chunk_number == 0 and should_show: await self._show_if_showing_details( @@ -675,10 +675,10 @@ async def _process_reference_script( # certificates - async def _process_certificates(self, certificates_list: HashBuilderList) -> None: + async def _process_certificates(self, certificates_set: HashBuilderSet) -> None: for _ in range(self.msg.certificates_count): certificate: messages.CardanoTxCertificate = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoTxCertificate + CardanoTxItemAck(), messages.CardanoTxCertificate ) self._validate_certificate(certificate) await self._show_certificate(certificate) @@ -688,20 +688,20 @@ async def _process_certificates(self, certificates_list: HashBuilderList) -> Non assert pool_parameters is not None # _validate_certificate pool_items_list: HashBuilderList = HashBuilderList( - POOL_REGISTRATION_CERTIFICATE_ITEMS_COUNT + _POOL_REGISTRATION_CERTIFICATE_ITEMS_COUNT ) - with certificates_list.append(pool_items_list): + with certificates_set.append(pool_items_list): for item in certificates.cborize_pool_registration_init( certificate ): pool_items_list.append(item) - pool_owners_list: HashBuilderList[bytes] = HashBuilderList( - pool_parameters.owners_count + pool_owners_set: HashBuilderSet[bytes] = HashBuilderSet( + pool_parameters.owners_count, tagged=self.msg.tag_cbor_sets ) - with pool_items_list.append(pool_owners_list): + with pool_items_list.append(pool_owners_set): await self._process_pool_owners( - pool_owners_list, pool_parameters.owners_count + pool_owners_set, pool_parameters.owners_count ) relays_list: HashBuilderList[cbor.CborSequence] = HashBuilderList( @@ -716,7 +716,7 @@ async def _process_certificates(self, certificates_list: HashBuilderList) -> Non certificates.cborize_pool_metadata(pool_parameters.metadata) ) else: - certificates_list.append( + certificates_set.append( certificates.cborize(self.keychain, certificate) ) @@ -733,7 +733,7 @@ async def _show_certificate( ) -> None: if certificate.path: await self._fail_or_warn_if_invalid_path( - SCHEMA_STAKING, certificate.path, CERTIFICATE_PATH_NAME + SCHEMA_STAKING, certificate.path, "Certificate path" ) if certificate.type == CardanoCertificateType.STAKE_POOL_REGISTRATION: @@ -745,33 +745,34 @@ async def _show_certificate( self.ctx, certificate.pool_parameters.metadata ) else: - await layout.confirm_certificate(self.ctx, certificate) + await layout.confirm_certificate(self.ctx, certificate, self.msg.network_id) # pool owners async def _process_pool_owners( - self, pool_owners_list: HashBuilderList[bytes], owners_count: int + self, pool_owners_set: HashBuilderSet[bytes], owners_count: int ) -> None: owners_as_path_count = 0 for _ in range(owners_count): owner: messages.CardanoPoolOwner = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoPoolOwner + CardanoTxItemAck(), messages.CardanoPoolOwner ) certificates.validate_pool_owner(owner, self.account_path_checker) await self._show_pool_owner(owner) - pool_owners_list.append( + pool_owners_set.append( certificates.cborize_pool_owner(self.keychain, owner) ) if owner.staking_key_path: owners_as_path_count += 1 + self.pool_owner_path = owner.staking_key_path certificates.assert_cond(owners_as_path_count == 1) async def _show_pool_owner(self, owner: messages.CardanoPoolOwner) -> None: if owner.staking_key_path: await self._fail_or_warn_if_invalid_path( - SCHEMA_STAKING, owner.staking_key_path, POOL_OWNER_STAKING_PATH_NAME + SCHEMA_STAKING, owner.staking_key_path, "Pool owner staking path" ) await layout.confirm_stake_pool_owner( @@ -787,7 +788,7 @@ async def _process_pool_relays( ) -> None: for _ in range(relays_count): relay: messages.CardanoPoolRelayParameters = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoPoolRelayParameters + CardanoTxItemAck(), messages.CardanoPoolRelayParameters ) certificates.validate_pool_relay(relay) relays_list.append(certificates.cborize_pool_relay(relay)) @@ -799,7 +800,7 @@ async def _process_withdrawals( ) -> None: for _ in range(self.msg.withdrawals_count): withdrawal: messages.CardanoTxWithdrawal = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoTxWithdrawal + CardanoTxItemAck(), messages.CardanoTxWithdrawal ) self._validate_withdrawal(withdrawal) address_bytes = self._derive_withdrawal_address_bytes(withdrawal) @@ -811,42 +812,48 @@ async def _process_withdrawals( withdrawals_dict.add(address_bytes, withdrawal.amount) def _validate_withdrawal(self, withdrawal: messages.CardanoTxWithdrawal) -> None: + from ..helpers.utils import validate_stake_credential + validate_stake_credential( withdrawal.path, withdrawal.script_hash, withdrawal.key_hash, - wire.ProcessError("Invalid withdrawal"), + ProcessError("Invalid withdrawal"), ) if not 0 <= withdrawal.amount < LOVELACE_MAX_SUPPLY: - raise wire.ProcessError("Invalid withdrawal") + raise ProcessError("Invalid withdrawal") self.account_path_checker.add_withdrawal(withdrawal) # auxiliary data async def _process_auxiliary_data(self) -> None: + from .. import auxiliary_data + + msg = self.msg # local_cache_attribute + data: messages.CardanoTxAuxiliaryData = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoTxAuxiliaryData + CardanoTxItemAck(), messages.CardanoTxAuxiliaryData ) - auxiliary_data.validate(data) + auxiliary_data.validate(data, msg.protocol_magic, msg.network_id) ( auxiliary_data_hash, auxiliary_data_supplement, ) = auxiliary_data.get_hash_and_supplement( - self.keychain, data, self.msg.protocol_magic, self.msg.network_id + self.keychain, data, msg.protocol_magic, msg.network_id ) await auxiliary_data.show( self.ctx, self.keychain, auxiliary_data_hash, - data.governance_registration_parameters, - self.msg.protocol_magic, - self.msg.network_id, + data.cvote_registration_parameters, + msg.protocol_magic, + msg.network_id, self.should_show_details, ) - self.tx_dict.add(TX_BODY_KEY_AUXILIARY_DATA, auxiliary_data_hash) + self.tx_dict.add(_TX_BODY_KEY_AUXILIARY_DATA, auxiliary_data_hash) await self.ctx.call(auxiliary_data_supplement, messages.CardanoTxHostAck) @@ -856,19 +863,19 @@ async def _process_minting( self, minting_dict: HashBuilderDict[bytes, HashBuilderDict] ) -> None: token_minting: messages.CardanoTxMint = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoTxMint + CardanoTxItemAck(), messages.CardanoTxMint ) await layout.warn_tx_contains_mint(self.ctx) for _ in range(token_minting.asset_groups_count): asset_group: messages.CardanoAssetGroup = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoAssetGroup + CardanoTxItemAck(), messages.CardanoAssetGroup ) self._validate_asset_group(asset_group, is_mint=True) tokens: HashBuilderDict[bytes, int] = HashBuilderDict( - asset_group.tokens_count, wire.ProcessError("Invalid mint token bundle") + asset_group.tokens_count, ProcessError("Invalid mint token bundle") ) with minting_dict.add(asset_group.policy_id, tokens): await self._process_minting_tokens( @@ -887,7 +894,7 @@ async def _process_minting_tokens( ) -> None: for _ in range(tokens_count): token: messages.CardanoToken = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoToken + CardanoTxItemAck(), messages.CardanoToken ) self._validate_token(token, is_mint=True) await layout.confirm_token_minting(self.ctx, policy_id, token) @@ -903,12 +910,14 @@ async def _process_script_data_hash(self) -> None: await self._show_if_showing_details( layout.confirm_script_data_hash(self.ctx, self.msg.script_data_hash) ) - self.tx_dict.add(TX_BODY_KEY_SCRIPT_DATA_HASH, self.msg.script_data_hash) + self.tx_dict.add(_TX_BODY_KEY_SCRIPT_DATA_HASH, self.msg.script_data_hash) def _validate_script_data_hash(self) -> None: + from ..helpers import SCRIPT_DATA_HASH_SIZE + assert self.msg.script_data_hash is not None if len(self.msg.script_data_hash) != SCRIPT_DATA_HASH_SIZE: - raise wire.ProcessError("Invalid script data hash") + raise ProcessError("Invalid script data hash") # collateral inputs @@ -917,7 +926,7 @@ async def _process_collateral_inputs( ) -> None: for _ in range(self.msg.collateral_inputs_count): collateral_input: messages.CardanoTxCollateralInput = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoTxCollateralInput + CardanoTxItemAck(), messages.CardanoTxCollateralInput ) self._validate_collateral_input(collateral_input) await self._show_collateral_input(collateral_input) @@ -929,7 +938,7 @@ def _validate_collateral_input( self, collateral_input: messages.CardanoTxCollateralInput ) -> None: if len(collateral_input.prev_hash) != INPUT_PREV_HASH_SIZE: - raise wire.ProcessError("Invalid collateral input") + raise ProcessError("Invalid collateral input") async def _show_collateral_input( self, collateral_input: messages.CardanoTxCollateralInput @@ -942,11 +951,13 @@ async def _show_collateral_input( # required signers async def _process_required_signers( - self, required_signers_list: HashBuilderList[bytes] + self, required_signers_set: HashBuilderSet[bytes] ) -> None: + from ..helpers.utils import get_public_key_hash + for _ in range(self.msg.required_signers_count): required_signer: messages.CardanoTxRequiredSigner = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoTxRequiredSigner + CardanoTxItemAck(), messages.CardanoTxRequiredSigner ) self._validate_required_signer(required_signer) await self._show_if_showing_details( @@ -956,24 +967,28 @@ async def _process_required_signers( key_hash = required_signer.key_hash or get_public_key_hash( self.keychain, required_signer.key_path ) - required_signers_list.append(key_hash) + required_signers_set.append(key_hash) def _validate_required_signer( self, required_signer: messages.CardanoTxRequiredSigner ) -> None: - INVALID_REQUIRED_SIGNER = wire.ProcessError("Invalid required signer") + from ..helpers import ADDRESS_KEY_HASH_SIZE + + key_path = required_signer.key_path # local_cache_attribute + + INVALID_REQUIRED_SIGNER = ProcessError("Invalid required signer") - if required_signer.key_hash and required_signer.key_path: + if required_signer.key_hash and key_path: raise INVALID_REQUIRED_SIGNER if required_signer.key_hash: if len(required_signer.key_hash) != ADDRESS_KEY_HASH_SIZE: raise INVALID_REQUIRED_SIGNER - elif required_signer.key_path: + elif key_path: if not ( - seed.is_shelley_path(required_signer.key_path) - or seed.is_multisig_path(required_signer.key_path) - or seed.is_minting_path(required_signer.key_path) + seed.is_shelley_path(key_path) + or seed.is_multisig_path(key_path) + or seed.is_minting_path(key_path) ): raise INVALID_REQUIRED_SIGNER else: @@ -982,8 +997,8 @@ def _validate_required_signer( # collateral return async def _process_collateral_return(self) -> None: - output: messages.CardanoTxOutput = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoTxOutput + output: CardanoTxOutput = await self.ctx.call( + CardanoTxItemAck(), CardanoTxOutput ) self._validate_collateral_return(output) should_show_init = self._should_show_collateral_return_init(output) @@ -995,38 +1010,36 @@ async def _process_collateral_return(self) -> None: output_items_count = 2 if output.format == CardanoTxOutputSerializationFormat.ARRAY_LEGACY: output_list: HashBuilderList = HashBuilderList(output_items_count) - with self.tx_dict.add(TX_BODY_KEY_COLLATERAL_RETURN, output_list): + with self.tx_dict.add(_TX_BODY_KEY_COLLATERAL_RETURN, output_list): await self._process_legacy_output( output_list, output, should_show_tokens ) elif output.format == CardanoTxOutputSerializationFormat.MAP_BABBAGE: output_dict: HashBuilderDict[int, Any] = HashBuilderDict( - output_items_count, wire.ProcessError("Invalid collateral return") + output_items_count, ProcessError("Invalid collateral return") ) - with self.tx_dict.add(TX_BODY_KEY_COLLATERAL_RETURN, output_dict): + with self.tx_dict.add(_TX_BODY_KEY_COLLATERAL_RETURN, output_dict): await self._process_babbage_output( output_dict, output, should_show_tokens ) else: raise RuntimeError # should be unreachable - def _validate_collateral_return(self, output: messages.CardanoTxOutput) -> None: + def _validate_collateral_return(self, output: CardanoTxOutput) -> None: self._validate_output(output) address_type = self._get_output_address_type(output) if address_type not in addresses.ADDRESS_TYPES_PAYMENT_KEY: - raise wire.ProcessError("Invalid collateral return") + raise ProcessError("Invalid collateral return") if ( output.datum_hash is not None or output.inline_datum_size > 0 or output.reference_script_size > 0 ): - raise wire.ProcessError("Invalid collateral return") + raise ProcessError("Invalid collateral return") - async def _show_collateral_return_init( - self, output: messages.CardanoTxOutput - ) -> None: + async def _show_collateral_return_init(self, output: CardanoTxOutput) -> None: # We don't display missing datum warning since datums are forbidden. if output.asset_groups_count > 0: @@ -1054,11 +1067,10 @@ async def _show_collateral_return_init( address, "collateral-return", self.msg.network_id, + chunkify=bool(self.msg.chunkify), ) - def _should_show_collateral_return_init( - self, output: messages.CardanoTxOutput - ) -> bool: + def _should_show_collateral_return_init(self, output: CardanoTxOutput) -> bool: if self.msg.total_collateral is None: return True @@ -1067,9 +1079,7 @@ def _should_show_collateral_return_init( return True - def _should_show_collateral_return_tokens( - self, output: messages.CardanoTxOutput - ) -> bool: + def _should_show_collateral_return_tokens(self, output: CardanoTxOutput) -> bool: if self._is_simple_change_output(output): return False @@ -1082,7 +1092,7 @@ async def _process_reference_inputs( ) -> None: for _ in range(self.msg.reference_inputs_count): reference_input: messages.CardanoTxReferenceInput = await self.ctx.call( - messages.CardanoTxItemAck(), messages.CardanoTxReferenceInput + CardanoTxItemAck(), messages.CardanoTxReferenceInput ) self._validate_reference_input(reference_input) await self._show_if_showing_details( @@ -1096,13 +1106,13 @@ def _validate_reference_input( self, reference_input: messages.CardanoTxReferenceInput ) -> None: if len(reference_input.prev_hash) != INPUT_PREV_HASH_SIZE: - raise wire.ProcessError("Invalid reference input") + raise ProcessError("Invalid reference input") # witness requests async def _process_witness_requests(self, tx_hash: bytes) -> CardanoTxResponseType: - response: CardanoTxResponseType = messages.CardanoTxItemAck() - + response: CardanoTxResponseType = CardanoTxItemAck() + confirmed = False for _ in range(self.msg.witness_requests_count): witness_request = await self.ctx.call( response, messages.CardanoTxWitnessRequest @@ -1110,6 +1120,9 @@ async def _process_witness_requests(self, tx_hash: bytes) -> CardanoTxResponseTy self._validate_witness_request(witness_request) path = witness_request.path await self._show_witness_request(path) + if not confirmed: + await confirm_final(self.ctx, "Cardano") + confirmed = True if seed.is_byron_path(path): response = self._get_byron_witness(path, tx_hash) else: @@ -1132,7 +1145,7 @@ async def _show_witness_request( def _assert_tx_init_cond(self, condition: bool) -> None: if not condition: - raise wire.ProcessError("Invalid tx signing request") + raise ProcessError("Invalid tx signing request") def _is_network_id_verifiable(self) -> bool: """ @@ -1150,7 +1163,7 @@ def _is_network_id_verifiable(self) -> bool: or self.msg.withdrawals_count != 0 ) - def _get_output_address(self, output: messages.CardanoTxOutput) -> bytes: + def _get_output_address(self, output: CardanoTxOutput) -> bytes: if output.address_parameters: return addresses.derive_bytes( self.keychain, @@ -1162,9 +1175,7 @@ def _get_output_address(self, output: messages.CardanoTxOutput) -> bytes: assert output.address is not None # _validate_output return addresses.get_bytes_unsafe(output.address) - def _get_output_address_type( - self, output: messages.CardanoTxOutput - ) -> CardanoAddressType: + def _get_output_address_type(self, output: CardanoTxOutput) -> CardanoAddressType: if output.address_parameters: return output.address_parameters.address_type assert output.address is not None # _validate_output @@ -1173,6 +1184,8 @@ def _get_output_address_type( def _derive_withdrawal_address_bytes( self, withdrawal: messages.CardanoTxWithdrawal ) -> bytes: + from trezor.enums import CardanoAddressType + reward_address_type = ( CardanoAddressType.REWARD if withdrawal.path or withdrawal.key_hash @@ -1192,18 +1205,18 @@ def _derive_withdrawal_address_bytes( def _get_chunks_count(self, data_size: int) -> int: assert data_size > 0 - return (data_size - 1) // MAX_CHUNK_SIZE + 1 + return (data_size - 1) // _MAX_CHUNK_SIZE + 1 def _validate_chunk( self, chunk_data: bytes, chunk_number: int, chunks_count: int, - error: wire.ProcessError, + error: ProcessError, ) -> None: - if chunk_number < chunks_count - 1 and len(chunk_data) != MAX_CHUNK_SIZE: + if chunk_number < chunks_count - 1 and len(chunk_data) != _MAX_CHUNK_SIZE: raise error - if chunk_number == chunks_count - 1 and len(chunk_data) > MAX_CHUNK_SIZE: + if chunk_number == chunks_count - 1 and len(chunk_data) > _MAX_CHUNK_SIZE: raise error def _get_byron_witness( @@ -1227,6 +1240,8 @@ def _get_shelley_witness( ) def _sign_tx_hash(self, tx_body_hash: bytes, path: list[int]) -> bytes: + from trezor.crypto.curve import ed25519 + node = self.keychain.derive(path) return ed25519.sign_ext( node.private_key(), node.private_key_ext(), tx_body_hash @@ -1240,7 +1255,7 @@ async def _fail_or_warn_if_invalid_path( async def _fail_or_warn_path(self, path: list[int], path_name: str) -> None: if safety_checks.is_strict(): - raise wire.DataError(f"Invalid {path_name.lower()}") + raise DataError(f"Invalid {path_name.lower()}") else: await layout.warn_path(self.ctx, path, path_name) @@ -1251,10 +1266,10 @@ def _fail_if_strict_and_unusual( return if Credential.payment_credential(address_parameters).is_unusual_path: - raise wire.DataError(f"Invalid {CHANGE_OUTPUT_PATH_NAME.lower()}") + raise DataError("Invalid change output path") if Credential.stake_credential(address_parameters).is_unusual_path: - raise wire.DataError(f"Invalid {CHANGE_OUTPUT_STAKING_PATH_NAME.lower()}") + raise DataError("Invalid change output staking path") async def _show_if_showing_details(self, layout_fn: Awaitable) -> None: if self.should_show_details: diff --git a/core/src/apps/common/cbor.py b/core/src/apps/common/cbor.py index dd90a4ba12..5fe4b7edc5 100644 --- a/core/src/apps/common/cbor.py +++ b/core/src/apps/common/cbor.py @@ -2,13 +2,13 @@ Minimalistic CBOR implementation, supports only what we need in cardano. """ -import ustruct as struct from micropython import const from typing import TYPE_CHECKING -from trezor import log, utils +from trezor.utils import BufferReader -from . import readers +if __debug__: + from trezor import log if TYPE_CHECKING: from typing import Any, Generic, Iterator, TypeVar @@ -44,20 +44,26 @@ _CBOR_TRUE = const(0x15) _CBOR_NULL = const(0x16) _CBOR_BREAK = const(0x1F) -_CBOR_RAW_TAG = const(0x18) + + +# See https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml +_CBOR_RAW_TAG = const(0x18) # Tag 24 +_CBOR_SET_TAG = const(0x102) # Tag 258 def _header(typ: int, l: int) -> bytes: + from ustruct import pack + if l < 24: - return struct.pack(">B", typ + l) + return pack(">B", typ + l) elif l < 2**8: - return struct.pack(">BB", typ + 24, l) + return pack(">BB", typ + 24, l) elif l < 2**16: - return struct.pack(">BH", typ + 25, l) + return pack(">BH", typ + 25, l) elif l < 2**32: - return struct.pack(">BI", typ + 26, l) + return pack(">BI", typ + 26, l) elif l < 2**64: - return struct.pack(">BQ", typ + 27, l) + return pack(">BQ", typ + 27, l) else: raise NotImplementedError # Length not supported @@ -117,7 +123,9 @@ def _cbor_encode(value: Value) -> Iterator[bytes]: raise NotImplementedError -def _read_length(r: utils.BufferReader, aux: int) -> int: +def _read_length(r: BufferReader, aux: int) -> int: + from . import readers + if aux < _CBOR_UINT8_FOLLOWS: return aux elif aux == _CBOR_UINT8_FOLLOWS: @@ -132,7 +140,7 @@ def _read_length(r: utils.BufferReader, aux: int) -> int: raise NotImplementedError # Length not supported -def _cbor_decode(r: utils.BufferReader) -> Value: +def _cbor_decode(r: BufferReader) -> Value: fb = r.get() fb_type = fb & _CBOR_TYPE_MASK fb_aux = fb & _CBOR_INFO_BITS @@ -271,40 +279,9 @@ def encode_streamed(value: Value) -> Iterator[bytes]: return _cbor_encode(value) -def encode_chunked(value: Value, max_chunk_size: int) -> Iterator[bytes]: - """ - Returns the encoded value as an iterable of chunks of a given size, - removing the need to reserve a continuous chunk of memory for the - full serialized representation of the value. - """ - if max_chunk_size <= 0: - raise ValueError - - chunks = encode_streamed(value) - - chunk_buffer = utils.empty_bytearray(max_chunk_size) - try: - current_chunk_view = utils.BufferReader(next(chunks)) - while True: - num_bytes_to_write = min( - current_chunk_view.remaining_count(), - max_chunk_size - len(chunk_buffer), - ) - chunk_buffer.extend(current_chunk_view.read(num_bytes_to_write)) - - if len(chunk_buffer) >= max_chunk_size: - yield chunk_buffer - chunk_buffer[:] = bytes() - - if current_chunk_view.remaining_count() == 0: - current_chunk_view = utils.BufferReader(next(chunks)) - except StopIteration: - if len(chunk_buffer) > 0: - yield chunk_buffer - - def decode(cbor: bytes, offset: int = 0) -> Value: - r = utils.BufferReader(cbor) + + r = BufferReader(cbor) r.seek(offset) res = _cbor_decode(r) if r.remaining_count(): @@ -328,6 +305,10 @@ def create_embedded_cbor_bytes_header(size: int) -> bytes: return _header(_CBOR_TAG, _CBOR_RAW_TAG) + _header(_CBOR_BYTE_STRING, size) +def create_tagged_set_header(size: int) -> bytes: + return _header(_CBOR_TAG, _CBOR_SET_TAG) + _header(_CBOR_ARRAY, size) + + def precedes(prev: bytes, curr: bytes) -> bool: """ Returns True if `prev` is smaller than `curr` with regards to diff --git a/core/src/apps/common/keychain.py b/core/src/apps/common/keychain.py index 22ae6f8156..c84c91efe5 100644 --- a/core/src/apps/common/keychain.py +++ b/core/src/apps/common/keychain.py @@ -101,11 +101,11 @@ def __del__(self) -> None: del self._cache del self.seed - def verify_path(self, path: paths.Bip32Path) -> None: + def verify_path(self, path: paths.Bip32Path, force_strict: bool = True) -> None: if "ed25519" in self.curve and not paths.path_is_hardened(path): raise wire.DataError("Non-hardened paths unsupported on Ed25519") - if not safety_checks.is_strict(): + if not safety_checks.is_strict() and not force_strict: return if self.is_in_keychain(path): @@ -139,17 +139,17 @@ def root_fingerprint(self) -> int: n = self._derive_with_cache( prefix_len=0, path=[0 | paths.HARDENED], - new_root=lambda: bip32.from_seed(self.seed, self.curve), + new_root=lambda: bip32.from_seed(self.seed, self.curve), # type: ignore[Argument of type "() -> HDNode" cannot be assigned to parameter "new_root" of type "() -> NodeType@_derive_with_cache" in function "_derive_with_cache"] ) - self._root_fingerprint = n.fingerprint() + self._root_fingerprint = n.fingerprint() # type: ignore[Cannot access member "fingerprint" for type "NodeProtocol[Unknown]*"] return self._root_fingerprint - def derive(self, path: paths.Bip32Path) -> bip32.HDNode: - self.verify_path(path) - return self._derive_with_cache( + def derive(self, path: paths.Bip32Path, force_strict: bool = True) -> bip32.HDNode: + self.verify_path(path, force_strict) + return self._derive_with_cache( # type: ignore[Expression of type "NodeType@_derive_with_cache" cannot be assigned to return type "HDNode"] prefix_len=3, path=path, - new_root=lambda: bip32.from_seed(self.seed, self.curve), + new_root=lambda: bip32.from_seed(self.seed, self.curve), # type: ignore[Argument of type "() -> HDNode" cannot be assigned to parameter "new_root" of type "() -> NodeType@_derive_with_cache" in function "_derive_with_cache"] ) def derive_slip21(self, path: paths.Slip21Path) -> Slip21Node: @@ -222,6 +222,8 @@ def auto_keychain( pattern = getattr(parent_module, "PATTERN") curve = getattr(parent_module, "CURVE") slip44_id = getattr(parent_module, "SLIP44_ID") + + pattern = pattern if isinstance(pattern, tuple) else (pattern,) return with_slip44_keychain( - pattern, slip44_id=slip44_id, curve=curve, allow_testnet=allow_testnet + *pattern, slip44_id=slip44_id, curve=curve, allow_testnet=allow_testnet ) diff --git a/core/src/apps/common/paths.py b/core/src/apps/common/paths.py index 952b145bf3..77c880e757 100644 --- a/core/src/apps/common/paths.py +++ b/core/src/apps/common/paths.py @@ -5,19 +5,11 @@ SLIP25_PURPOSE = const(10025 | HARDENED) if TYPE_CHECKING: - from typing import ( - Any, - Callable, - Collection, - Container, - Iterable, - Sequence, - TypeVar, - ) - from typing_extensions import Protocol + from typing import Any, Callable, Collection, Container, Iterable, Sequence, TypeVar from trezor import wire + from typing_extensions import Protocol - Bip32Path = Sequence[int] + Bip32Path = list[int] Slip21Path = Sequence[bytes] PathType = TypeVar("PathType", Bip32Path, Slip21Path, contravariant=True) @@ -29,7 +21,7 @@ class KeychainValidatorType(Protocol): def is_in_keychain(self, path: Bip32Path) -> bool: ... - def verify_path(self, path: Bip32Path) -> None: + def verify_path(self, path: Bip32Path, force_strict: bool = True) -> None: ... @@ -197,23 +189,24 @@ def parse(cls, pattern: str, slip44_id: int | Iterable[int]) -> "PathSchema": # optionally replace a keyword component = cls.REPLACEMENTS.get(component, component) + append = schema.append # local_cache_attribute if "-" in component: # parse as a range a, b = [parse(s) for s in component.split("-", 1)] - schema.append(Interval(a, b)) + append(Interval(a, b)) elif "," in component: # parse as a list of values - schema.append(set(parse(s) for s in component.split(","))) + append(set(parse(s) for s in component.split(","))) elif component == "coin_type": # substitute SLIP-44 ids - schema.append(set(parse(s) for s in slip44_id)) + append(set(parse(s) for s in slip44_id)) else: # plain constant - schema.append((parse(component),)) + append((parse(component),)) return cls(schema, trailing_components, compact=True) @@ -258,18 +251,19 @@ def restrict(self, path: Bip32Path) -> bool: path. If the restriction results in a never-matching schema, then False is returned. """ + schema = self.schema # local_cache_attribute for i, value in enumerate(path): - if i < len(self.schema): + if i < len(schema): # Ensure that the path is a prefix of the schema. - if value not in self.schema[i]: + if value not in schema[i]: self.set_never_matching() return False # Restrict the schema component if there are multiple choices. - component = self.schema[i] + component = schema[i] if not isinstance(component, tuple) or len(component) != 1: - self.schema[i] = (value,) + schema[i] = (value,) else: # The path is longer than the schema. We need to restrict the # trailing components. @@ -278,7 +272,7 @@ def restrict(self, path: Bip32Path) -> bool: self.set_never_matching() return False - self.schema.append((value,)) + schema.append((value,)) return True @@ -286,15 +280,13 @@ def restrict(self, path: Bip32Path) -> bool: def __repr__(self) -> str: components = ["m"] - - def unharden(item: int) -> int: - return item ^ (item & HARDENED) + append = components.append # local_cache_attribute for component in self.schema: if isinstance(component, Interval): a, b = component.min, component.max prime = "'" if a & HARDENED else "" - components.append(f"[{unharden(a)}-{unharden(b)}]{prime}") + append(f"[{unharden(a)}-{unharden(b)}]{prime}") else: # typechecker thinks component is a Contanier but we're using it # as a Collection. @@ -307,15 +299,15 @@ def unharden(item: int) -> int: component_str = "[" + component_str + "]" if next(iter(collection)) & HARDENED: component_str += "'" - components.append(component_str) + append(component_str) if self.trailing_components: for key, val in self.WILDCARD_RANGES.items(): if self.trailing_components is val: - components.append(key) + append(key) break else: - components.append("???") + append("???") return "" @@ -326,12 +318,6 @@ def match(path: Bip32Path) -> bool: return True -class NeverMatchingSchema: - @staticmethod - def match(path: Bip32Path) -> bool: - return False - - # BIP-44 for basic (legacy) Bitcoin accounts, and widely used for other currencies: # https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki PATTERN_BIP44 = "m/44'/coin_type'/account'/change/*" @@ -353,9 +339,10 @@ async def validate_path( ctx: wire.Context, keychain: KeychainValidatorType, path: Bip32Path, + force_strict: bool = True, *additional_checks: bool, ) -> None: - keychain.verify_path(path) + keychain.verify_path(path, force_strict) if not keychain.is_in_keychain(path) or not all(additional_checks): await show_path_warning(ctx, path) @@ -375,7 +362,7 @@ def path_is_hardened(address_n: Bip32Path) -> bool: def address_n_to_str(address_n: Iterable[int]) -> str: - def path_item(i: int) -> str: + def _path_item(i: int) -> str: if i & HARDENED: return str(i ^ HARDENED) + "'" else: @@ -384,4 +371,65 @@ def path_item(i: int) -> str: if not address_n: return "m" - return "m/" + "/".join(path_item(i) for i in address_n) + return "m/" + "/".join(_path_item(i) for i in address_n) + + +def parse_path(path: str) -> list[int]: + def _parse_path_item(item: str) -> int: + if item.endswith("'") or item.endswith("h") or item.endswith("H"): + return HARDENED | int(item[:-1]) + else: + return int(item) + + if not path: + return [] + if path.startswith("m/"): + path = path[2:] + return [_parse_path_item(item) for item in path.split("/")] + + +def unharden(item: int) -> int: + return item ^ (item & HARDENED) + + +def get_account_name( + coin: str, address_n: Bip32Path, pattern: str | Sequence[str], slip44_id: int +) -> str | None: + account_num = _get_account_num(address_n, pattern, slip44_id) + if account_num is None: + return None + return f"{coin} #{account_num}" + + +def _get_account_num( + address_n: Bip32Path, pattern: str | Sequence[str], slip44_id: int +) -> int | None: + if isinstance(pattern, str): + pattern = [pattern] + + # Trying all possible patterns - at least one should match + for patt in pattern: + try: + return _get_account_num_single(address_n, patt, slip44_id) + except ValueError: + pass + + # This function should not raise + return None + + +def _get_account_num_single(address_n: Bip32Path, pattern: str, slip44_id: int) -> int: + # Validating address_n is compatible with pattern + if not PathSchema.parse(pattern, slip44_id).match(address_n): + raise ValueError + + account_pos = pattern.find("/account") + if account_pos >= 0: + i = pattern.count("/", 0, account_pos) + num = address_n[i] + if is_hardened(num): + return unharden(num) + 1 + else: + return num + 1 + else: + raise ValueError diff --git a/core/src/apps/common/signverify.py b/core/src/apps/common/signverify.py index 38c1b2b575..eacd191607 100644 --- a/core/src/apps/common/signverify.py +++ b/core/src/apps/common/signverify.py @@ -27,8 +27,15 @@ def message_digest(coin: CoinInfo, message: bytes) -> bytes: return ret +def is_non_printable(message: str) -> bool: + return any(ord(c) < 32 or ord(c) == 127 for c in message) + + def decode_message(message: bytes) -> str: try: - return bytes(message).decode() + decoded_message = bytes(message).decode() + if is_non_printable(decoded_message): + return f"0x{hexlify(message).decode()}" + return decoded_message except UnicodeError: return f"0x{hexlify(message).decode()}" diff --git a/core/src/apps/ethereum/get_address.py b/core/src/apps/ethereum/get_address.py index 148457f3b7..e08752ffcc 100644 --- a/core/src/apps/ethereum/get_address.py +++ b/core/src/apps/ethereum/get_address.py @@ -21,9 +21,9 @@ async def get_address( ctx: Context, msg: EthereumGetAddress, keychain: Keychain, defs: Definitions ) -> EthereumAddress: - await paths.validate_path(ctx, keychain, msg.address_n) + await paths.validate_path(ctx, keychain, msg.address_n, force_strict=False) - node = keychain.derive(msg.address_n) + node = keychain.derive(msg.address_n, force_strict=False) address = address_from_bytes(node.ethereum_pubkeyhash(), defs.network) if msg.show_display: diff --git a/core/src/apps/ethereum/networks.py b/core/src/apps/ethereum/networks.py index f1b597c826..dae1a60ffa 100644 --- a/core/src/apps/ethereum/networks.py +++ b/core/src/apps/ethereum/networks.py @@ -120,7 +120,7 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: 6, # chain_id 1, # slip44 "tKOT", # symbol - "Ethereum Classic Testnet Kotti", # name + "Kotti Testnet", # name "evm-tkot.png", # name 0x969696, # primary_color ) @@ -200,7 +200,7 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: 16, # chain_id 1, # slip44 "tCFLR", # symbol - "Flare Testnet Coston", # name + "Songbird Testnet Coston", # name "evm-tcflr.png", # name 0x969696, # primary_color ) @@ -264,7 +264,7 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: 30, # chain_id 137, # slip44 "RBTC", # symbol - "RSK", # name + "Rootstock", # name "evm-rbtc.png", # name 0xD2D2D2, # primary_color ) @@ -272,7 +272,7 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: 31, # chain_id 1, # slip44 "tRBTC", # symbol - "RSK Testnet", # name + "Rootstock Testnet", # name "evm-trbtc.png", # name 0x969696, # primary_color ) @@ -312,7 +312,7 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: 44, # chain_id 60, # slip44 "CRAB", # symbol - "Darwinia Crab Network", # name + "Crab Network", # name "evm-crab.png", # name 0xD2D2D2, # primary_color ) @@ -320,7 +320,7 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: 50, # chain_id 60, # slip44 "XDC", # symbol - "XinFin XDC Network", # name + "XDC Network", # name "evm-xdc.png", # name 0xD2D2D2, # primary_color ) @@ -384,7 +384,7 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: 62, # chain_id 1, # slip44 "TETC", # symbol - "Ethereum Classic Testnet Morden", # name + "Morden Testnet", # name "evm-tetc.png", # name 0x969696, # primary_color ) @@ -392,7 +392,7 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: 63, # chain_id 1, # slip44 "tMETC", # symbol - "Ethereum Classic Testnet Mordor", # name + "Mordor Testnet", # name "evm-tmetc.png", # name 0x969696, # primary_color ) @@ -479,9 +479,9 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: yield ( 88, # chain_id 889, # slip44 - "TOMO", # symbol - "TomoChain", # name - "evm-tomo.png", # name + "VIC", # symbol + "Viction", # name + "evm-vic.png", # name 0xD2D2D2, # primary_color ) yield ( @@ -607,7 +607,7 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: yield ( 137, # chain_id 966, # slip44 - "MATIC", # symbol + "POL", # symbol "Polygon", # name "evm-matic.png", # name 0x8247E5, # primary_color @@ -620,6 +620,14 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: "evm-dax.png", # name 0xD2D2D2, # primary_color ) + yield ( + 146, # chain_id + 60, # slip44 + "S", # symbol + "Sonic", # name + "evm-s.png", # name + 0xFFFFFF, # primary_color + ) yield ( 162, # chain_id 1, # slip44 @@ -636,6 +644,14 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: "evm-pht.png", # name 0xD2D2D2, # primary_color ) + yield ( + 177, # chain_id + 60, # slip44 + "HSK", # symbol + "HashKey Chain", # name + "evm-hsk.png", # name + 0xFFFFFF, # primary_color + ) yield ( 186, # chain_id 60, # slip44 @@ -760,7 +776,7 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: 324, # chain_id 60, # slip44 "ETH", # symbol - "zkSync Era", # name + "zkSync", # name "evm-zksync-era.png", # name 0xFFFFFF, # primary_color ) @@ -919,9 +935,9 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: yield ( 1001, # chain_id 1, # slip44 - "tKLAY", # symbol - "Klaytn Testnet Baobab", # name - "evm-tklay.png", # name + "tKAIA", # symbol + "Kaia Kairos Testnet", # name + "evm-tkaia.png", # name 0x969696, # primary_color ) yield ( @@ -1054,7 +1070,7 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: ) yield ( 1287, # chain_id - 60, # slip44 + 1, # slip44 "DEV", # symbol "Moonbase Alpha", # name "evm-dev.png", # name @@ -1215,9 +1231,9 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: yield ( 8217, # chain_id 8217, # slip44 - "KLAY", # symbol - "Klaytn", # name - "evm-klay.png", # name + "KAIA", # symbol + "Kaia", # name + "evm-kaia.png", # name 0xD2D2D2, # primary_color ) yield ( @@ -1308,6 +1324,14 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: "evm-mtt.png", # name 0xD2D2D2, # primary_color ) + yield ( + 19515, # chain_id + 1, # slip44 + "tSEP", # symbol + "SEC Testnet", # name + "evm-tsep.png", # name + 0x969696, # primary_color + ) yield ( 19845, # chain_id 60, # slip44 @@ -1448,7 +1472,7 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: 63000, # chain_id 60, # slip44 "ECS", # symbol - "eCredits", # name + "eSync Network", # name "evm-ecs.png", # name 0xD2D2D2, # primary_color ) @@ -1470,7 +1494,7 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: ) yield ( 78110, # chain_id - 60, # slip44 + 1, # slip44 "FIN", # symbol "Firenze test network", # name "evm-fin.png", # name @@ -1828,3 +1852,11 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: "evm-mole.png", # name 0xD2D2D2, # primary_color ) + yield ( + 9798, # chain_id + 9798, # slip44 + "DTT", # symbol + "Data Trade Chain", # name + "evm-dtt.png", # icon + 0x1A2A5F, # primary_color + ) diff --git a/core/src/apps/ethereum/networks.py.mako b/core/src/apps/ethereum/networks.py.mako index be7558f02b..ca605409b3 100644 --- a/core/src/apps/ethereum/networks.py.mako +++ b/core/src/apps/ethereum/networks.py.mako @@ -86,3 +86,11 @@ def _networks_iterator() -> Iterator[NetworkInfoTuple]: ${n.primary_color}, # primary_color ) % endfor + yield ( + 9798, # chain_id + 9798, # slip44 + "DTT", # symbol + "Data Trade Chain", # name + "evm-dtt.png", # icon + 0x1A2A5F, # primary_color + ) diff --git a/core/src/apps/ethereum/onekey/get_address.py b/core/src/apps/ethereum/onekey/get_address.py index 9d29c563e2..2ec1356aba 100644 --- a/core/src/apps/ethereum/onekey/get_address.py +++ b/core/src/apps/ethereum/onekey/get_address.py @@ -20,9 +20,9 @@ async def get_address( ctx: Context, msg: EthereumGetAddress, keychain: Keychain ) -> EthereumAddress: - await paths.validate_path(ctx, keychain, msg.address_n) + await paths.validate_path(ctx, keychain, msg.address_n, force_strict=False) - node = keychain.derive(msg.address_n) + node = keychain.derive(msg.address_n, force_strict=False) if msg.chain_id: network = networks.by_chain_id(msg.chain_id) diff --git a/core/src/apps/ethereum/onekey/get_public_key.py b/core/src/apps/ethereum/onekey/get_public_key.py index eb025995e8..eaf8994a03 100644 --- a/core/src/apps/ethereum/onekey/get_public_key.py +++ b/core/src/apps/ethereum/onekey/get_public_key.py @@ -20,8 +20,8 @@ async def get_public_key( ctx: Context, msg: EthereumGetPublicKey, keychain: Keychain ) -> EthereumPublicKey: - await paths.validate_path(ctx, keychain, msg.address_n) - node = keychain.derive(msg.address_n) + await paths.validate_path(ctx, keychain, msg.address_n, force_strict=False) + node = keychain.derive(msg.address_n, force_strict=False) # we use the Bitcoin format for Ethereum xpubs btc = coins.by_name("Bitcoin") diff --git a/core/src/apps/ethereum/onekey/sign_message.py b/core/src/apps/ethereum/onekey/sign_message.py index ac0bba8718..dad942407e 100644 --- a/core/src/apps/ethereum/onekey/sign_message.py +++ b/core/src/apps/ethereum/onekey/sign_message.py @@ -25,9 +25,9 @@ async def sign_message( ctx: Context, msg: EthereumSignMessage, keychain: Keychain ) -> EthereumMessageSignature: validate_message(msg.message) - await paths.validate_path(ctx, keychain, msg.address_n) + await paths.validate_path(ctx, keychain, msg.address_n, force_strict=False) - node = keychain.derive(msg.address_n) + node = keychain.derive(msg.address_n, force_strict=False) address = address_from_bytes(node.ethereum_pubkeyhash()) if msg.chain_id: diff --git a/core/src/apps/ethereum/onekey/sign_tx.py b/core/src/apps/ethereum/onekey/sign_tx.py index a5f917bcf8..c06a7ec59a 100644 --- a/core/src/apps/ethereum/onekey/sign_tx.py +++ b/core/src/apps/ethereum/onekey/sign_tx.py @@ -46,7 +46,7 @@ async def sign_tx( ctx: wire.Context, msg: EthereumSignTx, keychain: Keychain ) -> EthereumTxRequest: check(msg) - await paths.validate_path(ctx, keychain, msg.address_n) + await paths.validate_path(ctx, keychain, msg.address_n, force_strict=False) # Handle ERC20s token, address_bytes, recipient, value = await handle_erc20(ctx, msg) @@ -86,7 +86,7 @@ async def sign_tx( if token is None and token_id is None and msg.data_length > 0: has_raw_data = True # await require_confirm_data(ctx, msg.data_initial_chunk, data_total) - node = keychain.derive(msg.address_n) + node = keychain.derive(msg.address_n, force_strict=False) recipient_str = address_from_bytes(recipient, network) from_str = address_from_bytes(from_addr or node.ethereum_pubkeyhash(), network) await require_confirm_fee( @@ -255,7 +255,7 @@ async def send_request_chunk(ctx: wire.Context, data_left: int) -> EthereumTxAck def sign_digest( msg: EthereumSignTx, keychain: Keychain, digest: bytes ) -> EthereumTxRequest: - node = keychain.derive(msg.address_n) + node = keychain.derive(msg.address_n, force_strict=False) signature = secp256k1.sign( node.private_key(), digest, False, secp256k1.CANONICAL_SIG_ETHEREUM ) diff --git a/core/src/apps/ethereum/onekey/sign_tx_eip1559.py b/core/src/apps/ethereum/onekey/sign_tx_eip1559.py index d4ffbb305c..77d2bd7cce 100644 --- a/core/src/apps/ethereum/onekey/sign_tx_eip1559.py +++ b/core/src/apps/ethereum/onekey/sign_tx_eip1559.py @@ -68,7 +68,7 @@ async def sign_tx_eip1559( ) -> EthereumTxRequest: check(msg) - await paths.validate_path(ctx, keychain, msg.address_n) + await paths.validate_path(ctx, keychain, msg.address_n, force_strict=False) # Handle ERC20s token, address_bytes, recipient, value = await handle_erc20(ctx, msg) @@ -106,7 +106,7 @@ async def sign_tx_eip1559( if token is None and token_id is None and msg.data_length > 0: has_raw_data = True # await require_confirm_data(ctx, msg.data_initial_chunk, data_total) - node = keychain.derive(msg.address_n) + node = keychain.derive(msg.address_n, force_strict=False) recipient_str = address_from_bytes(recipient, network) from_str = address_from_bytes(from_addr or node.ethereum_pubkeyhash(), network) @@ -198,7 +198,7 @@ def get_total_length(msg: EthereumSignTxEIP1559, data_total: int) -> int: def sign_digest( msg: EthereumSignTxEIP1559, keychain: Keychain, digest: bytes ) -> EthereumTxRequest: - node = keychain.derive(msg.address_n) + node = keychain.derive(msg.address_n, force_strict=False) signature = secp256k1.sign( node.private_key(), digest, False, secp256k1.CANONICAL_SIG_ETHEREUM ) diff --git a/core/src/apps/ethereum/onekey/sign_typed_data.py b/core/src/apps/ethereum/onekey/sign_typed_data.py index 662e197ba3..ac3b447769 100644 --- a/core/src/apps/ethereum/onekey/sign_typed_data.py +++ b/core/src/apps/ethereum/onekey/sign_typed_data.py @@ -47,7 +47,7 @@ async def sign_typed_data( ctx: Context, msg: EthereumSignTypedData, keychain: Keychain ) -> EthereumTypedDataSignature: - await paths.validate_path(ctx, keychain, msg.address_n) + await paths.validate_path(ctx, keychain, msg.address_n, force_strict=False) if msg.chain_id: network = networks.by_chain_id(msg.chain_id) @@ -64,7 +64,7 @@ async def sign_typed_data( ctx, msg.primary_type, msg.metamask_v4_compat ) - node = keychain.derive(msg.address_n) + node = keychain.derive(msg.address_n, force_strict=False) signature = secp256k1.sign( node.private_key(), data_hash, False, secp256k1.CANONICAL_SIG_ETHEREUM ) @@ -90,7 +90,9 @@ async def generate_typed_data_hash( metamask_v4_compat=metamask_v4_compat, ) await typed_data_envelope.collect_types() + from ..sign_typed_data import show_eip712_warning + await show_eip712_warning(ctx, primary_type) await confirm_domain(ctx, typed_data_envelope) domain_separator = await typed_data_envelope.hash_struct( primary_type="EIP712Domain", diff --git a/core/src/apps/ethereum/onekey/sign_typed_data_hash.py b/core/src/apps/ethereum/onekey/sign_typed_data_hash.py index ef444e249c..c304fa5b45 100644 --- a/core/src/apps/ethereum/onekey/sign_typed_data_hash.py +++ b/core/src/apps/ethereum/onekey/sign_typed_data_hash.py @@ -24,7 +24,7 @@ async def sign_typed_data_hash( ctx: Context, msg: EthereumSignTypedHash, keychain: Keychain ) -> EthereumTypedDataSignature: - await paths.validate_path(ctx, keychain, msg.address_n) + await paths.validate_path(ctx, keychain, msg.address_n, force_strict=False) if msg.chain_id: network = networks.by_chain_id(msg.chain_id) @@ -46,7 +46,7 @@ async def sign_typed_data_hash( await confirm_typed_hash_final(ctx) - node = keychain.derive(msg.address_n) + node = keychain.derive(msg.address_n, force_strict=False) signature = secp256k1.sign( node.private_key(), data_hash, False, secp256k1.CANONICAL_SIG_ETHEREUM ) diff --git a/core/src/apps/ethereum/sign_message.py b/core/src/apps/ethereum/sign_message.py index 98f3c9ff3e..34593c9509 100644 --- a/core/src/apps/ethereum/sign_message.py +++ b/core/src/apps/ethereum/sign_message.py @@ -36,9 +36,9 @@ async def sign_message( ctx: Context, msg: EthereumSignMessage, keychain: Keychain, defs: Definitions ) -> EthereumMessageSignature: validate_message(msg.message) - await paths.validate_path(ctx, keychain, msg.address_n) + await paths.validate_path(ctx, keychain, msg.address_n, force_strict=False) - node = keychain.derive(msg.address_n) + node = keychain.derive(msg.address_n, force_strict=False) address = address_from_bytes(node.ethereum_pubkeyhash()) network = defs.network diff --git a/core/src/apps/ethereum/sign_tx.py b/core/src/apps/ethereum/sign_tx.py index b24f45fedd..7691d61045 100644 --- a/core/src/apps/ethereum/sign_tx.py +++ b/core/src/apps/ethereum/sign_tx.py @@ -43,7 +43,7 @@ async def sign_tx( ctx: wire.Context, msg: EthereumSignTx, keychain: Keychain, defs: Definitions ) -> EthereumTxRequest: check(msg) - await paths.validate_path(ctx, keychain, msg.address_n) + await paths.validate_path(ctx, keychain, msg.address_n, force_strict=False) # Handle ERC20s token, address_bytes, recipient, value = await handle_erc20(ctx, msg) @@ -77,7 +77,7 @@ async def sign_tx( if token is None and token_id is None and msg.data_length > 0: has_raw_data = True # await require_confirm_data(ctx, msg.data_initial_chunk, data_total) - node = keychain.derive(msg.address_n) + node = keychain.derive(msg.address_n, force_strict=False) recipient_str = address_from_bytes(recipient, network) from_str = address_from_bytes(from_addr or node.ethereum_pubkeyhash(), network) await require_confirm_fee( @@ -246,7 +246,7 @@ async def send_request_chunk(ctx: wire.Context, data_left: int) -> EthereumTxAck def sign_digest( msg: EthereumSignTx, keychain: Keychain, digest: bytes ) -> EthereumTxRequest: - node = keychain.derive(msg.address_n) + node = keychain.derive(msg.address_n, force_strict=False) signature = secp256k1.sign( node.private_key(), digest, False, secp256k1.CANONICAL_SIG_ETHEREUM ) diff --git a/core/src/apps/ethereum/sign_tx_eip1559.py b/core/src/apps/ethereum/sign_tx_eip1559.py index 796e1f0a86..9c1f9c1801 100644 --- a/core/src/apps/ethereum/sign_tx_eip1559.py +++ b/core/src/apps/ethereum/sign_tx_eip1559.py @@ -66,7 +66,7 @@ async def sign_tx_eip1559( ) -> EthereumTxRequest: check(msg) - await paths.validate_path(ctx, keychain, msg.address_n) + await paths.validate_path(ctx, keychain, msg.address_n, force_strict=False) # Handle ERC20s token, address_bytes, recipient, value = await handle_erc20(ctx, msg) @@ -98,7 +98,7 @@ async def sign_tx_eip1559( if token is None and token_id is None and msg.data_length > 0: has_raw_data = True # await require_confirm_data(ctx, msg.data_initial_chunk, data_total) - node = keychain.derive(msg.address_n) + node = keychain.derive(msg.address_n, force_strict=False) recipient_str = address_from_bytes(recipient, network) from_str = address_from_bytes(from_addr or node.ethereum_pubkeyhash(), network) @@ -190,7 +190,7 @@ def get_total_length(msg: EthereumSignTxEIP1559, data_total: int) -> int: def sign_digest( msg: EthereumSignTxEIP1559, keychain: Keychain, digest: bytes ) -> EthereumTxRequest: - node = keychain.derive(msg.address_n) + node = keychain.derive(msg.address_n, force_strict=False) signature = secp256k1.sign( node.private_key(), digest, False, secp256k1.CANONICAL_SIG_ETHEREUM ) diff --git a/core/src/apps/ethereum/sign_typed_data.py b/core/src/apps/ethereum/sign_typed_data.py index 8f038d6778..38bd581949 100644 --- a/core/src/apps/ethereum/sign_typed_data.py +++ b/core/src/apps/ethereum/sign_typed_data.py @@ -42,12 +42,25 @@ # Maximum data size we support MAX_VALUE_BYTE_SIZE = 1536 # 1.5 KB +HIGH_RISK_PRIMARY_TYPES_PERMIT = ( + "Permit", + "PermitBatch", + "PermitBatchTransferFrom", + "PermitSingle", + "PermitTransferFrom", +) + +HIGH_RISK_PRIMARY_TYPES_ORDER = ( + "Order", + "OrderComponents", +) + @with_keychain_from_path(*PATTERNS_ADDRESS) async def sign_typed_data( ctx: Context, msg: EthereumSignTypedData, keychain: Keychain, defs: Definitions ) -> EthereumTypedDataSignature: - await paths.validate_path(ctx, keychain, msg.address_n) + await paths.validate_path(ctx, keychain, msg.address_n, force_strict=False) network = defs.network ctx.primary_color, ctx.icon_path = get_color_and_icon( @@ -58,7 +71,7 @@ async def sign_typed_data( ctx, msg.primary_type, msg.metamask_v4_compat ) - node = keychain.derive(msg.address_n) + node = keychain.derive(msg.address_n, force_strict=False) signature = secp256k1.sign( node.private_key(), data_hash, False, secp256k1.CANONICAL_SIG_ETHEREUM ) @@ -84,7 +97,7 @@ async def generate_typed_data_hash( metamask_v4_compat=metamask_v4_compat, ) await typed_data_envelope.collect_types() - + await show_eip712_warning(ctx, primary_type) await confirm_domain(ctx, typed_data_envelope) domain_separator = await typed_data_envelope.hash_struct( primary_type="EIP712Domain", @@ -553,3 +566,18 @@ async def confirm_domain(ctx: Context, typed_data_envelope: TypedDataEnvelope) - from .layout import confirm_domain await confirm_domain(ctx, eip712_domain) + + +async def show_eip712_warning(ctx: Context, primary_type: str) -> None: + warning_level = 0 + permit_type = "signTypedData" + if primary_type in HIGH_RISK_PRIMARY_TYPES_PERMIT: + warning_level = 2 + permit_type = "Permit" + elif primary_type in HIGH_RISK_PRIMARY_TYPES_ORDER: + warning_level = 2 + permit_type = "Order" + warning_text = _(i18n_keys.MSG___PERMIT_SIGN_ALERT).format(type=permit_type) + from trezor.ui.layouts.lvgl import confirm_eip712_warning + + await confirm_eip712_warning(ctx, primary_type, warning_level, warning_text) diff --git a/core/src/apps/ethereum/sign_typed_data_hash.py b/core/src/apps/ethereum/sign_typed_data_hash.py index bd40c6da9b..5f8cbff321 100644 --- a/core/src/apps/ethereum/sign_typed_data_hash.py +++ b/core/src/apps/ethereum/sign_typed_data_hash.py @@ -21,7 +21,7 @@ async def sign_typed_data_hash( ctx: Context, msg: EthereumSignTypedHash, keychain: Keychain, defs: Definitions ) -> EthereumTypedDataSignature: - await paths.validate_path(ctx, keychain, msg.address_n) + await paths.validate_path(ctx, keychain, msg.address_n, force_strict=False) network = defs.network ctx.primary_color, ctx.icon_path = get_color_and_icon( @@ -37,7 +37,7 @@ async def sign_typed_data_hash( await confirm_typed_hash_final(ctx) - node = keychain.derive(msg.address_n) + node = keychain.derive(msg.address_n, force_strict=False) signature = secp256k1.sign( node.private_key(), data_hash, False, secp256k1.CANONICAL_SIG_ETHEREUM ) diff --git a/core/src/apps/ethereum/tokens.py b/core/src/apps/ethereum/tokens.py index 71a70294b8..dde0314f39 100644 --- a/core/src/apps/ethereum/tokens.py +++ b/core/src/apps/ethereum/tokens.py @@ -174,6 +174,12 @@ def _token_iterator(chain_id: int) -> Iterator[tuple[bytes, str, int, str]]: 18, "Huobi BTC", ) + yield ( # address, symbol, decimals, name + b"\xe7\xc6\xbf\x46\x9e\x97\xee\xb0\xbf\xb7\x4c\x8d\xbf\xf5\xbd\x47\xd4\xc1\xc9\x8a", + "HSK", + 18, + "HashKey Platform Token", + ) yield ( # address, symbol, decimals, name b"\x6f\x25\x96\x37\xdc\xd7\x4c\x76\x77\x81\xe3\x7b\xc6\x13\x3c\xd6\xa6\x8a\xa1\x61", "HT", @@ -252,6 +258,12 @@ def _token_iterator(chain_id: int) -> Iterator[tuple[bytes, str, int, str]]: 18, "OMGToken", ) + yield ( # address, symbol, decimals, name + b"\x45\x5e\x53\xcb\xb8\x60\x18\xac\x2b\x80\x92\xfd\xcd\x39\xd8\x44\x4a\xff\xc3\xf6", + "POL", + 18, + "Polygon", + ) yield ( # address, symbol, decimals, name b"\x95\xad\x61\xb0\xa1\x50\xd7\x92\x19\xdc\xf6\x4e\x1e\x6c\xc0\x1f\x0b\x64\xc4\xce", "SHIB", @@ -1555,6 +1567,31 @@ def _token_iterator(chain_id: int) -> Iterator[tuple[bytes, str, int, str]]: 18, "(PoS) yearn.finance", ) + if chain_id == 177: # HashKey Chain + yield ( # address, symbol, decimals, name + b"\xf1\xb5\x0e\xd6\x7a\x9e\x2c\xc9\x4a\xd3\xc4\x77\x77\x9e\x2d\x4c\xbf\xff\x90\x29", + "USDT", + 6, + "Tether USD", + ) + yield ( # address, symbol, decimals, name + b"\x61\x19\xca\x49\xa7\x9f\x58\x25\xc8\xb3\x45\xf8\xd7\xac\x36\xb2\x72\x56\x5b\x14", + "WBTC", + 8, + "Wrapped BTC", + ) + yield ( # address, symbol, decimals, name + b"\xef\xd4\xbc\x9a\xfd\x21\x05\x17\x80\x3f\x29\x3a\xba\xbd\x70\x1c\xae\xec\xdf\xd0", + "WETH", + 18, + "Wrapped Ether", + ) + yield ( # address, symbol, decimals, name + b"\xb2\x10\xd2\x12\x0d\x57\xb7\x58\xee\x16\x3c\xff\xb4\x3e\x73\x72\x8c\x47\x1c\xf1", + "WHSK", + 18, + "Wrapped HSK", + ) if chain_id == 250: # FTM yield ( # address, symbol, decimals, name b"\x6a\x07\xa7\x92\xab\x29\x65\xc7\x2a\x5b\x80\x88\xd3\xa0\x69\xa7\xac\x3a\x99\x3b", @@ -1749,6 +1786,61 @@ def _token_iterator(chain_id: int) -> Iterator[tuple[bytes, str, int, str]]: 18, "Shibui", ) + if chain_id == 9798: # Data Trade Chain + yield ( # address, symbol, decimals, name + b"\x8e\x79\x85\x0c\x50\xe5\x25\xeb\x6b\xa6\x3e\x60\x1e\x7b\x41\x88\x8a\x1c\x91\x02", + "BV", + 2, + "BV", + ) + yield ( # address, symbol, decimals, name + b"\x89\x9f\x0b\x9d\x67\xdd\x1b\x83\x3f\xda\xa9\x0c\x8b\x09\xea\x61\x6d\x0e\x9e\x98", + "CNV", + 2, + "CNV", + ) + yield ( # address, symbol, decimals, name + b"\xe8\x95\xc5\x77\xd7\x47\xbb\x5d\xbb\xc1\xf0\x6c\xb4\x4d\x60\x67\x68\x0b\xe4\xbe", + "dBTC", + 8, + "dBTC", + ) + yield ( # address, symbol, decimals, name + b"\x8b\x71\x60\xc1\xe9\xfd\xb6\x89\xa0\x60\xff\x09\x19\xe8\x49\x15\xb0\xdf\xa0\x4a", + "dETH", + 18, + "dETH", + ) + yield ( # address, symbol, decimals, name + b"\x74\x5c\x11\xfb\x47\x83\xbd\x00\xa8\x8a\x0b\x99\x42\x02\x62\xf4\x09\xfa\x8b\xb8", + "DOS", + 2, + "DOS", + ) + yield ( # address, symbol, decimals, name + b"\x36\xe6\x50\x4c\x96\x8f\x5c\x2a\x31\x0b\x6a\xf7\xb9\x7b\xc2\x2c\xdd\x34\x02\xcc", + "dUSDT", + 6, + "dUSDT", + ) + yield ( # address, symbol, decimals, name + b"\xb8\x8a\xd7\x67\xb4\x16\x19\x7e\x62\x93\x9d\xec\x20\x74\x31\xb5\x61\xa9\x38\x3b", + "FEC", + 4, + "FEC", + ) + yield ( # address, symbol, decimals, name + b"\xe5\x2a\x73\x68\x28\xc7\x82\xc2\xa4\xa3\x45\xbb\xe8\x05\x2a\xed\x01\x0f\xc8\x2d", + "HLT", + 2, + "HLT", + ) + yield ( # address, symbol, decimals, name + b"\x6d\x88\x5b\x0b\x37\xc6\x2b\xe0\xc7\x2e\xcd\x6a\x61\xaf\x2b\xff\xf6\x81\x41\x9e", + "STC08375", + 0, + "STC08375", + ) if chain_id == 42161: # Arbitrum yield ( # address, symbol, decimals, name b"\x15\x5f\x0d\xd0\x44\x24\x93\x93\x68\x97\x2f\x4e\x18\x38\x68\x7d\x6a\x83\x11\x51", diff --git a/core/src/apps/polkadot/seed.py b/core/src/apps/polkadot/seed.py index 01d994e228..ffa128f888 100644 --- a/core/src/apps/polkadot/seed.py +++ b/core/src/apps/polkadot/seed.py @@ -36,7 +36,7 @@ def _derive_path(root: bip32.HDNode, path: Bip32Path) -> bip32.HDNode: node.derive_path(path) return node - def verify_path(self, path: Bip32Path) -> None: + def verify_path(self, path: Bip32Path, _force_strict: bool = True) -> None: if not self.is_in_keychain(path): raise wire.DataError("Forbidden key path") diff --git a/core/src/apps/polkadot/sign_tx.py b/core/src/apps/polkadot/sign_tx.py index 027feae44a..9bd551f958 100644 --- a/core/src/apps/polkadot/sign_tx.py +++ b/core/src/apps/polkadot/sign_tx.py @@ -4,7 +4,6 @@ from trezor.ui.layouts import confirm_final from apps.common import paths -from apps.common.keychain import FORBIDDEN_KEY_PATH from . import helper, seed, transaction @@ -14,12 +13,6 @@ async def sign_tx( ctx: wire.Context, msg: PolkadotSignTx, keychain: seed.Keychain ) -> PolkadotSignedTx: await paths.validate_path(ctx, keychain, msg.address_n) - if ( - len(msg.address_n) != 5 - or msg.address_n[0] != 0x8000002C - or msg.address_n[1] != 0x80000162 - ): - raise FORBIDDEN_KEY_PATH node = keychain.derive(msg.address_n) public_key = ed25519.publickey(node.private_key()) diff --git a/core/src/apps/solana/__init__.py b/core/src/apps/solana/__init__.py index 200e7121fc..c53033a6e6 100644 --- a/core/src/apps/solana/__init__.py +++ b/core/src/apps/solana/__init__.py @@ -1,7 +1,9 @@ CURVE = "ed25519" SLIP44_ID = 501 # https://github.com/satoshilabs/slips/blob/master/slip-0010.md -PATTERN = "m/44'/coin_type'/account'/change'" +PATTERN_LEGACY = "m/44'/coin_type'/account'/change'" +PATTERN_LEDGER_LIVE = "m/44'/coin_type'/account'" +PATTERN = (PATTERN_LEGACY, PATTERN_LEDGER_LIVE) PRIMARY_COLOR = 0xC74AE3 ICON = "A:/res/chain-sol.png" diff --git a/core/src/apps/solana/sign_offchain_message.py b/core/src/apps/solana/sign_offchain_message.py new file mode 100644 index 0000000000..a4664b26b3 --- /dev/null +++ b/core/src/apps/solana/sign_offchain_message.py @@ -0,0 +1,136 @@ +from typing import TYPE_CHECKING + +from trezor import wire +from trezor.crypto import base58 +from trezor.crypto.curve import ed25519 +from trezor.enums import SolanaOffChainMessageFormat, SolanaOffChainMessageVersion +from trezor.lvglui.scrs import lv +from trezor.messages import SolanaMessageSignature +from trezor.ui.layouts.lvgl import confirm_sol_message +from trezor.utils import BufferWriter + +from apps.common import paths, seed, writers +from apps.common.keychain import auto_keychain +from apps.common.signverify import decode_message + +from . import ICON, PRIMARY_COLOR +from .publickey import PublicKey + +if TYPE_CHECKING: + from trezor.messages import SolanaSignOffChainMessage + from apps.common.keychain import Keychain + +# The signing domain +_SIGN_DOMAIN = b"\xffsolana offchain" +# The header version, currently only version 0 is introduced in proposal[https://github.com/solana-labs/solana/blob/master/docs/src/proposals/off-chain-message-signing.md] +_ALLOWED_HEADER_VERSIONS = (SolanaOffChainMessageVersion.MESSAGE_VERSION_0,) +# The allowed message formats +_ALLOWED_MESSAGE_FORMATS = ( + SolanaOffChainMessageFormat.V0_RESTRICTED_ASCII, + SolanaOffChainMessageFormat.V0_LIMITED_UTF8, +) +# The number of signers +_SIGNER_COUNT = 1 +# The application domain length +_APPLICATION_DOMAIN_LENGTH = 32 +# The public key length +_PUBLIC_KEY_LENGTH = 32 +# Signing domain + header version + application domain + message format + signer count + signer public key + message length +_PREAMBLE_LENGTH = ( + 16 + 1 + _APPLICATION_DOMAIN_LENGTH + 1 + 1 + _SIGNER_COUNT * _PUBLIC_KEY_LENGTH + 2 +) +# The preamble length for ledger (signing domain + header version + message format + message length) +_PREAMBLE_LENGTH_LEDGER = 16 + 1 + 1 + 2 +# 1232 is the maximum length of the message with the preamble +_MAX_MESSAGE_LENGTH_WITH_PREAMBLE = 1232 +# The maximum length of the message +_MAX_MESSAGE_LENGTH = _MAX_MESSAGE_LENGTH_WITH_PREAMBLE - _PREAMBLE_LENGTH +# The maximum length of the message for ledger +_MAX_MESSAGE_LENGTH_LEDGER = _MAX_MESSAGE_LENGTH_WITH_PREAMBLE - _PREAMBLE_LENGTH_LEDGER + + +@auto_keychain(__name__) +async def sign_offchain_message( + ctx: wire.Context, msg: SolanaSignOffChainMessage, keychain: Keychain +) -> SolanaMessageSignature: + # sanitize message + sanitize_message(msg) + # path validation + await paths.validate_path(ctx, keychain, msg.address_n) + + node = keychain.derive(msg.address_n) + + signer_pub_key_bytes = seed.remove_ed25519_prefix(node.public_key()) + ctx.primary_color, ctx.icon_path = lv.color_hex(PRIMARY_COLOR), ICON + # the application domain displayed to the user is in base58 encoding + if msg.application_domain: + app_domain_fd = base58.encode(msg.application_domain) + else: + app_domain_fd = None + message = decode_message(msg.message) + address = str(PublicKey(signer_pub_key_bytes)) + + # display the message to confirm + await confirm_sol_message(ctx, address, app_domain_fd, message) + # prepare the message + message_to_sign = prepare_message(msg, signer_pub_key_bytes) + signature = ed25519.sign(node.private_key(), message_to_sign) + return SolanaMessageSignature(signature=signature, public_key=signer_pub_key_bytes) + + +def prepare_message( + msg: SolanaSignOffChainMessage, signer_pub_key_bytes: bytes +) -> bytes: + """Prepare the message to be signed.""" + message = msg.message + message_length = len(message) + buffer = bytearray( + message_length + + (_PREAMBLE_LENGTH if msg.application_domain else _PREAMBLE_LENGTH_LEDGER) + ) + bw = BufferWriter(buffer) + writers.write_bytes_fixed(bw, _SIGN_DOMAIN, 16) + writers.write_uint8(bw, msg.message_version) + if msg.application_domain: + writers.write_bytes_fixed( + bw, msg.application_domain, _APPLICATION_DOMAIN_LENGTH + ) + writers.write_uint8(bw, msg.message_format) + if msg.application_domain: + writers.write_uint8(bw, _SIGNER_COUNT) + writers.write_bytes_fixed(bw, signer_pub_key_bytes, _PUBLIC_KEY_LENGTH) + writers.write_uint16_le(bw, message_length) + writers.write_bytes_unchecked(bw, msg.message) + return bytes(bw.buffer) + + +def sanitize_message(msg: SolanaSignOffChainMessage): + """Sanitize the message.""" + message = msg.message + if ( + msg.application_domain + and len(msg.application_domain) != _APPLICATION_DOMAIN_LENGTH + ): + raise wire.DataError( + f"Application domain must be 32 bytes, got {len(msg.application_domain)}" + ) + if len(message) > ( + _MAX_MESSAGE_LENGTH if msg.application_domain else _MAX_MESSAGE_LENGTH_LEDGER + ): + raise wire.DataError( + f"Message is too long, maximum length is {_MAX_MESSAGE_LENGTH} bytes, got {len(message)}" + ) + if msg.message_version not in _ALLOWED_HEADER_VERSIONS: + raise wire.DataError(f"Message version must be 0, got {msg.message_version}") + if msg.message_format not in _ALLOWED_MESSAGE_FORMATS: + raise wire.DataError(f"Message format must be 0 or 1, got {msg.message_format}") + elif msg.message_format == SolanaOffChainMessageFormat.V0_RESTRICTED_ASCII: + if any(b < 0x20 or b > 0x7E for b in message): + raise wire.DataError( + "Message format 0 must contain only printable characters" + ) + elif msg.message_format == SolanaOffChainMessageFormat.V0_LIMITED_UTF8: + try: + message.decode("utf-8") + except UnicodeDecodeError: + raise wire.DataError("Message format 1 must be a valid UTF-8 string") diff --git a/core/src/apps/solana/sign_unsafe_message.py b/core/src/apps/solana/sign_unsafe_message.py new file mode 100644 index 0000000000..4298a76d02 --- /dev/null +++ b/core/src/apps/solana/sign_unsafe_message.py @@ -0,0 +1,58 @@ +from typing import TYPE_CHECKING + +from trezor import wire +from trezor.crypto.curve import ed25519 +from trezor.lvglui.scrs import lv +from trezor.messages import SolanaMessageSignature +from trezor.ui.layouts.lvgl import confirm_sol_message + +from apps.common import paths, seed +from apps.common.helpers import validate_message +from apps.common.keychain import auto_keychain +from apps.common.signverify import decode_message + +from . import ICON, PRIMARY_COLOR +from .message import Message +from .publickey import PublicKey + +if TYPE_CHECKING: + from trezor.messages import SolanaSignUnsafeMessage + from apps.common.keychain import Keychain + + +@auto_keychain(__name__) +async def sign_unsafe_message( + ctx: wire.Context, msg: SolanaSignUnsafeMessage, keychain: Keychain +) -> SolanaMessageSignature: + # sanitize message + message = msg.message + sanitize_message(message) + + # path validation + await paths.validate_path(ctx, keychain, msg.address_n) + + node = keychain.derive(msg.address_n) + + signer_pub_key_bytes = seed.remove_ed25519_prefix(node.public_key()) + ctx.primary_color, ctx.icon_path = lv.color_hex(PRIMARY_COLOR), ICON + decoded_message = decode_message(message) + address = str(PublicKey(signer_pub_key_bytes)) + + # display the decoded message to confirm + await confirm_sol_message(ctx, address, None, decoded_message, is_unsafe=True) + + signature = ed25519.sign(node.private_key(), message) + return SolanaMessageSignature(signature=signature) + + +def sanitize_message(message: bytes) -> None: + validate_message(message) + try: + _ = Message.deserialize(message) + raise wire.DataError("Valid transaction message format is not allowed") + except BaseException as e: + if __debug__: + import sys + + sys.print_exception(e) # type: ignore["print_exception" is not a known member of module] + return diff --git a/core/src/apps/tron/sign_message.py b/core/src/apps/tron/sign_message.py index 644df5f963..75f6ad80e7 100644 --- a/core/src/apps/tron/sign_message.py +++ b/core/src/apps/tron/sign_message.py @@ -24,8 +24,8 @@ async def sign_message( ctx: Context, msg: TronSignMessage, keychain: Keychain ) -> TronMessageSignature: validate_message(msg.message) - address_n = msg.address_n or () - await paths.validate_path(ctx, keychain, msg.address_n) + address_n = msg.address_n + await paths.validate_path(ctx, keychain, address_n) node = keychain.derive(address_n) seckey = node.private_key() diff --git a/core/src/apps/tron/sign_tx.py b/core/src/apps/tron/sign_tx.py index 52726a200e..f676e3d34a 100644 --- a/core/src/apps/tron/sign_tx.py +++ b/core/src/apps/tron/sign_tx.py @@ -20,8 +20,8 @@ async def sign_tx( """Parse and sign TRX transaction""" validate(msg) - address_n = msg.address_n or () - await paths.validate_path(ctx, keychain, msg.address_n) + address_n = msg.address_n + await paths.validate_path(ctx, keychain, address_n) node = keychain.derive(address_n) seckey = node.private_key() diff --git a/core/src/apps/workflow_handlers.py b/core/src/apps/workflow_handlers.py index 0a34c5aa35..a13ad5ba6e 100644 --- a/core/src/apps/workflow_handlers.py +++ b/core/src/apps/workflow_handlers.py @@ -92,6 +92,8 @@ def find_message_handler_module(msg_type: int) -> str: return "apps.bitcoin.sign_message" if msg_type == MessageType.VerifyMessage: return "apps.bitcoin.verify_message" + if msg_type == MessageType.SignPsbt: + return "apps.bitcoin.sign_taproot" # misc if msg_type == MessageType.GetEntropy: @@ -248,6 +250,10 @@ def find_message_handler_module(msg_type: int) -> str: return "apps.solana.get_address" if msg_type == MessageType.SolanaSignTx: return "apps.solana.sign_tx" + if msg_type == MessageType.SolanaSignUnsafeMessage: + return "apps.solana.sign_unsafe_message" + if msg_type == MessageType.SolanaSignOffChainMessage: + return "apps.solana.sign_offchain_message" # starcoin if msg_type == MessageType.StarcoinGetAddress: diff --git a/core/src/storage/device.py b/core/src/storage/device.py index 20430f970f..bcd17d68fd 100644 --- a/core/src/storage/device.py +++ b/core/src/storage/device.py @@ -71,9 +71,11 @@ if __debug__: AUTOLOCK_DELAY_MINIMUM = AUTOSHUTDOWN_DELAY_MINIMUM = 10 * 1000 # 10 seconds else: - AUTOLOCK_DELAY_MINIMUM = AUTOSHUTDOWN_DELAY_MINIMUM = 60 * 1000 # 1 minute + AUTOSHUTDOWN_DELAY_MINIMUM = 60 * 1000 # 1 minute + AUTOLOCK_DELAY_MINIMUM = 30 * 1000 # 30 seconds -AUTOLOCK_DELAY_DEFAULT = AUTOSHUTDOWN_DELAY_DEFAULT = 10 * 60 * 1000 # 10 minutes +AUTOSHUTDOWN_DELAY_DEFAULT = 10 * 60 * 1000 # 10 minutes +AUTOLOCK_DELAY_DEFAULT = 60 * 1000 # 1 minute # autolock intervals larger than AUTOLOCK_DELAY_MAXIMUM cause issues in the scheduler if __debug__: AUTOSHUTDOWN_DELAY_MAXIMUM = AUTOLOCK_DELAY_MAXIMUM = const(0x2000_0000) # ~6 days diff --git a/core/src/trezor/enums/CardanoGovernanceRegistrationFormat.py b/core/src/trezor/enums/CardanoCVoteRegistrationFormat.py similarity index 100% rename from core/src/trezor/enums/CardanoGovernanceRegistrationFormat.py rename to core/src/trezor/enums/CardanoCVoteRegistrationFormat.py diff --git a/core/src/trezor/enums/CardanoCertificateType.py b/core/src/trezor/enums/CardanoCertificateType.py index 4b7d241202..bc4abf6f20 100644 --- a/core/src/trezor/enums/CardanoCertificateType.py +++ b/core/src/trezor/enums/CardanoCertificateType.py @@ -6,3 +6,6 @@ STAKE_DEREGISTRATION = 1 STAKE_DELEGATION = 2 STAKE_POOL_REGISTRATION = 3 +STAKE_REGISTRATION_CONWAY = 7 +STAKE_DEREGISTRATION_CONWAY = 8 +VOTE_DELEGATION = 9 diff --git a/core/src/trezor/enums/CardanoDRepType.py b/core/src/trezor/enums/CardanoDRepType.py new file mode 100644 index 0000000000..bae3fbe218 --- /dev/null +++ b/core/src/trezor/enums/CardanoDRepType.py @@ -0,0 +1,8 @@ +# Automatically generated by pb2py +# fmt: off +# isort:skip_file + +KEY_HASH = 0 +SCRIPT_HASH = 1 +ABSTAIN = 2 +NO_CONFIDENCE = 3 diff --git a/core/src/trezor/enums/CardanoTxAuxiliaryDataSupplementType.py b/core/src/trezor/enums/CardanoTxAuxiliaryDataSupplementType.py index 3033edca2c..29ff1f41a4 100644 --- a/core/src/trezor/enums/CardanoTxAuxiliaryDataSupplementType.py +++ b/core/src/trezor/enums/CardanoTxAuxiliaryDataSupplementType.py @@ -3,4 +3,4 @@ # isort:skip_file NONE = 0 -GOVERNANCE_REGISTRATION_SIGNATURE = 1 +CVOTE_REGISTRATION_SIGNATURE = 1 diff --git a/core/src/trezor/enums/MessageType.py b/core/src/trezor/enums/MessageType.py index 275e932c3d..57fb30ceef 100644 --- a/core/src/trezor/enums/MessageType.py +++ b/core/src/trezor/enums/MessageType.py @@ -67,6 +67,8 @@ GetOwnershipProof = 49 OwnershipProof = 50 AuthorizeCoinJoin = 51 +SignPsbt = 10052 +SignedPsbt = 10053 CipherKeyValue = 23 CipheredKeyValue = 48 SignIdentity = 53 @@ -311,6 +313,9 @@ SolanaAddress = 10101 SolanaSignTx = 10102 SolanaSignedTx = 10103 + SolanaSignOffChainMessage = 10104 + SolanaMessageSignature = 10105 + SolanaSignUnsafeMessage = 10106 CosmosGetAddress = 10800 CosmosAddress = 10801 CosmosSignTx = 10802 diff --git a/core/src/trezor/enums/SolanaOffChainMessageFormat.py b/core/src/trezor/enums/SolanaOffChainMessageFormat.py new file mode 100644 index 0000000000..43ef9fb923 --- /dev/null +++ b/core/src/trezor/enums/SolanaOffChainMessageFormat.py @@ -0,0 +1,6 @@ +# Automatically generated by pb2py +# fmt: off +# isort:skip_file + +V0_RESTRICTED_ASCII = 0 +V0_LIMITED_UTF8 = 1 diff --git a/core/src/trezor/enums/SolanaOffChainMessageVersion.py b/core/src/trezor/enums/SolanaOffChainMessageVersion.py new file mode 100644 index 0000000000..0803896d71 --- /dev/null +++ b/core/src/trezor/enums/SolanaOffChainMessageVersion.py @@ -0,0 +1,5 @@ +# Automatically generated by pb2py +# fmt: off +# isort:skip_file + +MESSAGE_VERSION_0 = 0 diff --git a/core/src/trezor/enums/__init__.py b/core/src/trezor/enums/__init__.py index 3aba4f6999..9ba26042da 100644 --- a/core/src/trezor/enums/__init__.py +++ b/core/src/trezor/enums/__init__.py @@ -91,6 +91,8 @@ class MessageType(IntEnum): GetOwnershipProof = 49 OwnershipProof = 50 AuthorizeCoinJoin = 51 + SignPsbt = 10052 + SignedPsbt = 10053 CipherKeyValue = 23 CipheredKeyValue = 48 SignIdentity = 53 @@ -329,6 +331,9 @@ class MessageType(IntEnum): SolanaAddress = 10101 SolanaSignTx = 10102 SolanaSignedTx = 10103 + SolanaSignOffChainMessage = 10104 + SolanaMessageSignature = 10105 + SolanaSignUnsafeMessage = 10106 CosmosGetAddress = 10800 CosmosAddress = 10801 CosmosSignTx = 10802 @@ -528,6 +533,15 @@ class CardanoCertificateType(IntEnum): STAKE_DEREGISTRATION = 1 STAKE_DELEGATION = 2 STAKE_POOL_REGISTRATION = 3 + STAKE_REGISTRATION_CONWAY = 7 + STAKE_DEREGISTRATION_CONWAY = 8 + VOTE_DELEGATION = 9 + + class CardanoDRepType(IntEnum): + KEY_HASH = 0 + SCRIPT_HASH = 1 + ABSTAIN = 2 + NO_CONFIDENCE = 3 class CardanoPoolRelayType(IntEnum): SINGLE_HOST_IP = 0 @@ -536,9 +550,9 @@ class CardanoPoolRelayType(IntEnum): class CardanoTxAuxiliaryDataSupplementType(IntEnum): NONE = 0 - GOVERNANCE_REGISTRATION_SIGNATURE = 1 + CVOTE_REGISTRATION_SIGNATURE = 1 - class CardanoGovernanceRegistrationFormat(IntEnum): + class CardanoCVoteRegistrationFormat(IntEnum): CIP15 = 0 CIP36 = 1 @@ -667,6 +681,13 @@ class NEMImportanceTransferMode(IntEnum): ImportanceTransfer_Activate = 1 ImportanceTransfer_Deactivate = 2 + class SolanaOffChainMessageVersion(IntEnum): + MESSAGE_VERSION_0 = 0 + + class SolanaOffChainMessageFormat(IntEnum): + V0_RESTRICTED_ASCII = 0 + V0_LIMITED_UTF8 = 1 + class StellarAssetType(IntEnum): NATIVE = 0 ALPHANUM4 = 1 diff --git a/core/src/trezor/langs.py b/core/src/trezor/langs.py index 796e60c1a9..53f4e354d2 100644 --- a/core/src/trezor/langs.py +++ b/core/src/trezor/langs.py @@ -10,6 +10,7 @@ ("ru", "Russian"), ("es", "Spanish"), ("it", "Italiano"), + ("pt_br", "Portuguese (Brazil)"), ] langs_keys = [x[0] for x in langs] diff --git a/core/src/trezor/lvglui/i18n/keys.py b/core/src/trezor/lvglui/i18n/keys.py index 8140b3fa16..ad6de53350 100644 --- a/core/src/trezor/lvglui/i18n/keys.py +++ b/core/src/trezor/lvglui/i18n/keys.py @@ -1042,6 +1042,7 @@ SUBTITLE__FIDO2_AUTHENTICATE_NOT_REGISTERED = 472 # QR Code BUTTON__QRCODE = 473 +BUTTON__QR_CODE = 473 # {} Message TITLE__STR_MESSAGE = 474 # Entering Boardloader @@ -1280,7 +1281,7 @@ # If you want to make another KeyTag backup, you can view the KeyTag dotmap i # n "Wallet - Check Recovery Phrase" after verifying your recovery phrase. SUBTITLE__FINISH_KEYTAG_BACKUP = 582 -# Authenticity Check +# Device Authentication TITLE__SECURITY_CHECK = 583 # Disable Trezor Compatibility TITLE__DISABLE_TREZOR_COMPATIBILITY = 584 @@ -1497,8 +1498,8 @@ TITLE__LITE_PIN_ERROR_DESC = 685 # Lite has been Reset TITLE__LITE_HAS_BEEN_RESET = 686 -# The PIN has been entered incorrectly more than 10 times. OneKey Lite has bee -# n self-erased to prevent brute force cracking of the backup data. +# After 10 consecutive incorrect PIN entries, OneKey Lite has been automatical +# ly wiped to prevent brute force attacks on backup data. TITLE__LITE_HAS_BEEN_RESET_DESC = 687 # I Got It BUTTON__I_GOT_IT = 688 @@ -1580,7 +1581,7 @@ FORM__FINGER_STR = 721 # Charging MSG__CHARGING = 722 -# Scan transaction QR code or sign message +# Scan the QR Code displayed on the app CONTENT__SCAN_THE_QR_CODE_DISPLAYED_ON_THE_APP = 723 # Unsupported data format TITLE__DATA_FORMAT_NOT_SUPPORT = 724 @@ -1621,7 +1622,7 @@ CONTENT__ARE_YOU_SURE_TO_DISABLE_AIRGAP_MODE = 741 # Select the chain you need, then click Create button. SUBTITLE__ADD_ACCOUNT_2__TUTORIAL_AFTER_INIT = 742 -# ETH and EVM‒compatible chains +# ETH & EVM networks CONTENT__ETH_AND_EVM_POWERED_NETWORK = 743 # fingerprint not recognized, try again MSG__FINGERPRINT_NOT_RECOGNIZED_TRY_AGAIN = 744 @@ -1629,7 +1630,7 @@ MSG__USE_FINGERPRINT_OR_TAP_TO_UNLOCK = 745 # SE Firmware ITEM__SE_FIRMWARE = 746 -# Low battery (<10%). To keep the battery healthy, please ensure the device is +# Low battery (<20%). To keep the battery healthy, please ensure the device is # fully charged before long-term storage. CONTENT__POWER_OFF_LOW_BATTERY_DESC = 747 # Select the way to connect @@ -1724,31 +1725,189 @@ TITLE_CONFIRM_ADDRESS_DESC = 786 # BTC and EVM‒compatible networks CONTENT__BTC_AND_EVM_COMPATIBLE_NETWORKS = 787 -# ETH and EVM‒compatible networks -CONTENT__ETH_AND_EVM_COMPATIBLE_NETWORKS = 788 # Open OneKey and scan the QR code below -CONTENT__OPEN_ONEKEY_AND_SCAN_THE_QR_CODE_BELOW = 789 +CONTENT__OPEN_ONEKEY_AND_SCAN_THE_QR_CODE_BELOW = 788 # Export Account -CONTENT__EXPORT_ACCOUNT = 790 +CONTENT__EXPORT_ACCOUNT = 789 # Connecting... -TITLE__CONNECTING = 791 +TITLE__CONNECTING = 790 # Set OneKey Lite PIN -TITLE__SET_ONEKEY_LITE_PIN = 792 +TITLE__SET_ONEKEY_LITE_PIN = 791 # I understand that the backup will be overwritten -FORM__I_UNDERSTAND_THAT_THE_BACKUP_WILL_BE_OVERWRITTEN = 793 +FORM__I_UNDERSTAND_THAT_THE_BACKUP_WILL_BE_OVERWRITTEN = 792 # Confirm OneKey Lite PIN -TITLE__CONFIRM_ONEKEY_LITE_PIN = 794 +TITLE__CONFIRM_ONEKEY_LITE_PIN = 793 # PINs do not match, please reconfirm. -CONTENT__PINS_DO_NOT_MATCH = 795 +CONTENT__PINS_DO_NOT_MATCH = 794 # Connect again -TITLE__CONNECT_AGAIN = 796 -# Eixt -BUTTON__EXIT = 797 +TITLE__CONNECT_AGAIN = 795 +# Exit +BUTTON__EXIT = 796 # The two OneKey Lite used for connection are not the same. -CONTENT__THE_TWO_ONEKEY_LITE_USED_FOR_CONNECTION_ARE_NOT_THE_SAME = 798 +CONTENT__THE_TWO_ONEKEY_LITE_USED_FOR_CONNECTION_ARE_NOT_THE_SAME = 797 # Exit Backup Process? -TITLE__EXIT_BACKUP_PROCESS = 799 +TITLE__EXIT_BACKUP_PROCESS = 798 # If you exit now, you will need to re-verify your recovery phrase when you re # -enter. Are you sure you want to exit? -TITLE__EXIT_BACKUP_PROCESS_DESC = 800 +TITLE__EXIT_BACKUP_PROCESS_DESC = 799 +# Unsupported Recovery Phrase +TITLE__UNSUPPORTED_RECOVERY_PHRASE = 800 +# The current hardware wallet only supports 12, 18, and 24-word recovery phras +# es.\nThis Lite backup cannot be restored. +TITLE__UNSUPPORTED_RECOVERY_PHRASE_DESC = 801 +# Open OneKey and scan the QR code, supporting BTC and EVM networks. +CONTENT__OPEN_ONEKEY_SCAN_THE_QRCODE = 802 +# Wallet Mismatch +CONTENT__WALLET_MISMATCH = 803 +# Your selected wallet in the app does not match the hardware wallet. Please c +# heck and try again. +CONTENT__WALLET_MISMATCH_DESC = 804 +# Non-standard message signature. +CONTENT__NON_STANDARD_MESSAGE_SIGNATURE = 805 +# Send Tokens +TITLE__SEND_TOKENS = 806 +# The following transaction output contains contract data: +CONTENT__FOLLOWING_TRANSACTION_CONTAINS_CONTRACT = 807 +# You are using {type} authorization, ensure the dApp is trustworthy to avoid +# asset loss. +MSG___PERMIT_SIGN_ALERT = 808 +# Backup Limited +TITLE__BACKUP_LIMITED = 809 +# Airgap mode is enabled and NFC is disabled, so you cannot back up to OneKey +# Lite. Please disable Airgap mode and try again. +TITLE__BACKUP_LIMITED_DESC = 810 +# Go Settings +BUTTON__GO_SETTINGS = 811 +# Certifications +CONTENT__CERTIFICATIONS = 812 +# My Address +APP__ADDRESS = 813 +# Select Network +TITLE__SELECT_NETWORK = 814 +# {network} Address +TITLE__NETWORK_ADDRESS = 815 +# Select Derivation Path +TITLE__SELECT_DERIVATION_PATH = 816 +# Select Account +TITLE__SELECT_ACCOUNT = 817 +# Go To Account +TITLE__SET_INITIAL_ACCOUNT = 818 +# Input formatting error +TITLE__SET_INITIAL_ACCOUNT_ERROR = 819 +# Your address is an EVM network address. You can use it to manage your assets +# across other EVM-compatible networks (such as Ethereum, BNB Chain, Polygon, +# Arbitrum One, Avalanche, etc.). +CONTENT__NETWORK_ADDRESS_ETHEREUM = 820 +# Security Keys +APP__SECURITY = 821 +# Wallet Setup Required +ONBOARDING_BLUETOOTH_PAIRING_BEFORE_SETUP_PIN_TITLE = 822 +# To pair your device via Bluetooth, please create a wallet on your hardware d +# evice first. This is necessary for secure connection +ONBOARDING_BLUETOOTH_PAIRING_BEFORE_SETUP_PIN_DESC = 823 +# Bluetooth pairing unavailable while in Boot mode +BOOT_BLUETOOTH_PAIRING_ERROR_TITLE = 824 +# To update your device, visit firmware.onekey.so on your computer and connect +# via USB to install firmware. +BOOT_BLUETOOTH_PAIRING_ERROR_DESC = 825 +# Verify Now +ACTION_VERIFY_NOW = 826 +# Connect Device +VERIFY_DEVICE_CONNECT_DEVICE_TITLE = 827 +# Open OneKey App and connect your device to create a wallet. Device verificat +# ion will be performed automatically +VERIFY_DEVICE_CONNECT_DEVICE_DESC = 828 +# Verify Device +TITLE__VEIRIFY_DEVICE = 829 +# Visit https://bit.ly/3ZsHB40 for additional verification methods +VERIFY_DEVICE_HELP_CENTER_TEXT = 830 +# More Networks +BUTTON__MORE_NETWORKS = 831 +# Show Less +BUTTON__LESS_NETWORKS = 832 +# Bootloader URL requires device verification in OneKey App 5.5.0+ +VERIFY_DEVICE_BOOTLOADER_DESC = 833 +# Download OneKey App at: onekey.so/download +FORM__DOWNLOAD_APP_FROM_DOWNLOAD_CENTER = 834 +# FIDO Keys +FIDO_FIDO_KEYS_LABEL = 835 +# Secure accounts with FIDO security keys +FIDO_FIDO_KEYS_DESC = 836 +# Remove FIDO Key? +FIDO_REMOVE_KEY_TITLE = 837 +# This FIDO key will be removed permanently +FIDO_REMOVE_KEY_DESC = 838 +# FIDO Key Removed +FIDO_REMOVE_KEY_SUCCESS_TITLE = 839 +# Key Limit Reached +FIDO_ADD_KEY_LIMIT_REACHED_TITLE = 840 +# 60 FIDO keys limit reached. Remove unused to add new +FIDO_ADD_KEY_LIMIT_REACHED_DESC = 841 +# Manage FIDO Keys +FIDO_MANAGE_KEY_CTA_LABEL = 842 +# No FIDO keys yet +FIDO_LIST_EMPTY_TEXT = 843 +# Using OneKey Devices as Security Keys +TIPS_SECURITY_KEYS_TEXT = 844 +# Register +TIPS_SECURITY_KEYS_REGISTER_TITLE = 845 +# Plug in your OneKey to your computer +TIPS_SECURITY_KEYS_REGISTER_PLUG_IN = 846 +# Go to security settings of the website (e.g. Google, Facebook) +TIPS_SECURITY_KEYS_REGISTER_GO_TO_WEBSITE = 847 +# Select "Add Security Key" option +TIPS_SECURITY_KEYS_REGISTER_SELECT_OPTION = 848 +# Confirm with OneKey (unlock required) +TIPS_SECURITY_KEYS_REGISTER_CONFIRM = 849 +# Choose "Security Key" at login +TIPS_SECURITY_KEYS_AUTHENTICATE_CHOOSE_OPTION = 850 +# Approve with OneKey +TIPS_SECURITY_KEYS_AUTHENTICATE_APPROVE = 851 +# Authenticate +TIPS_SECURITY_KEYS_AUTHENTICATE_TITLE = 852 +# Slide to unlock +MISTOUCH_PROTECTION_TITLE = 853 +# 5 failed tries. Slide to continue +MISTOUCH_PROTECTION_DESC = 854 +# Slide to continue +MISTOUCH_PROTECTION_SLIDE_TEXT = 855 +# Back to home +BUTTON__BACK_TO_HOME = 856 +# Registering fido key... +FIDO_KEY_REGISTERING_DESC = 857 +# FIDO Key Registered +FIDO_KEY_REGISTERED_TITLE = 858 +# Low Battery +POWER_ON_LOW_BATTERY_TITLE = 859 +# Powering off +POWER_ON_LOW_BATTERY_DESC = 860 +# Auto Lock/Shutdown +ITEM__AUTO_LOCK_AND_SHUTDOWN = 861 +# Solana Raw Signing +SECURITY__SOLANA_RAW_SIGNING_TITLE = 862 +# Allows signing raw Solana messages without processing or validation. This ma +# y expose you to phishing, blind signing, and unauthorized approvals. Use wit +# h caution. +SECURITY__SOLANA_RAW_SIGNING_DESC = 863 +# Enable Solana Raw Signing? +SECURITY__SOLANA_RAW_SIGNING_ENABLE_TITLE = 864 +# This may expose you to phishing, blind signing, and unauthorized transaction +# s. Enable only if you fully understand the risks. +SECURITY__SOLANA_RAW_SIGNING_ENABLE_DESC = 865 +# Risk of phishing & blind signing. Proceed only if you trust the source. +SECURITY__SOLANA_RAW_SIGNING_TX_WARNING = 866 +# Security Protection +ITEM__SECURITY_PROTECTION = 867 +# Basic Tutorial +ITEM__BASIC_TUTORIAL = 868 +# BTC, SOL, ETH & EVM networks +CONTENT__BTC_SOL_ETH_N_EVM_NETWORKS = 869 +# OneKey App +TITLE__ONEKEY_APP = 870 +# Candidate +GLOBAL_CANDIDATE = 871 +# Target network +GLOBAL_TARGET_NETWORK = 872 +# Remove vote +TITLE_REMOVE_VOTE = 873 # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/de.py b/core/src/trezor/lvglui/i18n/locales/de.py index 0a5d06e56d..869e31caeb 100644 --- a/core/src/trezor/lvglui/i18n/locales/de.py +++ b/core/src/trezor/lvglui/i18n/locales/de.py @@ -200,7 +200,7 @@ "Falscher Paarungscode, versuchen Sie es erneut.", "{} Sekunden", "Benutzerdefiniert", - "Daten anzeigen", + "Datenansicht", "Größe", "Daten", "Möchten Sie Ihr Gerät wirklich nach {} automatisch sperren lassen?", @@ -583,7 +583,7 @@ "Stellen Sie eine Verbindung zur OneKey-App her, suchen Sie die NFT, die dieser Hardware-Wallet gehört, und tippen Sie auf die Schaltfläche „Sammeln“.", "Sie können später in \"Wallet - Check Recovery Phrase\" immer noch mit KeyTag sichern.", "Wenn Sie eine weitere KeyTag-Sicherung erstellen möchten, können Sie die KeyTag-Dotmap in \"Wallet - Check Recovery Phrase\" anzeigen, nachdem Sie Ihre Wiederherstellungsphrase überprüft haben.", - "Echtheitsprüfung", + "Geräteauthentifizierung", "Trezor-Kompatibilität deaktivieren", "Stellen Sie die Trezor-Kompatibilität wieder her", "Kompatibel mit Trezor", @@ -687,7 +687,7 @@ "Lite-PIN-Fehler", "Daten auf dieser Karte werden nach {} Fehlversuchen gelöscht.", "Lite wurde zurückgesetzt", - "Die PIN wurde mehr als 10 Mal falsch eingegeben. OneKey Lite wurde selbst gelöscht, um ein brutales Knacken der Sicherungsdaten zu verhindern.", + "Nach 10 aufeinanderfolgenden falschen PIN-Eingaben wurde OneKey Lite automatisch gelöscht, um Brute-Force-Angriffe auf Sicherungsdaten zu verhindern.", "Ich habe es", "Verbindung fehlgeschlagen", "Stellen Sie sicher, dass die Karte fest an der Rückseite des Geräts anliegt, und versuchen Sie es dann erneut.", @@ -723,7 +723,7 @@ "OneKey-Wallet", "Fingerabdruck {}", "Aufladen", - "QR-Code scannen oder Nachricht signieren", + "Scannen Sie den auf der App angezeigten QR-Code", "Nicht unterstütztes Datenformat", "Der QR-Codetyp wird nicht unterstützt. Bitte versuchen Sie es erneut.", "Ungültige Transaktion", @@ -743,11 +743,11 @@ "Luftspalt deaktivieren", "Sind Sie sicher, den Air Gap-Modus zu deaktivieren? Nach der Deaktivierung kann Ihr Gerät über Bluetooth oder USB verbunden werden.", "Wählen Sie die gewünschte Kette aus und klicken Sie dann auf die Schaltfläche „Erstellen“.", - "ETH- und EVM-kompatible Ketten", + "ETH- und EVM-Netzwerke", "Fingerabdruck nicht erkannt, versuchen Sie es erneut", "Verwenden Sie zum Entsperren den Fingerabdruck oder tippen Sie darauf", "SE Firmware", - "Niedriger Akkustand (<10%). Um den Akku gesund zu halten, stellen Sie bitte sicher, dass das Gerät vor der langfristigen Lagerung vollständig aufgeladen ist.", + "Niedriger Akkustand (<20%). Um den Akku gesund zu halten, stellen Sie bitte sicher, dass das Gerät vor der langfristigen Lagerung vollständig aufgeladen ist.", "Wählen Sie die Art der Verbindung aus", "Bluetooth Verbinden", "Wählen Sie die Wallet aus, die Sie verbinden möchten", @@ -788,7 +788,6 @@ "Adresse bestätigen", "Kehren Sie zur App zurück und scannen Sie den untenstehenden QR-Code.", "BTC- und EVM-kompatible Netzwerke", - "ETH‒ und EVM‒kompatible Netzwerke", "Öffnen Sie OneKey und scannen Sie den untenstehenden QR-Code", "Konto exportieren", "Verbinden ...", @@ -797,9 +796,83 @@ "Bestätigen Sie die OneKey Lite PIN", "Die PINs stimmen nicht überein, bitte bestätigen Sie erneut.", "Verbinden Sie erneut", - "Beenden", + "Ausfahrt", "Die beiden für die Verbindung verwendeten OneKey Lite sind nicht gleich.", "Sicherungsvorgang beenden?", "Wenn Sie jetzt beenden, müssen Sie Ihre Wiederherstellungsphrase beim erneuten Aufrufen erneut bestätigen. Möchten Sie wirklich beenden?", + "Nicht unterstützte Wiederherstellungsphrase", + "Die aktuelle Hardware-Wallet unterstützt nur Wiederherstellungsphrasen mit 12, 18 und 24 Wörtern.\nDiese Lite-Sicherung kann nicht wiederhergestellt werden.", + "Öffnen Sie OneKey und scannen Sie den QR-Code. BTC- und EVM-Netzwerke werden unterstützt.", + "Unstimmigkeit der Geldbörse", + "Ihre in der App ausgewählte Wallet stimmt nicht mit der Hardware-Wallet überein. Bitte überprüfen Sie dies und versuchen Sie es erneut.", + "Nicht standardmäßige Nachrichtensignatur.", + "Token senden", + "Die folgende Transaktionsausgabe enthält Vertragsdaten:", + "Sie nutzen die {type}-Autorisierung, prüfen Sie die Vertrauenswürdigkeit der dApp.", + "Sicherung nicht möglich", + "Der Airgap-Modus ist aktiviert und NFC ist deaktiviert, daher können Sie kein Backup auf OneKey Lite erstellen. Bitte deaktivieren Sie den Airgap-Modus und versuchen Sie es erneut.", + "Gehe zu Einstellungen", + "Zertifizierungen", + "Meine Adresse", + "Netzwerk auswählen", + "{network} Adresse", + "Ableitungspfad auswählen", + "Konto auswählen", + "Zum Konto gehen", + "Fehler bei der Eingabeformatierung", + "Ihre Adresse ist eine EVM-Netzwerkadresse. Sie können sie verwenden, um Ihre Vermögenswerte über andere EVM-kompatible Netzwerke (wie Ethereum, BNB Chain, Polygon, Arbitrum One, Avalanche usw.) zu verwalten.", + "Sicherheitsschlüssel", + "Einrichtung des Wallets erforderlich", + "Um Ihr Gerät über Bluetooth zu koppeln, erstellen Sie bitte zuerst eine Wallet auf Ihrem Hardware-Gerät. Dies ist notwendig für eine sichere Verbindung.", + "Bluetooth-Kopplung im Boot-Modus nicht verfügbar", + "Um Ihr Gerät zu aktualisieren, besuchen Sie firmware.onekey.so auf Ihrem Computer und verbinden Sie es über USB, um die Firmware zu installieren.", + "Jetzt verifizieren", + "Gerät verbinden", + "Öffnen Sie die OneKey App und verbinden Sie Ihr Gerät, um eine Wallet zu erstellen. Die Geräteüberprüfung wird automatisch durchgeführt.", + "Gerät verifizieren", + "Besuchen Sie https://bit.ly/3ZsHB40 für zusätzliche Verifizierungsmethoden", + "Weitere Netzwerke", + "Weniger zeigen", + "Bootloader-URL erfordert Geräteverifizierung in der OneKey App 5.5.0+", + "Laden Sie die OneKey App herunter unter: onekey.so/download", + "FIDO-Schlüssel", + "Sichern Sie Konten mit FIDO-Sicherheitsschlüsseln", + "FIDO-Schlüssel entfernen?", + "Dieser FIDO-Schlüssel wird dauerhaft entfernt", + "FIDO-Schlüssel entfernt", + "Schlüsselgrenze erreicht", + "60 FIDO-Schlüssel-Limit erreicht. Entfernen Sie ungenutzte, um neue hinzuzufügen", + "Verwalten von FIDO-Schlüsseln", + "Noch keine FIDO-Schlüssel", + "Verwendung von OneKey-Geräten als Sicherheitsschlüssel", + "Registrieren", + "Stecken Sie Ihren OneKey in Ihren Computer ein", + "Gehen Sie zu den Sicherheitseinstellungen der Website (z. B. Google, Facebook)", + "Wählen Sie die Option \"Add Security Key\" aus", + "Bestätigen mit OneKey (Entsperrung erforderlich)", + "Wählen Sie \"Security Key\" beim Login", + "Genehmigen mit OneKey", + "Authentifizieren", + "Zum Entsperren schieben", + "5 fehlgeschlagene Versuche. Wischen, um fortzufahren", + "Zum Fortfahren wischen", + "Zurück zur Startseite", + "Registrierung des FIDO-Schlüssels...", + "FIDO-Schlüssel registriert", + "Schwache Batterie", + "Ausschalten", + "Automatische Sperre/Abschaltung", + "Solana Raw Signing", + "Ermöglicht das Signieren von rohen Solana-Nachrichten ohne Verarbeitung oder Validierung. Dies kann Sie Phishing, blindem Signieren und unbefugten Genehmigungen aussetzen. Verwenden Sie es mit Vorsicht.", + "Solana Raw Signing aktivieren?", + "Dies kann Sie Phishing, blindem Signieren und unautorisierten Transaktionen aussetzen. Aktivieren Sie dies nur, wenn Sie die Risiken vollständig verstehen.", + "Risiko von Phishing & blindem Signieren. Fahren Sie nur fort, wenn Sie der Quelle vertrauen.", + "Sicherheitsschutz", + "Grundlegendes Tutorial", + "BTC-, SOL-, ETH- und EVM-Netzwerke", + "OneKey App", + "Kandidat", + "Zielnetzwerk", + "Stimme entfernen", ] # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/en.py b/core/src/trezor/lvglui/i18n/locales/en.py index c7fb98ac28..f066f1064d 100644 --- a/core/src/trezor/lvglui/i18n/locales/en.py +++ b/core/src/trezor/lvglui/i18n/locales/en.py @@ -583,7 +583,7 @@ "Connect to the OneKey app, find the NFT owned by this hardware wallet, and tap the Collect button.", "You can still backup with KeyTag later in \"Wallet - Check Recovery Phrase\".", "If you want to make another KeyTag backup, you can view the KeyTag dotmap in \"Wallet - Check Recovery Phrase\" after verifying your recovery phrase.", - "Authenticity Check", + "Device Authentication", "Disable Trezor Compatibility", "Restore Trezor Compatibility", "Compatible with Trezor", @@ -687,7 +687,7 @@ "Lite PIN Error", "Data on this card will be erased after {} wrong tries.", "Lite has been Reset", - "The PIN has been entered incorrectly more than 10 times. OneKey Lite has been self-erased to prevent brute force cracking of the backup data.", + "After 10 consecutive incorrect PIN entries, OneKey Lite has been automatically wiped to prevent brute force attacks on backup data.", "I Got It", "Connect Failed", "Make sure the card is placed firmly against the back of the device, then try again.", @@ -723,7 +723,7 @@ "OneKey Wallet", "Finger {}", "Charging", - "Scan transaction QR code or sign message", + "Scan the QR Code displayed on the app", "Unsupported data format", "QR code type not support, please try again.", "Invalid Transaction", @@ -743,11 +743,11 @@ "Disable Air Gap", "Are you sure to disable Air Gap mode? After disable, it will be able to connect your device via USB or Bluetooth.", "Select the chain you need, then click Create button.", - "ETH and EVM‒compatible chains", + "ETH & EVM networks", "fingerprint not recognized, try again", "Use fingerprint or tap to unlock", "SE Firmware", - "Low battery (<10%). To keep the battery healthy, please ensure the device is fully charged before long-term storage.", + "Low battery (<20%). To keep the battery healthy, please ensure the device is fully charged before long-term storage.", "Select the way to connect", "Bluetooth Connect", "Select the wallet you want to connect", @@ -788,7 +788,6 @@ "Confirm Address", "Return to the app, and scan the QR code below.", "BTC and EVM‒compatible networks", - "ETH and EVM‒compatible networks", "Open OneKey and scan the QR code below", "Export Account", "Connecting...", @@ -797,9 +796,83 @@ "Confirm OneKey Lite PIN", "PINs do not match, please reconfirm.", "Connect again", - "Eixt", + "Exit", "The two OneKey Lite used for connection are not the same.", "Exit Backup Process?", "If you exit now, you will need to re-verify your recovery phrase when you re-enter. Are you sure you want to exit?", + "Unsupported Recovery Phrase", + "The current hardware wallet only supports 12, 18, and 24-word recovery phrases.\nThis Lite backup cannot be restored.", + "Open OneKey and scan the QR code, supporting BTC and EVM networks.", + "Wallet Mismatch", + "Your selected wallet in the app does not match the hardware wallet. Please check and try again.", + "Non-standard message signature.", + "Send Tokens", + "The following transaction output contains contract data:", + "You are using {type} authorization, ensure the dApp is trustworthy to avoid asset loss.", + "Backup Limited", + "Airgap mode is enabled and NFC is disabled, so you cannot back up to OneKey Lite. Please disable Airgap mode and try again.", + "Go Settings", + "Certifications", + "My Address", + "Select Network", + "{network} Address", + "Select Derivation Path", + "Select Account", + "Go To Account", + "Input formatting error", + "Your address is an EVM network address. You can use it to manage your assets across other EVM-compatible networks (such as Ethereum, BNB Chain, Polygon, Arbitrum One, Avalanche, etc.).", + "Security Keys", + "Wallet Setup Required", + "To pair your device via Bluetooth, please create a wallet on your hardware device first. This is necessary for secure connection", + "Bluetooth pairing unavailable while in Boot mode", + "To update your device, visit firmware.onekey.so on your computer and connect via USB to install firmware.", + "Verify Now", + "Connect Device", + "Open OneKey App and connect your device to create a wallet. Device verification will be performed automatically", + "Verify Device", + "Visit https://bit.ly/3ZsHB40 for additional verification methods", + "More Networks", + "Show Less", + "Bootloader URL requires device verification in OneKey App 5.5.0+", + "Download OneKey App at: onekey.so/download", + "FIDO Keys", + "Secure accounts with FIDO security keys", + "Remove FIDO Key?", + "This FIDO key will be removed permanently", + "FIDO Key Removed", + "Key Limit Reached", + "60 FIDO keys limit reached. Remove unused to add new", + "Manage FIDO Keys", + "No FIDO keys yet", + "Using OneKey Devices as Security Keys", + "Register", + "Plug in your OneKey to your computer", + "Go to security settings of the website (e.g. Google, Facebook)", + "Select \"Add Security Key\" option", + "Confirm with OneKey (unlock required)", + "Choose \"Security Key\" at login", + "Approve with OneKey", + "Authenticate", + "Slide to unlock", + "5 failed tries. Slide to continue", + "Slide to continue", + "Back to home", + "Registering fido key...", + "FIDO Key Registered", + "Low Battery", + "Powering off", + "Auto Lock/Shutdown", + "Solana Raw Signing", + "Allows signing raw Solana messages without processing or validation. This may expose you to phishing, blind signing, and unauthorized approvals. Use with caution.", + "Enable Solana Raw Signing?", + "This may expose you to phishing, blind signing, and unauthorized transactions. Enable only if you fully understand the risks.", + "Risk of phishing & blind signing. Proceed only if you trust the source.", + "Security Protection", + "Basic Tutorial", + "BTC, SOL, ETH & EVM networks", + "OneKey App", + "Candidate", + "Target network", + "Remove vote", ] # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/es.py b/core/src/trezor/lvglui/i18n/locales/es.py index de1de9c780..606d95bb53 100644 --- a/core/src/trezor/lvglui/i18n/locales/es.py +++ b/core/src/trezor/lvglui/i18n/locales/es.py @@ -583,7 +583,7 @@ "Conéctate a la aplicación OneKey, busca el NFT propiedad de esta billetera física y toca el botón Recoger.", "Todavía puedes hacer una copia de seguridad con KeyTag más adelante en \"Billetera - Verificar frase de recuperación\".", "Si deseas realizar otra copia de seguridad de KeyTag, puedes ver el mapa de puntos de KeyTag en \"Billetera - Verificar frase de recuperación\" después de verificar tu frase de recuperación.", - "Comprobación de autenticidad", + "Autenticación de Dispositivo", "Deshabilitar la compatibilidad con Trezor", "Restaurar la compatibilidad de Trezor", "Compatible con Trezor", @@ -665,7 +665,7 @@ "Límite de errores alcanzado, ingrese PIN.", "Desliza hacia arriba para desbloquear", "Permite grabar hasta 3 huellas dactilares simultáneamente.", - "Desbloquear dispositivo", + "Desbloquear", "¿Eliminar esta huella digital?", "¿Estás seguro de que deseas eliminar esta huella digital?", "Después de configurar la huella digital, puede usarla para desbloquear el dispositivo. Toca el botón de encendido con tu dedo registrado en el escenario correspondiente. Puedes modificarlo en cualquier momento en la configuración.", @@ -687,7 +687,7 @@ "Error de PIN básico", "Los datos de esta tarjeta se borrarán después de {} intentos incorrectos.", "Lite ha sido reiniciado", - "El PIN se ha introducido incorrectamente más de 10 veces. OneKey Lite se ha autoborrado para evitar el descifrado por fuerza bruta de los datos de la copia de seguridad.", + "Después de 10 entradas de PIN incorrectas consecutivas, OneKey Lite se borró automáticamente para evitar ataques de fuerza bruta a los datos de respaldo.", "Lo tengo", "Conexión fallida", "Asegúrese de que la tarjeta esté colocada firmemente contra la parte posterior del dispositivo y vuelva a intentarlo.", @@ -723,7 +723,7 @@ "OneKey Wallet", "Huella dactilar {}", "Cargando", - "Escanee el código QR de la transacción o firme el mensaje", + "Escanee el código QR que se muestra en la aplicación", "Formato de datos no admitido", "El tipo de código QR no es compatible. Inténtelo de nuevo.", "Transacción inválida", @@ -743,11 +743,11 @@ "Deshabilitar el espacio de aire", "¿Estás seguro de desactivar el modo Air Gap? Después de desactivarlo, podrá conectar su dispositivo a través de Bluetooth o USB.", "Seleccione la cadena que necesita y luego haga clic en el botón Crear.", - "Cadenas compatibles con ETH y EVM", + "Redes ETH y EVM", "huella digital no reconocida, inténtalo de nuevo", "Use huella digital o toque para desbloquear", "Firmware SE", - "Batería baja (<10%). Para mantener la batería en buen estado, asegúrese de que el dispositivo esté completamente cargado antes de guardarlo a largo plazo.", + "Batería baja (<20%). Para mantener la batería en buen estado, asegúrese de que el dispositivo esté completamente cargado antes de guardarlo a largo plazo.", "Seleccione la forma de conectar", "Conectar Bluetooth", "Seleccione la cartera que desea conectar", @@ -788,7 +788,6 @@ "Confirmar dirección", "Regresa a la aplicación y escanea el código QR que se muestra a continuación.", "Redes compatibles con BTC y EVM", - "Redes compatibles con ETH y EVM", "Abra OneKey y escanee el código QR a continuación", "Exportar cuenta", "Conectando...", @@ -797,9 +796,83 @@ "Confirme el PIN de OneKey Lite", "Los PIN no coinciden, por favor confirme de nuevo.", "Vuelve a conectar", - "Salir", + "Salida", "Los dos OneKey Lite utilizados para la conexión no son iguales.", "¿Salir del proceso de copia de seguridad?", "Si sale ahora, deberá volver a verificar su frase de recuperación cuando vuelva a ingresar. ¿Seguro que quieres salir?", + "Frase de recuperación no compatible", + "La actual cartera de hardware solo admite frases de recuperación de 12, 18 y 24 palabras.\nEsta copia de seguridad Lite no se puede restaurar.", + "Abra OneKey y escanee el código QR, compatible con redes BTC y EVM.", + "Incompatibilidad de billetera", + "Tu billetera seleccionada en la aplicación no coincide con la billetera de hardware. Por favor, verifica y vuelve a intentarlo.", + "Firma de mensaje no estándar.", + "Enviar tokens", + "La siguiente salida de transacción contiene datos del contrato:", + "Usas autorización {type}, verifica que la dApp sea segura.", + "No se puede realizar una copia de seguridad", + "El modo Airgap está habilitado y NFC está deshabilitado, por lo que no puedes hacer copias de seguridad en OneKey Lite. Deshabilita el modo Airgap y vuelve a intentarlo.", + "Ir a configuración", + "Certificaciones", + "Mi dirección", + "Seleccionar red", + "Dirección {network}", + "Seleccionar ruta de derivación", + "Seleccionar cuenta", + "Ir a la cuenta", + "Error de formato de entrada", + "Tu dirección es una dirección de red EVM. Puedes usarla para gestionar tus activos a través de otras redes compatibles con EVM (como Ethereum, BNB Chain, Polygon, Arbitrum One, Avalanche, etc.).", + "Llaves de seguridad", + "Configuración de la billetera requerida", + "Para emparejar tu dispositivo a través de Bluetooth, primero crea una billetera en tu dispositivo de hardware. Esto es necesario para una conexión segura.", + "Emparejamiento Bluetooth no disponible mientras está en modo Boot", + "Para actualizar tu dispositivo, visita firmware.onekey.so en tu computadora y conéctalo vía USB para instalar el firmware.", + "Verificar ahora", + "Conectar dispositivo", + "Abre la aplicación OneKey y conecta tu dispositivo para crear una billetera. La verificación del dispositivo se realizará automáticamente.", + "Verificar dispositivo", + "Visita https://bit.ly/3ZsHB40 para métodos de verificación adicionales", + "Más redes", + "Mostrar menos", + "La URL del bootloader requiere verificación del dispositivo en la aplicación OneKey 5.5.0+", + "Descarga la aplicación OneKey en: onekey.so/download", + "Llaves FIDO", + "Asegure cuentas con llaves de seguridad FIDO", + "¿Eliminar FIDO Key?", + "Esta clave FIDO será eliminada permanentemente", + "Llave FIDO Eliminada", + "Límite de clave alcanzado", + "Se alcanzó el límite de 60 claves FIDO. Elimine las no utilizadas para agregar nuevas", + "Gestionar claves FIDO", + "Aún no hay llaves FIDO", + "Usar dispositivos OneKey como llaves de seguridad", + "Registrar", + "Conecta tu OneKey a tu computadora", + "Ve a la configuración de seguridad del sitio web (por ejemplo, Google, Facebook)", + "Seleccione la opción \"Add Security Key\"", + "Confirma con OneKey (desbloqueo requerido)", + "Elige \"Security Key\" al iniciar sesión", + "Aprobar con OneKey", + "Autenticar", + "Desliza para desbloquear", + "5 intentos fallidos. Desliza para continuar", + "Desliza para continuar", + "Volver a inicio", + "Registrando la clave FIDO...", + "Llave FIDO Registrada", + "Batería baja", + "Apagando", + "Bloqueo/Apagado Automático", + "Firma en bruto de Solana", + "Permite firmar mensajes sin procesar de Solana sin procesamiento ni validación. Esto puede exponerte a phishing, firmas ciegas y aprobaciones no autorizadas. Úsalo con precaución.", + "¿Habilitar la firma sin procesar de Solana?", + "Esto puede exponerte a phishing, firmas ciegas y transacciones no autorizadas. Activa solo si comprendes completamente los riesgos.", + "Riesgo de phishing y firma a ciegas. Proceda solo si confía en la fuente.", + "Protección de seguridad", + "Tutorial básico", + "Redes BTC, SOL, ETH y EVM", + "OneKey App", + "Candidato", + "Red objetivo", + "Eliminar voto", ] # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/fr.py b/core/src/trezor/lvglui/i18n/locales/fr.py index 39f42f11ab..f65c9a47f1 100644 --- a/core/src/trezor/lvglui/i18n/locales/fr.py +++ b/core/src/trezor/lvglui/i18n/locales/fr.py @@ -10,12 +10,12 @@ "Confirmer", "Utiliser un code PIN fort pour protéger votre portefeuille contre tout accès physique non autorisé.", "Gardez votre code PIN sécurisé, assurez-vous de le stocker séparément de la phrase de récupération.", - "Définir un NIP", - "Entrez le nouveau NIP", + "Définir un PIN", + "Entrez le nouveau PIN", "Saisissez à nouveau le code PIN", "Ne pas correspondre", "Les codes PIN que vous avez saisis ne correspondent pas. Veuillez réessayer.", - "NIP activé", + "PIN activé", "Vous avez activé avec succès la protection par code PIN.", "Sauvegarder", "Le portefeuille est prêt", @@ -65,18 +65,18 @@ "La phrase de récupération saisie est valide mais ne correspond pas à celle de l'appareil.", "Corriger", "La phrase de récupération que vous avez entrée correspond, votre sauvegarde est correcte.", - "Voulez-vous changer votre NIP ?", - "Changer le NIP", - "Entrer l'ancien NIP", + "Voulez-vous changer votre PIN ?", + "Changer le PIN", + "Entrer l'ancien PIN", "proche", "Le code PIN que vous avez entré est incorrect.", - "Mauvais NIP", - "NIP modifié", + "Mauvais PIN", + "PIN modifié", "Vous avez changé votre code PIN avec succès.", "Fait", - "NIP désactivé", + "PIN désactivé", "Vous avez désactivé avec succès la protection par code PIN.", - "Entrez le NIP", + "Entrez le PIN", "Code PIN incorrect, {} tentatives restantes", "Activer la phrase de passe", "Désactiver la phrase secrète", @@ -200,7 +200,7 @@ "Code de paire incorrect, réessayez.", "{} secondes", "Personnalisé", - "Voir les données", + "Voir données", "Taille", "Données", "Voulez-vous vraiment verrouiller automatiquement votre appareil après {} ?", @@ -429,7 +429,7 @@ "Êtes-vous sûr de vouloir supprimer ce fond d'écran ?", "Régler", "Gérer le fond d'écran", - "Clavier NIP", + "Clavier PIN", "Défaut", "Les chiffres du clavier PIN disposés en séquence.", "Les chiffres sur le clavier PIN sont disposés de manière aléatoire.", @@ -583,7 +583,7 @@ "connectez-vous à l'application OneKey, recherchez le NFT appartenant à ce portefeuille matériel et appuyez sur le bouton Collecter.", "Vous pouvez toujours sauvegarder avec KeyTag plus tard dans \"Wallet - Check Recovery Phrase\".", "Si vous souhaitez effectuer une autre sauvegarde KeyTag, vous pouvez afficher la carte de points KeyTag dans \"Wallet - Check Recovery Phrase\" après avoir vérifié votre phrase de récupération.", - "Contrôle d'authenticité", + "Authentification de l'appareil", "Désactiver la compatibilité Trezor", "Restaurer la compatibilité Trezor", "Compatible avec Trezor", @@ -665,7 +665,7 @@ "Limite d'erreurs atteinte, veuillez entrer le code PIN.", "Glissez vers le haut pour déverrouiller", "Permet d'enregistrer jusqu'à 3 empreintes digitales simultanément.", - "Déverrouiller l'appareil", + "Déverrouiller", "Supprimer cette empreinte digitale ?", "Êtes-vous sûr de vouloir supprimer cette empreinte digitale ?", "Après avoir configuré l'empreinte digitale, vous pouvez l'utiliser pour déverrouiller l'appareil. Touchez le bouton d'alimentation avec votre doigt enregistré dans le scénario correspondant. Vous pouvez le modifier à tout moment dans les paramètres.", @@ -687,7 +687,7 @@ "Erreur de code PIN simplifié", "Les données de cette carte seront effacées après {} mauvais essais.", "Lite a été réinitialisé", - "Le code PIN a été saisi incorrectement plus de 10 fois. OneKey Lite a été auto-effacé pour empêcher le craquage par force brute des données de sauvegarde.", + "Après 10 saisies consécutives de code PIN incorrectes, OneKey Lite a été automatiquement effacé pour empêcher les attaques par force brute sur les données de sauvegarde.", "J'ai compris", "Echec de connexion", "Assurez-vous que la carte est fermement placée contre l'arrière de l'appareil, puis réessayez.", @@ -723,7 +723,7 @@ "OneKey Wallet", "Empreinte digitale {}", "Mise en charge", - "Scannez le code QR de la transaction ou signez le message", + "Scannez le code QR affiché sur l'application", "Format de données non pris en charge", "Le type de code QR n'est pas pris en charge, veuillez réessayer.", "Transaction invalide", @@ -743,11 +743,11 @@ "Désactiver l'espace d'air", "Êtes-vous sûr de désactiver le mode Air Gap ? Après désactivation, il pourra connecter votre appareil via Bluetooth ou USB.", "Sélectionnez la chaîne dont vous avez besoin, puis cliquez sur le bouton Créer.", - "Chaînes compatibles ETH et EVM", + "Réseaux ETH et EVM", "empreinte digitale non reconnue, réessayez", "Utilisez l'empreinte digitale ou appuyez pour déverrouiller", "SE Firmware", - "Batterie faible (<10%). Pour maintenir la batterie en bonne santé, veuillez vous assurer que l'appareil est complètement chargé avant un stockage à long terme.", + "Batterie faible (<20%). Pour maintenir la batterie en bonne santé, veuillez vous assurer que l'appareil est complètement chargé avant un stockage à long terme.", "Sélectionnez la manière de vous connecter", "Connecter Bluetooth", "Sélectionnez le portefeuille que vous souhaitez connecter", @@ -788,7 +788,6 @@ "Confirmer l'adresse", "Retournez à l'application et scannez le code QR ci-dessous.", "Réseaux compatibles BTC et EVM", - "Réseaux compatibles ETH et EVM", "Ouvrez OneKey et scannez le code QR ci-dessous", "Exporter le compte", "Connexion...", @@ -801,5 +800,79 @@ "Les deux OneKey Lite utilisés pour la connexion ne sont pas les mêmes.", "Quitter le processus de sauvegarde ?", "Si vous quittez maintenant, vous devrez revérifier votre phrase de récupération lorsque vous reviendrez. Êtes-vous sûr de vouloir quitter?", + "Phrase de récupération non prise en charge", + "Le portefeuille matériel actuel ne prend en charge que les phrases de récupération de 12, 18 et 24 mots.\nCette sauvegarde Lite ne peut pas être restaurée.", + "Ouvrez OneKey et scannez le code QR, prenant en charge les réseaux BTC et EVM.", + "Incompatibilité de portefeuille", + "Votre portefeuille sélectionné dans l'application ne correspond pas au portefeuille matériel. Veuillez vérifier et réessayer.", + "Signature de message non standard.", + "Envoyer des jetons", + "La sortie de transaction suivante contient des données de contrat :", + "Vous utilisez une autorisation {type}, assurez-vous que la dApp est fiable pour éviter toute perte d'actifs.", + "Impossible de sauvegarder", + "Le mode Airgap est activé et le NFC est désactivé. Vous ne pouvez donc pas effectuer de sauvegarde sur OneKey Lite. Veuillez désactiver le mode Airgap et réessayer.", + "Accéder aux paramètres", + "Certifications", + "Mon adresse", + "Sélectionner le réseau", + "Adresse {network}", + "Sélectionner le chemin de dérivation", + "Sélectionner un compte", + "Aller au compte", + "Erreur de formatage d'entrée", + "Votre adresse est une adresse de réseau EVM. Vous pouvez l'utiliser pour gérer vos actifs sur d'autres réseaux compatibles EVM (tels que Ethereum, BNB Chain, Polygon, Arbitrum One, Avalanche, etc.).", + "Clés de sécurité", + "Configuration du portefeuille requise", + "Pour jumeler votre appareil via Bluetooth, veuillez d'abord créer un portefeuille sur votre appareil matériel. Cela est nécessaire pour une connexion sécurisée.", + "Appairage Bluetooth indisponible en mode Boot", + "Pour mettre à jour votre appareil, visitez firmware.onekey.so sur votre ordinateur et connectez-vous via USB pour installer le firmware.", + "Vérifier maintenant", + "Connecter l'appareil", + "Ouvrez l'application OneKey et connectez votre appareil pour créer un portefeuille. La vérification de l'appareil sera effectuée automatiquement.", + "Vérifier l'appareil", + "Visitez https://bit.ly/3ZsHB40 pour des méthodes de vérification supplémentaires", + "Plus de réseaux", + "Afficher moins", + "L'URL du bootloader nécessite une vérification de l'appareil dans l'application OneKey 5.5.0+.", + "Téléchargez l'application OneKey à : onekey.so/download", + "Clés FIDO", + "Sécurisez les comptes avec des clés de sécurité FIDO", + "Supprimer la clé FIDO ?", + "Cette clé FIDO sera supprimée définitivement", + "Clé FIDO retirée", + "Limite de clé atteinte", + "Limite de 60 clés FIDO atteinte. Supprimez les inutilisées pour en ajouter de nouvelles", + "Gérer les clés FIDO", + "Pas encore de clés FIDO", + "Utilisation des appareils OneKey comme clés de sécurité", + "S'inscrire", + "Branchez votre OneKey à votre ordinateur", + "Accédez aux paramètres de sécurité du site web (par exemple, Google, Facebook)", + "Sélectionnez l'option \"Add Security Key\"", + "Confirmer avec OneKey (déverrouillage requis)", + "Choisissez \"Security Key\" lors de la connexion", + "Approuver avec OneKey", + "Authentifier", + "Faites glisser pour déverrouiller", + "5 tentatives échouées. Glissez pour continuer", + "Faites glisser pour continuer", + "Retour à l'accueil", + "Enregistrement de la clé FIDO...", + "Clé FIDO enregistrée", + "Batterie faible", + "Mise hors tension", + "Verrouillage/Arrêt automatique", + "Signature brute Solana", + "Permet de signer des messages Solana bruts sans traitement ni validation. Cela peut vous exposer à des tentatives de phishing, à des signatures aveugles et à des approbations non autorisées. Utilisez avec prudence.", + "Activer la signature brute Solana ?", + "Cela peut vous exposer à des tentatives de phishing, à des signatures à l'aveugle et à des transactions non autorisées. Activez uniquement si vous comprenez pleinement les risques.", + "Risque de phishing et de signature à l'aveugle. Ne continuez que si vous faites confiance à la source.", + "Protection de sécurité", + "Tutoriel de base", + "Réseaux BTC, SOL, ETH et EVM", + "OneKey App", + "Candidat", + "Réseau cible", + "Retirer le vote", ] # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/it.py b/core/src/trezor/lvglui/i18n/locales/it.py index 15c4ad3094..598c6698a4 100644 --- a/core/src/trezor/lvglui/i18n/locales/it.py +++ b/core/src/trezor/lvglui/i18n/locales/it.py @@ -200,7 +200,7 @@ "Codice di accoppiamento errato, riprovare.", "{} secondi", "Costume", - "Visualizza i dati", + "Visualizza dati", "Dimensione", "Dati", "Si vuole davvero bloccare automaticamente il dispositivo dopo {}?", @@ -583,7 +583,7 @@ "Connettiti all'app OneKey, trova l'NFT di proprietà di questo portafoglio hardware e tocca il pulsante Ritira.", "È comunque possibile eseguire il backup con KeyTag in seguito in \"Portafoglio - Verifica frase di ripristino\".", "Se desideri eseguire un altro backup del KeyTag, puoi visualizzare la mappa dei punti del KeyTag in \"Wallet - Check Recovery Phrase\" dopo aver verificato la tua frase di ripristino.", - "Controllo di autenticità", + "Autenticazione del dispositivo", "Disabilita la compatibilità Trezor", "Ripristina la compatibilità di Trezor", "Compatibile con Trezor", @@ -687,7 +687,7 @@ "Errore PIN Lite", "I dati su questa carta verranno cancellati dopo {} tentativi errati.", "Lite è stato ripristinato", - "Il PIN è stato inserito in modo errato più di 10 volte. OneKey Lite è stato autocancellato per impedire il cracking della forza bruta dei dati di backup.", + "Dopo 10 inserimenti consecutivi di PIN errati, OneKey Lite è stato automaticamente cancellato per impedire attacchi brute force sui dati di backup.", "capito", "Connessione fallita", "Assicurati che la scheda sia posizionata saldamente contro il retro del dispositivo, quindi riprova.", @@ -723,7 +723,7 @@ "OneKey Wallet", "Impronta digitale {}", "In carica", - "Scansiona il codice QR della transazione o firma il messaggio", + "Scansiona il codice QR visualizzato sull'app", "Formato dati non supportato", "Il tipo di codice QR non è supportato, riprova.", "Transazione non valida", @@ -743,11 +743,11 @@ "Disabilita traferro", "Sei sicuro di disabilitare la modalità Air Gap? Dopo la disattivazione, sarà in grado di connettere il tuo dispositivo tramite Bluetooth o USB.", "Seleziona la catena che ti serve, quindi fai clic sul pulsante Crea.", - "Catene compatibili con ETH ed EVM", + "Reti ETH ed EVM", "impronta digitale non riconosciuta, riprovare", "Usa l'impronta digitale o tocca per sbloccare", "Firmware SE", - "Batteria scarica (<10%). Per mantenere la batteria in buona salute, assicurati che il dispositivo sia completamente carico prima di un lungo periodo di immagazzinamento.", + "Batteria scarica (<20%). Per mantenere la batteria in buona salute, assicurati che il dispositivo sia completamente carico prima di un lungo periodo di immagazzinamento.", "Seleziona il modo per connetterti", "Connetti Bluetooth", "Seleziona il portafoglio che vuoi collegare", @@ -788,7 +788,6 @@ "Conferma Indirizzo", "Torna all'app e scansiona il codice QR qui sotto.", "Reti compatibili con BTC ed EVM", - "Reti compatibili con ETH ed EVM", "Apri OneKey e scansiona il codice QR qui sotto", "Esporta Account", "Connessione in corso...", @@ -797,9 +796,83 @@ "Conferma il PIN di OneKey Lite", "I PIN non corrispondono, si prega di confermare nuovamente.", "Connetti di nuovo", - "Esci", + "Uscita", "I due OneKey Lite utilizzati per la connessione non sono uguali.", "Uscire dal processo di backup?", "Se esci adesso, dovrai verificare nuovamente la frase di recupero quando rientri. Sei sicuro di voler uscire?", + "Frase di recupero non supportata", + "Il portafoglio hardware attuale supporta solo frasi di recupero da 12, 18 e 24 parole.\nQuesto backup Lite non può essere ripristinato.", + "Apri OneKey e scansiona il codice QR, supportando le reti BTC ed EVM.", + "Incompatibilità del portafoglio", + "Il portafoglio selezionato nell'app non corrisponde al portafoglio hardware. Si prega di controllare e riprovare.", + "Firma del messaggio non standard.", + "Invia token", + "Il seguente output di transazione contiene i dati del contratto:", + "Stai utilizzando l'autorizzazione {type} , assicurati che la dApp sia affidabile per evitare la perdita di risorse.", + "Impossibile eseguire il backup", + "La modalità Airgap è abilitata e NFC è disabilitato, quindi non puoi eseguire il backup su OneKey Lite. Disattiva la modalità Airgap e riprova.", + "Vai a Impostazioni", + "Certificazioni", + "Il mio indirizzo", + "Seleziona rete", + "{network} Indirizzo", + "Seleziona percorso di derivazione", + "Seleziona Account", + "Vai all'account", + "Errore di formattazione dell'input", + "Il tuo indirizzo è un indirizzo di rete EVM. Puoi usarlo per gestire i tuoi asset su altre reti compatibili con EVM (come Ethereum, BNB Chain, Polygon, Arbitrum One, Avalanche, ecc.).", + "Chiavi di sicurezza", + "Configurazione del portafoglio richiesta", + "Per associare il tuo dispositivo tramite Bluetooth, crea prima un portafoglio sul tuo dispositivo hardware. Questo è necessario per una connessione sicura.", + "Associazione Bluetooth non disponibile in modalità Boot", + "Per aggiornare il tuo dispositivo, visita firmware.onekey.so sul tuo computer e connettiti tramite USB per installare il firmware.", + "Verifica ora", + "Collega dispositivo", + "Apri l'app OneKey e collega il tuo dispositivo per creare un portafoglio. La verifica del dispositivo verrà eseguita automaticamente", + "Verifica dispositivo", + "Visita https://bit.ly/3ZsHB40 per metodi di verifica aggiuntivi", + "Altre reti", + "Mostra meno", + "L'URL del bootloader richiede la verifica del dispositivo nell'app OneKey 5.5.0+", + "Scarica l'app OneKey su: onekey.so/download", + "Chiavi FIDO", + "Proteggi gli account con le chiavi di sicurezza FIDO", + "Rimuovere la chiave FIDO?", + "Questa chiave FIDO verrà rimossa permanentemente", + "Chiave FIDO rimossa", + "Limite di chiavi raggiunto", + "Limite di 60 chiavi FIDO raggiunto. Rimuovi quelle non utilizzate per aggiungerne di nuove", + "Gestire le chiavi FIDO", + "Nessuna chiave FIDO ancora", + "Utilizzare i dispositivi OneKey come chiavi di sicurezza", + "Registrati", + "Collega il tuo OneKey al computer", + "Vai alle impostazioni di sicurezza del sito web (ad esempio, Google, Facebook)", + "Seleziona l'opzione \"Add Security Key\"", + "Conferma con OneKey (sblocco richiesto)", + "Scegli \"Security Key\" al login", + "Approva con OneKey", + "Autenticare", + "Scorri per sbloccare", + "5 tentativi falliti. Scorri per continuare", + "Scorri per continuare", + "Torna alla home", + "Registrazione della chiave FIDO in corso...", + "Chiave FIDO registrata", + "Batteria scarica", + "Spegnimento", + "Blocco automatico/spegnimento", + "Firma Raw di Solana", + "Permette di firmare messaggi Solana grezzi senza elaborazione o validazione. Questo potrebbe esporvi a phishing, firme alla cieca e approvazioni non autorizzate. Usare con cautela.", + "Abilitare Solana Raw Signing?", + "Questo potrebbe esporvi a phishing, firme alla cieca e transazioni non autorizzate. Abilitate solo se comprendete pienamente i rischi.", + "Rischio di phishing e firma alla cieca. Procedi solo se ti fidi della fonte.", + "Protezione della sicurezza", + "Tutorial di base", + "Reti BTC, SOL, ETH e EVM", + "OneKey App", + "Candidato", + "Rete target", + "Rimuovi voto", ] # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/ja.py b/core/src/trezor/lvglui/i18n/locales/ja.py index 0b3c2d0882..30cd7f0dd5 100644 --- a/core/src/trezor/lvglui/i18n/locales/ja.py +++ b/core/src/trezor/lvglui/i18n/locales/ja.py @@ -583,7 +583,7 @@ "OneKey アプリに接続し、このハードウェア ウォレットが所有する NFT を見つけて、[収集] ボタンをタップします。", "後で「Wallet - Check Recovery Phrase」で KeyTag を使用してバックアップすることもできます。", "別の KeyTag バックアップを作成する場合は、リカバリ フレーズを確認した後、「Wallet - Check Recovery Phrase」で KeyTag ドットマップを表示できます。", - "真正性チェック", + "デバイス認証", "Trezor の互換性を無効にする", "Trezor の互換性を復元する", "トレザーに対応", @@ -665,7 +665,7 @@ "エラー制限達成、PINコードを入力。", "上にスワイプしてロックを解除します", "最大 3 つの指紋を同時に記録できます。", - "デバイスのロックを解除する", + "ロック解除", "この指紋を削除しますか?", "この指紋を削除してもよろしいですか?", "指紋を設定したら、それを使用してデバイスのロックを解除できます。対応するシナリオで、登録した指で電源ボタンをタッチします。設定でいつでも変更できます。", @@ -687,7 +687,7 @@ "ライトPINエラー", "{} 回失敗すると、このカードのデータが消去されます。", "ライトがリセットされました", - "PIN を 10 回以上間違えて入力しました。 OneKey Lite は、バックアップ データのブルート フォース クラッキングを防ぐために自己消去されています。", + "10 回連続して間違った PIN を入力すると、バックアップ データへのブルート フォース攻撃を防ぐために、OneKey Lite は自動的に消去されます。", "わかった", "通信失敗", "カードがデバイスの背面にしっかりと配置されていることを確認してから、もう一度お試しください。", @@ -723,7 +723,7 @@ "OneKey Wallet", "指紋 {}", "充電", - "QRコードをスキャンまたはメッセージに署名", + "アプリに表示されているQRコードをスキャンしてください", "サポートされていないデータ形式", "QR コードの種類はサポートされていません。もう一度お試しください。", "無効なトランザクション", @@ -743,11 +743,11 @@ "エアギャップを無効にする", "エアギャップモードを無効にしてもよろしいですか?無効にすると、Bluetooth または USB 経由でデバイスに接続できるようになります。", "必要なチェーンを選択し、「作成」ボタンをクリックします。", - "ETHおよびEVM互換チェーン", + "ETH および EVM ネットワーク", "指紋が認識されません。もう一度お試しください", "指紋またはタップを使用してロックを解除します", "SEファームウェア", - "バッテリー残量が低い(<10%)。バッテリーの健康を維持するために、長期保管前にデバイスが完全に充電されていることを確認してください。", + "バッテリー残量が低い(<20%)。バッテリーの健康を維持するために、長期保管前にデバイスが完全に充電されていることを確認してください。", "接続方法を選択してください", "Bluetooth接続", "接続するウォレットを選択してください", @@ -788,7 +788,6 @@ "住所の確認", "アプリに戻り、以下のQRコードをスキャンしてください。", "BTC および EVM 互換ネットワーク", - "ETHおよびEVM互換ネットワーク", "OneKeyを開いて下のQRコードをスキャンしてください", "アカウントをエクスポート", "接続中...", @@ -801,5 +800,79 @@ "接続に使用される2つのOneKey Liteが同じではありません。", "バックアッププロセスを終了しますか?", "今終了すると、再度ログインするときに回復フレーズを再確認する必要があります。終了してもよろしいですか?", + "サポートされていないリカバリーフレーズ", + "現在のハードウェアウォレットは、12語、18語、24語のリカバリーフレーズのみをサポートしています。\nこのLiteバックアップは復元できません。", + "OneKey を開いて QR コードをスキャンします。BTC および EVM ネットワークをサポートします。", + "ウォレットの不一致", + "アプリ内の選択されたウォレットがハードウェアウォレットと一致していません。確認してもう一度お試しください。", + "非標準のメッセージ署名。", + "トークンを送信", + "次のトランザクション出力には契約データが含まれています。", + "{type}認証を使用しています。資産の損失を避けるために、dApp が信頼できるものであることを確認してください。", + "バックアップできません", + "エアギャップ モードが有効で NFC が無効になっているため、OneKey Lite にバックアップできません。エアギャップ モードを無効にして、もう一度お試しください。", + "設定へ進む", + "認証", + "私の住所", + "ネットワークを選択", + "{network}アドレス", + "派生パスを選択", + "アカウントを選択", + "アカウントに移動", + "入力フォーマットエラー", + "あなたのアドレスはEVMネットワークアドレスです。他のEVM互換ネットワーク(Ethereum、BNB Chain、Polygon、Arbitrum One、Avalancheなど)で資産を管理するために使用できます。", + "セキュリティキー", + "ウォレットの設定が必要です", + "デバイスをBluetoothでペアリングするには、まずハードウェアデバイスでウォレットを作成してください。これは安全な接続のために必要です。", + "Bluetooth ペアリングはブートモード中は利用できません", + "デバイスを更新するには、コンピュータで firmware.onekey.so にアクセスし、USB で接続してファームウェアをインストールしてください。", + "今すぐ確認", + "デバイスを接続する", + "OneKey アプリを開いて、デバイスを接続してウォレットを作成します。デバイスの確認は自動的に行われます。", + "デバイスを確認", + "追加の確認方法については、https://bit.ly/3ZsHB40 をご覧ください。", + "その他のネットワーク", + "表示を減らす", + "ブートローダー URL は OneKey アプリ 5.5.0+ でデバイスの確認が必要です", + "OneKeyアプリをダウンロードするには、onekey.so/downloadをご利用ください。", + "FIDOキー", + "FIDOセキュリティキーでアカウントを保護する", + "FIDOキーを削除しますか?", + "このFIDOキーは永久に削除されます", + "FIDOキーが削除されました", + "キーの制限に達しました", + "60個のFIDOキーの制限に達しました。新しいものを追加するには、未使用のものを削除してください", + "FIDOキーを管理する", + "まだFIDOキーがありません", + "OneKeyデバイスをセキュリティキーとして使用する", + "登録", + "コンピュータに OneKey を接続してください", + "ウェブサイトのセキュリティ設定に移動します(例:Google、Facebook)", + "「セキュリティキーを追加」オプションを選択", + "OneKeyで確認(ロック解除が必要)", + "ログイン時に「Security Key」を選択してください", + "OneKeyで承認", + "認証", + "スライドしてロック解除", + "5回失敗しました。スライドして続行してください", + "スライドして続行", + "ホームに戻る", + "FIDOキーを登録中...", + "FIDOキーが登録されました", + "バッテリー残量低下", + "電源オフ", + "自動ロック/シャットダウン", + "Solana ローサイニング", + "生の Solana メッセージを処理や検証なしで署名することができます。これにより、フィッシング、ブラインドサイン、無許可の承認にさらされる可能性があります。注意して使用してください。", + "SolanaのRaw Signingを有効にしますか?", + "これにより、フィッシング、盲目的な署名、無許可の取引にさらされる可能性があります。リスクを十分に理解している場合のみ有効にしてください。", + "フィッシングや盲目的な署名のリスクがあります。信頼できる情報源である場合のみ進めてください。", + "セキュリティ保護", + "基本チュートリアル", + "BTC、SOL、ETH、EVM ネットワーク", + "OneKey App", + "候補者", + "ターゲットネットワーク", + "投票を削除", ] # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/ko.py b/core/src/trezor/lvglui/i18n/locales/ko.py index 0d417762ae..703c138695 100644 --- a/core/src/trezor/lvglui/i18n/locales/ko.py +++ b/core/src/trezor/lvglui/i18n/locales/ko.py @@ -82,7 +82,7 @@ "암호 비활성화", "암호구 암호화를 비활성화하시겠습니까?", "이 장치에 항상 암호를 입력하시겠습니까?", - "암호 소스", + "Passphrase 소스", "장치에 항상 암호 입력 설정을 취소하시겠습니까?", "암호 입력", "연결된 장치에 암호를 입력하십시오.", @@ -583,7 +583,7 @@ "OneKey 앱에 연결하고 이 하드웨어 지갑이 소유한 NFT를 찾은 다음 수집 버튼을 누릅니다.", "나중에 \"Wallet - Check Recovery Phrase\"에서 KeyTag로 백업할 수 있습니다.", "다른 KeyTag 백업을 하고 싶다면 복구 문구를 확인한 후 \"Wallet - 복구 문구 확인\"에서 KeyTag 도트맵을 볼 수 있습니다.", - "진위 확인", + "기기 인증", "Trezor 호환성 비활성화", "Trezor 호환성 복원", "트레저와 호환 가능", @@ -687,7 +687,7 @@ "라이트 PIN 오류", "{}번 잘못 시도하면 이 카드의 데이터가 삭제됩니다.", "라이트가 재설정되었습니다", - "PIN이 10회 이상 잘못 입력되었습니다. OneKey Lite는 백업 데이터의 무차별 대입 크래킹을 방지하기 위해 자체 삭제되었습니다.", + "10번 연속으로 잘못된 PIN을 입력하면 OneKey Lite가 자동으로 삭제되어 백업 데이터에 대한 무차별 대입 공격을 방지합니다.", "알겠어요", "연결 실패", "카드가 장치 뒷면에 단단히 고정되어 있는지 확인한 후 다시 시도하십시오.", @@ -723,7 +723,7 @@ "OneKey Wallet", "지문 {}", "충전 중", - "거래 QR 코드를 스캔하거나 메시지에 서명하세요", + "앱에 표시된 QR 코드를 스캔하세요", "지원되지 않는 데이터 형식", "QR 코드 유형이 지원되지 않습니다. 다시 시도해 주세요.", "잘못된 거래", @@ -743,11 +743,11 @@ "에어 갭 비활성화", "Air Gap 모드를 비활성화하시겠습니까? 비활성화한 후에는 Bluetooth 또는 USB를 통해 장치를 연결할 수 있습니다.", "필요한 체인을 선택한 후 생성 버튼을 클릭하세요.", - "ETH 및 EVM 호환 체인", + "ETH & EVM 네트워크", "지문이 인식되지 않습니다. 다시 시도하세요.", "지문을 사용하거나 탭하여 잠금 해제", "SE 펌웨어", - "배터리 부족 (<10%). 배터리를 건강하게 유지하기 위해 장기 보관 전에 장치가 완전히 충전되었는지 확인하십시오.", + "배터리 부족 (<20%). 배터리를 건강하게 유지하기 위해 장기 보관 전에 장치가 완전히 충전되었는지 확인하십시오.", "연결 방법을 선택하세요", "블루투스 연결", "연결하려는 지갑을 선택하세요", @@ -788,7 +788,6 @@ "주소 확인", "앱으로 돌아가서 아래의 QR 코드를 스캔하세요.", "BTC 및 EVM\u0036호환 네트워크", - "ETH 및 EVM 호환 네트워크", "OneKey를 열고 아래 QR 코드를 스캔하세요.", "계정 내보내기", "연결 중...", @@ -797,9 +796,83 @@ "OneKey Lite PIN 확인", "PIN이 일치하지 않습니다. 다시 확인해 주세요.", "다시 연결하기", - "종료", + "출구", "연결에 사용된 두 개의 OneKey Lite가 동일하지 않습니다.", "백업 프로세스를 종료하시겠습니까?", "지금 탈퇴하시면, 재가입 시 복구 문구를 다시 확인하셔야 합니다. 종료하시겠습니까?", + "지원되지 않는 복구 구문", + "현재 하드웨어 지갑은 12, 18, 24단어 복구 문구만 지원합니다. 이 Lite 백업은 복원할 수 없습니다.", + "OneKey를 열고 QR 코드를 스캔하여 BTC 및 EVM 네트워크를 지원하세요.", + "지갑 불일치", + "앱에서 선택한 지갑이 하드웨어 지갑과 일치하지 않습니다. 확인 후 다시 시도해 주세요.", + "비표준 메시지 서명.", + "토큰 보내기", + "다음 거래 출력에는 계약 데이터가 포함되어 있습니다.", + "{type} 인증을 사용하고 있으니 자산 손실을 방지하기 위해 dApp이 신뢰할 수 있는지 확인하세요.", + "백업할 수 없습니다", + "Airgap 모드가 활성화되어 있고 NFC가 비활성화되어 있으므로 OneKey Lite로 백업할 수 없습니다. Airgap 모드를 비활성화하고 다시 시도하세요.", + "설정으로 이동", + "인증", + "내 주소", + "네트워크 선택", + "{network} 주소", + "파생 경로 선택", + "계정 선택", + "계정으로 이동", + "입력 형식 오류", + "귀하의 주소는 EVM 네트워크 주소입니다. 이를 사용하여 Ethereum, BNB Chain, Polygon, Arbitrum One, Avalanche 등 다른 EVM 호환 네트워크에서 자산을 관리할 수 있습니다.", + "보안 키", + "지갑 설정 필요", + "Bluetooth를 통해 기기를 연결하려면 먼저 하드웨어 기기에서 지갑을 생성하세요. 이는 안전한 연결을 위해 필요합니다.", + "부트 모드에서는 Bluetooth 페어링이 불가능합니다.", + "기기를 업데이트하려면 컴퓨터에서 firmware.onekey.so를 방문하고 USB를 통해 연결하여 펌웨어를 설치하세요.", + "지금 확인하세요", + "기기 연결", + "OneKey 앱을 열고 기기를 연결하여 지갑을 만드세요. 기기 검증은 자동으로 수행됩니다.", + "기기 인증", + "추가 인증 방법은 https://bit.ly/3ZsHB40 를 방문하세요", + "더 많은 네트워크", + "더 적게 표시", + "부트로더 URL 은 OneKey App 5.5.0+ 에서 기기 인증이 필요합니다.", + "OneKey 앱을 다운로드하세요: onekey.so/download", + "FIDO 키", + "FIDO 보안 키로 계정을 보호하세요", + "FIDO 키를 제거하시겠습니까?", + "이 FIDO 키는 영구적으로 제거됩니다", + "FIDO 키 제거됨", + "키 제한에 도달했습니다", + "60 FIDO 키 제한에 도달했습니다. 새로 추가하려면 사용하지 않는 키를 제거하세요.", + "FIDO 키 관리", + "아직 FIDO 키가 없습니다", + "OneKey 장치를 보안 키로 사용하기", + "등록", + "컴퓨터에 OneKey 를 연결하세요", + "웹사이트(예: Google, Facebook)의 보안 설정으로 이동하세요", + "\"보안 키 추가\" 옵션 선택", + "OneKey로 확인 (잠금 해제 필요)", + "로그인 시 \"Security Key\"를 선택하세요", + "OneKey로 승인", + "인증하다", + "밀어서 잠금 해제", + "5번 시도 실패. 계속하려면 슬라이드하세요", + "슬라이드하여 계속", + "홈으로 돌아가기", + "FIDO 키 등록 중...", + "FIDO 키 등록됨", + "배터리 부족", + "전원 끄기", + "자동 잠금/종료", + "Solana Raw Signing", + "처리나 검증 없이 원시 Solana 메시지를 서명할 수 있습니다. 이는 피싱, 맹목적 서명, 무단 승인에 노출될 수 있습니다. 주의해서 사용하세요.", + "Solana 원시 서명을 활성화하시겠습니까?", + "이것은 피싱, 맹목적 서명, 그리고 무단 거래에 노출될 수 있습니다. 위험을 완전히 이해한 경우에만 활성화하십시오.", + "피싱 및 맹목적 서명의 위험. 출처를 신뢰할 경우에만 진행하세요.", + "보안 보호", + "기본 튜토리얼", + "BTC, SOL, ETH 및 EVM 네트워크", + "OneKey App", + "후보자", + "대상 네트워크", + "투표 제거", ] # fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/pt_br.py b/core/src/trezor/lvglui/i18n/locales/pt_br.py new file mode 100644 index 0000000000..33b1f4df1a --- /dev/null +++ b/core/src/trezor/lvglui/i18n/locales/pt_br.py @@ -0,0 +1,878 @@ +# fmt: off +translations = [ + "Continue", + "Selecione o idioma", + "Criar Nova Carteira", + "Início Rápido", + "Iniciar", + "Restaurar Carteira", + "Cancelar", + "Confirmar", + "Usar um PIN forte para proteger sua carteira de acessos físicos não autorizados.", + "Mantenha seu PIN seguro, certifique-se de guardá-lo separado da frase de recuperação.", + "Defina um PIN", + "Insira o Novo PIN", + "Digite o PIN Novamente", + "Not Match", + "Os PINs que você inseriu não correspondem. Por favor, tente novamente.", + "PIN Habilitado", + "Você ativou com sucesso a proteção por PIN.", + "Backup", + "Carteira está Pronta", + "Nova carteira criada com sucesso! Você deve fazer o backup da sua nova carteira agora mesmo.", + "Pular", + "Aviso", + "Pular o backup? Você pode fazer o backup do seu OneKey uma vez, a qualquer momento.", + "Mantendo seu backup seguro e nunca o envie para ninguém.", + "A próxima tela exibirá um conjunto de palavras, chamado Frase de Recuperação. Então, você precisa saber:", + "Frase de Recuperação do Backup", + "Nunca tire fotos ou faça cópias digitais, e nunca faça upload delas online.", + "Se você perder a frase de recuperação, perderá todo o seu fundo.", + "Mantenha pressionado para Confirmar", + "Anote as seguintes {} palavras em ordem.", + "Backup Manual", + "Escolha a palavra correta.", + "Próximo", + "Verifique a Palavra #{}", + "Você terminou de verificar sua frase de recuperação.", + "Verificado", + "Backup Completo", + "Use seu backup quando precisar recuperar sua carteira.", + "Habilitar PIN", + "Você deseja ativar a proteção por PIN?", + "Alterar Etiqueta", + "Você realmente quer mudar a etiqueta para \"{}\"?", + "Importe a frase de recuperação para restaurar a carteira existente.", + "Selecione o Número de Palavras", + "é seguro ejetar e continuar mais tarde.", + "Abortar", + "Abortar Recuperação", + "Você tem certeza de que deseja sair do processo de recuperação? Todo o progresso será perdido.", + "Selecione o número de palavras.", + "Pronto para Restaurar", + "Insira a Frase de Recuperação", + "Entrar", + "Digite a palavra #{} ", + "Tente Novamente", + "Frase de Recuperação Inválida", + "A frase de recuperação que você inseriu é inválida. Verifique seu backup e tente novamente.", + "Você recuperou sua carteira com sucesso.", + "Verifique seu backup, certifique-se de que ele corresponde exatamente à frase de recuperação armazenada no dispositivo.", + "Verificar Frase de Recuperação", + "Verificação de Aborto", + "Você quer abortar a verificação da frase de recuperação?", + "Pronto para Verificar!", + "A frase de recuperação inserida é válida, mas não corresponde à que está no dispositivo.", + "Correto", + "A frase de recuperação que você inseriu está correta, seu backup está correto.", + "Você quer alterar seu PIN?", + "Mudar PIN", + "Digite o PIN antigo", + "Fechar", + "O PIN que você inseriu está incorreto.", + "Senha incorreta", + "PIN Alterado", + "Você alterou seu PIN com sucesso.", + "Feito", + "PIN Desativado", + "Você desativou com sucesso a proteção por PIN.", + "Digite o PIN", + "PIN incorreto, {} tentativas restantes", + "Habilitar Senha", + "Desativar Senha", + "Você deseja desativar a criptografia de frase secreta?", + "Você quer sempre inserir a frase secreta neste dispositivo?", + "Fonte da Frase Secreta", + "Você deseja revogar a configuração de sempre inserir a senha no dispositivo?", + "Digite a Frase Secreta", + "Por favor, digite sua frase secreta no dispositivo conectado.", + "Verifique o PIN", + "Limpar Dispositivo", + "Para remover todos os dados do seu aparelho, você pode restaurar seu aparelho para as configurações de fábrica.", + "Restauração de Fábrica", + "Após o reset, a frase de recuperação neste dispositivo será permanentemente excluída.", + "Mantenha pressionado para reiniciar", + "Você ainda pode restaurar seus fundos a partir do backup da frase de recuperação.", + "Isso apagará todos os dados no armazenamento interno e no Elemento Seguro (SE). Antes, você precisa saber:", + "Dispositivo reiniciado com sucesso, reinicie o sistema agora.", + "Reset Completo", + "Reiniciar", + "{} Endereço", + "Caminho", + "Endereço", + "Exportar", + "Chave Pública", + "{} Chave Pública", + "Mensagem", + "Assinar", + "Assinar {} Mensagem", + "PARA", + "Visualizar Transação", + "ENVIAR", + "Rejeitar", + "Assinar {} Transação", + "Mantenha pressionado para assinar", + "Quantidade", + "Preço do Gás", + "Taxa Máxima", + "Para", + "Valor Total", + "De", + "Taxa Máxima por Gás", + "Taxa de Prioridade por Gás", + "Contrato", + "Token desconhecido", + "{} Endereço\n(Taproot)", + "{} Endereço\n(Legacy Segwit)", + "{} Endereço\n(Segwit)", + "Verificar Caminho", + "xPub #{} (meu)", + "{} Endereço Multisig\n({} de {})", + "xPub #{} (coassinante)", + "xPub", + "{} Descritor", + "Taxa", + "Exportar Chave de Observação", + "Você deseja exportar credenciais somente para visualização?", + "Rede", + "Segurança", + "Geral", + "Crypto", + "Desligar", + "Configurações", + "Sobre o Dispositivo", + "Conectar", + "Idioma", + "{} minutos", + "Nunca", + "Auto-travamento", + "USB", + "Resetar Dispositivo", + "Senha", + "Desligado", + "Ethereum", + "Assinatura Cega", + "On", + "{} A assinatura cega agora está desativada.", + "Assinatura Cega", + "A assinatura cega é necessária apenas para assinar algumas transações específicas de contratos inteligentes, que não podem ser reconhecidas no dispositivo. Antes de habilitar esse recurso, você precisa saber:", + "É recomendado desativar esse recurso após o uso.", + "Habilitar a assinatura cega terá certos riscos de segurança.", + "Habilitar {} Assinatura Cega?", + "Habilitar", + "Solana", + "{} A assinatura cega está agora ativada.", + "Desligando...", + "Toque para desbloquear", + "Guia do Usuário", + "Configuração", + "Pronto para Criar", + "A próxima tela mostrará a frase secreta que você inseriu no dispositivo conectado.", + "Acessar carteira oculta?", + "Esta é uma palavra errada, verifique seu backup e tente novamente.", + "Palavra Incorreta", + "Usar Esta Frase Secreta?", + "Modelo", + "Armazenamento", + "PIN incorreto, esta é a sua última tentativa", + "Modificar Taxa", + "Valor (Que Você Gasta)", + "Nova Taxa", + "Modificar Quantidade", + "Novo Valor", + "ID da Transação", + "Nome da Moeda", + "Rodadas Máximas", + "Taxa Máxima de Mineração", + "Identidade", + "Confirmar Transferência", + "Nome do Aplicativo", + "Nome da Conta", + "Confirmar {} Pagamento", + "Assinar {} Transação Conjunta", + "Diminuído Por", + "Aumentado Em", + "Sem Mudanças", + "Autorize o CoinJoin", + "Verificar {} Mensagem", + "Digite o código do par no seu dispositivo.", + "Emparelhamento Bluetooth", + "Código de par incorreto, tente novamente.", + "{} segundos", + "Personalizado", + "Ver Dados", + "Tamanho", + "Data", + "Você realmente quer bloquear automaticamente seu dispositivo após {}?", + "Modo experimental", + "Habilitar recursos experimentais?", + "Por Favor Aguarde", + "novo contrato?", + "Confirmar Taxa", + "Confirme os Dados", + "{} bytes", + "Confirme os Dados Digitados", + "Realmente assinar dados digitados EIP-712?", + "Mensagem de Confirmação", + "Sem campo de mensagem", + "Confirmar Transação", + "Taxa de Transação", + "Visualizar Tempo de Bloqueio", + "Defina o tempo de bloqueio para esta transação", + "Time", + "Taxa alta", + "A taxa está inesperadamente alta.", + "Há muitas saídas de mudança.", + "O tempo de bloqueio está definido, mas não terá efeito.", + "Altura do Bloco", + "Taxa de transação", + "Contagem de Mudanças", + "Reset FIDO2", + "Você realmente quer apagar todas as credenciais?", + "Obter Próximo Contador U2F", + "Você quer aumentar e recuperar o contador U2F?", + "Listar Credenciais", + "Você deseja exportar informações sobre as credenciais do residente armazenadas neste dispositivo?", + "Definir Contador U2F", + "Você deseja definir o contador U2F para {}?", + "Remover Credencial", + "Ver Mensagem Completa", + "Importar Credencial", + "A credencial que você está tentando importar não pertence a este autenticador.", + "Falha ao Emparelhar", + "O emparelhamento Bluetooth falhou, tente conectar novamente.", + "Eu anotei as palavras. Depois, segui o guia para verificar as palavras uma por uma.", + "Sem sementes", + "Falha no Backup!", + "Precisa de Backup!", + "PIN Não Definido!", + "Modo Experimental", + "U2F Já Registrado", + "Este dispositivo já está registrado com este aplicativo.", + "U2F Não Registrado", + "Este dispositivo não está registrado com este aplicativo.", + "FIDO2 Já Registrado", + "Registro U2F", + "Autenticação U2F", + "Este dispositivo já está registrado com {}.", + "Verificar Usuário FIDO2", + "Incapaz de verificar o usuário, por favor, habilite a proteção por PIN.", + "Autenticação FIDO2 Não Registrada", + "Registro FIDO2", + "Autenticação FIDO2", + "Brilho", + "Ver Dados Completos", + "Confirmar {}", + "Ver Estrutura Completa", + "Ver Array Completo", + "Assinar Dados Digitados", + "Você quer assinar os dados digitados EIP-712?", + "Contém {} chave", + "Matriz de {} {}", + "Confirmar Domínio", + "Teclado PIN", + "Aleatorizado", + "Ordenado", + "Ligar/Desligar", + "Pressione e segure o botão de ligar para ligar e desligar.", + "O que é Frase de Recuperação?", + "Uma \"chave privada\" legível para gerar sua carteira. Uma chave para restaurar todos os seus ativos cripto.", + "Habilite a Proteção por PIN", + "Mecanismo de proteção de segurança em nível de sistema, Defina um código PIN forte para proteger seus ativos cripto.", + "Como Funciona a Carteira de Hardware", + "OneKey armazena a frase de recuperação criptografada offline no elemento de segurança, conectando e interagindo com o aplicativo OneKey via bluetooth ou USB.", + "Senha: Acesse Carteiras Ocultas", + "A frase secreta pode ser considerada como um segundo fator para a \"palavra extra (mnemônica + 1) da carteira de hardware. Você pode acessar diferentes carteiras ocultas com diferentes frases secretas.", + "Precisa de Ajuda?", + "Tem alguma dúvida? Vá para o Centro de Suporte OneKey para obter ajuda.", + "O que é Frase de Recuperação", + "Guia", + "Criptografar Valor", + "Descriptografar Valor", + "Habilitar rotulagem?", + "Confirme a Entropia", + "Você quer exportar entropia? Continue apenas se souber o que está fazendo!", + "Extrair Firmware", + "Você quer extrair o firmware do dispositivo? Sua frase de recuperação não será revelada.", + "Você tem certeza de que deseja autenticar seu dispositivo com o servidor seguro OneKey? Toque em confirmar para verificar se o seu dispositivo é genuíno e não foi adulterado.", + "Toque para Fechar", + "Processando...", + "Deslize para Assinar", + "Deslize para Resetar", + "Deslize para Confirmar", + "USB Lock", + "O dispositivo será bloqueado automaticamente toda vez que o USB for conectado ou desconectado.", + "O dispositivo permanecerá desbloqueado quando o USB for conectado ou desconectado.", + "Vá para o Modo de Atualização", + "Você quer reiniciar o dispositivo no modo de atualização?", + "ID de Construção", + "Carteira", + "A frase secreta está ativada. O dispositivo precisará inserir a frase secreta toda vez que se conectar com os aplicativos OneKey.", + "A frase secreta está desativada.", + "Você deseja ativar a criptografia de frase secreta?", + "Desativar", + "Verifique a frase que você inseriu e certifique-se de que está correta.", + "{} Transferência", + "Pagador de Taxas", + "Nova Conta de Token:", + "Endereço Mint:", + "Proprietário:", + "Financiado por", + "Criar Conta de Token", + "Assinante", + "Transferência de Token", + "De (Conta Token)", + "Para (Conta Token)", + "Formato", + "Hash da Mensagem", + "Desconhecido", + "Memo", + "Modo de Atualização", + "Mude para o Modo de Atualização", + "Boardloader", + "Mude para o Boardloader", + "Você deseja reiniciar o dispositivo no carregador de inicialização?", + "Teclado Háptico", + "Háptico", + "Rigoroso", + "Verificações de Segurança", + "Prompt", + "Você quer impor verificações de segurança rigorosas? Isso fornecerá proteção de segurança completa.", + "Defina as Verificações de Segurança para Rigoroso", + "Defina as Verificações de Segurança para Solicitar", + "Permitirá temporariamente que você realize algumas ações com potencial risco.", + "Deslize para Desativar", + "Você tem certeza de que deseja desativar permanentemente as verificações de segurança? Continue apenas se souber o que está fazendo!", + "Baixe os aplicativos OneKey em", + "Download", + "Conectar Carteira", + "Toque em \"Conectar carteira de hardware\".", + "Conecte o dispositivo: {}.", + "Clique no botão Criar-Carteira.", + "Depois, o aplicativo OneKey irá recuperar as contas que você usou anteriormente.", + "Adicionar Conta", + "Clique no nome da conta no painel superior direito, depois clique no ícone Adicionar.", + "Tutorial do App OneKey", + "Definir Tela Inicial", + "Excluir", + "Definir Papel de Parede", + "Tela de Bloqueio", + "Durante o bloqueio, toque na tela para acordar o display.", + "Toque para Acordar", + "Durante o bloqueio, pressione o botão de ligar para acordar a tela.", + "Ligue o Bluetooth para se conectar com outros dispositivos Bluetooth próximos.", + "Certo!", + "Incorreto!", + "Atualização de Recurso", + "Você deseja atualizar o recurso de firmware?", + "Atualizar", + "Transação Assinada", + "Fechar ({}s)", + "Nota", + "Tipo", + "Fechar Restante Para", + "Rechavear Para", + "Congelar Conta", + "ID do Ativo", + "Congelar ID do Ativo", + "Ativo Congelado?", + "Verdadeiro", + "False", + "Valor do Ativo", + "Receptor de Ativo", + "Remetente do Ativo", + "Transferir ID do Ativo", + "Fechar Ativo Para", + "Parâmetros do Ativo", + "Nome do Ativo", + "URL", + "Endereço do Gerente", + "Endereço de Reserva", + "Endereço de Recuperação", + "Padrão Congelado?", + "Endereço de Congelamento", + "Total", + "Decimais", + "Nome da Unidade", + "Hash de Metadados", + "Chave Pública VRF", + "Chave Pública de Voto", + "Endereço de Revogação", + "Prova de Chave Pública do Estado", + "Não participação (Recompensa)?", + "Pagamento", + "Congelamento de Ativos", + "Transferência de Ativo", + "Configuração de Ativos", + "Registro de Chave", + "Transação Desconhecida", + "Tag de Destino", + "{} Transação", + "Enviar", + "para", + "Visualizar", + "Desativar Proteção por PIN", + "Um PIN protege seu dispositivo se ele for perdido ou roubado. Você tem certeza de que deseja desativar a proteção por PIN?", + "O dispositivo foi reiniciado, reinicie agora.", + "{} minuto", + "Desligamento", + "A tela nunca desligará.", + "O dispositivo nunca será desligado.", + "Vibração & Tátil", + "Feedback de vibração ao interagir com o sistema.", + "Visível como \"{}\" para outros dispositivos próximos.", + "Auto-Bloqueio", + "Definir como Tela Inicial", + "Você quer mudar a tela inicial?", + "Você quer alterar a tela inicial? Isso excluirá o papel de parede mais antigo enviado.", + "Deletar Papel de Parede?", + "Você tem certeza de que deseja excluir este papel de parede?", + "Conjunto", + "Gerenciar Papel de Parede", + "Teclado PIN", + "Padrão", + "Os números no teclado PIN estão organizados em sequência.", + "Os números no teclado PIN são organizados aleatoriamente.", + "Você quer assinar esta {} transação?", + "{} Dados Digitados", + "Interaja com:", + "Ver Memorando", + "Nome do Bluetooth", + "Número de Série", + "Atualização do Sistema", + "Tela inicial", + "Opções do Desenvolvedor", + "Importar Carteira", + "Crie uma nova carteira, ou restaure uma carteira previamente usada a partir do backup da frase de recuperação.", + "Defina um PIN de 4 a 50 dígitos para proteger sua carteira.", + "Isso irá gerar uma carteira padrão com um novo conjunto de frase de recuperação.", + "Bluetooth", + "Tela Inicial", + "Você quer mudar a imagem da tela inicial?", + "Você quer assinar esta transação?", + "Série", + "Quase Pronto!", + "Frase de Recuperação", + "Pronto para Importar", + "{} Palavras", + "Sair", + "Abortar importação?", + "Restaure seu dispositivo para as configurações de fábrica. ATENÇÃO: Isso removerá todos os dados do seu dispositivo.", + "Apague Este Dispositivo", + "Digite as palavras em ordem, certificando-se de que estão na mesma ordem que a sua frase de recuperação de backup.", + "Você tem certeza de que deseja abortar este processo? Todo o progresso será perdido.", + "Voltar", + "Pronto para Fazer Backup", + "Em seguida, siga o guia e verifique as palavras uma por uma, com base no backup da sua frase de recuperação.", + "O Safety-Checks está em execução, protegendo você de derivações de endereço não padrão (deve ser compatível com BIP-44), realizando transações potencialmente arriscadas ou taxas altas inesperadas.", + "As verificações de segurança estão permanentemente desativadas.", + "As verificações de segurança estão temporariamente desativadas. Elas serão reativadas após o reinício do dispositivo.", + "Desativar Verificações de Segurança?", + "Habilitar Verificações de Segurança?", + "Ele fornecerá proteção de segurança completa.", + "Palavra #{}", + "Safety-Checks protege você de realizar ações potencialmente arriscadas. é altamente recomendável ativá-lo.", + "Este dispositivo não está registrado com {}.", + "QR Code", + "{} Mensagem", + "Entrando no Boardloader", + "Deslize para cima para mostrar os aplicativos", + "Deslize para baixo para fechar", + "{} horas", + "{} hora", + "Enviado", + "Galeria NFT", + "Dicas", + "Após {} de inatividade, a tela será desligada.", + "Durante o bloqueio do dispositivo (tela desligada), ele será desligado após {} de inatividade.", + "\"{}\" é um caminho não padrão. Tem certeza de que deseja usar este caminho?", + "Em Breve", + "O tempo de bloqueio para esta transação está definido para:", + "Sem Itens", + "Colecione NFT", + "Você quer coletar este NFT?", + "Remova NFT", + "Você quer coletar este NFT? Você atingiu o limite de armazenamento, isso removerá o NFT carregado mais antigo.", + "Remover", + "Você tem certeza de que deseja remover este NFT? Você sempre pode adicionar o NFT existente de volta posteriormente.", + "{} item", + "{} itens", + "Animações", + "Permite efeitos dinâmicos na interface do usuário.", + "Efeitos dinâmicos da interface do usuário estão desativados.", + "Limite de Gás", + "Limite de Taxa de Gás", + "Gasolina Premium", + "Endereço do Contrato", + "ID do Token", + "Transferência de NFT", + "Você pode definir como tela inicial.", + "Gás", + "ID da Cadeia", + "Nome da Cadeia", + "Conta", + "Sequência", + "Delegador", + "Validador", + "Valor da Gorjeta", + "Caçamba", + "Granter", + "Depositar", + "Valor do Depósito", + "Denominação do Depósito", + "Endereço de Destino", + "Moedas de Destino", + "Endereço de Origem", + "Moedas de Origem", + "Fonte do Validador", + "Destino do Validador", + "Descrição", + "Propor", + "Proposta", + "ID da Proposta", + "Título", + "Opção", + "Votar", + "Delegado", + "Desatribuir", + "Redelegar", + "Propor", + "Depósito", + "Vote", + "Sacar Recompensa", + "Sacar Comissão do Validador", + "Multi Envio", + "nenhum", + "Conteúdo", + "Sucesso", + "A assinatura é válida.", + "Verifique", + "A seguinte saída de transação contém tokens.", + "Valor Enviado", + "Tipo de Transação", + "Registro de chave de participação", + "Delegação de participação", + "Desregistro de chave de participação", + "Registro de stakepool", + "Para a Piscina", + "Confirmando uma transação Plutus -- a perda de garantia é possível. Verifique todos os itens com cuidado.", + "A transação não contém entradas de garantia, o script Plutus não poderá ser executado.", + "ID de entrada", + "Índice de Entrada", + "Valor do colateral desconhecido, verifique todos os itens com cuidado.", + "A seguinte saída de transação contém hash de dados:", + "Datum Hash", + "Hash de Dados Auxiliares", + "Impressão Digital do Ativo", + "Hash de Dados do Script", + "Caminho do Assinante", + "Quantidade de Token", + "Atualização do Bootloader", + "Você quer atualizar o bootloader?", + "Backup com KeyTag", + "Já possui uma OneKey KeyTag? Toque no botão \"Backup\" para converter a frase de recuperação em um mapa de pontos BIP39.", + "Backup", + "Agora Não", + "Pular Backup", + "KeyTag", + "Frente (#{} - {})", + "Voltar (#{} - {})", + "Siga o mapa de pontos, perfure os pontos no KeyTag com um punção central para finalizar seu backup.", + "Finalizar Backup do KeyTag", + "Ver Verso", + "Conecte-se ao aplicativo OneKey, encontre o NFT pertencente a esta carteira de hardware e toque no botão Coletar.", + "Você ainda pode fazer backup com KeyTag mais tarde em \"Carteira - Verificar Frase de Recuperação\".", + "Se você quiser fazer outro backup do KeyTag, pode visualizar o dotmap do KeyTag em \"Carteira - Verificar Frase de Recuperação\" após verificar sua frase de recuperação.", + "Autenticação de Dispositivo", + "Desativar Compatibilidade com Trezor", + "Restaurar Compatibilidade com Trezor", + "Compatível com Trezor", + "Identificador USB:", + "Entra em vigor após a reinicialização do dispositivo.", + "Isso impedirá que você use clientes de carteira de terceiros e sites que suportam apenas Trezor.\nEntra em vigor após a reinicialização do dispositivo.", + "Se não tiver certeza, não altere essa configuração", + "{} Hash Digitado", + "Hash do Separador de Domínio", + "Assinar esta mensagem pode apresentar riscos de segurança. Assine apenas em sites em que você confia plenamente.", + "Assinar {} Hash Digitado", + "Você quer assinar este hash digitado?", + "Assinar {} Dados Digitados", + "Você quer assinar esses dados digitados?", + "Confirmar Exportação", + "Você realmente deseja exportar as credenciais somente para visualização?", + "Sincronização de Imagem-chave", + "Você realmente quer sincronizar as imagens-chave?", + "Confirmar Atualização", + "Você realmente quer iniciar a atualização?", + "Você realmente quer exportar tx_key?", + "Você realmente quer exportar tx_der para tx_proof?", + "Sincronizar", + "Atualizar", + "ID do Pagamento", + "Confirmar Hora de Desbloqueio", + "O tempo de desbloqueio para esta transação está definido para {}", + "Fonte", + "Dica", + "Manter Conectado", + "Saldo Congelado", + "Duração Congelada (Dia)", + "Endereço do destinatário", + "Recurso", + "Largura de banda", + "Energia", + "Carteira Criada", + "Trava", + "Detalhes da Transação", + "Ver Detalhes", + "Atualizar Transação", + "Finalize a Transação", + "Transação Meld", + "Sistema", + "Bootloader", + "Enviar \n{}", + "Detalhes", + "Interagindo com {}", + "Instruções", + "Taxas", + "Mais", + "Transação {}", + "Firmware Inseguro", + "Ainda instalando este firmware desconhecido?", + "Fornecedor alterado, ainda instalando este firmware desconhecido?", + "Dispositivo Limpo", + "Clique no botão abaixo para reiniciar.", + "Impressão digital", + "Maneira alternativa de desbloquear o PIN! Ative o sensor de identidade por impressão digital e facilite o seu acesso ao OneKey Pro.", + "Adicionar Impressão Digital", + "Comece Agora", + "Coloque seu dedo no sensor localizado na lateral do dispositivo.", + "Coloque o Dedo", + "Coloque seu dedo no botão de ligar e levante-o depois, repetindo este passo.", + "Ajustar Dedo", + "Deixe o resto da borda do seu dedo no sensor de impressão digital repetidamente.", + "Impressão Digital Adicionada.", + "{} foi adicionado e pode ser usado para funções relacionadas a impressões digitais.", + "Não pressione o botão de ligar.", + "O dedo se move muito rápido.", + "Impressões digitais parciais detectadas.", + "Falha ao adicionar Impressão Digital.", + "O cadastro de impressão digital não foi detectado por um longo período. Por favor, ajuste o ângulo do seu dedo e tente novamente.", + "Por favor, ajuste o ângulo dos seus dedos e tente novamente.", + "Verificar \n Impressão Digital", + "Use a impressão digital ou deslize para cima para desbloquear", + "Impressão Digital Desbloqueada", + "Impressão digital inválida, por favor tente novamente.", + "Limite de erros atingido, por favor insira o código PIN.", + "Deslize para cima para desbloquear", + "Permita que até 3 impressões digitais sejam registradas simultaneamente.", + "Desbloquear", + "Remover esta impressão digital?", + "Você tem certeza de que deseja remover esta impressão digital?", + "Depois de configurar a impressão digital, você pode usá-la para desbloquear o dispositivo. Toque no botão de ligar com o dedo registrado no cenário correspondente. Você pode modificá-lo a qualquer momento nas configurações.", + "Fazer backup com o Lite?", + "Já possui um OneKey Lite? Toque em \"Backup\" para armazenar sua frase de recuperação no seu OneKey Lite.", + "Coloque o Lite na parte de trás do dispositivo, em seguida, clique no botão continuar.", + "Digite o PIN do OneKey Lite", + "Pesquisando...", + "Mantenha o Lite e o dispositivo juntos até que a transferência esteja completa.", + "Transferindo ...", + "O dispositivo está conectado, por favor mantenha o cartão no lugar e não o mova.", + "Backup Concluído!", + "Você pode recuperar sua carteira usando este cartão e PIN a qualquer momento.", + "Este Cartão Contém Backup", + "Se você continuar, seu backup anterior será completamente sobrescrito e será perdido para sempre.", + "Sobrescrever", + "Reconectando...", + "Por favor, reconecte o dispositivo para continuar.", + "Erro de PIN Lite", + "Os dados neste cartão serão apagados após {} tentativas erradas.", + "Lite foi Resetado", + "Após 10 digitações consecutivas de PIN incorretas, o OneKey Lite foi automaticamente apagado para evitar ataques de força bruta aos dados de backup.", + "Entendi", + "Falha na Conexão", + "Certifique-se de que o cartão esteja firmemente colocado na parte traseira do dispositivo e tente novamente.", + "Backup Interrompido", + "Selecione a maneira como você deseja fazer o backup. Após verificar a frase de recuperação, usaremos a frase que você inseriu para o backup.", + "Fazer backup com Lite / KeyTag?", + "Faça backup usando a frase de recuperação de verificação bem-sucedida que você acabou de inserir.", + "Selecione a maneira como você deseja importar.", + "Mantenha o Lite e o dispositivo juntos até que a importação esteja completa.", + "Sem Backup neste Cartão", + "Substitua por outro OneKey Lite e tente novamente.", + "Importação Interrompida", + "Scan", + "Conectar\nApp Wallet", + "Selecione a carteira que você deseja conectar", + "Conecte-se a {}", + "Abra {} e escaneie o código QR abaixo", + "Cadeias Suportadas", + "Ethereum, Polygon, Avalanche, Base e outras redes EVM", + "Ethereum, Bitcoin, Polygon, Solana, OKT Chain, TRON e outras redes", + "Lanterna Ligada", + "Desligar Lanterna", + "Exportar Transação Assinada", + "Modo Air Gap", + "Após habilitar o Air Gap, as funções de transferência por Bluetooth, USB e NFC serão desativadas simultaneamente.", + "Air Gap", + "As funções de transferência Bluetooth, USB e NFC foram desativadas.", + "Habilite o Air Gap", + "Você tem certeza de que deseja ativar o modo Air Gap? Depois de ativado, operações como conectar carteiras de software e assinar transações serão realizadas offline por meio da leitura de códigos QR.", + "Air Gap é uma medida de segurança que impede ataques através de meios de transmissão intermediários.\nApós habilitar o Air Gap, o OneKey Pro desativará todos os módulos de comunicação com fio e sem fio e manterá um isolamento físico rigoroso de dispositivos externos ou redes não confiáveis.", + "OKX Wallet", + "MetaMask", + "OneKey Wallet", + "Dedo {}", + "Carregando", + "Escanee o código QR exibido no aplicativo", + "Formato de dados não suportado", + "Tipo de QR code não suportado, por favor tente novamente.", + "Transação Inválida", + "Os dados da transação estão incorretos, por favor tente novamente", + "Criptografar {} Mensagem", + "Descriptografar {} Mensagem", + "Assinatura de Exportação", + "Retorne ao aplicativo e escaneie o código QR da assinatura assinada abaixo.", + "Criptografar", + "Descriptografar", + "Levante e ajuste a posição, depois toque novamente no botão de ligar.", + "Limpe o botão de energia e tente novamente.", + "Coloque o dedo no botão de ligar e mantenha no lugar.", + "Domínio", + "LNURL Auth", + "Aprovar Autorização LNURL", + "Desative o Air Gap", + "Você tem certeza de que deseja desativar o modo Air Gap? Após a desativação, será possível conectar seu dispositivo via USB ou Bluetooth.", + "Selecione a corrente que você precisa, depois clique no botão Criar.", + "Redes ETH e EVM", + "impressão digital não reconhecida, tente novamente", + "Use a impressão digital ou toque para desbloquear", + "Firmware SE", + "Bateria fraca (<20%). Para manter a bateria saudável, certifique-se de que o dispositivo esteja totalmente carregado antes de armazená-lo a longo prazo.", + "Selecione a maneira de conectar", + "Conectar Bluetooth", + "Selecione a carteira que você deseja conectar", + "iOS & Android", + "Baixe o aplicativo OneKey", + "Baixe o OneKey App Desktop em: \n onekey.so/download", + "Conecte-se via Bluetooth", + "Ative o Bluetooth em ambos os dispositivos. Mantenha a carteira perto do seu telefone e clique no nome do dispositivo OneKey Pro detectado.", + "Emparelhar Dispositivos", + "Digite o código de pareamento exibido em sua carteira de hardware OneKey Pro no aplicativo OneKey para parear seus dispositivos.", + "Saiba mais", + "Escaneie o código QR para visualizar o tutorial detalhado", + "Acessar Carteira", + "Abra o aplicativo móvel OKX, vá para Carteira, selecione \"Eu já tenho uma\" > \"Carteira de hardware\" > \"OneKey\".", + "Importar Contas da Carteira", + "Digite seu PIN quando solicitado, aguarde a lista de carteiras e escolha a conta para importar.", + "Conectar USB", + "Desktop & Extensão de Navegador", + "Baixe o OneKey App Mobile de:\nonekey.so/download", + "Conecte seu dispositivo", + "Conecte seu OneKey Pro ao computador usando um cabo USB.", + "Inicie a Conexão", + "Abra o OneKey App Desktop e selecione \"Conectar Carteira de Hardware\".", + "Extensão do navegador", + "Abra o MetaMask no seu navegador, clique no nome da conta e selecione \"Adicionar conta ou carteira de hardware\" na parte inferior.", + "Conectar a carteira de hardware", + "Escolha \"TREZOR\", clique em \"Continuar\", selecione \"Permitir uma vez para esta sessão\" e siga as instruções na tela para exportar. Insira o seu PIN da carteira OneKey quando solicitado.", + "Desbloquear Conta", + "Na lista de contas, selecione a que deseja conectar e clique em \"Desbloquear\".", + "Abra a extensão da carteira OKX, selecione \"Importar Carteira\" > \"Carteira de hardware\" > \"OneKey\".", + "Instalar o OneKey Bridge", + "Baixe e instale o OneKey Bridge se solicitado, depois atualize, escolha seu dispositivo e insira seu PIN.", + "Uma vez conectado, selecione a conta para importar e clique em \"Conectar\" na parte inferior.", + "Conectar QR Code", + "Bluetooth e USB estão desativados no Modo Air Gap", + "Reiniciar o dispositivo?", + "A reinicialização fará com que o dispositivo saia do modo de atualização e interrompa o processo de upgrade.", + "Confirmar Endereço", + "Retorne ao aplicativo e escaneie o código QR abaixo.", + "Redes compatíveis com BTC e EVM", + "Abra o OneKey e escaneie o código QR abaixo", + "Exportar Conta", + "Conectando...", + "Defina o PIN do OneKey Lite", + "Entendo que o backup será sobrescrito", + "Confirme o PIN do OneKey Lite", + "Os PINs não correspondem, por favor confirme novamente.", + "Conecte novamente", + "Saída", + "Os dois OneKey Lite usados para conexão não são os mesmos.", + "Sair do processo de backup?", + "Se você sair agora, precisará verificar novamente sua frase de recuperação ao entrar novamente. Você tem certeza que quer sair?", + "Frase de recuperação não suportada", + "A carteira de hardware atual suporta apenas frases de recuperação de 12, 18 e 24 palavras.\nEste backup Lite não pode ser restaurado.", + "Abra o OneKey e escaneie o código QR, compatível com redes BTC e EVM.", + "Incompatibilidade de Carteira", + "Sua carteira selecionada no aplicativo não corresponde à carteira de hardware. Por favor, verifique e tente novamente.", + "Assinatura de mensagem não padrão.", + "Enviar Tokens", + "A seguinte saída de transação contém dados do contrato:", + "Você está usando a autorização {type}, verifique a confiabilidade do dApp.", + "Não é possível fazer backup", + "O modo Airgap está habilitado e o NFC está desabilitado, então você não pode fazer backup para o OneKey Lite. Desabilite o modo Airgap e tente novamente.", + "Ir para Configurações", + "Certificações", + "Meu endereço", + "Selecione a rede", + "Endereço {network}", + "Selecione o caminho de derivação", + "Selecione a conta", + "Ir para a Conta", + "Erro de formatação de entrada", + "Seu endereço é um endereço de rede EVM. Você pode usá-lo para gerenciar seus ativos em outras redes compatíveis com EVM (como Ethereum, BNB Chain, Polygon, Arbitrum One, Avalanche, etc.).", + "Chaves de segurança", + "Configuração da Carteira Necessária", + "Para emparelhar seu dispositivo via Bluetooth, por favor, crie uma carteira no seu dispositivo de hardware primeiro. Isso é necessário para uma conexão segura.", + "Emparelhamento Bluetooth indisponível enquanto estiver no modo Boot", + "Para atualizar seu dispositivo, visite firmware.onekey.so no seu computador e conecte via USB para instalar o firmware.", + "Verificar Agora", + "Conectar Dispositivo", + "Abra o aplicativo OneKey e conecte seu dispositivo para criar uma carteira. A verificação do dispositivo será realizada automaticamente", + "Verificar Dispositivo", + "Visite https://bit.ly/3ZsHB40 para métodos adicionais de verificação", + "Mais Redes", + "Mostrar menos", + "A URL do Bootloader requer verificação do dispositivo no OneKey App 5.5.0+", + "Baixe o aplicativo OneKey em: onekey.so/download", + "Chaves FIDO", + "Proteja contas com chaves de segurança FIDO", + "Remover chave FIDO?", + "Esta chave FIDO será removida permanentemente", + "Chave FIDO Removida", + "Limite de Chaves Atingido", + "Limite de 60 chaves FIDO atingido. Remova as não utilizadas para adicionar novas", + "Gerenciar Chaves FIDO", + "Ainda não há chaves FIDO", + "Usando dispositivos OneKey como chaves de segurança", + "Registrar", + "Conecte seu OneKey ao seu computador", + "Vá para as configurações de segurança do site (por exemplo, Google, Facebook)", + "Selecione a opção \"Add Security Key\"", + "Confirme com OneKey (desbloqueio necessário)", + "Escolha \"Security Key\" ao fazer login", + "Aprovar com OneKey", + "Autenticar", + "Deslize para desbloquear", + "5 tentativas falhas. Deslize para continuar", + "Deslize para continuar", + "Voltar para casa", + "Registrando chave FIDO...", + "Chave FIDO Registrada", + "Bateria fraca", + "Desligando", + "Bloqueio/Desligamento Automático", + "Assinatura Bruta Solana", + "Permite assinar mensagens brutas do Solana sem processamento ou validação. Isso pode expô-lo a phishing, assinaturas cegas e aprovações não autorizadas. Use com cautela.", + "Ativar Assinatura Bruta Solana?", + "Isso pode expor você a phishing, assinaturas cegas e transações não autorizadas. Ative apenas se você compreender totalmente os riscos.", + "Risco de phishing e assinatura cega. Prossiga apenas se confiar na fonte.", + "Proteção de Segurança", + "Tutorial Básico", + "Redes BTC, SOL, ETH e EVM", + "OneKey App", + "Candidato", + "Rede alvo", + "Remover voto", +] +# fmt: on diff --git a/core/src/trezor/lvglui/i18n/locales/ru.py b/core/src/trezor/lvglui/i18n/locales/ru.py index dfddf7888a..7a90edfae5 100644 --- a/core/src/trezor/lvglui/i18n/locales/ru.py +++ b/core/src/trezor/lvglui/i18n/locales/ru.py @@ -200,7 +200,7 @@ "Неверный код пары, попробуйте еще раз.", "{} секунды", "Пользовательский", - "Просмотр данных", + "Просмотр", "Размер", "Данные", "Вы действительно хотите автоматически блокировать устройство после {}?", @@ -583,7 +583,7 @@ "Подключитесь к приложению OneKey, найдите НФТ, принадлежащий этому аппаратному кошельку, и нажмите кнопку Collect.", "Вы все еще можете сделать резервную копию с помощью KeyTag позже в разделе «Кошелек — Проверить фразу восстановления».", "Если вы хотите сделать еще одну резервную копию KeyTag, вы можете просмотреть точечную карту KeyTag в разделе «Кошелек — Проверить фразу восстановления» после проверки фразы восстановления.", - "Проверка подлинности", + "Аутентификация устройства", "Отключить совместимость с Трезором", "Восстановить совместимость с Trezor", "Совместим с Trezor", @@ -665,7 +665,7 @@ "Лимит ошибок. Введите PIN-код.", "Проведите пальцем вверх, чтобы разблокировать", "Позволяет одновременно записывать до 3 отпечатков пальцев.", - "Разблокировать устройство", + "Разблокировать", "Удалить этот отпечаток пальца?", "Вы уверены, что хотите удалить этот отпечаток пальца?", "После настройки отпечатка пальца вы можете использовать его для разблокировки устройства. Коснитесь кнопки питания зарегистрированным пальцем в соответствующем сценарии. Вы можете изменить его в любое время в настройках.", @@ -687,7 +687,7 @@ "Ошибка PIN-кода Lite", "Данные на этой карте будут удалены после {} неверных попыток.", "Lite был сброшен", - "PIN-код был введен неверно более 10 раз. OneKey Lite имеет функцию самоудаления, чтобы предотвратить грубый взлом данных резервной копии.", + "После 10 последовательных попыток неверного ввода PIN-кода данные OneKey Lite автоматически стираются, чтобы предотвратить атаки методом подбора резервных копий данных.", "Я понял", "Сбой соединения", "Убедитесь, что карта плотно прилегает к задней части устройства, а затем повторите попытку.", @@ -723,7 +723,7 @@ "OneKey Wallet", "Отпечаток пальца {}", "Зарядка", - "Сканируйте QR-код транзакции или подпишите сообщение", + "Отсканируйте QR-код, отображаемый в приложении", "Неподдерживаемый формат данных", "Тип QR-кода не поддерживается. Повторите попытку.", "Недействительная транзакция", @@ -743,11 +743,11 @@ "Отключить воздушный зазор", "Вы уверены, что отключили режим Air Gap? После отключения вы сможете подключить ваше устройство через Bluetooth или USB.", "Выберите нужную цепочку и нажмите кнопку «Создать».", - "Цепочки, совместимые с ETH и EVM", + "Сети ETH и EVM", "отпечаток пальца не распознан, попробуйте еще раз", "Используйте отпечаток пальца или коснитесь экрана, чтобы разблокировать", "Прошивка SE", - "Низкий уровень заряда батареи (<10%). Для поддержания здоровья батареи, пожалуйста, убедитесь, что устройство полностью заряжено перед долгосрочным хранением.", + "Низкий уровень заряда батареи (<20%). Для поддержания здоровья батареи, пожалуйста, убедитесь, что устройство полностью заряжено перед долгосрочным хранением.", "Выберите способ подключения", "Подключение Bluetooth", "Выберите кошелек, к которому хотите подключиться", @@ -788,7 +788,6 @@ "Подтвердите адрес", "Вернитесь в приложение и отсканируйте QR-код ниже.", "Сети, совместимые с BTC и EVM", - "Сети, совместимые с ETH и EVM", "Откройте OneKey и отсканируйте QR-код ниже.", "Экспорт аккаунта", "Подключение...", @@ -801,5 +800,79 @@ "Два OneKey Lite, используемые для подключения, не одинаковы.", "Выйти из процесса резервного копирования?", "Если вы выйдете сейчас, вам нужно будет повторно подтвердить фразу восстановления при повторном входе. Вы уверены, что хотите выйти?", + "Неподдерживаемая фраза восстановления", + "Текущий аппаратный кошелек поддерживает только восстановительные фразы из 12, 18 и 24 слов.\nЭтот Lite резервный копии не может быть восстановлен.", + "Откройте OneKey и отсканируйте QR-код, поддерживающий сети BTC и EVM.", + "Несоответствие кошелька", + "Выбранный вами кошелек в приложении не совпадает с аппаратным кошельком. Пожалуйста, проверьте и попробуйте снова.", + "Нестандартная подпись сообщения.", + "Отправить токены", + "Следующий вывод транзакции содержит данные контракта:", + "Вы используете авторизацию {type}, убедитесь в надежности dApp.", + "Невозможно сделать резервную копию", + "Режим Airgap включен, а NFC отключен, поэтому вы не можете выполнить резервное копирование в OneKey Lite. Пожалуйста, отключите режим Airgap и повторите попытку.", + "Перейти к настройкам", + "Сертификаты", + "Мой адрес", + "Выбрать сеть", + "{network} Адрес", + "Выберите путь вывода", + "Выберите учетную запись", + "Перейти в аккаунт", + "Ошибка форматирования ввода", + "Ваш адрес является адресом сети EVM. Вы можете использовать его для управления своими активами в других сетях, совместимых с EVM (таких как Ethereum, BNB Chain, Polygon, Arbitrum One, Avalanche и т.д.).", + "Ключи безопасности", + "Требуется настройка кошелька", + "Чтобы подключить ваше устройство через Bluetooth, сначала создайте кошелек на вашем аппаратном устройстве. Это необходимо для безопасного соединения.", + "Подключение Bluetooth недоступно в режиме загрузки", + "Чтобы обновить ваше устройство, посетите firmware.onekey.so на вашем компьютере и подключитесь через USB для установки прошивки.", + "Подтвердите сейчас", + "Подключить устройство", + "Откройте OneKey App и подключите ваше устройство, чтобы создать кошелек. Проверка устройства будет выполнена автоматически", + "Проверка устройства", + "Посетите https://bit.ly/3ZsHB40 для получения дополнительных методов проверки", + "Больше сетей", + "Показать меньше", + "URL загрузчика требует проверки устройства в приложении OneKey версии 5.5.0 и выше", + "Скачайте приложение OneKey по адресу: onekey.so/download", + "Ключи FIDO", + "Защитите аккаунты с помощью ключей безопасности FIDO", + "Удалить FIDO ключ?", + "Этот ключ FIDO будет удален навсегда", + "Ключ FIDO удален", + "Достигнут лимит ключей", + "Достигнут лимит в 60 ключей FIDO. Удалите неиспользуемые, чтобы добавить новые", + "Управление ключами FIDO", + "Пока нет ключей FIDO", + "Использование устройств OneKey в качестве ключей безопасности", + "Зарегистрироваться", + "Подключите ваш OneKey к компьютеру", + "Перейдите в настройки безопасности на сайте (например, Google, Facebook)", + "Выберите опцию \"Add Security Key\"", + "Подтвердите с помощью OneKey (требуется разблокировка)", + "Выберите \"Security Key\" при входе", + "Утвердить с OneKey", + "Аутентифицировать", + "Проведите, чтобы разблокировать", + "5 неудачных попыток. Проведите, чтобы продолжить", + "Проведите, чтобы продолжить", + "Вернуться на главную", + "Регистрация ключа FIDO...", + "Ключ FIDO зарегистрирован", + "Низкий заряд батареи", + "Выключение питания", + "Автоматическая блокировка/выключение", + "Solana Raw Signing", + "Позволяет подписывать необработанные сообщения Solana без обработки или проверки. Это может подвергнуть вас фишингу, слепому подписанию и несанкционированным одобрениям. Используйте с осторожностью.", + "Включить Raw Signing для Solana?", + "Это может подвергнуть вас фишингу, слепой подписи и несанкционированным транзакциям. Включайте только в том случае, если вы полностью понимаете риски.", + "Риск фишинга и слепого подписания. Продолжайте только если доверяете источнику.", + "Безопасность Защита", + "Базовый учебник", + "Сети BTC, SOL, ETH и EVM", + "OneKey App", + "Кандидат", + "Целевая сеть", + "Удалить голос", ] # 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 5ad1df3b4d..86fb13aaca 100644 --- a/core/src/trezor/lvglui/i18n/locales/zh_cn.py +++ b/core/src/trezor/lvglui/i18n/locales/zh_cn.py @@ -583,7 +583,7 @@ "连接 OneKey app,找到此硬件钱包持有的 NFT,然后点击收藏按钮。", "你仍可以稍后在“钱包 - 核对助记词”中使用 KeyTag 进行备份", "如果您想要再次备份,请在“钱包 - 核对助记词”中,完成验证助记词后,根据提示进行 KeyTag 备份。", - "防伪校验", + "设备认证", "禁用兼容 Trezor", "恢复 Trezor 兼容模式", "兼容 Trezor", @@ -687,7 +687,7 @@ "Lite PIN 错误", "在 {} 次错误尝试后,此卡上的数据将被删除", "Lite 已重置", - "已连续错误输入 PIN 超过 10 次,OneKey Lite已被自动抹除以防止暴力破解备份数据。", + "已连续错误输入 PIN 达到 10 次,OneKey Lite已被自动抹除以防止暴力破解备份数据。", "我知道了", "连接失败", "请确保卡片贴紧设备的背面,然后再试一次。", @@ -723,7 +723,7 @@ "OneKey Wallet", "指纹 {}", "充电中", - "扫描交易二维码或进行签名", + "扫描 app 上显示的二维码", "不支持的数据\n格式", "不支持的二维码类型,请重试", "无效的交易", @@ -743,17 +743,17 @@ "禁用 Air Gap", "要关闭 Air Gap 模式吗?关闭后,将允许通过蓝牙和 USB 连接此设备。", "选择你需要的链,然后点击创建按钮。", - "ETH 和 EVM 兼容链", + "ETH 和 EVM 网络", "指纹识别失败,请重试", "轻触屏幕或指纹解锁", "SE 固件", - "电量低(<10%),若打算长期存放本设备,请先将设备充满电,以保持电池寿命。", + "电量低(<20%),若打算长期存放本设备,请先将设备充满电,以保持电池寿命。", "选择连接方式", "蓝牙连接", "选择您想要连接的钱包", "iOS & Android", "下载 OneKey App", - "在以下地址下载OneKey App桌面客户端:\nonekey.so/download", + "在以下地址下载 OneKey App桌面客户端:\nonekey.so/download", "通过蓝牙连接", "打开两个设备的蓝牙。保持钱包靠近手机,点击检测到的 OneKey Pro 设备名称。", "配对设备", @@ -766,7 +766,7 @@ "完成配对后输入PIN码,等待钱包列表加载后导入所需账户。", "USB连接", "桌面版 & 浏览器扩展", - "从以下地址下载OneKey App 移动端:\nonekey.so/download", + "从以下地址下载 OneKey App 移动端:\nonekey.so/download", "连接设备", "使用USB线将您的OneKey Pro连接到电脑。", "开始连接", @@ -788,10 +788,9 @@ "确认地址", "返回 APP,并扫描下方的二维码。", "BTC 和 EVM 兼容网络", - "ETH 和 EVM 兼容网络", "打开 OneKey,扫描下方二维码", "导出账户", - "连接中……", + "连接中...", "设置 OneKey Lite PIN", "我已知晓备份将被覆盖", "确认 OneKey Lite PIN", @@ -801,5 +800,79 @@ "两次用于连接的 OneKey Lite 不一致", "退出备份流程?", "退出后重新进入需要再次验证助记词,请确认是否要退出", + "不支持的助记词", + "当前硬件钱包仅支持 12、18 和 24 位助记词,无法恢复此 Lite 备份。", + "打开 OneKey 扫描二维码,支持 BTC 和 EVM 网络。", + "钱包不匹配", + "硬件钱包和 app 中选择的钱包不匹配,请检查后再试。", + "非标准格式消息签名。", + "发送代币", + "以下交易输出包含合约数据:", + "您正在使用 {type} 授权,请确保当前 dApp 可信,否则可能导致资产损失。", + "备份受限", + "Airgap 模式已启用,NFC 功能被禁用,无法备份到 OneKey Lite,请关闭 Airgap 模式后重试。", + "前往设置", + "设备认证", + "我的地址", + "选择网络", + "{network}地址", + "选择派生路径", + "选择帐户", + "前往账户", + "输入格式错误", + "当前地址是一个 EVM 网络地址。您可同步管理其他 EVM 兼容网络(如 Ethereum、BNB Chain、Polygon、Arbitrum One、Avalanche等)上的资产。", + "安全密钥", + "需要设置钱包", + "要通过 Bluetooth 配对您的设备,请先在您的硬件设备上创建一个 wallet 。这是确保安全连接所必需的。", + "蓝牙配对在 Boot 模式下不可用", + "要更新您的设备,请在电脑上访问 firmware.onekey.so,并通过 USB 连接以安装固件。", + "立即验证", + "连接设备", + "打开 OneKey App 并连接您的设备以创建钱包。设备验证将自动进行", + "验证设备", + "访问 https://bit.ly/3ZsHB40 获取更多验证方法", + "更多网络", + "显示更少", + "Bootloader URL 需要在 OneKey App 5.5.0+ 中进行设备验证", + "在 onekey.so/download 下载 OneKey App", + "FIDO 密钥", + "使用 FIDO 安全密钥保护账户", + "移除 FIDO 密钥?", + "此 FIDO 密钥将被永久删除", + "FIDO 密钥已移除", + "已达到密钥数量限制", + "已达到 60 个 FIDO 密钥的限制。移除未使用的以添加新的", + "管理 FIDO 密钥", + "还没有 FIDO 密钥", + "使用 OneKey 设备作为安全密钥", + "注册", + "将你的 OneKey 插入到电脑中", + "前往网站的安全设置(例如 Google, Facebook)", + "选择 \"Add Security Key\" 选项", + "使用 OneKey 确认(需要解锁)", + "选择 \"Security Key\" 进行登录", + "使用 OneKey 批准", + "认证", + "滑动解锁", + "5 次尝试失败。滑动继续", + "滑动继续", + "返回首页", + "注册 FIDO 密钥...", + "FIDO 密钥已注册", + "电池电量低", + "正在关机", + "自动锁定/关机", + "Solana 原始消息签名", + "允许签署原始 Solana 消息而不进行处理或验证。这可能会让您面临网络钓鱼、盲签和未经授权的批准。请谨慎使用。", + "启用 Solana 原始消息签名?", + "这可能会让你面临钓鱼、盲签和未经授权的交易风险。只有在完全了解风险的情况下才启用。", + "存在钓鱼和盲签的风险 。 只有在你信任来源的情况下才继续 。", + "安全保护", + "基础教程", + "BTC、SOL、ETH 和 EVM 网络", + "OneKey App", + "候选人", + "目标网络", + "移除投票", ] # 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 bda5342dee..1ba5f5716f 100644 --- a/core/src/trezor/lvglui/i18n/locales/zh_hk.py +++ b/core/src/trezor/lvglui/i18n/locales/zh_hk.py @@ -583,7 +583,7 @@ "連接 OneKey app,找到此硬件錢包持有的 NFT,然後點擊收藏按鈕。", "你仍可以稍後在「錢包 - 核對助記詞」中使用 KeyTag 進行備份", "如果您想要再次備份,請在「錢包 - 核對助記詞」中,完成驗證助記詞後,根據提示進行 KeyTag 備份。", - "防偽校驗", + "裝置認證", "禁用兼容 Trezor", "恢復 Trezor 兼容模式", "兼容 Trezor", @@ -687,7 +687,7 @@ "Lite PIN 錯誤", "在 {} 次錯誤嘗試後,此卡上的數據將被刪除", "Lite 已重置", - "已連續錯誤輸入 PIN 超過 10 次,OneKey Lite已被自動抹除以防止暴力破解備份數據。", + "已連續錯誤輸入 PIN 達到 10 次,OneKey Lite已被自動抹除以防止暴力破解備份數據。", "我知道了", "連接失敗", "請確保卡片貼緊設備的背面,然後再試一次。", @@ -723,7 +723,7 @@ "OneKey Wallet", "指紋 {}", "充電中", - "掃描交易二維碼或進行簽名", + "掃描 app 上顯示的 QR 碼", "不支持的數據\n格式", "不支持的二維碼類型,請重試", "無效的交易", @@ -743,17 +743,17 @@ "禁用 Air Gap", "要關閉 Air Gap 模式嗎?關閉後,將允許通過藍牙和 USB 連接此設備。", "選擇你需要的鏈,然後點擊創建按鈕。", - "ETH 和 EVM 兼容鏈", + "ETH 與 EVM 網路", "指紋識別失敗,請重試", "輕觸屏幕或指紋解鎖", "SE 固件", - "電量低(<10%),若打算長期存放本設備,請先將設備充滿電,以維持電池壽命。", + "電量低(<20%),若打算長期存放本設備,請先將設備充滿電,以維持電池壽命。", "選擇連接方式", "藍牙連接", "選擇您想要連接的錢包", "iOS & Android", "下載OneKey App", - "在以下地址下載OneKey App桌面客戶端:\nonekey.so/download", + "在以下地址下載 OneKey App桌面客戶端:\nonekey.so/download", "通過藍牙連接", "打開兩個設備的藍牙。保持錢包靠近手機,點擊檢測到的 OneKey Pro 設備名稱。", "配對設備", @@ -766,7 +766,7 @@ "完成配對後輸入PIN碼,等待錢包列表加載後導入所需賬戶。", "USB連接", "桌面版 & 瀏覽器擴展", - "從以下地址下載OneKey App 移動端:\nonekey.so/download", + "從以下地址下載 OneKey App 移動端:\nonekey.so/download", "連接設備", "使用USB線將您的OneKey Pro連接到電腦。", "開始連接", @@ -788,10 +788,9 @@ "確認地址", "返回 APP,並掃描下方的 QR 碼。", "BTC 和 EVM 相容網路", - "ETH 和 EVM 相容網絡", "打開一鍵掃描下方二維碼", "導出帳戶", - "連接中……", + "連接中...", "設定 OneKey Lite PIN", "我已知曉備份將被覆蓋", "確認OneKey Lite PIN", @@ -801,5 +800,79 @@ "兩次用於連接的 OneKey Lite 不一致", "退出備份流程?", "退出後重新進入需要再次驗證助記詞,請確認是否要退出", + "不支持的助記詞", + "當前硬件錢包僅支持 12、18 和 24 位助記詞,無法恢復此 Lite 備份。", + "開啟 OneKey 掃描二維碼,支援 BTC 和 EVM 網路。", + "錢包不匹配", + "硬體錢包和 app 中選擇的錢包不匹配,請檢查後再試。", + "非標準格式訊息簽名。", + "發送代幣", + "以下交易輸出包含合約資料:", + "您正在使用 {type} 授權,請確保當前 dApp 可信,否則可能導致資產損失。", + "備份受限", + "Airgap 模式已啓用,NFC 功能被禁用,無法備份到 OneKey Lite,請關閉 Airgap 模式後重試。", + "前往設定", + "設備認證", + "我的地址", + "選擇網路", + "{network}位址", + "選擇派生路徑", + "選擇帳戶", + "前往帳戶", + "輸入格式錯誤", + "當前地址是一個 EVM 網絡地址。你可同步管理其他 EVM 兼容網絡(例如 Ethereum、BNB Chain、Polygon、Arbitrum One、Avalanche等)上的資產。", + "安全金鑰", + "需要設置錢包", + "要通過 Bluetooth 配對您的設備,請先在您的硬件設備上創建一個 wallet 。這是確保安全連接所必需的。", + "藍牙配對在 Boot 模式下不可用", + "要更新您的設備,請在電腦上訪問 firmware.onekey.so,然後通過 USB 連接以安裝固件。", + "立即驗證", + "連接裝置", + "打開 OneKey App 並連接你的設備以創建錢包。設備驗證將自動進行", + "驗證裝置", + "請訪問 https://bit.ly/3ZsHB40 以獲取其他驗證方法", + "更多網絡", + "顯示更少", + "Bootloader URL 需要在 OneKey App 5.5.0+ 中進行設備驗證", + "到 onekey.so/download 下載 OneKey App", + "FIDO 金鑰", + "使用 FIDO 安全金鑰保護帳戶", + "移除 FIDO 金鑰?", + "這個 FIDO 金鑰將會被永久刪除", + "FIDO 金鑰已移除", + "已達到金鑰限制", + "已達到 60 個 FIDO 金鑰的限制。移除未使用的以新增", + "管理 FIDO 密钥", + "尚未有 FIDO 金鑰", + "使用 OneKey 裝置作為安全金鑰", + "註冊", + "將你的 OneKey 插入電腦", + "前往網站的安全設置(例如:Google、Facebook)", + "選擇 \"Add Security Key\" 選項", + "使用 OneKey 確認(需解鎖)", + "在 登入 時 選擇 \"Security Key\"", + "使用 OneKey 批准", + "認證", + "滑動解鎖", + "5 次嘗試失敗。 滑動以繼續", + "滑動以繼續", + "返回主頁", + "註冊 FIDO key...", + "FIDO key 已註冊", + "電池電量低", + "正在關機", + "自動鎖定/關機", + "Solana 原始訊息簽署", + "允許簽署原始 Solana 訊息而不進行處理或驗證。這可能會讓你面臨網絡釣魚、盲簽和未經授權的批准。使用時請謹慎。", + "啟用 Solana 原始訊息簽名?", + "這可能會令你面臨釣魚攻擊、盲簽和未經授權的交易風險。只有在你完全了解風險的情況下才啟用。", + "有釣魚及盲簽風險。 只有在信任來源的情況下才繼續。", + "安全保護", + "基礎教程", + "BTC、SOL、ETH 與 EVM 網絡", + "OneKey App", + "候選人", + "目標網絡", + "移除投票", ] # fmt: on diff --git a/core/src/trezor/lvglui/res/evm-dtt.png b/core/src/trezor/lvglui/res/evm-dtt.png new file mode 100644 index 0000000000..33d57b68d5 Binary files /dev/null and b/core/src/trezor/lvglui/res/evm-dtt.png differ diff --git a/core/src/trezor/lvglui/res/evm-hsk.png b/core/src/trezor/lvglui/res/evm-hsk.png new file mode 100644 index 0000000000..8daf2b1773 Binary files /dev/null and b/core/src/trezor/lvglui/res/evm-hsk.png differ diff --git a/core/src/trezor/lvglui/res/evm-s.png b/core/src/trezor/lvglui/res/evm-s.png new file mode 100644 index 0000000000..139917b78e Binary files /dev/null and b/core/src/trezor/lvglui/res/evm-s.png differ diff --git a/core/src/trezor/lvglui/scrs/homescreen.py b/core/src/trezor/lvglui/scrs/homescreen.py index c9373bd895..8b7a871305 100644 --- a/core/src/trezor/lvglui/scrs/homescreen.py +++ b/core/src/trezor/lvglui/scrs/homescreen.py @@ -636,6 +636,7 @@ def __init__(self, prev_scr=None): super().__init__(**kwargs) else: self.refresh_text() + self.container.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) return # if __debug__: # self.add_style(StyleWrapper().bg_color(lv_colors.ONEKEY_GREEN_1), 0) @@ -840,6 +841,9 @@ def get_str_from_ms(self, time_ms) -> str: if value == "1" else i18n_keys.OPTION__STR_HOURS ).format(value) + elif auto_lock_time < 1: + value = str(time_ms // 1000).split(".")[0] + text = _(i18n_keys.OPTION__STR_SECONDS).format(value) else: value = str(auto_lock_time).split(".")[0] text = _( @@ -886,7 +890,7 @@ def __init__(self, prev_scr=None): ) self.container = ContainerFlexCol(self.content_area, self.title, padding_row=2) - self.setting_items = [1, 2, 5, 10, 30, "Never", None] + self.setting_items = [0.5, 1, 2, 5, 10, 30, "Never", None] has_custom = True self.checked_index = 0 self.btns: [ListItemBtn] = [None] * (len(self.setting_items)) @@ -894,11 +898,14 @@ def __init__(self, prev_scr=None): if item is None: break if not item == "Never": # last item - item = _( - i18n_keys.ITEM__STATUS__STR_MINUTES - if item != 1 - else i18n_keys.OPTION__STR_MINUTE - ).format(item) + if item == 0.5: + item = _(i18n_keys.OPTION__STR_SECONDS).format(int(item * 60)) + else: + item = _( + i18n_keys.ITEM__STATUS__STR_MINUTES + if item != 1 + else i18n_keys.OPTION__STR_MINUTE + ).format(item) else: item = _(i18n_keys.ITEM__STATUS__NEVER) self.btns[index] = ListItemBtn( @@ -970,13 +977,13 @@ def on_click(self, event_obj): item.set_checked() self.btns[self.checked_index].set_uncheck() self.checked_index = index - if index == 5: + if index == 6: auto_lock_time = device.AUTOLOCK_DELAY_MAXIMUM - elif index == 6: + elif index == 7: auto_lock_time = self.custom else: auto_lock_time = self.setting_items[index] * 60 * 1000 - device.set_autolock_delay_ms(auto_lock_time) + device.set_autolock_delay_ms(int(auto_lock_time)) GeneralScreen.cur_auto_lock_ms = auto_lock_time self.fresh_tips() from apps.base import reload_settings_from_storage diff --git a/core/src/trezor/lvglui/scrs/template.py b/core/src/trezor/lvglui/scrs/template.py index 3ce47b5dd4..fb70e935ed 100644 --- a/core/src/trezor/lvglui/scrs/template.py +++ b/core/src/trezor/lvglui/scrs/template.py @@ -161,7 +161,12 @@ def __init__( primary_color, icon_path, verify: bool = False, - evm_chain_id: int | None = None, + *, + item_other: int | str | None = None, + item_addr_title: str | None = None, + item_other_title: str | None = None, + is_standard: bool = True, + warning_banner_text: str | None = None, ): super().__init__( title, @@ -173,17 +178,30 @@ def __init__( icon_path=icon_path, ) self.primary_color = primary_color + if not is_standard: + self.warning_banner = Banner( + self.content_area, + 2, + warning_banner_text + or _(i18n_keys.CONTENT__NON_STANDARD_MESSAGE_SIGNATURE), + ) + 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=8 + self.content_area, + self.title if is_standard else self.warning_banner, + pos=(0, 40), + padding_row=8, ) - if evm_chain_id: + if item_other: self.item3 = DisplayItemNoBgc( self.container, - _(i18n_keys.LIST_KEY__CHAIN_ID__COLON), - str(evm_chain_id), + item_other_title or _(i18n_keys.LIST_KEY__CHAIN_ID__COLON), + str(item_other), ) self.item1 = DisplayItemNoBgc( - self.container, _(i18n_keys.LIST_KEY__ADDRESS__COLON), address + self.container, + item_addr_title or _(i18n_keys.LIST_KEY__ADDRESS__COLON), + address, ) self.long_message = False if len(message) > 80: @@ -1069,6 +1087,29 @@ def on_click(self, event_obj): ) +class EIP712Warning(FullSizeWindow): + def __init__( + self, title: str, warning_level, text, primary_type, primary_color, icon_path + ): + super().__init__( + title, + None, + _(i18n_keys.BUTTON__CONTINUE), + _(i18n_keys.BUTTON__REJECT), + anim_dir=2, + primary_color=primary_color, + icon_path=icon_path, + ) + self.warning_banner = Banner(self.content_area, warning_level, text) + self.warning_banner.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 40) + self.primary_type = DisplayItemNoBgc( + self.content_area, + "PrimaryType:", + primary_type, + ) + self.primary_type.align_to(self.warning_banner, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 24) + + class TransactionDetailsTRON(FullSizeWindow): def __init__( self, diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index 6e3f033805..2adf8e764a 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -24,9 +24,10 @@ def __getattr__(name: str) -> Any: from trezor.enums import ButtonRequestType # noqa: F401 from trezor.enums import Capability # noqa: F401 from trezor.enums import CardanoAddressType # noqa: F401 + from trezor.enums import CardanoCVoteRegistrationFormat # noqa: F401 from trezor.enums import CardanoCertificateType # noqa: F401 + from trezor.enums import CardanoDRepType # noqa: F401 from trezor.enums import CardanoDerivationType # noqa: F401 - from trezor.enums import CardanoGovernanceRegistrationFormat # noqa: F401 from trezor.enums import CardanoNativeScriptHashDisplayFormat # noqa: F401 from trezor.enums import CardanoNativeScriptType # noqa: F401 from trezor.enums import CardanoPoolRelayType # noqa: F401 @@ -57,6 +58,8 @@ def __getattr__(name: str) -> Any: from trezor.enums import ResourceType # noqa: F401 from trezor.enums import SafetyCheckLevel # noqa: F401 from trezor.enums import SdProtectOperationType # noqa: F401 + from trezor.enums import SolanaOffChainMessageFormat # noqa: F401 + 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 StellarSignerType # noqa: F401 @@ -715,6 +718,7 @@ class SignMessage(protobuf.MessageType): coin_name: "str" script_type: "InputScriptType" no_script_type: "bool | None" + is_bip322_simple: "bool" def __init__( self, @@ -724,6 +728,7 @@ def __init__( coin_name: "str | None" = None, script_type: "InputScriptType | None" = None, no_script_type: "bool | None" = None, + is_bip322_simple: "bool | None" = None, ) -> None: pass @@ -1145,6 +1150,36 @@ def __init__( def is_type_of(cls, msg: Any) -> TypeGuard["AuthorizeCoinJoin"]: return isinstance(msg, cls) + class SignPsbt(protobuf.MessageType): + psbt: "bytes" + coin_name: "str" + + def __init__( + self, + *, + psbt: "bytes", + coin_name: "str | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["SignPsbt"]: + return isinstance(msg, cls) + + class SignedPsbt(protobuf.MessageType): + psbt: "bytes" + + def __init__( + self, + *, + psbt: "bytes", + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["SignedPsbt"]: + return isinstance(msg, cls) + class HDNodePathType(protobuf.MessageType): node: "HDNodeType" address_n: "list[int]" @@ -1467,6 +1502,7 @@ class CardanoGetAddress(protobuf.MessageType): network_id: "int" address_parameters: "CardanoAddressParametersType" derivation_type: "CardanoDerivationType" + chunkify: "bool | None" def __init__( self, @@ -1476,6 +1512,7 @@ def __init__( address_parameters: "CardanoAddressParametersType", derivation_type: "CardanoDerivationType", show_display: "bool | None" = None, + chunkify: "bool | None" = None, ) -> None: pass @@ -1553,6 +1590,8 @@ class CardanoSignTxInit(protobuf.MessageType): has_collateral_return: "bool" total_collateral: "int | None" reference_inputs_count: "int" + chunkify: "bool | None" + tag_cbor_sets: "bool" def __init__( self, @@ -1578,6 +1617,8 @@ def __init__( has_collateral_return: "bool | None" = None, total_collateral: "int | None" = None, reference_inputs_count: "int | None" = None, + chunkify: "bool | None" = None, + tag_cbor_sets: "bool | None" = None, ) -> None: pass @@ -1777,6 +1818,24 @@ def __init__( def is_type_of(cls, msg: Any) -> TypeGuard["CardanoPoolParametersType"]: return isinstance(msg, cls) + class CardanoDRep(protobuf.MessageType): + type: "CardanoDRepType" + key_hash: "bytes | None" + script_hash: "bytes | None" + + def __init__( + self, + *, + type: "CardanoDRepType", + key_hash: "bytes | None" = None, + script_hash: "bytes | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["CardanoDRep"]: + return isinstance(msg, cls) + class CardanoTxCertificate(protobuf.MessageType): type: "CardanoCertificateType" path: "list[int]" @@ -1784,6 +1843,8 @@ class CardanoTxCertificate(protobuf.MessageType): pool_parameters: "CardanoPoolParametersType | None" script_hash: "bytes | None" key_hash: "bytes | None" + deposit: "int | None" + drep: "CardanoDRep | None" def __init__( self, @@ -1794,6 +1855,8 @@ def __init__( pool_parameters: "CardanoPoolParametersType | None" = None, script_hash: "bytes | None" = None, key_hash: "bytes | None" = None, + deposit: "int | None" = None, + drep: "CardanoDRep | None" = None, ) -> None: pass @@ -1821,56 +1884,58 @@ def __init__( def is_type_of(cls, msg: Any) -> TypeGuard["CardanoTxWithdrawal"]: return isinstance(msg, cls) - class CardanoGovernanceRegistrationDelegation(protobuf.MessageType): - voting_public_key: "bytes" + class CardanoCVoteRegistrationDelegation(protobuf.MessageType): + vote_public_key: "bytes" weight: "int" def __init__( self, *, - voting_public_key: "bytes", + vote_public_key: "bytes", weight: "int", ) -> None: pass @classmethod - def is_type_of(cls, msg: Any) -> TypeGuard["CardanoGovernanceRegistrationDelegation"]: + def is_type_of(cls, msg: Any) -> TypeGuard["CardanoCVoteRegistrationDelegation"]: return isinstance(msg, cls) - class CardanoGovernanceRegistrationParametersType(protobuf.MessageType): - voting_public_key: "bytes | None" + class CardanoCVoteRegistrationParametersType(protobuf.MessageType): + vote_public_key: "bytes | None" staking_path: "list[int]" - reward_address_parameters: "CardanoAddressParametersType" + payment_address_parameters: "CardanoAddressParametersType | None" nonce: "int" - format: "CardanoGovernanceRegistrationFormat" - delegations: "list[CardanoGovernanceRegistrationDelegation]" + format: "CardanoCVoteRegistrationFormat" + delegations: "list[CardanoCVoteRegistrationDelegation]" voting_purpose: "int | None" + payment_address: "str | None" def __init__( self, *, - reward_address_parameters: "CardanoAddressParametersType", nonce: "int", staking_path: "list[int] | None" = None, - delegations: "list[CardanoGovernanceRegistrationDelegation] | None" = None, - voting_public_key: "bytes | None" = None, - format: "CardanoGovernanceRegistrationFormat | None" = None, + delegations: "list[CardanoCVoteRegistrationDelegation] | None" = None, + vote_public_key: "bytes | None" = None, + payment_address_parameters: "CardanoAddressParametersType | None" = None, + format: "CardanoCVoteRegistrationFormat | None" = None, voting_purpose: "int | None" = None, + payment_address: "str | None" = None, ) -> None: pass @classmethod - def is_type_of(cls, msg: Any) -> TypeGuard["CardanoGovernanceRegistrationParametersType"]: + def is_type_of(cls, msg: Any) -> TypeGuard["CardanoCVoteRegistrationParametersType"]: return isinstance(msg, cls) class CardanoTxAuxiliaryData(protobuf.MessageType): - governance_registration_parameters: "CardanoGovernanceRegistrationParametersType | None" + cvote_registration_parameters: "CardanoCVoteRegistrationParametersType | None" hash: "bytes | None" def __init__( self, *, - governance_registration_parameters: "CardanoGovernanceRegistrationParametersType | None" = None, + cvote_registration_parameters: "CardanoCVoteRegistrationParametersType | None" = None, hash: "bytes | None" = None, ) -> None: pass @@ -1950,14 +2015,14 @@ def is_type_of(cls, msg: Any) -> TypeGuard["CardanoTxItemAck"]: class CardanoTxAuxiliaryDataSupplement(protobuf.MessageType): type: "CardanoTxAuxiliaryDataSupplementType" auxiliary_data_hash: "bytes | None" - governance_signature: "bytes | None" + cvote_registration_signature: "bytes | None" def __init__( self, *, type: "CardanoTxAuxiliaryDataSupplementType", auxiliary_data_hash: "bytes | None" = None, - governance_signature: "bytes | None" = None, + cvote_registration_signature: "bytes | None" = None, ) -> None: pass @@ -7287,6 +7352,60 @@ def __init__( def is_type_of(cls, msg: Any) -> TypeGuard["SolanaSignedTx"]: return isinstance(msg, cls) + class SolanaSignOffChainMessage(protobuf.MessageType): + address_n: "list[int]" + message: "bytes" + message_version: "SolanaOffChainMessageVersion" + message_format: "SolanaOffChainMessageFormat" + application_domain: "bytes | None" + + def __init__( + self, + *, + message: "bytes", + address_n: "list[int] | None" = None, + message_version: "SolanaOffChainMessageVersion | None" = None, + message_format: "SolanaOffChainMessageFormat | None" = None, + application_domain: "bytes | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["SolanaSignOffChainMessage"]: + return isinstance(msg, cls) + + class SolanaSignUnsafeMessage(protobuf.MessageType): + address_n: "list[int]" + message: "bytes" + + def __init__( + self, + *, + message: "bytes", + address_n: "list[int] | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["SolanaSignUnsafeMessage"]: + return isinstance(msg, cls) + + class SolanaMessageSignature(protobuf.MessageType): + signature: "bytes" + public_key: "bytes | None" + + def __init__( + self, + *, + signature: "bytes", + public_key: "bytes | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["SolanaMessageSignature"]: + return isinstance(msg, cls) + class StarcoinGetAddress(protobuf.MessageType): address_n: "list[int]" show_display: "bool | None" diff --git a/core/src/trezor/strings.py b/core/src/trezor/strings.py index 19f7d5a51b..6275008f5a 100644 --- a/core/src/trezor/strings.py +++ b/core/src/trezor/strings.py @@ -92,18 +92,7 @@ def format_timestamp(timestamp: int) -> str: return f"{d[0]}-{d[1]:02d}-{d[2]:02d} {d[3]:02d}:{d[4]:02d}:{d[5]:02d}" -def format_customer_data(data: bytes | None) -> str: - """ - Returns human-friendly representation of a customer data. - """ - if data is None or len(data) == 0: - return "" - try: - formatted = data.decode() - if all((c <= 0x20 or c == 0x7F) for c in data[:33]): - raise UnicodeError # non-printable characters - except UnicodeError: # mp has no UnicodeDecodeError - from binascii import hexlify - - formatted = f"0x{hexlify(data).decode()}" - return formatted +def format_customer_data(data: bytes) -> str: + from apps.common import signverify + + return signverify.decode_message(data) diff --git a/core/src/trezor/uart.py b/core/src/trezor/uart.py index 7389f42de6..efe3a776be 100644 --- a/core/src/trezor/uart.py +++ b/core/src/trezor/uart.py @@ -228,6 +228,7 @@ async def _deal_pair_res(value: bytes) -> None: if res == _BLE_PAIR_FAILED: from trezor.ui.layouts import show_pairing_error + StatusBar.get_instance().show_ble(StatusBar.BLE_STATE_ENABLED) await show_pairing_error() @@ -237,6 +238,7 @@ async def _deal_ble_status(value: bytes) -> None: if res == _BLE_STATUS_CONNECTED: utils.BLE_CONNECTED = True # show icon in status bar + utils.turn_on_lcd_if_possible(2 * 60 * 1000) StatusBar.get_instance().show_ble(StatusBar.BLE_STATE_CONNECTED) elif res == _BLE_STATUS_DISCONNECTED: utils.BLE_CONNECTED = False diff --git a/core/src/trezor/ui/layouts/lvgl/__init__.py b/core/src/trezor/ui/layouts/lvgl/__init__.py index fa02625bba..f652e174d5 100644 --- a/core/src/trezor/ui/layouts/lvgl/__init__.py +++ b/core/src/trezor/ui/layouts/lvgl/__init__.py @@ -54,6 +54,7 @@ "confirm_sol_create_ata", "confirm_sol_token_transfer", "confirm_sol_memo", + "confirm_sol_message", "confirm_data", "confirm_final", "confirm_blind_sign_common", @@ -620,10 +621,9 @@ async def confirm_blob( from trezor.lvglui.scrs.template import BlobDisPlay if isinstance(data, (bytes, bytearray)): - try: - data_str = data.decode() - except UnicodeDecodeError: - data_str = hexlify(data).decode() + from trezor import strings + + data_str = strings.format_customer_data(data) else: data_str = data blob = BlobDisPlay( @@ -938,6 +938,7 @@ async def confirm_signverify( verify: bool, evm_chain_id: int | None = None, title: str | None = None, + is_standard: bool = True, ) -> None: if verify: header = _(i18n_keys.TITLE__VERIFY_STR_MESSAGE).format(coin) @@ -960,7 +961,8 @@ async def confirm_signverify( ctx.primary_color, ctx.icon_path, verify, - evm_chain_id, + item_other=evm_chain_id, + is_standard=is_standard, ), br_type, ButtonRequestType.Other, @@ -1123,6 +1125,24 @@ async def confirm_domain(ctx: wire.GenericContext, **kwargs) -> None: ) +async def confirm_eip712_warning( + ctx: wire.GenericContext, primary_type: str, warning_level: int, text: str +) -> None: + from trezor.lvglui.scrs.template import EIP712Warning + + screen = EIP712Warning( + _(i18n_keys.TITLE__STR_TYPED_DATA).format(ctx.name), + warning_level, + text, + primary_type, + ctx.primary_color, + ctx.icon_path, + ) + await raise_if_cancelled( + interact(ctx, screen, "confirm_eip712_warning", ButtonRequestType.ProtectCall) + ) + + async def confirm_security_check(ctx: wire.GenericContext) -> None: from trezor.lvglui.scrs.template import SecurityCheck @@ -1219,6 +1239,34 @@ async def confirm_sol_memo( ) +async def confirm_sol_message( + ctx: wire.GenericContext, + address: str, + app_domain_fd: str | None, + message: str, + is_unsafe: bool = False, +) -> None: + from trezor.lvglui.scrs.template import Message + + screen = Message( + _(i18n_keys.TITLE__SIGN_STR_MESSAGE).format("SOL"), + address, + message, + ctx.primary_color, + ctx.icon_path, + False, + item_other=app_domain_fd, + item_other_title="Application Domain:" if app_domain_fd else None, + is_standard=not is_unsafe, + warning_banner_text=_(i18n_keys.SECURITY__SOLANA_RAW_SIGNING_TX_WARNING) + if is_unsafe + else None, + ) + await raise_if_cancelled( + interact(ctx, screen, "confirm_sol_message", ButtonRequestType.ProtectCall) + ) + + async def confirm_final(ctx: wire.Context, chain_name: str) -> None: from trezor.ui.layouts.lvgl import confirm_action diff --git a/core/src/trezor/utils.py b/core/src/trezor/utils.py index eab60b9cbf..54e5d459e8 100644 --- a/core/src/trezor/utils.py +++ b/core/src/trezor/utils.py @@ -106,22 +106,24 @@ def try_remove_scr(screen): pass -def turn_on_lcd_if_possible() -> bool: - return lcd_resume() +def turn_on_lcd_if_possible(timeouts_ms: int | None = None) -> bool: + return lcd_resume(timeouts_ms) -def lcd_resume() -> bool: +def lcd_resume(timeouts_ms: int | None = None) -> bool: from trezor.ui import display from storage import device from apps import base from trezor import config - if display.backlight() != device.get_brightness(): + if display.backlight() != device.get_brightness() or timeouts_ms: global AUTO_POWER_OFF display.backlight(device.get_brightness()) AUTO_POWER_OFF = False base.reload_settings_from_storage( - timeout_ms=SHORT_AUTO_LOCK_TIME_MS if not config.is_unlocked() else None + timeout_ms=(SHORT_AUTO_LOCK_TIME_MS if not timeouts_ms else timeouts_ms) + if not config.is_unlocked() + else None ) return True return False @@ -367,6 +369,13 @@ def write(self, src: bytes) -> int: self.offset += nwrite return nwrite + def append(self, b: int) -> None: + self.buffer[self.offset] = b + self.offset += 1 + + def extend(self, src: bytes) -> None: + self.write(src) + class BufferReader: """Seekable and readable view into a buffer.""" @@ -446,6 +455,10 @@ def get(self) -> int: self.offset += 1 return byte + def tell(self) -> int: + """Return the current offset.""" + return self.offset + def obj_eq(self: Any, __o: Any) -> bool: """ diff --git a/python/src/trezorlib/cardano.py b/python/src/trezorlib/cardano.py index 481665c171..f9597c90f7 100644 --- a/python/src/trezorlib/cardano.py +++ b/python/src/trezorlib/cardano.py @@ -61,12 +61,11 @@ "owners", ) REQUIRED_FIELDS_TOKEN_GROUP = ("policy_id", "tokens") -REQUIRED_FIELDS_GOVERNANCE_REGISTRATION = ( +REQUIRED_FIELDS_CVOTE_REGISTRATION = ( "staking_path", "nonce", - "reward_address_parameters", ) -REQUIRED_FIELDS_GOVERNANCE_DELEGATION = ("voting_public_key", "weight") +REQUIRED_FIELDS_CVOTE_DELEGATION = ("vote_public_key", "weight") INCOMPLETE_OUTPUT_ERROR_MESSAGE = "The output is missing some fields" @@ -421,6 +420,27 @@ def parse_certificate(certificate: dict) -> CertificateWithPoolOwnersAndRelays: ), None, ) + elif certificate_type in ( + messages.CardanoCertificateType.STAKE_REGISTRATION_CONWAY, + messages.CardanoCertificateType.STAKE_DEREGISTRATION_CONWAY, + ): + if "deposit" not in certificate: + raise CERTIFICATE_MISSING_FIELDS_ERROR + + path, script_hash, key_hash = _parse_credential( + certificate, CERTIFICATE_MISSING_FIELDS_ERROR + ) + + return ( + messages.CardanoTxCertificate( + type=certificate_type, + path=path, + script_hash=script_hash, + key_hash=key_hash, + deposit=int(certificate["deposit"]), + ), + None, + ) elif certificate_type == messages.CardanoCertificateType.STAKE_POOL_REGISTRATION: pool_parameters = certificate["pool_parameters"] @@ -466,6 +486,30 @@ def parse_certificate(certificate: dict) -> CertificateWithPoolOwnersAndRelays: ), (owners, relays), ) + if certificate_type == messages.CardanoCertificateType.VOTE_DELEGATION: + if "drep" not in certificate: + raise CERTIFICATE_MISSING_FIELDS_ERROR + + path, script_hash, key_hash = _parse_credential( + certificate, CERTIFICATE_MISSING_FIELDS_ERROR + ) + + return ( + messages.CardanoTxCertificate( + type=certificate_type, + path=path, + script_hash=script_hash, + key_hash=key_hash, + drep=messages.CardanoDRep( + type=messages.CardanoDRepType(certificate["drep"]["type"]), + key_hash=parse_optional_bytes(certificate["drep"].get("key_hash")), + script_hash=parse_optional_bytes( + certificate["drep"].get("script_hash") + ), + ), + ), + None, + ) else: raise ValueError("Unknown certificate type") @@ -563,55 +607,55 @@ def parse_auxiliary_data( # include all provided fields so we can test validation in FW hash = parse_optional_bytes(auxiliary_data.get("hash")) - governance_registration_parameters = None - if "governance_registration_parameters" in auxiliary_data: - governance_registration = auxiliary_data["governance_registration_parameters"] - if not all( - k in governance_registration - for k in REQUIRED_FIELDS_GOVERNANCE_REGISTRATION - ): + cvote_registration_parameters = None + if "cvote_registration_parameters" in auxiliary_data: + cvote_registration = auxiliary_data["cvote_registration_parameters"] + if not all(k in cvote_registration for k in REQUIRED_FIELDS_CVOTE_REGISTRATION): raise AUXILIARY_DATA_MISSING_FIELDS_ERROR - serialization_format = governance_registration.get("format") + serialization_format = cvote_registration.get("format") delegations = [] - for delegation in governance_registration.get("delegations", []): - if not all(k in delegation for k in REQUIRED_FIELDS_GOVERNANCE_DELEGATION): + for delegation in cvote_registration.get("delegations", []): + if not all(k in delegation for k in REQUIRED_FIELDS_CVOTE_DELEGATION): raise AUXILIARY_DATA_MISSING_FIELDS_ERROR delegations.append( - messages.CardanoGovernanceRegistrationDelegation( - voting_public_key=bytes.fromhex(delegation["voting_public_key"]), + messages.CardanoCVoteRegistrationDelegation( + vote_public_key=bytes.fromhex(delegation["vote_public_key"]), weight=int(delegation["weight"]), ) ) voting_purpose = None - if serialization_format == messages.CardanoGovernanceRegistrationFormat.CIP36: - voting_purpose = governance_registration.get("voting_purpose") + if serialization_format == messages.CardanoCVoteRegistrationFormat.CIP36: + voting_purpose = cvote_registration.get("voting_purpose") - governance_registration_parameters = ( - messages.CardanoGovernanceRegistrationParametersType( - voting_public_key=parse_optional_bytes( - governance_registration.get("voting_public_key") - ), - staking_path=tools.parse_path(governance_registration["staking_path"]), - nonce=governance_registration["nonce"], - reward_address_parameters=_parse_address_parameters( - governance_registration["reward_address_parameters"], + cvote_registration_parameters = messages.CardanoCVoteRegistrationParametersType( + vote_public_key=parse_optional_bytes( + cvote_registration.get("vote_public_key") + ), + staking_path=tools.parse_path(cvote_registration["staking_path"]), + nonce=cvote_registration["nonce"], + payment_address=cvote_registration.get("payment_address"), + payment_address_parameters=( + _parse_address_parameters( + cvote_registration["payment_address_parameters"], str(AUXILIARY_DATA_MISSING_FIELDS_ERROR), - ), - format=serialization_format, - delegations=delegations, - voting_purpose=voting_purpose, - ) + ) + if "payment_address_parameters" in cvote_registration + else None + ), + format=serialization_format, + delegations=delegations, + voting_purpose=voting_purpose, ) - if hash is None and governance_registration_parameters is None: + if hash is None and cvote_registration_parameters is None: raise AUXILIARY_DATA_MISSING_FIELDS_ERROR return messages.CardanoTxAuxiliaryData( hash=hash, - governance_registration_parameters=governance_registration_parameters, + cvote_registration_parameters=cvote_registration_parameters, ) @@ -687,6 +731,9 @@ def _get_witness_requests( in ( messages.CardanoCertificateType.STAKE_DEREGISTRATION, messages.CardanoCertificateType.STAKE_DELEGATION, + messages.CardanoCertificateType.STAKE_REGISTRATION_CONWAY, + messages.CardanoCertificateType.STAKE_DEREGISTRATION_CONWAY, + messages.CardanoCertificateType.VOTE_DELEGATION, ) and certificate.path ): @@ -784,6 +831,7 @@ def get_address( network_id: int = NETWORK_IDS["mainnet"], show_display: bool = False, derivation_type: messages.CardanoDerivationType = messages.CardanoDerivationType.ICARUS, + chunkify: bool = False, ) -> "MessageType": return client.call( messages.CardanoGetAddress( @@ -792,6 +840,7 @@ def get_address( network_id=network_id, show_display=show_display, derivation_type=derivation_type, + chunkify=chunkify, ) ) @@ -801,10 +850,13 @@ def get_public_key( client: "TrezorClient", address_n: List[int], derivation_type: messages.CardanoDerivationType = messages.CardanoDerivationType.ICARUS, + show_display: bool = False, ) -> "MessageType": return client.call( messages.CardanoGetPublicKey( - address_n=address_n, derivation_type=derivation_type + address_n=address_n, + derivation_type=derivation_type, + show_display=show_display, ) ) @@ -848,6 +900,8 @@ def sign_tx( additional_witness_requests: Sequence[Path] = (), derivation_type: messages.CardanoDerivationType = messages.CardanoDerivationType.ICARUS, include_network_id: bool = False, + chunkify: bool = False, + tag_cbor_sets: bool = False, ) -> Dict[str, Any]: UNEXPECTED_RESPONSE_ERROR = exceptions.TrezorException("Unexpected response") @@ -884,6 +938,8 @@ def sign_tx( witness_requests_count=len(witness_requests), derivation_type=derivation_type, include_network_id=include_network_id, + chunkify=chunkify, + tag_cbor_sets=tag_cbor_sets, ) ) if not isinstance(response, messages.CardanoTxItemAck): @@ -911,9 +967,9 @@ def sign_tx( auxiliary_data_supplement.type != messages.CardanoTxAuxiliaryDataSupplementType.NONE ): - sign_tx_response[ - "auxiliary_data_supplement" - ] = auxiliary_data_supplement.__dict__ + sign_tx_response["auxiliary_data_supplement"] = ( + auxiliary_data_supplement.__dict__ + ) response = client.call(messages.CardanoTxHostAck()) if not isinstance(response, messages.CardanoTxItemAck): diff --git a/python/src/trezorlib/cli/cardano.py b/python/src/trezorlib/cli/cardano.py index 6b6e0b1702..561238bb62 100644 --- a/python/src/trezorlib/cli/cardano.py +++ b/python/src/trezorlib/cli/cardano.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from ..client import TrezorClient -PATH_HELP = "BIP-32 path to key, e.g. m/44'/1815'/0'/0/0" +PATH_HELP = "BIP-32 path to key, e.g. m/44h/1815h/0h/0/0" TESTNET_CHOICES = { "preprod": "testnet_preprod", @@ -60,6 +60,8 @@ def cli() -> None: default=messages.CardanoDerivationType.ICARUS, ) @click.option("-i", "--include-network-id", is_flag=True) +@click.option("-C", "chunkify", is_flag=True) +@click.option("-T", "--tag-cbor-sets", is_flag=True) @with_client def sign_tx( client: "TrezorClient", @@ -70,6 +72,8 @@ def sign_tx( testnet: str, derivation_type: messages.CardanoDerivationType, include_network_id: bool, + chunkify: bool, + tag_cbor_sets: bool, ) -> cardano.SignTxResponse: """Sign Cardano transaction.""" transaction = json.load(file) @@ -143,6 +147,8 @@ def sign_tx( additional_witness_requests, derivation_type=derivation_type, include_network_id=include_network_id, + chunkify=chunkify, + tag_cbor_sets=tag_cbor_sets, ) sign_tx_response["tx_hash"] = sign_tx_response["tx_hash"].hex() @@ -151,9 +157,11 @@ def sign_tx( "type": witness["type"], "pub_key": witness["pub_key"].hex(), "signature": witness["signature"].hex(), - "chain_code": witness["chain_code"].hex() - if witness["chain_code"] is not None - else None, + "chain_code": ( + witness["chain_code"].hex() + if witness["chain_code"] is not None + else None + ), } for witness in sign_tx_response["witnesses"] ] @@ -162,11 +170,13 @@ def sign_tx( auxiliary_data_supplement["auxiliary_data_hash"] = auxiliary_data_supplement[ "auxiliary_data_hash" ].hex() - governance_signature = auxiliary_data_supplement.get("governance_signature") - if governance_signature: - auxiliary_data_supplement[ - "governance_signature" - ] = governance_signature.hex() + cvote_registration_signature = auxiliary_data_supplement.get( + "cvote_registration_signature" + ) + if cvote_registration_signature: + auxiliary_data_supplement["cvote_registration_signature"] = ( + cvote_registration_signature.hex() + ) sign_tx_response["auxiliary_data_supplement"] = auxiliary_data_supplement return sign_tx_response @@ -198,6 +208,7 @@ def sign_tx( type=ChoiceType({m.name: m for m in messages.CardanoDerivationType}), default=messages.CardanoDerivationType.ICARUS, ) +@click.option("-C", "--chunkify", is_flag=True) @with_client def get_address( client: "TrezorClient", @@ -215,6 +226,7 @@ def get_address( show_display: bool, testnet: str, derivation_type: messages.CardanoDerivationType, + chunkify: bool, ) -> str: """ Get Cardano address. @@ -258,6 +270,7 @@ def get_address( network_id, show_display, derivation_type=derivation_type, + chunkify=chunkify, ) @@ -269,16 +282,20 @@ def get_address( type=ChoiceType({m.name: m for m in messages.CardanoDerivationType}), default=messages.CardanoDerivationType.ICARUS, ) +@click.option("-d", "--show-display", is_flag=True) @with_client def get_public_key( client: "TrezorClient", address: str, derivation_type: messages.CardanoDerivationType, + show_display: bool, ) -> messages.CardanoPublicKey: """Get Cardano public key.""" address_n = tools.parse_path(address) client.init_device(derive_cardano=True) - return cardano.get_public_key(client, address_n, derivation_type=derivation_type) + return cardano.get_public_key( + client, address_n, derivation_type=derivation_type, show_display=show_display + ) @cli.command() diff --git a/python/src/trezorlib/cli/sol.py b/python/src/trezorlib/cli/sol.py index 9530e3b4c7..2b21b35756 100644 --- a/python/src/trezorlib/cli/sol.py +++ b/python/src/trezorlib/cli/sol.py @@ -17,17 +17,28 @@ from typing import TYPE_CHECKING +from typing import Optional + import base58 import click -from .. import solana, tools -from . import with_client +from .. import solana, tools, messages +from . import with_client, ChoiceType if TYPE_CHECKING: from ..client import TrezorClient + + PATH_HELP = "BIP-32 path, e.g. m/44'/501'/0'/0'" PATH_RAW_TX = "Base58 encoded transaction" +MESSAGE_VERSIONS = { + "v0": messages.SolanaOffChainMessageVersion.MESSAGE_VERSION_0, +} +MESSAGE_FORMATS = { + "ascii": messages.SolanaOffChainMessageFormat.V0_RESTRICTED_ASCII, + "utf8": messages.SolanaOffChainMessageFormat.V0_LIMITED_UTF8, +} @click.group(name="sol") def cli(): @@ -53,3 +64,30 @@ def sign_tx(client: "TrezorClient", address: str, raw_tx: str) -> str: address_n = tools.parse_path(address) transaction = solana.sign_tx(client, address_n, base58.b58decode(raw_tx)) return transaction.signature.hex() + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-v", "--message-version", type=ChoiceType(MESSAGE_VERSIONS), default="v0") +@click.option("-f", "--message-format", type=ChoiceType(MESSAGE_FORMATS), default="ascii") +@click.option("-d", "--application-domain", default=None, help="32 bytes hex encoded application domain or None") +@click.option("-u", "--unsafe", is_flag=True, help="Use unsafe message signing protocol") +@click.argument("message") +@with_client +def sign_message(client: "TrezorClient", + address: str, + message: str, + message_version: str, + message_format: str, + application_domain: Optional[str], + unsafe: bool +): + """Sign Solana message.""" + address_n = tools.parse_path(address) + if unsafe: + rep = solana.sign_unsafe_message(client, address_n, tools.prepare_message_bytes(message)) + else: + rep = solana.sign_offchain_message(client, address_n, tools.prepare_message_bytes(message), message_version, message_format, application_domain) + return { + "public_key": f"0x{rep.public_key.hex()}" if not unsafe else None, + "signature": f"0x{rep.signature.hex()}", + } diff --git a/python/src/trezorlib/cli/ton.py b/python/src/trezorlib/cli/ton.py index 254b4b10cf..c5707f567c 100644 --- a/python/src/trezorlib/cli/ton.py +++ b/python/src/trezorlib/cli/ton.py @@ -15,7 +15,7 @@ # If not, see . -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Tuple, Dict import click import time @@ -58,7 +58,7 @@ def get_address(client: "TrezorClient", version: messages.TonWalletVersion, workchain: messages.TonWorkChain, show_display: bool - ) -> str: + ) -> Dict[str, str]: """Get Ton address for specified path.""" address_n = tools.parse_path(address) resp = ton.get_address(client, address_n, version, workchain, bounceable, test_only, wallet_id, show_display) @@ -109,11 +109,11 @@ def sign_message(client: "TrezorClient", workchain: messages.TonWorkChain, bounceable: bool, test_only: bool, - ext_destination: tuple[str, ...], - ext_ton_amount: tuple[int, ...], - ext_payload: tuple[str, ...], + ext_destination: Tuple[str, ...], + ext_ton_amount: Tuple[int, ...], + ext_payload: Tuple[str, ...], signing_message_hash: str - ) -> bytes: + ) -> Dict[str, str]: """Sign Ton Transaction.""" address_n = tools.parse_path(address) # expire_at = int(time.time()) + 300 @@ -142,8 +142,9 @@ def sign_message(client: "TrezorClient", list(ext_payload), signing_message_hash ) - - return resp.signature.hex(), resp.signning_message.hex() + assert resp.signature is not None + assert resp.signning_message is not None + return {"signature": f"0x{resp.signature.hex()}", "signed_message": f"0x{resp.signning_message.hex()}"} @cli.command() @click.option("-n", "--address", required=True, help=PATH_HELP) @@ -165,12 +166,12 @@ def sign_proof(client: "TrezorClient", workchain: messages.TonWorkChain, bounceable: bool, test_only: bool - ) -> bytes: + ) -> Dict[str, str]: """Sign Ton Proof.""" address_n = tools.parse_path(address) # expire_at = int(time.time()) + 300 expire_at = 1979465599 - signature = ton.sign_proof( + resp = ton.sign_proof( client, address_n, expire_at, @@ -181,6 +182,6 @@ def sign_proof(client: "TrezorClient", workchain, bounceable, test_only - ).signature.hex() - - return {"signature": f"0x{signature}"} \ No newline at end of file + ) + assert resp.signature is not None + return {"signature": f"0x{resp.signature.hex()}"} diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 07fbe74363..177e46bfed 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -99,6 +99,8 @@ class MessageType(IntEnum): GetOwnershipProof = 49 OwnershipProof = 50 AuthorizeCoinJoin = 51 + SignPsbt = 10052 + SignedPsbt = 10053 CipherKeyValue = 23 CipheredKeyValue = 48 SignIdentity = 53 @@ -337,6 +339,9 @@ class MessageType(IntEnum): SolanaAddress = 10101 SolanaSignTx = 10102 SolanaSignedTx = 10103 + SolanaSignOffChainMessage = 10104 + SolanaMessageSignature = 10105 + SolanaSignUnsafeMessage = 10106 CosmosGetAddress = 10800 CosmosAddress = 10801 CosmosSignTx = 10802 @@ -556,6 +561,16 @@ class CardanoCertificateType(IntEnum): STAKE_DEREGISTRATION = 1 STAKE_DELEGATION = 2 STAKE_POOL_REGISTRATION = 3 + STAKE_REGISTRATION_CONWAY = 7 + STAKE_DEREGISTRATION_CONWAY = 8 + VOTE_DELEGATION = 9 + + +class CardanoDRepType(IntEnum): + KEY_HASH = 0 + SCRIPT_HASH = 1 + ABSTAIN = 2 + NO_CONFIDENCE = 3 class CardanoPoolRelayType(IntEnum): @@ -566,10 +581,10 @@ class CardanoPoolRelayType(IntEnum): class CardanoTxAuxiliaryDataSupplementType(IntEnum): NONE = 0 - GOVERNANCE_REGISTRATION_SIGNATURE = 1 + CVOTE_REGISTRATION_SIGNATURE = 1 -class CardanoGovernanceRegistrationFormat(IntEnum): +class CardanoCVoteRegistrationFormat(IntEnum): CIP15 = 0 CIP36 = 1 @@ -720,6 +735,15 @@ class NEMImportanceTransferMode(IntEnum): ImportanceTransfer_Deactivate = 2 +class SolanaOffChainMessageVersion(IntEnum): + MESSAGE_VERSION_0 = 0 + + +class SolanaOffChainMessageFormat(IntEnum): + V0_RESTRICTED_ASCII = 0 + V0_LIMITED_UTF8 = 1 + + class StellarAssetType(IntEnum): NATIVE = 0 ALPHANUM4 = 1 @@ -1500,6 +1524,7 @@ class SignMessage(protobuf.MessageType): 3: protobuf.Field("coin_name", "string", repeated=False, required=False), 4: protobuf.Field("script_type", "InputScriptType", repeated=False, required=False), 5: protobuf.Field("no_script_type", "bool", repeated=False, required=False), + 10: protobuf.Field("is_bip322_simple", "bool", repeated=False, required=False), } def __init__( @@ -1510,12 +1535,14 @@ def __init__( coin_name: Optional["str"] = 'Bitcoin', script_type: Optional["InputScriptType"] = InputScriptType.SPENDADDRESS, no_script_type: Optional["bool"] = None, + is_bip322_simple: Optional["bool"] = False, ) -> None: self.address_n: Sequence["int"] = address_n if address_n is not None else [] self.message = message self.coin_name = coin_name self.script_type = script_type self.no_script_type = no_script_type + self.is_bip322_simple = is_bip322_simple class MessageSignature(protobuf.MessageType): @@ -2023,6 +2050,37 @@ def __init__( self.amount_unit = amount_unit +class SignPsbt(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 10052 + FIELDS = { + 1: protobuf.Field("psbt", "bytes", repeated=False, required=True), + 2: protobuf.Field("coin_name", "string", repeated=False, required=False), + } + + def __init__( + self, + *, + psbt: "bytes", + coin_name: Optional["str"] = 'Bitcoin', + ) -> None: + self.psbt = psbt + self.coin_name = coin_name + + +class SignedPsbt(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 10053 + FIELDS = { + 1: protobuf.Field("psbt", "bytes", repeated=False, required=True), + } + + def __init__( + self, + *, + psbt: "bytes", + ) -> None: + self.psbt = psbt + + class HDNodePathType(protobuf.MessageType): MESSAGE_WIRE_TYPE = None FIELDS = { @@ -2659,6 +2717,7 @@ class CardanoGetAddress(protobuf.MessageType): 4: protobuf.Field("network_id", "uint32", repeated=False, required=True), 5: protobuf.Field("address_parameters", "CardanoAddressParametersType", repeated=False, required=True), 6: protobuf.Field("derivation_type", "CardanoDerivationType", repeated=False, required=True), + 7: protobuf.Field("chunkify", "bool", repeated=False, required=False), } def __init__( @@ -2669,12 +2728,14 @@ def __init__( address_parameters: "CardanoAddressParametersType", derivation_type: "CardanoDerivationType", show_display: Optional["bool"] = False, + chunkify: Optional["bool"] = None, ) -> None: self.protocol_magic = protocol_magic self.network_id = network_id self.address_parameters = address_parameters self.derivation_type = derivation_type self.show_display = show_display + self.chunkify = chunkify class CardanoAddress(protobuf.MessageType): @@ -2752,6 +2813,8 @@ class CardanoSignTxInit(protobuf.MessageType): 19: protobuf.Field("has_collateral_return", "bool", repeated=False, required=False), 20: protobuf.Field("total_collateral", "uint64", repeated=False, required=False), 21: protobuf.Field("reference_inputs_count", "uint32", repeated=False, required=False), + 22: protobuf.Field("chunkify", "bool", repeated=False, required=False), + 23: protobuf.Field("tag_cbor_sets", "bool", repeated=False, required=False), } def __init__( @@ -2778,6 +2841,8 @@ def __init__( has_collateral_return: Optional["bool"] = False, total_collateral: Optional["int"] = None, reference_inputs_count: Optional["int"] = 0, + chunkify: Optional["bool"] = None, + tag_cbor_sets: Optional["bool"] = False, ) -> None: self.signing_mode = signing_mode self.protocol_magic = protocol_magic @@ -2800,6 +2865,8 @@ def __init__( self.has_collateral_return = has_collateral_return self.total_collateral = total_collateral self.reference_inputs_count = reference_inputs_count + self.chunkify = chunkify + self.tag_cbor_sets = tag_cbor_sets class CardanoTxInput(protobuf.MessageType): @@ -3020,6 +3087,26 @@ def __init__( self.metadata = metadata +class CardanoDRep(protobuf.MessageType): + MESSAGE_WIRE_TYPE = None + FIELDS = { + 1: protobuf.Field("type", "CardanoDRepType", repeated=False, required=True), + 2: protobuf.Field("key_hash", "bytes", repeated=False, required=False), + 3: protobuf.Field("script_hash", "bytes", repeated=False, required=False), + } + + def __init__( + self, + *, + type: "CardanoDRepType", + key_hash: Optional["bytes"] = None, + script_hash: Optional["bytes"] = None, + ) -> None: + self.type = type + self.key_hash = key_hash + self.script_hash = script_hash + + class CardanoTxCertificate(protobuf.MessageType): MESSAGE_WIRE_TYPE = 325 FIELDS = { @@ -3029,6 +3116,8 @@ class CardanoTxCertificate(protobuf.MessageType): 4: protobuf.Field("pool_parameters", "CardanoPoolParametersType", repeated=False, required=False), 5: protobuf.Field("script_hash", "bytes", repeated=False, required=False), 6: protobuf.Field("key_hash", "bytes", repeated=False, required=False), + 7: protobuf.Field("deposit", "uint64", repeated=False, required=False), + 8: protobuf.Field("drep", "CardanoDRep", repeated=False, required=False), } def __init__( @@ -3040,6 +3129,8 @@ def __init__( pool_parameters: Optional["CardanoPoolParametersType"] = None, script_hash: Optional["bytes"] = None, key_hash: Optional["bytes"] = None, + deposit: Optional["int"] = None, + drep: Optional["CardanoDRep"] = None, ) -> None: self.path: Sequence["int"] = path if path is not None else [] self.type = type @@ -3047,6 +3138,8 @@ def __init__( self.pool_parameters = pool_parameters self.script_hash = script_hash self.key_hash = key_hash + self.deposit = deposit + self.drep = drep class CardanoTxWithdrawal(protobuf.MessageType): @@ -3072,69 +3165,72 @@ def __init__( self.key_hash = key_hash -class CardanoGovernanceRegistrationDelegation(protobuf.MessageType): +class CardanoCVoteRegistrationDelegation(protobuf.MessageType): MESSAGE_WIRE_TYPE = None FIELDS = { - 1: protobuf.Field("voting_public_key", "bytes", repeated=False, required=True), + 1: protobuf.Field("vote_public_key", "bytes", repeated=False, required=True), 2: protobuf.Field("weight", "uint32", repeated=False, required=True), } def __init__( self, *, - voting_public_key: "bytes", + vote_public_key: "bytes", weight: "int", ) -> None: - self.voting_public_key = voting_public_key + self.vote_public_key = vote_public_key self.weight = weight -class CardanoGovernanceRegistrationParametersType(protobuf.MessageType): +class CardanoCVoteRegistrationParametersType(protobuf.MessageType): MESSAGE_WIRE_TYPE = None FIELDS = { - 1: protobuf.Field("voting_public_key", "bytes", repeated=False, required=False), + 1: protobuf.Field("vote_public_key", "bytes", repeated=False, required=False), 2: protobuf.Field("staking_path", "uint32", repeated=True, required=False), - 3: protobuf.Field("reward_address_parameters", "CardanoAddressParametersType", repeated=False, required=True), + 3: protobuf.Field("payment_address_parameters", "CardanoAddressParametersType", repeated=False, required=False), 4: protobuf.Field("nonce", "uint64", repeated=False, required=True), - 5: protobuf.Field("format", "CardanoGovernanceRegistrationFormat", repeated=False, required=False), - 6: protobuf.Field("delegations", "CardanoGovernanceRegistrationDelegation", repeated=True, required=False), + 5: protobuf.Field("format", "CardanoCVoteRegistrationFormat", repeated=False, required=False), + 6: protobuf.Field("delegations", "CardanoCVoteRegistrationDelegation", repeated=True, required=False), 7: protobuf.Field("voting_purpose", "uint64", repeated=False, required=False), + 8: protobuf.Field("payment_address", "string", repeated=False, required=False), } def __init__( self, *, - reward_address_parameters: "CardanoAddressParametersType", nonce: "int", staking_path: Optional[Sequence["int"]] = None, - delegations: Optional[Sequence["CardanoGovernanceRegistrationDelegation"]] = None, - voting_public_key: Optional["bytes"] = None, - format: Optional["CardanoGovernanceRegistrationFormat"] = CardanoGovernanceRegistrationFormat.CIP15, + delegations: Optional[Sequence["CardanoCVoteRegistrationDelegation"]] = None, + vote_public_key: Optional["bytes"] = None, + payment_address_parameters: Optional["CardanoAddressParametersType"] = None, + format: Optional["CardanoCVoteRegistrationFormat"] = CardanoCVoteRegistrationFormat.CIP15, voting_purpose: Optional["int"] = None, + payment_address: Optional["str"] = None, ) -> None: self.staking_path: Sequence["int"] = staking_path if staking_path is not None else [] - self.delegations: Sequence["CardanoGovernanceRegistrationDelegation"] = delegations if delegations is not None else [] - self.reward_address_parameters = reward_address_parameters + self.delegations: Sequence["CardanoCVoteRegistrationDelegation"] = delegations if delegations is not None else [] self.nonce = nonce - self.voting_public_key = voting_public_key + self.vote_public_key = vote_public_key + self.payment_address_parameters = payment_address_parameters self.format = format self.voting_purpose = voting_purpose + self.payment_address = payment_address class CardanoTxAuxiliaryData(protobuf.MessageType): MESSAGE_WIRE_TYPE = 327 FIELDS = { - 1: protobuf.Field("governance_registration_parameters", "CardanoGovernanceRegistrationParametersType", repeated=False, required=False), + 1: protobuf.Field("cvote_registration_parameters", "CardanoCVoteRegistrationParametersType", repeated=False, required=False), 2: protobuf.Field("hash", "bytes", repeated=False, required=False), } def __init__( self, *, - governance_registration_parameters: Optional["CardanoGovernanceRegistrationParametersType"] = None, + cvote_registration_parameters: Optional["CardanoCVoteRegistrationParametersType"] = None, hash: Optional["bytes"] = None, ) -> None: - self.governance_registration_parameters = governance_registration_parameters + self.cvote_registration_parameters = cvote_registration_parameters self.hash = hash @@ -3212,7 +3308,7 @@ class CardanoTxAuxiliaryDataSupplement(protobuf.MessageType): FIELDS = { 1: protobuf.Field("type", "CardanoTxAuxiliaryDataSupplementType", repeated=False, required=True), 2: protobuf.Field("auxiliary_data_hash", "bytes", repeated=False, required=False), - 3: protobuf.Field("governance_signature", "bytes", repeated=False, required=False), + 3: protobuf.Field("cvote_registration_signature", "bytes", repeated=False, required=False), } def __init__( @@ -3220,11 +3316,11 @@ def __init__( *, type: "CardanoTxAuxiliaryDataSupplementType", auxiliary_data_hash: Optional["bytes"] = None, - governance_signature: Optional["bytes"] = None, + cvote_registration_signature: Optional["bytes"] = None, ) -> None: self.type = type self.auxiliary_data_hash = auxiliary_data_hash - self.governance_signature = governance_signature + self.cvote_registration_signature = cvote_registration_signature class CardanoTxWitnessRequest(protobuf.MessageType): @@ -9148,6 +9244,66 @@ def __init__( self.signature = signature +class SolanaSignOffChainMessage(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 10104 + FIELDS = { + 1: protobuf.Field("address_n", "uint32", repeated=True, required=False), + 2: protobuf.Field("message", "bytes", repeated=False, required=True), + 3: protobuf.Field("message_version", "SolanaOffChainMessageVersion", repeated=False, required=False), + 4: protobuf.Field("message_format", "SolanaOffChainMessageFormat", repeated=False, required=False), + 5: protobuf.Field("application_domain", "bytes", repeated=False, required=False), + } + + def __init__( + self, + *, + message: "bytes", + address_n: Optional[Sequence["int"]] = None, + message_version: Optional["SolanaOffChainMessageVersion"] = SolanaOffChainMessageVersion.MESSAGE_VERSION_0, + message_format: Optional["SolanaOffChainMessageFormat"] = SolanaOffChainMessageFormat.V0_RESTRICTED_ASCII, + application_domain: Optional["bytes"] = None, + ) -> None: + self.address_n: Sequence["int"] = address_n if address_n is not None else [] + self.message = message + self.message_version = message_version + self.message_format = message_format + self.application_domain = application_domain + + +class SolanaSignUnsafeMessage(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 10106 + FIELDS = { + 1: protobuf.Field("address_n", "uint32", repeated=True, required=False), + 2: protobuf.Field("message", "bytes", repeated=False, required=True), + } + + def __init__( + self, + *, + message: "bytes", + address_n: Optional[Sequence["int"]] = None, + ) -> None: + self.address_n: Sequence["int"] = address_n if address_n is not None else [] + self.message = message + + +class SolanaMessageSignature(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 10105 + FIELDS = { + 1: protobuf.Field("signature", "bytes", repeated=False, required=True), + 2: protobuf.Field("public_key", "bytes", repeated=False, required=False), + } + + def __init__( + self, + *, + signature: "bytes", + public_key: Optional["bytes"] = None, + ) -> None: + self.signature = signature + self.public_key = public_key + + class StarcoinGetAddress(protobuf.MessageType): MESSAGE_WIRE_TYPE = 10300 FIELDS = { diff --git a/python/src/trezorlib/solana.py b/python/src/trezorlib/solana.py index fb3723f221..9224f5c509 100644 --- a/python/src/trezorlib/solana.py +++ b/python/src/trezorlib/solana.py @@ -1,5 +1,7 @@ from typing import TYPE_CHECKING +from typing import Optional + from . import messages from .tools import expect @@ -26,3 +28,33 @@ def sign_tx( address_n=n, ) return client.call(msg) + +@expect(messages.SolanaMessageSignature) +def sign_offchain_message( + client: "TrezorClient", + n: "Address", + message: bytes, + message_version: messages.SolanaOffChainMessageVersion, + message_format: messages.SolanaOffChainMessageFormat, + application_domain: Optional[str] = None +): + msg = messages.SolanaSignOffChainMessage( + message=message, + address_n=n, + message_version=message_version, + message_format=message_format, + application_domain=bytes.fromhex(application_domain) if application_domain else None, + ) + return client.call(msg) + +@expect(messages.SolanaMessageSignature) +def sign_unsafe_message( + client: "TrezorClient", + n: "Address", + message: bytes, +): + msg = messages.SolanaSignUnsafeMessage( + message=message, + address_n=n, + ) + return client.call(msg) diff --git a/python/src/trezorlib/ton.py b/python/src/trezorlib/ton.py index dc096f68f6..502e77a94d 100644 --- a/python/src/trezorlib/ton.py +++ b/python/src/trezorlib/ton.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Optional from . import messages from .tools import expect @@ -49,10 +49,10 @@ def sign_message(client: "TrezorClient", workchain: messages.TonWorkChain=messages.TonWorkChain.BASECHAIN, bounceable: bool = False, test_only: bool = False, - ext_destination: list[str] = None, - ext_ton_amount: list[int] = None, - ext_payload: list[str] = None, - signing_message_hash: str = None + ext_destination: Optional[List[str]] = None, + ext_ton_amount: Optional[List[int]] = None, + ext_payload: Optional[List[str]] = None, + signing_message_hash: Optional[str] = None ): if jetton_amount_bytes is not None: jetton_amount_bytes = int(jetton_amount_bytes).to_bytes((int(jetton_amount_bytes).bit_length() + 7) // 8, byteorder='big') @@ -113,4 +113,4 @@ def sign_proof(client: "TrezorClient", bounceable=bounceable, is_test_only=test_only, ) - ) \ No newline at end of file + ) diff --git a/tools/i18n.py b/tools/i18n.py index 77eb40a5c8..807f1ca8fb 100644 --- a/tools/i18n.py +++ b/tools/i18n.py @@ -6,7 +6,7 @@ BASE_PATH = os.path.join( os.path.dirname(__file__), "..", "core/src/trezor/lvglui/i18n/" ) -SUPPORTED_LANGS = ("en", "zh_CN", "zh_HK", "ja", "ko", "fr", "de", "ru", "es", "it") +SUPPORTED_LANGS = ("en", "zh_CN", "zh_HK", "ja", "ko", "fr", "de", "ru", "es", "it", "pt_BR") CHARS_NORMAL = set() CHARS_TITLE = set() CHARS_SUBTITLE = set() @@ -25,6 +25,7 @@ 'ko': 'Korean', 'mn_MN': 'Mongolian', 'pt': 'Portuguese', + 'pt_BR': 'Portuguese (Brazil)', 'ru': 'Russian', 'es': 'Spanish', 'th': 'Thai',