1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-23 06:48:16 +00:00

trezorctl: update ethereum-sign-tx to use web3 instead of ethjsonrpc

fixes #366
This commit is contained in:
matejcik 2019-03-07 17:46:12 +01:00
parent c24a8770c5
commit 3997b402b4

240
trezorctl
View File

@ -23,7 +23,9 @@
import base64 import base64
import json import json
import os import os
import re
import sys import sys
from decimal import Decimal
import click import click
import requests import requests
@ -55,6 +57,14 @@ from trezorlib import (
from trezorlib.client import TrezorClient from trezorlib.client import TrezorClient
from trezorlib.transport import enumerate_devices, get_transport 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): class ChoiceType(click.Choice):
def __init__(self, typemap): def __init__(self, typemap):
@ -1158,60 +1168,8 @@ def ethereum_get_public_node(connect, address, show_display):
} }
@cli.command( # fmt: off
help='Sign (and optionally publish) Ethereum transaction. Use TO as destination address or set TO to "" for contract creation.' ETHER_UNITS = {
)
@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
# fmt: off
ether_units = {
'wei': 1, 'wei': 1,
'kwei': 1000, 'kwei': 1000,
'babbage': 1000, 'babbage': 1000,
@ -1231,60 +1189,148 @@ def ethereum_sign_tx(
'milli': 1000000000000000, 'milli': 1000000000000000,
'ether': 1000000000000000000, 'ether': 1000000000000000000,
'eth': 1000000000000000000, 'eth': 1000000000000000000,
} }
# fmt: on # fmt: on
if " " in value:
value, unit = value.split(" ", 1) def ethereum_amount_to_int(ctx, param, value):
if unit.lower() not in ether_units: if value is None:
raise tools.CallException( return None
proto.Failure.DataError, "Unrecognized ether unit %r" % unit 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",
"--address",
required=True,
help="BIP-32 path to source address, e.g., m/44'/60'/0'/0/0",
)
@click.option(
"-g", "--gas-limit", type=int, help="Gas limit (required for offline signing)"
)
@click.option(
"-t",
"--gas-price",
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)"
)
@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,
chain_id,
address,
amount,
gas_limit,
gas_price,
nonce,
data,
publish,
to,
tx_type,
):
"""Sign (and optionally publish) Ethereum transaction.
Use TO as destination address, or set TO to "" for contract creation.
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()] sys.exit(1)
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) to_address = ethereum_decode_hex(to)
client = connect() client = connect()
address_n = tools.parse_path(address) 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: if data:
host, port = host.split(":")
eth = EthJsonRpc(host, int(port))
if not data:
data = ""
data = ethereum_decode_hex(data) data = ethereum_decode_hex(data)
else:
data = b""
if gas_price is None: if gas_price is None:
gas_price = eth.eth_gasPrice() gas_price = w3.eth.gasPrice
if gas_limit is None: if gas_limit is None:
gas_limit = eth.eth_estimateGas( gas_limit = w3.eth.estimateGas(
to_address=to, {
from_address=address, "to": to_address,
value=("0x%x" % value), "from": from_address,
data="0x%s" % data.hex(), "value": amount,
"data": "0x%s" % data.hex(),
}
) )
if nonce is None: if nonce is None:
nonce = eth.eth_getTransactionCount(address) nonce = w3.eth.getTransactionCount(from_address)
sig = ethereum.sign_tx( sig = ethereum.sign_tx(
client, client,
@ -1294,26 +1340,26 @@ def ethereum_sign_tx(
gas_price=gas_price, gas_price=gas_price,
gas_limit=gas_limit, gas_limit=gas_limit,
to=to, to=to,
value=value, value=amount,
data=data, data=data,
chain_id=chain_id, chain_id=chain_id,
) )
if tx_type is None: if tx_type is None:
transaction = rlp.encode( transaction = rlp.encode(
(nonce, gas_price, gas_limit, to_address, value, data) + sig (nonce, gas_price, gas_limit, to_address, amount, data) + sig
) )
else: else:
transaction = rlp.encode( 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() tx_hex = "0x%s" % transaction.hex()
if publish: 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 return "Transaction published with ID: %s" % tx_hash
else: else:
return "Signed raw transaction: %s" % tx_hex return "Signed raw transaction:\n%s" % tx_hex
# #