From 3997b402b4ff2f56bdaaf7b17680236421298a5b Mon Sep 17 00:00:00 2001 From: matejcik Date: Thu, 7 Mar 2019 17:46:12 +0100 Subject: [PATCH] trezorctl: update ethereum-sign-tx to use web3 instead of ethjsonrpc fixes #366 --- trezorctl | 214 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 130 insertions(+), 84 deletions(-) diff --git a/trezorctl b/trezorctl index c3271df7fc..3fd352d309 100755 --- a/trezorctl +++ b/trezorctl @@ -23,7 +23,9 @@ import base64 import json import os +import re import sys +from decimal import Decimal import click import requests @@ -55,6 +57,14 @@ from trezorlib import ( from trezorlib.client import TrezorClient from trezorlib.transport import enumerate_devices, get_transport +try: + import rlp + import web3 + + ETHEREUM_SIGN_TX = True +except Exception: + ETHEREUM_SIGN_TX = False + class ChoiceType(click.Choice): def __init__(self, typemap): @@ -1158,15 +1168,59 @@ def ethereum_get_public_node(connect, address, show_display): } -@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", -) +# fmt: off +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, +} +# fmt: on + + +def ethereum_amount_to_int(ctx, param, value): + if value is None: + return None + if value.isdigit(): + return int(value) + try: + number, unit = re.match(r"^(\d+(?:.\d+)?)([a-z]+)", value).groups() + scale = ETHER_UNITS[unit] + decoded_number = Decimal(number) + return int(decoded_number * scale) + + except Exception: + import traceback + + traceback.print_exc() + raise click.BadParameter("Amount not understood") + + +def ethereum_list_units(ctx, param, value): + if not value or ctx.resilient_parsing: + return + maxlen = max(len(k) for k in ETHER_UNITS.keys()) + 1 + for unit, scale in ETHER_UNITS.items(): + click.echo("{:{maxlen}}: {}".format(unit, scale, maxlen=maxlen)) + ctx.exit() + + +@cli.command() @click.option("-c", "--chain-id", type=int, help="EIP-155 chain id (replay protection)") @click.option( "-n", @@ -1175,30 +1229,36 @@ def ethereum_get_public_node(connect, address, show_display): 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" + "-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', + help="Gas price (required for offline signing)", + callback=ethereum_amount_to_int, ) @click.option( - "-i", "--nonce", type=int, help="Transaction counter - Required for offline signing" + "-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.option( + "--list-units", + is_flag=True, + help="List known currency units and exit.", + is_eager=True, + callback=ethereum_list_units, + expose_value=False, +) @click.argument("to") +@click.argument("amount", callback=ethereum_amount_to_int) @click.pass_obj def ethereum_sign_tx( connect, - host, chain_id, address, - value, + amount, gas_limit, gas_price, nonce, @@ -1207,84 +1267,70 @@ def ethereum_sign_tx( to, tx_type, ): - from ethjsonrpc import EthJsonRpc - import rlp + """Sign (and optionally publish) Ethereum transaction. - # fmt: off - 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, - } - # fmt: on + Use TO as destination address, or set TO to "" for contract creation. - 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 + You can specify AMOUNT and gas price either as a number of wei, + or you can use a unit suffix. + E.g., the following are equivalent: + + \b + 0.000314eth + 0.314milliether + 314000000nano + 314000000000000wei + 314000000000000 + + Use the --list-units option to show all known currency units. + + If any of gas price, gas limit and nonce is not specified, this command will + try to connect to an ethereum node and auto-fill these values. You can configure + the connection with WEB3_PROVIDER_URI environment variable. + """ + if not ETHEREUM_SIGN_TX: + click.echo("Ethereum requirements not installed.") + click.echo("Please run:") + click.echo() + click.echo(" pip install web3 rlp") + sys.exit(1) + + if gas_price is None or gas_limit is None or nonce is None or publish: + w3 = web3.Web3() + if not w3.isConnected(): + click.echo("Failed to connect to Ethereum node.") + click.echo( + "If you want to sign offline, make sure you provide --gas-price, " + "--gas-limit and --nonce arguments" ) - 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) + sys.exit(1) to_address = ethereum_decode_hex(to) client = connect() address_n = tools.parse_path(address) - address = ethereum.get_address(client, address_n) + from_address = ethereum.get_address(client, address_n) - 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 data: + data = ethereum_decode_hex(data) + else: + data = b"" if gas_price is None: - gas_price = eth.eth_gasPrice() + gas_price = w3.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" % data.hex(), + gas_limit = w3.eth.estimateGas( + { + "to": to_address, + "from": from_address, + "value": amount, + "data": "0x%s" % data.hex(), + } ) if nonce is None: - nonce = eth.eth_getTransactionCount(address) + nonce = w3.eth.getTransactionCount(from_address) sig = ethereum.sign_tx( client, @@ -1294,26 +1340,26 @@ def ethereum_sign_tx( gas_price=gas_price, gas_limit=gas_limit, to=to, - value=value, + value=amount, data=data, chain_id=chain_id, ) if tx_type is None: transaction = rlp.encode( - (nonce, gas_price, gas_limit, to_address, value, data) + sig + (nonce, gas_price, gas_limit, to_address, amount, data) + sig ) else: transaction = rlp.encode( - (tx_type, nonce, gas_price, gas_limit, to_address, value, data) + sig + (tx_type, nonce, gas_price, gas_limit, to_address, amount, data) + sig ) tx_hex = "0x%s" % transaction.hex() if publish: - tx_hash = eth.eth_sendRawTransaction(tx_hex) + tx_hash = w3.eth.sendRawTransaction(tx_hex).hex() return "Transaction published with ID: %s" % tx_hash else: - return "Signed raw transaction: %s" % tx_hex + return "Signed raw transaction:\n%s" % tx_hex #