diff --git a/SConscript.firmware b/SConscript.firmware index 805a98378..be4bb8f42 100644 --- a/SConscript.firmware +++ b/SConscript.firmware @@ -27,6 +27,7 @@ CPPDEFINES_MOD += [ 'RAND_PLATFORM_INDEPENDENT', ('USE_KECCAK', '1'), ('USE_ETHEREUM', '1'), + ('USE_NEM', '1'), ] SOURCE_MOD += [ 'embed/extmod/modtrezorcrypto/modtrezorcrypto.c', @@ -38,6 +39,7 @@ SOURCE_MOD += [ 'vendor/trezor-crypto/aes/aes_modes.c', 'vendor/trezor-crypto/aes/aestab.c', 'vendor/trezor-crypto/base58.c', + 'vendor/trezor-crypto/base32.c', 'vendor/trezor-crypto/bignum.c', 'vendor/trezor-crypto/bip32.c', 'vendor/trezor-crypto/bip39.c', @@ -63,6 +65,7 @@ SOURCE_MOD += [ 'vendor/trezor-crypto/hasher.c', 'vendor/trezor-crypto/hmac.c', 'vendor/trezor-crypto/memzero.c', + 'vendor/trezor-crypto/nem.c', 'vendor/trezor-crypto/nist256p1.c', 'vendor/trezor-crypto/pbkdf2.c', 'vendor/trezor-crypto/rand.c', diff --git a/SConscript.unix b/SConscript.unix index 01fdf7d18..818d511ac 100644 --- a/SConscript.unix +++ b/SConscript.unix @@ -25,6 +25,7 @@ CPPDEFINES_MOD += [ 'AES_192', ('USE_KECCAK', '1'), ('USE_ETHEREUM', '1'), + ('USE_NEM', '1'), ] SOURCE_MOD += [ 'embed/extmod/modtrezorcrypto/modtrezorcrypto.c', @@ -35,6 +36,7 @@ SOURCE_MOD += [ 'vendor/trezor-crypto/aes/aes_modes.c', 'vendor/trezor-crypto/aes/aestab.c', 'vendor/trezor-crypto/base58.c', + 'vendor/trezor-crypto/base32.c', 'vendor/trezor-crypto/bignum.c', 'vendor/trezor-crypto/bip32.c', 'vendor/trezor-crypto/bip39.c', @@ -67,6 +69,7 @@ SOURCE_MOD += [ 'vendor/trezor-crypto/secp256k1.c', 'vendor/trezor-crypto/sha2.c', 'vendor/trezor-crypto/sha3.c', + 'vendor/trezor-crypto/nem.c', ] # modtrezorio diff --git a/embed/extmod/modtrezorcrypto/modtrezorcrypto-bip32.h b/embed/extmod/modtrezorcrypto/modtrezorcrypto-bip32.h index 6fc81aa1c..1542b2173 100644 --- a/embed/extmod/modtrezorcrypto/modtrezorcrypto-bip32.h +++ b/embed/extmod/modtrezorcrypto/modtrezorcrypto-bip32.h @@ -24,6 +24,7 @@ #include "bip32.h" #include "curves.h" #include "memzero.h" +#include "nem.h" /// class HDNode: /// ''' @@ -307,12 +308,68 @@ STATIC mp_obj_t mod_trezorcrypto_HDNode_address(mp_obj_t self, mp_obj_t version) mp_obj_HDNode_t *o = MP_OBJ_TO_PTR(self); uint32_t v = trezor_obj_get_uint(version); + char address[ADDRESS_MAXLEN]; hdnode_get_address(&o->hdnode, v, address, ADDRESS_MAXLEN); return mp_obj_new_str(address, strlen(address), false); } STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_HDNode_address_obj, mod_trezorcrypto_HDNode_address); +/// def nem_address(self, network: int) -> str: +/// ''' +/// Compute a NEM address string from the HD node. +/// ''' +STATIC mp_obj_t mod_trezorcrypto_HDNode_nem_address(mp_obj_t self, mp_obj_t network) { + mp_obj_HDNode_t *o = MP_OBJ_TO_PTR(self); + + uint8_t n = trezor_obj_get_uint8(network); + + char address[NEM_ADDRESS_SIZE + 1]; // + 1 for the 0 byte + if (!hdnode_get_nem_address(&o->hdnode, n, address)) { + mp_raise_ValueError("Failed to compute a NEM address"); + } + return mp_obj_new_str_of_type(&mp_type_str, (const uint8_t *)address, strlen(address)); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_HDNode_nem_address_obj, mod_trezorcrypto_HDNode_nem_address); + +/// def nem_encrypt(self, transfer_public_key: bytes, iv: bytes, salt: bytes, payload: bytes) -> bytes: +/// ''' +/// Encrypts payload using the transfer's public key +/// ''' +STATIC mp_obj_t mod_trezorcrypto_HDNode_nem_encrypt(size_t n_args, const mp_obj_t *args) { + mp_obj_HDNode_t *o = MP_OBJ_TO_PTR(args[0]); + + mp_buffer_info_t transfer_pk; + mp_get_buffer_raise(args[1], &transfer_pk, MP_BUFFER_READ); + if (transfer_pk.len != 32) { + mp_raise_ValueError("transfer_public_key has invalid length"); + } + + mp_buffer_info_t iv; + mp_get_buffer_raise(args[2], &iv, MP_BUFFER_READ); + if (iv.len != 16) { + mp_raise_ValueError("iv has invalid length"); + } + mp_buffer_info_t salt; + mp_get_buffer_raise(args[3], &salt, MP_BUFFER_READ); + if (salt.len != NEM_SALT_SIZE) { + mp_raise_ValueError("salt has invalid length"); + } + mp_buffer_info_t payload; + mp_get_buffer_raise(args[4], &payload, MP_BUFFER_READ); + if (payload.len == 0) { + mp_raise_ValueError("payload is empty"); + } + + vstr_t vstr; + vstr_init_len(&vstr, NEM_ENCRYPTED_SIZE(payload.len)); + if (!hdnode_nem_encrypt(&o->hdnode, *(const ed25519_public_key *)transfer_pk.buf, iv.buf, salt.buf, payload.buf, payload.len, (uint8_t *)vstr.buf)) { + mp_raise_ValueError("HDNode nem encrypt failed"); + } + return mp_obj_new_str_from_vstr(&mp_type_bytes, &vstr); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_HDNode_nem_encrypt_obj, 5, 5, mod_trezorcrypto_HDNode_nem_encrypt); + /// def ethereum_pubkeyhash(self) -> bytes: /// ''' /// Compute an Ethereum pubkeyhash (aka address) from the HD node. @@ -340,6 +397,8 @@ STATIC const mp_rom_map_elem_t mod_trezorcrypto_HDNode_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_private_key), MP_ROM_PTR(&mod_trezorcrypto_HDNode_private_key_obj) }, { MP_ROM_QSTR(MP_QSTR_public_key), MP_ROM_PTR(&mod_trezorcrypto_HDNode_public_key_obj) }, { MP_ROM_QSTR(MP_QSTR_address), MP_ROM_PTR(&mod_trezorcrypto_HDNode_address_obj) }, + { MP_ROM_QSTR(MP_QSTR_nem_address), MP_ROM_PTR(&mod_trezorcrypto_HDNode_nem_address_obj) }, + { MP_ROM_QSTR(MP_QSTR_nem_encrypt), MP_ROM_PTR(&mod_trezorcrypto_HDNode_nem_encrypt_obj) }, { MP_ROM_QSTR(MP_QSTR_ethereum_pubkeyhash), MP_ROM_PTR(&mod_trezorcrypto_HDNode_ethereum_pubkeyhash_obj) }, }; STATIC MP_DEFINE_CONST_DICT(mod_trezorcrypto_HDNode_locals_dict, mod_trezorcrypto_HDNode_locals_dict_table); diff --git a/embed/extmod/modtrezorcrypto/modtrezorcrypto-ed25519.h b/embed/extmod/modtrezorcrypto/modtrezorcrypto-ed25519.h index 450d36668..683815f77 100644 --- a/embed/extmod/modtrezorcrypto/modtrezorcrypto-ed25519.h +++ b/embed/extmod/modtrezorcrypto/modtrezorcrypto-ed25519.h @@ -20,6 +20,7 @@ #include "py/objstr.h" #include "ed25519-donna/ed25519.h" +#include "ed25519-donna/ed25519-keccak.h" #include "rand.h" @@ -54,14 +55,14 @@ STATIC mp_obj_t mod_trezorcrypto_ed25519_publickey(mp_obj_t secret_key) { } STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_ed25519_publickey_obj, mod_trezorcrypto_ed25519_publickey); -/// def sign(secret_key: bytes, message: bytes) -> bytes: +/// def sign(secret_key: bytes, message: bytes, hasher: str='') -> bytes: /// ''' /// Uses secret key to produce the signature of message. /// ''' -STATIC mp_obj_t mod_trezorcrypto_ed25519_sign(mp_obj_t secret_key, mp_obj_t message) { +STATIC mp_obj_t mod_trezorcrypto_ed25519_sign(size_t n_args, const mp_obj_t *args) { mp_buffer_info_t sk, msg; - mp_get_buffer_raise(secret_key, &sk, MP_BUFFER_READ); - mp_get_buffer_raise(message, &msg, MP_BUFFER_READ); + mp_get_buffer_raise(args[0], &sk, MP_BUFFER_READ); + mp_get_buffer_raise(args[1], &msg, MP_BUFFER_READ); if (sk.len != 32) { mp_raise_ValueError("Invalid length of secret key"); } @@ -69,12 +70,26 @@ STATIC mp_obj_t mod_trezorcrypto_ed25519_sign(mp_obj_t secret_key, mp_obj_t mess mp_raise_ValueError("Empty data to sign"); } ed25519_public_key pk; - ed25519_publickey(*(const ed25519_secret_key *)sk.buf, pk); uint8_t out[64]; - ed25519_sign(msg.buf, msg.len, *(const ed25519_secret_key *)sk.buf, pk, *(ed25519_signature *)out); + mp_buffer_info_t hash_func; + + if (n_args == 3) { + mp_get_buffer_raise(args[2], &hash_func, MP_BUFFER_READ); + // if hash_func == 'keccak': + if (memcmp(hash_func.buf, "keccak", sizeof("keccak")) == 0) { + ed25519_publickey_keccak(*(const ed25519_secret_key *)sk.buf, pk); + ed25519_sign_keccak(msg.buf, msg.len, *(const ed25519_secret_key *)sk.buf, pk, *(ed25519_signature *)out); + } else { + mp_raise_ValueError("Unknown hash function"); + } + } else { + ed25519_publickey(*(const ed25519_secret_key *)sk.buf, pk); + ed25519_sign(msg.buf, msg.len, *(const ed25519_secret_key *)sk.buf, pk, *(ed25519_signature *)out); + } + return mp_obj_new_bytes(out, sizeof(out)); } -STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_ed25519_sign_obj, mod_trezorcrypto_ed25519_sign); +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_ed25519_sign_obj, 2, 3, mod_trezorcrypto_ed25519_sign); /// def verify(public_key: bytes, signature: bytes, message: bytes) -> bool: /// ''' diff --git a/embed/extmod/modtrezorcrypto/modtrezorcrypto-nem.h b/embed/extmod/modtrezorcrypto/modtrezorcrypto-nem.h new file mode 100644 index 000000000..57d199029 --- /dev/null +++ b/embed/extmod/modtrezorcrypto/modtrezorcrypto-nem.h @@ -0,0 +1,69 @@ +/* + * This file is part of the TREZOR project, https://trezor.io/ + * + * Copyright (c) SatoshiLabs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "py/objstr.h" + +#include "embed/extmod/trezorobj.h" + +#include "nem.h" + +/// def validate_address(address: str, network: int) -> bool: +/// ''' +/// Validate a NEM address +/// ''' +STATIC mp_obj_t mod_trezorcrypto_nem_validate_address(mp_obj_t address, mp_obj_t network) { + + mp_buffer_info_t addr; + mp_get_buffer_raise(address, &addr, MP_BUFFER_READ); + + uint32_t n = trezor_obj_get_uint(network); + return mp_obj_new_bool(nem_validate_address(addr.buf, n)); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_nem_validate_address_obj, mod_trezorcrypto_nem_validate_address); + +/// def compute_address(public_key: bytes, network: int) -> str: +/// ''' +/// Compute a NEM address from a public key +/// ''' +STATIC mp_obj_t mod_trezorcrypto_nem_compute_address(mp_obj_t public_key, mp_obj_t network) { + mp_buffer_info_t p; + mp_get_buffer_raise(public_key, &p, MP_BUFFER_READ); + + uint32_t n = trezor_obj_get_uint(network); + + char address[NEM_ADDRESS_SIZE + 1]; // + 1 for the 0 byte + if (!nem_get_address(p.buf, n, address)) { + mp_raise_ValueError("Failed to compute a NEM address from provided public key"); + } + return mp_obj_new_str_of_type(&mp_type_str, (const uint8_t *)address, strlen(address)); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_nem_compute_address_obj, mod_trezorcrypto_nem_compute_address); + +// objects definition +STATIC const mp_rom_map_elem_t mod_trezorcrypto_nem_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR_validate_address), MP_ROM_PTR(&mod_trezorcrypto_nem_validate_address_obj) }, + { MP_ROM_QSTR(MP_QSTR_compute_address), MP_ROM_PTR(&mod_trezorcrypto_nem_compute_address_obj) }, +}; +STATIC MP_DEFINE_CONST_DICT(mod_trezorcrypto_nem_globals, mod_trezorcrypto_nem_globals_table); + +// module definition +STATIC const mp_obj_module_t mod_trezorcrypto_nem_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t*)&mod_trezorcrypto_nem_globals, +}; diff --git a/embed/extmod/modtrezorcrypto/modtrezorcrypto.c b/embed/extmod/modtrezorcrypto/modtrezorcrypto.c index 466a409b4..de107b881 100644 --- a/embed/extmod/modtrezorcrypto/modtrezorcrypto.c +++ b/embed/extmod/modtrezorcrypto/modtrezorcrypto.c @@ -36,6 +36,7 @@ #include "modtrezorcrypto-curve25519.h" #include "modtrezorcrypto-ed25519.h" #include "modtrezorcrypto-nist256p1.h" +#include "modtrezorcrypto-nem.h" #include "modtrezorcrypto-pbkdf2.h" #include "modtrezorcrypto-random.h" #include "modtrezorcrypto-rfc6979.h" @@ -60,6 +61,7 @@ STATIC const mp_rom_map_elem_t mp_module_trezorcrypto_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_curve25519), MP_ROM_PTR(&mod_trezorcrypto_curve25519_module) }, { MP_ROM_QSTR(MP_QSTR_ed25519), MP_ROM_PTR(&mod_trezorcrypto_ed25519_module) }, { MP_ROM_QSTR(MP_QSTR_nist256p1), MP_ROM_PTR(&mod_trezorcrypto_nist256p1_module) }, + { MP_ROM_QSTR(MP_QSTR_nem), MP_ROM_PTR(&mod_trezorcrypto_nem_module) }, { MP_ROM_QSTR(MP_QSTR_pbkdf2), MP_ROM_PTR(&mod_trezorcrypto_Pbkdf2_type) }, { MP_ROM_QSTR(MP_QSTR_random), MP_ROM_PTR(&mod_trezorcrypto_random_module) }, { MP_ROM_QSTR(MP_QSTR_rfc6979), MP_ROM_PTR(&mod_trezorcrypto_Rfc6979_type) }, diff --git a/mocks/generated/trezorconfig.py b/mocks/generated/trezorconfig.py index ab1462b94..376b5184e 100644 --- a/mocks/generated/trezorconfig.py +++ b/mocks/generated/trezorconfig.py @@ -7,6 +7,12 @@ def init() -> None: called from this module! ''' +# extmod/modtrezorconfig/modtrezorconfig.c +def check_pin(pin: int, waitcallback: (int, int -> None)) -> bool: + ''' + Check the given PIN. Returns True on success, False on failure. + ''' + # extmod/modtrezorconfig/modtrezorconfig.c def unlock(pin: int, waitcallback: (int, int -> None)) -> bool: ''' diff --git a/mocks/generated/trezorcrypto.py b/mocks/generated/trezorcrypto.py index af87455a7..62d89b923 100644 --- a/mocks/generated/trezorcrypto.py +++ b/mocks/generated/trezorcrypto.py @@ -93,6 +93,16 @@ class HDNode: Compute a base58-encoded address string from the HD node. ''' + def nem_address(self, network: int) -> str: + ''' + Compute a NEM address string from the HD node. + ''' + + def nem_encrypt(self, transfer_public_key: bytes, iv: bytes, salt: bytes, payload: bytes) -> bytes: + ''' + Encrypts payload using the transfer's public key + ''' + def ethereum_pubkeyhash(self) -> bytes: ''' Compute an Ethereum pubkeyhash (aka address) from the HD node. @@ -274,7 +284,7 @@ def publickey(secret_key: bytes) -> bytes: ''' # extmod/modtrezorcrypto/modtrezorcrypto-ed25519.h -def sign(secret_key: bytes, message: bytes) -> bytes: +def sign(secret_key: bytes, message: bytes, hasher: str='') -> bytes: ''' Uses secret key to produce the signature of message. ''' @@ -304,6 +314,18 @@ def cosi_sign(secret_key: bytes, message: bytes, nonce: bytes, sigR: bytes, comb Produce signature of message using COSI cosigning scheme. ''' +# extmod/modtrezorcrypto/modtrezorcrypto-nem.h +def validate_address(address: str, network: int) -> bool: + ''' + Validate a NEM address + ''' + +# extmod/modtrezorcrypto/modtrezorcrypto-nem.h +def compute_address(public_key: bytes, network: int) -> str: + ''' + Compute a NEM address from a public key + ''' + # extmod/modtrezorcrypto/modtrezorcrypto-nist256p1.h def generate_secret() -> bytes: ''' diff --git a/mocks/generated/trezorio.py b/mocks/generated/trezorio.py index 0c79c832c..e816c89c0 100644 --- a/mocks/generated/trezorio.py +++ b/mocks/generated/trezorio.py @@ -128,14 +128,17 @@ class USB: ''' def __init__(self, + device_class: int=0, + device_subclass: int=0, + device_protocol: int=0, vendor_id: int, product_id: int, release_num: int, manufacturer: str='', product: str='', serial_number: str='', - configuration: str='', - interface: str='') -> None: + interface: str='', + usb21_enabled: bool=True) -> None: ''' ''' diff --git a/mocks/generated/trezorui.py b/mocks/generated/trezorui.py index acec41492..16392fd67 100644 --- a/mocks/generated/trezorui.py +++ b/mocks/generated/trezorui.py @@ -112,7 +112,7 @@ class Display: Call without the xy parameter to just perform the read of the value. ''' - def save(self, filename: str) -> None: + def save(self, prefix: str) -> None: ''' - Saves current display contents to file filename. + Saves current display contents to PNG file with given prefix. ''' diff --git a/mocks/generated/trezorutils.py b/mocks/generated/trezorutils.py index fefe5d3ff..8c7a3763c 100644 --- a/mocks/generated/trezorutils.py +++ b/mocks/generated/trezorutils.py @@ -24,3 +24,21 @@ def halt(msg: str = None) -> None: ''' Halts execution. ''' + +# extmod/modtrezorutils/modtrezorutils.c +def set_mode_unprivileged() -> None: + ''' + Set unprivileged mode. + ''' + +# extmod/modtrezorutils/modtrezorutils.c +def symbol(name: str) -> str/int/None: + ''' + Retrieve internal symbol. + ''' + +# extmod/modtrezorutils/modtrezorutils.c +def model() -> str: + ''' + Return which hardware model we are running on. + ''' diff --git a/pytest.ini b/pytest.ini index 0f83d26ca..8f2bb1509 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ [pytest] addopts = --pyargs trezorlib.tests.device_tests xfail_strict = true -run_xfail = lisk +run_xfail = lisk nem diff --git a/src/apps/nem/README.md b/src/apps/nem/README.md new file mode 100644 index 000000000..0dace162f --- /dev/null +++ b/src/apps/nem/README.md @@ -0,0 +1,64 @@ +# NEM + +MAINTAINER = Tomas Susanka + +AUTHOR = Tomas Susanka + +REVIEWER = Jan Pochyla + +ADVISORS = Grégory Saive, Saleem Rashid + +----- + +This implementation of NEM for Trezor Core is mostly based on the trezor-mcu C implementation by Saleem Rashid. The protobuf messages are heavily inspired by the [NEM NSI API](https://nemproject.github.io/). + +You can read a lot about NEM in the [Technical Reference paper](https://nem.io/wp-content/themes/nem/files/NEM_techRef.pdf). + +This app supports number of NEM services (transfers, mosaics, namespaces and multisig), also called _transactions_ as the general term. Each of those services is divided into corresponding folders. In those folders we use `serialize.py` for the transaction serialization and `layout.py` to display data and to require user interactions. + +In this app we support the following: + +### Mosaics + +You can read more on mosaics [here](https://blog.nem.io/mosaics-and-namespaces-2/). Each mosaic has a name and lives under certain namespace, this identification _namespace.mosaic_ is unique in the network. + +Trezor Core supports mosaic creation and changing the mosaic's supply. + +### Namespaces + +You can read more on namespaces at the [same link](https://blog.nem.io/mosaics-and-namespaces-2/). Namespace creation is supported. + +### Transfers + +There is a number of things the term _transfer_ may refer to: + +##### Regular transfers + +The traditional transfer of the native NEM coins – XEM. + +##### Mosaic transfers + +Except XEM you can also transfer mosaics. Mosaics are _attached_ to a regular transfer. + +Each such attached mosaic has a `quantity` denoting the amount of such mosaic to be transferred. There is a catch though: the actual amount of the mosaic to be transferred isn't `quantity` but `mosaic.quantity * transfer.amount`. In other words, the quantity is multiplied by the `amount` field in transfer. This is most likely due to backwards compatibility where transfers with amount 0 where discarded. + +You can also transfer XEM and mosaics at the same time. In that case you need to attach the nem.xem mosaic. From the user point of view Trezor shows this as a regular XEM transfer. + +##### Importance transfer + +Importance transfer is a special kind of transaction, which enables (or disables) delegated harvesting. + +### Multisig + +NEM supports multisig accounts. First you convert an account into a multisig and add cosignatories. After that any cosignatory can initiate a multisig transaction. + +Multisig is a wrapper, so you can use any of the services mentioned above wrapped into a multisig transaction requiring n out of m signatures. + +A common scenario to test out multisig with NanoWallet might be: + +- Create a simple account without Trezor (account A) +- Create another account with Trezor One (account B) +- Convert A to a multisig with B as a cosigner +- Create another account with Trezor T (account C) +- Add C as a cosigner +- Try to send a transaction where B and C cosigns diff --git a/src/apps/nem/__init__.py b/src/apps/nem/__init__.py new file mode 100644 index 000000000..483d795f8 --- /dev/null +++ b/src/apps/nem/__init__.py @@ -0,0 +1,17 @@ +from trezor.wire import register, protobuf_workflow +from trezor.messages.wire_types import NEMGetAddress, NEMSignTx + + +def dispatch_NemGetAddress(*args, **kwargs): + from .get_address import get_address + return get_address(*args, **kwargs) + + +def dispatch_NemSignTx(*args, **kwargs): + from .signing import sign_tx + return sign_tx(*args, **kwargs) + + +def boot(): + register(NEMGetAddress, protobuf_workflow, dispatch_NemGetAddress) + register(NEMSignTx, protobuf_workflow, dispatch_NemSignTx) diff --git a/src/apps/nem/get_address.py b/src/apps/nem/get_address.py new file mode 100644 index 000000000..b1d2f8427 --- /dev/null +++ b/src/apps/nem/get_address.py @@ -0,0 +1,32 @@ +from trezor import ui +from trezor.messages import ButtonRequestType +from trezor.messages.NEMAddress import NEMAddress +from trezor.ui.text import Text + +from apps.common import seed +from apps.common.confirm import require_confirm + +from .layout import split_address +from .helpers import get_network_str, NEM_CURVE +from .validators import validate_network + + +async def get_address(ctx, msg): + network = validate_network(msg.network) + node = await seed.derive_node(ctx, msg.address_n, NEM_CURVE) + address = node.nem_address(network) + + if msg.show_display: + await _require_confirm_address(ctx, address, network) + + return NEMAddress(address=address) + + +async def _require_confirm_address(ctx, address: str, network: int): + lines = split_address(address) + content = Text( + 'Export NEM address', ui.ICON_RECEIVE, + ui.NORMAL, '%s network' % get_network_str(network), + ui.MONO, *lines, + icon_color=ui.GREEN) + await require_confirm(ctx, content, code=ButtonRequestType.Address) diff --git a/src/apps/nem/helpers.py b/src/apps/nem/helpers.py new file mode 100644 index 000000000..cc2b4ad68 --- /dev/null +++ b/src/apps/nem/helpers.py @@ -0,0 +1,37 @@ +from micropython import const + +NEM_NETWORK_MAINNET = const(0x68) +NEM_NETWORK_TESTNET = const(0x98) +NEM_NETWORK_MIJIN = const(0x60) +NEM_CURVE = 'ed25519-keccak' + +NEM_TRANSACTION_TYPE_TRANSFER = const(0x0101) +NEM_TRANSACTION_TYPE_IMPORTANCE_TRANSFER = const(0x0801) +NEM_TRANSACTION_TYPE_AGGREGATE_MODIFICATION = const(0x1001) +NEM_TRANSACTION_TYPE_MULTISIG_SIGNATURE = const(0x1002) +NEM_TRANSACTION_TYPE_MULTISIG = const(0x1004) +NEM_TRANSACTION_TYPE_PROVISION_NAMESPACE = const(0x2001) +NEM_TRANSACTION_TYPE_MOSAIC_CREATION = const(0x4001) +NEM_TRANSACTION_TYPE_MOSAIC_SUPPLY_CHANGE = const(0x4002) + +NEM_MAX_DIVISIBILITY = const(6) +NEM_MAX_SUPPLY = const(9000000000) + +NEM_SALT_SIZE = const(32) +AES_BLOCK_SIZE = const(16) +NEM_HASH_ALG = 'keccak' +NEM_PUBLIC_KEY_SIZE = const(32) # ed25519 public key +NEM_LEVY_PERCENTILE_DIVISOR_ABSOLUTE = const(10000) +NEM_MOSAIC_AMOUNT_DIVISOR = const(1000000) + +NEM_MAX_PLAIN_PAYLOAD_SIZE = const(1024) +NEM_MAX_ENCRYPTED_PAYLOAD_SIZE = const(960) + + +def get_network_str(network: int) -> str: + if network == NEM_NETWORK_MAINNET: + return 'Mainnet' + elif network == NEM_NETWORK_TESTNET: + return 'Testnet' + elif network == NEM_NETWORK_MIJIN: + return 'Mijin' diff --git a/src/apps/nem/layout.py b/src/apps/nem/layout.py new file mode 100644 index 000000000..ce9a64a86 --- /dev/null +++ b/src/apps/nem/layout.py @@ -0,0 +1,47 @@ +from trezor import ui +from trezor.messages import ButtonRequestType +from trezor.ui.text import Text +from trezor.utils import chunks, format_amount, split_words + +from apps.common.confirm import require_confirm, require_hold_to_confirm + +from .helpers import NEM_MAX_DIVISIBILITY + + +async def require_confirm_text(ctx, action: str): + words = split_words(action, 18) + await require_confirm_content(ctx, 'Confirm action', words) + + +async def require_confirm_fee(ctx, action: str, fee: int): + content = ( + ui.NORMAL, action, + ui.BOLD, '%s XEM' % format_amount(fee, NEM_MAX_DIVISIBILITY), + ) + await require_confirm_content(ctx, 'Confirm fee', content) + + +async def require_confirm_content(ctx, headline: str, content: list): + text = Text(headline, ui.ICON_SEND, *content, icon_color=ui.GREEN) + await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) + + +async def require_confirm_final(ctx, fee: int): + content = Text( + 'Final confirm', ui.ICON_SEND, + ui.NORMAL, 'Sign this transaction', + ui.BOLD, 'and pay %s XEM' % format_amount(fee, NEM_MAX_DIVISIBILITY), + ui.NORMAL, 'for network fee?', + icon_color=ui.GREEN) + # we use SignTx, not ConfirmOutput, for compatibility with T1 + await require_hold_to_confirm(ctx, content, ButtonRequestType.SignTx) + + +def split_address(address: str): + return chunks(address, 17) + + +def trim(payload: str, length: int) -> str: + if len(payload) > length: + return payload[:length] + '..' + return payload diff --git a/src/apps/nem/mosaic/__init__.py b/src/apps/nem/mosaic/__init__.py new file mode 100644 index 000000000..da6ba45d5 --- /dev/null +++ b/src/apps/nem/mosaic/__init__.py @@ -0,0 +1,15 @@ +from trezor.messages.NEMTransactionCommon import NEMTransactionCommon +from trezor.messages.NEMMosaicCreation import NEMMosaicCreation +from trezor.messages.NEMMosaicSupplyChange import NEMMosaicSupplyChange + +from . import layout, serialize + + +async def mosaic_creation(ctx, public_key: bytes, common: NEMTransactionCommon, creation: NEMMosaicCreation) -> bytearray: + await layout.ask_mosaic_creation(ctx, common, creation) + return serialize.serialize_mosaic_creation(common, creation, public_key) + + +async def supply_change(ctx, public_key: bytes, common: NEMTransactionCommon, change: NEMMosaicSupplyChange) -> bytearray: + await layout.ask_supply_change(ctx, common, change) + return serialize.serialize_mosaic_supply_change(common, change, public_key) diff --git a/src/apps/nem/mosaic/helpers.py b/src/apps/nem/mosaic/helpers.py new file mode 100644 index 000000000..af56432a7 --- /dev/null +++ b/src/apps/nem/mosaic/helpers.py @@ -0,0 +1,15 @@ +from .nem_mosaics import mosaics + + +def get_mosaic_definition(namespace_name: str, mosaic_name: str, network: int) -> dict: + for m in mosaics: + if namespace_name == m["namespace"] and mosaic_name == m["mosaic"]: + if ("networks" not in m) or (network in m["networks"]): + return m + return None + + +def is_nem_xem_mosaic(namespace_name: str, mosaic_name: str) -> bool: + if namespace_name == "nem" and mosaic_name == "xem": + return True + return False diff --git a/src/apps/nem/mosaic/layout.py b/src/apps/nem/mosaic/layout.py new file mode 100644 index 000000000..2670dcdcb --- /dev/null +++ b/src/apps/nem/mosaic/layout.py @@ -0,0 +1,123 @@ +from micropython import const +from trezor import ui +from trezor.messages import (NEMMosaicCreation, NEMMosaicDefinition, + NEMMosaicLevy, NEMMosaicSupplyChange, + NEMSupplyChangeType, NEMTransactionCommon) +from trezor.ui.confirm import ConfirmDialog +from trezor.ui.scroll import Scrollpage, animate_swipe, paginate +from trezor.ui.text import Text +from trezor.utils import split_words + +from ..layout import (require_confirm_content, require_confirm_fee, + require_confirm_final, require_confirm_text, + split_address, trim) + + +async def ask_mosaic_creation(ctx, common: NEMTransactionCommon, creation: NEMMosaicCreation): + await require_confirm_content(ctx, 'Create mosaic', _creation_message(creation)) + await _require_confirm_properties(ctx, creation.definition) + await require_confirm_fee(ctx, 'Confirm creation fee', creation.fee) + + await require_confirm_final(ctx, common.fee) + + +async def ask_supply_change(ctx, common: NEMTransactionCommon, change: NEMMosaicSupplyChange): + await require_confirm_content(ctx, 'Supply change', _supply_message(change)) + if change.type == NEMSupplyChangeType.SupplyChange_Decrease: + msg = 'Decrease supply by ' + str(change.delta) + ' whole units?' + elif change.type == NEMSupplyChangeType.SupplyChange_Increase: + msg = 'Increase supply by ' + str(change.delta) + ' whole units?' + else: + raise ValueError('Invalid supply change type') + await require_confirm_text(ctx, msg) + + await require_confirm_final(ctx, common.fee) + + +def _creation_message(mosaic_creation): + return [ui.NORMAL, 'Create mosaic', + ui.BOLD, mosaic_creation.definition.mosaic, + ui.NORMAL, 'under namespace', + ui.BOLD, mosaic_creation.definition.namespace] + + +def _supply_message(supply_change): + return [ui.NORMAL, 'Modify supply for', + ui.BOLD, supply_change.mosaic, + ui.NORMAL, 'under namespace', + ui.BOLD, supply_change.namespace] + + +async def _require_confirm_properties(ctx, definition: NEMMosaicDefinition): + properties = _get_mosaic_properties(definition) + first_page = const(0) + paginator = paginate(_show_page, len(properties), first_page, properties) + await ctx.wait(paginator) + + +@ui.layout +async def _show_page(page: int, page_count: int, content): + content = Scrollpage(content[page], page, page_count) + if page + 1 == page_count: + await ConfirmDialog(content) + else: + content.render() + await animate_swipe() + + +def _get_mosaic_properties(definition: NEMMosaicDefinition): + properties = [] + if definition.description: + t = Text('Confirm properties', ui.ICON_SEND, + ui.BOLD, 'Description:', + ui.NORMAL, *split_words(trim(definition.description, 70), 22)) + properties.append(t) + if definition.transferable: + transferable = 'Yes' + else: + transferable = 'No' + t = Text('Confirm properties', ui.ICON_SEND, + ui.BOLD, 'Transferable?', + ui.NORMAL, transferable) + properties.append(t) + if definition.mutable_supply: + imm = 'mutable' + else: + imm = 'immutable' + if definition.supply: + t = Text('Confirm properties', ui.ICON_SEND, + ui.BOLD, 'Initial supply:', + ui.NORMAL, str(definition.supply), + ui.NORMAL, imm) + else: + t = Text('Confirm properties', ui.ICON_SEND, + ui.BOLD, 'Initial supply:', + ui.NORMAL, imm) + properties.append(t) + if definition.levy: + t = Text('Confirm properties', ui.ICON_SEND, + ui.BOLD, 'Levy recipient:', + ui.MONO, *split_address(definition.levy_address)) + properties.append(t) + t = Text('Confirm properties', ui.ICON_SEND, + ui.BOLD, 'Levy fee:', + ui.NORMAL, str(definition.fee), + ui.BOLD, 'Levy divisibility:', + ui.NORMAL, str(definition.divisibility)) + properties.append(t) + t = Text('Confirm properties', ui.ICON_SEND, + ui.BOLD, 'Levy namespace:', + ui.NORMAL, definition.levy_namespace, + ui.BOLD, 'Levy mosaic:', + ui.NORMAL, definition.levy_mosaic) + properties.append(t) + if definition.levy == NEMMosaicLevy.MosaicLevy_Absolute: + levy_type = 'absolute' + else: + levy_type = 'percentile' + t = Text('Confirm properties', ui.ICON_SEND, + ui.BOLD, 'Levy type:', + ui.NORMAL, levy_type) + properties.append(t) + + return properties diff --git a/src/apps/nem/mosaic/nem_mosaics.py b/src/apps/nem/mosaic/nem_mosaics.py new file mode 100644 index 000000000..94d70caaa --- /dev/null +++ b/src/apps/nem/mosaic/nem_mosaics.py @@ -0,0 +1,59 @@ +# generated using gen_nem_mosaics.py from trezor-common nem_mosaics.json - do not edit directly! + +mosaics = [ + { + "name": "XEM", + "ticker": " XEM", + "namespace": "nem", + "mosaic": "xem", + "divisibility": 6, + }, + { + "name": "DIMCOIN", + "ticker": " DIM", + "namespace": "dim", + "mosaic": "coin", + "divisibility": 6, + "levy": "MosaicLevy_Percentile", + "fee": 10, + "levy_namespace": "dim", + "levy_mosaic": "coin", + "networks": [104], + }, + { + "name": "DIM TOKEN", + "ticker": " DIMTOK", + "namespace": "dim", + "mosaic": "token", + "divisibility": 6, + "networks": [104], + }, + { + "name": "Breeze Token", + "ticker": " BREEZE", + "namespace": "breeze", + "mosaic": "breeze-token", + "divisibility": 0, + "networks": [104], + }, + { + "name": "PacNEM Game Credits", + "ticker": " PAC:HRT", + "namespace": "pacnem", + "mosaic": "heart", + "divisibility": 0, + "networks": [104], + }, + { + "name": "PacNEM Score Tokens", + "ticker": " PAC:CHS", + "namespace": "pacnem", + "mosaic": "cheese", + "divisibility": 6, + "levy": "MosaicLevy_Percentile", + "fee": 100, + "levy_namespace": "nem", + "levy_mosaic": "xem", + "networks": [104], + }, +] diff --git a/src/apps/nem/mosaic/serialize.py b/src/apps/nem/mosaic/serialize.py new file mode 100644 index 000000000..aee2a97d4 --- /dev/null +++ b/src/apps/nem/mosaic/serialize.py @@ -0,0 +1,79 @@ +from trezor.messages.NEMMosaicCreation import NEMMosaicCreation +from trezor.messages.NEMMosaicSupplyChange import NEMMosaicSupplyChange +from trezor.messages.NEMTransactionCommon import NEMTransactionCommon + +from ..helpers import (NEM_TRANSACTION_TYPE_MOSAIC_CREATION, + NEM_TRANSACTION_TYPE_MOSAIC_SUPPLY_CHANGE) +from ..writers import (write_bytes_with_length, write_common, write_uint32, + write_uint64) + + +def serialize_mosaic_creation(common: NEMTransactionCommon, creation: NEMMosaicCreation, public_key: bytes): + w = write_common(common, bytearray(public_key), NEM_TRANSACTION_TYPE_MOSAIC_CREATION) + + mosaics_w = bytearray() + write_bytes_with_length(mosaics_w, bytearray(public_key)) + identifier_length = 4 + len(creation.definition.namespace) + 4 + len(creation.definition.mosaic) + write_uint32(mosaics_w, identifier_length) + write_bytes_with_length(mosaics_w, bytearray(creation.definition.namespace)) + write_bytes_with_length(mosaics_w, bytearray(creation.definition.mosaic)) + write_bytes_with_length(mosaics_w, bytearray(creation.definition.description)) + write_uint32(mosaics_w, 4) # number of properties + + _write_property(mosaics_w, 'divisibility', creation.definition.divisibility) + _write_property(mosaics_w, 'initialSupply', creation.definition.supply) + _write_property(mosaics_w, 'supplyMutable', creation.definition.mutable_supply) + _write_property(mosaics_w, 'transferable', creation.definition.transferable) + + if creation.definition.levy: + levy_identifier_length = 4 + len(creation.definition.levy_namespace) + 4 + len(creation.definition.levy_mosaic) + write_uint32(mosaics_w, 4 + 4 + len(creation.definition.levy_address) + 4 + levy_identifier_length + 8) + write_uint32(mosaics_w, creation.definition.levy) + write_bytes_with_length(mosaics_w, bytearray(creation.definition.levy_address)) + write_uint32(mosaics_w, levy_identifier_length) + write_bytes_with_length(mosaics_w, bytearray(creation.definition.levy_namespace)) + write_bytes_with_length(mosaics_w, bytearray(creation.definition.levy_mosaic)) + write_uint64(mosaics_w, creation.definition.fee) + else: + write_uint32(mosaics_w, 0) + + # write mosaic bytes with length + write_bytes_with_length(w, mosaics_w) + + write_bytes_with_length(w, bytearray(creation.sink)) + write_uint64(w, creation.fee) + + return w + + +def serialize_mosaic_supply_change(common: NEMTransactionCommon, change: NEMMosaicSupplyChange, public_key: bytes): + w = write_common(common, bytearray(public_key), NEM_TRANSACTION_TYPE_MOSAIC_SUPPLY_CHANGE) + + identifier_length = 4 + len(change.namespace) + 4 + len(change.mosaic) + write_uint32(w, identifier_length) + write_bytes_with_length(w, bytearray(change.namespace)) + write_bytes_with_length(w, bytearray(change.mosaic)) + + write_uint32(w, change.type) + write_uint64(w, change.delta) + return w + + +def _write_property(w: bytearray, name: str, value): + if value is None: + if name in ('divisibility', 'initialSupply'): + value = 0 + elif name in ('supplyMutable', 'transferable'): + value = False + if type(value) == bool: + if value: + value = 'true' + else: + value = 'false' + elif type(value) == int: + value = str(value) + elif type(value) != str: + raise ValueError('Incompatible value type') + write_uint32(w, 4 + len(name) + 4 + len(value)) + write_bytes_with_length(w, bytearray(name)) + write_bytes_with_length(w, bytearray(value)) diff --git a/src/apps/nem/multisig/__init__.py b/src/apps/nem/multisig/__init__.py new file mode 100644 index 000000000..2f98386f3 --- /dev/null +++ b/src/apps/nem/multisig/__init__.py @@ -0,0 +1,33 @@ +from trezor.messages.NEMAggregateModification import NEMAggregateModification +from trezor.messages.NEMSignTx import NEMSignTx +from trezor.messages.NEMTransactionCommon import NEMTransactionCommon + +from . import layout, serialize + + +async def ask(ctx, msg: NEMSignTx): + await layout.ask_multisig(ctx, msg) + + +def initiate(public_key, common: NEMTransactionCommon, inner_tx: bytes) -> bytes: + return serialize.serialize_multisig(common, public_key, inner_tx) + + +def cosign(public_key, common: NEMTransactionCommon, inner_tx: bytes, signer: bytes) -> bytes: + return serialize.serialize_multisig_signature(common, public_key, inner_tx, signer) + + +async def aggregate_modification(ctx, + public_key: bytes, + common: NEMTransactionCommon, + aggr: NEMAggregateModification, + multisig: bool): + await layout.ask_aggregate_modification(ctx, common, aggr, multisig) + w = serialize.serialize_aggregate_modification(common, aggr, public_key) + + for m in aggr.modifications: + serialize.serialize_cosignatory_modification(w, m.type, m.public_key) + + if aggr.relative_change: + serialize.serialize_minimum_cosignatories(w, aggr.relative_change) + return w diff --git a/src/apps/nem/multisig/layout.py b/src/apps/nem/multisig/layout.py new file mode 100644 index 000000000..ff800dde0 --- /dev/null +++ b/src/apps/nem/multisig/layout.py @@ -0,0 +1,49 @@ +from trezor import ui +from trezor.crypto import nem +from trezor.messages import (ButtonRequestType, NEMAggregateModification, + NEMModificationType, NEMSignTx, + NEMTransactionCommon) +from trezor.ui.text import Text + +from ..layout import (require_confirm, require_confirm_fee, + require_confirm_final, require_confirm_text, + split_address) + + +async def ask_multisig(ctx, msg: NEMSignTx): + address = nem.compute_address(msg.multisig.signer, msg.transaction.network) + if msg.cosigning: + await _require_confirm_address(ctx, 'Cosign transaction for', address) + else: + await _require_confirm_address(ctx, 'Initiate transaction for', address) + await require_confirm_fee(ctx, 'Confirm multisig fee', msg.transaction.fee) + + +async def ask_aggregate_modification(ctx, common: NEMTransactionCommon, mod: NEMAggregateModification, multisig: bool): + if not multisig: + await require_confirm_text(ctx, 'Convert account to multisig account?') + + for m in mod.modifications: + if m.type == NEMModificationType.CosignatoryModification_Add: + action = 'Add' + else: + action = 'Remove' + address = nem.compute_address(m.public_key, common.network) + await _require_confirm_address(ctx, action + ' cosignatory', address) + + if mod.relative_change: + if multisig: + action = 'Modify the number of cosignatories by ' + else: + action = 'Set minimum cosignatories to ' + await require_confirm_text(ctx, action + str(mod.relative_change) + '?') + + await require_confirm_final(ctx, common.fee) + + +async def _require_confirm_address(ctx, action: str, address: str): + content = Text('Confirm address', ui.ICON_SEND, + ui.NORMAL, action, + ui.MONO, *split_address(address), + icon_color=ui.GREEN) + await require_confirm(ctx, content, ButtonRequestType.ConfirmOutput) diff --git a/src/apps/nem/multisig/serialize.py b/src/apps/nem/multisig/serialize.py new file mode 100644 index 000000000..98358fa9c --- /dev/null +++ b/src/apps/nem/multisig/serialize.py @@ -0,0 +1,51 @@ +from trezor.crypto import hashlib, nem +from trezor.messages.NEMAggregateModification import NEMAggregateModification +from trezor.messages.NEMTransactionCommon import NEMTransactionCommon + +from ..helpers import (NEM_TRANSACTION_TYPE_AGGREGATE_MODIFICATION, + NEM_TRANSACTION_TYPE_MULTISIG, + NEM_TRANSACTION_TYPE_MULTISIG_SIGNATURE) +from ..writers import write_bytes_with_length, write_common, write_uint32 + + +def serialize_multisig(common: NEMTransactionCommon, public_key: bytes, inner: bytes): + w = write_common(common, bytearray(public_key), NEM_TRANSACTION_TYPE_MULTISIG) + write_bytes_with_length(w, bytearray(inner)) + return w + + +def serialize_multisig_signature(common: NEMTransactionCommon, public_key: bytes, + inner: bytes, address_public_key: bytes): + address = nem.compute_address(address_public_key, common.network) + w = write_common(common, bytearray(public_key), NEM_TRANSACTION_TYPE_MULTISIG_SIGNATURE) + digest = hashlib.sha3_256(inner).digest(True) + + write_uint32(w, 4 + len(digest)) + write_bytes_with_length(w, digest) + write_bytes_with_length(w, address) + return w + + +def serialize_aggregate_modification(common: NEMTransactionCommon, mod: NEMAggregateModification, public_key: bytes): + version = common.network << 24 | 1 + if mod.relative_change: + version = common.network << 24 | 2 + + w = write_common(common, + bytearray(public_key), + NEM_TRANSACTION_TYPE_AGGREGATE_MODIFICATION, + version) + write_uint32(w, len(mod.modifications)) + return w + + +def serialize_cosignatory_modification(w: bytearray, type: int, cosignatory_pubkey: bytes): + write_uint32(w, 4 + 4 + len(cosignatory_pubkey)) + write_uint32(w, type) + write_bytes_with_length(w, bytearray(cosignatory_pubkey)) + return w + + +def serialize_minimum_cosignatories(w: bytearray, relative_change: int): + write_uint32(w, 4) + write_uint32(w, relative_change) diff --git a/src/apps/nem/namespace/__init__.py b/src/apps/nem/namespace/__init__.py new file mode 100644 index 000000000..662c30b5c --- /dev/null +++ b/src/apps/nem/namespace/__init__.py @@ -0,0 +1,9 @@ +from trezor.messages.NEMTransactionCommon import NEMTransactionCommon +from trezor.messages.NEMProvisionNamespace import NEMProvisionNamespace + +from . import layout, serialize + + +async def namespace(ctx, public_key: bytes, common: NEMTransactionCommon, namespace: NEMProvisionNamespace) -> bytearray: + await layout.ask_provision_namespace(ctx, common, namespace) + return serialize.serialize_provision_namespace(common, namespace, public_key) diff --git a/src/apps/nem/namespace/layout.py b/src/apps/nem/namespace/layout.py new file mode 100644 index 000000000..012cdec93 --- /dev/null +++ b/src/apps/nem/namespace/layout.py @@ -0,0 +1,22 @@ +from trezor import ui +from trezor.messages import NEMProvisionNamespace, NEMTransactionCommon + +from ..layout import (require_confirm_content, require_confirm_fee, + require_confirm_final) + + +async def ask_provision_namespace(ctx, common: NEMTransactionCommon, namespace: NEMProvisionNamespace): + if namespace.parent: + content = (ui.NORMAL, 'Create namespace', + ui.BOLD, namespace.namespace, + ui.NORMAL, 'under namespace', + ui.BOLD, namespace.parent) + await require_confirm_content(ctx, 'Confirm namespace', content) + else: + content = (ui.NORMAL, 'Create namespace', + ui.BOLD, namespace.namespace) + await require_confirm_content(ctx, 'Confirm namespace', content) + + await require_confirm_fee(ctx, 'Confirm rental fee', namespace.fee) + + await require_confirm_final(ctx, common.fee) diff --git a/src/apps/nem/namespace/serialize.py b/src/apps/nem/namespace/serialize.py new file mode 100644 index 000000000..0cf6fd31b --- /dev/null +++ b/src/apps/nem/namespace/serialize.py @@ -0,0 +1,21 @@ +from trezor.messages.NEMProvisionNamespace import NEMProvisionNamespace +from trezor.messages.NEMTransactionCommon import NEMTransactionCommon + +from ..helpers import NEM_TRANSACTION_TYPE_PROVISION_NAMESPACE +from ..writers import write_bytes_with_length, write_common, write_uint32, write_uint64 + + +def serialize_provision_namespace(common: NEMTransactionCommon, namespace: NEMProvisionNamespace, public_key: bytes) -> bytearray: + tx = write_common(common, + bytearray(public_key), + NEM_TRANSACTION_TYPE_PROVISION_NAMESPACE) + + write_bytes_with_length(tx, bytearray(namespace.sink)) + write_uint64(tx, namespace.fee) + write_bytes_with_length(tx, bytearray(namespace.namespace)) + if namespace.parent: + write_bytes_with_length(tx, bytearray(namespace.parent)) + else: + write_uint32(tx, 0xffffffff) + + return tx diff --git a/src/apps/nem/signing.py b/src/apps/nem/signing.py new file mode 100644 index 000000000..d6739a487 --- /dev/null +++ b/src/apps/nem/signing.py @@ -0,0 +1,55 @@ +from trezor.crypto.curve import ed25519 +from trezor.messages.NEMSignedTx import NEMSignedTx +from trezor.messages.NEMSignTx import NEMSignTx +from apps.common import seed + +from . import mosaic, multisig, namespace, transfer +from .helpers import NEM_CURVE, NEM_HASH_ALG +from .validators import validate + + +async def sign_tx(ctx, msg: NEMSignTx): + validate(msg) + node = await seed.derive_node(ctx, msg.transaction.address_n, NEM_CURVE) + + if msg.multisig: + public_key = msg.multisig.signer + await multisig.ask(ctx, msg) + common = msg.multisig + else: + public_key = _get_public_key(node) + common = msg.transaction + + if msg.transfer: + tx = await transfer.transfer(ctx, public_key, common, msg.transfer, node) + elif msg.provision_namespace: + tx = await namespace.namespace(ctx, public_key, common, msg.provision_namespace) + elif msg.mosaic_creation: + tx = await mosaic.mosaic_creation(ctx, public_key, common, msg.mosaic_creation) + elif msg.supply_change: + tx = await mosaic.supply_change(ctx, public_key, common, msg.supply_change) + elif msg.aggregate_modification: + tx = await multisig.aggregate_modification(ctx, public_key, common, msg.aggregate_modification, msg.multisig is not None) + elif msg.importance_transfer: + tx = await transfer.importance_transfer(ctx, public_key, common, msg.importance_transfer) + else: + raise ValueError('No transaction provided') + + if msg.multisig: + # wrap transaction in multisig wrapper + if msg.cosigning: + tx = multisig.cosign(_get_public_key(node), msg.transaction, tx, msg.multisig.signer) + else: + tx = multisig.initiate(_get_public_key(node), msg.transaction, tx) + + signature = ed25519.sign(node.private_key(), tx, NEM_HASH_ALG) + + resp = NEMSignedTx() + resp.data = tx + resp.signature = signature + return resp + + +def _get_public_key(node) -> bytes: + # 0x01 prefix is not part of the actual public key, hence removed + return node.public_key()[1:] diff --git a/src/apps/nem/transfer/__init__.py b/src/apps/nem/transfer/__init__.py new file mode 100644 index 000000000..f9fcc4173 --- /dev/null +++ b/src/apps/nem/transfer/__init__.py @@ -0,0 +1,22 @@ +from trezor.messages.NEMTransfer import NEMTransfer +from trezor.messages.NEMTransactionCommon import NEMTransactionCommon +from trezor.messages.NEMImportanceTransfer import NEMImportanceTransfer + +from . import layout, serialize + + +async def transfer(ctx, public_key: bytes, common: NEMTransactionCommon, transfer: NEMTransfer, node): + transfer.mosaics = serialize.canonicalize_mosaics(transfer.mosaics) + payload, encrypted = serialize.get_transfer_payload(transfer, node) + + await layout.ask_transfer(ctx, common, transfer, payload, encrypted) + + w = serialize.serialize_transfer(common, transfer, public_key, payload, encrypted) + for mosaic in transfer.mosaics: + serialize.serialize_mosaic(w, mosaic.namespace, mosaic.mosaic, mosaic.quantity) + return w + + +async def importance_transfer(ctx, public_key: bytes, common: NEMTransactionCommon, imp: NEMImportanceTransfer): + await layout.ask_importance_transfer(ctx, common, imp) + return serialize.serialize_importance_transfer(common, imp, public_key) diff --git a/src/apps/nem/transfer/layout.py b/src/apps/nem/transfer/layout.py new file mode 100644 index 000000000..5d459c1db --- /dev/null +++ b/src/apps/nem/transfer/layout.py @@ -0,0 +1,128 @@ +from trezor import ui +from trezor.messages import (ButtonRequestType, NEMImportanceTransfer, + NEMImportanceTransferMode, NEMMosaic, + NEMMosaicLevy, NEMTransactionCommon, NEMTransfer) +from trezor.ui.text import Text +from trezor.utils import format_amount, split_words + +from apps.common.confirm import require_confirm + +from ..helpers import (NEM_LEVY_PERCENTILE_DIVISOR_ABSOLUTE, + NEM_MAX_DIVISIBILITY, NEM_MOSAIC_AMOUNT_DIVISOR) +from ..layout import require_confirm_final, require_confirm_text, split_address +from ..mosaic.helpers import get_mosaic_definition, is_nem_xem_mosaic + + +async def ask_transfer(ctx, common: NEMTransactionCommon, transfer: NEMTransfer, payload: bytes, encrypted: bool): + if payload: + await _require_confirm_payload(ctx, transfer.payload, encrypted) + for mosaic in transfer.mosaics: + await ask_transfer_mosaic(ctx, common, transfer, mosaic) + await _require_confirm_transfer(ctx, transfer.recipient, _get_xem_amount(transfer)) + await require_confirm_final(ctx, common.fee) + + +async def ask_transfer_mosaic(ctx, common: NEMTransactionCommon, transfer: NEMTransfer, mosaic: NEMMosaic): + if is_nem_xem_mosaic(mosaic.namespace, mosaic.mosaic): + return + + definition = get_mosaic_definition(mosaic.namespace, mosaic.mosaic, common.network) + mosaic_quantity = mosaic.quantity * transfer.amount / NEM_MOSAIC_AMOUNT_DIVISOR + + if definition: + msg = Text('Confirm mosaic', ui.ICON_SEND, + 'Confirm transfer of', + ui.BOLD, format_amount(mosaic_quantity, definition['divisibility']) + definition['ticker'], + ui.NORMAL, 'of', + ui.BOLD, definition['name'], + icon_color=ui.GREEN) + + await require_confirm(ctx, msg, ButtonRequestType.ConfirmOutput) + + if 'levy' in definition and 'fee' in definition: + levy_msg = _get_levy_msg(definition, mosaic_quantity, common.network) + msg = Text('Confirm mosaic', ui.ICON_SEND, + 'Confirm mosaic', + 'levy fee of', + ui.BOLD, levy_msg, + icon_color=ui.GREEN) + + await require_confirm(ctx, msg, ButtonRequestType.ConfirmOutput) + + else: + msg = Text('Confirm mosaic', ui.ICON_SEND, + ui.BOLD, 'Unknown mosaic!', + ui.NORMAL, *split_words('Divisibility and levy cannot be shown for unknown mosaics', 22), + icon_color=ui.RED) + await require_confirm(ctx, msg, ButtonRequestType.ConfirmOutput) + + msg = Text('Confirm mosaic', ui.ICON_SEND, + ui.NORMAL, 'Confirm transfer of', + ui.BOLD, '%s raw units' % mosaic_quantity, + ui.NORMAL, 'of', + ui.BOLD, '%s.%s' % (mosaic.namespace, mosaic.mosaic), + icon_color=ui.GREEN) + await require_confirm(ctx, msg, ButtonRequestType.ConfirmOutput) + + +def _get_xem_amount(transfer: NEMTransfer): + # if mosaics are empty the transfer.amount denotes the xem amount + if not transfer.mosaics: + return transfer.amount + # otherwise xem amount is taken from the nem xem mosaic if present + for mosaic in transfer.mosaics: + if is_nem_xem_mosaic(mosaic.namespace, mosaic.mosaic): + return mosaic.quantity * transfer.amount / NEM_MOSAIC_AMOUNT_DIVISOR + # if there are mosaics but do not include xem, 0 xem is sent + return 0 + + +def _get_levy_msg(mosaic_definition, quantity: int, network: int) -> str: + levy_definition = get_mosaic_definition( + mosaic_definition['levy_namespace'], + mosaic_definition['levy_mosaic'], + network) + if mosaic_definition['levy'] == NEMMosaicLevy.MosaicLevy_Absolute: + levy_fee = mosaic_definition['fee'] + else: + levy_fee = quantity * mosaic_definition['fee'] / NEM_LEVY_PERCENTILE_DIVISOR_ABSOLUTE + return format_amount( + levy_fee, + levy_definition['divisibility'] + ) + levy_definition['ticker'] + + +async def ask_importance_transfer(ctx, common: NEMTransactionCommon, imp: NEMImportanceTransfer): + if imp.mode == NEMImportanceTransferMode.ImportanceTransfer_Activate: + m = 'Activate' + else: + m = 'Deactivate' + await require_confirm_text(ctx, m + ' remote harvesting?') + await require_confirm_final(ctx, common.fee) + + +async def _require_confirm_transfer(ctx, recipient, value): + content = Text('Confirm transfer', ui.ICON_SEND, + ui.BOLD, 'Send %s XEM' % format_amount(value, NEM_MAX_DIVISIBILITY), + ui.NORMAL, 'to', + ui.MONO, *split_address(recipient), + icon_color=ui.GREEN) + await require_confirm(ctx, content, ButtonRequestType.ConfirmOutput) + + +async def _require_confirm_payload(ctx, payload: bytes, encrypt=False): + payload = str(payload, 'utf-8') + + if len(payload) > 48: + payload = payload[:48] + '..' + if encrypt: + content = Text('Confirm payload', ui.ICON_SEND, + ui.BOLD, 'Encrypted:', + ui.NORMAL, *split_words(payload, 22), + icon_color=ui.GREEN) + else: + content = Text('Confirm payload', ui.ICON_SEND, + ui.BOLD, 'Unencrypted:', + ui.NORMAL, *split_words(payload, 22), + icon_color=ui.RED) + await require_confirm(ctx, content, ButtonRequestType.ConfirmOutput) diff --git a/src/apps/nem/transfer/serialize.py b/src/apps/nem/transfer/serialize.py new file mode 100644 index 000000000..9086c32bb --- /dev/null +++ b/src/apps/nem/transfer/serialize.py @@ -0,0 +1,117 @@ +from trezor.crypto import random +from trezor.messages.NEMImportanceTransfer import NEMImportanceTransfer +from trezor.messages.NEMMosaic import NEMMosaic +from trezor.messages.NEMTransactionCommon import NEMTransactionCommon +from trezor.messages.NEMTransfer import NEMTransfer + +from ..helpers import (AES_BLOCK_SIZE, NEM_SALT_SIZE, + NEM_TRANSACTION_TYPE_IMPORTANCE_TRANSFER, + NEM_TRANSACTION_TYPE_TRANSFER) +from ..writers import write_bytes_with_length, write_common, write_uint32, write_uint64 + + +def serialize_transfer(common: NEMTransactionCommon, + transfer: NEMTransfer, + public_key: bytes, + payload: bytes = None, + encrypted: bool = False) -> bytearray: + tx = write_common(common, bytearray(public_key), + NEM_TRANSACTION_TYPE_TRANSFER, + _get_version(common.network, transfer.mosaics)) + + write_bytes_with_length(tx, bytearray(transfer.recipient)) + write_uint64(tx, transfer.amount) + + if payload: + # payload + payload size (u32) + encryption flag (u32) + write_uint32(tx, len(payload) + 2 * 4) + if encrypted: + write_uint32(tx, 0x02) + else: + write_uint32(tx, 0x01) + write_bytes_with_length(tx, bytearray(payload)) + else: + write_uint32(tx, 0) + + if transfer.mosaics: + write_uint32(tx, len(transfer.mosaics)) + + return tx + + +def serialize_mosaic(w: bytearray, namespace: str, mosaic: str, quantity: int): + identifier_length = 4 + len(namespace) + 4 + len(mosaic) + # indentifier length (u32) + quantity (u64) + identifier size + write_uint32(w, 4 + 8 + identifier_length) + write_uint32(w, identifier_length) + write_bytes_with_length(w, bytearray(namespace)) + write_bytes_with_length(w, bytearray(mosaic)) + write_uint64(w, quantity) + + +def serialize_importance_transfer(common: NEMTransactionCommon, + imp: NEMImportanceTransfer, + public_key: bytes) -> bytearray: + w = write_common(common, bytearray(public_key), + NEM_TRANSACTION_TYPE_IMPORTANCE_TRANSFER) + + write_uint32(w, imp.mode) + write_bytes_with_length(w, bytearray(imp.public_key)) + return w + + +def get_transfer_payload(transfer: NEMTransfer, node) -> [bytes, bool]: + payload = transfer.payload + encrypted = False + if transfer.public_key is not None: + if payload is None: + raise ValueError('Public key provided but no payload to encrypt') + payload = _encrypt(node, transfer.public_key, transfer.payload) + encrypted = True + + return payload, encrypted + + +def _encrypt(node, public_key: bytes, payload: bytes) -> bytes: + salt = random.bytes(NEM_SALT_SIZE) + iv = random.bytes(AES_BLOCK_SIZE) + encrypted = node.nem_encrypt(public_key, iv, salt, payload) + return iv + salt + encrypted + + +def _get_version(network, mosaics=None) -> int: + if mosaics: + return network << 24 | 2 + return network << 24 | 1 + + +def canonicalize_mosaics(mosaics: list): + if len(mosaics) <= 1: + return mosaics + mosaics = merge_mosaics(mosaics) + return sort_mosaics(mosaics) + + +def are_mosaics_equal(a: NEMMosaic, b: NEMMosaic) -> bool: + if a.namespace == b.namespace and a.mosaic == b.mosaic: + return True + return False + + +def merge_mosaics(mosaics: list) -> list: + if not mosaics: + return list() + ret = list() + for i in mosaics: + found = False + for k, y in enumerate(ret): + if are_mosaics_equal(i, y): + ret[k].quantity += i.quantity + found = True + if not found: + ret.append(i) + return ret + + +def sort_mosaics(mosaics: list) -> list: + return sorted(mosaics, key=lambda m: (m.namespace, m.mosaic)) diff --git a/src/apps/nem/validators.py b/src/apps/nem/validators.py new file mode 100644 index 000000000..c5d11c0e4 --- /dev/null +++ b/src/apps/nem/validators.py @@ -0,0 +1,236 @@ +from trezor.crypto import nem +from trezor.messages import NEMModificationType +from trezor.messages.NEMSignTx import (NEMAggregateModification, + NEMImportanceTransfer, + NEMMosaicCreation, + NEMMosaicSupplyChange, + NEMProvisionNamespace, NEMSignTx, + NEMTransactionCommon, NEMTransfer) + +from .helpers import (NEM_MAX_DIVISIBILITY, NEM_MAX_ENCRYPTED_PAYLOAD_SIZE, + NEM_MAX_PLAIN_PAYLOAD_SIZE, NEM_MAX_SUPPLY, + NEM_NETWORK_MAINNET, NEM_NETWORK_MIJIN, + NEM_NETWORK_TESTNET, NEM_PUBLIC_KEY_SIZE) + + +def validate(msg: NEMSignTx): + if msg.transaction is None: + raise ValueError('No common provided') + + _validate_single_tx(msg) + _validate_common(msg.transaction) + + if msg.multisig: + _validate_common(msg.multisig, True) + _validate_multisig(msg.multisig, msg.transaction.network) + if not msg.multisig and msg.cosigning: + raise ValueError('No multisig transaction to cosign') + + if msg.transfer: + _validate_transfer(msg.transfer, msg.transaction.network) + if msg.provision_namespace: + _validate_provision_namespace(msg.provision_namespace, msg.transaction.network) + if msg.mosaic_creation: + _validate_mosaic_creation(msg.mosaic_creation, msg.transaction.network) + if msg.supply_change: + _validate_supply_change(msg.supply_change) + if msg.aggregate_modification: + _validate_aggregate_modification(msg.aggregate_modification, msg.multisig is None) + if msg.importance_transfer: + _validate_importance_transfer(msg.importance_transfer) + + +def validate_network(network: int) -> int: + if network is None: + return NEM_NETWORK_MAINNET + if network not in (NEM_NETWORK_MAINNET, NEM_NETWORK_TESTNET, NEM_NETWORK_MIJIN): + raise ValueError('Invalid NEM network') + return network + + +def _validate_single_tx(msg: NEMSignTx): + # ensure exactly one transaction is provided + tx_count = bool(msg.transfer) + \ + bool(msg.provision_namespace) + \ + bool(msg.mosaic_creation) + \ + bool(msg.supply_change) + \ + bool(msg.aggregate_modification) + \ + bool(msg.importance_transfer) + if tx_count == 0: + raise ValueError('No transaction provided') + if tx_count > 1: + raise ValueError('More than one transaction provided') + + +def _validate_common(common: NEMTransactionCommon, inner: bool = False): + common.network = validate_network(common.network) + + err = None + if common.timestamp is None: + err = 'timestamp' + if common.fee is None: + err = 'fee' + if common.deadline is None: + err = 'deadline' + + if not inner and common.signer: + raise ValueError('Signer not allowed in outer transaction') + + if inner and common.signer is None: + err = 'signer' + + if err: + if inner: + raise ValueError('No %s provided in inner transaction' % err) + else: + raise ValueError('No %s provided' % err) + + if common.signer is not None: + _validate_public_key(common.signer, 'Invalid signer public key in inner transaction') + + +def _validate_public_key(public_key: bytes, err_msg: str): + if not public_key: + raise ValueError('%s (none provided)' % err_msg) + if len(public_key) != NEM_PUBLIC_KEY_SIZE: + raise ValueError('%s (invalid length)' % err_msg) + + +def _validate_importance_transfer(importance_transfer: NEMImportanceTransfer): + if importance_transfer.mode is None: + raise ValueError('No mode provided') + _validate_public_key(importance_transfer.public_key, 'Invalid remote account public key provided') + + +def _validate_multisig(multisig: NEMTransactionCommon, network: int): + if multisig.network != network: + raise ValueError('Inner transaction network is different') + _validate_public_key(multisig.signer, 'Invalid multisig signer public key provided') + + +def _validate_aggregate_modification( + aggregate_modification: NEMAggregateModification, + creation: bool = False): + + if creation and not aggregate_modification.modifications: + raise ValueError('No modifications provided') + + for m in aggregate_modification.modifications: + if not m.type: + raise ValueError('No modification type provided') + if m.type not in ( + NEMModificationType.CosignatoryModification_Add, + NEMModificationType.CosignatoryModification_Delete + ): + raise ValueError('Unknown aggregate modification') + if creation and m.type == NEMModificationType.CosignatoryModification_Delete: + raise ValueError('Cannot remove cosignatory when converting account') + _validate_public_key(m.public_key, 'Invalid cosignatory public key provided') + + +def _validate_supply_change(supply_change: NEMMosaicSupplyChange): + if supply_change.namespace is None: + raise ValueError('No namespace provided') + if supply_change.mosaic is None: + raise ValueError('No mosaic provided') + if supply_change.type is None: + raise ValueError('No type provided') + if supply_change.delta is None: + raise ValueError('No delta provided') + + +def _validate_mosaic_creation(mosaic_creation: NEMMosaicCreation, network: int): + if mosaic_creation.definition is None: + raise ValueError('No mosaic definition provided') + if mosaic_creation.sink is None: + raise ValueError('No creation sink provided') + if mosaic_creation.fee is None: + raise ValueError('No creation sink fee provided') + + if not nem.validate_address(mosaic_creation.sink, network): + raise ValueError('Invalid creation sink address') + + if mosaic_creation.definition.name is not None: + raise ValueError('Name not allowed in mosaic creation transactions') + if mosaic_creation.definition.ticker is not None: + raise ValueError('Ticker not allowed in mosaic creation transactions') + if mosaic_creation.definition.networks: + raise ValueError('Networks not allowed in mosaic creation transactions') + + if mosaic_creation.definition.namespace is None: + raise ValueError('No mosaic namespace provided') + if mosaic_creation.definition.mosaic is None: + raise ValueError('No mosaic name provided') + + if mosaic_creation.definition.supply is not None and mosaic_creation.definition.divisibility is None: + raise ValueError('Definition divisibility needs to be provided when supply is') + if mosaic_creation.definition.supply is None and mosaic_creation.definition.divisibility is not None: + raise ValueError('Definition supply needs to be provided when divisibility is') + + if mosaic_creation.definition.levy is not None: + if mosaic_creation.definition.fee is None: + raise ValueError('No levy fee provided') + if mosaic_creation.definition.levy_address is None: + raise ValueError('No levy address provided') + if mosaic_creation.definition.levy_namespace is None: + raise ValueError('No levy namespace provided') + if mosaic_creation.definition.levy_mosaic is None: + raise ValueError('No levy mosaic name provided') + + if mosaic_creation.definition.divisibility is None: + raise ValueError('No divisibility provided') + if mosaic_creation.definition.supply is None: + raise ValueError('No supply provided') + if mosaic_creation.definition.mutable_supply is None: + raise ValueError('No supply mutability provided') + if mosaic_creation.definition.transferable is None: + raise ValueError('No mosaic transferability provided') + if mosaic_creation.definition.description is None: + raise ValueError('No description provided') + + if mosaic_creation.definition.divisibility > NEM_MAX_DIVISIBILITY: + raise ValueError('Invalid divisibility provided') + if mosaic_creation.definition.supply > NEM_MAX_SUPPLY: + raise ValueError('Invalid supply provided') + + if not nem.validate_address(mosaic_creation.definition.levy_address, network): + raise ValueError('Invalid levy address') + + +def _validate_provision_namespace(provision_namespace: NEMProvisionNamespace, network: int): + if provision_namespace.namespace is None: + raise ValueError('No namespace provided') + if provision_namespace.sink is None: + raise ValueError('No rental sink provided') + if provision_namespace.fee is None: + raise ValueError('No rental sink fee provided') + + if not nem.validate_address(provision_namespace.sink, network): + raise ValueError('Invalid rental sink address') + + +def _validate_transfer(transfer: NEMTransfer, network: int): + if transfer.recipient is None: + raise ValueError('No recipient provided') + if transfer.amount is None: + raise ValueError('No amount provided') + + if transfer.public_key is not None: + _validate_public_key(transfer.public_key, 'Invalid recipient public key') + + if transfer.payload: + if len(transfer.payload) > NEM_MAX_PLAIN_PAYLOAD_SIZE: + raise ValueError('Payload too large') + if transfer.public_key and len(transfer.payload) > NEM_MAX_ENCRYPTED_PAYLOAD_SIZE: + raise ValueError('Payload too large') + + if not nem.validate_address(transfer.recipient, network): + raise ValueError('Invalid recipient address') + + for m in transfer.mosaics: + if m.namespace is None: + raise ValueError('No mosaic namespace provided') + if m.mosaic is None: + raise ValueError('No mosaic name provided') + if m.quantity is None: + raise ValueError('No mosaic quantity provided') diff --git a/src/apps/nem/writers.py b/src/apps/nem/writers.py new file mode 100644 index 000000000..5bcb6c0c5 --- /dev/null +++ b/src/apps/nem/writers.py @@ -0,0 +1,47 @@ +from trezor.messages.NEMTransactionCommon import NEMTransactionCommon + + +def write_uint32(w, n: int): + w.append(n & 0xFF) + w.append((n >> 8) & 0xFF) + w.append((n >> 16) & 0xFF) + w.append((n >> 24) & 0xFF) + + +def write_uint64(w, n: int): + w.append(n & 0xFF) + w.append((n >> 8) & 0xFF) + w.append((n >> 16) & 0xFF) + w.append((n >> 24) & 0xFF) + w.append((n >> 32) & 0xFF) + w.append((n >> 40) & 0xFF) + w.append((n >> 48) & 0xFF) + w.append((n >> 56) & 0xFF) + + +def write_bytes(w, buf: bytearray): + w.extend(buf) + + +def write_bytes_with_length(w, buf: bytearray): + write_uint32(w, len(buf)) + write_bytes(w, buf) + + +def write_common(common: NEMTransactionCommon, + public_key: bytearray, + transaction_type: int, + version: int = None) -> bytearray: + ret = bytearray() + + write_uint32(ret, transaction_type) + if version is None: + version = common.network << 24 | 1 + write_uint32(ret, version) + write_uint32(ret, common.timestamp) + + write_bytes_with_length(ret, public_key) + write_uint64(ret, common.fee) + write_uint32(ret, common.deadline) + + return ret diff --git a/src/main.py b/src/main.py index 5b1fe06c2..0c239327c 100644 --- a/src/main.py +++ b/src/main.py @@ -12,6 +12,7 @@ import apps.management import apps.wallet import apps.ethereum import apps.lisk +import apps.nem if __debug__: import apps.debug else: @@ -23,6 +24,7 @@ apps.management.boot() apps.wallet.boot() apps.ethereum.boot() apps.lisk.boot() +apps.nem.boot() if __debug__: apps.debug.boot() else: diff --git a/src/trezor/crypto/__init__.py b/src/trezor/crypto/__init__.py index 0cb5c1497..1041d98cf 100644 --- a/src/trezor/crypto/__init__.py +++ b/src/trezor/crypto/__init__.py @@ -1 +1 @@ -from trezorcrypto import bip32, bip39, chacha20poly1305, crc, pbkdf2, random, rfc6979 # noqa: F401 +from trezorcrypto import bip32, bip39, chacha20poly1305, crc, pbkdf2, random, rfc6979, nem # noqa: F401 diff --git a/tests/test_apps.nem.address.py b/tests/test_apps.nem.address.py new file mode 100644 index 000000000..dab6faa8e --- /dev/null +++ b/tests/test_apps.nem.address.py @@ -0,0 +1,37 @@ +from common import * +from ubinascii import unhexlify +from trezor.crypto import nem +from apps.nem.helpers import NEM_NETWORK_MAINNET, NEM_NETWORK_TESTNET + + +class TestNemAddress(unittest.TestCase): + + def test_addresses(self): + pubkey = unhexlify('c5f54ba980fcbb657dbaaa42700539b207873e134d2375efeab5f1ab52f87844') + address = nem.compute_address(pubkey, NEM_NETWORK_MAINNET) + self.assertEqual(address, 'NDD2CT6LQLIYQ56KIXI3ENTM6EK3D44P5JFXJ4R4') + + pubkey = unhexlify('114171230ad6f8522a000cdc73fbc5c733b30bb71f2b146ccbdf34499f79a810') + address = nem.compute_address(pubkey, NEM_NETWORK_MAINNET) + self.assertEqual(address, 'NCUKWDY3J3THKQHAKOK5ALF6ANJQABZHCH7VN6DP') + + def test_validate_address(self): + validity = nem.validate_address('NDD2CT6LQLIYQ56KIXI3ENTM6EK3D44P5JFXJ4R4', NEM_NETWORK_MAINNET) + self.assertTrue(validity) + + validity = nem.validate_address('NCUKWDY3J3THKQHAKOK5ALF6ANJQABZHCH7VN6DP', NEM_NETWORK_MAINNET) + self.assertTrue(validity) + + validity = nem.validate_address('TAU5HO3DRQZNELFEMZZTUKQEZGQ7IUAHKPO7OOLK', NEM_NETWORK_TESTNET) + self.assertTrue(validity) + + validity = nem.validate_address('nope', NEM_NETWORK_TESTNET) + self.assertFalse(validity) + + # not valid on testnet + validity = nem.validate_address('NCUKWDY3J3THKQHAKOK5ALF6ANJQABZHCH7VN6DP', NEM_NETWORK_TESTNET) + self.assertFalse(validity) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_apps.nem.hdnode.py b/tests/test_apps.nem.hdnode.py new file mode 100644 index 000000000..ea109c655 --- /dev/null +++ b/tests/test_apps.nem.hdnode.py @@ -0,0 +1,237 @@ +from common import * +from ubinascii import unhexlify +from trezor.crypto import bip32 +from apps.nem.helpers import NEM_NETWORK_MAINNET, NEM_CURVE + + +class TestNemHDNode(unittest.TestCase): + + def test_addresses(self): + # test vectors from https://raw.githubusercontent.com/NemProject/nem-test-vectors/master/1.test-keys.dat + # private key, public key, address + test_cases = [ + ('575dbb3062267eff57c970a336ebbc8fbcfe12c5bd3ed7bc11eb0481d7704ced', + 'c5f54ba980fcbb657dbaaa42700539b207873e134d2375efeab5f1ab52f87844', + 'NDD2CT6LQLIYQ56KIXI3ENTM6EK3D44P5JFXJ4R4'), + ('5b0e3fa5d3b49a79022d7c1e121ba1cbbf4db5821f47ab8c708ef88defc29bfe', + '96eb2a145211b1b7ab5f0d4b14f8abc8d695c7aee31a3cfc2d4881313c68eea3', + 'NABHFGE5ORQD3LE4O6B7JUFN47ECOFBFASC3SCAC'), + ('738ba9bb9110aea8f15caa353aca5653b4bdfca1db9f34d0efed2ce1325aeeda', + '2d8425e4ca2d8926346c7a7ca39826acd881a8639e81bd68820409c6e30d142a', + 'NAVOZX4HDVOAR4W6K4WJHWPD3MOFU27DFHC7KZOZ'), + ('e8bf9bc0f35c12d8c8bf94dd3a8b5b4034f1063948e3cc5304e55e31aa4b95a6', + '4feed486777ed38e44c489c7c4e93a830e4c4a907fa19a174e630ef0f6ed0409', + 'NBZ6JK5YOCU6UPSSZ5D3G27UHAPHTY5HDQMGE6TT'), + ('c325ea529674396db5675939e7988883d59a5fc17a28ca977e3ba85370232a83', + '83ee32e4e145024d29bca54f71fa335a98b3e68283f1a3099c4d4ae113b53e54', + 'NCQW2P5DNZ5BBXQVGS367DQ4AHC3RXOEVGRCLY6V'), + ('a811cb7a80a7227ae61f6da536534ee3c2744e3c7e4b85f3e0df3c6a9c5613df', + '6d34c04f3a0e42f0c3c6f50e475ae018cfa2f56df58c481ad4300424a6270cbb', + 'NA5IG3XFXZHIPJ5QLKX2FBJPEZYPMBPPK2ZRC3EH'), + ('9c66de1ec77f4dfaaebdf9c8bc599ca7e8e6f0bc71390ffee2c9dd3f3619242a', + 'a8fefd72a3b833dc7c7ed7d57ed86906dac22f88f1f4331873eb2da3152a3e77', + 'NAABHVFJDBM74XMJJ52R7QN2MTTG2ZUXPQS62QZ7'), + ('c56bc16ecf727878c15e24f4ae68569600ac7b251218a44ef50ce54175776edc', + 'c92f761e6d83d20068fd46fe4bd5b97f4c6ba05d23180679b718d1f3e4fb066e', + 'NCLK3OLMHR3F2E3KSBUIZ4K5PNWUDN37MLSJBJZP'), + ('9dd73599283882fa1561ddfc9be5830b5dd453c90465d3fe5eeb646a3606374e', + 'eaf16a4833e59370a04ccd5c63395058de34877b48c17174c71db5ed37b537ed', + 'ND3AHW4VTI5R5QE5V44KIGPRU5FBJ5AFUCJXOY5H'), + ('d9639dc6f49dad02a42fd8c217f1b1b4f8ce31ccd770388b645e639c72ff24fa', + '0f74a2f537cd9c986df018994dde75bdeee05e35eb9fe27adf506ca8475064f7', + 'NCTZ4YAP43ONK3UYTASQVNDMBO24ZHJE65F3QPYE'), + ('efc1992cd50b70ca55ac12c07aa5d026a8b78ffe28a7dbffc9228b26e02c38c1', + '2ebff201255f6cf948c78f528658b99a7c13ac791942fa22d59af610558111f5', + 'NDQ2TMCMXBSFPZQPE2YKH6XLC24HD6LUMN6Z4GIC'), + ('143a815e92e43f3ed1a921ee48cd143931b88b7c3d8e1e981f743c2a5be3c5ba', + '419ed11d48730e4ae2c93f0ea4df853b8d578713a36dab227517cf965861af4e', + 'NA32IDDW2C53BDSBJNFL3Z6UU3J5CJZJMCZDXCF4'), + ('bc1a082f5ac6fdd3a83ade211e5986ac0551bad6c7da96727ec744e5df963e2a', + 'a160e6f9112233a7ce94202ed7a4443e1dac444b5095f9fecbb965fba3f92cac', + 'NADUCEQLC3FTGB25GTA5HOUTB53CBVQNVOIP7NTJ'), + ('4e47b4c6f4c7886e49ec109c61f4af5cfbb1637283218941d55a7f9fe1053f72', + 'fbb91b16df828e21a9802980a44fc757c588bc1382a4cea429d6fa2ae0333f56', + 'NBAF3BFLLPWH33MYE6VUPP5T6DQBZBKIDEQKZQOE'), + ('efc4389da48ce49f85365cfa578c746530e9eac42db1b64ec346119b1becd347', + '2232f24dda0f2ded3ecd831210d4e8521a096b50cadd5a34f3f7083374e1ec12', + 'NBOGTK2I2ATOGGD7ZFJHROG5MWL7XCKAUKSWIVSA'), + ('bdba57c78ca7da16a3360efd13f06276284db8c40351de7fcd38ba0c35ac754d', + 'c334c6c0dad5aaa2a0d0fb4c6032cb6a0edd96bf61125b5ea9062d5a00ee0eee', + 'NCLERTEFYXKLK7RA4MVACEFMXMK3P7QMWTM7FBW2'), + ('20694c1ec3c4a311bcdb29ed2edc428f6d4f9a4c429ad6a5bf3222084e35695f', + '518c4de412efa93de06a55947d11f697639443916ec8fcf04ebc3e6d17d0bd93', + 'NB5V4BPIJHXVONO7UGMJDPFARMFA73BOBNOOYCOV'), + ('e0d4f3760ac107b33c22c2cac24ab2f520b282684f5f66a4212ff95d926323ce', + 'b3d16f4ead9de67c290144da535a0ed2504b03c05e5f1ceb8c7863762f786857', + 'NC4PBAO5TPCAVQKBVOC4F6DMZP3CFSQBU46PSKBD'), + ('efa9afc617412093c9c7a7c211a5332dd556f941e1a88c494ec860608610eea2', + '7e7716e4cebceb731d6f1fd28676f34888e9a0000fcfa1471db1c616c2ddf559', + 'NCFW2LPXIWLBWAQN2QVIWEOD7IVDO3HQBD2OU56K'), + ('d98499b3db61944684ce06a91735af4e14105338473fcf6ebe2b0bcada3dfd21', + '114171230ad6f8522a000cdc73fbc5c733b30bb71f2b146ccbdf34499f79a810', + 'NCUKWDY3J3THKQHAKOK5ALF6ANJQABZHCH7VN6DP') + ] + + for test in test_cases: + private_key = bytearray(reversed(unhexlify(test[0]))) + + node = bip32.HDNode( + depth=0, + fingerprint=0, + child_num=0, + chain_code=bytearray(32), + private_key=private_key, + curve_name=NEM_CURVE + ) + + self.assertEqual(node.nem_address(NEM_NETWORK_MAINNET), test[2]) + # public key is prepended with 1, removing + self.assertEqual(node.public_key()[1:], unhexlify(test[1])) + + def test_encryption(self): + # test vectors from https://raw.githubusercontent.com/NemProject/nem-test-vectors/master/4.test-cipher.dat + # private key, transfer public key, salt, iv, plain text, cipher text + test_cases = [ + {'private': '3140f94c79f249787d1ec75a97a885980eb8f0a7d9b7aa03e7200296e422b2b6', + 'public': '57a70eb553a7b3fd621f0dba6abf51312ea2e2a2a1e19d0305516730f4bcbd21', + 'salt': '83616c67f076d356fd1288a6e0fd7a60488ba312a3adf0088b1b33c7655c3e6a', + 'iv': 'a73ff5c32f8fd055b09775817a6a3f95', + 'input': '86ddb9e713a8ebf67a51830eff03b837e147c20d75e67b2a54aa29e98c', + 'output': '70815da779b1b954d7a7f00c16940e9917a0412a06a444b539bf147603eef87f'}, + {'private': '3140f94c79f249787d1ec75a97a885980eb8f0a7d9b7aa03e7200296e422b2b6', + 'public': '57a70eb553a7b3fd621f0dba6abf51312ea2e2a2a1e19d0305516730f4bcbd21', + 'salt': '703ce0b1d276b10eef35672df03234385a903460db18ba9d4e05b3ad31abb284', + 'iv': '91246c2d5493867c4fa3e78f85963677', + 'input': '86ddb9e713a8ebf67a51830eff03b837e147c20d75e67b2a54aa29e98c', + 'output': '564b2f40d42c0efc1bd6f057115a5abd1564cae36d7ccacf5d825d38401aa894'}, + {'private': '3140f94c79f249787d1ec75a97a885980eb8f0a7d9b7aa03e7200296e422b2b6', + 'public': '57a70eb553a7b3fd621f0dba6abf51312ea2e2a2a1e19d0305516730f4bcbd21', + 'salt': 'b22e8e8e7373ac31ca7f0f6eb8b93130aba5266772a658593f3a11792e7e8d92', + 'iv': '9f8e33d82374dad6aac0e3dbe7aea704', + 'input': '86ddb9e713a8ebf67a51830eff03b837e147c20d75e67b2a54aa29e98c', + 'output': '7cab88d00a3fc656002eccbbd966e1d5d14a3090d92cf502cdbf843515625dcf'}, + {'private': '3140f94c79f249787d1ec75a97a885980eb8f0a7d9b7aa03e7200296e422b2b6', + 'public': '57a70eb553a7b3fd621f0dba6abf51312ea2e2a2a1e19d0305516730f4bcbd21', + 'salt': 'af646c54cd153dffe453b60efbceeb85c1e95a414ea0036c4da94afb3366f5d9', + 'iv': '6acdf8e01acc8074ddc807281b6af888', + 'input': '86ddb9e713a8ebf67a51830eff03b837e147c20d75e67b2a54aa29e98c', + 'output': 'aa70543a485b63a4dd141bb7fd78019092ac6fad731e914280a287c7467bae1a'}, + {'private': '3140f94c79f249787d1ec75a97a885980eb8f0a7d9b7aa03e7200296e422b2b6', + 'public': '57a70eb553a7b3fd621f0dba6abf51312ea2e2a2a1e19d0305516730f4bcbd21', + 'salt': 'd9c0d386636c8a024935c024589f9cd39e820a16485b14951e690a967830e269', + 'iv': 'f2e9f18aeb374965f54d2f4e31189a8f', + 'input': '86ddb9e713a8ebf67a51830eff03b837e147c20d75e67b2a54aa29e98c', + 'output': '33d97c216ea6498dfddabf94c2e2403d73efc495e9b284d9d90aaff840217d25'}, + {'private': 'd5c0762ecea2cd6b5c56751b58debcb32713aab348f4a59c493e38beb3244f3a', + 'public': '66a35941d615b5644d19c2a602c363ada8b1a8a0dac3682623852dcab4afac04', + 'salt': '06c227baac1ae3b0b1dc583f4850f13f9ba5d53be4a98fa5c3ea16217847530d', + 'iv': '3735123e78c44895df6ea33fa57e9a72', + 'input': '86ddb9e713a8ebf67a51830eff03b837e147c20d75e67b2a54aa29e98c', + 'output': 'd5b5d66ba8cee0eb7ecf95b143fa77a46d6de13749e12eff40f5a7e649167ccb'}, + {'private': 'd5c0762ecea2cd6b5c56751b58debcb32713aab348f4a59c493e38beb3244f3a', + 'public': '66a35941d615b5644d19c2a602c363ada8b1a8a0dac3682623852dcab4afac04', + 'salt': '92f55ba5bc6fc2f23e3eedc299357c71518e36ba2447a4da7a9dfe9dfeb107b5', + 'iv': '1cbc4982e53e370052af97ab088fa942', + 'input': '86ddb9e713a8ebf67a51830eff03b837e147c20d75e67b2a54aa29e98c', + 'output': 'd48ef1ef526d805656cfc932aff259eadb17aa3391dde1877a722cba31d935b2'}, + {'private': 'd5c0762ecea2cd6b5c56751b58debcb32713aab348f4a59c493e38beb3244f3a', + 'public': '66a35941d615b5644d19c2a602c363ada8b1a8a0dac3682623852dcab4afac04', + 'salt': '10f15a39ba49866292a43b7781bc71ca8bbd4889f1616461caf056bcb91b0158', + 'iv': 'c40d531d92bfee969dce91417346c892', + 'input': '49de3cd5890e0cd0559f143807ff688ff62789b7236a332b7d7255ec0b4e73e6b3a4', + 'output': 'e6d75afdb542785669b42198577c5b358d95397d71ec6f5835dca46d332cc08dbf73ea790b7bcb169a65719c0d55054c'}, + {'private': 'd5c0762ecea2cd6b5c56751b58debcb32713aab348f4a59c493e38beb3244f3a', + 'public': '66a35941d615b5644d19c2a602c363ada8b1a8a0dac3682623852dcab4afac04', + 'salt': '9c01ed42b219b3bbe1a43ae9d7af5c1dd09363baacfdba8f4d03d1046915e26e', + 'iv': '059a35d5f83249e632790015ed6518b9', + 'input': '49de3cd5890e0cd0559f143807ff688ff62789b7236a332b7d7255ec0b4e73e6b3a4', + 'output': '5ef11aadff2eccee8b712dab968fa842eb770818ec0e6663ed242ea8b6bbc1c66d6285ee5b5f03d55dfee382fb4fa25d'}, + {'private': 'd5c0762ecea2cd6b5c56751b58debcb32713aab348f4a59c493e38beb3244f3a', + 'public': '66a35941d615b5644d19c2a602c363ada8b1a8a0dac3682623852dcab4afac04', + 'salt': 'bc1067e2a7415ea45ff1ca9894338c591ff15f2e57ae2789ae31b9d5bea0f11e', + 'iv': '8c73f0d6613898daeefa3cf8b0686d37', + 'input': '49de3cd5890e0cd0559f143807ff688ff62789b7236a332b7d7255ec0b4e73e6b3a4', + 'output': '6d220213b1878cd40a458f2a1e6e3b48040455fdf504dcd857f4f2ca1ad642e3a44fc401d04e339d302f66a9fad3d919'}, + {'private': '9ef87ba8aa2e664bdfdb978b98bc30fb61773d9298e7b8c72911683eeff41921', + 'public': '441e76d7e53be0a967181076a842f69c20fd8c0e3f0ce3aa421b490b059fe094', + 'salt': 'cf4a21cb790552165827b678ca9695fcaf77566d382325112ff79483455de667', + 'iv': 'bfbf5482e06f55b88bdd9e053b7eee6e', + 'input': '49de3cd5890e0cd0559f143807ff688ff62789b7236a332b7d7255ec0b4e73e6b3a4', + 'output': '1198a78c29c215d5c450f7b8513ead253160bc9fde80d9cc8e6bee2efe9713cf5a09d6293c41033271c9e8c22036a28b'}, + {'private': '9ef87ba8aa2e664bdfdb978b98bc30fb61773d9298e7b8c72911683eeff41921', + 'public': '441e76d7e53be0a967181076a842f69c20fd8c0e3f0ce3aa421b490b059fe094', + 'salt': 'eba5eae8aef79114082c3e70baef95bb02edf13b3897e8be7a70272962ef8838', + 'iv': 'af9a56da3da18e2fbd2948a16332532b', + 'input': '49de3cd5890e0cd0559f143807ff688ff62789b7236a332b7d7255ec0b4e73e6b3a4', + 'output': '1062ab5fbbdee9042ad35bdadfd3047c0a2127fe0f001da1be1b0582185edfc9687be8d68f85795833bb04af9cedd3bb'}, + {'private': '9ef87ba8aa2e664bdfdb978b98bc30fb61773d9298e7b8c72911683eeff41921', + 'public': '441e76d7e53be0a967181076a842f69c20fd8c0e3f0ce3aa421b490b059fe094', + 'salt': '518f8dfd0c138f1ffb4ea8029db15441d70abd893c3d767dc668f23ba7770e27', + 'iv': '42d28307974a1b2a2d921d270cfce03b', + 'input': '49de3cd5890e0cd0559f143807ff688ff62789b7236a332b7d7255ec0b4e73e6b3a4', + 'output': '005e49fb7c5da540a84b034c853fc9f78a6b901ea495aed0c2abd4f08f1a96f9ffefc6a57f1ac09e0aea95ca0f03ffd8'}, + {'private': '9ef87ba8aa2e664bdfdb978b98bc30fb61773d9298e7b8c72911683eeff41921', + 'public': '441e76d7e53be0a967181076a842f69c20fd8c0e3f0ce3aa421b490b059fe094', + 'salt': '582fdf58b53715c26e10ba809e8f2ab70502e5a3d4e9a81100b7227732ab0bbc', + 'iv': '91f2aad3189bb2edc93bc891e73911ba', + 'input': '49de3cd5890e0cd0559f143807ff688ff62789b7236a332b7d7255ec0b4e73e6b3a4', + 'output': '821a69cb16c57f0cb866e590b38069e35faec3ae18f158bb067db83a11237d29ab1e6b868b3147236a0958f15c2e2167'}, + {'private': '9ef87ba8aa2e664bdfdb978b98bc30fb61773d9298e7b8c72911683eeff41921', + 'public': '441e76d7e53be0a967181076a842f69c20fd8c0e3f0ce3aa421b490b059fe094', + 'salt': 'a415b4c006118fb72fc37b2746ef288e23ac45c8ff7ade5f368a31557b6ac93a', + 'iv': '2b7c5f75606c0b8106c6489ea5657a9e', + 'input': '24512b714aefd5cbc4bcc4ef44ce6c67ffc447c65460a6c6e4a92e85', + 'output': '2781d5ee8ef1cb1596f8902b33dfae5045f84a987ca58173af5830dbce386062'}, + {'private': 'ed93c5a101ab53382ceee4f7e6b5aa112621d3bb9d18891509b1834ede235bcc', + 'public': '5a5e14c633d7d269302849d739d80344ff14db51d7bcda86045723f05c4e4541', + 'salt': '47e73ec362ea82d3a7c5d55532ad51d2cdf5316b981b2b2bd542b0efa027e8ea', + 'iv': 'b2193f59030c8d05a7d3577b7f64dd33', + 'input': '24512b714aefd5cbc4bcc4ef44ce6c67ffc447c65460a6c6e4a92e85', + 'output': '3f43912db8dd6672b9996e5272e18c4b88fec9d7e8372db9c5f4709a4af1d86f'}, + {'private': 'ed93c5a101ab53382ceee4f7e6b5aa112621d3bb9d18891509b1834ede235bcc', + 'public': '5a5e14c633d7d269302849d739d80344ff14db51d7bcda86045723f05c4e4541', + 'salt': 'aaa006c57b6d1e402650577fe9787d8d285f4bacd7c01f998be49c766f8860c7', + 'iv': '130304ddb9adc8870cf56bcae9487b7f', + 'input': '24512b714aefd5cbc4bcc4ef44ce6c67ffc447c65460a6c6e4a92e85', + 'output': '878cc7d8c0ef8dac0182a78eedc8080a402f59d8062a6b4ca8f4a74f3c3b3de7'}, + {'private': 'ed93c5a101ab53382ceee4f7e6b5aa112621d3bb9d18891509b1834ede235bcc', + 'public': '5a5e14c633d7d269302849d739d80344ff14db51d7bcda86045723f05c4e4541', + 'salt': '28dc7ccd6c2a939eef64b8be7b9ae248295e7fcd8471c22fa2f98733fea97611', + 'iv': 'cb13890d3a11bc0a7433738263006710', + 'input': '24512b714aefd5cbc4bcc4ef44ce6c67ffc447c65460a6c6e4a92e85', + 'output': 'e74ded846bebfa912fa1720e4c1415e6e5df7e7a1a7fedb5665d68f1763209a4'}, + {'private': 'ed93c5a101ab53382ceee4f7e6b5aa112621d3bb9d18891509b1834ede235bcc', + 'public': '5a5e14c633d7d269302849d739d80344ff14db51d7bcda86045723f05c4e4541', + 'salt': '79974fa2cad95154d0873902c153ccc3e7d54b17f2eeb3f29b6344cad9365a9a', + 'iv': '22123357979d20f44cc8eb0263d84e0e', + 'input': '24512b714aefd5cbc4bcc4ef44ce6c67ffc447c65460a6c6e4a92e85', + 'output': 'eb14dec7b8b64d81a2ee4db07b0adf144d4f79a519bbf332b823583fa2d45405'}, + {'private': 'ed93c5a101ab53382ceee4f7e6b5aa112621d3bb9d18891509b1834ede235bcc', + 'public': '5a5e14c633d7d269302849d739d80344ff14db51d7bcda86045723f05c4e4541', + 'salt': '3409a6f8c4dcd9bd04144eb67e55a98696b674735b01bf1196191f29871ef966', + 'iv': 'a823a0965969380ea1f8659ea5fd8fdd', + 'input': '24512b714aefd5cbc4bcc4ef44ce6c67ffc447c65460a6c6e4a92e85', + 'output': '00a7eb708eae745847173f8217efb05be13059710aee632e3f471ac3c6202b51'}, + ] + + for test in test_cases: + private_key = bytearray(reversed(unhexlify(test['private']))) + node = bip32.HDNode( + depth=0, + fingerprint=0, + child_num=0, + chain_code=bytearray(32), + private_key=private_key, + curve_name=NEM_CURVE + ) + + encrypted = node.nem_encrypt(unhexlify(test['public']), + unhexlify(test['iv']), + unhexlify(test['salt']), + unhexlify(test['input'])) + + self.assertEqual(encrypted, unhexlify(test['output'])) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_apps.nem.mosaic.py b/tests/test_apps.nem.mosaic.py new file mode 100644 index 000000000..7cee25d3e --- /dev/null +++ b/tests/test_apps.nem.mosaic.py @@ -0,0 +1,165 @@ +from common import * +from trezor.messages.NEMMosaic import NEMMosaic +from apps.nem.mosaic.helpers import get_mosaic_definition +from apps.nem.transfer import * +from apps.nem.transfer.serialize import * + + +class TestNemMosaic(unittest.TestCase): + + def test_get_mosaic_definition(self): + m = get_mosaic_definition("nem", "xem", 104) + self.assertEqual(m["name"], "XEM") + self.assertEqual(m["ticker"], " XEM") + + m = get_mosaic_definition("nem", "xxx", 104) + self.assertEqual(m, None) + + m = get_mosaic_definition("aaaa", "xxx", 104) + self.assertEqual(m, None) + + m = get_mosaic_definition("pacnem", "cheese", 104) + self.assertEqual(m["name"], "PacNEM Score Tokens") + self.assertEqual(m["ticker"], " PAC:CHS") + self.assertEqual(m["fee"], 100) + + def test_mosaic_canonicalization(self): + a = NEMMosaic() + a.namespace = 'abc' + a.quantity = 3 + a.mosaic = 'mosaic' + b = NEMMosaic() + b.namespace = 'abc' + b.quantity = 4 + b.mosaic = 'a' + c = NEMMosaic() + c.namespace = 'zzz' + c.quantity = 3 + c.mosaic = 'mosaic' + d = NEMMosaic() + d.namespace = 'abc' + d.quantity = 8 + d.mosaic = 'mosaic' + e = NEMMosaic() + e.namespace = 'aaa' + e.quantity = 1 + e.mosaic = 'mosaic' + f = NEMMosaic() + f.namespace = 'aaa' + f.quantity = 1 + f.mosaic = 'mosaicz' + g = NEMMosaic() + g.namespace = 'zzz' + g.quantity = 30 + g.mosaic = 'mosaic' + + res = canonicalize_mosaics([a, b, c, d, e, f, g]) + self.assertEqual(res, [e, f, b, a, c]) + self.assertEqual(res[2].quantity, b.quantity) + self.assertEqual(res[3].quantity, 3 + 8) # a + d + self.assertEqual(res[4].quantity, 3 + 30) # c + g + + def test_mosaic_merge(self): + a = NEMMosaic() + a.namespace = 'abc' + a.quantity = 1 + a.mosaic = 'mosaic' + b = NEMMosaic() + b.namespace = 'abc' + b.quantity = 1 + b.mosaic = 'mosaic' + + merged = merge_mosaics([a, b]) + self.assertEqual(merged[0].quantity, 2) + self.assertEqual(len(merged), 1) + + a.quantity = 1 + b.quantity = 10 + merged = merge_mosaics([a, b]) + self.assertEqual(merged[0].quantity, 11) + + a.namespace = 'abcdef' + merged = merge_mosaics([a, b]) + self.assertEqual(len(merged), 2) + + c = NEMMosaic() + c.namespace = 'abc' + c.mosaic = 'xxx' + c.quantity = 2 + merged = merge_mosaics([a, b, c]) + self.assertEqual(len(merged), 3) + + a.namespace = 'abcdef' + a.quantity = 1 + a.mosaic = 'mosaic' + b.namespace = 'abc' + b.quantity = 2 + b.mosaic = 'mosaic' + c.namespace = 'abc' + c.mosaic = 'mosaic' + c.quantity = 3 + merged = merge_mosaics([a, b, c]) + self.assertEqual(merged[0].quantity, 1) + self.assertEqual(merged[1].quantity, 5) + self.assertEqual(len(merged), 2) + + a.namespace = 'abc' + a.quantity = 1 + a.mosaic = 'mosaic' + b.namespace = 'abc' + b.quantity = 2 + b.mosaic = 'mosaic' + c.namespace = 'abc' + c.mosaic = 'mosaic' + c.quantity = 3 + merged = merge_mosaics([a, b, c]) + self.assertEqual(merged[0].quantity, 6) + self.assertEqual(len(merged), 1) + + def test_mosaic_sort(self): + a = NEMMosaic() + a.namespace = 'abcz' + a.quantity = 1 + a.mosaic = 'mosaic' + b = NEMMosaic() + b.namespace = 'abca' + b.quantity = 1 + b.mosaic = 'mosaic' + res = sort_mosaics([a, b]) + self.assertEqual(res, [b, a]) + + a.namespace = '' + b.namespace = 'a.b.c' + res = sort_mosaics([a, b]) + self.assertEqual(res, [a, b]) + + a.namespace = 'z.z.z' + b.namespace = 'a.b.c' + res = sort_mosaics([a, b]) + self.assertEqual(res, [b, a]) + + a.namespace = 'a' + b.namespace = 'a' + a.mosaic = 'mosaic' + b.mosaic = 'mosaic' + res = sort_mosaics([a, b]) + self.assertEqual(res, [a, b]) + + a.mosaic = 'www' + b.mosaic = 'aaa' + res = sort_mosaics([a, b]) + self.assertEqual(res, [b, a]) + + c = NEMMosaic() + c.namespace = 'a' + c.mosaic = 'zzz' + res = sort_mosaics([a, b, c]) + self.assertEqual(res, [b, a, c]) + + c.mosaic = 'bbb' + res = sort_mosaics([a, b, c]) + self.assertEqual(res, [b, c, a]) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_apps.nem.mosaic_creation.py b/tests/test_apps.nem.mosaic_creation.py new file mode 100644 index 000000000..6c387bfab --- /dev/null +++ b/tests/test_apps.nem.mosaic_creation.py @@ -0,0 +1,154 @@ +from common import * + +from apps.nem.helpers import * +from apps.nem.mosaic import * +from apps.nem.mosaic.serialize import * + +from trezor.crypto import hashlib +from trezor.messages.NEMSignTx import NEMSignTx +from trezor.messages.NEMMosaicCreation import NEMMosaicCreation +from trezor.messages.NEMMosaicDefinition import NEMMosaicDefinition + + +class TestNemMosaicCreation(unittest.TestCase): + + def test_nem_transaction_mosaic_creation(self): + + # http://bob.nem.ninja:8765/#/mosaic/68364353c29105e6d361ad1a42abbccbf419cfc7adb8b74c8f35d8f8bdaca3fa/0 + m = _create_msg(NEM_NETWORK_TESTNET, + 14070896, + 108000000, + 14074496, + 'gimre.games.pong', + 'paddles', + 'Paddles for the bong game.\n', + 0, + 10000, + True, + True, + 0, + 0, + '', + '', + '', + 'TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC', + 50000000000) + + t = serialize_mosaic_creation(m.transaction, m.mosaic_creation, unhexlify('994793ba1c789fa9bdea918afc9b06e2d0309beb1081ac5b6952991e4defd324')) + + self.assertEqual(t, unhexlify('014000000100009870b4d60020000000994793ba1c789fa9bdea918afc9b06e2d0309beb1081ac5b6952991e4defd32400f36f060000000080c2d600de00000020000000994793ba1c789fa9bdea918afc9b06e2d0309beb1081ac5b6952991e4defd3241f0000001000000067696d72652e67616d65732e706f6e6707000000706164646c65731b000000506164646c657320666f722074686520626f6e672067616d652e0a04000000150000000c00000064697669736962696c69747901000000301a0000000d000000696e697469616c537570706c79050000003130303030190000000d000000737570706c794d757461626c650400000074727565180000000c0000007472616e7366657261626c650400000074727565000000002800000054424d4f534149434f443446353445453543444d523233434342474f414d3258534a4252354f4c4300743ba40b000000')) + self.assertEqual(hashlib.sha3_256(t).digest(True), unhexlify('68364353c29105e6d361ad1a42abbccbf419cfc7adb8b74c8f35d8f8bdaca3fa')) + + def test_nem_transaction_mosaic_creation_with_levy(self): + # http://bob.nem.ninja:8765/#/mosaic/b2f4a98113ff1f3a8f1e9d7197aa982545297fe0aa3fa6094af8031569953a55/0 + m = _create_msg(NEM_NETWORK_TESTNET, + 21497248, + 108000000, + 21500848, + "alice.misc", + "bar", + "Special offer: get one bar extra by bying one foo!", + 0, + 1000, + False, + True, + 1, + 1, + "TALICE2GMA34CXHD7XLJQ536NM5UNKQHTORNNT2J", + "nem", + "xem", + "TBMOSAICOD4F54EE5CDMR23CCBGOAM2XSJBR5OLC", + 50000000000) + + t = serialize_mosaic_creation(m.transaction, m.mosaic_creation, unhexlify("244fa194e2509ac0d2fbc18779c2618d8c2ebb61c16a3bcbebcf448c661ba8dc"),) + + self.assertEqual(hashlib.sha3_256(t).digest(True), unhexlify('b2f4a98113ff1f3a8f1e9d7197aa982545297fe0aa3fa6094af8031569953a55')) + + # http://chain.nem.ninja/#/mosaic/e8dc14821dbea4831d9051f86158ef348001447968fc22c01644fdaf2bda75c6/0 + m = _create_msg(NEM_NETWORK_MAINNET, + 69251020, + 20000000, + 69337420, + "dim", + "coin", + "DIM COIN", + 6, + 9000000000, + False, + True, + 2, + 10, + "NCGGLVO2G3CUACVI5GNX2KRBJSQCN4RDL2ZWJ4DP", + "dim", + "coin", + "NBMOSAICOD4F54EE5CDMR23CCBGOAM2XSIUX6TRS", + 500000000) + + t = serialize_mosaic_creation(m.transaction, m.mosaic_creation, unhexlify("a1df5306355766bd2f9a64efdc089eb294be265987b3359093ae474c051d7d5a")) + self.assertEqual(t, unhexlify('0140000001000068ccaf200420000000a1df5306355766bd2f9a64efdc089eb294be265987b3359093ae474c051d7d5a002d3101000000004c0122040c01000020000000a1df5306355766bd2f9a64efdc089eb294be265987b3359093ae474c051d7d5a0f0000000300000064696d04000000636f696e0800000044494d20434f494e04000000150000000c00000064697669736962696c69747901000000361f0000000d000000696e697469616c537570706c790a000000393030303030303030301a0000000d000000737570706c794d757461626c650500000066616c7365180000000c0000007472616e7366657261626c6504000000747275654b00000002000000280000004e4347474c564f32473343554143564935474e58324b52424a5351434e3452444c325a574a3444500f0000000300000064696d04000000636f696e0a00000000000000280000004e424d4f534149434f443446353445453543444d523233434342474f414d325853495558365452530065cd1d00000000')) + self.assertEqual(hashlib.sha3_256(t).digest(True), unhexlify('e8dc14821dbea4831d9051f86158ef348001447968fc22c01644fdaf2bda75c6')) + + def test_nem_transaction_mosaic_creation_with_description(self): + # http://chain.nem.ninja/#/mosaic/269c6fda657aba3053a0e5b138c075808cc20e244e1182d9b730798b60a1f77b/0 + m = _create_msg(NEM_NETWORK_MAINNET, + 26729938, + 108000000, + 26733538, + "jabo38", + "red_token", + "This token is to celebrate the release of Namespaces and Mosaics " + "on the NEM system. This token was the fist ever mosaic created " + "other than nem.xem. There are only 10,000 Red Tokens that will " + "ever be created. It has no levy and can be traded freely among " + "third parties.", + 2, + 10000, + False, + True, + 0, + 0, + "", + "", + "", + "NBMOSAICOD4F54EE5CDMR23CCBGOAM2XSIUX6TRS", + 50000000000) + t = serialize_mosaic_creation(m.transaction, m.mosaic_creation, unhexlify("58956ac77951622dc5f1c938affbf017c458e30e6b21ddb5783d38b302531f23")) + + self.assertEqual(t, unhexlify('0140000001000068d2dd97012000000058956ac77951622dc5f1c938affbf017c458e30e6b21ddb5783d38b302531f2300f36f0600000000e2eb9701c80100002000000058956ac77951622dc5f1c938affbf017c458e30e6b21ddb5783d38b302531f2317000000060000006a61626f3338090000007265645f746f6b656e0c0100005468697320746f6b656e20697320746f2063656c656272617465207468652072656c65617365206f66204e616d6573706163657320616e64204d6f7361696373206f6e20746865204e454d2073797374656d2e205468697320746f6b656e207761732074686520666973742065766572206d6f736169632063726561746564206f74686572207468616e206e656d2e78656d2e20546865726520617265206f6e6c792031302c3030302052656420546f6b656e7320746861742077696c6c206576657220626520637265617465642e20497420686173206e6f206c65767920616e642063616e2062652074726164656420667265656c7920616d6f6e6720746869726420706172746965732e04000000150000000c00000064697669736962696c69747901000000321a0000000d000000696e697469616c537570706c790500000031303030301a0000000d000000737570706c794d757461626c650500000066616c7365180000000c0000007472616e7366657261626c65040000007472756500000000280000004e424d4f534149434f443446353445453543444d523233434342474f414d3258534955583654525300743ba40b000000')) + self.assertEqual(hashlib.sha3_256(t).digest(True), unhexlify('269c6fda657aba3053a0e5b138c075808cc20e244e1182d9b730798b60a1f77b')) + + +def _create_msg(network: int, timestamp: int, fee: int, deadline: int, + namespace: str, mosaic: str, description: str, + divisibility: int, supply: int, mutable_supply: bool, transferable: bool, + levy_type: int, levy_fee: int, levy_address: str, levy_namespace: str, + levy_mosaic: str, creation_sink: str, creation_fee: int): + m = NEMSignTx() + m.transaction = NEMTransactionCommon() + m.transaction.network = network + m.transaction.timestamp = timestamp + m.transaction.fee = fee + m.transaction.deadline = deadline + + m.mosaic_creation = NEMMosaicCreation() + m.mosaic_creation.sink = creation_sink + m.mosaic_creation.fee = creation_fee + + m.mosaic_creation.definition = NEMMosaicDefinition() + m.mosaic_creation.definition.namespace = namespace + m.mosaic_creation.definition.mosaic = mosaic + m.mosaic_creation.definition.description = description + m.mosaic_creation.definition.divisibility = divisibility + m.mosaic_creation.definition.supply = supply + m.mosaic_creation.definition.mutable_supply = mutable_supply + m.mosaic_creation.definition.transferable = transferable + m.mosaic_creation.definition.levy = levy_type + m.mosaic_creation.definition.fee = levy_fee + m.mosaic_creation.definition.levy_address = levy_address + m.mosaic_creation.definition.levy_namespace = levy_namespace + m.mosaic_creation.definition.levy_mosaic = levy_mosaic + return m + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_apps.nem.mosaic_supply_change.py b/tests/test_apps.nem.mosaic_supply_change.py new file mode 100644 index 000000000..424b0a67e --- /dev/null +++ b/tests/test_apps.nem.mosaic_supply_change.py @@ -0,0 +1,91 @@ +from common import * + +from apps.nem.helpers import * +from apps.nem.mosaic import * +from apps.nem.mosaic.serialize import * + +from trezor.crypto import hashlib +from trezor.messages.NEMSignTx import NEMSignTx +from trezor.messages.NEMMosaicSupplyChange import NEMMosaicSupplyChange + + +class TestNemMosaicSupplyChange(unittest.TestCase): + + def test_nem_transaction_create_mosaic_supply_change(self): + + # http://bigalice2.nem.ninja:7890/transaction/get?hash=33a50fdd4a54913643a580b2af08b9a5b51b7cee922bde380e84c573a7969c50 + m = _create_msg(NEM_NETWORK_TESTNET, + 14071648, + 108000000, + 14075248, + "gimre.games.pong", + "paddles", + 1, + 1234) + t = serialize_mosaic_supply_change(m.transaction, m.supply_change, unhexlify("994793ba1c789fa9bdea918afc9b06e2d0309beb1081ac5b6952991e4defd324")) + + self.assertEqual(hashlib.sha3_256(t).digest(True), + unhexlify('33a50fdd4a54913643a580b2af08b9a5b51b7cee922bde380e84c573a7969c50')) + + # http://bigalice2.nem.ninja:7890/transaction/get?hash=1ce8e8894d077a66ff22294b000825d090a60742ec407efd80eb8b19657704f2 + m = _create_msg(NEM_NETWORK_TESTNET, + 14126909, + 108000000, + 14130509, + "jabo38_ltd.fuzzy_kittens_cafe", + "coupons", + 2, + 1) + t = serialize_mosaic_supply_change(m.transaction, m.supply_change, unhexlify("84afa1bbc993b7f5536344914dde86141e61f8cbecaf8c9cefc07391f3287cf5")) + + self.assertEqual(hashlib.sha3_256(t).digest(True), + unhexlify('1ce8e8894d077a66ff22294b000825d090a60742ec407efd80eb8b19657704f2')) + + # http://bigalice3.nem.ninja:7890/transaction/get?hash=694e493e9576d2bcf60d85747e302ac2e1cc27783187947180d4275a713ff1ff + m = _create_msg(NEM_NETWORK_MAINNET, + 53377685, + 20000000, + 53464085, + "abvapp", + "abv", + 1, + 9000000) + t = serialize_mosaic_supply_change(m.transaction, m.supply_change, unhexlify("b7ccc27b21ba6cf5c699a8dc86ba6ba98950442597ff9fa30e0abe0f5f4dd05d")) + + self.assertEqual(hashlib.sha3_256(t).digest(True), + unhexlify('694e493e9576d2bcf60d85747e302ac2e1cc27783187947180d4275a713ff1ff')) + + # http://bigalice3.nem.ninja:7890/transaction/get?hash=09836334e123970e068d5b411e4d1df54a3ead10acf1ad5935a2cdd9f9680185 + m = _create_msg(NEM_NETWORK_MAINNET, + 55176304, + 20000000, + 55262704, + "sushi", + "wasabi", + 2, + 20) + t = serialize_mosaic_supply_change(m.transaction, m.supply_change, unhexlify("75f001a8641e2ce5c4386883dda561399ed346177411b492a677b73899502f13")) + + self.assertEqual(hashlib.sha3_256(t).digest(True), + unhexlify('09836334e123970e068d5b411e4d1df54a3ead10acf1ad5935a2cdd9f9680185')) + + +def _create_msg(network: int, timestamp: int, fee: int, deadline: int, + namespace: str, mosaic: str, mod_type: int, delta: int): + m = NEMSignTx() + m.transaction = NEMTransactionCommon() + m.transaction.network = network + m.transaction.timestamp = timestamp + m.transaction.fee = fee + m.transaction.deadline = deadline + + m.supply_change = NEMMosaicSupplyChange() + m.supply_change.namespace = namespace + m.supply_change.mosaic = mosaic + m.supply_change.type = mod_type + m.supply_change.delta = delta + return m + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_apps.nem.multisig.aggregate_modification.py b/tests/test_apps.nem.multisig.aggregate_modification.py new file mode 100644 index 000000000..3b145d3a9 --- /dev/null +++ b/tests/test_apps.nem.multisig.aggregate_modification.py @@ -0,0 +1,98 @@ +from common import * + +from apps.nem.helpers import * +from apps.nem.multisig import * +from apps.nem.multisig.serialize import * + +from trezor.crypto import hashlib +from trezor.messages.NEMSignTx import NEMSignTx +from trezor.messages.NEMAggregateModification import NEMAggregateModification +from trezor.messages.NEMCosignatoryModification import NEMCosignatoryModification +from trezor.messages.NEMTransactionCommon import NEMTransactionCommon + + +class TestNemMultisigAggregateModification(unittest.TestCase): + + def test_nem_transaction_aggregate_modification(self): + # http://bob.nem.ninja:8765/#/aggregate/6a55471b17159e5b6cd579c421e95a4e39d92e3f78b0a55ee337e785a601d3a2 + m = _create_msg(NEM_NETWORK_TESTNET, + 0, + 22000000, + 0, + 2, + 0) + t = serialize_aggregate_modification(m.transaction, m.aggregate_modification, unhexlify("462ee976890916e54fa825d26bdd0235f5eb5b6a143c199ab0ae5ee9328e08ce")) + + serialize_cosignatory_modification(t, 1, unhexlify( + "994793ba1c789fa9bdea918afc9b06e2d0309beb1081ac5b6952991e4defd324")) + serialize_cosignatory_modification(t, 1, unhexlify( + "c54d6e33ed1446eedd7f7a80a588dd01857f723687a09200c1917d5524752f8b")) + + self.assertEqual(hashlib.sha3_256(t).digest(True), + unhexlify("6a55471b17159e5b6cd579c421e95a4e39d92e3f78b0a55ee337e785a601d3a2")) + + # http://chain.nem.ninja/#/aggregate/cc64ca69bfa95db2ff7ac1e21fe6d27ece189c603200ebc9778d8bb80ca25c3c + m = _create_msg(NEM_NETWORK_MAINNET, + 0, + 40000000, + 0, + 5, + 0) + t = serialize_aggregate_modification(m.transaction, m.aggregate_modification, unhexlify("f41b99320549741c5cce42d9e4bb836d98c50ed5415d0c3c2912d1bb50e6a0e5")) + + serialize_cosignatory_modification(t, 1, unhexlify( + "1fbdbdde28daf828245e4533765726f0b7790e0b7146e2ce205df3e86366980b")) + serialize_cosignatory_modification(t, 1, unhexlify( + "f94e8702eb1943b23570b1b83be1b81536df35538978820e98bfce8f999e2d37")) + serialize_cosignatory_modification(t, 1, unhexlify( + "826cedee421ff66e708858c17815fcd831a4bb68e3d8956299334e9e24380ba8")) + serialize_cosignatory_modification(t, 1, unhexlify( + "719862cd7d0f4e875a6a0274c9a1738f38f40ad9944179006a54c34724c1274d")) + serialize_cosignatory_modification(t, 1, unhexlify( + "43aa69177018fc3e2bdbeb259c81cddf24be50eef9c5386db51d82386c41475a")) + + self.assertEqual(hashlib.sha3_256(t).digest(True), + unhexlify("cc64ca69bfa95db2ff7ac1e21fe6d27ece189c603200ebc9778d8bb80ca25c3c")) + + def test_nem_transaction_aggregate_modification_relative_change(self): + # http://bob.nem.ninja:8765/#/aggregate/1fbdae5ba753e68af270930413ae90f671eb8ab58988116684bac0abd5726584 + m = _create_msg(NEM_NETWORK_TESTNET, + 6542254, + 40000000, + 6545854, + 4, + 2) + t = serialize_aggregate_modification(m.transaction, m.aggregate_modification, unhexlify("6bf7849c1eec6a2002995cc457dc00c4e29bad5c88de63f51e42dfdcd7b2131d")) + + serialize_cosignatory_modification(t, 1, unhexlify( + "5f53d076c8c3ec3110b98364bc423092c3ec2be2b1b3c40fd8ab68d54fa39295")) + serialize_cosignatory_modification(t, 1, unhexlify( + "9eb199c2b4d406f64cb7aa5b2b0815264b56ba8fe44d558a6cb423a31a33c4c2")) + serialize_cosignatory_modification(t, 1, unhexlify( + "94b2323dab23a3faba24fa6ddda0ece4fbb06acfedd74e76ad9fae38d006882b")) + serialize_cosignatory_modification(t, 1, unhexlify( + "d88c6ee2a2cd3929d0d76b6b14ecb549d21296ab196a2b3a4cb2536bcce32e87")) + serialize_minimum_cosignatories(t, 2) + + self.assertEqual(hashlib.sha3_256(t).digest(True), + unhexlify("1fbdae5ba753e68af270930413ae90f671eb8ab58988116684bac0abd5726584")) + + +def _create_msg(network: int, timestamp: int, fee: int, deadline: int, + modifications: int, relative_change: int): + m = NEMSignTx() + m.transaction = NEMTransactionCommon() + m.transaction.network = network + m.transaction.timestamp = timestamp + m.transaction.fee = fee + m.transaction.deadline = deadline + + m.aggregate_modification = NEMAggregateModification() + for i in range(modifications): + m.aggregate_modification.modifications.append(NEMCosignatoryModification()) + m.aggregate_modification.relative_change = relative_change + return m + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_apps.nem.multisig.py b/tests/test_apps.nem.multisig.py new file mode 100644 index 000000000..df85f3b7a --- /dev/null +++ b/tests/test_apps.nem.multisig.py @@ -0,0 +1,116 @@ +from common import * + +from apps.nem.helpers import * +from apps.nem.multisig import * +from apps.nem.multisig.serialize import * +from apps.nem.namespace import * +from apps.nem.namespace.serialize import * + +from trezor.messages.NEMSignTx import NEMSignTx +from trezor.messages.NEMAggregateModification import NEMAggregateModification +from trezor.messages.NEMProvisionNamespace import NEMProvisionNamespace +from trezor.messages.NEMCosignatoryModification import NEMCosignatoryModification + + +class TestNemMultisig(unittest.TestCase): + + def test_nem_multisig(self): + # http://bob.nem.ninja:8765/#/multisig/7d3a7087023ee29005262016706818579a2b5499eb9ca76bad98c1e6f4c46642 + m = _create_msg(NEM_NETWORK_TESTNET, + 3939039, + 16000000, + 3960639, + 1, + 0) + base_tx = serialize_aggregate_modification(m.transaction, m.aggregate_modification, unhexlify("abac2ee3d4aaa7a3bfb65261a00cc04c761521527dd3f2cf741e2815cbba83ac")) + + base_tx = serialize_cosignatory_modification(base_tx, 2, unhexlify("e6cff9b3725a91f31089c3acca0fac3e341c00b1c8c6e9578f66c4514509c3b3")) + m = _create_common_msg(NEM_NETWORK_TESTNET, + 3939039, + 6000000, + 3960639) + multisig = serialize_multisig(m, unhexlify("59d89076964742ef2a2089d26a5aa1d2c7a7bb052a46c1de159891e91ad3d76e"), base_tx) + + self.assertEqual(multisig, unhexlify("0410000001000098df1a3c002000000059d89076964742ef2a2089d26a5aa1d2c7a7bb052a46c1de159891e91ad3d76e808d5b00000000003f6f3c006c0000000110000001000098df1a3c0020000000abac2ee3d4aaa7a3bfb65261a00cc04c761521527dd3f2cf741e2815cbba83ac0024f400000000003f6f3c0001000000280000000200000020000000e6cff9b3725a91f31089c3acca0fac3e341c00b1c8c6e9578f66c4514509c3b3")) + + address_pubkey = unhexlify("abac2ee3d4aaa7a3bfb65261a00cc04c761521527dd3f2cf741e2815cbba83ac") + m = _create_common_msg(NEM_NETWORK_TESTNET, + 3939891, + 6000000, + 3961491) + multisig = serialize_multisig_signature(m, unhexlify("71cba4f2a28fd19f902ba40e9937994154d9eeaad0631d25d525ec37922567d4"), base_tx, address_pubkey) + + self.assertEqual(multisig, unhexlify("0210000001000098331e3c002000000071cba4f2a28fd19f902ba40e9937994154d9eeaad0631d25d525ec37922567d4808d5b000000000093723c0024000000200000008ec165580bdabfd31ce6007a1748ce5bdf30eab7a214743097de3bc822ac7e002800000054435258595551494d464137414f474c354c463359574c43375641424c59554d4a35414342554e4c")) + + def test_nem_multisig_2(self): + # http://chain.nem.ninja/#/multisig/1016cf3bdd61bd57b9b2b07b6ff2dee390279d8d899265bdc23d42360abe2e6c + m = _create_provision_msg(NEM_NETWORK_MAINNET, + 59414272, + 20000000, + 59500672, + "dim", + "", + "NAMESPACEWH4MKFMBCVFERDPOOP4FK7MTBXDPZZA", + 5000000000) + base_tx = serialize_provision_namespace(m.transaction, m.provision_namespace, unhexlify("a1df5306355766bd2f9a64efdc089eb294be265987b3359093ae474c051d7d5a")) + + m = _create_common_msg(NEM_NETWORK_MAINNET, + 59414272, + 6000000, + 59500672) + + multisig = serialize_multisig(m, unhexlify("cfe58463f0eaebceb5d00717f8aead49171a5d7c08f6b1299bd534f11715acc9"), base_tx) + self.assertEqual(multisig, unhexlify("041000000100006800978a0320000000cfe58463f0eaebceb5d00717f8aead49171a5d7c08f6b1299bd534f11715acc9808d5b000000000080e88b037b000000012000000100006800978a0320000000a1df5306355766bd2f9a64efdc089eb294be265987b3359093ae474c051d7d5a002d31010000000080e88b03280000004e414d4553504143455748344d4b464d42435646455244504f4f5034464b374d54425844505a5a4100f2052a010000000300000064696dffffffff")) + + m = _create_common_msg(NEM_NETWORK_MAINNET, + 59414342, + 6000000, + 59500742) + address_pubkey = unhexlify("a1df5306355766bd2f9a64efdc089eb294be265987b3359093ae474c051d7d5a") + multisig = serialize_multisig_signature(m, unhexlify("1b49b80203007117d034e45234ffcdf402c044aeef6dbb06351f346ca892bce2"), base_tx, address_pubkey) + self.assertEqual(multisig, unhexlify("021000000100006846978a03200000001b49b80203007117d034e45234ffcdf402c044aeef6dbb06351f346ca892bce2808d5b0000000000c6e88b032400000020000000bfa2088f7720f89dd4664d650e321dabd02fab61b7355bc88a391a848a49786a280000004e4444524733554542354c5a5a5a4d445742453452544b5a4b37334a424850414957424843464d56")) + + m = _create_common_msg(NEM_NETWORK_MAINNET, + 59414381, + 6000000, + 59500781) + multisig = serialize_multisig_signature(m, unhexlify("7ba4b39209f1b9846b098fe43f74381e43cb2882ccde780f558a63355840aa87"), base_tx, address_pubkey) + self.assertEqual(multisig, unhexlify("02100000010000686d978a03200000007ba4b39209f1b9846b098fe43f74381e43cb2882ccde780f558a63355840aa87808d5b0000000000ede88b032400000020000000bfa2088f7720f89dd4664d650e321dabd02fab61b7355bc88a391a848a49786a280000004e4444524733554542354c5a5a5a4d445742453452544b5a4b37334a424850414957424843464d56")) + + +def _create_common_msg(network: int, timestamp: int, fee: int, deadline: int): + m = NEMTransactionCommon() + m.network = network + m.timestamp = timestamp + m.fee = fee + m.deadline = deadline + return m + + +def _create_msg(network: int, timestamp: int, fee: int, deadline: int, + modifications: int, relative_change: int): + m = NEMSignTx() + m.transaction = _create_common_msg(network, timestamp, fee, deadline) + + m.aggregate_modification = NEMAggregateModification() + for i in range(modifications): + m.aggregate_modification.modifications.append(NEMCosignatoryModification()) + m.aggregate_modification.relative_change = relative_change + return m + + +def _create_provision_msg(network: int, timestamp: int, fee: int, deadline: int, + name: str, parent: str, sink: str, rental_fee: int): + m = NEMSignTx() + m.transaction = _create_common_msg(network, timestamp, fee, deadline) + + m.provision_namespace = NEMProvisionNamespace() + m.provision_namespace.namespace = name + m.provision_namespace.parent = parent + m.provision_namespace.sink = sink + m.provision_namespace.fee = rental_fee + return m + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_apps.nem.namespace.py b/tests/test_apps.nem.namespace.py new file mode 100644 index 000000000..c9375800d --- /dev/null +++ b/tests/test_apps.nem.namespace.py @@ -0,0 +1,72 @@ +from common import * + +from apps.nem.helpers import * +from apps.nem.namespace import * +from apps.nem.namespace.serialize import * + +from trezor.crypto import hashlib +from trezor.messages.NEMProvisionNamespace import NEMProvisionNamespace +from trezor.messages.NEMSignTx import NEMSignTx + + +class TestNemNamespace(unittest.TestCase): + + def test_create_provision_namespace(self): + + # http://bob.nem.ninja:8765/#/transfer/0acbf8df91e6a65dc56c56c43d65f31ff2a6a48d06fc66e78c7f3436faf3e74f + m = _create_msg(NEM_NETWORK_TESTNET, + 56999445, + 20000000, + 57003045, + 'gimre', + '', + 'TAMESPACEWH4MKFMBCVFERDPOOP4FK7MTDJEYP35', + 5000000000) + t = serialize_provision_namespace(m.transaction, m.provision_namespace, unhexlify('84afa1bbc993b7f5536344914dde86141e61f8cbecaf8c9cefc07391f3287cf5')) + self.assertEqual(hashlib.sha3_256(t).digest(True), unhexlify('f7cab28da57204d01a907c697836577a4ae755e6c9bac60dcc318494a22debb3')) + + # http://bob.nem.ninja:8765/#/namespace/7ddd5fe607e1bfb5606e0ac576024c318c8300d237273117d4db32a60c49524d + m = _create_msg(NEM_NETWORK_TESTNET, + 21496797, + 108000000, + 21500397, + 'misc', + 'alice', + 'TAMESPACEWH4MKFMBCVFERDPOOP4FK7MTDJEYP35', + 5000000000) + t = serialize_provision_namespace(m.transaction, m.provision_namespace, unhexlify('244fa194e2509ac0d2fbc18779c2618d8c2ebb61c16a3bcbebcf448c661ba8dc')) + + self.assertEqual(hashlib.sha3_256(t).digest(True), unhexlify('7ddd5fe607e1bfb5606e0ac576024c318c8300d237273117d4db32a60c49524d')) + + # http://chain.nem.ninja/#/namespace/57071aad93ca125dc231dc02c07ad8610cd243d35068f9b36a7d231383907569 + m = _create_msg(NEM_NETWORK_MAINNET, + 26699717, + 108000000, + 26703317, + 'sex', + '', + 'NAMESPACEWH4MKFMBCVFERDPOOP4FK7MTBXDPZZA', + 50000000000) + t = serialize_provision_namespace(m.transaction, m.provision_namespace, unhexlify('9f3c14f304309c8b72b2821339c4428793b1518bea72d58dd01f19d523518614')) + + self.assertEqual(hashlib.sha3_256(t).digest(True), unhexlify('57071aad93ca125dc231dc02c07ad8610cd243d35068f9b36a7d231383907569')) + + +def _create_msg(network: int, timestamp: int, fee: int, deadline: int, + name: str, parent: str, sink: str, rental_fee: int): + m = NEMSignTx() + m.transaction = NEMTransactionCommon() + m.transaction.network = network + m.transaction.timestamp = timestamp + m.transaction.fee = fee + m.transaction.deadline = deadline + m.provision_namespace = NEMProvisionNamespace() + m.provision_namespace.namespace = name + m.provision_namespace.parent = parent + m.provision_namespace.sink = sink + m.provision_namespace.fee = rental_fee + return m + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_apps.nem.transfer.py b/tests/test_apps.nem.transfer.py new file mode 100644 index 000000000..356dc27ba --- /dev/null +++ b/tests/test_apps.nem.transfer.py @@ -0,0 +1,119 @@ +from common import * + +from apps.nem.helpers import * +from apps.nem.mosaic import * +from apps.nem.transfer import * +from apps.nem.transfer.serialize import * + +from trezor.crypto import hashlib +from trezor.messages.NEMTransfer import NEMTransfer +from trezor.messages.NEMSignTx import NEMSignTx + + +class TestNemTransfer(unittest.TestCase): + + def test_create_transfer(self): + + # http://bob.nem.ninja:8765/#/transfer/0acbf8df91e6a65dc56c56c43d65f31ff2a6a48d06fc66e78c7f3436faf3e74f + m = _create_msg(NEM_NETWORK_TESTNET, + 0, + 0, + 0, + 'TBGIMRE4SBFRUJXMH7DVF2IBY36L2EDWZ37GVSC4', + 50000000000000) + + t = serialize_transfer(m.transaction, m.transfer, unhexlify('e59ef184a612d4c3c4d89b5950eb57262c69862b2f96e59c5043bf41765c482f')) + self.assertEqual(t, unhexlify('01010000010000980000000020000000e59ef184a612d4c3c4d89b5950eb57262c69862b2f96e59c5043bf41765c482f00000000000000000000000028000000544247494d52453453424652554a584d48374456463249425933364c324544575a3337475653433400203d88792d000000000000')) + self.assertEqual(hashlib.sha3_256(t).digest(True), unhexlify('0acbf8df91e6a65dc56c56c43d65f31ff2a6a48d06fc66e78c7f3436faf3e74f')) + + def test_create_transfer_with_payload(self): + + # http://chain.nem.ninja/#/transfer/e90e98614c7598fbfa4db5411db1b331d157c2f86b558fb7c943d013ed9f71cb + m = _create_msg(NEM_NETWORK_MAINNET, + 0, + 0, + 0, + 'NBT3WHA2YXG2IR4PWKFFMO772JWOITTD2V4PECSB', + 5175000000000) + + t = serialize_transfer(m.transaction, m.transfer, + unhexlify('8d07f90fb4bbe7715fa327c926770166a11be2e494a970605f2e12557f66c9b9'), + bytearray('Good luck!')) + self.assertEqual(hashlib.sha3_256(t).digest(True), unhexlify('e90e98614c7598fbfa4db5411db1b331d157c2f86b558fb7c943d013ed9f71cb')) + + def test_create_transfer_with_encrypted_payload(self): + + # http://chain.nem.ninja/#/transfer/40e89160e6f83d37f7c82defc0afe2c1605ae8c919134570a51dd27ea1bb516c + m = _create_msg(NEM_NETWORK_MAINNET, + 77229, + 30000000, + 80829, + 'NALICEPFLZQRZGPRIJTMJOCPWDNECXTNNG7QLSG3', + 30000000) + + t = serialize_transfer(m.transaction, m.transfer, + unhexlify('f85ab43dad059b9d2331ddacc384ad925d3467f03207182e01296bacfb242d01'), + unhexlify('4d9dcf9186967d30be93d6d5404ded22812dbbae7c3f0de501bcd7228cba45bded13000eec7b4c6215fc4d3588168c9218167cec98e6977359153a4132e050f594548e61e0dc61c153f0f53c5e65c595239c9eb7c4e7d48e0f4bb8b1dd2f5ddc'), + True) + self.assertEqual(hashlib.sha3_256(t).digest(True), unhexlify('40e89160e6f83d37f7c82defc0afe2c1605ae8c919134570a51dd27ea1bb516c')) + + def test_create_transfer_with_mosaic(self): + + # http://bob.nem.ninja:8765/#/transfer/3409d9ece28d6296d6d5e220a7e3cb8641a3fb235ffcbd20c95da64f003ace6c + m = _create_msg(NEM_NETWORK_TESTNET, + 14072100, + 194000000, + 14075700, + 'TBLOODPLWOWMZ2TARX4RFPOSOWLULHXMROBN2WXI', + 3000000, + 2) + + t = serialize_transfer(m.transaction, m.transfer, + unhexlify('994793ba1c789fa9bdea918afc9b06e2d0309beb1081ac5b6952991e4defd324'), + bytearray('sending you 3 pairs of paddles\n'), + False) + + self.assertEqual(t, unhexlify('010100000200009824b9d60020000000994793ba1c789fa9bdea918afc9b06e2d0309beb1081ac5b6952991e4defd3248034900b0000000034c7d6002800000054424c4f4f44504c574f574d5a3254415258345246504f534f574c554c48584d524f424e32575849c0c62d000000000027000000010000001f00000073656e64696e6720796f752033207061697273206f6620706164646c65730a02000000')) + + serialize_mosaic(t, 'gimre.games.pong', 'paddles', 2) + serialize_mosaic(t, 'nem', 'xem', 44000000) + + self.assertEqual(hashlib.sha3_256(t).digest(True), unhexlify('3409d9ece28d6296d6d5e220a7e3cb8641a3fb235ffcbd20c95da64f003ace6c')) + + # http://chain.nem.ninja/#/transfer/882dca18dcbe075e15e0ec5a1d7e6ccd69cc0f1309ffd3fde227bfbc107b3f6e + m = _create_msg(NEM_NETWORK_MAINNET, + 26730750, + 179500000, + 26734350, + 'NBE223WPKEBHQPCYUC4U4CDUQCRRFMPZLOQLB5OP', + 1000000, + 1) + + t = serialize_transfer(m.transaction, m.transfer, + unhexlify('f85ab43dad059b9d2331ddacc384ad925d3467f03207182e01296bacfb242d01'), + bytearray('enjoy! :)'), + False) + serialize_mosaic(t, 'imre.g', 'tokens', 1) + + self.assertEqual(hashlib.sha3_256(t).digest(True), unhexlify('882dca18dcbe075e15e0ec5a1d7e6ccd69cc0f1309ffd3fde227bfbc107b3f6e')) + + +def _create_msg(network: int, timestamp: int, fee: int, deadline: int, + recipient: str, amount: int, mosaics: int = 0): + m = NEMSignTx() + m.transaction = NEMTransactionCommon() + m.transaction.network = network + m.transaction.timestamp = timestamp + m.transaction.fee = fee + m.transaction.deadline = deadline + m.transfer = NEMTransfer() + m.transfer.recipient = recipient + m.transfer.amount = amount + m.transfer.mosaics = list() + for i in range(mosaics): + m.transfer.mosaics.append(NEMMosaic()) + return m + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/codegen/gen_nem_mosaics.py b/tools/codegen/gen_nem_mosaics.py new file mode 100755 index 000000000..b24dfcd77 --- /dev/null +++ b/tools/codegen/gen_nem_mosaics.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +import json + + +def format_str(value): + return '"' + value + '"' + + +def format_primitive(value): + if isinstance(value, int): + return value + elif isinstance(value, str): + return format_str(value) + elif isinstance(value, list): + return value + else: + raise TypeError + + +fields = [ + 'name', + 'ticker', + 'namespace', + 'mosaic', + 'divisibility', + 'levy', + 'fee', + 'levy_namespace', + 'levy_mosaic', + 'networks', +] + +mosaics = json.load(open('../../vendor/trezor-common/defs/nem/nem_mosaics.json', 'r')) + +print('# generated using gen_nem_mosaics.py from trezor-common nem_mosaics.json - do not edit directly!') +print('') +print('mosaics = [') +for m in mosaics: + print(' {') + for name in fields: + if name in m: + print(' %s: %s,' % (format_str(name), format_primitive(m[name]))) + # else: + # print(' %s: None,' % format_str(name)) + print(' },') +print(']')