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:
parent
c24a8770c5
commit
3997b402b4
214
trezorctl
214
trezorctl
@ -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,15 +1168,59 @@ 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 = {
|
||||||
)
|
'wei': 1,
|
||||||
@click.option(
|
'kwei': 1000,
|
||||||
"-a",
|
'babbage': 1000,
|
||||||
"--host",
|
'femtoether': 1000,
|
||||||
default="localhost:8545",
|
'mwei': 1000000,
|
||||||
help="RPC port of ethereum node for automatic gas/nonce estimation and publishing",
|
'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("-c", "--chain-id", type=int, help="EIP-155 chain id (replay protection)")
|
||||||
@click.option(
|
@click.option(
|
||||||
"-n",
|
"-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",
|
help="BIP-32 path to source address, e.g., m/44'/60'/0'/0/0",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"-v", "--value", default="0", help='Ether amount to transfer, e.g. "100 milliether"'
|
"-g", "--gas-limit", type=int, help="Gas limit (required for offline signing)"
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"-g", "--gas-limit", type=int, help="Gas limit - Required for offline signing"
|
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"-t",
|
"-t",
|
||||||
"--gas-price",
|
"--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(
|
@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("-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("-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("-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("to")
|
||||||
|
@click.argument("amount", callback=ethereum_amount_to_int)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def ethereum_sign_tx(
|
def ethereum_sign_tx(
|
||||||
connect,
|
connect,
|
||||||
host,
|
|
||||||
chain_id,
|
chain_id,
|
||||||
address,
|
address,
|
||||||
value,
|
amount,
|
||||||
gas_limit,
|
gas_limit,
|
||||||
gas_price,
|
gas_price,
|
||||||
nonce,
|
nonce,
|
||||||
@ -1207,84 +1267,70 @@ def ethereum_sign_tx(
|
|||||||
to,
|
to,
|
||||||
tx_type,
|
tx_type,
|
||||||
):
|
):
|
||||||
from ethjsonrpc import EthJsonRpc
|
"""Sign (and optionally publish) Ethereum transaction.
|
||||||
import rlp
|
|
||||||
|
|
||||||
# fmt: off
|
Use TO as destination address, or set TO to "" for contract creation.
|
||||||
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
|
|
||||||
|
|
||||||
if " " in value:
|
You can specify AMOUNT and gas price either as a number of wei,
|
||||||
value, unit = value.split(" ", 1)
|
or you can use a unit suffix.
|
||||||
if unit.lower() not in ether_units:
|
E.g., the following are equivalent:
|
||||||
raise tools.CallException(
|
|
||||||
proto.Failure.DataError, "Unrecognized ether unit %r" % unit
|
\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(":")
|
data = ethereum_decode_hex(data)
|
||||||
eth = EthJsonRpc(host, int(port))
|
else:
|
||||||
|
data = b""
|
||||||
if not data:
|
|
||||||
data = ""
|
|
||||||
data = ethereum_decode_hex(data)
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
Loading…
Reference in New Issue
Block a user