#!/usr/bin/env python3 # This file is part of the TREZOR project. # # Copyright (C) 2012-2017 Marek Palatinus # Copyright (C) 2012-2017 Pavol Rusnak # Copyright (C) 2016-2017 Jochen Hoenicke # Copyright (C) 2017 mruddy # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # 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 GNU Lesser General Public License # along with this library. If not, see . import base64 import binascii import click import hashlib import io import json import logging import os import sys from trezorlib.client import TrezorClient from trezorlib.transport import get_transport, enumerate_devices from trezorlib import coins from trezorlib import log from trezorlib import messages as proto from trezorlib import protobuf from trezorlib import stellar from trezorlib import tools from trezorlib import ripple class ChoiceType(click.Choice): def __init__(self, typemap): super(ChoiceType, self).__init__(typemap.keys()) self.typemap = typemap def convert(self, value, param, ctx): value = super(ChoiceType, self).convert(value, param, ctx) return self.typemap[value] CHOICE_RECOVERY_DEVICE_TYPE = ChoiceType({ 'scrambled': proto.RecoveryDeviceType.ScrambledWords, 'matrix': proto.RecoveryDeviceType.Matrix, }) CHOICE_INPUT_SCRIPT_TYPE = ChoiceType({ 'address': proto.InputScriptType.SPENDADDRESS, 'segwit': proto.InputScriptType.SPENDWITNESS, 'p2shsegwit': proto.InputScriptType.SPENDP2SHWITNESS, }) CHOICE_OUTPUT_SCRIPT_TYPE = ChoiceType({ 'address': proto.OutputScriptType.PAYTOADDRESS, 'segwit': proto.OutputScriptType.PAYTOWITNESS, 'p2shsegwit': proto.OutputScriptType.PAYTOP2SHWITNESS, }) def enable_logging(): log.enable_debug_output() log.OMITTED_MESSAGES.add(proto.Features) @click.group(context_settings={'max_content_width': 400}) @click.option('-p', '--path', help='Select device by specific path.', default=os.environ.get('TREZOR_PATH')) @click.option('-v', '--verbose', is_flag=True, help='Show communication messages.') @click.option('-j', '--json', 'is_json', is_flag=True, help='Print result as JSON object') @click.pass_context def cli(ctx, path, verbose, is_json): if verbose: enable_logging() def get_device(): try: device = get_transport(path, prefix_search=False) except: try: device = get_transport(path, prefix_search=True) except: click.echo("Failed to find a TREZOR device.") if path is not None: click.echo("Using path: {}".format(path)) sys.exit(1) return TrezorClient(transport=device) ctx.obj = get_device @cli.resultcallback() def print_result(res, path, verbose, is_json): if is_json: if isinstance(res, protobuf.MessageType): click.echo(json.dumps({res.__class__.__name__: res.__dict__})) else: click.echo(json.dumps(res, sort_keys=True, indent=4)) else: if isinstance(res, list): for line in res: click.echo(line) elif isinstance(res, dict): for k, v in res.items(): if isinstance(v, dict): for kk, vv in v.items(): click.echo('%s.%s: %s' % (k, kk, vv)) else: click.echo('%s: %s' % (k, v)) elif isinstance(res, protobuf.MessageType): click.echo(protobuf.format_message(res)) else: click.echo(res) # # Common functions # @cli.command(name='list', help='List connected TREZOR devices.') def ls(): return enumerate_devices() @cli.command(help='Show version of trezorctl/trezorlib.') def version(): from trezorlib import __version__ as VERSION return VERSION # # Basic device functions # @cli.command(help='Send ping message.') @click.argument('message') @click.option('-b', '--button-protection', is_flag=True) @click.option('-p', '--pin-protection', is_flag=True) @click.option('-r', '--passphrase-protection', is_flag=True) @click.pass_obj def ping(connect, message, button_protection, pin_protection, passphrase_protection): return connect().ping(message, button_protection=button_protection, pin_protection=pin_protection, passphrase_protection=passphrase_protection) @cli.command(help='Clear session (remove cached PIN, passphrase, etc.).') @click.pass_obj def clear_session(connect): return connect().clear_session() @cli.command(help='Get example entropy.') @click.argument('size', type=int) @click.pass_obj def get_entropy(connect, size): return binascii.hexlify(connect().get_entropy(size)) @cli.command(help='Retrieve device features and settings.') @click.pass_obj def get_features(connect): return connect().features # # Device management functions # @cli.command(help='Change new PIN or remove existing.') @click.option('-r', '--remove', is_flag=True) @click.pass_obj def change_pin(connect, remove): return connect().change_pin(remove) @cli.command(help='Enable passphrase.') @click.pass_obj def enable_passphrase(connect): return connect().apply_settings(use_passphrase=True) @cli.command(help='Disable passphrase.') @click.pass_obj def disable_passphrase(connect): return connect().apply_settings(use_passphrase=False) @cli.command(help='Set new device label.') @click.option('-l', '--label') @click.pass_obj def set_label(connect, label): return connect().apply_settings(label=label) @cli.command(help='Set passphrase source.') @click.argument('source', type=int) @click.pass_obj def set_passphrase_source(connect, source): return connect().apply_settings(passphrase_source=source) @cli.command(help='Set auto-lock delay (in seconds).') @click.argument('delay', type=str) @click.pass_obj def set_auto_lock_delay(connect, delay): value, unit = delay[:-1], delay[-1:] units = { 's': 1, 'm': 60, 'h': 3600, } if unit in units: seconds = float(value) * units[unit] else: seconds = float(delay) # assume seconds if no unit is specified return connect().apply_settings(auto_lock_delay_ms=int(seconds * 1000)) @cli.command(help='Set device flags.') @click.argument('flags') @click.pass_obj def set_flags(connect, flags): flags = flags.lower() if flags.startswith('0b'): flags = int(flags, 2) elif flags.startswith('0x'): flags = int(flags, 16) else: flags = int(flags) return connect().apply_flags(flags=flags) @cli.command(help='Set new homescreen.') @click.option('-f', '--filename', default=None) @click.pass_obj def set_homescreen(connect, filename): if filename is None: img = b'\x00' elif filename.endswith('.toif'): img = open(filename, 'rb').read() if img[:8] != b'TOIf\x90\x00\x90\x00': raise tools.CallException(proto.FailureType.DataError, 'File is not a TOIF file with size of 144x144') else: from PIL import Image im = Image.open(filename) if im.size != (128, 64): raise tools.CallException(proto.FailureType.DataError, 'Wrong size of the image') im = im.convert('1') pix = im.load() img = bytearray(1024) for j in range(64): for i in range(128): if pix[i, j]: o = (i + j * 128) img[o // 8] |= (1 << (7 - o % 8)) img = bytes(img) return connect().apply_settings(homescreen=img) @cli.command(help='Set U2F counter.') @click.argument('counter', type=int) @click.pass_obj def set_u2f_counter(connect, counter): return connect().set_u2f_counter(counter) @cli.command(help='Reset device to factory defaults and remove all private data.') @click.option('-b', '--bootloader', help='Wipe device in bootloader mode. This also erases the firmware.', is_flag=True) @click.pass_obj def wipe_device(connect, bootloader): client = connect() if bootloader: if not client.features.bootloader_mode: click.echo('Please switch your device to bootloader mode.') sys.exit(1) else: click.echo('Wiping user data and firmware! Please confirm the action on your device ...') else: if client.features.bootloader_mode: click.echo('Your device is in bootloader mode. This operation would also erase firmware.') click.echo('Specify "--bootloader" if that is what you want, or disconnect and reconnect device in normal mode.') click.echo('Aborting.') sys.exit(1) else: click.echo('Wiping user data! Please confirm the action on your device ...') try: return connect().wipe_device() except tools.CallException as e: click.echo('Action failed: {} {}'.format(*e.args)) sys.exit(3) @cli.command(help='Load custom configuration to the device.') @click.option('-m', '--mnemonic') @click.option('-e', '--expand', is_flag=True) @click.option('-x', '--xprv') @click.option('-p', '--pin', default='') @click.option('-r', '--passphrase-protection', is_flag=True) @click.option('-l', '--label', default='') @click.option('-i', '--ignore-checksum', is_flag=True) @click.option('-s', '--slip0014', is_flag=True) @click.pass_obj def load_device(connect, mnemonic, expand, xprv, pin, passphrase_protection, label, ignore_checksum, slip0014): if not mnemonic and not xprv and not slip0014: raise tools.CallException(proto.FailureType.DataError, 'Please provide mnemonic or xprv') client = connect() if mnemonic: return client.load_device_by_mnemonic( mnemonic, pin, passphrase_protection, label, 'english', ignore_checksum, expand ) if xprv: return client.load_device_by_xprv( xprv, pin, passphrase_protection, label, 'english' ) if slip0014: return client.load_device_by_mnemonic( ' '.join(['all'] * 12), pin, passphrase_protection, 'SLIP-0014' ) @cli.command(help='Start safe recovery workflow.') @click.option('-w', '--words', type=click.Choice(['12', '18', '24']), default='24') @click.option('-e', '--expand', is_flag=True) @click.option('-p', '--pin-protection', is_flag=True) @click.option('-r', '--passphrase-protection', is_flag=True) @click.option('-l', '--label') @click.option('-t', '--type', 'rec_type', type=CHOICE_RECOVERY_DEVICE_TYPE, default='scrambled') @click.option('-d', '--dry-run', is_flag=True) @click.pass_obj def recovery_device(connect, words, expand, pin_protection, passphrase_protection, label, rec_type, dry_run): return connect().recovery_device( int(words), passphrase_protection, pin_protection, label, 'english', rec_type, expand, dry_run ) @cli.command(help='Perform device setup and generate new seed.') @click.option('-e', '--entropy', is_flag=True) @click.option('-t', '--strength', type=click.Choice(['128', '192', '256']), default='256') @click.option('-r', '--passphrase-protection', is_flag=True) @click.option('-p', '--pin-protection', is_flag=True) @click.option('-l', '--label') @click.option('-u', '--u2f-counter', default=0) @click.option('-s', '--skip-backup', is_flag=True) @click.pass_obj def reset_device(connect, entropy, strength, passphrase_protection, pin_protection, label, u2f_counter, skip_backup): return connect().reset_device( entropy, int(strength), passphrase_protection, pin_protection, label, 'english', u2f_counter, skip_backup ) @cli.command(help='Perform device seed backup.') @click.pass_obj def backup_device(connect): return connect().backup_device() # # Firmware update # @cli.command(help='Upload new firmware to device (must be in bootloader mode).') @click.option('-f', '--filename') @click.option('-u', '--url') @click.option('-v', '--version') @click.option('-s', '--skip-check', is_flag=True) @click.option('--fingerprint', help='Expected firmware fingerprint in hex') @click.pass_obj def firmware_update(connect, filename, url, version, skip_check, fingerprint): if sum(bool(x) for x in (filename, url, version)) > 1: click.echo("You can use only one of: firmware, url, version.") sys.exit(1) client = connect() if not client.features.bootloader_mode: click.echo("Please switch your device to bootloader mode.") sys.exit(1) firmware_version = client.features.major_version if filename: fp = open(filename, 'rb').read() elif url: import requests click.echo('Downloading from', url) r = requests.get(url) fp = r.content else: import requests r = requests.get('https://wallet.trezor.io/data/firmware/{}/releases.json'.format(firmware_version)) releases = r.json() def version_func(r): return r['version'] def version_string(r): return '.'.join(map(str, version_func(r))) if version: try: release = next(r for r in releases if version_string(r) == version) except StopIteration: click.echo("Version {} not found.".format(version)) sys.exit(1) else: release = max(releases, key=version_func) click.echo('Fetching version: %s' % version_string(release)) if not fingerprint: fingerprint = release['fingerprint'] url = 'https://wallet.trezor.io/' + release['url'] click.echo('Downloading from %s' % url) r = requests.get(url) fp = r.content if not skip_check: if fp[:8] == b'54525a52' or fp[:8] == b'54525a56': fp = binascii.unhexlify(fp) if fp[:4] != b'TRZR' and fp[:4] != b'TRZV': click.echo("Trezor firmware header expected.") sys.exit(2) if fingerprint and firmware_version > 1: click.echo("Checking Trezor T fingerprint is not supported yet.") elif firmware_version == 1: calculated_fingerprint = hashlib.sha256(fp[256:]).hexdigest() click.echo("Firmware fingerprint: {}".format(calculated_fingerprint)) if fingerprint and fingerprint != calculated_fingerprint: click.echo("Expected fingerprint: {}".format(fingerprint)) click.echo("Fingerprints do not match, aborting.") sys.exit(5) click.echo('If asked, please confirm the action on your device ...') try: return client.firmware_update(fp=io.BytesIO(fp)) except tools.CallException as e: if e.args[0] in (proto.FailureType.FirmwareError, proto.FailureType.ActionCancelled): click.echo("Update aborted on device.") else: click.echo("Update failed: {} {}".format(*e.args)) sys.exit(3) @cli.command(help='Perform a self-test.') @click.pass_obj def self_test(connect): return connect().self_test() # # Basic coin functions # @cli.command(help='Get address for specified path.') @click.option('-c', '--coin', default='Bitcoin') @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0") @click.option('-t', '--script-type', type=CHOICE_INPUT_SCRIPT_TYPE, default='address') @click.option('-d', '--show-display', is_flag=True) @click.pass_obj def get_address(connect, coin, address, script_type, show_display): client = connect() address_n = tools.parse_path(address) return client.get_address(coin, address_n, show_display, script_type=script_type) @cli.command(help='Get public node of given path.') @click.option('-c', '--coin', default='Bitcoin') @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/0'/0'") @click.option('-e', '--curve') @click.option('-d', '--show-display', is_flag=True) @click.pass_obj def get_public_node(connect, coin, address, curve, show_display): client = connect() address_n = tools.parse_path(address) result = client.get_public_node(address_n, ecdsa_curve_name=curve, show_display=show_display, coin_name=coin) return { 'node': { 'depth': result.node.depth, 'fingerprint': "%08x" % result.node.fingerprint, 'child_num': result.node.child_num, 'chain_code': binascii.hexlify(result.node.chain_code), 'public_key': binascii.hexlify(result.node.public_key), }, 'xpub': result.xpub } # # Signing options # @cli.command(help='Sign transaction.') @click.option('-c', '--coin', default='Bitcoin') # @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0") # @click.option('-t', '--script-type', type=CHOICE_INPUT_SCRIPT_TYPE, default='address') # @click.option('-o', '--output', required=True, help='Transaction output') # @click.option('-f', '--fee', required=True, help='Transaction fee (sat/B)') @click.pass_obj def sign_tx(connect, coin): client = connect() if coin in coins.tx_api: txapi = coins.tx_api[coin] else: click.echo('Coin "%s" is not recognized.' % coin, err=True) click.echo('Supported coin types: %s' % ', '.join(coins.tx_api.keys()), err=True) sys.exit(1) client.set_tx_api(txapi) def default_script_type(address_n): script_type = 'address' if address_n is None: pass elif address_n[0] == tools.H_(49): script_type = 'p2shsegwit' return script_type def outpoint(s): txid, vout = s.split(':') return binascii.unhexlify(txid), int(vout) inputs = [] while True: click.echo() prev = click.prompt('Previous output to spend (txid:vout)', type=outpoint, default='') if not prev: break prev_hash, prev_index = prev address_n = click.prompt('BIP-32 path to derive the key', type=tools.parse_path) amount = click.prompt('Input amount (satoshis)', type=int, default=0) sequence = click.prompt('Sequence Number to use (RBF opt-in enabled by default)', type=int, default=0xfffffffd) script_type = click.prompt('Input type', type=CHOICE_INPUT_SCRIPT_TYPE, default=default_script_type(address_n)) script_type = script_type if isinstance(script_type, int) else CHOICE_INPUT_SCRIPT_TYPE.typemap[script_type] if txapi.bip115: prev_output = txapi.get_tx(binascii.hexlify(prev_hash).decode("utf-8")).bin_outputs[prev_index] prev_blockhash = prev_output.block_hash prev_blockheight = prev_output.block_height inputs.append(proto.TxInputType( address_n=address_n, prev_hash=prev_hash, prev_index=prev_index, amount=amount, script_type=script_type, sequence=sequence, prev_block_hash_bip115=prev_blockhash, prev_block_height_bip115=prev_blockheight, )) if txapi.bip115: current_block_height = txapi.current_height() block_height = current_block_height - 300 # Zencash recommendation for the better protection block_hash = txapi.get_block_hash(block_height) outputs = [] while True: click.echo() address = click.prompt('Output address (for non-change output)', default='') if address: address_n = None else: address = None address_n = click.prompt('BIP-32 path (for change output)', type=tools.parse_path, default='') if not address_n: break amount = click.prompt('Amount to spend (satoshis)', type=int) script_type = click.prompt('Output type', type=CHOICE_OUTPUT_SCRIPT_TYPE, default=default_script_type(address_n)) script_type = script_type if isinstance(script_type, int) else CHOICE_OUTPUT_SCRIPT_TYPE.typemap[script_type] outputs.append(proto.TxOutputType( address_n=address_n, address=address, amount=amount, script_type=script_type, block_hash_bip115=block_hash[::-1], # Blockhash passed in reverse order block_height_bip115=block_height )) tx_version = click.prompt('Transaction version', type=int, default=2) tx_locktime = click.prompt('Transaction locktime', type=int, default=0) _, serialized_tx = client.sign_tx(coin, inputs, outputs, tx_version, tx_locktime) client.close() click.echo() click.echo('Signed Transaction:') click.echo(binascii.hexlify(serialized_tx)) click.echo() click.echo('Use the following form to broadcast it to the network:') click.echo(txapi.pushtx_url) # # Message functions # @cli.command(help='Sign message using address of given path.') @click.option('-c', '--coin', default='Bitcoin') @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0") @click.option('-t', '--script-type', type=click.Choice(['address', 'segwit', 'p2shsegwit']), default='address') @click.argument('message') @click.pass_obj def sign_message(connect, coin, address, message, script_type): client = connect() address_n = tools.parse_path(address) typemap = { 'address': proto.InputScriptType.SPENDADDRESS, 'segwit': proto.InputScriptType.SPENDWITNESS, 'p2shsegwit': proto.InputScriptType.SPENDP2SHWITNESS, } script_type = typemap[script_type] res = client.sign_message(coin, address_n, message, script_type) return { 'message': message, 'address': res.address, 'signature': base64.b64encode(res.signature) } @cli.command(help='Verify message.') @click.option('-c', '--coin', default='Bitcoin') @click.argument('address') @click.argument('signature') @click.argument('message') @click.pass_obj def verify_message(connect, coin, address, signature, message): signature = base64.b64decode(signature) return connect().verify_message(coin, address, signature, message) @cli.command(help='Sign message with Ethereum address.') @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/60'/0'/0/0") @click.argument('message') @click.pass_obj def ethereum_sign_message(connect, address, message): client = connect() address_n = tools.parse_path(address) ret = client.ethereum_sign_message(address_n, message) output = { 'message': message, 'address': '0x%s' % binascii.hexlify(ret.address).decode(), 'signature': '0x%s' % binascii.hexlify(ret.signature).decode() } return output def ethereum_decode_hex(value): if value.startswith('0x') or value.startswith('0X'): return binascii.unhexlify(value[2:]) else: return binascii.unhexlify(value) @cli.command(help='Verify message signed with Ethereum address.') @click.argument('address') @click.argument('signature') @click.argument('message') @click.pass_obj def ethereum_verify_message(connect, address, signature, message): address = ethereum_decode_hex(address) signature = ethereum_decode_hex(signature) return connect().ethereum_verify_message(address, signature, message) @cli.command(help='Encrypt value by given key and path.') @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/10016'/0") @click.argument('key') @click.argument('value') @click.pass_obj def encrypt_keyvalue(connect, address, key, value): client = connect() address_n = tools.parse_path(address) res = client.encrypt_keyvalue(address_n, key, value.encode()) return binascii.hexlify(res) @cli.command(help='Decrypt value by given key and path.') @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/10016'/0") @click.argument('key') @click.argument('value') @click.pass_obj def decrypt_keyvalue(connect, address, key, value): client = connect() address_n = tools.parse_path(address) return client.decrypt_keyvalue(address_n, key, binascii.unhexlify(value)) # @cli.command(help='Encrypt message.') # @click.option('-c', '--coin', default='Bitcoin') # @click.option('-d', '--display-only', is_flag=True) # @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0") # @click.argument('pubkey') # @click.argument('message') # @click.pass_obj # def encrypt_message(connect, coin, display_only, address, pubkey, message): # client = connect() # pubkey = binascii.unhexlify(pubkey) # address_n = tools.parse_path(address) # res = client.encrypt_message(pubkey, message, display_only, coin, address_n) # return { # 'nonce': binascii.hexlify(res.nonce), # 'message': binascii.hexlify(res.message), # 'hmac': binascii.hexlify(res.hmac), # 'payload': base64.b64encode(res.nonce + res.message + res.hmac), # } # @cli.command(help='Decrypt message.') # @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0") # @click.argument('payload') # @click.pass_obj # def decrypt_message(connect, address, payload): # client = connect() # address_n = tools.parse_path(address) # payload = base64.b64decode(payload) # nonce, message, msg_hmac = payload[:33], payload[33:-8], payload[-8:] # return client.decrypt_message(address_n, nonce, message, msg_hmac) # # Ethereum functions # @cli.command(help='Get Ethereum address in hex encoding.') @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/60'/0'/0/0") @click.option('-d', '--show-display', is_flag=True) @click.pass_obj def ethereum_get_address(connect, address, show_display): client = connect() address_n = tools.parse_path(address) address = client.ethereum_get_address(address_n, show_display) return '0x%s' % binascii.hexlify(address).decode() @cli.command(help='Sign (and optionally publish) Ethereum transaction. Use TO as destination address or set TO to "" for contract creation.') @click.option('-a', '--host', default='localhost:8545', help='RPC port of ethereum node for automatic gas/nonce estimation and publishing') @click.option('-c', '--chain-id', type=int, help='EIP-155 chain id (replay protection)') @click.option('-n', '--address', required=True, help="BIP-32 path to source address, e.g., m/44'/60'/0'/0/0") @click.option('-v', '--value', default='0', help='Ether amount to transfer, e.g. "100 milliether"') @click.option('-g', '--gas-limit', type=int, help='Gas limit - Required for offline signing') @click.option('-t', '--gas-price', help='Gas price, e.g. "20 nanoether" - Required for offline signing') @click.option('-i', '--nonce', type=int, help='Transaction counter - Required for offline signing') @click.option('-d', '--data', default='', help='Data as hex string, e.g. 0x12345678') @click.option('-p', '--publish', is_flag=True, help='Publish transaction via RPC') @click.option('-x', '--tx-type', type=int, help='TX type (used only for Wanchain)') @click.argument('to') @click.pass_obj def ethereum_sign_tx(connect, host, chain_id, address, value, gas_limit, gas_price, nonce, data, publish, to, tx_type): from ethjsonrpc import EthJsonRpc import rlp ether_units = { 'wei': 1, 'kwei': 1000, 'babbage': 1000, 'femtoether': 1000, 'mwei': 1000000, 'lovelace': 1000000, 'picoether': 1000000, 'gwei': 1000000000, 'shannon': 1000000000, 'nanoether': 1000000000, 'nano': 1000000000, 'szabo': 1000000000000, 'microether': 1000000000000, 'micro': 1000000000000, 'finney': 1000000000000000, 'milliether': 1000000000000000, 'milli': 1000000000000000, 'ether': 1000000000000000000, 'eth': 1000000000000000000, } if ' ' in value: value, unit = value.split(' ', 1) if unit.lower() not in ether_units: raise tools.CallException(proto.Failure.DataError, 'Unrecognized ether unit %r' % unit) value = int(value) * ether_units[unit.lower()] else: value = int(value) if gas_price is not None: if ' ' in gas_price: gas_price, unit = gas_price.split(' ', 1) if unit.lower() not in ether_units: raise tools.CallException(proto.Failure.DataError, 'Unrecognized gas price unit %r' % unit) gas_price = int(gas_price) * ether_units[unit.lower()] else: gas_price = int(gas_price) if gas_limit is not None: gas_limit = int(gas_limit) to_address = ethereum_decode_hex(to) client = connect() address_n = tools.parse_path(address) address = '0x%s' % (binascii.hexlify(client.ethereum_get_address(address_n)).decode()) if gas_price is None or gas_limit is None or nonce is None or publish: host, port = host.split(':') eth = EthJsonRpc(host, int(port)) if not data: data = '' data = ethereum_decode_hex(data) if gas_price is None: gas_price = eth.eth_gasPrice() if gas_limit is None: gas_limit = eth.eth_estimateGas( to_address=to, from_address=address, value=('0x%x' % value), data='0x%s' % (binascii.hexlify(data).decode())) if nonce is None: nonce = eth.eth_getTransactionCount(address) sig = client.ethereum_sign_tx( n=address_n, tx_type=tx_type, nonce=nonce, gas_price=gas_price, gas_limit=gas_limit, to=to_address, value=value, data=data, chain_id=chain_id) if tx_type is None: transaction = rlp.encode( (nonce, gas_price, gas_limit, to_address, value, data) + sig) else: transaction = rlp.encode( (tx_type, nonce, gas_price, gas_limit, to_address, value, data) + sig) tx_hex = '0x%s' % binascii.hexlify(transaction).decode() if publish: tx_hash = eth.eth_sendRawTransaction(tx_hex) return 'Transaction published with ID: %s' % tx_hash else: return 'Signed raw transaction: %s' % tx_hex # # NEM functions # @cli.command(help='Get NEM address for specified path.') @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/0'/43'/0/0") @click.option('-N', '--network', type=int, default=0x68) @click.option('-d', '--show-display', is_flag=True) @click.pass_obj def nem_get_address(connect, address, network, show_display): client = connect() address_n = tools.parse_path(address) return client.nem_get_address(address_n, network, show_display) @cli.command(help='Sign (and optionally broadcast) NEM transaction.') @click.option('-n', '--address', help='BIP-32 path to signing key') @click.option('-f', '--file', type=click.File('r'), default='-', help='Transaction in NIS (RequestPrepareAnnounce) format') @click.option('-b', '--broadcast', help='NIS to announce transaction to') @click.pass_obj def nem_sign_tx(connect, address, file, broadcast): client = connect() address_n = tools.parse_path(address) transaction = client.nem_sign_tx(address_n, json.load(file)) payload = { "data": binascii.hexlify(transaction.data).decode(), "signature": binascii.hexlify(transaction.signature).decode() } if broadcast: import requests return requests.post("{}/transaction/announce".format(broadcast), json=payload).json() else: return payload # # Lisk functions # @cli.command(help='Get Lisk address for specified path.') @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/134'/0'/0'") @click.option('-d', '--show-display', is_flag=True) @click.pass_obj def lisk_get_address(connect, address, show_display): client = connect() address_n = tools.parse_path(address) return client.lisk_get_address(address_n, show_display) @cli.command(help='Get Lisk public key for specified path.') @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/134'/0'/0'") @click.option('-d', '--show-display', is_flag=True) @click.pass_obj def lisk_get_public_key(connect, address, show_display): client = connect() address_n = tools.parse_path(address) res = client.lisk_get_public_key(address_n, show_display) output = { "public_key": binascii.hexlify(res.public_key).decode() } return output @cli.command(help='Sign Lisk transaction.') @click.option('-n', '--address', required=True, help="BIP-32 path to signing key, e.g. m/44'/134'/0'/0'") @click.option('-f', '--file', type=click.File('r'), default='-', help='Transaction in JSON format') # @click.option('-b', '--broadcast', help='Broadcast Lisk transaction') @click.pass_obj def lisk_sign_tx(connect, address, file): client = connect() address_n = tools.parse_path(address) transaction = client.lisk_sign_tx(address_n, json.load(file)) payload = { "signature": binascii.hexlify(transaction.signature).decode() } return payload @cli.command(help='Sign message with Lisk address.') @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/134'/0'/0'") @click.argument('message') @click.pass_obj def lisk_sign_message(connect, address, message): client = connect() address_n = client.expand_path(address) res = client.lisk_sign_message(address_n, message) output = { "message": message, "public_key": binascii.hexlify(res.public_key).decode(), "signature": binascii.hexlify(res.signature).decode() } return output @cli.command(help='Verify message signed with Lisk address.') @click.argument('pubkey') @click.argument('signature') @click.argument('message') @click.pass_obj def lisk_verify_message(connect, pubkey, signature, message): signature = bytes.fromhex(signature) pubkey = bytes.fromhex(pubkey) return connect().lisk_verify_message(pubkey, signature, message) # # CoSi functions # @cli.command(help='Ask device to commit to CoSi signing.') @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0") @click.argument('data') @click.pass_obj def cosi_commit(connect, address, data): client = connect() address_n = tools.parse_path(address) return client.cosi_commit(address_n, binascii.unhexlify(data)) @cli.command(help='Ask device to sign using CoSi.') @click.option('-n', '--address', required=True, help="BIP-32 path, e.g. m/44'/0'/0'/0/0") @click.argument('data') @click.argument('global_commitment') @click.argument('global_pubkey') @click.pass_obj def cosi_sign(connect, address, data, global_commitment, global_pubkey): client = connect() address_n = tools.parse_path(address) return client.cosi_sign(address_n, binascii.unhexlify(data), binascii.unhexlify(global_commitment), binascii.unhexlify(global_pubkey)) # # Stellar functions # @cli.command(help='Get Stellar public address') @click.option('-n', '--address', required=False, help="BIP32 path. Always use hardened paths and the m/44'/148'/ prefix", default=stellar.DEFAULT_BIP32_PATH) @click.option('-d', '--show-display', is_flag=True) @click.pass_obj def stellar_get_address(connect, address, show_display): client = connect() address_n = tools.parse_path(address) return client.stellar_get_address(address_n, show_display) @cli.command(help='Get Stellar public key') @click.option('-n', '--address', required=False, help="BIP32 path. Always use hardened paths and the m/44'/148'/ prefix", default=stellar.DEFAULT_BIP32_PATH) @click.option('-d', '--show-display', is_flag=True) @click.pass_obj def stellar_get_public_key(connect, address, show_display): client = connect() address_n = tools.parse_path(address) return binascii.hexlify(client.stellar_get_public_key(address_n, show_display)) @cli.command(help='Sign a base64-encoded transaction envelope') @click.option('-n', '--address', required=False, help="BIP32 path. Always use hardened paths and the m/44'/148'/ prefix", default=stellar.DEFAULT_BIP32_PATH) @click.option('-n', '--network-passphrase', default=stellar.DEFAULT_NETWORK_PASSPHRASE, required=False, help="Network passphrase (blank for public network). Testnet is: 'Test SDF Network ; September 2015'") @click.argument('b64envelope') @click.pass_obj def stellar_sign_transaction(connect, b64envelope, address, network_passphrase): client = connect() address_n = tools.parse_path(address) tx, operations = stellar.parse_transaction_bytes(base64.b64decode(b64envelope)) resp = client.stellar_sign_transaction(tx, operations, address_n, network_passphrase) return base64.b64encode(resp.signature) # # Ripple functions # @cli.command(help='Get Ripple address') @click.option('-n', '--address', required=True, help="BIP-32 path to key, e.g. m/44'/144'/0'/0/0") @click.option('-d', '--show-display', is_flag=True) @click.pass_obj def ripple_get_address(connect, address, show_display): client = connect() address_n = tools.parse_path(address) return ripple.get_address(client, address_n, show_display) @cli.command(help='Sign Ripple transaction') @click.option('-n', '--address', required=True, help="BIP-32 path to key, e.g. m/44'/144'/0'/0/0") @click.option('-f', '--file', type=click.File('r'), default='-', help='Transaction in JSON format') @click.pass_obj def ripple_sign_tx(connect, address, file): client = connect() address_n = tools.parse_path(address) msg = ripple.create_sign_tx_msg(json.load(file)) result = ripple.sign_tx(client, address_n, msg) click.echo("Signature:") click.echo(binascii.hexlify(result.signature)) click.echo() click.echo("Serialized tx including the signature:") click.echo(binascii.hexlify(result.serialized_tx)) # # Main # if __name__ == '__main__': cli() # pylint: disable=E1120