mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-25 00:48:19 +00:00
c82fad68e7
[no changelog]
218 lines
6.7 KiB
Python
Executable File
218 lines
6.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# This file is part of the Trezor project.
|
|
#
|
|
# Copyright (C) 2012-2022 SatoshiLabs and contributors
|
|
#
|
|
# This library is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Lesser General Public License version 3
|
|
# as published by the Free Software Foundation.
|
|
#
|
|
# 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 License along with this library.
|
|
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
|
|
|
|
import decimal
|
|
import json
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
import click
|
|
import requests
|
|
|
|
from trezorlib import btc, messages, tools
|
|
from trezorlib.cli import ChoiceType
|
|
from trezorlib.cli.btc import INPUT_SCRIPTS, OUTPUT_SCRIPTS
|
|
from trezorlib.protobuf import to_dict
|
|
|
|
SESSION = requests.Session()
|
|
SESSION.headers.update({"User-Agent": "trezorlib"})
|
|
|
|
# the following script type mapping is only valid for single-sig Trezor-generated utxos
|
|
BITCOIN_CORE_INPUT_TYPES = {
|
|
"pubkeyhash": messages.InputScriptType.SPENDADDRESS,
|
|
"scripthash": messages.InputScriptType.SPENDP2SHWITNESS,
|
|
"witness_v0_keyhash": messages.InputScriptType.SPENDWITNESS,
|
|
"witness_v1_taproot": messages.InputScriptType.SPENDTAPROOT,
|
|
}
|
|
|
|
|
|
def echo(*args: Any, **kwargs: Any):
|
|
return click.echo(*args, err=True, **kwargs)
|
|
|
|
|
|
def prompt(*args: Any, **kwargs: Any):
|
|
return click.prompt(*args, err=True, **kwargs)
|
|
|
|
|
|
def _default_script_type(address_n: Optional[List[int]], script_types: Any) -> str:
|
|
script_type = "address"
|
|
|
|
if address_n is None:
|
|
pass
|
|
elif address_n[0] == tools.H_(49):
|
|
script_type = "p2shsegwit"
|
|
elif address_n[0] == tools.H_(84):
|
|
script_type = "segwit"
|
|
|
|
return script_type
|
|
# return script_types[script_type]
|
|
|
|
|
|
def parse_vin(s: str) -> Tuple[bytes, int]:
|
|
txid, vout = s.split(":")
|
|
return bytes.fromhex(txid), int(vout)
|
|
|
|
|
|
def _get_inputs_interactive(
|
|
blockbook_url: str,
|
|
) -> Tuple[List[messages.TxInputType], Dict[str, messages.TransactionType]]:
|
|
inputs: List[messages.TxInputType] = []
|
|
txes: Dict[str, messages.TransactionType] = {}
|
|
while True:
|
|
echo()
|
|
prev = prompt(
|
|
"Previous output to spend (txid:vout / empty string to skip)", default=""
|
|
)
|
|
if not prev:
|
|
break
|
|
try:
|
|
prev_hash, prev_index = parse_vin(prev)
|
|
except Exception:
|
|
continue
|
|
|
|
txhash = prev_hash.hex()
|
|
tx_url = blockbook_url + txhash
|
|
r = SESSION.get(tx_url)
|
|
if not r.ok:
|
|
raise click.ClickException(f"Failed to fetch URL: {tx_url}")
|
|
|
|
tx_json = r.json(parse_float=decimal.Decimal)
|
|
if "error" in tx_json:
|
|
raise click.ClickException(f"Transaction not found: {txhash}")
|
|
|
|
tx = btc.from_json(tx_json)
|
|
txes[txhash] = tx
|
|
try:
|
|
from_address = tx_json["vout"][prev_index]["scriptPubKey"]["address"]
|
|
echo(f"From address: {from_address}")
|
|
except Exception:
|
|
pass
|
|
amount = tx.bin_outputs[prev_index].amount
|
|
echo(f"Input amount: {amount}")
|
|
|
|
address_n = prompt("BIP-32 path to derive the key", type=tools.parse_path)
|
|
|
|
reported_type = tx_json["vout"][prev_index]["scriptPubKey"].get("type")
|
|
if reported_type in BITCOIN_CORE_INPUT_TYPES:
|
|
script_type = BITCOIN_CORE_INPUT_TYPES[reported_type]
|
|
click.echo(f"Script type: {script_type.name}")
|
|
else:
|
|
script_type = prompt(
|
|
"Input type",
|
|
type=ChoiceType(INPUT_SCRIPTS),
|
|
default=_default_script_type(address_n, INPUT_SCRIPTS),
|
|
)
|
|
if isinstance(script_type, str):
|
|
script_type = INPUT_SCRIPTS[script_type]
|
|
|
|
sequence = prompt(
|
|
"Sequence Number to use (RBF opt-in enabled by default)",
|
|
type=int,
|
|
default=0xFFFFFFFD,
|
|
)
|
|
|
|
new_input = messages.TxInputType(
|
|
address_n=address_n,
|
|
prev_hash=prev_hash,
|
|
prev_index=prev_index,
|
|
amount=amount,
|
|
script_type=script_type,
|
|
sequence=sequence,
|
|
)
|
|
|
|
inputs.append(new_input)
|
|
|
|
return inputs, txes
|
|
|
|
|
|
def _get_outputs_interactive() -> List[messages.TxOutputType]:
|
|
outputs: List[messages.TxOutputType] = []
|
|
while True:
|
|
echo()
|
|
address = prompt("Output address for non-change output", default="")
|
|
if address:
|
|
address_n = None
|
|
script_type = messages.OutputScriptType.PAYTOADDRESS
|
|
else:
|
|
address = None
|
|
address_n = prompt(
|
|
"BIP-32 path for change output (empty string to skip)", default=""
|
|
)
|
|
if not address_n:
|
|
break
|
|
try:
|
|
address_n = tools.parse_path(address_n)
|
|
except Exception:
|
|
continue
|
|
script_type = prompt(
|
|
"Output type",
|
|
type=ChoiceType(OUTPUT_SCRIPTS),
|
|
default=_default_script_type(address_n, OUTPUT_SCRIPTS),
|
|
)
|
|
if isinstance(script_type, str):
|
|
script_type = OUTPUT_SCRIPTS[script_type]
|
|
|
|
amount = prompt("Amount to spend (satoshis)", type=int)
|
|
|
|
outputs.append(
|
|
messages.TxOutputType(
|
|
address_n=address_n,
|
|
address=address,
|
|
amount=amount,
|
|
script_type=script_type,
|
|
)
|
|
)
|
|
|
|
return outputs
|
|
|
|
|
|
@click.command()
|
|
def sign_interactive() -> None:
|
|
coin = prompt("Coin name", default="Bitcoin")
|
|
blockbook_host = prompt("Blockbook server", default="btc1.trezor.io")
|
|
|
|
if not SESSION.get(f"https://{blockbook_host}/api/block/1").ok:
|
|
raise click.ClickException("Could not connect to blockbook")
|
|
|
|
blockbook_url = f"https://{blockbook_host}/api/tx-specific/"
|
|
|
|
inputs, txes = _get_inputs_interactive(blockbook_url)
|
|
outputs = _get_outputs_interactive()
|
|
|
|
version = prompt("Transaction version", type=int, default=2)
|
|
lock_time = prompt("Transaction locktime", type=int, default=0)
|
|
|
|
result = {
|
|
"coin_name": coin,
|
|
"inputs": [to_dict(i, hexlify_bytes=True) for i in inputs],
|
|
"outputs": [to_dict(o, hexlify_bytes=True) for o in outputs],
|
|
"details": {
|
|
"version": version,
|
|
"lock_time": lock_time,
|
|
},
|
|
"prev_txes": {
|
|
txhash: to_dict(txdata, hexlify_bytes=True)
|
|
for txhash, txdata in txes.items()
|
|
},
|
|
}
|
|
|
|
print(json.dumps(result, sort_keys=True, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sign_interactive()
|