diff --git a/core/SConscript.firmware b/core/SConscript.firmware index 626e21389..a46436be7 100644 --- a/core/SConscript.firmware +++ b/core/SConscript.firmware @@ -35,6 +35,7 @@ CPPDEFINES_MOD += [ ('USE_MONERO', '1'), ('USE_CARDANO', '1'), ('USE_NEM', '1'), + ('USE_EOS', '1'), ] SOURCE_MOD += [ 'embed/extmod/modtrezorcrypto/modtrezorcrypto.c', diff --git a/core/SConscript.unix b/core/SConscript.unix index d66bdb5d2..45e802a43 100644 --- a/core/SConscript.unix +++ b/core/SConscript.unix @@ -34,6 +34,7 @@ CPPDEFINES_MOD += [ ('USE_MONERO', '1'), ('USE_CARDANO', '1'), ('USE_NEM', '1'), + ('USE_EOS', '1'), ] SOURCE_MOD += [ 'embed/extmod/modtrezorcrypto/modtrezorcrypto.c', diff --git a/core/embed/extmod/modtrezorcrypto/modtrezorcrypto-secp256k1.h b/core/embed/extmod/modtrezorcrypto/modtrezorcrypto-secp256k1.h index a5cb41f79..d0483e083 100644 --- a/core/embed/extmod/modtrezorcrypto/modtrezorcrypto-secp256k1.h +++ b/core/embed/extmod/modtrezorcrypto/modtrezorcrypto-secp256k1.h @@ -85,8 +85,17 @@ static int ethereum_is_canonical(uint8_t v, uint8_t signature[64]) { return (v & 2) == 0; } +static int eos_is_canonical(uint8_t v, uint8_t signature[64]) { + (void)v; + return !(signature[0] & 0x80) && + !(signature[0] == 0 && !(signature[1] & 0x80)) && + !(signature[32] & 0x80) && + !(signature[32] == 0 && !(signature[33] & 0x80)); +} + enum { CANONICAL_SIG_ETHEREUM = 1, + CANONICAL_SIG_EOS = 2, }; /// def sign(secret_key: bytes, digest: bytes, compressed: bool = True, @@ -106,6 +115,9 @@ STATIC mp_obj_t mod_trezorcrypto_secp256k1_sign(size_t n_args, case CANONICAL_SIG_ETHEREUM: is_canonical = ethereum_is_canonical; break; + case CANONICAL_SIG_EOS: + is_canonical = eos_is_canonical; + break; } if (sk.len != 32) { mp_raise_ValueError("Invalid length of secret key"); @@ -236,6 +248,8 @@ STATIC const mp_rom_map_elem_t mod_trezorcrypto_secp256k1_globals_table[] = { MP_ROM_PTR(&mod_trezorcrypto_secp256k1_multiply_obj)}, {MP_ROM_QSTR(MP_QSTR_CANONICAL_SIG_ETHEREUM), MP_OBJ_NEW_SMALL_INT(CANONICAL_SIG_ETHEREUM)}, + {MP_ROM_QSTR(MP_QSTR_CANONICAL_SIG_EOS), + MP_OBJ_NEW_SMALL_INT(CANONICAL_SIG_EOS)}, }; STATIC MP_DEFINE_CONST_DICT(mod_trezorcrypto_secp256k1_globals, mod_trezorcrypto_secp256k1_globals_table); diff --git a/core/embed/extmod/modtrezorui/display.h b/core/embed/extmod/modtrezorui/display.h index baffc9300..b1fc5cb65 100644 --- a/core/embed/extmod/modtrezorui/display.h +++ b/core/embed/extmod/modtrezorui/display.h @@ -20,8 +20,8 @@ #ifndef __DISPLAY_H__ #define __DISPLAY_H__ -#include #include +#include #if TREZOR_MODEL == T diff --git a/core/src/apps/eos/__init__.py b/core/src/apps/eos/__init__.py new file mode 100755 index 000000000..5ba816604 --- /dev/null +++ b/core/src/apps/eos/__init__.py @@ -0,0 +1,13 @@ +from trezor import wire +from trezor.messages import MessageType + +from apps.common import HARDENED + +CURVE = "secp256k1" + + +def boot(): + ns = [[CURVE, HARDENED | 44, HARDENED | 194]] + + wire.add(MessageType.EosGetPublicKey, __name__, "get_public_key", ns) + wire.add(MessageType.EosSignTx, __name__, "sign_tx", ns) diff --git a/core/src/apps/eos/actions/__init__.py b/core/src/apps/eos/actions/__init__.py new file mode 100644 index 000000000..4c6456499 --- /dev/null +++ b/core/src/apps/eos/actions/__init__.py @@ -0,0 +1,120 @@ +from trezor.crypto.hashlib import sha256 +from trezor.messages.EosTxActionRequest import EosTxActionRequest +from trezor.messages.MessageType import EosTxActionAck +from trezor.utils import HashWriter + +from apps.eos import helpers, writers +from apps.eos.actions import layout + + +async def process_action(ctx, sha, action): + name = helpers.eos_name_to_string(action.common.name) + account = helpers.eos_name_to_string(action.common.account) + + if not check_action(action, name, account): + raise ValueError("Invalid action") + + w = bytearray() + if account == "eosio": + if name == "buyram": + await layout.confirm_action_buyram(ctx, action.buy_ram) + writers.write_action_buyram(w, action.buy_ram) + elif name == "buyrambytes": + await layout.confirm_action_buyrambytes(ctx, action.buy_ram_bytes) + writers.write_action_buyrambytes(w, action.buy_ram_bytes) + elif name == "sellram": + await layout.confirm_action_sellram(ctx, action.sell_ram) + writers.write_action_sellram(w, action.sell_ram) + elif name == "delegatebw": + await layout.confirm_action_delegate(ctx, action.delegate) + writers.write_action_delegate(w, action.delegate) + elif name == "undelegatebw": + await layout.confirm_action_undelegate(ctx, action.undelegate) + writers.write_action_undelegate(w, action.undelegate) + elif name == "refund": + await layout.confirm_action_refund(ctx, action.refund) + writers.write_action_refund(w, action.refund) + elif name == "voteproducer": + await layout.confirm_action_voteproducer(ctx, action.vote_producer) + writers.write_action_voteproducer(w, action.vote_producer) + elif name == "updateauth": + await layout.confirm_action_updateauth(ctx, action.update_auth) + writers.write_action_updateauth(w, action.update_auth) + elif name == "deleteauth": + await layout.confirm_action_deleteauth(ctx, action.delete_auth) + writers.write_action_deleteauth(w, action.delete_auth) + elif name == "linkauth": + await layout.confirm_action_linkauth(ctx, action.link_auth) + writers.write_action_linkauth(w, action.link_auth) + elif name == "unlinkauth": + await layout.confirm_action_unlinkauth(ctx, action.unlink_auth) + writers.write_action_unlinkauth(w, action.unlink_auth) + elif name == "newaccount": + await layout.confirm_action_newaccount(ctx, action.new_account) + writers.write_action_newaccount(w, action.new_account) + else: + raise ValueError("Unrecognized action type for eosio") + elif name == "transfer": + await layout.confirm_action_transfer(ctx, action.transfer, account) + writers.write_action_transfer(w, action.transfer) + else: + await process_unknown_action(ctx, w, action) + + writers.write_action_common(sha, action.common) + writers.write_variant32(sha, len(w)) + writers.write_bytes(sha, w) + + +async def process_unknown_action(ctx, w, action): + checksum = HashWriter(sha256()) + writers.write_variant32(checksum, action.unknown.data_size) + checksum.extend(action.unknown.data_chunk) + + writers.write_bytes(w, action.unknown.data_chunk) + bytes_left = action.unknown.data_size - len(action.unknown.data_chunk) + + while bytes_left != 0: + action = await ctx.call( + EosTxActionRequest(data_size=bytes_left), EosTxActionAck + ) + + if action.unknown is None: + raise ValueError("Bad response. Unknown struct expected.") + + checksum.extend(action.unknown.data_chunk) + writers.write_bytes(w, action.unknown.data_chunk) + + bytes_left -= len(action.unknown.data_chunk) + if bytes_left < 0: + raise ValueError("Bad response. Buffer overflow.") + + await layout.confirm_action_unknown(ctx, action.common, checksum.get_digest()) + + +def check_action(action, name, account): + if account == "eosio": + if ( + (name == "buyram" and action.buy_ram is not None) + or (name == "buyrambytes" and action.buy_ram_bytes is not None) + or (name == "sellram" and action.sell_ram is not None) + or (name == "delegatebw" and action.delegate is not None) + or (name == "undelegatebw" and action.undelegate is not None) + or (name == "refund" and action.refund is not None) + or (name == "voteproducer" and action.vote_producer is not None) + or (name == "updateauth" and action.update_auth is not None) + or (name == "deleteauth" and action.delete_auth is not None) + or (name == "linkauth" and action.link_auth is not None) + or (name == "unlinkauth" and action.unlink_auth is not None) + or (name == "newaccount" and action.new_account is not None) + ): + return True + else: + return False + + elif name == "transfer": + return action.transfer is not None + + elif action.unknown is not None: + return True + + return False diff --git a/core/src/apps/eos/actions/layout.py b/core/src/apps/eos/actions/layout.py new file mode 100644 index 000000000..f84c48694 --- /dev/null +++ b/core/src/apps/eos/actions/layout.py @@ -0,0 +1,411 @@ +from micropython import const +from ubinascii import hexlify + +from trezor import ui, wire +from trezor.messages import ( + ButtonRequestType, + EosActionBuyRam, + EosActionBuyRamBytes, + EosActionDelegate, + EosActionDeleteAuth, + EosActionLinkAuth, + EosActionNewAccount, + EosActionRefund, + EosActionSellRam, + EosActionTransfer, + EosActionUndelegate, + EosActionUnlinkAuth, + EosActionUpdateAuth, + EosActionVoteProducer, + MessageType, +) +from trezor.messages.ButtonRequest import ButtonRequest +from trezor.ui.confirm import CONFIRMED, ConfirmDialog +from trezor.ui.scroll import Scrollpage, animate_swipe, paginate +from trezor.ui.text import Text +from trezor.utils import chunks + +from apps.eos import helpers +from apps.eos.get_public_key import _public_key_to_wif +from apps.eos.layout import require_confirm + +_LINE_LENGTH = const(17) +_LINE_PLACEHOLDER = "{:<" + str(_LINE_LENGTH) + "}" +_FIRST_PAGE = const(0) +_TWO_FIELDS_PER_PAGE = const(2) +_THREE_FIELDS_PER_PAGE = const(3) +_FOUR_FIELDS_PER_PAGE = const(4) +_FIVE_FIELDS_PER_PAGE = const(5) + + +async def confirm_action_buyram(ctx, msg: EosActionBuyRam): + await ctx.call( + ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck + ) + + text = "Buy RAM" + fields = [] + fields.append("Payer:") + fields.append(helpers.eos_name_to_string(msg.payer)) + fields.append("Receiver:") + fields.append(helpers.eos_name_to_string(msg.receiver)) + fields.append("Amount:") + fields.append(helpers.eos_asset_to_string(msg.quantity)) + + pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) + + paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) + await ctx.wait(paginator) + + +async def confirm_action_buyrambytes(ctx, msg: EosActionBuyRamBytes): + await ctx.call( + ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck + ) + + text = "Buy RAM" + fields = [] + fields.append("Payer:") + fields.append(helpers.eos_name_to_string(msg.payer)) + fields.append("Receiver:") + fields.append(helpers.eos_name_to_string(msg.receiver)) + fields.append("Bytes:") + fields.append(str(msg.bytes)) + + pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) + paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) + + await ctx.wait(paginator) + + +async def confirm_action_delegate(ctx, msg: EosActionDelegate): + await ctx.call( + ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck + ) + + text = "Delegate" + fields = [] + fields.append("Sender:") + fields.append(helpers.eos_name_to_string(msg.sender)) + fields.append("Receiver:") + fields.append(helpers.eos_name_to_string(msg.receiver)) + fields.append("CPU:") + fields.append(helpers.eos_asset_to_string(msg.cpu_quantity)) + fields.append("NET:") + fields.append(helpers.eos_asset_to_string(msg.net_quantity)) + + if msg.transfer: + fields.append("Transfer: Yes") + fields.append("Receiver:") + fields.append(helpers.eos_name_to_string(msg.receiver)) + else: + fields.append("Transfer: No") + + pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) + paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) + + await ctx.wait(paginator) + + +async def confirm_action_sellram(ctx, msg: EosActionSellRam): + await ctx.call( + ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck + ) + + text = "Sell RAM" + fields = [] + fields.append("Receiver:") + fields.append(helpers.eos_name_to_string(msg.account)) + fields.append("Bytes:") + fields.append(str(msg.bytes)) + + pages = list(chunks(fields, _TWO_FIELDS_PER_PAGE)) + paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) + + await ctx.wait(paginator) + + +async def confirm_action_undelegate(ctx, msg: EosActionUndelegate): + await ctx.call( + ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck + ) + + text = "Undelegate" + fields = [] + fields.append("Sender:") + fields.append(helpers.eos_name_to_string(msg.sender)) + fields.append("Receiver:") + fields.append(helpers.eos_name_to_string(msg.receiver)) + fields.append("CPU:") + fields.append(helpers.eos_asset_to_string(msg.cpu_quantity)) + fields.append("NET:") + fields.append(helpers.eos_asset_to_string(msg.net_quantity)) + + pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) + paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) + + await ctx.wait(paginator) + + +async def confirm_action_refund(ctx, msg: EosActionRefund): + text = Text("Refund", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text.normal("Owner:") + text.normal(helpers.eos_name_to_string(msg.owner)) + await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) + + +async def confirm_action_voteproducer(ctx, msg: EosActionVoteProducer): + if msg.proxy and not msg.producers: + # PROXY + text = Text("Vote for proxy", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text.normal("Voter:") + text.normal(helpers.eos_name_to_string(msg.voter)) + text.normal("Proxy:") + text.normal(helpers.eos_name_to_string(msg.proxy)) + await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) + + elif msg.producers: + # PRODUCERS + await ctx.call( + ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck + ) + producers = list(enumerate(msg.producers)) + pages = list(chunks(producers, _FIVE_FIELDS_PER_PAGE)) + paginator = paginate(show_voter_page, len(pages), _FIRST_PAGE, pages) + await ctx.wait(paginator) + + else: + # Cancel vote + text = Text("Cancel vote", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text.normal("Voter:") + text.normal(helpers.eos_name_to_string(msg.voter)) + await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) + + +async def confirm_action_transfer(ctx, msg: EosActionTransfer, account: str): + await ctx.call( + ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck + ) + + text = "Transfer" + fields = [] + fields.append("From:") + fields.append(helpers.eos_name_to_string(msg.sender)) + fields.append("To:") + fields.append(helpers.eos_name_to_string(msg.receiver)) + fields.append("Amount:") + fields.append(helpers.eos_asset_to_string(msg.quantity)) + fields.append("Contract:") + fields.append(account) + + if msg.memo is not None: + fields.append("Memo:") + fields += split_data(msg.memo[:512]) + + pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) + + paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) + await ctx.wait(paginator) + + +async def confirm_action_updateauth(ctx, msg: EosActionUpdateAuth): + await ctx.call( + ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck + ) + + text = "Update Auth" + fields = [] + fields.append("Account:") + fields.append(helpers.eos_name_to_string(msg.account)) + fields.append("Permission:") + fields.append(helpers.eos_name_to_string(msg.permission)) + fields.append("Parent:") + fields.append(helpers.eos_name_to_string(msg.parent)) + fields += authorization_fields(msg.auth) + + pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) + + paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) + await ctx.wait(paginator) + + +async def confirm_action_deleteauth(ctx, msg: EosActionDeleteAuth): + text = Text("Delete auth", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text.normal("Account:") + text.normal(helpers.eos_name_to_string(msg.account)) + text.normal("Permission:") + text.normal(helpers.eos_name_to_string(msg.permission)) + await require_confirm(ctx, text, ButtonRequestType.ConfirmOutput) + + +async def confirm_action_linkauth(ctx, msg: EosActionLinkAuth): + await ctx.call( + ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck + ) + + text = "Link Auth" + fields = [] + fields.append("Account:") + fields.append(helpers.eos_name_to_string(msg.account)) + fields.append("Code:") + fields.append(helpers.eos_name_to_string(msg.code)) + fields.append("Type:") + fields.append(helpers.eos_name_to_string(msg.type)) + fields.append("Requirement:") + fields.append(helpers.eos_name_to_string(msg.requirement)) + + pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) + paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) + + await ctx.wait(paginator) + + +async def confirm_action_unlinkauth(ctx, msg: EosActionUnlinkAuth): + await ctx.call( + ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck + ) + + text = "Unlink Auth" + fields = [] + fields.append("Account:") + fields.append(helpers.eos_name_to_string(msg.account)) + fields.append("Code:") + fields.append(helpers.eos_name_to_string(msg.code)) + fields.append("Type:") + fields.append(helpers.eos_name_to_string(msg.type)) + + pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) + paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) + + await ctx.wait(paginator) + + +async def confirm_action_newaccount(ctx, msg: EosActionNewAccount): + await ctx.call( + ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck + ) + + text = "New Account" + fields = [] + fields.append("Creator:") + fields.append(helpers.eos_name_to_string(msg.creator)) + fields.append("Name:") + fields.append(helpers.eos_name_to_string(msg.name)) + fields += authorization_fields(msg.owner) + fields += authorization_fields(msg.active) + + pages = list(chunks(fields, _FOUR_FIELDS_PER_PAGE)) + paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) + + await ctx.wait(paginator) + + +async def confirm_action_unknown(ctx, action, checksum): + await ctx.call( + ButtonRequest(code=ButtonRequestType.ConfirmOutput), MessageType.ButtonAck + ) + text = "Arbitrary data" + fields = [] + fields.append("Contract:") + fields.append(helpers.eos_name_to_string(action.account)) + fields.append("Action Name:") + fields.append(helpers.eos_name_to_string(action.name)) + + fields.append("Checksum: ") + fields += split_data(hexlify(checksum).decode("ascii")) + + pages = list(chunks(fields, _FIVE_FIELDS_PER_PAGE)) + paginator = paginate(show_lines_page, len(pages), _FIRST_PAGE, pages, text) + + await ctx.wait(paginator) + + +@ui.layout +async def show_lines_page(page: int, page_count: int, pages: list, header: str): + if header == "Arbitrary data": + text = Text(header, ui.ICON_WIPE, icon_color=ui.RED) + else: + text = Text(header, ui.ICON_CONFIRM, icon_color=ui.GREEN) + text.mono(*pages[page]) + + content = Scrollpage(text, page, page_count) + if page + 1 == page_count: + if await ConfirmDialog(content) != CONFIRMED: + raise wire.ActionCancelled("Action cancelled") + else: + content.render() + await animate_swipe() + + +@ui.layout +async def show_voter_page(page: int, page_count: int, pages: list): + lines = [ + "{:2d}. {}".format(wi + 1, helpers.eos_name_to_string(producer)) + for wi, producer in pages[page] + ] + text = Text("Vote for producers", ui.ICON_CONFIRM, icon_color=ui.GREEN) + text.mono(*lines) + content = Scrollpage(text, page, page_count) + + if page + 1 == page_count: + if await ConfirmDialog(content) != CONFIRMED: + raise wire.ActionCancelled("Action cancelled") + else: + content.render() + await animate_swipe() + + +def authorization_fields(auth): + fields = [] + + fields.append("Threshold:") + fields.append(str(auth.threshold)) + + for i, key in enumerate(auth.keys): + _key = _public_key_to_wif(bytes(key.key)) + _weight = str(key.weight) + + header = "Key #{}:".format(i + 1) + w_header = "Key #{} Weight:".format(i + 1) + fields.append(header) + fields += split_data(_key) + fields.append(w_header) + fields.append(_weight) + + for i, account in enumerate(auth.accounts): + _account = helpers.eos_name_to_string(account.account.actor) + _permission = helpers.eos_name_to_string(account.account.permission) + + a_header = "Account #{}:".format(i + 1) + p_header = "Acc Permission #{}:".format(i + 1) + w_header = "Account #{} weight:".format(i + 1) + + fields.append(a_header) + fields.append(_account) + fields.append(p_header) + fields.append(_permission) + fields.append(w_header) + fields.append(str(account.weight)) + + for i, wait in enumerate(auth.waits): + _wait = str(wait.wait_sec) + _weight = str(wait.weight) + + header = "Delay #{}".format(i + 1) + w_header = "Delay #{} weight:".format(i + 1) + fields.append(header) + fields.append("{} sec".format(_wait)) + fields.append(w_header) + fields.append(_weight) + + return fields + + +def split_data(data): + temp_list = [] + len_left = len(data) + while len_left > 0: + temp_list.append("{} ".format(data[:_LINE_LENGTH])) + data = data[_LINE_LENGTH:] + len_left = len(data) + return temp_list diff --git a/core/src/apps/eos/get_public_key.py b/core/src/apps/eos/get_public_key.py new file mode 100755 index 000000000..de72c0597 --- /dev/null +++ b/core/src/apps/eos/get_public_key.py @@ -0,0 +1,43 @@ +from trezor import wire +from trezor.crypto import base58 +from trezor.crypto.curve import secp256k1 +from trezor.crypto.hashlib import ripemd160 +from trezor.messages.EosGetPublicKey import EosGetPublicKey +from trezor.messages.EosPublicKey import EosPublicKey + +from apps.common import paths +from apps.eos import CURVE +from apps.eos.helpers import validate_full_path +from apps.eos.layout import require_get_public_key + + +def _ripemd160_32(data: bytes) -> bytes: + return ripemd160(data).digest()[:4] + + +def _public_key_to_wif(pub_key: bytes) -> str: + if len(pub_key) == 65: + head = 0x03 if pub_key[64] & 0x01 else 0x02 + compresed_pub_key = bytes([head]) + pub_key[1:33] + elif len(pub_key) == 33: + compresed_pub_key = pub_key + else: + raise wire.DataError("invalid public key length") + return "EOS" + base58.encode_check(compresed_pub_key, _ripemd160_32) + + +def _get_public_key(node): + seckey = node.private_key() + public_key = secp256k1.publickey(seckey, True) + wif = _public_key_to_wif(public_key) + return wif, public_key + + +async def get_public_key(ctx, msg: EosGetPublicKey, keychain): + await paths.validate_path(ctx, validate_full_path, keychain, msg.address_n, CURVE) + + node = keychain.derive(msg.address_n) + wif, public_key = _get_public_key(node) + if msg.show_display: + await require_get_public_key(ctx, wif) + return EosPublicKey(wif, public_key) diff --git a/core/src/apps/eos/helpers.py b/core/src/apps/eos/helpers.py new file mode 100644 index 000000000..ca5001225 --- /dev/null +++ b/core/src/apps/eos/helpers.py @@ -0,0 +1,54 @@ +from trezor.messages import EosAsset + +from apps.common import HARDENED + + +def eos_name_to_string(value) -> str: + charmap = ".12345abcdefghijklmnopqrstuvwxyz" + tmp = value + string = "" + for i in range(0, 13): + c = charmap[tmp & (0x0F if i == 0 else 0x1F)] + string = c + string + tmp >>= 4 if i == 0 else 5 + + return string.rstrip(".") + + +def eos_asset_to_string(asset: EosAsset) -> str: + symbol_bytes = int.to_bytes(asset.symbol, 8, "big") + precision = symbol_bytes[7] + symbol = bytes(reversed(symbol_bytes[:7])).rstrip(b"\x00").decode("ascii") + + amount_digits = "{:0{precision}d}".format(asset.amount, precision=precision) + if precision > 0: + integer = amount_digits[:-precision] + if integer == "": + integer = "0" + fraction = amount_digits[-precision:] + + return "{}.{} {}".format(integer, fraction, symbol) + else: + return "{} {}".format(amount_digits, symbol) + + +def validate_full_path(path: list) -> bool: + """ + Validates derivation path to equal 44'/194'/a'/0/0, + where `a` is an account index from 0 to 1 000 000. + Similar to Ethereum this should be 44'/194'/a', but for + compatibility with other HW vendors we use 44'/194'/a'/0/0. + """ + if len(path) != 5: + return False + if path[0] != 44 | HARDENED: + return False + if path[1] != 194 | HARDENED: + return False + if path[2] < HARDENED or path[2] > 1000000 | HARDENED: + return False + if path[3] != 0: + return False + if path[4] != 0: + return False + return True diff --git a/core/src/apps/eos/layout.py b/core/src/apps/eos/layout.py new file mode 100644 index 000000000..278335902 --- /dev/null +++ b/core/src/apps/eos/layout.py @@ -0,0 +1,19 @@ +from trezor import ui +from trezor.messages import ButtonRequestType +from trezor.ui.text import Text + +from apps.common.confirm import require_confirm + + +async def require_get_public_key(ctx, public_key): + text = Text("Confirm public key", ui.ICON_RECEIVE, icon_color=ui.GREEN) + text.normal(public_key) + return await require_confirm(ctx, text, code=ButtonRequestType.PublicKey) + + +async def require_sign_tx(ctx, num_actions): + text = Text("Sign transaction", ui.ICON_SEND, icon_color=ui.GREEN) + text.normal("You are about") + text.normal("to sign {}".format(num_actions)) + text.normal("action(s).") + return await require_confirm(ctx, text, code=ButtonRequestType.SignTx) diff --git a/core/src/apps/eos/sign_tx.py b/core/src/apps/eos/sign_tx.py new file mode 100644 index 000000000..b0f005371 --- /dev/null +++ b/core/src/apps/eos/sign_tx.py @@ -0,0 +1,54 @@ +from trezor import wire +from trezor.crypto.curve import secp256k1 +from trezor.crypto.hashlib import sha256 +from trezor.messages.EosSignedTx import EosSignedTx +from trezor.messages.EosSignTx import EosSignTx +from trezor.messages.EosTxActionRequest import EosTxActionRequest +from trezor.messages.MessageType import EosTxActionAck +from trezor.utils import HashWriter + +from apps.common import paths +from apps.eos import CURVE, writers +from apps.eos.actions import process_action +from apps.eos.helpers import validate_full_path +from apps.eos.layout import require_sign_tx + + +async def sign_tx(ctx, msg: EosSignTx, keychain): + if msg.chain_id is None: + raise wire.DataError("No chain id") + if msg.header is None: + raise wire.DataError("No header") + if msg.num_actions is None or msg.num_actions == 0: + raise wire.DataError("No actions") + + await paths.validate_path(ctx, validate_full_path, keychain, msg.address_n, CURVE) + + node = keychain.derive(msg.address_n) + sha = HashWriter(sha256()) + await _init(ctx, sha, msg) + await _actions(ctx, sha, msg.num_actions) + writers.write_variant32(sha, 0) + writers.write_bytes(sha, bytearray(32)) + + digest = sha.get_digest() + signature = secp256k1.sign( + node.private_key(), digest, True, secp256k1.CANONICAL_SIG_EOS + ) + + return EosSignedTx(signature[0], signature[1:33], signature[33:]) + + +async def _init(ctx, sha, msg): + writers.write_bytes(sha, msg.chain_id) + writers.write_header(sha, msg.header) + writers.write_variant32(sha, 0) + writers.write_variant32(sha, msg.num_actions) + + await require_sign_tx(ctx, msg.num_actions) + + +async def _actions(ctx, sha, num_actions: int): + for i in range(num_actions): + action = await ctx.call(EosTxActionRequest(), EosTxActionAck) + await process_action(ctx, sha, action) diff --git a/core/src/apps/eos/writers.py b/core/src/apps/eos/writers.py new file mode 100644 index 000000000..5b392bb3b --- /dev/null +++ b/core/src/apps/eos/writers.py @@ -0,0 +1,168 @@ +from trezor.messages import ( + EosActionBuyRam, + EosActionBuyRamBytes, + EosActionCommon, + EosActionDelegate, + EosActionDeleteAuth, + EosActionLinkAuth, + EosActionNewAccount, + EosActionRefund, + EosActionSellRam, + EosActionTransfer, + EosActionUndelegate, + EosActionUpdateAuth, + EosActionVoteProducer, + EosAsset, + EosAuthorization, + EosTxHeader, +) +from trezor.utils import HashWriter + +from apps.common.writers import ( + write_bytes, + write_uint8, + write_uint16_le, + write_uint32_le, + write_uint64_le, +) + + +def write_auth(w: bytearray, auth: EosAuthorization) -> int: + write_uint32_le(w, auth.threshold) + write_variant32(w, len(auth.keys)) + for key in auth.keys: + write_variant32(w, key.type) + write_bytes(w, key.key) + write_uint16_le(w, key.weight) + + write_variant32(w, len(auth.accounts)) + for account in auth.accounts: + write_uint64_le(w, account.account.actor) + write_uint64_le(w, account.account.permission) + write_uint16_le(w, account.weight) + + write_variant32(w, len(auth.waits)) + for wait in auth.waits: + write_uint32_le(w, wait.wait_sec) + write_uint16_le(w, wait.weight) + + +def write_header(hasher: HashWriter, header: EosTxHeader): + write_uint32_le(hasher, header.expiration) + write_uint16_le(hasher, header.ref_block_num) + write_uint32_le(hasher, header.ref_block_prefix) + write_variant32(hasher, header.max_net_usage_words) + write_uint8(hasher, header.max_cpu_usage_ms) + write_variant32(hasher, header.delay_sec) + + +def write_action_transfer(w: bytearray, msg: EosActionTransfer): + write_uint64_le(w, msg.sender) + write_uint64_le(w, msg.receiver) + write_asset(w, msg.quantity) + write_variant32(w, len(msg.memo)) + write_bytes(w, msg.memo) + + +def write_action_buyram(w: bytearray, msg: EosActionBuyRam): + write_uint64_le(w, msg.payer) + write_uint64_le(w, msg.receiver) + write_asset(w, msg.quantity) + + +def write_action_buyrambytes(w: bytearray, msg: EosActionBuyRamBytes): + write_uint64_le(w, msg.payer) + write_uint64_le(w, msg.receiver) + write_uint32_le(w, msg.bytes) + + +def write_action_sellram(w: bytearray, msg: EosActionSellRam): + write_uint64_le(w, msg.account) + write_uint64_le(w, msg.bytes) + + +def write_action_delegate(w: bytearray, msg: EosActionDelegate): + write_uint64_le(w, msg.sender) + write_uint64_le(w, msg.receiver) + write_asset(w, msg.net_quantity) + write_asset(w, msg.cpu_quantity) + write_uint8(w, 1 if msg.transfer else 0) + + +def write_action_undelegate(w: bytearray, msg: EosActionUndelegate): + write_uint64_le(w, msg.sender) + write_uint64_le(w, msg.receiver) + write_asset(w, msg.net_quantity) + write_asset(w, msg.cpu_quantity) + + +def write_action_refund(w: bytearray, msg: EosActionRefund): + write_uint64_le(w, msg.owner) + + +def write_action_voteproducer(w: bytearray, msg: EosActionVoteProducer): + write_uint64_le(w, msg.voter) + write_uint64_le(w, msg.proxy) + write_variant32(w, len(msg.producers)) + for producer in msg.producers: + write_uint64_le(w, producer) + + +def write_action_updateauth(w: bytearray, msg: EosActionUpdateAuth): + write_uint64_le(w, msg.account) + write_uint64_le(w, msg.permission) + write_uint64_le(w, msg.parent) + write_auth(w, msg.auth) + + +def write_action_deleteauth(w: bytearray, msg: EosActionDeleteAuth): + write_uint64_le(w, msg.account) + write_uint64_le(w, msg.permission) + + +def write_action_linkauth(w: bytearray, msg: EosActionLinkAuth): + write_uint64_le(w, msg.account) + write_uint64_le(w, msg.code) + write_uint64_le(w, msg.type) + write_uint64_le(w, msg.requirement) + + +def write_action_unlinkauth(w: bytearray, msg: EosActionLinkAuth): + write_uint64_le(w, msg.account) + write_uint64_le(w, msg.code) + write_uint64_le(w, msg.type) + + +def write_action_newaccount(w: bytearray, msg: EosActionNewAccount): + write_uint64_le(w, msg.creator) + write_uint64_le(w, msg.name) + write_auth(w, msg.owner) + write_auth(w, msg.active) + + +def write_action_common(hasher: HashWriter, msg: EosActionCommon): + write_uint64_le(hasher, msg.account) + write_uint64_le(hasher, msg.name) + write_variant32(hasher, len(msg.authorization)) + for authorization in msg.authorization: + write_uint64_le(hasher, authorization.actor) + write_uint64_le(hasher, authorization.permission) + + +def write_asset(w: bytearray, asset: EosAsset) -> int: + write_uint64_le(w, asset.amount) + write_uint64_le(w, asset.symbol) + + +def write_variant32(w: bytearray, value: int) -> int: + variant = bytearray() + while True: + b = value & 0x7F + value >>= 7 + b |= (value > 0) << 7 + variant.append(b) + + if value == 0: + break + + write_bytes(w, bytes(variant)) diff --git a/core/src/main.py b/core/src/main.py index b419aa23a..2265028e4 100644 --- a/core/src/main.py +++ b/core/src/main.py @@ -20,6 +20,7 @@ import apps.stellar import apps.ripple import apps.cardano import apps.tezos +import apps.eos if __debug__: import apps.debug @@ -38,6 +39,7 @@ apps.stellar.boot() apps.ripple.boot() apps.cardano.boot() apps.tezos.boot() +apps.eos.boot() if __debug__: apps.debug.boot() else: diff --git a/core/src/trezor/messages/EosActionBuyRam.py b/core/src/trezor/messages/EosActionBuyRam.py new file mode 100644 index 000000000..e8f17dc6b --- /dev/null +++ b/core/src/trezor/messages/EosActionBuyRam.py @@ -0,0 +1,26 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .EosAsset import EosAsset + + +class EosActionBuyRam(p.MessageType): + + def __init__( + self, + payer: int = None, + receiver: int = None, + quantity: EosAsset = None, + ) -> None: + self.payer = payer + self.receiver = receiver + self.quantity = quantity + + @classmethod + def get_fields(cls): + return { + 1: ('payer', p.UVarintType, 0), + 2: ('receiver', p.UVarintType, 0), + 3: ('quantity', EosAsset, 0), + } diff --git a/core/src/trezor/messages/EosActionBuyRamBytes.py b/core/src/trezor/messages/EosActionBuyRamBytes.py new file mode 100644 index 000000000..509631e36 --- /dev/null +++ b/core/src/trezor/messages/EosActionBuyRamBytes.py @@ -0,0 +1,24 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + + +class EosActionBuyRamBytes(p.MessageType): + + def __init__( + self, + payer: int = None, + receiver: int = None, + bytes: int = None, + ) -> None: + self.payer = payer + self.receiver = receiver + self.bytes = bytes + + @classmethod + def get_fields(cls): + return { + 1: ('payer', p.UVarintType, 0), + 2: ('receiver', p.UVarintType, 0), + 3: ('bytes', p.UVarintType, 0), + } diff --git a/core/src/trezor/messages/EosActionCommon.py b/core/src/trezor/messages/EosActionCommon.py new file mode 100644 index 000000000..ebe79d802 --- /dev/null +++ b/core/src/trezor/messages/EosActionCommon.py @@ -0,0 +1,32 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .EosPermissionLevel import EosPermissionLevel + +if __debug__: + try: + from typing import List + except ImportError: + List = None # type: ignore + + +class EosActionCommon(p.MessageType): + + def __init__( + self, + account: int = None, + name: int = None, + authorization: List[EosPermissionLevel] = None, + ) -> None: + self.account = account + self.name = name + self.authorization = authorization if authorization is not None else [] + + @classmethod + def get_fields(cls): + return { + 1: ('account', p.UVarintType, 0), + 2: ('name', p.UVarintType, 0), + 3: ('authorization', EosPermissionLevel, p.FLAG_REPEATED), + } diff --git a/core/src/trezor/messages/EosActionDelegate.py b/core/src/trezor/messages/EosActionDelegate.py new file mode 100644 index 000000000..103aa91cd --- /dev/null +++ b/core/src/trezor/messages/EosActionDelegate.py @@ -0,0 +1,32 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .EosAsset import EosAsset + + +class EosActionDelegate(p.MessageType): + + def __init__( + self, + sender: int = None, + receiver: int = None, + net_quantity: EosAsset = None, + cpu_quantity: EosAsset = None, + transfer: bool = None, + ) -> None: + self.sender = sender + self.receiver = receiver + self.net_quantity = net_quantity + self.cpu_quantity = cpu_quantity + self.transfer = transfer + + @classmethod + def get_fields(cls): + return { + 1: ('sender', p.UVarintType, 0), + 2: ('receiver', p.UVarintType, 0), + 3: ('net_quantity', EosAsset, 0), + 4: ('cpu_quantity', EosAsset, 0), + 5: ('transfer', p.BoolType, 0), + } diff --git a/core/src/trezor/messages/EosActionDeleteAuth.py b/core/src/trezor/messages/EosActionDeleteAuth.py new file mode 100644 index 000000000..fc0bc5733 --- /dev/null +++ b/core/src/trezor/messages/EosActionDeleteAuth.py @@ -0,0 +1,21 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + + +class EosActionDeleteAuth(p.MessageType): + + def __init__( + self, + account: int = None, + permission: int = None, + ) -> None: + self.account = account + self.permission = permission + + @classmethod + def get_fields(cls): + return { + 1: ('account', p.UVarintType, 0), + 2: ('permission', p.UVarintType, 0), + } diff --git a/core/src/trezor/messages/EosActionLinkAuth.py b/core/src/trezor/messages/EosActionLinkAuth.py new file mode 100644 index 000000000..03f090ab0 --- /dev/null +++ b/core/src/trezor/messages/EosActionLinkAuth.py @@ -0,0 +1,27 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + + +class EosActionLinkAuth(p.MessageType): + + def __init__( + self, + account: int = None, + code: int = None, + type: int = None, + requirement: int = None, + ) -> None: + self.account = account + self.code = code + self.type = type + self.requirement = requirement + + @classmethod + def get_fields(cls): + return { + 1: ('account', p.UVarintType, 0), + 2: ('code', p.UVarintType, 0), + 3: ('type', p.UVarintType, 0), + 4: ('requirement', p.UVarintType, 0), + } diff --git a/core/src/trezor/messages/EosActionNewAccount.py b/core/src/trezor/messages/EosActionNewAccount.py new file mode 100644 index 000000000..8cc13487f --- /dev/null +++ b/core/src/trezor/messages/EosActionNewAccount.py @@ -0,0 +1,29 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .EosAuthorization import EosAuthorization + + +class EosActionNewAccount(p.MessageType): + + def __init__( + self, + creator: int = None, + name: int = None, + owner: EosAuthorization = None, + active: EosAuthorization = None, + ) -> None: + self.creator = creator + self.name = name + self.owner = owner + self.active = active + + @classmethod + def get_fields(cls): + return { + 1: ('creator', p.UVarintType, 0), + 2: ('name', p.UVarintType, 0), + 3: ('owner', EosAuthorization, 0), + 4: ('active', EosAuthorization, 0), + } diff --git a/core/src/trezor/messages/EosActionRefund.py b/core/src/trezor/messages/EosActionRefund.py new file mode 100644 index 000000000..99cf91190 --- /dev/null +++ b/core/src/trezor/messages/EosActionRefund.py @@ -0,0 +1,18 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + + +class EosActionRefund(p.MessageType): + + def __init__( + self, + owner: int = None, + ) -> None: + self.owner = owner + + @classmethod + def get_fields(cls): + return { + 1: ('owner', p.UVarintType, 0), + } diff --git a/core/src/trezor/messages/EosActionSellRam.py b/core/src/trezor/messages/EosActionSellRam.py new file mode 100644 index 000000000..18f355625 --- /dev/null +++ b/core/src/trezor/messages/EosActionSellRam.py @@ -0,0 +1,21 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + + +class EosActionSellRam(p.MessageType): + + def __init__( + self, + account: int = None, + bytes: int = None, + ) -> None: + self.account = account + self.bytes = bytes + + @classmethod + def get_fields(cls): + return { + 1: ('account', p.UVarintType, 0), + 2: ('bytes', p.UVarintType, 0), + } diff --git a/core/src/trezor/messages/EosActionTransfer.py b/core/src/trezor/messages/EosActionTransfer.py new file mode 100644 index 000000000..0e1e5229a --- /dev/null +++ b/core/src/trezor/messages/EosActionTransfer.py @@ -0,0 +1,29 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .EosAsset import EosAsset + + +class EosActionTransfer(p.MessageType): + + def __init__( + self, + sender: int = None, + receiver: int = None, + quantity: EosAsset = None, + memo: str = None, + ) -> None: + self.sender = sender + self.receiver = receiver + self.quantity = quantity + self.memo = memo + + @classmethod + def get_fields(cls): + return { + 1: ('sender', p.UVarintType, 0), + 2: ('receiver', p.UVarintType, 0), + 3: ('quantity', EosAsset, 0), + 4: ('memo', p.UnicodeType, 0), + } diff --git a/core/src/trezor/messages/EosActionUndelegate.py b/core/src/trezor/messages/EosActionUndelegate.py new file mode 100644 index 000000000..046f7ef41 --- /dev/null +++ b/core/src/trezor/messages/EosActionUndelegate.py @@ -0,0 +1,29 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .EosAsset import EosAsset + + +class EosActionUndelegate(p.MessageType): + + def __init__( + self, + sender: int = None, + receiver: int = None, + net_quantity: EosAsset = None, + cpu_quantity: EosAsset = None, + ) -> None: + self.sender = sender + self.receiver = receiver + self.net_quantity = net_quantity + self.cpu_quantity = cpu_quantity + + @classmethod + def get_fields(cls): + return { + 1: ('sender', p.UVarintType, 0), + 2: ('receiver', p.UVarintType, 0), + 3: ('net_quantity', EosAsset, 0), + 4: ('cpu_quantity', EosAsset, 0), + } diff --git a/core/src/trezor/messages/EosActionUnknown.py b/core/src/trezor/messages/EosActionUnknown.py new file mode 100644 index 000000000..001f4ba23 --- /dev/null +++ b/core/src/trezor/messages/EosActionUnknown.py @@ -0,0 +1,21 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + + +class EosActionUnknown(p.MessageType): + + def __init__( + self, + data_size: int = None, + data_chunk: bytes = None, + ) -> None: + self.data_size = data_size + self.data_chunk = data_chunk + + @classmethod + def get_fields(cls): + return { + 1: ('data_size', p.UVarintType, 0), + 2: ('data_chunk', p.BytesType, 0), + } diff --git a/core/src/trezor/messages/EosActionUnlinkAuth.py b/core/src/trezor/messages/EosActionUnlinkAuth.py new file mode 100644 index 000000000..c1378dc00 --- /dev/null +++ b/core/src/trezor/messages/EosActionUnlinkAuth.py @@ -0,0 +1,24 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + + +class EosActionUnlinkAuth(p.MessageType): + + def __init__( + self, + account: int = None, + code: int = None, + type: int = None, + ) -> None: + self.account = account + self.code = code + self.type = type + + @classmethod + def get_fields(cls): + return { + 1: ('account', p.UVarintType, 0), + 2: ('code', p.UVarintType, 0), + 3: ('type', p.UVarintType, 0), + } diff --git a/core/src/trezor/messages/EosActionUpdateAuth.py b/core/src/trezor/messages/EosActionUpdateAuth.py new file mode 100644 index 000000000..249d2026b --- /dev/null +++ b/core/src/trezor/messages/EosActionUpdateAuth.py @@ -0,0 +1,29 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .EosAuthorization import EosAuthorization + + +class EosActionUpdateAuth(p.MessageType): + + def __init__( + self, + account: int = None, + permission: int = None, + parent: int = None, + auth: EosAuthorization = None, + ) -> None: + self.account = account + self.permission = permission + self.parent = parent + self.auth = auth + + @classmethod + def get_fields(cls): + return { + 1: ('account', p.UVarintType, 0), + 2: ('permission', p.UVarintType, 0), + 3: ('parent', p.UVarintType, 0), + 4: ('auth', EosAuthorization, 0), + } diff --git a/core/src/trezor/messages/EosActionVoteProducer.py b/core/src/trezor/messages/EosActionVoteProducer.py new file mode 100644 index 000000000..709e44ddf --- /dev/null +++ b/core/src/trezor/messages/EosActionVoteProducer.py @@ -0,0 +1,30 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +if __debug__: + try: + from typing import List + except ImportError: + List = None # type: ignore + + +class EosActionVoteProducer(p.MessageType): + + def __init__( + self, + voter: int = None, + proxy: int = None, + producers: List[int] = None, + ) -> None: + self.voter = voter + self.proxy = proxy + self.producers = producers if producers is not None else [] + + @classmethod + def get_fields(cls): + return { + 1: ('voter', p.UVarintType, 0), + 2: ('proxy', p.UVarintType, 0), + 3: ('producers', p.UVarintType, p.FLAG_REPEATED), + } diff --git a/core/src/trezor/messages/EosAsset.py b/core/src/trezor/messages/EosAsset.py new file mode 100644 index 000000000..1c3b66ef3 --- /dev/null +++ b/core/src/trezor/messages/EosAsset.py @@ -0,0 +1,21 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + + +class EosAsset(p.MessageType): + + def __init__( + self, + amount: int = None, + symbol: int = None, + ) -> None: + self.amount = amount + self.symbol = symbol + + @classmethod + def get_fields(cls): + return { + 1: ('amount', p.SVarintType, 0), + 2: ('symbol', p.UVarintType, 0), + } diff --git a/core/src/trezor/messages/EosAuthorization.py b/core/src/trezor/messages/EosAuthorization.py new file mode 100644 index 000000000..345fe3504 --- /dev/null +++ b/core/src/trezor/messages/EosAuthorization.py @@ -0,0 +1,37 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .EosAuthorizationAccount import EosAuthorizationAccount +from .EosAuthorizationKey import EosAuthorizationKey +from .EosAuthorizationWait import EosAuthorizationWait + +if __debug__: + try: + from typing import List + except ImportError: + List = None # type: ignore + + +class EosAuthorization(p.MessageType): + + def __init__( + self, + threshold: int = None, + keys: List[EosAuthorizationKey] = None, + accounts: List[EosAuthorizationAccount] = None, + waits: List[EosAuthorizationWait] = None, + ) -> None: + self.threshold = threshold + self.keys = keys if keys is not None else [] + self.accounts = accounts if accounts is not None else [] + self.waits = waits if waits is not None else [] + + @classmethod + def get_fields(cls): + return { + 1: ('threshold', p.UVarintType, 0), + 2: ('keys', EosAuthorizationKey, p.FLAG_REPEATED), + 3: ('accounts', EosAuthorizationAccount, p.FLAG_REPEATED), + 4: ('waits', EosAuthorizationWait, p.FLAG_REPEATED), + } diff --git a/core/src/trezor/messages/EosAuthorizationAccount.py b/core/src/trezor/messages/EosAuthorizationAccount.py new file mode 100644 index 000000000..ddc5698f6 --- /dev/null +++ b/core/src/trezor/messages/EosAuthorizationAccount.py @@ -0,0 +1,23 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .EosPermissionLevel import EosPermissionLevel + + +class EosAuthorizationAccount(p.MessageType): + + def __init__( + self, + account: EosPermissionLevel = None, + weight: int = None, + ) -> None: + self.account = account + self.weight = weight + + @classmethod + def get_fields(cls): + return { + 1: ('account', EosPermissionLevel, 0), + 2: ('weight', p.UVarintType, 0), + } diff --git a/core/src/trezor/messages/EosAuthorizationKey.py b/core/src/trezor/messages/EosAuthorizationKey.py new file mode 100644 index 000000000..fcada310e --- /dev/null +++ b/core/src/trezor/messages/EosAuthorizationKey.py @@ -0,0 +1,33 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +if __debug__: + try: + from typing import List + except ImportError: + List = None # type: ignore + + +class EosAuthorizationKey(p.MessageType): + + def __init__( + self, + type: int = None, + key: bytes = None, + address_n: List[int] = None, + weight: int = None, + ) -> None: + self.type = type + self.key = key + self.address_n = address_n if address_n is not None else [] + self.weight = weight + + @classmethod + def get_fields(cls): + return { + 1: ('type', p.UVarintType, 0), + 2: ('key', p.BytesType, 0), + 3: ('address_n', p.UVarintType, p.FLAG_REPEATED), + 4: ('weight', p.UVarintType, 0), + } diff --git a/core/src/trezor/messages/EosAuthorizationWait.py b/core/src/trezor/messages/EosAuthorizationWait.py new file mode 100644 index 000000000..8c2d95d53 --- /dev/null +++ b/core/src/trezor/messages/EosAuthorizationWait.py @@ -0,0 +1,21 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + + +class EosAuthorizationWait(p.MessageType): + + def __init__( + self, + wait_sec: int = None, + weight: int = None, + ) -> None: + self.wait_sec = wait_sec + self.weight = weight + + @classmethod + def get_fields(cls): + return { + 1: ('wait_sec', p.UVarintType, 0), + 2: ('weight', p.UVarintType, 0), + } diff --git a/core/src/trezor/messages/EosGetPublicKey.py b/core/src/trezor/messages/EosGetPublicKey.py new file mode 100644 index 000000000..0d3ad22fa --- /dev/null +++ b/core/src/trezor/messages/EosGetPublicKey.py @@ -0,0 +1,28 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +if __debug__: + try: + from typing import List + except ImportError: + List = None # type: ignore + + +class EosGetPublicKey(p.MessageType): + MESSAGE_WIRE_TYPE = 600 + + def __init__( + self, + address_n: List[int] = None, + show_display: bool = None, + ) -> None: + self.address_n = address_n if address_n is not None else [] + self.show_display = show_display + + @classmethod + def get_fields(cls): + return { + 1: ('address_n', p.UVarintType, p.FLAG_REPEATED), + 2: ('show_display', p.BoolType, 0), + } diff --git a/core/src/trezor/messages/EosPermissionLevel.py b/core/src/trezor/messages/EosPermissionLevel.py new file mode 100644 index 000000000..242053952 --- /dev/null +++ b/core/src/trezor/messages/EosPermissionLevel.py @@ -0,0 +1,21 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + + +class EosPermissionLevel(p.MessageType): + + def __init__( + self, + actor: int = None, + permission: int = None, + ) -> None: + self.actor = actor + self.permission = permission + + @classmethod + def get_fields(cls): + return { + 1: ('actor', p.UVarintType, 0), + 2: ('permission', p.UVarintType, 0), + } diff --git a/core/src/trezor/messages/EosPublicKey.py b/core/src/trezor/messages/EosPublicKey.py new file mode 100644 index 000000000..3a97fd0e5 --- /dev/null +++ b/core/src/trezor/messages/EosPublicKey.py @@ -0,0 +1,22 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + + +class EosPublicKey(p.MessageType): + MESSAGE_WIRE_TYPE = 601 + + def __init__( + self, + wif_public_key: str = None, + raw_public_key: bytes = None, + ) -> None: + self.wif_public_key = wif_public_key + self.raw_public_key = raw_public_key + + @classmethod + def get_fields(cls): + return { + 1: ('wif_public_key', p.UnicodeType, 0), + 2: ('raw_public_key', p.BytesType, 0), + } diff --git a/core/src/trezor/messages/EosSignTx.py b/core/src/trezor/messages/EosSignTx.py new file mode 100644 index 000000000..e763be1f8 --- /dev/null +++ b/core/src/trezor/messages/EosSignTx.py @@ -0,0 +1,36 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .EosTxHeader import EosTxHeader + +if __debug__: + try: + from typing import List + except ImportError: + List = None # type: ignore + + +class EosSignTx(p.MessageType): + MESSAGE_WIRE_TYPE = 602 + + def __init__( + self, + address_n: List[int] = None, + chain_id: bytes = None, + header: EosTxHeader = None, + num_actions: int = None, + ) -> None: + self.address_n = address_n if address_n is not None else [] + self.chain_id = chain_id + self.header = header + self.num_actions = num_actions + + @classmethod + def get_fields(cls): + return { + 1: ('address_n', p.UVarintType, p.FLAG_REPEATED), + 2: ('chain_id', p.BytesType, 0), + 3: ('header', EosTxHeader, 0), + 4: ('num_actions', p.UVarintType, 0), + } diff --git a/core/src/trezor/messages/EosSignedTx.py b/core/src/trezor/messages/EosSignedTx.py new file mode 100644 index 000000000..946ce5759 --- /dev/null +++ b/core/src/trezor/messages/EosSignedTx.py @@ -0,0 +1,25 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + + +class EosSignedTx(p.MessageType): + MESSAGE_WIRE_TYPE = 605 + + def __init__( + self, + signature_v: int = None, + signature_r: bytes = None, + signature_s: bytes = None, + ) -> None: + self.signature_v = signature_v + self.signature_r = signature_r + self.signature_s = signature_s + + @classmethod + def get_fields(cls): + return { + 1: ('signature_v', p.UVarintType, 0), + 2: ('signature_r', p.BytesType, 0), + 3: ('signature_s', p.BytesType, 0), + } diff --git a/core/src/trezor/messages/EosTxActionAck.py b/core/src/trezor/messages/EosTxActionAck.py new file mode 100644 index 000000000..09f46d755 --- /dev/null +++ b/core/src/trezor/messages/EosTxActionAck.py @@ -0,0 +1,77 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + +from .EosActionBuyRam import EosActionBuyRam +from .EosActionBuyRamBytes import EosActionBuyRamBytes +from .EosActionCommon import EosActionCommon +from .EosActionDelegate import EosActionDelegate +from .EosActionDeleteAuth import EosActionDeleteAuth +from .EosActionLinkAuth import EosActionLinkAuth +from .EosActionNewAccount import EosActionNewAccount +from .EosActionRefund import EosActionRefund +from .EosActionSellRam import EosActionSellRam +from .EosActionTransfer import EosActionTransfer +from .EosActionUndelegate import EosActionUndelegate +from .EosActionUnknown import EosActionUnknown +from .EosActionUnlinkAuth import EosActionUnlinkAuth +from .EosActionUpdateAuth import EosActionUpdateAuth +from .EosActionVoteProducer import EosActionVoteProducer + + +class EosTxActionAck(p.MessageType): + MESSAGE_WIRE_TYPE = 604 + + def __init__( + self, + common: EosActionCommon = None, + transfer: EosActionTransfer = None, + delegate: EosActionDelegate = None, + undelegate: EosActionUndelegate = None, + refund: EosActionRefund = None, + buy_ram: EosActionBuyRam = None, + buy_ram_bytes: EosActionBuyRamBytes = None, + sell_ram: EosActionSellRam = None, + vote_producer: EosActionVoteProducer = None, + update_auth: EosActionUpdateAuth = None, + delete_auth: EosActionDeleteAuth = None, + link_auth: EosActionLinkAuth = None, + unlink_auth: EosActionUnlinkAuth = None, + new_account: EosActionNewAccount = None, + unknown: EosActionUnknown = None, + ) -> None: + self.common = common + self.transfer = transfer + self.delegate = delegate + self.undelegate = undelegate + self.refund = refund + self.buy_ram = buy_ram + self.buy_ram_bytes = buy_ram_bytes + self.sell_ram = sell_ram + self.vote_producer = vote_producer + self.update_auth = update_auth + self.delete_auth = delete_auth + self.link_auth = link_auth + self.unlink_auth = unlink_auth + self.new_account = new_account + self.unknown = unknown + + @classmethod + def get_fields(cls): + return { + 1: ('common', EosActionCommon, 0), + 2: ('transfer', EosActionTransfer, 0), + 3: ('delegate', EosActionDelegate, 0), + 4: ('undelegate', EosActionUndelegate, 0), + 5: ('refund', EosActionRefund, 0), + 6: ('buy_ram', EosActionBuyRam, 0), + 7: ('buy_ram_bytes', EosActionBuyRamBytes, 0), + 8: ('sell_ram', EosActionSellRam, 0), + 9: ('vote_producer', EosActionVoteProducer, 0), + 10: ('update_auth', EosActionUpdateAuth, 0), + 11: ('delete_auth', EosActionDeleteAuth, 0), + 12: ('link_auth', EosActionLinkAuth, 0), + 13: ('unlink_auth', EosActionUnlinkAuth, 0), + 14: ('new_account', EosActionNewAccount, 0), + 15: ('unknown', EosActionUnknown, 0), + } diff --git a/core/src/trezor/messages/EosTxActionRequest.py b/core/src/trezor/messages/EosTxActionRequest.py new file mode 100644 index 000000000..c2c5cd41f --- /dev/null +++ b/core/src/trezor/messages/EosTxActionRequest.py @@ -0,0 +1,19 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + + +class EosTxActionRequest(p.MessageType): + MESSAGE_WIRE_TYPE = 603 + + def __init__( + self, + data_size: int = None, + ) -> None: + self.data_size = data_size + + @classmethod + def get_fields(cls): + return { + 1: ('data_size', p.UVarintType, 0), + } diff --git a/core/src/trezor/messages/EosTxHeader.py b/core/src/trezor/messages/EosTxHeader.py new file mode 100644 index 000000000..2ef7e881c --- /dev/null +++ b/core/src/trezor/messages/EosTxHeader.py @@ -0,0 +1,33 @@ +# Automatically generated by pb2py +# fmt: off +import protobuf as p + + +class EosTxHeader(p.MessageType): + + def __init__( + self, + expiration: int = None, + ref_block_num: int = None, + ref_block_prefix: int = None, + max_net_usage_words: int = None, + max_cpu_usage_ms: int = None, + delay_sec: int = None, + ) -> None: + self.expiration = expiration + self.ref_block_num = ref_block_num + self.ref_block_prefix = ref_block_prefix + self.max_net_usage_words = max_net_usage_words + self.max_cpu_usage_ms = max_cpu_usage_ms + self.delay_sec = delay_sec + + @classmethod + def get_fields(cls): + return { + 1: ('expiration', p.UVarintType, 0), # required + 2: ('ref_block_num', p.UVarintType, 0), # required + 3: ('ref_block_prefix', p.UVarintType, 0), # required + 4: ('max_net_usage_words', p.UVarintType, 0), # required + 5: ('max_cpu_usage_ms', p.UVarintType, 0), # required + 6: ('delay_sec', p.UVarintType, 0), # required + } diff --git a/core/tests/test_apps.eos.check_action.py b/core/tests/test_apps.eos.check_action.py new file mode 100644 index 000000000..99c30db4b --- /dev/null +++ b/core/tests/test_apps.eos.check_action.py @@ -0,0 +1,64 @@ +from common import * + +from apps.eos.actions import check_action +from trezor.messages.EosTxActionAck import EosTxActionAck +from trezor.messages.EosActionBuyRam import EosActionBuyRam +from trezor.messages.EosActionBuyRamBytes import EosActionBuyRamBytes +from trezor.messages.EosActionDelegate import EosActionDelegate +from trezor.messages.EosActionDeleteAuth import EosActionDeleteAuth +from trezor.messages.EosActionLinkAuth import EosActionLinkAuth +from trezor.messages.EosActionNewAccount import EosActionNewAccount +from trezor.messages.EosActionRefund import EosActionRefund +from trezor.messages.EosActionSellRam import EosActionSellRam +from trezor.messages.EosActionTransfer import EosActionTransfer +from trezor.messages.EosActionUndelegate import EosActionUndelegate +from trezor.messages.EosActionUnlinkAuth import EosActionUnlinkAuth +from trezor.messages.EosActionUpdateAuth import EosActionUpdateAuth +from trezor.messages.EosActionVoteProducer import EosActionVoteProducer + + +class TestEosActions(unittest.TestCase): + def test_check_action(self): + # return True + self.assertEqual(check_action(EosTxActionAck(buy_ram=EosActionBuyRam()), 'buyram', 'eosio'), True) + self.assertEqual(check_action(EosTxActionAck(buy_ram_bytes=EosActionBuyRamBytes()), 'buyrambytes', 'eosio'), True) + self.assertEqual(check_action(EosTxActionAck(sell_ram=EosActionSellRam()), 'sellram', 'eosio'), True) + self.assertEqual(check_action(EosTxActionAck(delegate=EosActionDelegate()), 'delegatebw', 'eosio'), True) + self.assertEqual(check_action(EosTxActionAck(undelegate=EosActionDeleteAuth()), 'undelegatebw', 'eosio'), True) + self.assertEqual(check_action(EosTxActionAck(refund=EosActionRefund()), 'refund', 'eosio'), True) + self.assertEqual(check_action(EosTxActionAck(vote_producer=EosActionVoteProducer()), 'voteproducer', 'eosio'), True) + self.assertEqual(check_action(EosTxActionAck(update_auth=EosActionUpdateAuth()), 'updateauth', 'eosio'), True) + self.assertEqual(check_action(EosTxActionAck(delete_auth=EosActionDeleteAuth()), 'deleteauth', 'eosio'), True) + self.assertEqual(check_action(EosTxActionAck(link_auth=EosActionLinkAuth()), 'linkauth', 'eosio'), True) + self.assertEqual(check_action(EosTxActionAck(unlink_auth=EosActionUnlinkAuth()), 'unlinkauth', 'eosio'), True) + self.assertEqual(check_action(EosTxActionAck(new_account=EosActionNewAccount()), 'newaccount', 'eosio'), True) + self.assertEqual(check_action(EosTxActionAck(transfer=EosActionTransfer()), 'transfer', 'not_eosio'), True) + self.assertEqual(check_action(EosTxActionAck(unknown=[]), 'unknown', 'not_eosio'), True) + self.assertEqual(check_action(EosTxActionAck(unknown=[]), 'buyram', 'buygoods'), True) + + + # returns False + self.assertEqual(check_action(EosTxActionAck(buy_ram=EosActionBuyRam()), 'buyram', 'not_eosio'), False) + self.assertEqual(check_action(EosTxActionAck(), 'buyram', 'eosio'), False) + self.assertEqual(check_action(EosTxActionAck(buy_ram_bytes=EosActionBuyRamBytes()), 'buyrambytes', 'not_eosio'), False) + self.assertEqual(check_action(EosTxActionAck(sell_ram=EosActionSellRam()), 'sellram', 'not_eosio'), False) + self.assertEqual(check_action(EosTxActionAck(delegate=EosActionDelegate()), 'delegatebw', 'not_eosio'), False) + self.assertEqual(check_action(EosTxActionAck(undelegate=EosActionDeleteAuth()), 'undelegatebw', 'not_eosio'), False) + self.assertEqual(check_action(EosTxActionAck(refund=EosActionRefund()), 'refund', 'not_eosio'), False) + self.assertEqual(check_action(EosTxActionAck(), 'refund', 'eosio'), False) + self.assertEqual(check_action(EosTxActionAck(vote_producer=EosActionVoteProducer()), 'voteproducer', 'not_eosio'), False) + self.assertEqual(check_action(EosTxActionAck(update_auth=EosActionUpdateAuth()), 'updateauth', 'not_eosio'), False) + self.assertEqual(check_action(EosTxActionAck(delete_auth=EosActionDeleteAuth()), 'deleteauth', 'not_eosio'), False) + self.assertEqual(check_action(EosTxActionAck(link_auth=EosActionLinkAuth()), 'linkauth', 'not_eosio'), False) + self.assertEqual(check_action(EosTxActionAck(unlink_auth=EosActionUnlinkAuth()), 'unlinkauth', 'not_eosio'), False) + self.assertEqual(check_action(EosTxActionAck(), 'unlinkauth', 'eosio'), False) + self.assertEqual(check_action(EosTxActionAck(new_account=EosActionNewAccount()), 'newaccount', 'not_eosio'), False) + self.assertEqual(check_action(EosTxActionAck(transfer=EosActionTransfer()), 'transfer', 'eosio'), False) + self.assertEqual(check_action(EosTxActionAck(), 'unknown', 'not_eosio'), False) + self.assertEqual(check_action(EosTxActionAck(buy_ram=EosActionBuyRam()), 'test', 'eosio'), False) + self.assertEqual(check_action(EosTxActionAck(unknown=[]), 'buyram', 'eosio'), False) + self.assertEqual(check_action(EosTxActionAck(unknown=[]), 'transfer', 'loveme'), False) + + +if __name__ == '__main__': + unittest.main() diff --git a/core/tests/test_apps.eos.conversions.py b/core/tests/test_apps.eos.conversions.py new file mode 100644 index 000000000..cbba808f7 --- /dev/null +++ b/core/tests/test_apps.eos.conversions.py @@ -0,0 +1,66 @@ +from common import * + +from apps.eos import helpers +from trezor.messages.EosAsset import EosAsset + +class TestEosConversions(unittest.TestCase): + def test_eos_name_to_string(self): + names_in = [ + 10639447606881920736, + 614251623682315968, + 614251535012020768, + 7754926748989239168, + 14895601873759291472, + 595056260442243600, + ] + names_out = [ + 'miniminimini', + '12345abcdefg', + '123451234512', + 'hijklmnopqrs', + 'tuvwxyz12345', + '111111111111', + ] + for i, o in zip(names_in, names_out): + self.assertEqual(helpers.eos_name_to_string(i), o) + + def test_eos_asset_to_string(self): + asset_in = [ + EosAsset( + amount=10000, + symbol=1397703940, + ), + EosAsset( + amount=200000, + symbol=1397703940, + ), + EosAsset( + amount=255000, + symbol=1397703940, + ), + EosAsset( + amount=999999, + symbol=1397703939, + ), + EosAsset( + amount=1, + symbol=1397703940, + ), + EosAsset( + amount=999, + symbol=1397703939, + ), + ] + asset_out = [ + '1.0000 EOS', + '20.0000 EOS', + '25.5000 EOS', + '999.999 EOS', + '0.0001 EOS', + '0.999 EOS', + ] + for i, o in zip(asset_in, asset_out): + self.assertEqual(helpers.eos_asset_to_string(i), o) + +if __name__ == '__main__': + unittest.main() diff --git a/core/tests/test_apps.eos.get_public_key.py b/core/tests/test_apps.eos.get_public_key.py new file mode 100755 index 000000000..91e4e9540 --- /dev/null +++ b/core/tests/test_apps.eos.get_public_key.py @@ -0,0 +1,72 @@ +from common import * + +from apps.eos.get_public_key import _get_public_key, _public_key_to_wif +from trezor.crypto import bip32, bip39 +from ubinascii import hexlify, unhexlify +from apps.common.paths import HARDENED +from apps.eos.helpers import validate_full_path + + +class TestEosGetPublicKey(unittest.TestCase): + def test_get_public_key_scheme(self): + mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + seed = bip39.seed(mnemonic, '') + + derivation_paths = [ + [0x80000000 | 44, 0x80000000 | 194, 0x80000000, 0, 0], + [0x80000000 | 44, 0x80000000 | 194, 0x80000000, 0, 1], + [0x80000000 | 44, 0x80000000 | 194], + [0x80000000 | 44, 0x80000000 | 194, 0x80000000, 0, 0x80000000], + ] + + public_keys = [ + b'0315c358024ce46767102578947584c4342a6982b922d454f63588effa34597197', + b'029622eff7248c4d298fe28f2df19ee0d5f7674f678844e05c31d1a5632412869e', + b'02625f33c10399703e95e41bd5054beef3ab893dcc7df2bb9bdcee48359b29069d', + b'037c9b7d24d42589941cca3f4debc75b37c0e7b881e6eb00d2e674958debe3bbc3', + ] + + wif_keys = [ + 'EOS6zpSNY1YoLxNt2VsvJjoDfBueU6xC1M1ERJw1UoekL1NHn8KNA', + 'EOS62cPUiWnLqbUjiBMxbEU4pm4Hp5X3RGk4KMTadvZNygjX72yHW', + 'EOS5dp8aCFoFwrKo6KuUfos1hwMfZGkiZUbaF2CyuD4chyBEN2wQK', + 'EOS7n7TXwR4Y3DtPt2ji6akhQi5uw4SruuPArvoNJso84vhwPQt1G', + ] + + for index, path in enumerate(derivation_paths): + node = bip32.from_seed(seed, 'secp256k1') + node.derive_path(path) + wif, public_key = _get_public_key(node) + + self.assertEqual(hexlify(public_key), public_keys[index]) + self.assertEqual(wif, wif_keys[index]) + self.assertEqual(_public_key_to_wif(public_key), wif_keys[index]) + + def test_paths(self): + # 44'/194'/a'/0/0 is correct + incorrect_paths = [ + [44 | HARDENED], + [44 | HARDENED, 194 | HARDENED], + [44 | HARDENED, 194 | HARDENED, 0 | HARDENED, 0, 0, 0], + [44 | HARDENED, 194 | HARDENED, 0 | HARDENED, 0 | HARDENED], + [44 | HARDENED, 194 | HARDENED, 0 | HARDENED, 0 | HARDENED, 0 | HARDENED], + [44 | HARDENED, 194 | HARDENED, 0 | HARDENED, 1, 0], + [44 | HARDENED, 194 | HARDENED, 0 | HARDENED, 0, 1], + [44 | HARDENED, 160 | HARDENED, 0 | HARDENED, 0, 0], + [44 | HARDENED, 199 | HARDENED, 0 | HARDENED, 0, 9999], + ] + correct_paths = [ + [44 | HARDENED, 194 | HARDENED, 0 | HARDENED, 0, 0], + [44 | HARDENED, 194 | HARDENED, 9 | HARDENED, 0, 0], + [44 | HARDENED, 194 | HARDENED, 9999 | HARDENED, 0, 0], + ] + + for path in incorrect_paths: + self.assertFalse(validate_full_path(path)) + + for path in correct_paths: + self.assertTrue(validate_full_path(path)) + + +if __name__ == '__main__': + unittest.main() diff --git a/core/tools/build_protobuf b/core/tools/build_protobuf index 0df512e43..d88664ca7 100755 --- a/core/tools/build_protobuf +++ b/core/tools/build_protobuf @@ -12,6 +12,7 @@ rm -f ../src/trezor/messages/[A-Z]*.py ../vendor/trezor-common/protob/messages-common.proto \ ../vendor/trezor-common/protob/messages-crypto.proto \ ../vendor/trezor-common/protob/messages-debug.proto \ + ../vendor/trezor-common/protob/messages-eos.proto \ ../vendor/trezor-common/protob/messages-ethereum.proto \ ../vendor/trezor-common/protob/messages-lisk.proto \ ../vendor/trezor-common/protob/messages-management.proto \ diff --git a/python/docs/OPTIONS.rst b/python/docs/OPTIONS.rst index 170a48aa2..6a2c20af8 100644 --- a/python/docs/OPTIONS.rst +++ b/python/docs/OPTIONS.rst @@ -32,6 +32,8 @@ Use the following command to see all options: disable-passphrase Disable passphrase. enable-passphrase Enable passphrase. encrypt-keyvalue Encrypt value by given key and path. + eos_get_public_key Get EOS public key in base58 encoding. + eos_sign_transaction Sign EOS transaction... ethereum-get-address Get Ethereum address in hex encoding. ethereum-sign-message Sign message with Ethereum address. ethereum-sign-tx Sign (and optionally publish) Ethereum transaction. diff --git a/python/trezorctl b/python/trezorctl index 87b691133..86b2ac525 100755 --- a/python/trezorctl +++ b/python/trezorctl @@ -37,6 +37,7 @@ from trezorlib import ( cosi, debuglink, device, + eos, ethereum, exceptions, firmware, @@ -1414,6 +1415,48 @@ def ethereum_sign_tx( return "Signed raw transaction:\n%s" % tx_hex +# +# EOS functions +# + + +@cli.command(help="Get Eos public key in base58 encoding.") +@click.option( + "-n", "--address", required=True, help="BIP-32 path, e.g. m/44'/194'/0'/0/0" +) +@click.option("-d", "--show-display", is_flag=True) +@click.pass_obj +def eos_get_public_key(connect, address, show_display): + client = connect() + address_n = tools.parse_path(address) + res = eos.get_public_key(client, address_n, show_display) + return "WIF: {}\nRaw: {}".format(res.wif_public_key, res.raw_public_key.hex()) + + +@cli.command(help="Init sign (and optionally publish) EOS transaction. ") +@click.option( + "-n", + "--address", + required=True, + help="BIP-32 path to source address, e.g., m/44'/194'/0'/0/0", +) +@click.option( + "-f", + "--file", + type=click.File("r"), + required=True, + help="Transaction in JSON format", +) +@click.pass_obj +def eos_sign_transaction(connect, address, file): + client = connect() + + tx_json = json.load(file) + + address_n = tools.parse_path(address) + return eos.sign_tx(client, address_n, tx_json["transaction"], tx_json["chain_id"]) + + # # ADA functions # diff --git a/python/trezorlib/eos.py b/python/trezorlib/eos.py new file mode 100644 index 000000000..985aa4924 --- /dev/null +++ b/python/trezorlib/eos.py @@ -0,0 +1,332 @@ +from datetime import datetime + +from . import messages +from .tools import CallException, b58decode, expect, session + + +def name_to_number(name): + length = len(name) + value = 0 + + for i in range(0, 13): + c = 0 + if i < length and i < 13: + c = char_to_symbol(name[i]) + + if i < 12: + c &= 0x1F + c <<= 64 - 5 * (i + 1) + else: + c &= 0x0F + + value |= c + + return value + + +def char_to_symbol(c): + if c >= "a" and c <= "z": + return ord(c) - ord("a") + 6 + elif c >= "1" and c <= "5": + return ord(c) - ord("1") + 1 + else: + return 0 + + +def parse_asset(asset): + amount_str, symbol_str = asset.split(" ") + + # "-1.0000" => ["-1", "0000"] => -10000 + amount_parts = amount_str.split(".", maxsplit=1) + amount = int("".join(amount_parts)) + + precision = 0 + if len(amount_parts) > 1: + precision = len(amount_parts[1]) + + # 4, "EOS" => b"\x04EOS" => little-endian uint32 + symbol_bytes = bytes([precision]) + symbol_str.encode() + symbol = int.from_bytes(symbol_bytes, "little") + + return messages.EosAsset(amount=amount, symbol=symbol) + + +def public_key_to_buffer(pub_key): + _t = 0 + if pub_key[:3] == "EOS": + pub_key = pub_key[3:] + _t = 0 + elif pub_key[:7] == "PUB_K1_": + pub_key = pub_key[7:] + _t = 0 + elif pub_key[:7] == "PUB_R1_": + pub_key = pub_key[7:] + _t = 1 + + return _t, b58decode(pub_key, None)[:-4] + + +def parse_common(action): + authorization = [] + for auth in action["authorization"]: + authorization.append( + messages.EosPermissionLevel( + actor=name_to_number(auth["actor"]), + permission=name_to_number(auth["permission"]), + ) + ) + + return messages.EosActionCommon( + account=name_to_number(action["account"]), + name=name_to_number(action["name"]), + authorization=authorization, + ) + + +def parse_transfer(data): + return messages.EosActionTransfer( + sender=name_to_number(data["from"]), + receiver=name_to_number(data["to"]), + memo=data["memo"], + quantity=parse_asset(data["quantity"]), + ) + + +def parse_vote_producer(data): + producers = [] + for producer in data["producers"]: + producers.append(name_to_number(producer)) + + return messages.EosActionVoteProducer( + voter=name_to_number(data["account"]), + proxy=name_to_number(data["proxy"]), + producers=producers, + ) + + +def parse_buy_ram(data): + return messages.EosActionBuyRam( + payer=name_to_number(data["payer"]), + receiver=name_to_number(data["receiver"]), + quantity=parse_asset(data["quant"]), + ) + + +def parse_buy_rambytes(data): + return messages.EosActionBuyRamBytes( + payer=name_to_number(data["payer"]), + receiver=name_to_number(data["receiver"]), + bytes=int(data["bytes"]), + ) + + +def parse_sell_ram(data): + return messages.EosActionSellRam( + account=name_to_number(data["account"]), bytes=int(data["bytes"]) + ) + + +def parse_delegate(data): + return messages.EosActionDelegate( + sender=name_to_number(data["sender"]), + receiver=name_to_number(data["receiver"]), + net_quantity=parse_asset(data["stake_net_quantity"]), + cpu_quantity=parse_asset(data["stake_cpu_quantity"]), + transfer=bool(data["transfer"]), + ) + + +def parse_undelegate(data): + return messages.EosActionUndelegate( + sender=name_to_number(data["sender"]), + receiver=name_to_number(data["receiver"]), + net_quantity=parse_asset(data["unstake_net_quantity"]), + cpu_quantity=parse_asset(data["unstake_cpu_quantity"]), + ) + + +def parse_refund(data): + return messages.EosActionRefund(owner=name_to_number(data["owner"])) + + +def parse_updateauth(data): + auth = parse_authorization(data["auth"]) + + return messages.EosActionUpdateAuth( + account=name_to_number(data["account"]), + permission=name_to_number(data["permission"]), + parent=name_to_number(data["parent"]), + auth=auth, + ) + + +def parse_deleteauth(data): + return messages.EosActionDeleteAuth( + account=name_to_number(data["account"]), + permission=name_to_number(data["permission"]), + ) + + +def parse_linkauth(data): + return messages.EosActionLinkAuth( + account=name_to_number(data["account"]), + code=name_to_number(data["code"]), + type=name_to_number(data["type"]), + requirement=name_to_number(data["requirement"]), + ) + + +def parse_unlinkauth(data): + return messages.EosActionUnlinkAuth( + account=name_to_number(data["account"]), + code=name_to_number(data["code"]), + type=name_to_number(data["type"]), + ) + + +def parse_authorization(data): + keys = [] + for key in data["keys"]: + _t, _k = public_key_to_buffer(key["key"]) + + keys.append( + messages.EosAuthorizationKey(type=_t, key=_k, weight=int(key["weight"])) + ) + + accounts = [] + for account in data["accounts"]: + accounts.append( + messages.EosAuthorizationAccount( + account=messages.EosPermissionLevel( + actor=name_to_number(account["permission"]["actor"]), + permission=name_to_number(account["permission"]["permission"]), + ), + weight=int(account["weight"]), + ) + ) + + waits = [] + for wait in data["waits"]: + waits.append( + messages.EosAuthorizationWait( + wait_sec=int(wait["wait_sec"]), weight=int(wait["weight"]) + ) + ) + + return messages.EosAuthorization( + threshold=int(data["threshold"]), keys=keys, accounts=accounts, waits=waits + ) + + +def parse_new_account(data): + owner = parse_authorization(data["owner"]) + active = parse_authorization(data["active"]) + + return messages.EosActionNewAccount( + creator=name_to_number(data["creator"]), + name=name_to_number(data["name"]), + owner=owner, + active=active, + ) + + +def parse_unknown(data): + data_bytes = bytes.fromhex(data) + return messages.EosActionUnknown(data_size=len(data_bytes), data_chunk=data_bytes) + + +def parse_action(action): + tx_action = messages.EosTxActionAck() + data = action["data"] + + tx_action.common = parse_common(action) + + if action["account"] == "eosio": + if action["name"] == "voteproducer": + tx_action.vote_producer = parse_vote_producer(data) + elif action["name"] == "buyram": + tx_action.buy_ram = parse_buy_ram(data) + elif action["name"] == "buyrambytes": + tx_action.buy_ram_bytes = parse_buy_rambytes(data) + elif action["name"] == "sellram": + tx_action.sell_ram = parse_sell_ram(data) + elif action["name"] == "delegatebw": + tx_action.delegate = parse_delegate(data) + elif action["name"] == "undelegatebw": + tx_action.undelegate = parse_undelegate(data) + elif action["name"] == "refund": + tx_action.refund = parse_refund(data) + elif action["name"] == "updateauth": + tx_action.update_auth = parse_updateauth(data) + elif action["name"] == "deleteauth": + tx_action.delete_auth = parse_deleteauth(data) + elif action["name"] == "linkauth": + tx_action.link_auth = parse_linkauth(data) + elif action["name"] == "unlinkauth": + tx_action.unlink_auth = parse_unlinkauth(data) + elif action["name"] == "newaccount": + tx_action.new_account = parse_new_account(data) + elif action["name"] == "transfer": + tx_action.transfer = parse_transfer(data) + else: + tx_action.unknown = parse_unknown(data) + + return tx_action + + +def parse_transaction_json(transaction): + header = messages.EosTxHeader() + header.expiration = int( + ( + datetime.strptime(transaction["expiration"], "%Y-%m-%dT%H:%M:%S") + - datetime(1970, 1, 1) + ).total_seconds() + ) + header.ref_block_num = int(transaction["ref_block_num"]) + header.ref_block_prefix = int(transaction["ref_block_prefix"]) + header.max_net_usage_words = int(transaction["net_usage_words"]) + header.max_cpu_usage_ms = int(transaction["max_cpu_usage_ms"]) + header.delay_sec = int(transaction["delay_sec"]) + + actions = [parse_action(a) for a in transaction["actions"]] + + return header, actions + + +# ====== Client functions ====== # + + +@expect(messages.EosPublicKey) +def get_public_key(client, n, show_display=False, multisig=None): + response = client.call( + messages.EosGetPublicKey(address_n=n, show_display=show_display) + ) + return response + + +@session +def sign_tx(client, address, transaction, chain_id): + header, actions = parse_transaction_json(transaction) + + msg = messages.EosSignTx() + msg.address_n = address + msg.chain_id = bytes.fromhex(chain_id) + msg.header = header + msg.num_actions = len(actions) + + response = client.call(msg) + + try: + while isinstance(response, messages.EosTxActionRequest): + response = client.call(actions.pop(0)) + except IndexError: + # pop from empty list + raise CallException( + "Eos.UnexpectedEndOfOperations", + "Reached end of operations without a signature.", + ) from None + + if not isinstance(response, messages.EosSignedTx): + raise CallException(messages.FailureType.UnexpectedMessage, response) + + return response diff --git a/python/trezorlib/tests/device_tests/test_msg_eos_get_public_key.py b/python/trezorlib/tests/device_tests/test_msg_eos_get_public_key.py new file mode 100644 index 000000000..f7ff9f755 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_eos_get_public_key.py @@ -0,0 +1,40 @@ +import pytest + +from trezorlib.eos import get_public_key +from trezorlib.tools import parse_path + +from .common import TrezorTest + + +@pytest.mark.skip_t1 +@pytest.mark.eos +class TestMsgEosGetpublickey(TrezorTest): + def test_eos_get_public_key(self): + self.setup_mnemonic_nopin_nopassphrase() + public_key = get_public_key(self.client, parse_path("m/44'/194'/0'/0/0")) + assert ( + public_key.wif_public_key + == "EOS4u6Sfnzj4Sh2pEQnkXyZQJqH3PkKjGByDCbsqqmyq6PttM9KyB" + ) + assert ( + public_key.raw_public_key.hex() + == "02015fabe197c955036bab25f4e7c16558f9f672f9f625314ab1ec8f64f7b1198e" + ) + public_key = get_public_key(self.client, parse_path("m/44'/194'/0'/0/1")) + assert ( + public_key.wif_public_key + == "EOS5d1VP15RKxT4dSakWu2TFuEgnmaGC2ckfSvQwND7pZC1tXkfLP" + ) + assert ( + public_key.raw_public_key.hex() + == "02608bc2c431521dee0b9d5f2fe34053e15fc3b20d2895e0abda857b9ed8e77a78" + ) + public_key = get_public_key(self.client, parse_path("m/44'/194'/1'/0/0")) + assert ( + public_key.wif_public_key + == "EOS7UuNeTf13nfcG85rDB7AHGugZi4C4wJ4ft12QRotqNfxdV2NvP" + ) + assert ( + public_key.raw_public_key.hex() + == "035588a197bd5a7356e8a702361b2d535c6372f843874bed6617cd1afe1dfcb502" + ) diff --git a/python/trezorlib/tests/device_tests/test_msg_eos_signtx.py b/python/trezorlib/tests/device_tests/test_msg_eos_signtx.py new file mode 100644 index 000000000..f913569a4 --- /dev/null +++ b/python/trezorlib/tests/device_tests/test_msg_eos_signtx.py @@ -0,0 +1,860 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2018 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +import time + +import pytest + +from trezorlib import eos +from trezorlib.messages import EosSignedTx +from trezorlib.tools import parse_path + +from .common import TrezorTest + +CHAIN_ID = "cf057bbfb72640471fd910bcb67639c22df9f92470936cddc1ade0e2f2e7dc4f" +ADDRESS_N = parse_path("m/44'/194'/0'/0/0") + + +@pytest.mark.skip_t1 +@pytest.mark.eos +class TestMsgEosSignTx(TrezorTest): + def input_flow(self, pages): + # confirm number of actions + yield + self.client.debug.press_yes() + + # swipe through pages + yield + for _ in range(pages - 1): + self.client.debug.swipe_down() + time.sleep(1) + + # confirm last page + self.client.debug.press_yes() + + def test_eos_signtx_transfer_token(self): + self.setup_mnemonic_nopin_nopassphrase() + transaction = { + "expiration": "2018-07-14T10:43:28", + "ref_block_num": 6439, + "ref_block_prefix": 2995713264, + "net_usage_words": 0, + "max_cpu_usage_ms": 0, + "delay_sec": 0, + "context_free_actions": [], + "actions": [ + { + "account": "eosio.token", + "name": "transfer", + "authorization": [ + {"actor": "miniminimini", "permission": "active"} + ], + "data": { + "from": "miniminimini", + "to": "maximaximaxi", + "quantity": "1.0000 EOS", + "memo": "testtest", + }, + } + ], + "transaction_extensions": [], + } + + with self.client: + self.client.set_input_flow(self.input_flow(pages=3)) + resp = eos.sign_tx(self.client, ADDRESS_N, transaction, CHAIN_ID) + + assert isinstance(resp, EosSignedTx) + assert ( + resp.signature_r.hex() + == "0a9a0f467697010b743ffd02eae6698464c8b5c84b696245397287c225f85e01" + ) + assert ( + resp.signature_s.hex() + == "3ec6a0175e5209be6789587a9d6b5f61593b841a751112faa05d9efdd9239d40" + ) + assert resp.signature_v == 31 + + def test_eos_signtx_buyram(self): + self.setup_mnemonic_nopin_nopassphrase() + transaction = { + "expiration": "2018-07-14T10:43:28", + "ref_block_num": 6439, + "ref_block_prefix": 2995713264, + "net_usage_words": 0, + "max_cpu_usage_ms": 0, + "delay_sec": 0, + "context_free_actions": [], + "actions": [ + { + "account": "eosio", + "name": "buyram", + "authorization": [ + {"actor": "miniminimini", "permission": "active"} + ], + "data": { + "payer": "miniminimini", + "receiver": "miniminimini", + "quant": "1000000000.0000 EOS", + }, + } + ], + "transaction_extensions": [], + } + + with self.client: + self.client.set_input_flow(self.input_flow(pages=2)) + resp = eos.sign_tx(self.client, ADDRESS_N, transaction, CHAIN_ID) + + assert isinstance(resp, EosSignedTx) + assert ( + resp.signature_r.hex() + == "480bdc505ba196d445d92ea12bda9d39f986d01620efcffe98bcf645ddcbb4ec" + ) + assert ( + resp.signature_s.hex() + == "35c8e2105f0228c9e1e511682ae79eac1b7b90bc84c1a0dae13245b7f0f96abf" + ) + assert resp.signature_v == 31 + + def test_eos_signtx_buyrambytes(self): + self.setup_mnemonic_nopin_nopassphrase() + transaction = { + "expiration": "2018-07-14T10:43:28", + "ref_block_num": 6439, + "ref_block_prefix": 2995713264, + "net_usage_words": 0, + "max_cpu_usage_ms": 0, + "delay_sec": 0, + "context_free_actions": [], + "actions": [ + { + "account": "eosio", + "name": "buyrambytes", + "authorization": [ + {"actor": "miniminimini", "permission": "active"} + ], + "data": { + "payer": "miniminimini", + "receiver": "miniminimini", + "bytes": 1023, + }, + } + ], + "transaction_extensions": [], + } + + with self.client: + self.client.set_input_flow(self.input_flow(pages=2)) + resp = eos.sign_tx(self.client, ADDRESS_N, transaction, CHAIN_ID) + + assert isinstance(resp, EosSignedTx) + assert ( + resp.signature_r.hex() + == "52267ee5f3ff73939af5ccdaa3406e0783deaf76accf5ce4ceb9714cdbdf7d6b" + ) + assert ( + resp.signature_s.hex() + == "53aa9a9ecf044396441a559b51d3b97e239321c895823aad6888b0de2063a078" + ) + assert resp.signature_v == 31 + + def test_eos_signtx_sellram(self): + self.setup_mnemonic_nopin_nopassphrase() + transaction = { + "expiration": "2018-07-14T10:43:28", + "ref_block_num": 6439, + "ref_block_prefix": 2995713264, + "net_usage_words": 0, + "max_cpu_usage_ms": 0, + "delay_sec": 0, + "context_free_actions": [], + "actions": [ + { + "account": "eosio", + "name": "sellram", + "authorization": [ + {"actor": "miniminimini", "permission": "active"} + ], + "data": {"account": "miniminimini", "bytes": 1024}, + } + ], + "transaction_extensions": [], + } + + with self.client: + self.client.set_input_flow(self.input_flow(pages=2)) + resp = eos.sign_tx(self.client, ADDRESS_N, transaction, CHAIN_ID) + + assert isinstance(resp, EosSignedTx) + assert ( + resp.signature_r.hex() + == "04c0fdf1d2e0ea21af292173eacc2c7db90f7764abe69b79a8c2b24201af27c4" + ) + assert ( + resp.signature_s.hex() + == "7bb29f12eaaabbebdb5190d30367012a80128138b5024b30e93e3afb3d24734e" + ) + assert resp.signature_v == 31 + + def test_eos_signtx_delegate(self): + self.setup_mnemonic_nopin_nopassphrase() + transaction = { + "expiration": "2018-07-14T10:43:28", + "ref_block_num": 6439, + "ref_block_prefix": 2995713264, + "net_usage_words": 0, + "max_cpu_usage_ms": 0, + "delay_sec": 0, + "context_free_actions": [], + "actions": [ + { + "account": "eosio", + "name": "delegatebw", + "authorization": [ + {"actor": "miniminimini", "permission": "active"} + ], + "data": { + "sender": "miniminimini", + "receiver": "maximaximaxi", + "stake_net_quantity": "1.0000 EOS", + "stake_cpu_quantity": "1.0000 EOS", + "transfer": True, + }, + } + ], + "transaction_extensions": [], + } + + with self.client: + self.client.set_input_flow(self.input_flow(pages=3)) + resp = eos.sign_tx(self.client, ADDRESS_N, transaction, CHAIN_ID) + + assert isinstance(resp, EosSignedTx) + assert ( + resp.signature_r.hex() + == "03b4ccb74b7ad54f28fdeda244facb3038cf70424fd6aa4b171a3bb02a591504" + ) + assert ( + resp.signature_s.hex() + == "4e24e08d1789421e17ba47e0e4635a3721400a795e40a8896dc5e5af4a95343d" + ) + assert resp.signature_v == 31 + + def test_eos_signtx_undelegate(self): + self.setup_mnemonic_nopin_nopassphrase() + transaction = { + "expiration": "2018-07-14T10:43:28", + "ref_block_num": 6439, + "ref_block_prefix": 2995713264, + "net_usage_words": 0, + "max_cpu_usage_ms": 0, + "delay_sec": 0, + "context_free_actions": [], + "actions": [ + { + "account": "eosio", + "name": "undelegatebw", + "authorization": [ + {"actor": "miniminimini", "permission": "active"} + ], + "data": { + "sender": "miniminimini", + "receiver": "maximaximaxi", + "unstake_net_quantity": "1.0000 EOS", + "unstake_cpu_quantity": "1.0000 EOS", + }, + } + ], + "transaction_extensions": [], + } + + with self.client: + self.client.set_input_flow(self.input_flow(pages=2)) + resp = eos.sign_tx(self.client, ADDRESS_N, transaction, CHAIN_ID) + + assert isinstance(resp, EosSignedTx) + assert ( + resp.signature_r.hex() + == "3f39722a88f12395f3cfcdbe218c185f02295ec07a5da8f4b953d5ec3c9ec36b" + ) + assert ( + resp.signature_s.hex() + == "7acbae47d60cd538ca28fcc8f3dae8f03b3812e7719dd4e9c069a66dbac5ebf3" + ) + assert resp.signature_v == 31 + + def test_eos_signtx_refund(self): + self.setup_mnemonic_nopin_nopassphrase() + transaction = { + "expiration": "2018-07-14T10:43:28", + "ref_block_num": 6439, + "ref_block_prefix": 2995713264, + "net_usage_words": 0, + "max_cpu_usage_ms": 0, + "delay_sec": 0, + "context_free_actions": [], + "actions": [ + { + "account": "eosio", + "name": "refund", + "authorization": [ + {"actor": "miniminimini", "permission": "active"} + ], + "data": {"owner": "miniminimini"}, + } + ], + "transaction_extensions": [], + } + + with self.client: + self.client.set_input_flow(self.input_flow(pages=2)) + resp = eos.sign_tx(self.client, ADDRESS_N, transaction, CHAIN_ID) + + assert isinstance(resp, EosSignedTx) + assert ( + resp.signature_r.hex() + == "113c4867f77c371ff4701beb794ff0a0a6a1137a0115d0f4b5245c391e9f596f" + ) + assert ( + resp.signature_s.hex() + == "27203aaaeb8cdbc92c0af32f840c385ac6202e3b4e927bda59d397ebef513381" + ) + assert resp.signature_v == 31 + + def test_eos_signtx_linkauth(self): + self.setup_mnemonic_nopin_nopassphrase() + transaction = { + "expiration": "2018-07-14T10:43:28", + "ref_block_num": 6439, + "ref_block_prefix": 2995713264, + "net_usage_words": 0, + "max_cpu_usage_ms": 0, + "delay_sec": 0, + "context_free_actions": [], + "actions": [ + { + "account": "eosio", + "name": "linkauth", + "authorization": [ + {"actor": "miniminimini", "permission": "active"} + ], + "data": { + "account": "maximaximaxi", + "code": "eosbet", + "type": "whatever", + "requirement": "active", + }, + } + ], + "transaction_extensions": [], + } + + with self.client: + self.client.set_input_flow(self.input_flow(pages=2)) + resp = eos.sign_tx(self.client, ADDRESS_N, transaction, CHAIN_ID) + + assert isinstance(resp, EosSignedTx) + assert ( + resp.signature_r.hex() + == "5c9bf154dc77649ccf5a997441fcd4041e9da79149078df27a1c6268cf237c75" + ) + assert ( + resp.signature_s.hex() + == "3e432ddcd17feb2997145d11240b0ca4344a01e2d96e9886533bca7ffceb10cd" + ) + assert resp.signature_v == 32 + + def test_eos_signtx_unlinkauth(self): + self.setup_mnemonic_nopin_nopassphrase() + transaction = { + "expiration": "2018-07-14T10:43:28", + "ref_block_num": 6439, + "ref_block_prefix": 2995713264, + "net_usage_words": 0, + "max_cpu_usage_ms": 0, + "delay_sec": 0, + "context_free_actions": [], + "actions": [ + { + "account": "eosio", + "name": "unlinkauth", + "authorization": [ + {"actor": "miniminimini", "permission": "active"} + ], + "data": { + "account": "miniminimini", + "code": "eosbet", + "type": "whatever", + }, + } + ], + "transaction_extensions": [], + } + + with self.client: + self.client.set_input_flow(self.input_flow(pages=2)) + resp = eos.sign_tx(self.client, ADDRESS_N, transaction, CHAIN_ID) + + assert isinstance(resp, EosSignedTx) + assert ( + resp.signature_r.hex() + == "316c296594fd7a4dd3b615d80c630fda256e9a3460b00d4f16eede1fb2af9574" + ) + assert ( + resp.signature_s.hex() + == "76d023913b4f323cfa857d144bf78a4d561954bb23c5df9a31649c9503c3a3b7" + ) + assert resp.signature_v == 31 + + def test_eos_signtx_updateauth(self): + self.setup_mnemonic_nopin_nopassphrase() + transaction = { + "expiration": "2018-07-14T10:43:28", + "ref_block_num": 6439, + "ref_block_prefix": 2995713264, + "net_usage_words": 0, + "max_cpu_usage_ms": 0, + "delay_sec": 0, + "context_free_actions": [], + "actions": [ + { + "account": "eosio", + "name": "updateauth", + "authorization": [ + {"actor": "miniminimini", "permission": "active"} + ], + "data": { + "account": "miniminimini", + "permission": "active", + "parent": "owner", + "auth": { + "threshold": 1, + "keys": [ + { + "key": "EOS8Dkj827FpinZBGmhTM28B85H9eXiFH5XzvLoeukCJV5sKfLc6K", + "weight": 1, + }, + { + "key": "EOS8Dkj827FpinZBGmhTM28B85H9eXiFH5XzvLoeukCJV5sKfLc6K", + "weight": 2, + }, + ], + "accounts": [ + { + "permission": { + "actor": "miniminimini", + "permission": "active", + }, + "weight": 3, + } + ], + "waits": [{"wait_sec": 55, "weight": 4}], + }, + }, + } + ], + "transaction_extensions": [], + } + + with self.client: + self.client.set_input_flow(self.input_flow(pages=8)) + resp = eos.sign_tx(self.client, ADDRESS_N, transaction, CHAIN_ID) + + assert isinstance(resp, EosSignedTx) + assert ( + resp.signature_r.hex() + == "00f0ca8ffa8208e72df509a3b356e77056b234d4db167b58d485f30cb9c61841" + ) + assert ( + resp.signature_s.hex() + == "3f6fb40ffa4e1cf6f3bcb0d8fa3873a2b5a05384ca9251159968558688a4e43d" + ) + assert resp.signature_v == 31 + + def test_eos_signtx_deleteauth(self): + self.setup_mnemonic_nopin_nopassphrase() + transaction = { + "expiration": "2018-07-14T10:43:28", + "ref_block_num": 6439, + "ref_block_prefix": 2995713264, + "net_usage_words": 0, + "max_cpu_usage_ms": 0, + "delay_sec": 0, + "context_free_actions": [], + "actions": [ + { + "account": "eosio", + "name": "deleteauth", + "authorization": [ + {"actor": "miniminimini", "permission": "active"} + ], + "data": {"account": "maximaximaxi", "permission": "active"}, + } + ], + "transaction_extensions": [], + } + + with self.client: + self.client.set_input_flow(self.input_flow(pages=2)) + resp = eos.sign_tx(self.client, ADDRESS_N, transaction, CHAIN_ID) + + assert isinstance(resp, EosSignedTx) + assert ( + resp.signature_r.hex() + == "6fe7d66f8be2fe3de23c48561e8a17113d1a0aabcf0d4160e9bd8af90f5a608f" + ) + assert ( + resp.signature_s.hex() + == "3cec8db96be2f6aa7bb00302cec6ad3c8655b492f9a2b84b3c61df6bc81f0d83" + ) + assert resp.signature_v == 32 + + def test_eos_signtx_vote(self): + self.setup_mnemonic_nopin_nopassphrase() + transaction = { + "expiration": "2018-07-14T10:43:28", + "ref_block_num": 6439, + "ref_block_prefix": 2995713264, + "net_usage_words": 0, + "max_cpu_usage_ms": 0, + "delay_sec": 0, + "context_free_actions": [], + "actions": [ + { + "account": "eosio", + "name": "voteproducer", + "authorization": [ + {"actor": "miniminimini", "permission": "active"} + ], + "data": { + "account": "miniminimini", + "proxy": "", + "producers": [ + "argentinaeos", + "bitfinexeos1", + "cryptolions1", + "eos42freedom", + "eosamsterdam", + "eosasia11111", + "eosauthority", + "eosbeijingbp", + "eosbixinboot", + "eoscafeblock", + "eoscanadacom", + "eoscannonchn", + "eoscleanerbp", + "eosdacserver", + "eosfishrocks", + "eosflytomars", + "eoshuobipool", + "eosisgravity", + "eoslaomaocom", + "eosliquideos", + "eosnewyorkio", + "eosriobrazil", + "eosswedenorg", + "eostribeprod", + "helloeoscnbp", + "jedaaaaaaaaa", + "libertyblock", + "starteosiobp", + "teamgreymass", + ], + }, + } + ], + "transaction_extensions": [], + } + + with self.client: + self.client.set_input_flow(self.input_flow(pages=6)) + resp = eos.sign_tx(self.client, ADDRESS_N, transaction, CHAIN_ID) + + assert isinstance(resp, EosSignedTx) + assert ( + resp.signature_r.hex() + == "1a303dcb27d2d17bc9efc89b10c41d9d78f7e3d671e3475bb1115b988f918770" + ) + assert ( + resp.signature_s.hex() + == "07869385bf3af8cf0a4ee9daf4f8dd122650c7d59da48d6d9ce1e26b59753324" + ) + assert resp.signature_v == 31 + + def test_eos_signtx_vote_proxy(self): + self.setup_mnemonic_nopin_nopassphrase() + transaction = { + "expiration": "2018-07-14T10:43:28", + "ref_block_num": 6439, + "ref_block_prefix": 2995713264, + "net_usage_words": 0, + "max_cpu_usage_ms": 0, + "delay_sec": 0, + "context_free_actions": [], + "actions": [ + { + "account": "eosio", + "name": "voteproducer", + "authorization": [ + {"actor": "miniminimini", "permission": "active"} + ], + "data": {"account": "miniminimini", "proxy": "", "producers": []}, + } + ], + "transaction_extensions": [], + } + + with self.client: + self.client.set_input_flow(self.input_flow(pages=2)) + resp = eos.sign_tx(self.client, ADDRESS_N, transaction, CHAIN_ID) + + assert isinstance(resp, EosSignedTx) + assert ( + resp.signature_r.hex() + == "6f511059a910d256ac20483bfedef2ada3b2d04f3261c97c0fce9455ca8b7ef4" + ) + assert ( + resp.signature_s.hex() + == "58d795deaf5c9b686e5bcaeabee801ad78e6675f051c24972d8c47abd33585f0" + ) + assert resp.signature_v == 32 + + def test_eos_signtx_unknown(self): + self.setup_mnemonic_nopin_nopassphrase() + transaction = { + "expiration": "2018-07-14T10:43:28", + "ref_block_num": 6439, + "ref_block_prefix": 2995713264, + "net_usage_words": 0, + "max_cpu_usage_ms": 0, + "delay_sec": 0, + "context_free_actions": [], + "actions": [ + { + "account": "foocontract", + "name": "baraction", + "authorization": [ + {"actor": "miniminimini", "permission": "active"} + ], + "data": "deadbeef", + } + ], + "transaction_extensions": [], + } + + with self.client: + self.client.set_input_flow(self.input_flow(pages=2)) + resp = eos.sign_tx(self.client, ADDRESS_N, transaction, CHAIN_ID) + + assert isinstance(resp, EosSignedTx) + assert ( + resp.signature_r.hex() + == "0bcc986299cf4eb1d5e5bc73620972b2b6683cd4230953a6f1725017927fd9ba" + ) + assert ( + resp.signature_s.hex() + == "488f7830e30eea5c7b4a96156bf7ffb0983c45a96211ca070b9db3bc6ba4db02" + ) + assert resp.signature_v == 31 + + def test_eos_signtx_newaccount(self): + self.setup_mnemonic_nopin_nopassphrase() + transaction = { + "expiration": "2018-07-14T10:43:28", + "ref_block_num": 6439, + "ref_block_prefix": 2995713264, + "net_usage_words": 0, + "max_cpu_usage_ms": 0, + "delay_sec": 0, + "context_free_actions": [], + "actions": [ + { + "account": "eosio", + "name": "newaccount", + "authorization": [ + {"actor": "miniminimini", "permission": "active"} + ], + "data": { + "creator": "miniminimini", + "name": "maximaximaxi", + "owner": { + "threshold": 1, + "keys": [ + { + "key": "EOS8Dkj827FpinZBGmhTM28B85H9eXiFH5XzvLoeukCJV5sKfLc6K", + "weight": 1, + } + ], + "accounts": [], + "waits": [], + }, + "active": { + "threshold": 1, + "keys": [ + { + "key": "EOS8Dkj827FpinZBGmhTM28B85H9eXiFH5XzvLoeukCJV5sKfLc6K", + "weight": 1, + } + ], + "accounts": [], + "waits": [], + }, + }, + }, + { + "account": "eosio", + "name": "buyrambytes", + "authorization": [ + {"actor": "miniminimini", "permission": "active"} + ], + "data": { + "payer": "miniminimini", + "receiver": "maximaximaxi", + "bytes": 4096, + }, + }, + { + "account": "eosio", + "name": "delegatebw", + "authorization": [ + {"actor": "miniminimini", "permission": "active"} + ], + "data": { + "sender": "miniminimini", + "receiver": "maximaximaxi", + "stake_net_quantity": "1.0000 EOS", + "stake_cpu_quantity": "1.0000 EOS", + "transfer": True, + }, + }, + ], + "transaction_extensions": [], + } + + def input_flow(): + # confirm number of actions + yield + self.client.debug.press_yes() + + # swipe through new account + yield + for _ in range(5): + self.client.debug.swipe_down() + time.sleep(1) + + # confirm new account + self.client.debug.press_yes() + + # swipe through buyrambytes + yield + self.client.debug.swipe_down() + time.sleep(1) + + # confirm buyrambytes + self.client.debug.press_yes() + + # swipe through delegatebw + yield + for _ in range(2): + self.client.debug.swipe_down() + time.sleep(1) + + # confirm delegatebw + self.client.debug.press_yes() + + with self.client: + self.client.set_input_flow(input_flow) + resp = eos.sign_tx(self.client, ADDRESS_N, transaction, CHAIN_ID) + + assert isinstance(resp, EosSignedTx) + assert ( + resp.signature_r.hex() + == "6346a807eef0257a34269b034df7470e134261833d0da5fe0bd91aedf5a47f86" + ) + assert ( + resp.signature_s.hex() + == "676a1fcd0d8faff63ec206c8596de9cb5d35037d05f337afdc22c7b9e0863e77" + ) + assert resp.signature_v == 32 + + def test_eos_signtx_setcontract(self): + self.setup_mnemonic_nopin_nopassphrase() + transaction = { + "expiration": "2018-06-19T13:29:53", + "ref_block_num": 30587, + "ref_block_prefix": 338239089, + "net_usage_words": 0, + "max_cpu_usage_ms": 0, + "delay_sec": 0, + "context_free_actions": [], + "actions": [ + { + "account": "eosio1", + "name": "setcode", + "authorization": [ + {"actor": "ednazztokens", "permission": "active"} + ], + "data": "00" * 1024, + }, + { + "account": "eosio1", + "name": "setabi", + "authorization": [ + {"actor": "ednazztokens", "permission": "active"} + ], + "data": "00" * 1024, + }, + ], + "transaction_extensions": [], + "context_free_data": [], + } + + def input_flow(): + # confirm number of actions + yield + self.client.debug.press_yes() + + # swipe through setcode + yield + self.client.debug.swipe_down() + time.sleep(1) + + # confirm setcode + self.client.debug.press_yes() + + # swipe through setabi + yield + self.client.debug.swipe_down() + time.sleep(1) + + # confirm setabi + self.client.debug.press_yes() + + with self.client: + self.client.set_input_flow(input_flow) + resp = eos.sign_tx(self.client, ADDRESS_N, transaction, CHAIN_ID) + + assert isinstance(resp, EosSignedTx) + assert ( + resp.signature_r.hex() + == "674bbe7c8c7b9abf03ab38851cb53411e794afff04737895962643b1ed94b7d1" + ) + assert ( + resp.signature_s.hex() + == "1e47559db68d435494e832a16cc08ae7a67b533013ab3407f7a89d5e28de98b7" + ) + assert resp.signature_v == 32