diff --git a/common/protob/check.py b/common/protob/check.py index 410b84f984..e67a15070e 100755 --- a/common/protob/check.py +++ b/common/protob/check.py @@ -15,7 +15,7 @@ for fn in sorted(glob(os.path.join(MYDIR, "messages-*.proto"))): with open(fn, "rt") as f: prefix = EXPECTED_PREFIX_RE.search(fn).group(1).capitalize() - if prefix in ("Bitcoin", "Bootloader", "Common", "Crypto", "Management"): + if prefix in ("Bitcoin", "Bootloader", "Common", "Crypto", "Management", "Ton"): continue if prefix == "Nem": prefix = "NEM" diff --git a/common/protob/messages-ton.proto b/common/protob/messages-ton.proto new file mode 100644 index 0000000000..5455971e51 --- /dev/null +++ b/common/protob/messages-ton.proto @@ -0,0 +1,109 @@ +syntax = "proto2"; +package hw.trezor.messages.ton; + +// Sugar for easier handling in Java +option java_package = "com.satoshilabs.trezor.lib.protobuf"; +option java_outer_classname = "TrezorMessageTon"; + +enum TonWalletVersion { + // V3R1 = 0; + // V3R2 = 1; + // V4R1 = 2; + V4R2 = 3; +} + +enum TonWorkChain { + BASECHAIN = 0; + MASTERCHAIN = 1; +} + +/** + * Request: Ask device for Ton address(account_id) corresponding to address_n path + * @start + * @next TonAddress + */ +message TonGetAddress { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + optional bool show_display = 2; // optionally show on display before sending the result + optional TonWalletVersion wallet_version = 3 [default=V4R2]; // ton wallet version + optional bool is_bounceable = 4 [default=false]; // bounceable flag + optional bool is_testnet_only = 5 [default=false]; // testnet only flag + optional TonWorkChain workchain = 6 [default=BASECHAIN]; // 0 for the BaseChain, 1 for the MasterChain + optional uint32 wallet_id = 7 [default=698983191]; // 698983191 is the default subwallet_id value +} + +/** + * Response: Contains an Ton address calculated from hash(initial code, initial state) + * @end + */ +message TonAddress { + required bytes public_key = 1; + required string address = 2; // ton base64 user-friendly url-safe address +} + +/** + * Request: Require Device to sign toncoin/jetton message + * @start + * @next TonSignedMessage + * @next Failure + */ +message TonSignMessage { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + required string destination = 2; // destination address of the message + optional string jetton_master_address = 3; // Jetton master smart contract address + optional string jetton_wallet_address = 4; // Jetton wallet smart contract address + required uint64 ton_amount = 5; // TON value for gas + optional uint64 jetton_amount = 6; // jetton value + optional uint64 fwd_fee = 7 [default=0]; // toncoin is needed to transfer notification message + optional string comment = 8; // message comment + optional bool is_raw_data = 9 [default=false]; // raw data flag + optional uint32 mode = 10 [default=3]; // message modes + required uint32 seqno = 11; // message sequence number + required uint64 expire_at = 12; // message expiration time + optional TonWalletVersion wallet_version = 13 [default=V4R2]; // ton wallet version + optional uint32 wallet_id = 14 [default=698983191]; // 698983191 is the default subwallet_id value + optional TonWorkChain workchain = 15 [default=BASECHAIN]; // 0: BaseChain, 1: MasterChain + optional bool is_bounceable = 16 [default=false]; // bounceable flag + optional bool is_testnet_only = 17 [default=false]; // testnet only flag + repeated string ext_destination = 18; + repeated uint64 ext_ton_amount = 19; + repeated string ext_payload = 20; + optional bytes jetton_amount_bytes = 21; // jetton value in bytes + optional bytes signing_message_hash = 22; // signing message hash +} + +/** + * Response: transaction signature corresponding to TonSignMessage + * @end + */ +message TonSignedMessage { + optional bytes signature = 1; // signed transaction message + optional bytes signning_message = 2; // message to sign +} + +/** + * Request: Require Device to sign proof + * @start + * @next TonSignedProof + * @next Failure + */ +message TonSignProof { + repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node + required bytes appdomain = 2; // dapp address + optional bytes comment = 3; // message comment + required uint64 expire_at = 4; // message expiration time + optional TonWalletVersion wallet_version = 5 [default=V4R2]; // ton wallet version + optional uint32 wallet_id = 6 [default=698983191]; // 698983191 is the default subwallet_id value + optional TonWorkChain workchain = 7 [default=BASECHAIN]; // 0: BaseChain, 1: MasterChain + optional bool is_bounceable = 8 [default=false]; // bounceable flag + optional bool is_testnet_only = 9 [default=false]; // testnet only flag +} + +/** + * Response: transaction signature corresponding to TonSignProof + * @end + */ +message TonSignedProof { + optional bytes signature = 1; // signed transaction message +} + diff --git a/common/protob/messages.proto b/common/protob/messages.proto index 2613a177c5..1f0261091f 100644 --- a/common/protob/messages.proto +++ b/common/protob/messages.proto @@ -498,7 +498,7 @@ enum MessageType { MessageType_KaspaSignedTx = 11303 [(wire_out) = true]; MessageType_KaspaTxInputRequest = 11304 [(wire_out) = true]; MessageType_KaspaTxInputAck = 11305 [(wire_in) = true]; - + // Nexa MessageType_NexaGetAddress = 11400 [(wire_in) = true]; MessageType_NexaAddress = 11401 [(wire_out) = true]; @@ -520,11 +520,18 @@ enum MessageType { MessageType_NostrSignSchnorr = 11508 [(wire_in) = true]; MessageType_NostrSignedSchnorr = 11509 [(wire_out) = true]; + // Ton + MessageType_TonGetAddress = 11901 [(wire_in) = true]; + MessageType_TonAddress = 11902 [(wire_out) = true]; + MessageType_TonSignMessage = 11903 [(wire_in) = true]; + MessageType_TonSignedMessage = 11904 [(wire_out) = true]; + MessageType_TonSignProof = 11905 [(wire_in) = true]; + MessageType_TonSignedProof = 11906 [(wire_out) = true]; + // lnurl MessageType_LnurlAuth = 11600 [(wire_in) = true]; MessageType_LnurlAuthResp = 11601 [(wire_out) = true]; - //onekey MessageType_DeviceBackToBoot = 903 [(wire_in) = true]; MessageType_RebootToBoardloader = 904 [(wire_in) = true]; diff --git a/core/SConscript.firmware b/core/SConscript.firmware index 804458fbd0..ed1b359837 100644 --- a/core/SConscript.firmware +++ b/core/SConscript.firmware @@ -913,6 +913,12 @@ if FROZEN: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/tron/*.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/ton/*.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/ton/*/*.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/ton/*/*/*.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/ton/*/*/*/*.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/ton/*/*/*/*/*.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/webauthn/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/decred.py')) diff --git a/core/embed/firmware/memory_H.ld b/core/embed/firmware/memory_H.ld index e2bdf6920c..5c347789ca 100644 --- a/core/embed/firmware/memory_H.ld +++ b/core/embed/firmware/memory_H.ld @@ -52,6 +52,7 @@ SECTIONS { .flash2 : ALIGN(512) { build/firmware/frozen_mpy.o(.rodata*); build/firmware/embed/lvgl/lv_font_pljs_bold_36.o(.rodata*); + build/firmware/embed/lvgl/lv_font_pljs_bold_48.o(.rodata*); build/firmware/vendor/secp256k1-zkp/src/secp256k1.o(.rodata*); . = ALIGN(512); } >EXRAM AT>FLASH2 diff --git a/core/embed/firmware/mpconfigport.h b/core/embed/firmware/mpconfigport.h index 4f7efd8ef4..3307639154 100644 --- a/core/embed/firmware/mpconfigport.h +++ b/core/embed/firmware/mpconfigport.h @@ -109,7 +109,7 @@ #define MICROPY_PY_COLLECTIONS (1) #define MICROPY_PY_COLLECTIONS_DEQUE (0) #define MICROPY_PY_COLLECTIONS_ORDEREDDICT (0) -#define MICROPY_PY_MATH_SPECIAL_FUNCTIONS (0) +#define MICROPY_PY_MATH_SPECIAL_FUNCTIONS (1) #define MICROPY_PY_MATH_ISCLOSE (0) #define MICROPY_PY_MATH_FACTORIAL (0) #define MICROPY_PY_CMATH (0) diff --git a/core/embed/unix/mpconfigport.h b/core/embed/unix/mpconfigport.h index 16ecd2b4ca..5812682c59 100644 --- a/core/embed/unix/mpconfigport.h +++ b/core/embed/unix/mpconfigport.h @@ -123,7 +123,7 @@ #define MICROPY_PY_COLLECTIONS (1) #define MICROPY_PY_COLLECTIONS_DEQUE (0) #define MICROPY_PY_COLLECTIONS_ORDEREDDICT (0) -#define MICROPY_PY_MATH_SPECIAL_FUNCTIONS (0) +#define MICROPY_PY_MATH_SPECIAL_FUNCTIONS (1) #define MICROPY_PY_MATH_ISCLOSE (0) #define MICROPY_PY_MATH_FACTORIAL (0) #define MICROPY_PY_CMATH (0) diff --git a/core/src/all_modules.py b/core/src/all_modules.py index 710c0456b6..d5d025d4b5 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.TonWalletVersion +import trezor.enums.TonWalletVersion +trezor.enums.TonWorkChain +import trezor.enums.TonWorkChain trezor.enums.TronResourceCode import trezor.enums.TronResourceCode trezor.enums.WordRequestType @@ -779,6 +783,70 @@ import apps.sui.sign_message apps.sui.sign_tx import apps.sui.sign_tx +apps.ton +import apps.ton +apps.ton.get_address +import apps.ton.get_address +apps.ton.layout +import apps.ton.layout +apps.ton.sign_message +import apps.ton.sign_message +apps.ton.sign_proof +import apps.ton.sign_proof +apps.ton.tokens +import apps.ton.tokens +apps.ton.tonsdk +import apps.ton.tonsdk +apps.ton.tonsdk.boc +import apps.ton.tonsdk.boc +apps.ton.tonsdk.boc._bit_string +import apps.ton.tonsdk.boc._bit_string +apps.ton.tonsdk.boc._builder +import apps.ton.tonsdk.boc._builder +apps.ton.tonsdk.boc._cell +import apps.ton.tonsdk.boc._cell +apps.ton.tonsdk.boc._dict_builder +import apps.ton.tonsdk.boc._dict_builder +apps.ton.tonsdk.boc.dict +import apps.ton.tonsdk.boc.dict +apps.ton.tonsdk.boc.dict.find_common_prefix +import apps.ton.tonsdk.boc.dict.find_common_prefix +apps.ton.tonsdk.boc.dict.serialize_dict +import apps.ton.tonsdk.boc.dict.serialize_dict +apps.ton.tonsdk.contract +import apps.ton.tonsdk.contract +apps.ton.tonsdk.contract.token +import apps.ton.tonsdk.contract.token +apps.ton.tonsdk.contract.token.ft +import apps.ton.tonsdk.contract.token.ft +apps.ton.tonsdk.contract.token.ft.jetton_minter +import apps.ton.tonsdk.contract.token.ft.jetton_minter +apps.ton.tonsdk.contract.token.ft.jetton_wallet +import apps.ton.tonsdk.contract.token.ft.jetton_wallet +apps.ton.tonsdk.contract.token.nft +import apps.ton.tonsdk.contract.token.nft +apps.ton.tonsdk.contract.token.nft.nft_collection +import apps.ton.tonsdk.contract.token.nft.nft_collection +apps.ton.tonsdk.contract.token.nft.nft_item +import apps.ton.tonsdk.contract.token.nft.nft_item +apps.ton.tonsdk.contract.token.nft.nft_sale +import apps.ton.tonsdk.contract.token.nft.nft_sale +apps.ton.tonsdk.contract.token.nft.nft_utils +import apps.ton.tonsdk.contract.token.nft.nft_utils +apps.ton.tonsdk.contract.wallet +import apps.ton.tonsdk.contract.wallet +apps.ton.tonsdk.contract.wallet._wallet_contract +import apps.ton.tonsdk.contract.wallet._wallet_contract +apps.ton.tonsdk.contract.wallet._wallet_contract_v3 +import apps.ton.tonsdk.contract.wallet._wallet_contract_v3 +apps.ton.tonsdk.contract.wallet._wallet_contract_v4 +import apps.ton.tonsdk.contract.wallet._wallet_contract_v4 +apps.ton.tonsdk.utils +import apps.ton.tonsdk.utils +apps.ton.tonsdk.utils._address +import apps.ton.tonsdk.utils._address +apps.ton.tonsdk.utils._utils +import apps.ton.tonsdk.utils._utils apps.tron import apps.tron apps.tron.address diff --git a/core/src/apps/ton/__init__.py b/core/src/apps/ton/__init__.py new file mode 100644 index 0000000000..ff83c6a78a --- /dev/null +++ b/core/src/apps/ton/__init__.py @@ -0,0 +1,8 @@ +CURVE = "ed25519" + +SLIP44_ID = 607 +# https://github.com/satoshilabs/slips/blob/master/slip-0010.md + +PATTERN = "m/44'/coin_type'/account'" +PRIMARY_COLOR = 0x0098EA +ICON = "A:/res/chain-ton.png" diff --git a/core/src/apps/ton/get_address.py b/core/src/apps/ton/get_address.py new file mode 100644 index 0000000000..6782fe9f6f --- /dev/null +++ b/core/src/apps/ton/get_address.py @@ -0,0 +1,56 @@ +from typing import TYPE_CHECKING + +from trezor import wire +from trezor.enums import TonWalletVersion, TonWorkChain +from trezor.lvglui.scrs import lv +from trezor.messages import TonAddress, TonGetAddress +from trezor.ui.layouts import show_address + +from apps.common import paths, seed +from apps.common.keychain import Keychain, auto_keychain + +from . import ICON, PRIMARY_COLOR +from .tonsdk.contract.wallet import Wallets, WalletVersionEnum + +if TYPE_CHECKING: + from trezor.wire import Context + + +@auto_keychain(__name__) +async def get_address( + ctx: Context, msg: TonGetAddress, keychain: Keychain +) -> TonAddress: + await paths.validate_path(ctx, keychain, msg.address_n) + + node = keychain.derive(msg.address_n) + public_key = seed.remove_ed25519_prefix(node.public_key()) + workchain = ( + -1 if msg.workchain == TonWorkChain.MASTERCHAIN else TonWorkChain.BASECHAIN + ) + + if msg.wallet_version == TonWalletVersion.V4R2: + wallet_version = WalletVersionEnum.v4r2 + else: + raise wire.DataError("Invalid wallet version.") + + wallet = Wallets.ALL[wallet_version]( + public_key=public_key, wallet_id=msg.wallet_id, wc=workchain + ) + address = wallet.address.to_string( + is_user_friendly=True, + is_url_safe=True, + is_bounceable=msg.is_bounceable, + is_test_only=msg.is_testnet_only, + ) + + if msg.show_display: + path = paths.address_n_to_str(msg.address_n) + ctx.primary_color, ctx.icon_path = lv.color_hex(PRIMARY_COLOR), ICON + await show_address( + ctx, + address=address, + address_n=path, + network="TON", + ) + + return TonAddress(public_key=public_key, address=address) diff --git a/core/src/apps/ton/layout.py b/core/src/apps/ton/layout.py new file mode 100644 index 0000000000..dece738bcd --- /dev/null +++ b/core/src/apps/ton/layout.py @@ -0,0 +1,87 @@ +from typing import TYPE_CHECKING + +from trezor import ui +from trezor.enums import ButtonRequestType +from trezor.lvglui.i18n import gettext as _, keys as i18n_keys +from trezor.strings import format_amount +from trezor.ui.layouts import confirm_address, should_show_details + +from . import tokens + +if TYPE_CHECKING: + from typing import Awaitable + + from trezor.wire import Context + + +def require_confirm_fee( + ctx: Context, + from_address: str | None = None, + to_address: str | None = None, + value: int = 0, + gas_price: int = 0, + gas_limit: int = 0, + token: tokens.TokenInfo | None = None, + raw_data: bytes | None = None, + is_raw_data: bool = False, +) -> Awaitable[None]: + from trezor.ui.layouts.lvgl.altcoin import confirm_total_ton + + fee_limit = gas_price * gas_limit + + return confirm_total_ton( + ctx, + format_ton_amount(value, token), + None, + format_ton_amount(fee_limit, None), + from_address, + to_address, + format_ton_amount(value + fee_limit, None) if token is None else None, + raw_data=raw_data, + is_raw_data=is_raw_data, + ) + + +def require_show_overview( + ctx: Context, + to_addr: str, + value: int, + token: tokens.TokenInfo | None = None, +) -> Awaitable[bool]: + + return should_show_details( + ctx, + title=_(i18n_keys.TITLE__STR_TRANSACTION).format("Ton"), + address=to_addr, + amount=format_ton_amount(value, token), + br_code=ButtonRequestType.SignTx, + ) + + +def format_ton_amount(value: int, token: tokens.TokenInfo | None) -> str: + if token: + suffix = token.symbol + decimals = token.decimals + else: + suffix = "TON" + decimals = 9 + + # Don't want to display wei values for tokens with small decimal numbers + # if decimals > 9 and value < 10 ** (decimals - 9): + # suffix = "Drip " + suffix + # decimals = 0 + + return f"{format_amount(value, decimals)} {suffix}" + + +def require_confirm_unknown_token(ctx: Context, address: str) -> Awaitable[None]: + return confirm_address( + ctx, + _(i18n_keys.TITLE__UNKNOWN_TOKEN), + address, + description=_(i18n_keys.LIST_KEY__CONTRACT__COLON), + br_type="unknown_token", + icon="A:/res/shriek.png", + icon_color=ui.ORANGE, + br_code=ButtonRequestType.SignTx, + ) diff --git a/core/src/apps/ton/sign_message.py b/core/src/apps/ton/sign_message.py new file mode 100644 index 0000000000..54124dc3ba --- /dev/null +++ b/core/src/apps/ton/sign_message.py @@ -0,0 +1,155 @@ +from typing import TYPE_CHECKING + +from trezor import wire +from trezor.crypto.curve import ed25519 +from trezor.enums import TonWalletVersion, TonWorkChain +from trezor.lvglui.scrs import lv +from trezor.messages import TonSignedMessage, TonSignMessage + +from apps.common import paths, seed +from apps.common.keychain import Keychain, auto_keychain +from apps.ton.tonsdk.contract.token.ft import JettonWallet +from apps.ton.tonsdk.contract.wallet import Wallets, WalletVersionEnum +from apps.ton.tonsdk.utils._address import Address + +from . import ICON, PRIMARY_COLOR, tokens +from .layout import require_confirm_fee, require_show_overview + +if TYPE_CHECKING: + from trezor.wire import Context + + +@auto_keychain(__name__) +async def sign_message( + ctx: Context, msg: TonSignMessage, keychain: Keychain +) -> TonSignedMessage: + await paths.validate_path(ctx, keychain, msg.address_n) + + node = keychain.derive(msg.address_n) + public_key = seed.remove_ed25519_prefix(node.public_key()) + workchain = ( + -1 if msg.workchain == TonWorkChain.MASTERCHAIN else TonWorkChain.BASECHAIN + ) + + if msg.wallet_version == TonWalletVersion.V4R2: + wallet_version = WalletVersionEnum.v4r2 + else: + raise wire.DataError("Invalid wallet version.") + + jetton_amount = check_jetton_transfer(msg) + + wallet = Wallets.ALL[wallet_version]( + public_key=public_key, wallet_id=msg.wallet_id, wc=workchain + ) + address = wallet.address.to_string( + is_user_friendly=True, + is_url_safe=True, + is_bounceable=msg.is_bounceable, + is_test_only=msg.is_testnet_only, + ) + + # display + ctx.primary_color, ctx.icon_path = lv.color_hex(PRIMARY_COLOR), ICON + from trezor.ui.layouts import confirm_final, confirm_unknown_token_transfer + + token = None + recipient = Address(msg.destination).to_string(True, True) + + if jetton_amount: + token = tokens.token_by_address("TON_TOKEN", msg.jetton_master_address) + + if token is tokens.UNKNOWN_TOKEN: + # unknown token, confirm contract address + if msg.jetton_master_address is None: + raise ValueError("Address cannot be None") + await confirm_unknown_token_transfer(ctx, msg.jetton_master_address) + + amount = jetton_amount if jetton_amount else msg.ton_amount + if amount is None: + raise ValueError("Amount cannot be None") + + show_details = await require_show_overview( + ctx, + recipient, + amount, + token, + ) + + if show_details: + comment = msg.comment.encode("utf-8") if msg.comment else None + await require_confirm_fee( + ctx, + from_address=address, + to_address=recipient, + value=amount, + token=token, + raw_data=comment if comment else None, + is_raw_data=msg.is_raw_data, + ) + + if msg.ext_destination: + for ext_addr, ext_payload, ext_amount in zip( + msg.ext_destination, msg.ext_payload, msg.ext_ton_amount + ): + show_details = False + show_details = await require_show_overview( + ctx, + ext_addr, + ext_amount, + None, + ) + if show_details: + ext_comment = ext_payload.encode("utf-8") if ext_payload else None + await require_confirm_fee( + ctx, + from_address=address, + to_address=ext_addr, + value=ext_amount, + token=None, + raw_data=ext_comment if ext_comment else None, + is_raw_data=msg.is_raw_data, + ) + + await confirm_final(ctx, "TON") + + if jetton_amount: + body = JettonWallet().create_transfer_body( + Address(msg.destination), + jetton_amount, + msg.fwd_fee, + msg.comment, + wallet.address, + ) + payload = body + else: + payload = msg.comment + + digest, boc = wallet.create_transaction_digest( + to_addr=msg.jetton_wallet_address if jetton_amount else msg.destination, + amount=msg.ton_amount, + seqno=msg.seqno, + expire_at=msg.expire_at, + payload=payload, + is_raw_data=msg.is_raw_data, + send_mode=msg.mode, + ext_to=None if jetton_amount else msg.ext_destination, + ext_amount=None if jetton_amount else msg.ext_ton_amount, + ext_payload=None if jetton_amount else msg.ext_payload, + ) + + signature = ed25519.sign(node.private_key(), digest) + + return TonSignedMessage(signature=signature, signning_message=boc) + + +def check_jetton_transfer(msg: TonSignMessage) -> int: + if msg.jetton_amount is None and msg.jetton_amount_bytes is None: + return 0 + # fmt: off + elif msg.jetton_amount_bytes is not None and msg.jetton_master_address is not None: + return int.from_bytes(msg.jetton_amount_bytes, "big") + # fmt: on + elif msg.jetton_amount is not None and msg.jetton_master_address is not None: + return msg.jetton_amount + else: + raise wire.DataError("Invalid jetton transfer message.") diff --git a/core/src/apps/ton/sign_proof.py b/core/src/apps/ton/sign_proof.py new file mode 100644 index 0000000000..8168520c81 --- /dev/null +++ b/core/src/apps/ton/sign_proof.py @@ -0,0 +1,80 @@ +from typing import TYPE_CHECKING + +from trezor import wire +from trezor.crypto.curve import ed25519 +from trezor.crypto.hashlib import sha256 +from trezor.enums import TonWalletVersion, TonWorkChain +from trezor.lvglui.scrs import lv +from trezor.messages import TonSignedProof, TonSignProof + +from apps.common import paths, seed +from apps.common.keychain import Keychain, auto_keychain + +from . import ICON, PRIMARY_COLOR +from .tonsdk.contract.wallet import Wallets, WalletVersionEnum + +if TYPE_CHECKING: + from trezor.wire import Context + + +@auto_keychain(__name__) +async def sign_proof( + ctx: Context, msg: TonSignProof, keychain: Keychain +) -> TonSignedProof: + await paths.validate_path(ctx, keychain, msg.address_n) + + node = keychain.derive(msg.address_n) + public_key = seed.remove_ed25519_prefix(node.public_key()) + workchain = ( + -1 if msg.workchain == TonWorkChain.MASTERCHAIN else TonWorkChain.BASECHAIN + ) + + if msg.wallet_version == TonWalletVersion.V4R2: + wallet_version = WalletVersionEnum.v4r2 + else: + raise wire.DataError("Invalid wallet version.") + + wallet = Wallets.ALL[wallet_version]( + public_key=public_key, wallet_id=msg.wallet_id, wc=workchain + ) + address = wallet.address.to_string( + is_user_friendly=True, + is_url_safe=True, + is_bounceable=msg.is_bounceable, + is_test_only=msg.is_testnet_only, + ) + + # display + ctx.primary_color, ctx.icon_path = lv.color_hex(PRIMARY_COLOR), ICON + from trezor.ui.layouts import confirm_ton_signverify + + if msg.appdomain is None: + raise ValueError("Domain cannot be None") + await confirm_ton_signverify( + ctx, + "TON", + msg.comment.decode("UTF-8") if msg.comment else "", + address, + msg.appdomain.decode("UTF-8"), + verify=False, + ) + + ton_proof_prefix = "ton-proof-item-v2/" + ton_connect_prefix = "ton-connect" + + message = ( + ton_proof_prefix.encode("utf-8") + + workchain.to_bytes(4, "big") + + wallet.address.get_hash_part() + + len(msg.appdomain).to_bytes(4, "little") + + msg.appdomain + + msg.expire_at.to_bytes(8, "little") + + msg.comment + ) + message_hash = sha256(message).digest() + + full_message = b"\xff\xff" + ton_connect_prefix.encode("utf-8") + message_hash + + signature = ed25519.sign(node.private_key(), sha256(full_message).digest()) + + return TonSignedProof(signature=signature) diff --git a/core/src/apps/ton/tokens.py b/core/src/apps/ton/tokens.py new file mode 100644 index 0000000000..f11426a504 --- /dev/null +++ b/core/src/apps/ton/tokens.py @@ -0,0 +1,18 @@ +class TokenInfo: + def __init__(self, symbol: str, decimals: int) -> None: + self.symbol = symbol + self.decimals = decimals + + +UNKNOWN_TOKEN = TokenInfo("UNKN", 0) + + +def token_by_address(token_type, address) -> TokenInfo: + if token_type == "TON_TOKEN": + if address == "EQBynBO23ywHy_CgarY9NK9FTz0yDsG82PtcbSTQgGoXwiuA": + return TokenInfo("jUSDT", 6) + if address == "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs": + return TokenInfo("USDT", 6) + if address == "EQAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM__NOT": + return TokenInfo("NOT", 9) + return UNKNOWN_TOKEN diff --git a/core/src/apps/ton/tonsdk/__init__.py b/core/src/apps/ton/tonsdk/__init__.py new file mode 100755 index 0000000000..e69de29bb2 diff --git a/core/src/apps/ton/tonsdk/boc/__init__.py b/core/src/apps/ton/tonsdk/boc/__init__.py new file mode 100755 index 0000000000..628157d984 --- /dev/null +++ b/core/src/apps/ton/tonsdk/boc/__init__.py @@ -0,0 +1,13 @@ +from ._builder import Builder, begin_cell +from ._cell import Cell, deserialize_cell_data, parse_boc_header +from ._dict_builder import DictBuilder, begin_dict + +__all__ = [ + "Cell", + "Builder", + "begin_cell", + "DictBuilder", + "begin_dict", + "deserialize_cell_data", + "parse_boc_header", +] diff --git a/core/src/apps/ton/tonsdk/boc/_bit_string.py b/core/src/apps/ton/tonsdk/boc/_bit_string.py new file mode 100755 index 0000000000..dce6fe1746 --- /dev/null +++ b/core/src/apps/ton/tonsdk/boc/_bit_string.py @@ -0,0 +1,188 @@ +import math + +from ..utils._address import Address + + +class BitString: + def __init__(self, length: int): + self.array = bytearray(math.ceil(length / 8)) + self.cursor = 0 + self.length = length + + def __repr__(self): + return str(self.get_top_upped_array()) + + def __iter__(self): + for i in range(self.cursor): + yield self.get(i) + + def __getitem__(self, key): + if isinstance(key, slice): + start = key.start if key.start else 0 + stop = key.stop if key.stop else len(self) + step = key.step if key.step else 1 + + return [self[ii] for ii in range(start, stop, step)] + elif isinstance(key, int): + if key < 0: + key += len(self) + if key < 0 or key >= len(self): + raise IndexError(f"The index {key} is out of range.") + return self.get(key) + else: + raise TypeError("Invalid argument type.") + + def __len__(self): + return self.length + + def get(self, n: int): + """Just returns n bits from cursor. Does not move the cursor.""" + return int((self.array[(n // 8) | 0] & (1 << (7 - (n % 8)))) > 0) + + def off(self, n): + """Sets next from cursor n bits to 0. Does not move cursor.""" + self.check_range(n) + self.array[(n // 8) | 0] &= ~(1 << (7 - (n % 8))) + + def on(self, n): + """Sets next from cursor n bits to 1. Does not move cursor.""" + self.check_range(n) + self.array[(n // 8) | 0] |= 1 << (7 - (n % 8)) + + def check_range(self, n: int) -> None: + """Throws an exception if the cursor + n is out of range.""" + if n > self.length: + raise Exception("BitString overflow") + + def set_top_upped_array(self, array: bytearray, fullfilled_bytes=True): + self.length = len(array) * 8 + self.array = array + self.cursor = self.length + + if fullfilled_bytes or not self.length: + return + + else: + found_end_bit = False + for _ in range(7): + self.cursor -= 1 + + if self.get(self.cursor): + found_end_bit = True + self.off(self.cursor) + break + + if not found_end_bit: + raise Exception(f"Incorrect TopUppedArray {array}, {fullfilled_bytes}") + + def get_top_upped_array(self) -> bytearray: + ret = BitString(self.length) + ret.array = self.array[:] + ret.cursor = self.cursor + + tu = math.ceil(ret.cursor / 8) * 8 - ret.cursor + if tu > 0: + tu -= 1 + ret.write_bit(1) + while tu > 0: + tu -= 1 + ret.write_bit(0) + ret.array = ret.array[: math.ceil(ret.cursor / 8)] + return ret.array + + def get_free_bits(self) -> int: + """Returns the number of not used bits in the BitString.""" + return self.length - self.cursor + + def get_used_bits(self): + return self.cursor + + def write_bit_array(self, ba: bytearray | bytes): + """Writes a bytearray as a bit array one bit by one.""" + for b in ba.decode("utf-8"): + self.write_bit(b) + + def write_bit(self, b): + b = int(b) + if b == 1: + self.on(self.cursor) + elif b == 0: + self.off(self.cursor) + else: + raise Exception("BitString can only write 1 or 0") + + self.cursor += 1 + + def write_uint(self, number: int, bit_length: int): + if bit_length == 0 or number >= 2**bit_length: + if number == 0: + return + + raise Exception( + f"bitLength is too small for number, got number={number},bitLength={bit_length}" + ) + + for i in range(bit_length, 0, -1): + k = 2 ** (i - 1) + if number // k == 1: + self.write_bit(1) + number -= k + else: + self.write_bit(0) + + def write_uint8(self, ui8: int): + """Just as write_uint(n, 8), but only write_uint8(n) (?).""" + self.write_uint(ui8, 8) + + def write_int(self, number: int, bit_length: int): + if bit_length == 1: + if number == -1: + self.write_bit(1) + return + + if number == 0: + self.write_bit(0) + return + + raise Exception("Bitlength is too small for number") + else: + if number < 0: + self.write_bit(1) + s = 2 ** (bit_length - 1) + self.write_uint(s + number, bit_length - 1) + else: + self.write_bit(0) + self.write_uint(number, bit_length - 1) + + def write_string(self, value: str): + self.write_bytes(value.encode("utf-8")) + + def write_bytes(self, ui8_array: bytes): + for ui8 in ui8_array: + self.write_uint8(ui8) + + def write_bit_string(self, another_bit_string: "BitString"): + for bit in another_bit_string: + self.write_bit(bit) + + def write_address(self, address: Address | None): + """Writes an address, maybe zero-address (None) to the BitString.""" + if address is None: + self.write_uint(0, 2) + else: + self.write_uint(2, 2) + self.write_uint(0, 1) # anycast + self.write_int(address.wc, 8) + self.write_bytes(address.hash_part) + + def write_grams(self, amount: int): + if amount == 0: + self.write_uint(0, 4) + else: + amount = int(amount) + l = math.ceil(len(hex(amount)[2:]) / 2) # ? [2:] removes 0x + self.write_uint(l, 4) + self.write_uint(amount, l * 8) + + def write_coins(self, amount): + self.write_grams(amount) diff --git a/core/src/apps/ton/tonsdk/boc/_builder.py b/core/src/apps/ton/tonsdk/boc/_builder.py new file mode 100755 index 0000000000..7219d2e140 --- /dev/null +++ b/core/src/apps/ton/tonsdk/boc/_builder.py @@ -0,0 +1,83 @@ +from ._bit_string import BitString +from ._cell import Cell + + +class Builder: + def __init__(self): + self.bits = BitString(1023) + self.refs = [] + self.is_exotic = False + + def __repr__(self): + return f"" + + def store_cell(self, src: Cell): + self.bits.write_bit_string(src.bits) + self.refs += src.refs + return self + + def store_ref(self, src: Cell): + self.refs.append(src) + return self + + def store_maybe_ref(self, src): + if src: + self.bits.write_bit(1) + self.store_ref(src) + else: + self.bits.write_bit(0) + + return self + + def store_bit(self, value): + self.bits.write_bit(value) + return self + + def store_bit_array(self, value): + self.bits.write_bit_array(value) + return self + + def store_uint(self, value, bit_length): + self.bits.write_uint(value, bit_length) + return self + + def store_uint8(self, value): + self.bits.write_uint8(value) + return self + + def store_int(self, value, bit_length): + self.bits.write_int(value, bit_length) + return self + + def store_string(self, value): + self.bits.write_string(value) + return self + + def store_bytes(self, value): + self.bits.write_bytes(value) + return self + + def store_bit_string(self, value): + self.bits.write_bit_string(value) + return self + + def store_address(self, value): + self.bits.write_address(value) + return self + + def store_grams(self, value): + self.bits.write_grams(value) + return self + + def store_coins(self, value): + self.bits.write_coins(value) + return self + + def end_cell(self): + cell = Cell() + cell.write_cell(self) + return cell + + +def begin_cell(): + return Builder() diff --git a/core/src/apps/ton/tonsdk/boc/_cell.py b/core/src/apps/ton/tonsdk/boc/_cell.py new file mode 100755 index 0000000000..65bbc7b0a8 --- /dev/null +++ b/core/src/apps/ton/tonsdk/boc/_cell.py @@ -0,0 +1,345 @@ +import math +from binascii import unhexlify + +from trezor.crypto.hashlib import sha256 + +from ..utils import ( + compare_bytes, + concat_bytes, + crc32c, + int_to_hex, + read_n_bytes_uint_from_array, + tree_walk, +) +from ._bit_string import BitString + + +class Cell: + REACH_BOC_MAGIC_PREFIX = unhexlify("B5EE9C72") + LEAN_BOC_MAGIC_PREFIX = unhexlify("68ff65f3") + LEAN_BOC_MAGIC_PREFIX_CRC = unhexlify("acc3a728") + + def __init__(self): + self.bits = BitString(1023) + self.refs = [] + self.is_exotic = False + + def __repr__(self): + return f"" + + def __bool__(self): + return bool(self.bits.cursor) or bool(self.refs) + + def bytes_hash(self): + return sha256(self.bytes_repr()).digest() + + def bytes_repr(self): + repr_array = [] + repr_array.append(self.get_data_with_descriptors()) + for r in self.refs: + q = r.get_max_depth_as_array() + repr_array.append(q) + for r in self.refs: + q = r.bytes_hash() + repr_array.append(q) + x = bytes() + for r in repr_array: + x = concat_bytes(x, r) + return x + + def write_cell(self, another_cell): + self.bits.write_bit_string(another_cell.bits) + self.refs += another_cell.refs + + def get_data_with_descriptors(self): + d1 = self.get_refs_descriptor() + d2 = self.get_bits_descriptor() + tuBits = self.bits.get_top_upped_array() + + return concat_bytes(concat_bytes(d1, d2), tuBits) + + def get_bits_descriptor(self): + d2 = bytearray([0]) + d2[0] = math.ceil(self.bits.cursor / 8) + math.floor(self.bits.cursor / 8) + return d2 + + def get_refs_descriptor(self): + d1 = bytearray([0]) + d1[0] = len(self.refs) + self.is_exotic * 8 + self.get_max_level() * 32 + return d1 + + def get_max_level(self): + if self.is_exotic: + raise NotImplementedError( + "Calculating max level for exotic cells is not implemented" + ) + max_level = 0 + for r in self.refs: + r_max_level = r.get_max_level() + if r_max_level > max_level: + max_level = r_max_level + return max_level + + def get_max_depth_as_array(self): + max_depth = self.get_max_depth() + return bytearray([max_depth // 256, max_depth % 256]) + + def get_max_depth(self): + max_depth = 0 + if len(self.refs) > 0: + for r in self.refs: + r_max_depth = r.get_max_depth() + if r_max_depth > max_depth: + max_depth = r_max_depth + max_depth += 1 + return max_depth + + def tree_walk(self): + return tree_walk(self, [], {}) + + def is_explicitly_stored_hashes(self): + return 0 + + def serialize_for_boc(self, cells_index, ref_size): + repr_arr = [] + + repr_arr.append(self.get_data_with_descriptors()) + if self.is_explicitly_stored_hashes(): + raise NotImplementedError("Cell hashes explicit storing is not implemented") + + for ref in self.refs: + ref_hash = ref.bytes_hash() + ref_index_int = cells_index[ref_hash] + ref_index_hex = int_to_hex(ref_index_int) + if len(ref_index_hex) % 2: + ref_index_hex = "0" + ref_index_hex + reference = unhexlify(ref_index_hex) + repr_arr.append(reference) + + x = b"" + for data in repr_arr: + x = concat_bytes(x, data) + + return x + + def to_boc(self, has_idx=True, hash_crc32=True, has_cache_bits=False, flags=0): + root_cell = Cell() + root_cell.write_cell(self) + + all_cells = root_cell.tree_walk() + topological_order = all_cells[0] + cells_index = all_cells[1] + + cells_num = len(topological_order) + # Minimal number of bits to represent reference (unused?) + s = len(f"{cells_num:b}") + s_bytes = max(math.ceil(s / 8), 1) + full_size = 0 + cell_sizes = {} + for (_hash, subcell) in topological_order: + cell_sizes[_hash] = subcell.boc_serialization_size(cells_index, s_bytes) + full_size += cell_sizes[_hash] + + offset_bits = len(f"{full_size:b}") + offset_bytes = max(math.ceil(offset_bits / 8), 1) + + serialization = BitString((1023 + 32 * 4 + 32 * 3) * len(topological_order)) + serialization.write_bytes(Cell.REACH_BOC_MAGIC_PREFIX) + settings = bytes( + "".join(["1" if i else "0" for i in [has_idx, hash_crc32, has_cache_bits]]), + "utf-8", + ) + serialization.write_bit_array(settings) + serialization.write_uint(flags, 2) + serialization.write_uint(s_bytes, 3) + serialization.write_uint8(offset_bytes) + serialization.write_uint(cells_num, s_bytes * 8) + serialization.write_uint(1, s_bytes * 8) # One root for now + serialization.write_uint(0, s_bytes * 8) # Complete BOCs only + serialization.write_uint(full_size, offset_bytes * 8) + serialization.write_uint(0, s_bytes * 8) # Root shoulh have index 0 + + if has_idx: + for (_hash, subcell) in topological_order: + serialization.write_uint(cell_sizes[_hash], offset_bytes * 8) + + for cell_info in topological_order: + ref_cell_ser = cell_info[1].serialize_for_boc(cells_index, s_bytes) + serialization.write_bytes(ref_cell_ser) + + ser_arr = serialization.get_top_upped_array() + if hash_crc32: + ser_arr += crc32c(ser_arr) + + return ser_arr + + def boc_serialization_size(self, cells_index, ref_size): + return len(self.serialize_for_boc(cells_index, ref_size)) + + @staticmethod + def one_from_boc(serialized_boc): + cells = deserialize_boc(serialized_boc) + + if len(cells) != 1: + raise Exception("Expected 1 root cell") + + return cells[0] + + +def deserialize_cell_data(cell_data, reference_index_size): + if len(cell_data) < 2: + raise Exception("Not enough bytes to encode cell descriptors") + + d1, d2 = cell_data[0], cell_data[1] + cell_data = cell_data[2:] + # level = math.floor(d1 / 32) + is_exotic = d1 & 8 + ref_num = d1 % 8 + data_bytes_size = math.ceil(d2 / 2) + fullfilled_bytes = not d2 % 2 + + cell = Cell() + cell.is_exotic = is_exotic + + if cell.is_exotic: + raise NotImplementedError("Exotic cells are not implemented") + + if len(cell_data) < data_bytes_size + reference_index_size * ref_num: + raise Exception("Not enough bytes to encode cell data") + + cell.bits.set_top_upped_array( + bytearray(cell_data[:data_bytes_size]), fullfilled_bytes + ) + cell_data = cell_data[data_bytes_size:] + for _ in range(ref_num): + cell.refs.append(read_n_bytes_uint_from_array(reference_index_size, cell_data)) + cell_data = cell_data[reference_index_size:] + + return {"cell": cell, "residue": cell_data} + + +def parse_boc_header(serialized_boc): + if len(serialized_boc) < 4 + 1: + raise Exception("Not enough bytes for magic prefix") + + input_data = serialized_boc + prefix = serialized_boc[:4] + serialized_boc = serialized_boc[4:] + if compare_bytes(prefix, Cell.REACH_BOC_MAGIC_PREFIX): + flags_byte = serialized_boc[0] + has_idx = flags_byte & 128 + hash_crc32 = flags_byte & 64 + has_cache_bits = flags_byte & 32 + flags = (flags_byte & 16) * 2 + (flags_byte & 8) + size_bytes = flags_byte % 8 + + elif compare_bytes(prefix, Cell.LEAN_BOC_MAGIC_PREFIX): + has_idx = 1 + hash_crc32 = 0 + has_cache_bits = 0 + flags = 0 + size_bytes = serialized_boc[0] + + elif compare_bytes(prefix, Cell.LEAN_BOC_MAGIC_PREFIX_CRC): + has_idx = 1 + hash_crc32 = 1 + has_cache_bits = 0 + flags = 0 + size_bytes = serialized_boc[0] + else: + raise Exception("Unknown BOC serialization format") + + serialized_boc = serialized_boc[1:] + + if len(serialized_boc) < 1 + 5 * size_bytes: + raise Exception("Not enough bytes for encoding cells counters") + + offset_bytes = serialized_boc[0] + serialized_boc = serialized_boc[1:] + cells_num = read_n_bytes_uint_from_array(size_bytes, serialized_boc) + serialized_boc = serialized_boc[size_bytes:] + roots_num = read_n_bytes_uint_from_array(size_bytes, serialized_boc) + serialized_boc = serialized_boc[size_bytes:] + absent_num = read_n_bytes_uint_from_array(size_bytes, serialized_boc) + serialized_boc = serialized_boc[size_bytes:] + tot_cells_size = read_n_bytes_uint_from_array(offset_bytes, serialized_boc) + serialized_boc = serialized_boc[offset_bytes:] + + if len(serialized_boc) < roots_num * size_bytes: + raise Exception("Not enough bytes for encoding root cells hashes") + + root_list = [] + for _ in range(roots_num): + root_list.append(read_n_bytes_uint_from_array(size_bytes, serialized_boc)) + serialized_boc = serialized_boc[size_bytes:] + + index = False + if has_idx: + index = [] + if len(serialized_boc) < offset_bytes * cells_num: + raise Exception("Not enough bytes for index encoding") + for _ in range(cells_num): + index.append(read_n_bytes_uint_from_array(offset_bytes, serialized_boc)) + serialized_boc = serialized_boc[offset_bytes:] + + if len(serialized_boc) < tot_cells_size: + raise Exception("Not enough bytes for cells data") + cells_data = serialized_boc[:tot_cells_size] + serialized_boc = serialized_boc[tot_cells_size:] + + if hash_crc32: + if len(serialized_boc) < 4: + raise Exception("Not enough bytes for crc32c hashsum") + + length = len(input_data) + if not compare_bytes(crc32c(input_data[: length - 4]), serialized_boc[:4]): + raise Exception("Crc32c hashsum mismatch") + + serialized_boc = serialized_boc[4:] + + if len(serialized_boc): + raise Exception("Too much bytes in BoC serialization") + + return { + "has_idx": has_idx, + "hash_crc32": hash_crc32, + "has_cache_bits": has_cache_bits, + "flags": flags, + "size_bytes": size_bytes, + "off_bytes": offset_bytes, + "cells_num": cells_num, + "roots_num": roots_num, + "absent_num": absent_num, + "tot_cells_size": tot_cells_size, + "root_list": root_list, + "index": index, + "cells_data": cells_data, + } + + +def deserialize_boc(serialized_boc): + if type(serialized_boc) == str: + serialized_boc = unhexlify(serialized_boc) + + header = parse_boc_header(serialized_boc) + cells_data = header["cells_data"] + cells_array = [] + + for ci in range(header["cells_num"]): + dd = deserialize_cell_data(cells_data, header["size_bytes"]) + cells_data = dd["residue"] + cells_array.append(dd["cell"]) + + for ci in reversed(range(header["cells_num"])): + c = cells_array[ci] + for ri in range(len(c.refs)): + r = c.refs[ri] + if r < ci: + raise Exception("Topological order is broken") + c.refs[ri] = cells_array[r] + + root_cells = [] + for ri in header["root_list"]: + root_cells.append(cells_array[ri]) + + return root_cells diff --git a/core/src/apps/ton/tonsdk/boc/_dict_builder.py b/core/src/apps/ton/tonsdk/boc/_dict_builder.py new file mode 100755 index 0000000000..bde02959ec --- /dev/null +++ b/core/src/apps/ton/tonsdk/boc/_dict_builder.py @@ -0,0 +1,47 @@ +from ._cell import Cell +from .dict import serialize_dict + + +class DictBuilder: + def __init__(self, key_size: int): + self.key_size = key_size + self.items = {} + self.ended = False + + def store_cell(self, index, value: Cell): + assert self.ended is False, "Already ended" + if type(index) == bytes: + index = int(index.hex(), 16) + + assert type(index) == int, "Invalid index type" + assert not (index in self.items), f"Item {index} already exist" + self.items[index] = value + return self + + def store_ref(self, index, value: Cell): + assert self.ended is False, "Already ended" + + cell = Cell() + cell.refs.append(value) + self.store_cell(index, cell) + return self + + def end_dict(self) -> Cell: + assert self.ended is False, "Already ended" + self.ended = True + if not self.items: + return Cell() # ? + + def default_serializer(src, dest): + dest.write_cell(src) + + return serialize_dict(self.items, self.key_size, default_serializer) + + def end_cell(self) -> Cell: + assert self.ended is False, "Already ended" + assert self.items, "Dict is empty" + return self.end_dict() + + +def begin_dict(key_size): + return DictBuilder(key_size) diff --git a/core/src/apps/ton/tonsdk/boc/dict/__init__.py b/core/src/apps/ton/tonsdk/boc/dict/__init__.py new file mode 100755 index 0000000000..ce7da1b8a0 --- /dev/null +++ b/core/src/apps/ton/tonsdk/boc/dict/__init__.py @@ -0,0 +1,2 @@ +from .find_common_prefix import find_common_prefix # noqa: F401 +from .serialize_dict import serialize_dict # noqa: F401 diff --git a/core/src/apps/ton/tonsdk/boc/dict/find_common_prefix.py b/core/src/apps/ton/tonsdk/boc/dict/find_common_prefix.py new file mode 100755 index 0000000000..1ada0633b9 --- /dev/null +++ b/core/src/apps/ton/tonsdk/boc/dict/find_common_prefix.py @@ -0,0 +1,17 @@ +def find_common_prefix(src): + # Corner cases + if len(src) == 0: + return "" + if len(src) == 1: + return src[0] + + # Searching for prefix + _sorted = sorted(src) + size = 0 + for i, e in enumerate(_sorted[0]): + if e == _sorted[-1][i]: + size += 1 + else: + break + + return _sorted[0][:size] diff --git a/core/src/apps/ton/tonsdk/boc/dict/serialize_dict.py b/core/src/apps/ton/tonsdk/boc/dict/serialize_dict.py new file mode 100755 index 0000000000..883ead95a0 --- /dev/null +++ b/core/src/apps/ton/tonsdk/boc/dict/serialize_dict.py @@ -0,0 +1,182 @@ +from math import ceil, log2 + +from .._cell import Cell +from .find_common_prefix import find_common_prefix + + +def pad(src: str, size: int) -> str: + while len(src) < size: + src = "0" + src + + return src + + +def remove_prefix_map(src, length): + if length == 0: + return src + else: + res = {} + for k in src: + res[k[length:]] = src[k] + + return res + + +def fork_map(src): + assert len(src) > 0, "Internal inconsistency" + left = {} + right = {} + for k in src: + if k.find("0") == 0: + left[k[1:]] = src[k] + else: + right[k[1:]] = src[k] + + assert len(left) > 0, "Internal inconsistency. Left empty." + assert len(right) > 0, "Internal inconsistency. Left empty." + return left, right + + +def build_node(src): + assert len(src) > 0, "Internal inconsistency" + if len(src) == 1: + return {"type": "leaf", "value": list(src.values())[0]} + + left, right = fork_map(src) + return {"type": "fork", "left": build_edge(left), "right": build_edge(right)} + + +def build_edge(src): + assert len(src) > 0, "Internal inconsistency" + label = find_common_prefix(list(src.keys())) + return {"label": label, "node": build_node(remove_prefix_map(src, len(label)))} + + +def build_tree(src, key_size): + # Convert map keys + tree = {} + for key in src: + padded = pad(bin(key)[2:], key_size) + tree[padded] = src[key] + + # Calculate root label + return build_edge(tree) + + +# Serialization +def write_label_short(src, to): + # Header + to.write_bit(0) + + # Unary length + for e in src: + to.write_bit(1) + to.write_bit(0) + + # Value + for e in src: + to.write_bit(e == "1") + + return to + + +def label_short_length(src): + return 1 + len(src) + 1 + len(src) + + +def write_label_long(src, key_length, to): + # Header + to.write_bit(1) + to.write_bit(0) + + # Length + length = ceil(log2(key_length + 1)) + to.write_uint(len(src), length) + + # Value + for e in src: + to.write_bit(e == "1") + + return to + + +def label_long_length(src, key_length): + return 1 + 1 + ceil(log2(key_length + 1)) + len(src) + + +def write_label_same(value: bool, length, key_length, to): + to.write_bit(1) + to.write_bit(1) + + to.write_bit(value) + + len_len = ceil(log2(key_length + 1)) + to.write_uint(length, len_len) + + +def label_same_length(key_size): + return 1 + 1 + 1 + ceil(log2(key_size + 1)) + + +def is_same(src): + if len(src) == 0 or len(src) == 1: + return True + + for e in src[1:]: + if e != src[0]: + return False + + return True + + +def detect_label_type(src, key_size): + kind = "short" + kind_length = label_short_length(src) + + long_length = label_long_length(src, key_size) + if long_length < kind_length: + kind_length = long_length + kind = "long" + + if is_same(src): + same_length = label_same_length(key_size) + if same_length < kind_length: + kind_length = same_length + kind = "same" + + return kind + + +def write_label(src, key_size, to): + type = detect_label_type(src, key_size) + if type == "short": + write_label_short(src, to) + elif type == "long": + write_label_long(src, key_size, to) + elif type == "same": + write_label_same(src[0] == "1", len(src), key_size, to) + + +def write_node(src, key_size, serializer, to): + if src["type"] == "leaf": + serializer(src["value"], to) + + if src["type"] == "fork": + left_cell = Cell() + right_cell = Cell() + write_edge(src["left"], key_size - 1, serializer, left_cell) + write_edge(src["right"], key_size - 1, serializer, right_cell) + to.refs.append(left_cell) + to.refs.append(right_cell) + + +def write_edge(src, key_size, serializer, to): + write_label(src["label"], key_size, to.bits) + write_node(src["node"], key_size - len(src["label"]), serializer, to) + + +def serialize_dict(src, key_size, serializer): + tree = build_tree(src, key_size) + dest = Cell() + write_edge(tree, key_size, serializer, dest) + return dest diff --git a/core/src/apps/ton/tonsdk/contract/__init__.py b/core/src/apps/ton/tonsdk/contract/__init__.py new file mode 100755 index 0000000000..b102c2608d --- /dev/null +++ b/core/src/apps/ton/tonsdk/contract/__init__.py @@ -0,0 +1,171 @@ +from binascii import hexlify + +from ..boc import Cell +from ..utils import Address + + +class Contract: + def __init__(self, **kwargs): + self.options = kwargs + self._address = Address(kwargs["address"]) if "address" in kwargs else None + if "wc" not in kwargs: + kwargs["wc"] = self._address.wc if self._address is not None else 0 + + @property + def address(self): + if self._address is None: + self._address = self.create_state_init()["address"] + + return self._address + + def create_state_init(self): + code_cell = self.create_code_cell() + data_cell = self.create_data_cell() + state_init = self.__create_state_init(code_cell, data_cell) + state_init_hash = state_init.bytes_hash() + + address = Address( + str(self.options["wc"]) + ":" + hexlify(state_init_hash).decode() + ) + + return { + "code": code_cell, + "data": data_cell, + "address": address, + "state_init": state_init, + } + + def create_code_cell(self): + if "code" not in self.options or self.options["code"] is None: + raise Exception("Contract: options.code is not defined") + return self.options["code"] + + def create_data_cell(self): + return Cell() + + def create_init_external_message(self): + create_state_init = self.create_state_init() + state_init = create_state_init["state_init"] + address = create_state_init["address"] + code = create_state_init["code"] + data = create_state_init["data"] + header = Contract.create_external_message_header(address) + external_message = Contract.create_common_msg_info(header, state_init) + return { + "address": address, + "message": external_message, + "state_init": state_init, + "code": code, + "data": data, + } + + @classmethod + def create_external_message_header(cls, dest, src=None, import_fee=0): + message = Cell() + message.bits.write_uint(2, 2) + message.bits.write_address(Address(src) if src else None) + message.bits.write_address(Address(dest)) + message.bits.write_grams(import_fee) + return message + + @classmethod + def create_internal_message_header( + cls, + dest, + grams=0, + ihr_disabled=True, + bounce=None, + bounced=False, + src=None, + currency_collection=None, + ihr_fees=0, + fwd_fees=0, + created_lt=0, + created_at=0, + ): + message = Cell() + message.bits.write_bit(0) + message.bits.write_bit(ihr_disabled) + + if bounce is not None: + message.bits.write_bit(bounce) + else: + message.bits.write_bit(Address(dest).is_bounceable) + message.bits.write_bit(bounced) + message.bits.write_address(Address(src) if src else None) + message.bits.write_address(Address(dest)) + message.bits.write_grams(grams) + if currency_collection: + raise Exception("Currency collections are not implemented yet") + + message.bits.write_bit(bool(currency_collection)) + message.bits.write_grams(ihr_fees) + message.bits.write_grams(fwd_fees) + message.bits.write_uint(created_lt, 64) + message.bits.write_uint(created_at, 32) + return message + + @classmethod + def create_common_msg_info(cls, header, state_init=None, body=None): + common_msg_info = Cell() + common_msg_info.write_cell(header) + if state_init: + common_msg_info.bits.write_bit(1) + if ( + common_msg_info.bits.get_free_bits() - 1 + >= state_init.bits.get_used_bits() + ): + common_msg_info.bits.write_bit(0) + common_msg_info.write_cell(state_init) + else: + common_msg_info.bits.write_bit(1) + common_msg_info.refs.append(state_init) + else: + common_msg_info.bits.write_bit(0) + + if body: + # if False: + if common_msg_info.bits.get_free_bits() >= body.bits.get_used_bits(): + common_msg_info.bits.write_bit(0) + common_msg_info.write_cell(body) + else: + common_msg_info.bits.write_bit(1) + common_msg_info.refs.append(body) + else: + common_msg_info.bits.write_bit(0) + + return common_msg_info + + def __create_state_init( + self, code, data, library=None, split_depth=None, ticktock=None + ): + if library or split_depth or ticktock: + raise Exception( + "Library/SplitDepth/Ticktock in state init is not implemented" + ) + + state_init = Cell() + settings = bytearray( + "".join( + [ + "1" if i else "0" + for i in [ + bool(split_depth), + bool(ticktock), + bool(code), + bool(data), + bool(library), + ] + ] + ), + "utf-8", + ) + state_init.bits.write_bit_array(settings) + + if code: + state_init.refs.append(code) + if data: + state_init.refs.append(data) + if library: + state_init.refs.append(library) + return state_init diff --git a/core/src/apps/ton/tonsdk/contract/token/__init__.py b/core/src/apps/ton/tonsdk/contract/token/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/src/apps/ton/tonsdk/contract/token/ft/__init__.py b/core/src/apps/ton/tonsdk/contract/token/ft/__init__.py new file mode 100644 index 0000000000..2b9a79274c --- /dev/null +++ b/core/src/apps/ton/tonsdk/contract/token/ft/__init__.py @@ -0,0 +1,5 @@ +from .jetton_wallet import JettonWallet + +__all__ = [ + "JettonWallet", +] diff --git a/core/src/apps/ton/tonsdk/contract/token/ft/jetton_minter.py b/core/src/apps/ton/tonsdk/contract/token/ft/jetton_minter.py new file mode 100644 index 0000000000..1fd5392d29 --- /dev/null +++ b/core/src/apps/ton/tonsdk/contract/token/ft/jetton_minter.py @@ -0,0 +1,66 @@ +from ....boc import Cell +from ....utils import Address +from ... import Contract +from ..nft.nft_utils import create_offchain_uri_cell + + +class JettonMinter(Contract): + code = "B5EE9C7241020B010001ED000114FF00F4A413F4BCF2C80B0102016202030202CC040502037A60090A03EFD9910E38048ADF068698180B8D848ADF07D201800E98FE99FF6A2687D007D206A6A18400AA9385D47181A9AA8AAE382F9702480FD207D006A18106840306B90FD001812881A28217804502A906428027D012C678B666664F6AA7041083DEECBEF29385D71811A92E001F1811802600271812F82C207F97840607080093DFC142201B82A1009AA0A01E428027D012C678B00E78B666491646580897A007A00658064907C80383A6465816503E5FFE4E83BC00C646582AC678B28027D0109E5B589666664B8FD80400FE3603FA00FA40F82854120870542013541403C85004FA0258CF1601CF16CCC922C8CB0112F400F400CB00C9F9007074C8CB02CA07CBFFC9D05008C705F2E04A12A1035024C85004FA0258CF16CCCCC9ED5401FA403020D70B01C3008E1F8210D53276DB708010C8CB055003CF1622FA0212CB6ACB1FCB3FC98042FB00915BE200303515C705F2E049FA403059C85004FA0258CF16CCCCC9ED54002E5143C705F2E049D43001C85004FA0258CF16CCCCC9ED54007DADBCF6A2687D007D206A6A183618FC1400B82A1009AA0A01E428027D012C678B00E78B666491646580897A007A00658064FC80383A6465816503E5FFE4E840001FAF16F6A2687D007D206A6A183FAA904051007F09" + + def __init__(self, **kwargs): + self.code = kwargs.get("code") or self.code + kwargs["code"] = Cell.one_from_boc(self.code) + super().__init__(**kwargs) + + def create_data_cell(self) -> Cell: + cell = Cell() + cell.bits.write_grams(0) # total supply + cell.bits.write_address(self.options["admin_address"]) + cell.refs.append(create_offchain_uri_cell(self.options["jetton_content_uri"])) + cell.refs.append(Cell.one_from_boc(self.options["jetton_wallet_code_hex"])) + return cell + + def create_mint_body( + self, + destination: Address, + jetton_amount: int, + amount: int = 50000000, + query_id: int = 0, + ) -> Cell: + body = Cell() + body.bits.write_uint(21, 32) # OP mint + body.bits.write_uint(query_id, 64) + body.bits.write_address(destination) + body.bits.write_grams(amount) + + transfer_body = Cell() # internal transfer + transfer_body.bits.write_uint(0x178D4519, 32) # OP transfer + transfer_body.bits.write_uint(query_id, 64) + transfer_body.bits.write_grams(jetton_amount) # jetton amount + transfer_body.bits.write_address(None) # from_address + transfer_body.bits.write_address(None) # response_address + transfer_body.bits.write_grams(0) # forward amount + transfer_body.bits.write_bit( + 0 + ) # forward_payload in this slice, not separate cell + + body.refs.append(transfer_body) + return body + + def create_change_admin_body( + self, new_admin_address: Address, query_id: int = 0 + ) -> Cell: + body = Cell() + body.bits.write_uint(3, 32) # OP + body.bits.write_uint(query_id, 64) # query_id + body.bits.write_address(new_admin_address) + return body + + def create_edit_content_body( + self, jetton_content_uri: str, query_id: int = 0 + ) -> Cell: + body = Cell() + body.bits.write_uint(4, 32) # OP + body.bits.write_uint(query_id, 64) # query_id + body.refs.append(create_offchain_uri_cell(jetton_content_uri)) + return body diff --git a/core/src/apps/ton/tonsdk/contract/token/ft/jetton_wallet.py b/core/src/apps/ton/tonsdk/contract/token/ft/jetton_wallet.py new file mode 100644 index 0000000000..de7d5136b8 --- /dev/null +++ b/core/src/apps/ton/tonsdk/contract/token/ft/jetton_wallet.py @@ -0,0 +1,46 @@ +from ....boc import Cell +from ....utils import Address +from ... import Contract + + +class JettonWallet(Contract): + code = "B5EE9C7241021201000328000114FF00F4A413F4BCF2C80B0102016202030202CC0405001BA0F605DA89A1F401F481F481A8610201D40607020148080900BB0831C02497C138007434C0C05C6C2544D7C0FC02F83E903E900C7E800C5C75C87E800C7E800C00B4C7E08403E29FA954882EA54C4D167C0238208405E3514654882EA58C511100FC02780D60841657C1EF2EA4D67C02B817C12103FCBC2000113E910C1C2EBCB853600201200A0B020120101101F500F4CFFE803E90087C007B51343E803E903E90350C144DA8548AB1C17CB8B04A30BFFCB8B0950D109C150804D50500F214013E809633C58073C5B33248B232C044BD003D0032C032483E401C1D3232C0B281F2FFF274013E903D010C7E801DE0063232C1540233C59C3E8085F2DAC4F3208405E351467232C7C6600C03F73B51343E803E903E90350C0234CFFE80145468017E903E9014D6F1C1551CDB5C150804D50500F214013E809633C58073C5B33248B232C044BD003D0032C0327E401C1D3232C0B281F2FFF274140371C1472C7CB8B0C2BE80146A2860822625A020822625A004AD822860822625A028062849F8C3C975C2C070C008E00D0E0F009ACB3F5007FA0222CF165006CF1625FA025003CF16C95005CC2391729171E25008A813A08208989680AA008208989680A0A014BCF2E2C504C98040FB001023C85004FA0258CF1601CF16CCC9ED5400705279A018A182107362D09CC8CB1F5230CB3F58FA025007CF165007CF16C9718018C8CB0524CF165006FA0215CB6A14CCC971FB0010241023000E10491038375F040076C200B08E218210D53276DB708010C8CB055008CF165004FA0216CB6A12CB1F12CB3FC972FB0093356C21E203C85004FA0258CF1601CF16CCC9ED5400DB3B51343E803E903E90350C01F4CFFE803E900C145468549271C17CB8B049F0BFFCB8B0A0822625A02A8005A805AF3CB8B0E0841EF765F7B232C7C572CFD400FE8088B3C58073C5B25C60063232C14933C59C3E80B2DAB33260103EC01004F214013E809633C58073C5B3327B55200083200835C87B51343E803E903E90350C0134C7E08405E3514654882EA0841EF765F784EE84AC7CB8B174CFCC7E800C04E81408F214013E809633C58073C5B3327B55205ECCF23D" + + def __init__(self, **kwargs): + self.code = kwargs.get("code") or self.code + kwargs["code"] = Cell.one_from_boc(self.code) + super().__init__(**kwargs) + + def create_transfer_body( + self, + to_address: Address, + jetton_amount: int, + forward_amount: int = 0, + forward_payload: str = None, + response_address: Address = None, + query_id: int = 0, + ) -> Cell: + cell = Cell() + cell.bits.write_uint(0xF8A7EA5, 32) # request_transfer op + cell.bits.write_uint(query_id, 64) + cell.bits.write_grams(jetton_amount) + cell.bits.write_address(to_address) + cell.bits.write_address(response_address or to_address) + cell.bits.write_bit(0) # null custom_payload + cell.bits.write_grams(forward_amount) + cell.bits.write_bit(0) # forward_payload in this slice, not separate cell + if forward_payload: + cell.bits.write_uint(0, 32) + cell.bits.write_string(forward_payload) + + return cell + + def create_burn_body( + self, jetton_amount: int, response_address: Address = None, query_id: int = 0 + ) -> Cell: + cell = Cell() + cell.bits.write_uint(0x595F07BC, 32) # burn OP + cell.bits.write_uint(query_id, 64) + cell.bits.write_grams(jetton_amount) + cell.bits.write_address(response_address) + return cell diff --git a/core/src/apps/ton/tonsdk/contract/token/nft/__init__.py b/core/src/apps/ton/tonsdk/contract/token/nft/__init__.py new file mode 100644 index 0000000000..41b235dad0 --- /dev/null +++ b/core/src/apps/ton/tonsdk/contract/token/nft/__init__.py @@ -0,0 +1,9 @@ +from .nft_collection import NFTCollection +from .nft_item import NFTItem +from .nft_sale import NFTSale + +__all__ = [ + "NFTItem", + "NFTCollection", + "NFTSale", +] diff --git a/core/src/apps/ton/tonsdk/contract/token/nft/nft_collection.py b/core/src/apps/ton/tonsdk/contract/token/nft/nft_collection.py new file mode 100644 index 0000000000..802185b20d --- /dev/null +++ b/core/src/apps/ton/tonsdk/contract/token/nft/nft_collection.py @@ -0,0 +1,120 @@ +from math import floor +from typing import List, Tuple + +from ....boc import Cell, DictBuilder +from ....utils import Address +from ... import Contract +from .nft_utils import create_offchain_uri_cell, serialize_uri + + +class NFTCollection(Contract): + code = "B5EE9C724102140100021F000114FF00F4A413F4BCF2C80B0102016202030202CD04050201200E0F04E7D10638048ADF000E8698180B8D848ADF07D201800E98FE99FF6A2687D20699FEA6A6A184108349E9CA829405D47141BAF8280E8410854658056B84008646582A802E78B127D010A65B509E58FE59F80E78B64C0207D80701B28B9E382F970C892E000F18112E001718112E001F181181981E0024060708090201200A0B00603502D33F5313BBF2E1925313BA01FA00D43028103459F0068E1201A44343C85005CF1613CB3FCCCCCCC9ED54925F05E200A6357003D4308E378040F4966FA5208E2906A4208100FABE93F2C18FDE81019321A05325BBF2F402FA00D43022544B30F00623BA9302A402DE04926C21E2B3E6303250444313C85005CF1613CB3FCCCCCCC9ED54002C323401FA40304144C85005CF1613CB3FCCCCCCC9ED54003C8E15D4D43010344130C85005CF1613CB3FCCCCCCC9ED54E05F04840FF2F00201200C0D003D45AF0047021F005778018C8CB0558CF165004FA0213CB6B12CCCCC971FB008002D007232CFFE0A33C5B25C083232C044FD003D0032C03260001B3E401D3232C084B281F2FFF2742002012010110025BC82DF6A2687D20699FEA6A6A182DE86A182C40043B8B5D31ED44D0FA40D33FD4D4D43010245F04D0D431D430D071C8CB0701CF16CCC980201201213002FB5DAFDA89A1F481A67FA9A9A860D883A1A61FA61FF480610002DB4F47DA89A1F481A67FA9A9A86028BE09E008E003E00B01A500C6E" + + def __init__(self, **kwargs): + self.code = kwargs.get("code") or self.code + kwargs["code"] = Cell.one_from_boc(self.code) + super().__init__(**kwargs) + self.options["royalty_base"] = self.options.get("royalty_base", 1000) + self.options["royalty_factor"] = floor( + self.options.get("royalty", 0) * self.options["royalty_base"] + ) + + def create_content_cell(self, params) -> Cell: + collection_content_cell = create_offchain_uri_cell( + params["collection_content_uri"] + ) + common_content_cell = Cell() + common_content_cell.bits.write_bytes( + serialize_uri(params["nft_item_content_base_uri"]) + ) + content_cell = Cell() + content_cell.refs.append(collection_content_cell) + content_cell.refs.append(common_content_cell) + return content_cell + + def create_royalty_cell(self, params) -> Cell: + royalty_cell = Cell() + royalty_cell.bits.write_uint(params["royalty_factor"], 16) + royalty_cell.bits.write_uint(params["royalty_base"], 16) + royalty_cell.bits.write_address(params["royalty_address"]) + return royalty_cell + + def create_data_cell(self) -> Cell: + cell = Cell() + cell.bits.write_address(self.options["owner_address"]) + cell.bits.write_uint(0, 64) # next_item_index + cell.refs.append(self.create_content_cell(self.options)) + cell.refs.append(Cell.one_from_boc(self.options["nft_item_code_hex"])) + cell.refs.append(self.create_royalty_cell(self.options)) + return cell + + def create_mint_body( + self, + item_index: int, + new_owner_address: Address, + item_content_uri: str, + amount: int = 50000000, + query_id: int = 0, + ) -> Cell: + body = Cell() + body.bits.write_uint(1, 32) + body.bits.write_uint(query_id, 64) + body.bits.write_uint(item_index, 64) + body.bits.write_grams(amount) + content_cell = Cell() + content_cell.bits.write_address(new_owner_address) + uri_content = Cell() + uri_content.bits.write_bytes(serialize_uri(item_content_uri)) + content_cell.refs.append(uri_content) + body.refs.append(content_cell) + return body + + def create_batch_mint_body( + self, + from_item_index: int, + contents_and_owners: List[Tuple[str, Address]], + amount_per_one: int = 50000000, + query_id: int = 0, + ) -> Cell: + body = Cell() + body.bits.write_uint(2, 32) + body.bits.write_uint(query_id, 64) + deploy_list = DictBuilder(64) + for i, (item_content_uri, new_owner_address) in enumerate(contents_and_owners): + item = Cell() + item.bits.write_grams(amount_per_one) + content = Cell() + content.bits.write_address(new_owner_address) + uri_content = Cell() + uri_content.bits.write_bytes(serialize_uri(item_content_uri)) + content.refs.append(uri_content) + item.refs.append(content) + deploy_list.store_cell(i + from_item_index, item) + body.refs.append(deploy_list.end_dict()) + return body + + def create_get_royalty_params_body(self, query_id: int = 0) -> Cell: + body = Cell() + body.bits.write_uint(0x693D3950, 32) # OP + body.bits.write_uint(query_id, 64) # query_id + return body + + def create_change_owner_body( + self, new_owner_address: Address, query_id: int = 0 + ) -> Cell: + body = Cell() + body.bits.write_uint(3, 32) # OP + body.bits.write_uint(query_id, 64) # query_id + body.bits.write_address(new_owner_address) + return body + + def create_edit_content_body(self, params) -> Cell: + if params["royalty"] > 1: + raise Exception("royalty must be less than 1") + + body = Cell() + body.bits.write_uint(4, 32) # OP + body.bits.write_uint(params.get("query_id", 0), 64) # query_id + body.refs.append(self.create_content_cell(params)) + body.refs.append(self.create_royalty_cell(params)) + return body diff --git a/core/src/apps/ton/tonsdk/contract/token/nft/nft_item.py b/core/src/apps/ton/tonsdk/contract/token/nft/nft_item.py new file mode 100644 index 0000000000..c69920c226 --- /dev/null +++ b/core/src/apps/ton/tonsdk/contract/token/nft/nft_item.py @@ -0,0 +1,49 @@ +from ....boc import Cell +from ....utils import Address +from ... import Contract + + +class NFTItem(Contract): + code = "B5EE9C7241020D010001D0000114FF00F4A413F4BCF2C80B0102016202030202CE04050009A11F9FE00502012006070201200B0C02D70C8871C02497C0F83434C0C05C6C2497C0F83E903E900C7E800C5C75C87E800C7E800C3C00812CE3850C1B088D148CB1C17CB865407E90350C0408FC00F801B4C7F4CFE08417F30F45148C2EA3A1CC840DD78C9004F80C0D0D0D4D60840BF2C9A884AEB8C097C12103FCBC20080900113E910C1C2EBCB8536001F65135C705F2E191FA4021F001FA40D20031FA00820AFAF0801BA121945315A0A1DE22D70B01C300209206A19136E220C2FFF2E192218E3E821005138D91C85009CF16500BCF16712449145446A0708010C8CB055007CF165005FA0215CB6A12CB1FCB3F226EB39458CF17019132E201C901FB00104794102A375BE20A00727082108B77173505C8CBFF5004CF1610248040708010C8CB055007CF165005FA0215CB6A12CB1FCB3F226EB39458CF17019132E201C901FB000082028E3526F0018210D53276DB103744006D71708010C8CB055007CF165005FA0215CB6A12CB1FCB3F226EB39458CF17019132E201C901FB0093303234E25502F003003B3B513434CFFE900835D27080269FC07E90350C04090408F80C1C165B5B60001D00F232CFD633C58073C5B3327B5520BF75041B" + + def __init__(self, **kwargs): + self.code = kwargs.get("code") or self.code + kwargs["code"] = Cell.one_from_boc(self.code) + super().__init__(**kwargs) + + def create_data_cell(self) -> Cell: + cell = Cell() + cell.bits.write_uint(self.options.get("index", 0), 64) + cell.bits.write_address(self.options.get("collection_address", None)) + if "owner_address" in self.options: + cell.bits.write_address(self.options["owner_address"]) + if "content" in self.options: + cell.refs.append(self.options["content"]) + return cell + + def create_transfer_body( + self, + new_owner_address: Address, + response_address: Address = None, + forward_amount: int = 0, + forward_payload: bytes = None, + query_id: int = 0, + ) -> Cell: + cell = Cell() + cell.bits.write_uint(0x5FCC3D14, 32) # transfer OP + cell.bits.write_uint(query_id, 64) + cell.bits.write_address(new_owner_address) + cell.bits.write_address(response_address or new_owner_address) + cell.bits.write_bit(False) # null custom_payload + cell.bits.write_grams(forward_amount) + cell.bits.write_bit(False) # forward_payload in this slice, not separate cell + if forward_payload: + cell.bits.write_bytes(forward_payload) + + return cell + + def create_get_static_data_body(self, query_id: int = 0) -> Cell: + cell = Cell() + cell.bits.write_uint(0x2FCB26A2, 32) + cell.bits.write_uint(query_id, 64) + return cell diff --git a/core/src/apps/ton/tonsdk/contract/token/nft/nft_sale.py b/core/src/apps/ton/tonsdk/contract/token/nft/nft_sale.py new file mode 100644 index 0000000000..0baf032169 --- /dev/null +++ b/core/src/apps/ton/tonsdk/contract/token/nft/nft_sale.py @@ -0,0 +1,31 @@ +from ....boc import Cell +from ... import Contract + + +class NFTSale(Contract): + code = "B5EE9C7241020A010001B4000114FF00F4A413F4BCF2C80B01020120020302014804050004F2300202CD0607002FA03859DA89A1F481F481F481F401A861A1F401F481F4006101F7D00E8698180B8D8492F82707D201876A2687D207D207D207D006A18116BA4E10159C71D991B1B2990E382C92F837028916382F970FA01698FC1080289C6C8895D7970FAE99F98FD2018201A642802E78B2801E78B00E78B00FD016664F6AA701363804C9B081B2299823878027003698FE99F9810E000C92F857010C0801F5D41081DCD650029285029185F7970E101E87D007D207D0018384008646582A804E78B28B9D090D0A85AD08A500AFD010AE5B564B8FD80384008646582AC678B2803FD010B65B564B8FD80384008646582A802E78B00FD0109E5B564B8FD80381041082FE61E8A10C00C646582A802E78B117D010A65B509E58F8A40900C8C0029A3110471036454012F004E032363704C0038E4782103B9ACA0015BEF2E1C95312C70559C705B1F2E1CA702082105FCC3D14218010C8CB055006CF1622FA0215CB6A14CB1F14CB3F21CF1601CF16CA0021FA02CA00C98100A0FB00E05F06840FF2F0002ACB3F22CF1658CF16CA0021FA02CA00C98100A0FB00AECABAD1" + + def __init__(self, **kwargs): + self.code = kwargs.get("code") or self.code + kwargs["code"] = Cell.one_from_boc(self.code) + super().__init__(**kwargs) + + def create_data_cell(self) -> Cell: + cell = Cell() + cell.bits.write_address(self.options["marketplace_address"]) + cell.bits.write_address(self.options["nft_address"]) + cell.bits.write_address(None) # nft_owner_address + cell.bits.write_grams(self.options["full_price"]) + + fees_cell = Cell() + fees_cell.bits.write_coins(self.options["marketplace_fee"]) + fees_cell.bits.write_address(self.options["royalty_address"]) + fees_cell.bits.write_coins(self.options["royalty_amount"]) + cell.refs.append(fees_cell) + return cell + + def create_cancel_body(self, query_id: int = 0) -> Cell: + cell = Cell() + cell.bits.write_uint(3, 32) # cancel OP-code + cell.bits.write_uint(query_id, 64) + return cell diff --git a/core/src/apps/ton/tonsdk/contract/token/nft/nft_utils.py b/core/src/apps/ton/tonsdk/contract/token/nft/nft_utils.py new file mode 100644 index 0000000000..b23b80599a --- /dev/null +++ b/core/src/apps/ton/tonsdk/contract/token/nft/nft_utils.py @@ -0,0 +1,42 @@ +import urllib.parse + +from ....boc import Cell + +SNAKE_DATA_PREFIX = 0x00 +CHUNK_DATA_PREFIX = 0x01 +ONCHAIN_CONTENT_PREFIX = 0x00 +OFFCHAIN_CONTENT_PREFIX = 0x01 + + +def serialize_uri(uri): + return urllib.parse.quote(uri, safe="~@#$&()*!+=:;,?/'").encode() + + +def parse_uri(uri): + return uri.decode() + + +def create_offchain_uri_cell(uri): + cell = Cell() + cell.bits.write_uint8(OFFCHAIN_CONTENT_PREFIX) + cell.bits.write_bytes(serialize_uri(uri)) + return cell + + +def parse_offchain_uri_cell(cell): + assert cell.bits[0] == OFFCHAIN_CONTENT_PREFIX, "Invalid offchain uri cell" + length = 0 + c = cell + while c: + length += len(c.bits) + c = c.refs[0] if c.refs else None + + _bytes = b"" + length = 0 + c = cell + while c: + _bytes += c.bits + length += len(c.bits) + c = c.refs[0] if c.refs else None + + return parse_uri(_bytes[1:]) diff --git a/core/src/apps/ton/tonsdk/contract/wallet/__init__.py b/core/src/apps/ton/tonsdk/contract/wallet/__init__.py new file mode 100755 index 0000000000..311ebd77a0 --- /dev/null +++ b/core/src/apps/ton/tonsdk/contract/wallet/__init__.py @@ -0,0 +1,39 @@ +from typing import TYPE_CHECKING + +from ._wallet_contract import SendModeEnum, WalletContract +from ._wallet_contract_v3 import WalletV3ContractR1, WalletV3ContractR2 +from ._wallet_contract_v4 import WalletV4ContractR1, WalletV4ContractR2 + +if TYPE_CHECKING: + from enum import Enum +else: + Enum = object + + +class WalletVersionEnum(str, Enum): + v3r1 = "v3r1" + v3r2 = "v3r2" + v4r1 = "v4r1" + v4r2 = "v4r2" + + +class Wallets: + default_version = WalletVersionEnum.v3r2 + ALL = { + WalletVersionEnum.v3r1: WalletV3ContractR1, + WalletVersionEnum.v3r2: WalletV3ContractR2, + WalletVersionEnum.v4r1: WalletV4ContractR1, + WalletVersionEnum.v4r2: WalletV4ContractR2, + } + + +__all__ = [ + "WalletV3ContractR1", + "WalletV3ContractR2", + "WalletV4ContractR1", + "WalletV4ContractR2", + "WalletContract", + "SendModeEnum", + "WalletVersionEnum", + "Wallets", +] diff --git a/core/src/apps/ton/tonsdk/contract/wallet/_wallet_contract.py b/core/src/apps/ton/tonsdk/contract/wallet/_wallet_contract.py new file mode 100755 index 0000000000..ba6e2a9ffa --- /dev/null +++ b/core/src/apps/ton/tonsdk/contract/wallet/_wallet_contract.py @@ -0,0 +1,121 @@ +from typing import TYPE_CHECKING, List, Union + +from ...boc import Cell +from ...utils import Address +from .. import Contract + +if TYPE_CHECKING: + from enum import IntEnum +else: + IntEnum = int + + +class SendModeEnum(IntEnum): + carry_all_remaining_balance = 128 + carry_all_remaining_incoming_value = 64 + destroy_account_if_zero = 32 + ignore_errors = 2 + pay_gas_separately = 1 + + def __str__(self) -> str: + return super().__str__() + + +class WalletContract(Contract): + def __init__(self, **kwargs): + if "public_key" not in kwargs: + raise Exception("WalletContract required publicKey in options") + super().__init__(**kwargs) + + def create_data_cell(self): + cell = Cell() + cell.bits.write_uint(0, 32) + cell.bits.write_bytes(self.options["public_key"]) + return cell + + def create_signing_message(self, _expiration_time, seqno=None): + seqno = seqno or 0 + cell = Cell() + cell.bits.write_uint(seqno, 32) + return cell + + def create_transaction_digest( + self, + to_addr: str, + amount: int, + seqno: int, + expire_at: int, + payload: Union[Cell, str, bytes, None] = None, + is_raw_data: bool = False, + send_mode=SendModeEnum.ignore_errors | SendModeEnum.pay_gas_separately, + state_init=None, + ext_to: List[str] = None, + ext_amount: List[int] = None, + ext_payload: List[Union[Cell, str, bytes, None]] = None, + ): + payload_cell = Cell() + if payload: + if isinstance(payload, str): + # check payload type + # if is_raw_data: + if payload.startswith("b5ee9c72"): + payload_cell = Cell.one_from_boc(payload) + else: + payload_cell.bits.write_uint(0, 32) + payload_cell.bits.write_string(payload) + elif isinstance(payload, Cell): + payload_cell = payload + else: + payload_cell.bits.write_bytes(payload) + + order_header = Contract.create_internal_message_header( + dest=Address(to_addr), grams=amount + ) + order = Contract.create_common_msg_info(order_header, state_init, payload_cell) + signing_message = self.create_signing_message(expire_at, seqno) + signing_message.bits.write_uint8(send_mode) + signing_message.refs.append(order) + + if ext_to: + if len(ext_to) > 3: + raise ValueError( + "Number of extra messages exceeds the maximum limit of 3" + ) + + ext_payload_list = ( + ext_payload if ext_payload is not None else [None] * len(ext_to) + ) + ext_amount_list = ( + ext_amount if ext_amount is not None else [0] * len(ext_to) + ) + + zipped_ext_data = zip(ext_to, ext_payload_list, ext_amount_list) + + for ext_addr, current_payload, ext_amt in zipped_ext_data: + ext_payload_cell = Cell() + if current_payload: + if isinstance(current_payload, str): + # check payload type + if current_payload.startswith("b5ee9c72"): + ext_payload_cell = Cell.one_from_boc(current_payload) + else: + ext_payload_cell.bits.write_uint(0, 32) + ext_payload_cell.bits.write_string(current_payload) + elif isinstance(current_payload, Cell): + ext_payload_cell = current_payload + else: + ext_payload_cell.bits.write_bytes(current_payload) + + ext_order_header = Contract.create_internal_message_header( + dest=Address(ext_addr), grams=ext_amt + ) + ext_order = Contract.create_common_msg_info( + ext_order_header, state_init, ext_payload_cell + ) + + signing_message.bits.write_uint8(send_mode) + signing_message.refs.append(ext_order) + + boc = bytes(signing_message.to_boc()) + + return signing_message.bytes_hash(), boc diff --git a/core/src/apps/ton/tonsdk/contract/wallet/_wallet_contract_v3.py b/core/src/apps/ton/tonsdk/contract/wallet/_wallet_contract_v3.py new file mode 100755 index 0000000000..81d765fece --- /dev/null +++ b/core/src/apps/ton/tonsdk/contract/wallet/_wallet_contract_v3.py @@ -0,0 +1,42 @@ +from ...boc import Cell +from ._wallet_contract import WalletContract + + +class WalletV3ContractBase(WalletContract): + def create_data_cell(self): + cell = Cell() + cell.bits.write_uint(0, 32) + cell.bits.write_uint(self.options["wallet_id"], 32) + cell.bits.write_bytes(self.options["public_key"]) + return cell + + def create_signing_message(self, expiration_time, seqno=None): + seqno = seqno or 0 + message = Cell() + message.bits.write_uint(self.options["wallet_id"], 32) + if seqno == 0: + for _ in range(32): + message.bits.write_bit(1) + else: + message.bits.write_uint(expiration_time, 32) + + message.bits.write_uint(seqno, 32) + return message + + +class WalletV3ContractR1(WalletV3ContractBase): + def __init__(self, **kwargs) -> None: + self.code = "B5EE9C724101010100620000C0FF0020DD2082014C97BA9730ED44D0D70B1FE0A4F2608308D71820D31FD31FD31FF82313BBF263ED44D0D31FD31FD3FFD15132BAF2A15144BAF2A204F901541055F910F2A3F8009320D74A96D307D402FB00E8D101A4C8CB1FCB1FCBFFC9ED543FBE6EE0" + kwargs["code"] = Cell.one_from_boc(self.code) + super().__init__(**kwargs) + if "wallet_id" not in kwargs: + self.options["wallet_id"] = 698983191 + self.options["wc"] + + +class WalletV3ContractR2(WalletV3ContractBase): + def __init__(self, **kwargs) -> None: + self.code = "B5EE9C724101010100710000DEFF0020DD2082014C97BA218201339CBAB19F71B0ED44D0D31FD31F31D70BFFE304E0A4F2608308D71820D31FD31FD31FF82313BBF263ED44D0D31FD31FD3FFD15132BAF2A15144BAF2A204F901541055F910F2A3F8009320D74A96D307D402FB00E8D101A4C8CB1FCB1FCBFFC9ED5410BD6DAD" + kwargs["code"] = Cell.one_from_boc(self.code) + super().__init__(**kwargs) + if "wallet_id" not in kwargs: + self.options["wallet_id"] = 698983191 + self.options["wc"] diff --git a/core/src/apps/ton/tonsdk/contract/wallet/_wallet_contract_v4.py b/core/src/apps/ton/tonsdk/contract/wallet/_wallet_contract_v4.py new file mode 100755 index 0000000000..5538a12d7d --- /dev/null +++ b/core/src/apps/ton/tonsdk/contract/wallet/_wallet_contract_v4.py @@ -0,0 +1,46 @@ +from ...boc import Cell +from ._wallet_contract import WalletContract + + +class WalletV4ContractBase(WalletContract): + def create_data_cell(self): + cell = Cell() + cell.bits.write_uint(0, 32) + cell.bits.write_uint(self.options["wallet_id"], 32) + cell.bits.write_bytes(self.options["public_key"]) + cell.bits.write_uint(0, 1) # plugins dict empty + return cell + + def create_signing_message(self, expiration_time, seqno=None, without_op=False): + seqno = seqno or 0 + message = Cell() + message.bits.write_uint(self.options["wallet_id"], 32) + if seqno == 0: + for _ in range(32): + message.bits.write_bit(1) + else: + message.bits.write_uint(expiration_time, 32) + + message.bits.write_uint(seqno, 32) + + if not without_op: + message.bits.write_uint(0, 8) + return message + + +class WalletV4ContractR1(WalletV4ContractBase): + def __init__(self, **kwargs) -> None: + self.code = "B5EE9C72410215010002F5000114FF00F4A413F4BCF2C80B010201200203020148040504F8F28308D71820D31FD31FD31F02F823BBF263ED44D0D31FD31FD3FFF404D15143BAF2A15151BAF2A205F901541064F910F2A3F80024A4C8CB1F5240CB1F5230CBFF5210F400C9ED54F80F01D30721C0009F6C519320D74A96D307D402FB00E830E021C001E30021C002E30001C0039130E30D03A4C8CB1F12CB1FCBFF1112131403EED001D0D3030171B0915BE021D749C120915BE001D31F218210706C7567BD228210626C6E63BDB022821064737472BDB0925F03E002FA403020FA4401C8CA07CBFFC9D0ED44D0810140D721F404305C810108F40A6FA131B3925F05E004D33FC8258210706C7567BA9131E30D248210626C6E63BAE30004060708020120090A005001FA00F404308210706C7567831EB17080185005CB0527CF165003FA02F40012CB69CB1F5210CB3F0052F8276F228210626C6E63831EB17080185005CB0527CF1624FA0214CB6A13CB1F5230CB3F01FA02F4000092821064737472BA8E3504810108F45930ED44D0810140D720C801CF16F400C9ED54821064737472831EB17080185004CB0558CF1622FA0212CB6ACB1FCB3F9410345F04E2C98040FB000201200B0C0059BD242B6F6A2684080A06B90FA0218470D4080847A4937D29910CE6903E9FF9837812801B7810148987159F31840201580D0E0011B8C97ED44D0D70B1F8003DB29DFB513420405035C87D010C00B23281F2FFF274006040423D029BE84C600201200F100019ADCE76A26840206B90EB85FFC00019AF1DF6A26840106B90EB858FC0006ED207FA00D4D422F90005C8CA0715CBFFC9D077748018C8CB05CB0222CF165005FA0214CB6B12CCCCC971FB00C84014810108F451F2A702006C810108D718C8542025810108F451F2A782106E6F746570748018C8CB05CB025004CF16821005F5E100FA0213CB6A12CB1FC971FB00020072810108D718305202810108F459F2A7F82582106473747270748018C8CB05CB025005CF16821005F5E100FA0214CB6A13CB1F12CB3FC973FB00000AF400C9ED5446A9F34F" + kwargs["code"] = Cell.one_from_boc(self.code) + super().__init__(**kwargs) + if "wallet_id" not in kwargs: + self.options["wallet_id"] = 698983191 + self.options["wc"] + + +class WalletV4ContractR2(WalletV4ContractBase): + def __init__(self, **kwargs) -> None: + self.code = "B5EE9C72410214010002D4000114FF00F4A413F4BCF2C80B010201200203020148040504F8F28308D71820D31FD31FD31F02F823BBF264ED44D0D31FD31FD3FFF404D15143BAF2A15151BAF2A205F901541064F910F2A3F80024A4C8CB1F5240CB1F5230CBFF5210F400C9ED54F80F01D30721C0009F6C519320D74A96D307D402FB00E830E021C001E30021C002E30001C0039130E30D03A4C8CB1F12CB1FCBFF1011121302E6D001D0D3032171B0925F04E022D749C120925F04E002D31F218210706C7567BD22821064737472BDB0925F05E003FA403020FA4401C8CA07CBFFC9D0ED44D0810140D721F404305C810108F40A6FA131B3925F07E005D33FC8258210706C7567BA923830E30D03821064737472BA925F06E30D06070201200809007801FA00F40430F8276F2230500AA121BEF2E0508210706C7567831EB17080185004CB0526CF1658FA0219F400CB6917CB1F5260CB3F20C98040FB0006008A5004810108F45930ED44D0810140D720C801CF16F400C9ED540172B08E23821064737472831EB17080185005CB055003CF1623FA0213CB6ACB1FCB3FC98040FB00925F03E20201200A0B0059BD242B6F6A2684080A06B90FA0218470D4080847A4937D29910CE6903E9FF9837812801B7810148987159F31840201580C0D0011B8C97ED44D0D70B1F8003DB29DFB513420405035C87D010C00B23281F2FFF274006040423D029BE84C600201200E0F0019ADCE76A26840206B90EB85FFC00019AF1DF6A26840106B90EB858FC0006ED207FA00D4D422F90005C8CA0715CBFFC9D077748018C8CB05CB0222CF165005FA0214CB6B12CCCCC973FB00C84014810108F451F2A7020070810108D718FA00D33FC8542047810108F451F2A782106E6F746570748018C8CB05CB025006CF165004FA0214CB6A12CB1FCB3FC973FB0002006C810108D718FA00D33F305224810108F459F2A782106473747270748018C8CB05CB025005CF165003FA0213CB6ACB1F12CB3FC973FB00000AF400C9ED54696225E5" + kwargs["code"] = Cell.one_from_boc(self.code) + super().__init__(**kwargs) + if "wallet_id" not in kwargs: + self.options["wallet_id"] = 698983191 + self.options["wc"] diff --git a/core/src/apps/ton/tonsdk/utils/__init__.py b/core/src/apps/ton/tonsdk/utils/__init__.py new file mode 100755 index 0000000000..ff6a31b33a --- /dev/null +++ b/core/src/apps/ton/tonsdk/utils/__init__.py @@ -0,0 +1,23 @@ +from ._address import Address +from ._utils import ( + compare_bytes, + concat_bytes, + crc16, + crc32c, + int_to_hex, + move_to_end, + read_n_bytes_uint_from_array, + tree_walk, +) + +__all__ = [ + "Address", + "concat_bytes", + "move_to_end", + "tree_walk", + "crc32c", + "crc16", + "read_n_bytes_uint_from_array", + "compare_bytes", + "int_to_hex", +] diff --git a/core/src/apps/ton/tonsdk/utils/_address.py b/core/src/apps/ton/tonsdk/utils/_address.py new file mode 100755 index 0000000000..b2eb57cd5d --- /dev/null +++ b/core/src/apps/ton/tonsdk/utils/_address.py @@ -0,0 +1,148 @@ +from binascii import a2b_base64, b2a_base64, unhexlify + +from ._utils import crc16, string_to_bytes + + +def parse_friendly_address(addr_str): + if len(addr_str) != 48: + raise Exception("User-friendly address should contain strictly 48 characters") + + # avoid padding error (https://gist.github.com/perrygeo/ee7c65bb1541ff6ac770) + data = string_to_bytes(a2b_base64(addr_str + "==")) + + if len(data) != 36: + raise Exception("Unknown address type: byte length is not equal to 36") + + addr = data[:34] + crc = data[34:36] + calced_crc = crc16(addr) + if not (calced_crc[0] == crc[0] and calced_crc[1] == crc[1]): + raise Exception("Wrong crc16 hashsum") + + tag = addr[0] + is_test_only = False + is_bounceable = False + if tag & Address.TEST_FLAG: + is_test_only = True + tag ^= Address.TEST_FLAG + if tag not in [Address.BOUNCEABLE_TAG, Address.NON_BOUNCEABLE_TAG]: + raise Exception("Unknown address tag") + + is_bounceable = tag == Address.BOUNCEABLE_TAG + + if addr[1] == 0xFF: + workchain = -1 + else: + workchain = addr[1] + if workchain not in [0, -1]: + raise Exception(f"Invalid address wc {workchain}") + + hash_part = bytearray(addr[2:34]) + return { + "is_test_only": is_test_only, + "is_bounceable": is_bounceable, + "workchain": workchain, + "hash_part": hash_part, + } + + +class Address: + BOUNCEABLE_TAG = 0x11 + NON_BOUNCEABLE_TAG = 0x51 + TEST_FLAG = 0x80 + + def __init__(self, any_form): + if any_form is None: + raise Exception("Invalid address") + + if isinstance(any_form, Address): + self.wc = any_form.wc + self.hash_part = any_form.hash_part + self.is_test_only = any_form.is_test_only + self.is_user_friendly = any_form.is_user_friendly + self.is_bounceable = any_form.is_bounceable + self.is_url_safe = any_form.is_url_safe + return + + if any_form.find("-") > 0 or any_form.find("_") > 0: + any_form = any_form.replace("-", "+").replace("_", "/") + self.is_url_safe = True + else: + self.is_url_safe = False + + try: + colon_index = any_form.index(":") + except ValueError: + colon_index = -1 + + if colon_index > -1: + arr = any_form.split(":") + if len(arr) != 2: + raise Exception(f"Invalid address {any_form}") + + wc = int(arr[0]) + if wc not in [0, -1]: + raise Exception(f"Invalid address wc {wc}") + + address_hex = arr[1] + if len(address_hex) != 64: + raise Exception(f"Invalid address hex {any_form}") + + self.is_user_friendly = False + self.wc = wc + self.hash_part = unhexlify(address_hex) + self.is_test_only = False + self.is_bounceable = False + else: + self.is_user_friendly = True + parse_result = parse_friendly_address(any_form) + self.wc = parse_result["workchain"] + self.hash_part = parse_result["hash_part"] + self.is_test_only = parse_result["is_test_only"] + self.is_bounceable = parse_result["is_bounceable"] + + def to_string( + self, + is_user_friendly=None, + is_url_safe=None, + is_bounceable=None, + is_test_only=None, + ): + if is_user_friendly is None: + is_user_friendly = self.is_user_friendly + if is_url_safe is None: + is_url_safe = self.is_url_safe + if is_bounceable is None: + is_bounceable = self.is_bounceable + if is_test_only is None: + is_test_only = self.is_test_only + + if not is_user_friendly: + return f"{self.wc}:{self.hash_part.hex()}" + else: + tag = ( + Address.BOUNCEABLE_TAG if is_bounceable else Address.NON_BOUNCEABLE_TAG + ) + + if is_test_only: + tag |= Address.TEST_FLAG + + addr = bytearray(34) + addr[0] = tag + addr[1] = self.wc + addr[2:] = self.hash_part + address_with_checksum = bytearray(36) + address_with_checksum[:34] = addr + address_with_checksum[34:] = crc16(addr) + + address_base_64 = b2a_base64(address_with_checksum)[:-1].decode("utf-8") + if is_url_safe: + address_base_64 = address_base_64.replace("+", "-").replace("/", "_") + + return str(address_base_64) + + def get_hash_part(self): + return self.hash_part + + def to_buffer(self): + return self.hash_part + bytearray([self.wc, self.wc, self.wc, self.wc]) diff --git a/core/src/apps/ton/tonsdk/utils/_utils.py b/core/src/apps/ton/tonsdk/utils/_utils.py new file mode 100755 index 0000000000..e2f52ef356 --- /dev/null +++ b/core/src/apps/ton/tonsdk/utils/_utils.py @@ -0,0 +1,132 @@ +import math +import ustruct as struct + + +def concat_bytes(a, b): + return a + b # ? + + +def move_to_end(index_hashmap, topological_order_arr, target): + target_index = index_hashmap[target] + for _hash in index_hashmap: + if index_hashmap[_hash] > target_index: + index_hashmap[_hash] -= 1 + index_hashmap[target] = len(topological_order_arr) - 1 + data = topological_order_arr.pop(target_index) + topological_order_arr.append(data) + for sub_cell in data[1].refs: + topological_order_arr, index_hashmap = move_to_end( + index_hashmap, topological_order_arr, sub_cell.bytes_hash() + ) + return [topological_order_arr, index_hashmap] + + +def tree_walk(cell, topological_order_arr, index_hashmap, parent_hash=None): + cell_hash = cell.bytes_hash() + if cell_hash in index_hashmap: + if parent_hash: + if index_hashmap[parent_hash] > index_hashmap[cell_hash]: + topological_order_arr, index_hashmap = move_to_end( + index_hashmap, topological_order_arr, cell_hash + ) + return [topological_order_arr, index_hashmap] + + index_hashmap[cell_hash] = len(topological_order_arr) + topological_order_arr.append([cell_hash, cell]) + for sub_cell in cell.refs: + topological_order_arr, index_hashmap = tree_walk( + sub_cell, topological_order_arr, index_hashmap, cell_hash + ) + return [topological_order_arr, index_hashmap] + + +def _crc32c(crc, bytes_arr): + POLY = 0x82F63B78 + + crc ^= 0xFFFFFFFF + + for n in range(len(bytes_arr)): + crc ^= bytes_arr[n] + crc = (crc >> 1) ^ POLY if crc & 1 else crc >> 1 + crc = (crc >> 1) ^ POLY if crc & 1 else crc >> 1 + crc = (crc >> 1) ^ POLY if crc & 1 else crc >> 1 + crc = (crc >> 1) ^ POLY if crc & 1 else crc >> 1 + crc = (crc >> 1) ^ POLY if crc & 1 else crc >> 1 + crc = (crc >> 1) ^ POLY if crc & 1 else crc >> 1 + crc = (crc >> 1) ^ POLY if crc & 1 else crc >> 1 + crc = (crc >> 1) ^ POLY if crc & 1 else crc >> 1 + + return crc ^ 0xFFFFFFFF + + +def crc32c(bytes_array): + int_crc = _crc32c(0, bytes_array) + + # TODO: check mistakes + arr = bytearray(4) + struct.pack_into(" 0: + reg <<= 1 + if byte & mask: + reg += 1 + mask >>= 1 + if reg > 0xFFFF: + reg &= 0xFFFF + reg ^= POLY + + return bytearray([math.floor(reg / 256), reg % 256]) + + +def read_n_bytes_uint_from_array(size_bytes, uint8_array): + res = 0 + for c in range(size_bytes): + res *= 256 + res += uint8_array[c] # must be uint8 + + return res + + +def compare_bytes(bytes_1, bytes_2): + return str(bytes_1) == str(bytes_2) # why str? + + +def string_to_bytes(string, size=1): # ? + if size == 1: + buf = bytearray(len(string)) + elif size == 2: + buf = bytearray(len(string) * 2) + elif size == 4: + buf = bytearray(len(string) * 4) + else: + raise Exception("Invalid size") + + for i, c in enumerate(string): + # buf[i] = ord(c) + buf[i] = c # ? + + return bytes(buf) + + +def int_to_hex(n): + if n == 0: + return "0" + + hex_digits = "0123456789abcdef" + hex_string = "" + + while n > 0: + remainder = n % 16 + hex_string = hex_digits[remainder] + hex_string + n = n // 16 + + return hex_string diff --git a/core/src/apps/workflow_handlers.py b/core/src/apps/workflow_handlers.py index 3fcd84ac61..0a34c5aa35 100644 --- a/core/src/apps/workflow_handlers.py +++ b/core/src/apps/workflow_handlers.py @@ -339,6 +339,14 @@ def find_message_handler_module(msg_type: int) -> str: if msg_type == MessageType.LnurlAuth: return "apps.lnurl.auth" + # ton + if msg_type == MessageType.TonGetAddress: + return "apps.ton.get_address" + if msg_type == MessageType.TonSignMessage: + return "apps.ton.sign_message" + if msg_type == MessageType.TonSignProof: + return "apps.ton.sign_proof" + raise ValueError diff --git a/core/src/trezor/enums/MessageType.py b/core/src/trezor/enums/MessageType.py index f41a065b25..275e932c3d 100644 --- a/core/src/trezor/enums/MessageType.py +++ b/core/src/trezor/enums/MessageType.py @@ -357,6 +357,12 @@ NostrDecryptedMessage = 11507 NostrSignSchnorr = 11508 NostrSignedSchnorr = 11509 + TonGetAddress = 11901 + TonAddress = 11902 + TonSignMessage = 11903 + TonSignedMessage = 11904 + TonSignProof = 11905 + TonSignedProof = 11906 LnurlAuth = 11600 LnurlAuthResp = 11601 DeviceBackToBoot = 903 diff --git a/core/src/trezor/enums/TonWalletVersion.py b/core/src/trezor/enums/TonWalletVersion.py new file mode 100644 index 0000000000..54284e8681 --- /dev/null +++ b/core/src/trezor/enums/TonWalletVersion.py @@ -0,0 +1,5 @@ +# Automatically generated by pb2py +# fmt: off +# isort:skip_file + +V4R2 = 3 diff --git a/core/src/trezor/enums/TonWorkChain.py b/core/src/trezor/enums/TonWorkChain.py new file mode 100644 index 0000000000..2a7bace073 --- /dev/null +++ b/core/src/trezor/enums/TonWorkChain.py @@ -0,0 +1,6 @@ +# Automatically generated by pb2py +# fmt: off +# isort:skip_file + +BASECHAIN = 0 +MASTERCHAIN = 1 diff --git a/core/src/trezor/enums/__init__.py b/core/src/trezor/enums/__init__.py index 0ae8f78bcf..3aba4f6999 100644 --- a/core/src/trezor/enums/__init__.py +++ b/core/src/trezor/enums/__init__.py @@ -375,6 +375,12 @@ class MessageType(IntEnum): NostrDecryptedMessage = 11507 NostrSignSchnorr = 11508 NostrSignedSchnorr = 11509 + TonGetAddress = 11901 + TonAddress = 11902 + TonSignMessage = 11903 + TonSignedMessage = 11904 + TonSignProof = 11905 + TonSignedProof = 11906 LnurlAuth = 11600 LnurlAuthResp = 11601 DeviceBackToBoot = 903 @@ -687,6 +693,13 @@ class TezosBallotType(IntEnum): Nay = 1 Pass = 2 + class TonWalletVersion(IntEnum): + V4R2 = 3 + + class TonWorkChain(IntEnum): + BASECHAIN = 0 + MASTERCHAIN = 1 + class TronResourceCode(IntEnum): BANDWIDTH = 0 ENERGY = 1 diff --git a/core/src/trezor/lvglui/res/chain-ton.png b/core/src/trezor/lvglui/res/chain-ton.png new file mode 100755 index 0000000000..0be1c2aadf Binary files /dev/null and b/core/src/trezor/lvglui/res/chain-ton.png differ diff --git a/core/src/trezor/lvglui/scrs/template.py b/core/src/trezor/lvglui/scrs/template.py index 0fbc6ae4f9..3ce47b5dd4 100644 --- a/core/src/trezor/lvglui/scrs/template.py +++ b/core/src/trezor/lvglui/scrs/template.py @@ -850,6 +850,225 @@ def __init__(self, title: str, primary_color, icon_path, **kwargs): ) +class TonMessage(FullSizeWindow): + def __init__( + self, + title, + address, + message, + domain, + primary_color, + icon_path, + verify: bool = False, + ): + super().__init__( + title, + None, + _(i18n_keys.BUTTON__VERIFY) if verify else _(i18n_keys.BUTTON__SIGN), + _(i18n_keys.BUTTON__CANCEL), + anim_dir=2, + primary_color=primary_color, + icon_path=icon_path, + ) + self.primary_color = primary_color + self.container = ContainerFlexCol( + self.content_area, self.title, pos=(0, 40), padding_row=8 + ) + if domain: + self.item3 = DisplayItemNoBgc( + self.container, + _(i18n_keys.LIST_KEY__DOMAIN__COLON), + str(domain), + ) + self.item1 = DisplayItemNoBgc( + self.container, _(i18n_keys.LIST_KEY__ADDRESS__COLON), address + ) + self.long_message = False + if len(message) > 80: + # self.show_full_message = NormalButton( + # self, _(i18n_keys.BUTTON__VIEW_FULL_MESSAGE) + # ) + # self.show_full_message.align_to(self.item2, lv.ALIGN.OUT_BOTTOM_MID, 0, 32) + # self.show_full_message.add_event_cb(self.on_click, lv.EVENT.CLICKED, None) + self.message = message + self.long_message = True + self.btn_yes.label.set_text(_(i18n_keys.BUTTON__VIEW)) + else: + self.item2 = DisplayItemNoBgc( + self.container, _(i18n_keys.LIST_KEY__MESSAGE__COLON), message + ) + + def eventhandler(self, event_obj): + code = event_obj.code + target = event_obj.get_target() + if code == lv.EVENT.CLICKED: + if target == self.btn_yes: + if self.long_message: + PageAbleMessage( + _(i18n_keys.LIST_KEY__MESSAGE__COLON), + self.message, + self.channel, + primary_color=self.primary_color, + confirm_text=_(i18n_keys.BUTTON__SIGN), + ) + self.destroy() + else: + self.show_unload_anim() + self.channel.publish(1) + elif target == self.btn_no: + self.show_dismiss_anim() + self.channel.publish(0) + + +class TransactionDetailsTON(FullSizeWindow): + def __init__( + self, + title, + address_from, + address_to, + amount, + fee_max, + is_eip1559=False, + gas_price=None, + max_priority_fee_per_gas=None, + max_fee_per_gas=None, + total_amount=None, + primary_color=lv_colors.ONEKEY_GREEN, + contract_addr=None, + token_id=None, + evm_chain_id=None, + raw_data=None, + is_raw_data=False, + sub_icon_path=None, + striped=False, + ): + super().__init__( + title, + None, + _(i18n_keys.BUTTON__CONTINUE), + _(i18n_keys.BUTTON__REJECT), + primary_color=primary_color, + ) + self.primary_color = primary_color + self.container = ContainerFlexCol(self.content_area, self.title, pos=(0, 40)) + + self.item1 = DisplayItemNoBgc( + self.container, + _(i18n_keys.LIST_KEY__AMOUNT__COLON), + amount, + ) + self.item4 = DisplayItemNoBgc( + self.container, + _(i18n_keys.LIST_KEY__TO__COLON), + address_to, + ) + self.item5 = DisplayItemNoBgc( + self.container, + _(i18n_keys.LIST_KEY__FROM__COLON), + address_from, + ) + if contract_addr: + self.item0 = DisplayItemNoBgc( + self.container, + _(i18n_keys.LIST_KEY__CONTRACT_ADDRESS__COLON), + contract_addr, + ) + self.item1 = DisplayItemNoBgc( + self.container, + _(i18n_keys.LIST_KEY__TOKEN_ID__COLON), + token_id, + ) + + # if total_amount is None: + # if not contract_addr: # token transfer + # total_amount = f"{amount}\n{fee_max}" + # else: # nft transfer + # total_amount = f"{fee_max}" + # self.item6 = DisplayItemNoBgc( + # self.container, + # _(i18n_keys.LIST_KEY__TOTAL_AMOUNT__COLON), + # total_amount, + # ) + + if raw_data: + from trezor import strings + + self.data_str = strings.format_customer_data(raw_data) + + self.item7 = DisplayItemNoBgc( + self.container, + _(i18n_keys.LIST_KEY__DATA__COLON) + if self.data_str.startswith("b5ee9c72") + else _(i18n_keys.LIST_KEY__MEMO__COLON), + _(i18n_keys.SUBTITLE__STR_BYTES).format(len(raw_data)), + ) + self.panel = lv.obj(self.content_area) + self.panel.clear_flag(lv.obj.FLAG.SCROLLABLE) + self.panel.add_style( + StyleWrapper() + .width(464) + .height(lv.SIZE.CONTENT) + .bg_color(lv_colors.ONEKEY_BLACK_3) + .bg_opa() + .border_width(1) + .border_color(lv_colors.ONEKEY_GRAY_1) + .pad_all(8) + .radius(0) + .max_height(256) + .text_font(font_MONO24) + .text_color(lv_colors.LIGHT_GRAY) + .text_align_left() + .text_letter_space(-1), + 0, + ) + self.panel.align_to(self.container, lv.ALIGN.OUT_BOTTOM_MID, 0, 8) + self.content = lv.label(self.panel) + self.content.set_align(lv.ALIGN.TOP_LEFT) + self.content.set_size(lv.pct(100), lv.SIZE.CONTENT) + + if len(self.data_str) > 216: + self.content.set_long_mode(lv.label.LONG.WRAP) + self.content.set_text(self.data_str[:213] + "...") + self.view_btn = NormalButton(self.panel, _(i18n_keys.BUTTON__VIEW)) + self.view_btn.set_width(246) + self.view_btn.align(lv.ALIGN.CENTER, 0, 0) + self.view_btn.add_style( + StyleWrapper() + .bg_color(lv_colors.ONEKEY_BLACK_3) + .border_width(2) + .border_color(lv_colors.ONEKEY_GRAY_1), + 0, + ) + self.view_btn.add_style( + StyleWrapper() + .bg_opa(lv.OPA.COVER) + .bg_color(lv_colors.ONEKEY_GRAY_3) + .transform_height(-2) + .transform_width(-2) + .transition(BtnClickTransition()), + lv.PART.MAIN | lv.STATE.PRESSED, + ) + + self.view_btn.add_event_cb(self.on_click, lv.EVENT.CLICKED, None) + else: + self.content.set_text(self.data_str) + + def on_click(self, event_obj): + code = event_obj.code + target = event_obj.get_target() + if code == lv.EVENT.CLICKED: + if target == self.view_btn: + PageAbleMessage( + _(i18n_keys.TITLE__VIEW_DATA), + self.data_str, + channel=None, + cancel_text=_(i18n_keys.BUTTON__CLOSE), + confirm_text=None, + page_size=405, + font=font_MONO24, + ) + + class TransactionDetailsTRON(FullSizeWindow): def __init__( self, @@ -2055,3 +2274,35 @@ def __init__( self.item2 = DisplayItemNoBgc( self.container, _(i18n_keys.LIST_KEY__DATA__COLON), data ) + + +class TonTransfer(FullSizeWindow): + def __init__( + self, + address_from, + address_to, + amount, + memo, + primary_color=None, + ): + super().__init__( + _(i18n_keys.TITLE__SIGN_STR_TRANSACTION).format("TON"), + None, + _(i18n_keys.BUTTON__CONTINUE), + _(i18n_keys.BUTTON__REJECT), + primary_color=primary_color, + ) + self.container = ContainerFlexCol(self.content_area, self.title, pos=(0, 40)) + self.item1 = DisplayItemNoBgc( + self.container, _(i18n_keys.LIST_KEY__AMOUNT__COLON), amount + ) + self.item2 = DisplayItemNoBgc( + self.container, _(i18n_keys.LIST_KEY__TO__COLON), address_to + ) + self.item3 = DisplayItemNoBgc( + self.container, _(i18n_keys.LIST_KEY__FROM__COLON), address_from + ) + if memo: + self.item4 = DisplayItemNoBgc( + self.container, _(i18n_keys.LIST_KEY__MEMO__COLON), memo + ) diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index 36ec8ea8c8..6e3f033805 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -62,6 +62,8 @@ def __getattr__(name: str) -> Any: from trezor.enums import StellarSignerType # noqa: F401 from trezor.enums import TezosBallotType # noqa: F401 from trezor.enums import TezosContractType # noqa: F401 + from trezor.enums import TonWalletVersion # noqa: F401 + from trezor.enums import TonWorkChain # noqa: F401 from trezor.enums import TronResourceCode # noqa: F401 from trezor.enums import WordRequestType # noqa: F401 @@ -8261,6 +8263,164 @@ def __init__( def is_type_of(cls, msg: Any) -> TypeGuard["TezosManagerTransfer"]: return isinstance(msg, cls) + class TonGetAddress(protobuf.MessageType): + address_n: "list[int]" + show_display: "bool | None" + wallet_version: "TonWalletVersion" + is_bounceable: "bool" + is_testnet_only: "bool" + workchain: "TonWorkChain" + wallet_id: "int" + + def __init__( + self, + *, + address_n: "list[int] | None" = None, + show_display: "bool | None" = None, + wallet_version: "TonWalletVersion | None" = None, + is_bounceable: "bool | None" = None, + is_testnet_only: "bool | None" = None, + workchain: "TonWorkChain | None" = None, + wallet_id: "int | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["TonGetAddress"]: + return isinstance(msg, cls) + + class TonAddress(protobuf.MessageType): + public_key: "bytes" + address: "str" + + def __init__( + self, + *, + public_key: "bytes", + address: "str", + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["TonAddress"]: + return isinstance(msg, cls) + + class TonSignMessage(protobuf.MessageType): + address_n: "list[int]" + destination: "str" + jetton_master_address: "str | None" + jetton_wallet_address: "str | None" + ton_amount: "int" + jetton_amount: "int | None" + fwd_fee: "int" + comment: "str | None" + is_raw_data: "bool" + mode: "int" + seqno: "int" + expire_at: "int" + wallet_version: "TonWalletVersion" + wallet_id: "int" + workchain: "TonWorkChain" + is_bounceable: "bool" + is_testnet_only: "bool" + ext_destination: "list[str]" + ext_ton_amount: "list[int]" + ext_payload: "list[str]" + jetton_amount_bytes: "bytes | None" + signing_message_hash: "bytes | None" + + def __init__( + self, + *, + destination: "str", + ton_amount: "int", + seqno: "int", + expire_at: "int", + address_n: "list[int] | None" = None, + ext_destination: "list[str] | None" = None, + ext_ton_amount: "list[int] | None" = None, + ext_payload: "list[str] | None" = None, + jetton_master_address: "str | None" = None, + jetton_wallet_address: "str | None" = None, + jetton_amount: "int | None" = None, + fwd_fee: "int | None" = None, + comment: "str | None" = None, + is_raw_data: "bool | None" = None, + mode: "int | None" = None, + wallet_version: "TonWalletVersion | None" = None, + wallet_id: "int | None" = None, + workchain: "TonWorkChain | None" = None, + is_bounceable: "bool | None" = None, + is_testnet_only: "bool | None" = None, + jetton_amount_bytes: "bytes | None" = None, + signing_message_hash: "bytes | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["TonSignMessage"]: + return isinstance(msg, cls) + + class TonSignedMessage(protobuf.MessageType): + signature: "bytes | None" + signning_message: "bytes | None" + + def __init__( + self, + *, + signature: "bytes | None" = None, + signning_message: "bytes | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["TonSignedMessage"]: + return isinstance(msg, cls) + + class TonSignProof(protobuf.MessageType): + address_n: "list[int]" + appdomain: "bytes" + comment: "bytes | None" + expire_at: "int" + wallet_version: "TonWalletVersion" + wallet_id: "int" + workchain: "TonWorkChain" + is_bounceable: "bool" + is_testnet_only: "bool" + + def __init__( + self, + *, + appdomain: "bytes", + expire_at: "int", + address_n: "list[int] | None" = None, + comment: "bytes | None" = None, + wallet_version: "TonWalletVersion | None" = None, + wallet_id: "int | None" = None, + workchain: "TonWorkChain | None" = None, + is_bounceable: "bool | None" = None, + is_testnet_only: "bool | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["TonSignProof"]: + return isinstance(msg, cls) + + class TonSignedProof(protobuf.MessageType): + signature: "bytes | None" + + def __init__( + self, + *, + signature: "bytes | None" = None, + ) -> None: + pass + + @classmethod + def is_type_of(cls, msg: Any) -> TypeGuard["TonSignedProof"]: + return isinstance(msg, cls) + class TronGetAddress(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 c26414c5ba..19f7d5a51b 100644 --- a/core/src/trezor/strings.py +++ b/core/src/trezor/strings.py @@ -90,3 +90,20 @@ def format_timestamp(timestamp: int) -> str: # that is used internally. d = utime.gmtime2000(timestamp - _SECONDS_1970_TO_2000) 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 diff --git a/core/src/trezor/ui/layouts/lvgl/__init__.py b/core/src/trezor/ui/layouts/lvgl/__init__.py index 8a094a64e6..fa02625bba 100644 --- a/core/src/trezor/ui/layouts/lvgl/__init__.py +++ b/core/src/trezor/ui/layouts/lvgl/__init__.py @@ -71,6 +71,9 @@ "should_show_details", "confirm_nostrmessage", "confirm_lnurl_auth", + "confirm_ton_transfer", + "confirm_unknown_token_transfer", + "confirm_ton_signverify", ) @@ -965,6 +968,40 @@ async def confirm_signverify( ) +async def confirm_ton_signverify( + ctx: wire.GenericContext, + coin: str, + message: str, + address: str, + domain: str, + verify: bool, +) -> None: + if verify: + header = _(i18n_keys.TITLE__VERIFY_STR_MESSAGE).format(coin) + br_type = "verify_message" + else: + header = _(i18n_keys.TITLE__SIGN_STR_MESSAGE).format(coin) + br_type = "sign_message" + from trezor.lvglui.scrs.template import TonMessage + + await raise_if_cancelled( + interact( + ctx, + TonMessage( + header, + address, + message, + domain, + ctx.primary_color, + ctx.icon_path, + verify, + ), + br_type, + ButtonRequestType.Other, + ) + ) + + async def show_popup( title: str, description: str | None = None, @@ -1865,6 +1902,22 @@ async def confirm_polkadot_balances( ) +def confirm_unknown_token_transfer( + ctx: wire.GenericContext, + address: str, +): + return confirm_address( + ctx, + _(i18n_keys.TITLE__UNKNOWN_TOKEN), + address, + description=_(i18n_keys.LIST_KEY__CONTRACT_ADDRESS__COLON), + br_type="unknown_token", + icon="A:/res/warning.png", + icon_color=ui.ORANGE, + br_code=ButtonRequestType.SignTx, + ) + + async def confirm_tron_freeze( ctx: wire.GenericContext, title: str, @@ -2015,3 +2068,19 @@ async def confirm_lnurl_auth( ButtonRequestType.Other, ) ) + + +async def confirm_ton_transfer( + ctx: wire.GenericContext, + from_addr: str, + to_addr: str, + amount: str, + memo: str | None, +): + from trezor.lvglui.scrs.template import TonTransfer + + screen = TonTransfer(from_addr, to_addr, amount, memo, ctx.primary_color) + + await raise_if_cancelled( + interact(ctx, screen, "confirm_ton_transfer", ButtonRequestType.ProtectCall) + ) diff --git a/core/src/trezor/ui/layouts/lvgl/altcoin.py b/core/src/trezor/ui/layouts/lvgl/altcoin.py index 3faab24814..3380b284bf 100644 --- a/core/src/trezor/ui/layouts/lvgl/altcoin.py +++ b/core/src/trezor/ui/layouts/lvgl/altcoin.py @@ -160,3 +160,38 @@ async def confirm_total_tron( await raise_if_cancelled( interact(ctx, screen, "confirm_total", ButtonRequestType.SignTx) ) + + +async def confirm_total_ton( + ctx: wire.GenericContext, + amount: str, + gas_price: str | None, + fee_max: str, + from_address: str | None, + to_address: str | None, + total_amount: str | None, + contract_addr: str | None = None, + token_id: int | None = None, + evm_chain_id: int | None = None, + raw_data: bytes | None = None, + is_raw_data: bool = False, +) -> None: + from trezor.lvglui.scrs.template import TransactionDetailsTON + + screen = TransactionDetailsTON( + _(i18n_keys.TITLE__TRANSACTION_DETAILS), + from_address, + to_address, + amount, + fee_max, + gas_price=gas_price, + total_amount=total_amount, + primary_color=ctx.primary_color, + contract_addr=contract_addr, + token_id=str(token_id), + evm_chain_id=evm_chain_id, + raw_data=raw_data, + ) + await raise_if_cancelled( + interact(ctx, screen, "confirm_total", ButtonRequestType.SignTx) + ) diff --git a/python/src/trezorlib/cli/ton.py b/python/src/trezorlib/cli/ton.py new file mode 100644 index 0000000000..254b4b10cf --- /dev/null +++ b/python/src/trezorlib/cli/ton.py @@ -0,0 +1,186 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + + +from typing import TYPE_CHECKING + +import click +import time +from trezorlib import ton, tools +from trezorlib.cli import with_client, ChoiceType +from trezorlib import messages +if TYPE_CHECKING: + from ..client import TrezorClient +PATH_HELP = "BIP-32 path, e.g. m/44'/607'/0'/0'" + +WORKCHAIN = { + "base": messages.TonWorkChain.BASECHAIN, + "master": messages.TonWorkChain.MASTERCHAIN, +} +WALLET_VERSION = { + # "v3r1": messages.TonWalletVersion.V3R1, + # "v3r2": messages.TonWalletVersion.V3R2, + # "v4r1": messages.TonWalletVersion.V4R1, + "v4r2": messages.TonWalletVersion.V4R2, +} +@click.group(name="ton") +def cli(): + """Ton commands.""" + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-b", "--bounceable", is_flag=True) +@click.option("-t", "--test-only", is_flag=True) +@click.option("-i", "--wallet-id", type=int, default=698983191) +@click.option("-v", "--version", type=ChoiceType(WALLET_VERSION), default="v4r2") +@click.option("-w", "--workchain", type=ChoiceType(WORKCHAIN), default="base") +@click.option("-d", "--show-display", is_flag=True) +@with_client +def get_address(client: "TrezorClient", + address: str, + bounceable: bool, + test_only: bool, + wallet_id: int, + version: messages.TonWalletVersion, + workchain: messages.TonWorkChain, + show_display: bool + ) -> 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) + public_key = resp.public_key.hex() + return {"public_key": f"{public_key}", "address": f"{resp.address}"} + + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-d", "--destination", type=str, required=True) +@click.option("-j", "--jetton_master_address", type=str) +@click.option("-jw", "--jetton_wallet_address", type=str) +@click.option("-ta", "--ton_amount", type=int, required=True) +@click.option("-ja", "--jetton_amount", type=int) +@click.option("-jab", "--jetton_amount_bytes", type=str) +@click.option("-f", "--fwd_fee", type=int) +@click.option("-c", "--comment", type=str) +@click.option("-r", "--is_raw_data", is_flag=True) +@click.option("-m", "--mode", type=int) +@click.option("-s", "--seqno", type=int, required=True) +@click.option("-e", "--expire_at", type=int, required=True) +@click.option("-v", "--version", type=ChoiceType(WALLET_VERSION), default="v4r2") +@click.option("-i", "--wallet-id", type=int, default=698983191) +@click.option("-w", "--workchain", type=ChoiceType(WORKCHAIN), default="base") +@click.option("-b", "--bounceable", is_flag=True) +@click.option("-t", "--test-only", is_flag=True) +@click.option("-ed", "--ext-destination", multiple=True, type=str) +@click.option("-ea", "--ext-ton-amount", multiple=True, type=int) +@click.option("-ep", "--ext-payload", multiple=True, type=str) +@click.option("-h", "--signing-message-hash", type=str) +@with_client +def sign_message(client: "TrezorClient", + address: str, + destination: str, + jetton_master_address: str, + jetton_wallet_address: str, + ton_amount: int, + jetton_amount: int, + jetton_amount_bytes: str, + fwd_fee: int, + mode: int, + seqno: int, + expire_at: int, + comment: str, + is_raw_data: bool, + version: messages.TonWalletVersion, + wallet_id: int, + workchain: messages.TonWorkChain, + bounceable: bool, + test_only: bool, + ext_destination: tuple[str, ...], + ext_ton_amount: tuple[int, ...], + ext_payload: tuple[str, ...], + signing_message_hash: str + ) -> bytes: + """Sign Ton Transaction.""" + address_n = tools.parse_path(address) + # expire_at = int(time.time()) + 300 + resp = ton.sign_message( + client, + address_n, + destination, + jetton_master_address, + jetton_wallet_address, + ton_amount, + jetton_amount, + jetton_amount_bytes, + fwd_fee, + mode, + seqno, + expire_at, + comment, + is_raw_data, + version, + wallet_id, + workchain, + bounceable, + test_only, + list(ext_destination), + list(ext_ton_amount), + list(ext_payload), + signing_message_hash + ) + + return resp.signature.hex(), resp.signning_message.hex() + +@cli.command() +@click.option("-n", "--address", required=True, help=PATH_HELP) +@click.option("-a", "--appdomain", required=True, type=str) +@click.option("-c", "--comment", type=str) +@click.option("-v", "--version", type=ChoiceType(WALLET_VERSION), default="v4r2") +@click.option("-i", "--wallet-id", type=int, default=698983191) +@click.option("-w", "--workchain", type=ChoiceType(WORKCHAIN), default="base") +@click.option("-b", "--bounceable", is_flag=True) +@click.option("-t", "--test-only", is_flag=True) +@with_client +def sign_proof(client: "TrezorClient", + address: str, + # expire_at: int, + appdomain: str, + comment: str, + version: messages.TonWalletVersion, + wallet_id: int, + workchain: messages.TonWorkChain, + bounceable: bool, + test_only: bool + ) -> bytes: + """Sign Ton Proof.""" + address_n = tools.parse_path(address) + # expire_at = int(time.time()) + 300 + expire_at = 1979465599 + signature = ton.sign_proof( + client, + address_n, + expire_at, + appdomain, + comment, + version, + wallet_id, + workchain, + bounceable, + test_only + ).signature.hex() + + return {"signature": f"0x{signature}"} \ No newline at end of file diff --git a/python/src/trezorlib/cli/trezorctl.py b/python/src/trezorlib/cli/trezorctl.py index f5bf032534..5a8f2ccc5f 100755 --- a/python/src/trezorlib/cli/trezorctl.py +++ b/python/src/trezorlib/cli/trezorctl.py @@ -66,6 +66,7 @@ nexa, nostr, lnurl, + ton, ) F = TypeVar("F", bound=Callable) @@ -113,6 +114,7 @@ "nexa": nexa.cli, "nostr": nostr.cli, "lnurl": lnurl.cli, + "ton": ton.cli, # firmware aliases: "fw": firmware.cli, "update-firmware": firmware.update, @@ -470,6 +472,7 @@ def wait_for_emulator(obj: TrezorConnection, timeout: float) -> None: cli.add_command(nexa.cli) cli.add_command(nostr.cli) cli.add_command(lnurl.cli) +cli.add_command(ton.cli) # # Main diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index 5c75f47807..07fbe74363 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -383,6 +383,12 @@ class MessageType(IntEnum): NostrDecryptedMessage = 11507 NostrSignSchnorr = 11508 NostrSignedSchnorr = 11509 + TonGetAddress = 11901 + TonAddress = 11902 + TonSignMessage = 11903 + TonSignedMessage = 11904 + TonSignProof = 11905 + TonSignedProof = 11906 LnurlAuth = 11600 LnurlAuthResp = 11601 DeviceBackToBoot = 903 @@ -745,6 +751,15 @@ class TezosBallotType(IntEnum): Pass = 2 +class TonWalletVersion(IntEnum): + V4R2 = 3 + + +class TonWorkChain(IntEnum): + BASECHAIN = 0 + MASTERCHAIN = 1 + + class TronResourceCode(IntEnum): BANDWIDTH = 0 ENERGY = 1 @@ -10242,6 +10257,201 @@ def __init__( self.amount = amount +class TonGetAddress(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 11901 + FIELDS = { + 1: protobuf.Field("address_n", "uint32", repeated=True, required=False), + 2: protobuf.Field("show_display", "bool", repeated=False, required=False), + 3: protobuf.Field("wallet_version", "TonWalletVersion", repeated=False, required=False), + 4: protobuf.Field("is_bounceable", "bool", repeated=False, required=False), + 5: protobuf.Field("is_testnet_only", "bool", repeated=False, required=False), + 6: protobuf.Field("workchain", "TonWorkChain", repeated=False, required=False), + 7: protobuf.Field("wallet_id", "uint32", repeated=False, required=False), + } + + def __init__( + self, + *, + address_n: Optional[Sequence["int"]] = None, + show_display: Optional["bool"] = None, + wallet_version: Optional["TonWalletVersion"] = TonWalletVersion.V4R2, + is_bounceable: Optional["bool"] = False, + is_testnet_only: Optional["bool"] = False, + workchain: Optional["TonWorkChain"] = TonWorkChain.BASECHAIN, + wallet_id: Optional["int"] = 698983191, + ) -> None: + self.address_n: Sequence["int"] = address_n if address_n is not None else [] + self.show_display = show_display + self.wallet_version = wallet_version + self.is_bounceable = is_bounceable + self.is_testnet_only = is_testnet_only + self.workchain = workchain + self.wallet_id = wallet_id + + +class TonAddress(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 11902 + FIELDS = { + 1: protobuf.Field("public_key", "bytes", repeated=False, required=True), + 2: protobuf.Field("address", "string", repeated=False, required=True), + } + + def __init__( + self, + *, + public_key: "bytes", + address: "str", + ) -> None: + self.public_key = public_key + self.address = address + + +class TonSignMessage(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 11903 + FIELDS = { + 1: protobuf.Field("address_n", "uint32", repeated=True, required=False), + 2: protobuf.Field("destination", "string", repeated=False, required=True), + 3: protobuf.Field("jetton_master_address", "string", repeated=False, required=False), + 4: protobuf.Field("jetton_wallet_address", "string", repeated=False, required=False), + 5: protobuf.Field("ton_amount", "uint64", repeated=False, required=True), + 6: protobuf.Field("jetton_amount", "uint64", repeated=False, required=False), + 7: protobuf.Field("fwd_fee", "uint64", repeated=False, required=False), + 8: protobuf.Field("comment", "string", repeated=False, required=False), + 9: protobuf.Field("is_raw_data", "bool", repeated=False, required=False), + 10: protobuf.Field("mode", "uint32", repeated=False, required=False), + 11: protobuf.Field("seqno", "uint32", repeated=False, required=True), + 12: protobuf.Field("expire_at", "uint64", repeated=False, required=True), + 13: protobuf.Field("wallet_version", "TonWalletVersion", repeated=False, required=False), + 14: protobuf.Field("wallet_id", "uint32", repeated=False, required=False), + 15: protobuf.Field("workchain", "TonWorkChain", repeated=False, required=False), + 16: protobuf.Field("is_bounceable", "bool", repeated=False, required=False), + 17: protobuf.Field("is_testnet_only", "bool", repeated=False, required=False), + 18: protobuf.Field("ext_destination", "string", repeated=True, required=False), + 19: protobuf.Field("ext_ton_amount", "uint64", repeated=True, required=False), + 20: protobuf.Field("ext_payload", "string", repeated=True, required=False), + 21: protobuf.Field("jetton_amount_bytes", "bytes", repeated=False, required=False), + 22: protobuf.Field("signing_message_hash", "bytes", repeated=False, required=False), + } + + def __init__( + self, + *, + destination: "str", + ton_amount: "int", + seqno: "int", + expire_at: "int", + address_n: Optional[Sequence["int"]] = None, + ext_destination: Optional[Sequence["str"]] = None, + ext_ton_amount: Optional[Sequence["int"]] = None, + ext_payload: Optional[Sequence["str"]] = None, + jetton_master_address: Optional["str"] = None, + jetton_wallet_address: Optional["str"] = None, + jetton_amount: Optional["int"] = None, + fwd_fee: Optional["int"] = 0, + comment: Optional["str"] = None, + is_raw_data: Optional["bool"] = False, + mode: Optional["int"] = 3, + wallet_version: Optional["TonWalletVersion"] = TonWalletVersion.V4R2, + wallet_id: Optional["int"] = 698983191, + workchain: Optional["TonWorkChain"] = TonWorkChain.BASECHAIN, + is_bounceable: Optional["bool"] = False, + is_testnet_only: Optional["bool"] = False, + jetton_amount_bytes: Optional["bytes"] = None, + signing_message_hash: Optional["bytes"] = None, + ) -> None: + self.address_n: Sequence["int"] = address_n if address_n is not None else [] + self.ext_destination: Sequence["str"] = ext_destination if ext_destination is not None else [] + self.ext_ton_amount: Sequence["int"] = ext_ton_amount if ext_ton_amount is not None else [] + self.ext_payload: Sequence["str"] = ext_payload if ext_payload is not None else [] + self.destination = destination + self.ton_amount = ton_amount + self.seqno = seqno + self.expire_at = expire_at + self.jetton_master_address = jetton_master_address + self.jetton_wallet_address = jetton_wallet_address + self.jetton_amount = jetton_amount + self.fwd_fee = fwd_fee + self.comment = comment + self.is_raw_data = is_raw_data + self.mode = mode + self.wallet_version = wallet_version + self.wallet_id = wallet_id + self.workchain = workchain + self.is_bounceable = is_bounceable + self.is_testnet_only = is_testnet_only + self.jetton_amount_bytes = jetton_amount_bytes + self.signing_message_hash = signing_message_hash + + +class TonSignedMessage(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 11904 + FIELDS = { + 1: protobuf.Field("signature", "bytes", repeated=False, required=False), + 2: protobuf.Field("signning_message", "bytes", repeated=False, required=False), + } + + def __init__( + self, + *, + signature: Optional["bytes"] = None, + signning_message: Optional["bytes"] = None, + ) -> None: + self.signature = signature + self.signning_message = signning_message + + +class TonSignProof(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 11905 + FIELDS = { + 1: protobuf.Field("address_n", "uint32", repeated=True, required=False), + 2: protobuf.Field("appdomain", "bytes", repeated=False, required=True), + 3: protobuf.Field("comment", "bytes", repeated=False, required=False), + 4: protobuf.Field("expire_at", "uint64", repeated=False, required=True), + 5: protobuf.Field("wallet_version", "TonWalletVersion", repeated=False, required=False), + 6: protobuf.Field("wallet_id", "uint32", repeated=False, required=False), + 7: protobuf.Field("workchain", "TonWorkChain", repeated=False, required=False), + 8: protobuf.Field("is_bounceable", "bool", repeated=False, required=False), + 9: protobuf.Field("is_testnet_only", "bool", repeated=False, required=False), + } + + def __init__( + self, + *, + appdomain: "bytes", + expire_at: "int", + address_n: Optional[Sequence["int"]] = None, + comment: Optional["bytes"] = None, + wallet_version: Optional["TonWalletVersion"] = TonWalletVersion.V4R2, + wallet_id: Optional["int"] = 698983191, + workchain: Optional["TonWorkChain"] = TonWorkChain.BASECHAIN, + is_bounceable: Optional["bool"] = False, + is_testnet_only: Optional["bool"] = False, + ) -> None: + self.address_n: Sequence["int"] = address_n if address_n is not None else [] + self.appdomain = appdomain + self.expire_at = expire_at + self.comment = comment + self.wallet_version = wallet_version + self.wallet_id = wallet_id + self.workchain = workchain + self.is_bounceable = is_bounceable + self.is_testnet_only = is_testnet_only + + +class TonSignedProof(protobuf.MessageType): + MESSAGE_WIRE_TYPE = 11906 + FIELDS = { + 1: protobuf.Field("signature", "bytes", repeated=False, required=False), + } + + def __init__( + self, + *, + signature: Optional["bytes"] = None, + ) -> None: + self.signature = signature + + class TronGetAddress(protobuf.MessageType): MESSAGE_WIRE_TYPE = 10501 FIELDS = { diff --git a/python/src/trezorlib/ton.py b/python/src/trezorlib/ton.py new file mode 100644 index 0000000000..dc096f68f6 --- /dev/null +++ b/python/src/trezorlib/ton.py @@ -0,0 +1,116 @@ +from typing import TYPE_CHECKING + +from . import messages +from .tools import expect + +if TYPE_CHECKING: + from .client import TrezorClient + from .tools import Address + + +@expect(messages.TonAddress) +def get_address(client: "TrezorClient", + n: "Address", + version: messages.TonWalletVersion=messages.TonWalletVersion.V4R2, + workchain: messages.TonWorkChain=messages.TonWorkChain.BASECHAIN, + bounceable: bool = False, + test_only: bool = False, + wallet_id: int = 698983191, + show_display: bool = False): + return client.call( + messages.TonGetAddress( + address_n=n, + wallet_version=version, + workchain=workchain, + is_bounceable=bounceable, + is_testnet_only=test_only, + wallet_id=wallet_id, + show_display=show_display + ) + ) + +@expect(messages.TonSignedMessage) +def sign_message(client: "TrezorClient", + n: "Address", + destination: str, + jetton_master_address: str, + jetton_wallet_address: str, + ton_amount: int, + jetton_amount: int, + jetton_amount_bytes: str, + fwd_fee: int, + mode: int, + seqno: int, + expire_at: int, + comment: str="", + is_raw_data: bool = False, + version: messages.TonWalletVersion=messages.TonWalletVersion.V4R2, + wallet_id: int = 698983191, + 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 + ): + 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') + + if signing_message_hash is not None: + signing_message_hash = bytes.fromhex(signing_message_hash[2:] if signing_message_hash.startswith("0x") else signing_message_hash) + + return client.call( + messages.TonSignMessage( + address_n=n, + destination=destination, + jetton_master_address=jetton_master_address, + jetton_wallet_address=jetton_wallet_address, + ton_amount=ton_amount, + jetton_amount=jetton_amount, + jetton_amount_bytes=jetton_amount_bytes, + fwd_fee=fwd_fee, + comment=comment, + mode=mode, + seqno=seqno, + expire_at=expire_at, + version=version, + is_raw_data=is_raw_data, + wallet_id=wallet_id, + workchain=workchain, + bounceable=bounceable, + is_test_only=test_only, + ext_destination=ext_destination, + ext_ton_amount=ext_ton_amount, + ext_payload=ext_payload, + signing_message_hash=signing_message_hash + ) + ) + +@expect(messages.TonSignedProof) +def sign_proof(client: "TrezorClient", + n: "Address", + expire_at: int, + appdomain: str, + comment: str, + version: messages.TonWalletVersion=messages.TonWalletVersion.V4R2, + wallet_id: int = 698983191, + workchain: messages.TonWorkChain=messages.TonWorkChain.BASECHAIN, + bounceable: bool = False, + test_only: bool = False): + appdomain = appdomain.encode("utf-8") + if comment is not None: + comment = comment.encode("utf-8") + return client.call( + messages.TonSignProof( + address_n=n, + appdomain=appdomain, + comment=comment, + expire_at=expire_at, + version=version, + wallet_id=wallet_id, + workchain=workchain, + bounceable=bounceable, + is_test_only=test_only, + ) + ) \ No newline at end of file