1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-22 22:38:08 +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

214
trezorctl
View File

@ -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
#