tMerge pull request #5721 from SomberNight/201910_psbt - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
DIR commit 707b74d22b28d942c445754311736f158e505990
DIR parent 6d12ebabbb686bcd028b659c6a6ce2cb4782bc14
HTML Author: ThomasV <thomasv@electrum.org>
Date: Thu, 7 Nov 2019 17:10:20 +0100
Merge pull request #5721 from SomberNight/201910_psbt
integrate PSBT support natively. WIP
Diffstat:
M electrum/address_synchronizer.py | 94 +++++++++++++++----------------
M electrum/base_wizard.py | 9 ++++++---
M electrum/bip32.py | 71 ++++++++++++++++++++++++++++---
M electrum/bitcoin.py | 24 ++++++++++++++++--------
M electrum/coinchooser.py | 81 +++++++++++++++++--------------
M electrum/commands.py | 97 ++++++++++++++++++-------------
M electrum/ecc.py | 10 ++++++++++
M electrum/gui/kivy/main_window.py | 16 ++++++----------
M electrum/gui/kivy/uix/dialogs/__in… | 11 ++++++++---
M electrum/gui/kivy/uix/dialogs/tx_d… | 37 +++++++++++++++++++++++--------
M electrum/gui/kivy/uix/screens.py | 12 +++++-------
M electrum/gui/qt/address_dialog.py | 12 ++++++++----
M electrum/gui/qt/main_window.py | 104 ++++++++++++++++----------------
M electrum/gui/qt/paytoedit.py | 40 +++++++++++++++----------------
M electrum/gui/qt/transaction_dialog… | 227 ++++++++++++++++++++++++-------
M electrum/gui/qt/util.py | 9 ++++++---
M electrum/gui/qt/utxo_list.py | 48 ++++++++++++++++---------------
M electrum/gui/stdio.py | 9 +++++----
M electrum/gui/text.py | 9 +++++----
M electrum/json_db.py | 72 ++++++++++++++++++++++---------
M electrum/keystore.py | 372 ++++++++++++++++++-------------
M electrum/lnchannel.py | 48 ++++++++++++++-----------------
M electrum/lnpeer.py | 28 +++++++++++++++-------------
M electrum/lnsweep.py | 113 +++++++++++++------------------
M electrum/lnutil.py | 105 +++++++++++++++----------------
M electrum/lnwatcher.py | 6 ++++--
M electrum/lnworker.py | 2 +-
M electrum/network.py | 5 +++--
M electrum/paymentrequest.py | 23 ++++++++++++-----------
M electrum/plugin.py | 8 ++++++--
M electrum/plugins/audio_modem/qt.py | 8 ++++++--
D electrum/plugins/coldcard/basic_ps… | 313 -------------------------------
D electrum/plugins/coldcard/build_ps… | 397 -------------------------------
M electrum/plugins/coldcard/coldcard… | 169 ++++++++++++-------------------
M electrum/plugins/coldcard/qt.py | 145 ++++---------------------------
M electrum/plugins/cosigner_pool/qt.… | 48 ++++++++++++++++----------------
M electrum/plugins/digitalbitbox/dig… | 103 +++++++++++++------------------
M electrum/plugins/digitalbitbox/qt.… | 12 ++++++------
M electrum/plugins/greenaddress_inst… | 9 +++++----
M electrum/plugins/hw_wallet/plugin.… | 47 ++++++++++++++++++++++---------
M electrum/plugins/keepkey/keepkey.py | 220 ++++++++++++++-----------------
M electrum/plugins/ledger/ledger.py | 141 +++++++++++++------------------
M electrum/plugins/safe_t/safe_t.py | 220 ++++++++++++++-----------------
M electrum/plugins/trezor/trezor.py | 155 ++++++++++++++-----------------
M electrum/plugins/trustedcoin/cmdli… | 2 +-
A electrum/plugins/trustedcoin/legac… | 106 ++++++++++++++++++++++++++++++
M electrum/plugins/trustedcoin/trust… | 37 ++++++++++++++++++-------------
M electrum/scripts/bip70.py | 3 ++-
M electrum/segwit_addr.py | 2 ++
M electrum/synchronizer.py | 6 +++---
M electrum/tests/regtest/regtest.sh | 6 +++---
M electrum/tests/test_bitcoin.py | 10 +++++++++-
M electrum/tests/test_commands.py | 21 +++++++++++++++++++++
M electrum/tests/test_lnchannel.py | 2 +-
M electrum/tests/test_lnutil.py | 11 ++++-------
A electrum/tests/test_psbt.py | 269 +++++++++++++++++++++++++++++++
M electrum/tests/test_transaction.py | 289 +++++++++++++++----------------
M electrum/tests/test_wallet.py | 2 +-
M electrum/tests/test_wallet_vertica… | 463 +++++++++++++++++++++++--------
M electrum/transaction.py | 2130 ++++++++++++++++++++-----------
M electrum/util.py | 6 ++++--
M electrum/wallet.py | 537 ++++++++++++++++++-------------
62 files changed, 4171 insertions(+), 3420 deletions(-)
---
DIR diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py
t@@ -29,9 +29,9 @@ from collections import defaultdict
from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple, NamedTuple, Sequence
from . import bitcoin
-from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY
+from .bitcoin import COINBASE_MATURITY
from .util import profiler, bfh, TxMinedInfo
-from .transaction import Transaction, TxOutput
+from .transaction import Transaction, TxOutput, TxInput, PartialTxInput, TxOutpoint, PartialTransaction
from .synchronizer import Synchronizer
from .verifier import SPV
from .blockchain import hash_header
t@@ -125,12 +125,12 @@ class AddressSynchronizer(Logger):
"""Return number of transactions where address is involved."""
return len(self._history_local.get(addr, ()))
- def get_txin_address(self, txi) -> Optional[str]:
- addr = txi.get('address')
- if addr and addr != "(pubkey)":
- return addr
- prevout_hash = txi.get('prevout_hash')
- prevout_n = txi.get('prevout_n')
+ def get_txin_address(self, txin: TxInput) -> Optional[str]:
+ if isinstance(txin, PartialTxInput):
+ if txin.address:
+ return txin.address
+ prevout_hash = txin.prevout.txid.hex()
+ prevout_n = txin.prevout.out_idx
for addr in self.db.get_txo_addresses(prevout_hash):
l = self.db.get_txo_addr(prevout_hash, addr)
for n, v, is_cb in l:
t@@ -138,14 +138,8 @@ class AddressSynchronizer(Logger):
return addr
return None
- def get_txout_address(self, txo: TxOutput):
- if txo.type == TYPE_ADDRESS:
- addr = txo.address
- elif txo.type == TYPE_PUBKEY:
- addr = bitcoin.public_key_to_p2pkh(bfh(txo.address))
- else:
- addr = None
- return addr
+ def get_txout_address(self, txo: TxOutput) -> Optional[str]:
+ return txo.address
def load_unverified_transactions(self):
# review transactions that are in the history
t@@ -183,7 +177,7 @@ class AddressSynchronizer(Logger):
if self.synchronizer:
self.synchronizer.add(address)
- def get_conflicting_transactions(self, tx_hash, tx, include_self=False):
+ def get_conflicting_transactions(self, tx_hash, tx: Transaction, include_self=False):
"""Returns a set of transaction hashes from the wallet history that are
directly conflicting with tx, i.e. they have common outpoints being
spent with tx.
t@@ -194,10 +188,10 @@ class AddressSynchronizer(Logger):
conflicting_txns = set()
with self.transaction_lock:
for txin in tx.inputs():
- if txin['type'] == 'coinbase':
+ if txin.is_coinbase():
continue
- prevout_hash = txin['prevout_hash']
- prevout_n = txin['prevout_n']
+ prevout_hash = txin.prevout.txid.hex()
+ prevout_n = txin.prevout.out_idx
spending_tx_hash = self.db.get_spent_outpoint(prevout_hash, prevout_n)
if spending_tx_hash is None:
continue
t@@ -213,7 +207,7 @@ class AddressSynchronizer(Logger):
conflicting_txns -= {tx_hash}
return conflicting_txns
- def add_transaction(self, tx_hash, tx, allow_unrelated=False) -> bool:
+ def add_transaction(self, tx_hash, tx: Transaction, allow_unrelated=False) -> bool:
"""Returns whether the tx was successfully added to the wallet history."""
assert tx_hash, tx_hash
assert tx, tx
t@@ -226,7 +220,7 @@ class AddressSynchronizer(Logger):
# BUT we track is_mine inputs in a txn, and during subsequent calls
# of add_transaction tx, we might learn of more-and-more inputs of
# being is_mine, as we roll the gap_limit forward
- is_coinbase = tx.inputs()[0]['type'] == 'coinbase'
+ is_coinbase = tx.inputs()[0].is_coinbase()
tx_height = self.get_tx_height(tx_hash).height
if not allow_unrelated:
# note that during sync, if the transactions are not properly sorted,
t@@ -277,11 +271,11 @@ class AddressSynchronizer(Logger):
self._get_addr_balance_cache.pop(addr, None) # invalidate cache
return
for txi in tx.inputs():
- if txi['type'] == 'coinbase':
+ if txi.is_coinbase():
continue
- prevout_hash = txi['prevout_hash']
- prevout_n = txi['prevout_n']
- ser = prevout_hash + ':%d' % prevout_n
+ prevout_hash = txi.prevout.txid.hex()
+ prevout_n = txi.prevout.out_idx
+ ser = txi.prevout.to_str()
self.db.set_spent_outpoint(prevout_hash, prevout_n, tx_hash)
add_value_from_prev_output()
# add outputs
t@@ -310,10 +304,10 @@ class AddressSynchronizer(Logger):
if tx is not None:
# if we have the tx, this branch is faster
for txin in tx.inputs():
- if txin['type'] == 'coinbase':
+ if txin.is_coinbase():
continue
- prevout_hash = txin['prevout_hash']
- prevout_n = txin['prevout_n']
+ prevout_hash = txin.prevout.txid.hex()
+ prevout_n = txin.prevout.out_idx
self.db.remove_spent_outpoint(prevout_hash, prevout_n)
else:
# expensive but always works
t@@ -572,7 +566,7 @@ class AddressSynchronizer(Logger):
return cached_local_height
return self.network.get_local_height() if self.network else self.db.get('stored_height', 0)
- def add_future_tx(self, tx, num_blocks):
+ def add_future_tx(self, tx: Transaction, num_blocks):
with self.lock:
self.add_transaction(tx.txid(), tx)
self.future_tx[tx.txid()] = num_blocks
t@@ -649,14 +643,16 @@ class AddressSynchronizer(Logger):
if self.is_mine(addr):
is_mine = True
is_relevant = True
- d = self.db.get_txo_addr(txin['prevout_hash'], addr)
+ d = self.db.get_txo_addr(txin.prevout.txid.hex(), addr)
for n, v, cb in d:
- if n == txin['prevout_n']:
+ if n == txin.prevout.out_idx:
value = v
break
else:
value = None
if value is None:
+ value = txin.value_sats()
+ if value is None:
is_pruned = True
else:
v_in += value
t@@ -736,23 +732,19 @@ class AddressSynchronizer(Logger):
sent[txi] = height
return received, sent
- def get_addr_utxo(self, address):
+ def get_addr_utxo(self, address: str) -> Dict[TxOutpoint, PartialTxInput]:
coins, spent = self.get_addr_io(address)
for txi in spent:
coins.pop(txi)
out = {}
- for txo, v in coins.items():
+ for prevout_str, v in coins.items():
tx_height, value, is_cb = v
- prevout_hash, prevout_n = txo.split(':')
- x = {
- 'address':address,
- 'value':value,
- 'prevout_n':int(prevout_n),
- 'prevout_hash':prevout_hash,
- 'height':tx_height,
- 'coinbase':is_cb
- }
- out[txo] = x
+ prevout = TxOutpoint.from_str(prevout_str)
+ utxo = PartialTxInput(prevout=prevout)
+ utxo._trusted_address = address
+ utxo._trusted_value_sats = value
+ utxo.block_height = tx_height
+ out[prevout] = utxo
return out
# return the total amount ever received by an address
t@@ -799,7 +791,8 @@ class AddressSynchronizer(Logger):
@with_local_height_cached
def get_utxos(self, domain=None, *, excluded_addresses=None,
- mature_only: bool = False, confirmed_only: bool = False, nonlocal_only: bool = False):
+ mature_only: bool = False, confirmed_only: bool = False,
+ nonlocal_only: bool = False) -> Sequence[PartialTxInput]:
coins = []
if domain is None:
domain = self.get_addresses()
t@@ -809,14 +802,15 @@ class AddressSynchronizer(Logger):
mempool_height = self.get_local_height() + 1 # height of next block
for addr in domain:
utxos = self.get_addr_utxo(addr)
- for x in utxos.values():
- if confirmed_only and x['height'] <= 0:
+ for utxo in utxos.values():
+ if confirmed_only and utxo.block_height <= 0:
continue
- if nonlocal_only and x['height'] == TX_HEIGHT_LOCAL:
+ if nonlocal_only and utxo.block_height == TX_HEIGHT_LOCAL:
continue
- if mature_only and x['coinbase'] and x['height'] + COINBASE_MATURITY > mempool_height:
+ if (mature_only and utxo.prevout.is_coinbase()
+ and utxo.block_height + COINBASE_MATURITY > mempool_height):
continue
- coins.append(x)
+ coins.append(utxo)
continue
return coins
DIR diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py
t@@ -33,7 +33,7 @@ from typing import List, TYPE_CHECKING, Tuple, NamedTuple, Any, Dict, Optional
from . import bitcoin
from . import keystore
from . import mnemonic
-from .bip32 import is_bip32_derivation, xpub_type, normalize_bip32_derivation
+from .bip32 import is_bip32_derivation, xpub_type, normalize_bip32_derivation, BIP32Node
from .keystore import bip44_derivation, purpose48_derivation
from .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet,
wallet_types, Wallet, Abstract_Wallet)
t@@ -230,7 +230,7 @@ class BaseWizard(Logger):
assert bitcoin.is_private_key(pk)
txin_type, pubkey = k.import_privkey(pk, None)
addr = bitcoin.pubkey_to_address(txin_type, pubkey)
- self.data['addresses'][addr] = {'type':txin_type, 'pubkey':pubkey, 'redeem_script':None}
+ self.data['addresses'][addr] = {'type':txin_type, 'pubkey':pubkey}
self.keystores.append(k)
else:
return self.terminate()
t@@ -394,7 +394,7 @@ class BaseWizard(Logger):
# For segwit, a custom path is used, as there is no standard at all.
default_choice_idx = 2
choices = [
- ('standard', 'legacy multisig (p2sh)', "m/45'/0"),
+ ('standard', 'legacy multisig (p2sh)', normalize_bip32_derivation("m/45'/0")),
('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')),
('p2wsh', 'native segwit multisig (p2wsh)', purpose48_derivation(0, xtype='p2wsh')),
]
t@@ -420,16 +420,19 @@ class BaseWizard(Logger):
from .keystore import hardware_keystore
try:
xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self)
+ root_xpub = self.plugin.get_xpub(device_info.device.id_, 'm', 'standard', self)
except ScriptTypeNotSupported:
raise # this is handled in derivation_dialog
except BaseException as e:
self.logger.exception('')
self.show_error(e)
return
+ xfp = BIP32Node.from_xkey(root_xpub).calc_fingerprint_of_this_node().hex().lower()
d = {
'type': 'hardware',
'hw_type': name,
'derivation': derivation,
+ 'root_fingerprint': xfp,
'xpub': xpub,
'label': device_info.label,
}
DIR diff --git a/electrum/bip32.py b/electrum/bip32.py
t@@ -3,7 +3,7 @@
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
import hashlib
-from typing import List, Tuple, NamedTuple, Union, Iterable
+from typing import List, Tuple, NamedTuple, Union, Iterable, Sequence, Optional
from .util import bfh, bh2u, BitcoinException
from . import constants
t@@ -116,7 +116,7 @@ class BIP32Node(NamedTuple):
eckey: Union[ecc.ECPubkey, ecc.ECPrivkey]
chaincode: bytes
depth: int = 0
- fingerprint: bytes = b'\x00'*4
+ fingerprint: bytes = b'\x00'*4 # as in serialized format, this is the *parent's* fingerprint
child_number: bytes = b'\x00'*4
@classmethod
t@@ -161,7 +161,18 @@ class BIP32Node(NamedTuple):
eckey=ecc.ECPrivkey(master_k),
chaincode=master_c)
+ @classmethod
+ def from_bytes(cls, b: bytes) -> 'BIP32Node':
+ if len(b) != 78:
+ raise Exception(f"unexpected xkey raw bytes len {len(b)} != 78")
+ xkey = EncodeBase58Check(b)
+ return cls.from_xkey(xkey)
+
def to_xprv(self, *, net=None) -> str:
+ payload = self.to_xprv_bytes(net=net)
+ return EncodeBase58Check(payload)
+
+ def to_xprv_bytes(self, *, net=None) -> bytes:
if not self.is_private():
raise Exception("cannot serialize as xprv; private key missing")
payload = (xprv_header(self.xtype, net=net) +
t@@ -172,9 +183,13 @@ class BIP32Node(NamedTuple):
bytes([0]) +
self.eckey.get_secret_bytes())
assert len(payload) == 78, f"unexpected xprv payload len {len(payload)}"
- return EncodeBase58Check(payload)
+ return payload
def to_xpub(self, *, net=None) -> str:
+ payload = self.to_xpub_bytes(net=net)
+ return EncodeBase58Check(payload)
+
+ def to_xpub_bytes(self, *, net=None) -> bytes:
payload = (xpub_header(self.xtype, net=net) +
bytes([self.depth]) +
self.fingerprint +
t@@ -182,7 +197,7 @@ class BIP32Node(NamedTuple):
self.chaincode +
self.eckey.get_public_key_bytes(compressed=True))
assert len(payload) == 78, f"unexpected xpub payload len {len(payload)}"
- return EncodeBase58Check(payload)
+ return payload
def to_xkey(self, *, net=None) -> str:
if self.is_private():
t@@ -190,6 +205,12 @@ class BIP32Node(NamedTuple):
else:
return self.to_xpub(net=net)
+ def to_bytes(self, *, net=None) -> bytes:
+ if self.is_private():
+ return self.to_xprv_bytes(net=net)
+ else:
+ return self.to_xpub_bytes(net=net)
+
def convert_to_public(self) -> 'BIP32Node':
if not self.is_private():
return self
t@@ -248,6 +269,12 @@ class BIP32Node(NamedTuple):
fingerprint=fingerprint,
child_number=child_number)
+ def calc_fingerprint_of_this_node(self) -> bytes:
+ """Returns the fingerprint of this node.
+ Note that self.fingerprint is of the *parent*.
+ """
+ return hash_160(self.eckey.get_public_key_bytes(compressed=True))[0:4]
+
def xpub_type(x):
return BIP32Node.from_xkey(x).xtype
t@@ -308,7 +335,7 @@ def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]:
return path
-def convert_bip32_intpath_to_strpath(path: List[int]) -> str:
+def convert_bip32_intpath_to_strpath(path: Sequence[int]) -> str:
s = "m/"
for child_index in path:
if not isinstance(child_index, int):
t@@ -336,8 +363,40 @@ def is_bip32_derivation(s: str) -> bool:
return True
-def normalize_bip32_derivation(s: str) -> str:
+def normalize_bip32_derivation(s: Optional[str]) -> Optional[str]:
+ if s is None:
+ return None
if not is_bip32_derivation(s):
raise ValueError(f"invalid bip32 derivation: {s}")
ints = convert_bip32_path_to_list_of_uint32(s)
return convert_bip32_intpath_to_strpath(ints)
+
+
+def is_all_public_derivation(path: Union[str, Iterable[int]]) -> bool:
+ """Returns whether all levels in path use non-hardened derivation."""
+ if isinstance(path, str):
+ path = convert_bip32_path_to_list_of_uint32(path)
+ for child_index in path:
+ if child_index < 0:
+ raise ValueError('the bip32 index needs to be non-negative')
+ if child_index & BIP32_PRIME:
+ return False
+ return True
+
+
+def root_fp_and_der_prefix_from_xkey(xkey: str) -> Tuple[Optional[str], Optional[str]]:
+ """Returns the root bip32 fingerprint and the derivation path from the
+ root to the given xkey, if they can be determined. Otherwise (None, None).
+ """
+ node = BIP32Node.from_xkey(xkey)
+ derivation_prefix = None
+ root_fingerprint = None
+ assert node.depth >= 0, node.depth
+ if node.depth == 0:
+ derivation_prefix = 'm'
+ root_fingerprint = node.calc_fingerprint_of_this_node().hex().lower()
+ elif node.depth == 1:
+ child_number_int = int.from_bytes(node.child_number, 'big')
+ derivation_prefix = convert_bip32_intpath_to_strpath([child_number_int])
+ root_fingerprint = node.fingerprint.hex()
+ return root_fingerprint, derivation_prefix
DIR diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py
t@@ -45,6 +45,7 @@ COIN = 100000000
TOTAL_COIN_SUPPLY_LIMIT_IN_BTC = 21000000
# supported types of transaction outputs
+# TODO kill these with fire
TYPE_ADDRESS = 0
TYPE_PUBKEY = 1
TYPE_SCRIPT = 2
t@@ -237,6 +238,9 @@ def script_num_to_hex(i: int) -> str:
def var_int(i: int) -> str:
# https://en.bitcoin.it/wiki/Protocol_specification#Variable_length_integer
+ # https://github.com/bitcoin/bitcoin/blob/efe1ee0d8d7f82150789f1f6840f139289628a2b/src/serialize.h#L247
+ # "CompactSize"
+ assert i >= 0, i
if i<0xfd:
return int_to_hex(i)
elif i<=0xffff:
t@@ -372,24 +376,28 @@ def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str:
else:
raise NotImplementedError(txin_type)
-def redeem_script_to_address(txin_type: str, redeem_script: str, *, net=None) -> str:
+
+# TODO this method is confusingly named
+def redeem_script_to_address(txin_type: str, scriptcode: str, *, net=None) -> str:
if net is None: net = constants.net
if txin_type == 'p2sh':
- return hash160_to_p2sh(hash_160(bfh(redeem_script)), net=net)
+ # given scriptcode is a redeem_script
+ return hash160_to_p2sh(hash_160(bfh(scriptcode)), net=net)
elif txin_type == 'p2wsh':
- return script_to_p2wsh(redeem_script, net=net)
+ # given scriptcode is a witness_script
+ return script_to_p2wsh(scriptcode, net=net)
elif txin_type == 'p2wsh-p2sh':
- scriptSig = p2wsh_nested_script(redeem_script)
- return hash160_to_p2sh(hash_160(bfh(scriptSig)), net=net)
+ # given scriptcode is a witness_script
+ redeem_script = p2wsh_nested_script(scriptcode)
+ return hash160_to_p2sh(hash_160(bfh(redeem_script)), net=net)
else:
raise NotImplementedError(txin_type)
def script_to_address(script: str, *, net=None) -> str:
from .transaction import get_address_from_output_script
- t, addr = get_address_from_output_script(bfh(script), net=net)
- assert t == TYPE_ADDRESS
- return addr
+ return get_address_from_output_script(bfh(script), net=net)
+
def address_to_script(addr: str, *, net=None) -> str:
if net is None: net = constants.net
DIR diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py
t@@ -24,11 +24,11 @@
# SOFTWARE.
from collections import defaultdict
from math import floor, log10
-from typing import NamedTuple, List, Callable
+from typing import NamedTuple, List, Callable, Sequence, Union, Dict, Tuple
from decimal import Decimal
-from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address
-from .transaction import Transaction, TxOutput
+from .bitcoin import sha256, COIN, is_address
+from .transaction import Transaction, TxOutput, PartialTransaction, PartialTxInput, PartialTxOutput
from .util import NotEnoughFunds
from .logging import Logger
t@@ -73,21 +73,21 @@ class PRNG:
class Bucket(NamedTuple):
desc: str
- weight: int # as in BIP-141
- value: int # in satoshis
- effective_value: int # estimate of value left after subtracting fees. in satoshis
- coins: List[dict] # UTXOs
- min_height: int # min block height where a coin was confirmed
- witness: bool # whether any coin uses segwit
+ weight: int # as in BIP-141
+ value: int # in satoshis
+ effective_value: int # estimate of value left after subtracting fees. in satoshis
+ coins: List[PartialTxInput] # UTXOs
+ min_height: int # min block height where a coin was confirmed
+ witness: bool # whether any coin uses segwit
class ScoredCandidate(NamedTuple):
penalty: float
- tx: Transaction
+ tx: PartialTransaction
buckets: List[Bucket]
-def strip_unneeded(bkts, sufficient_funds):
+def strip_unneeded(bkts: List[Bucket], sufficient_funds) -> List[Bucket]:
'''Remove buckets that are unnecessary in achieving the spend amount'''
if sufficient_funds([], bucket_value_sum=0):
# none of the buckets are needed
t@@ -108,26 +108,27 @@ class CoinChooserBase(Logger):
def __init__(self):
Logger.__init__(self)
- def keys(self, coins):
+ def keys(self, coins: Sequence[PartialTxInput]) -> Sequence[str]:
raise NotImplementedError
- def bucketize_coins(self, coins, *, fee_estimator_vb):
+ def bucketize_coins(self, coins: Sequence[PartialTxInput], *, fee_estimator_vb):
keys = self.keys(coins)
- buckets = defaultdict(list)
+ buckets = defaultdict(list) # type: Dict[str, List[PartialTxInput]]
for key, coin in zip(keys, coins):
buckets[key].append(coin)
# fee_estimator returns fee to be paid, for given vbytes.
# guess whether it is just returning a constant as follows.
constant_fee = fee_estimator_vb(2000) == fee_estimator_vb(200)
- def make_Bucket(desc, coins):
+ def make_Bucket(desc: str, coins: List[PartialTxInput]):
witness = any(Transaction.is_segwit_input(coin, guess_for_address=True) for coin in coins)
# note that we're guessing whether the tx uses segwit based
# on this single bucket
weight = sum(Transaction.estimated_input_weight(coin, witness)
for coin in coins)
- value = sum(coin['value'] for coin in coins)
- min_height = min(coin['height'] for coin in coins)
+ value = sum(coin.value_sats() for coin in coins)
+ min_height = min(coin.block_height for coin in coins)
+ assert min_height is not None
# the fee estimator is typically either a constant or a linear function,
# so the "function:" effective_value(bucket) will be homomorphic for addition
# i.e. effective_value(b1) + effective_value(b2) = effective_value(b1 + b2)
t@@ -148,10 +149,12 @@ class CoinChooserBase(Logger):
return list(map(make_Bucket, buckets.keys(), buckets.values()))
- def penalty_func(self, base_tx, *, tx_from_buckets) -> Callable[[List[Bucket]], ScoredCandidate]:
+ def penalty_func(self, base_tx, *,
+ tx_from_buckets: Callable[[List[Bucket]], Tuple[PartialTransaction, List[PartialTxOutput]]]) \
+ -> Callable[[List[Bucket]], ScoredCandidate]:
raise NotImplementedError
- def _change_amounts(self, tx, count, fee_estimator_numchange) -> List[int]:
+ def _change_amounts(self, tx: PartialTransaction, count: int, fee_estimator_numchange) -> List[int]:
# Break change up if bigger than max_change
output_amounts = [o.value for o in tx.outputs()]
# Don't split change of less than 0.02 BTC
t@@ -205,7 +208,8 @@ class CoinChooserBase(Logger):
return amounts
- def _change_outputs(self, tx, change_addrs, fee_estimator_numchange, dust_threshold):
+ def _change_outputs(self, tx: PartialTransaction, change_addrs, fee_estimator_numchange,
+ dust_threshold) -> List[PartialTxOutput]:
amounts = self._change_amounts(tx, len(change_addrs), fee_estimator_numchange)
assert min(amounts) >= 0
assert len(change_addrs) >= len(amounts)
t@@ -213,21 +217,23 @@ class CoinChooserBase(Logger):
# If change is above dust threshold after accounting for the
# size of the change output, add it to the transaction.
amounts = [amount for amount in amounts if amount >= dust_threshold]
- change = [TxOutput(TYPE_ADDRESS, addr, amount)
+ change = [PartialTxOutput.from_address_and_value(addr, amount)
for addr, amount in zip(change_addrs, amounts)]
return change
- def _construct_tx_from_selected_buckets(self, *, buckets, base_tx, change_addrs,
- fee_estimator_w, dust_threshold, base_weight):
+ def _construct_tx_from_selected_buckets(self, *, buckets: Sequence[Bucket],
+ base_tx: PartialTransaction, change_addrs,
+ fee_estimator_w, dust_threshold,
+ base_weight) -> Tuple[PartialTransaction, List[PartialTxOutput]]:
# make a copy of base_tx so it won't get mutated
- tx = Transaction.from_io(base_tx.inputs()[:], base_tx.outputs()[:])
+ tx = PartialTransaction.from_io(base_tx.inputs()[:], base_tx.outputs()[:])
tx.add_inputs([coin for b in buckets for coin in b.coins])
tx_weight = self._get_tx_weight(buckets, base_weight=base_weight)
# change is sent back to sending address unless specified
if not change_addrs:
- change_addrs = [tx.inputs()[0]['address']]
+ change_addrs = [tx.inputs()[0].address]
# note: this is not necessarily the final "first input address"
# because the inputs had not been sorted at this point
assert is_address(change_addrs[0])
t@@ -240,7 +246,7 @@ class CoinChooserBase(Logger):
return tx, change
- def _get_tx_weight(self, buckets, *, base_weight) -> int:
+ def _get_tx_weight(self, buckets: Sequence[Bucket], *, base_weight: int) -> int:
"""Given a collection of buckets, return the total weight of the
resulting transaction.
base_weight is the weight of the tx that includes the fixed (non-change)
t@@ -260,8 +266,9 @@ class CoinChooserBase(Logger):
return total_weight
- def make_tx(self, coins, inputs, outputs, change_addrs, fee_estimator_vb,
- dust_threshold):
+ def make_tx(self, *, coins: Sequence[PartialTxInput], inputs: List[PartialTxInput],
+ outputs: List[PartialTxOutput], change_addrs: Sequence[str],
+ fee_estimator_vb: Callable, dust_threshold: int) -> PartialTransaction:
"""Select unspent coins to spend to pay outputs. If the change is
greater than dust_threshold (after adding the change output to
the transaction) it is kept, otherwise none is sent and it is
t@@ -276,11 +283,11 @@ class CoinChooserBase(Logger):
assert outputs, 'tx outputs cannot be empty'
# Deterministic randomness from coins
- utxos = [c['prevout_hash'] + str(c['prevout_n']) for c in coins]
- self.p = PRNG(''.join(sorted(utxos)))
+ utxos = [c.prevout.serialize_to_network() for c in coins]
+ self.p = PRNG(b''.join(sorted(utxos)))
# Copy the outputs so when adding change we don't modify "outputs"
- base_tx = Transaction.from_io(inputs[:], outputs[:])
+ base_tx = PartialTransaction.from_io(inputs[:], outputs[:])
input_value = base_tx.input_value()
# Weight of the transaction with no inputs and no change
t@@ -331,14 +338,15 @@ class CoinChooserBase(Logger):
return tx
- def choose_buckets(self, buckets, sufficient_funds,
+ def choose_buckets(self, buckets: List[Bucket],
+ sufficient_funds: Callable,
penalty_func: Callable[[List[Bucket]], ScoredCandidate]) -> ScoredCandidate:
raise NotImplemented('To be subclassed')
class CoinChooserRandom(CoinChooserBase):
- def bucket_candidates_any(self, buckets, sufficient_funds):
+ def bucket_candidates_any(self, buckets: List[Bucket], sufficient_funds) -> List[List[Bucket]]:
'''Returns a list of bucket sets.'''
if not buckets:
raise NotEnoughFunds()
t@@ -373,7 +381,8 @@ class CoinChooserRandom(CoinChooserBase):
candidates = [[buckets[n] for n in c] for c in candidates]
return [strip_unneeded(c, sufficient_funds) for c in candidates]
- def bucket_candidates_prefer_confirmed(self, buckets, sufficient_funds):
+ def bucket_candidates_prefer_confirmed(self, buckets: List[Bucket],
+ sufficient_funds) -> List[List[Bucket]]:
"""Returns a list of bucket sets preferring confirmed coins.
Any bucket can be:
t@@ -433,13 +442,13 @@ class CoinChooserPrivacy(CoinChooserRandom):
"""
def keys(self, coins):
- return [coin['address'] for coin in coins]
+ return [coin.scriptpubkey.hex() for coin in coins]
def penalty_func(self, base_tx, *, tx_from_buckets):
min_change = min(o.value for o in base_tx.outputs()) * 0.75
max_change = max(o.value for o in base_tx.outputs()) * 1.33
- def penalty(buckets) -> ScoredCandidate:
+ def penalty(buckets: List[Bucket]) -> ScoredCandidate:
# Penalize using many buckets (~inputs)
badness = len(buckets) - 1
tx, change_outputs = tx_from_buckets(buckets)
DIR diff --git a/electrum/commands.py b/electrum/commands.py
t@@ -35,16 +35,17 @@ import asyncio
import inspect
from functools import wraps, partial
from decimal import Decimal
-from typing import Optional, TYPE_CHECKING, Dict
+from typing import Optional, TYPE_CHECKING, Dict, List
from .import util, ecc
from .util import bfh, bh2u, format_satoshis, json_decode, json_encode, is_hash256_str, is_hex_str, to_bytes, timestamp_to_datetime
from .util import standardize_path
from . import bitcoin
-from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS
+from .bitcoin import is_address, hash_160, COIN
from .bip32 import BIP32Node
from .i18n import _
-from .transaction import Transaction, multisig_script, TxOutput
+from .transaction import (Transaction, multisig_script, TxOutput, PartialTransaction, PartialTxOutput,
+ tx_from_any, PartialTxInput, TxOutpoint)
from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
from .synchronizer import Notifier
from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text
t@@ -299,11 +300,13 @@ class Commands:
async def listunspent(self, wallet: Abstract_Wallet = None):
"""List unspent outputs. Returns the list of unspent transaction
outputs in your wallet."""
- l = copy.deepcopy(wallet.get_utxos())
- for i in l:
- v = i["value"]
- i["value"] = str(Decimal(v)/COIN) if v is not None else None
- return l
+ coins = []
+ for txin in wallet.get_utxos():
+ d = txin.to_json()
+ v = d.pop("value_sats")
+ d["value"] = str(Decimal(v)/COIN) if v is not None else None
+ coins.append(d)
+ return coins
@command('n')
async def getaddressunspent(self, address):
t@@ -320,46 +323,50 @@ class Commands:
Outputs must be a list of {'address':address, 'value':satoshi_amount}.
"""
keypairs = {}
- inputs = jsontx.get('inputs')
- outputs = jsontx.get('outputs')
+ inputs = [] # type: List[PartialTxInput]
locktime = jsontx.get('lockTime', 0)
- for txin in inputs:
- if txin.get('output'):
- prevout_hash, prevout_n = txin['output'].split(':')
- txin['prevout_n'] = int(prevout_n)
- txin['prevout_hash'] = prevout_hash
- sec = txin.get('privkey')
+ for txin_dict in jsontx.get('inputs'):
+ if txin_dict.get('prevout_hash') is not None and txin_dict.get('prevout_n') is not None:
+ prevout = TxOutpoint(txid=bfh(txin_dict['prevout_hash']), out_idx=int(txin_dict['prevout_n']))
+ elif txin_dict.get('output'):
+ prevout = TxOutpoint.from_str(txin_dict['output'])
+ else:
+ raise Exception("missing prevout for txin")
+ txin = PartialTxInput(prevout=prevout)
+ txin._trusted_value_sats = int(txin_dict['value'])
+ sec = txin_dict.get('privkey')
if sec:
txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)
pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)
keypairs[pubkey] = privkey, compressed
- txin['type'] = txin_type
- txin['x_pubkeys'] = [pubkey]
- txin['signatures'] = [None]
- txin['num_sig'] = 1
-
- outputs = [TxOutput(TYPE_ADDRESS, x['address'], int(x['value'])) for x in outputs]
- tx = Transaction.from_io(inputs, outputs, locktime=locktime)
+ txin.script_type = txin_type
+ txin.pubkeys = [bfh(pubkey)]
+ txin.num_sig = 1
+ inputs.append(txin)
+
+ outputs = [PartialTxOutput.from_address_and_value(txout['address'], int(txout['value']))
+ for txout in jsontx.get('outputs')]
+ tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
tx.sign(keypairs)
- return tx.as_dict()
+ return tx.serialize()
@command('wp')
async def signtransaction(self, tx, privkey=None, password=None, wallet: Abstract_Wallet = None):
"""Sign a transaction. The wallet keys will be used unless a private key is provided."""
- tx = Transaction(tx)
+ tx = PartialTransaction(tx)
if privkey:
txin_type, privkey2, compressed = bitcoin.deserialize_privkey(privkey)
pubkey = ecc.ECPrivkey(privkey2).get_public_key_bytes(compressed=compressed).hex()
tx.sign({pubkey:(privkey2, compressed)})
else:
wallet.sign_transaction(tx, password)
- return tx.as_dict()
+ return tx.serialize()
@command('')
async def deserialize(self, tx):
"""Deserialize a serialized transaction"""
- tx = Transaction(tx)
- return tx.deserialize(force_full_parse=True)
+ tx = tx_from_any(tx)
+ return tx.to_json()
@command('n')
async def broadcast(self, tx):
t@@ -392,9 +399,9 @@ class Commands:
if isinstance(address, str):
address = address.strip()
if is_address(address):
- return wallet.export_private_key(address, password)[0]
+ return wallet.export_private_key(address, password)
domain = address
- return [wallet.export_private_key(address, password)[0] for address in domain]
+ return [wallet.export_private_key(address, password) for address in domain]
@command('w')
async def ismine(self, address, wallet: Abstract_Wallet = None):
t@@ -513,8 +520,13 @@ class Commands:
privkeys = privkey.split()
self.nocheck = nocheck
#dest = self._resolver(destination)
- tx = sweep(privkeys, self.network, self.config, destination, tx_fee, imax)
- return tx.as_dict() if tx else None
+ tx = sweep(privkeys,
+ network=self.network,
+ config=self.config,
+ to_address=destination,
+ fee=tx_fee,
+ imax=imax)
+ return tx.serialize() if tx else None
@command('wp')
async def signmessage(self, address, message, password=None, wallet: Abstract_Wallet = None):
t@@ -541,17 +553,20 @@ class Commands:
for address, amount in outputs:
address = self._resolver(address, wallet)
amount = satoshis(amount)
- final_outputs.append(TxOutput(TYPE_ADDRESS, address, amount))
+ final_outputs.append(PartialTxOutput.from_address_and_value(address, amount))
coins = wallet.get_spendable_coins(domain_addr)
if domain_coins is not None:
- coins = [coin for coin in coins if (coin['prevout_hash'] + ':' + str(coin['prevout_n']) in domain_coins)]
+ coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)]
if feerate is not None:
fee_per_kb = 1000 * Decimal(feerate)
fee_estimator = partial(SimpleConfig.estimate_fee_for_feerate, fee_per_kb)
else:
fee_estimator = fee
- tx = wallet.make_unsigned_transaction(coins, final_outputs, fee_estimator, change_addr)
+ tx = wallet.make_unsigned_transaction(coins=coins,
+ outputs=final_outputs,
+ fee=fee_estimator,
+ change_addr=change_addr)
if locktime is not None:
tx.locktime = locktime
if rbf is None:
t@@ -581,7 +596,7 @@ class Commands:
rbf=rbf,
password=password,
locktime=locktime)
- return tx.as_dict()
+ return tx.serialize()
@command('wp')
async def paytomany(self, outputs, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None,
t@@ -602,7 +617,7 @@ class Commands:
rbf=rbf,
password=password,
locktime=locktime)
- return tx.as_dict()
+ return tx.serialize()
@command('w')
async def onchain_history(self, year=None, show_addresses=False, show_fiat=False, wallet: Abstract_Wallet = None):
t@@ -703,7 +718,7 @@ class Commands:
raise Exception("Unknown transaction")
if tx.txid() != txid:
raise Exception("Mismatching txid")
- return tx.as_dict()
+ return tx.serialize()
@command('')
async def encrypt(self, pubkey, message) -> str:
t@@ -960,7 +975,7 @@ class Commands:
chan_id, _ = channel_id_from_funding_tx(txid, int(index))
chan = wallet.lnworker.channels[chan_id]
tx = chan.force_close_tx()
- return tx.as_dict()
+ return tx.serialize()
def eval_bool(x: str) -> bool:
if x == 'false': return False
t@@ -1037,7 +1052,7 @@ command_options = {
# don't use floats because of rounding errors
-from .transaction import tx_from_str
+from .transaction import convert_tx_str_to_hex
json_loads = lambda x: json.loads(x, parse_float=lambda x: str(Decimal(x)))
arg_types = {
'num': int,
t@@ -1046,7 +1061,7 @@ arg_types = {
'year': int,
'from_height': int,
'to_height': int,
- 'tx': tx_from_str,
+ 'tx': convert_tx_str_to_hex,
'pubkeys': json_loads,
'jsontx': json_loads,
'inputs': json_loads,
DIR diff --git a/electrum/ecc.py b/electrum/ecc.py
t@@ -25,6 +25,7 @@
import base64
import hashlib
+import functools
from typing import Union, Tuple, Optional
import ecdsa
t@@ -181,6 +182,7 @@ class _PubkeyForPointAtInfinity:
point = ecdsa.ellipticcurve.INFINITY
+@functools.total_ordering
class ECPubkey(object):
def __init__(self, b: Optional[bytes]):
t@@ -257,6 +259,14 @@ class ECPubkey(object):
def __ne__(self, other):
return not (self == other)
+ def __hash__(self):
+ return hash(self._pubkey.point.x())
+
+ def __lt__(self, other):
+ if not isinstance(other, ECPubkey):
+ raise TypeError('comparison not defined for ECPubkey and {}'.format(type(other)))
+ return self._pubkey.point.x() < other._pubkey.point.x()
+
def verify_message_for_address(self, sig65: bytes, message: bytes, algo=lambda x: sha256d(msg_magic(x))) -> None:
assert_bytes(message)
h = algo(message)
DIR diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
t@@ -9,7 +9,6 @@ import threading
import asyncio
from typing import TYPE_CHECKING, Optional
-from electrum.bitcoin import TYPE_ADDRESS
from electrum.storage import WalletStorage, StorageReadWriteError
from electrum.wallet import Wallet, InternalAddressCorruption
from electrum.util import profiler, InvalidPassword, send_exception_to_crash_reporter
t@@ -398,12 +397,9 @@ class ElectrumWindow(App):
self.set_ln_invoice(data)
return
# try to decode transaction
- from electrum.transaction import Transaction
- from electrum.util import bh2u
+ from electrum.transaction import tx_from_any
try:
- text = bh2u(base_decode(data, None, base=43))
- tx = Transaction(text)
- tx.deserialize()
+ tx = tx_from_any(data)
except:
tx = None
if tx:
t@@ -855,7 +851,7 @@ class ElectrumWindow(App):
self._trigger_update_status()
def get_max_amount(self):
- from electrum.transaction import TxOutput
+ from electrum.transaction import PartialTxOutput
if run_hook('abort_send', self):
return ''
inputs = self.wallet.get_spendable_coins(None)
t@@ -866,9 +862,9 @@ class ElectrumWindow(App):
addr = str(self.send_screen.screen.address)
if not addr:
addr = self.wallet.dummy_address()
- outputs = [TxOutput(TYPE_ADDRESS, addr, '!')]
+ outputs = [PartialTxOutput.from_address_and_value(addr, '!')]
try:
- tx = self.wallet.make_unsigned_transaction(inputs, outputs)
+ tx = self.wallet.make_unsigned_transaction(coins=inputs, outputs=outputs)
except NoDynamicFeeEstimates as e:
Clock.schedule_once(lambda dt, bound_e=e: self.show_error(str(bound_e)))
return ''
t@@ -1199,7 +1195,7 @@ class ElectrumWindow(App):
if not self.wallet.can_export():
return
try:
- key = str(self.wallet.export_private_key(addr, password)[0])
+ key = str(self.wallet.export_private_key(addr, password))
pk_label.data = key
except InvalidPassword:
self.show_error("Invalid PIN")
DIR diff --git a/electrum/gui/kivy/uix/dialogs/__init__.py b/electrum/gui/kivy/uix/dialogs/__init__.py
t@@ -1,3 +1,5 @@
+from typing import TYPE_CHECKING, Sequence
+
from kivy.app import App
from kivy.clock import Clock
from kivy.factory import Factory
t@@ -8,6 +10,9 @@ from kivy.uix.boxlayout import BoxLayout
from electrum.gui.kivy.i18n import _
+if TYPE_CHECKING:
+ from ...main_window import ElectrumWindow
+ from electrum.transaction import TxOutput
class AnimatedPopup(Factory.Popup):
t@@ -202,13 +207,13 @@ class OutputList(RecycleView):
def __init__(self, **kwargs):
super(OutputList, self).__init__(**kwargs)
- self.app = App.get_running_app()
+ self.app = App.get_running_app() # type: ElectrumWindow
- def update(self, outputs):
+ def update(self, outputs: Sequence['TxOutput']):
res = []
for o in outputs:
value = self.app.format_amount_and_units(o.value)
- res.append({'address': o.address, 'value': value})
+ res.append({'address': o.get_ui_address_str(), 'value': value})
self.data = res
DIR diff --git a/electrum/gui/kivy/uix/dialogs/tx_dialog.py b/electrum/gui/kivy/uix/dialogs/tx_dialog.py
t@@ -1,5 +1,6 @@
+import copy
from datetime import datetime
-from typing import NamedTuple, Callable
+from typing import NamedTuple, Callable, TYPE_CHECKING
from kivy.app import App
from kivy.factory import Factory
t@@ -16,6 +17,10 @@ from electrum.gui.kivy.i18n import _
from electrum.util import InvalidPassword
from electrum.address_synchronizer import TX_HEIGHT_LOCAL
from electrum.wallet import CannotBumpFee
+from electrum.transaction import Transaction, PartialTransaction
+
+if TYPE_CHECKING:
+ from ...main_window import ElectrumWindow
Builder.load_string('''
t@@ -121,11 +126,16 @@ class TxDialog(Factory.Popup):
def __init__(self, app, tx):
Factory.Popup.__init__(self)
- self.app = app
+ self.app = app # type: ElectrumWindow
self.wallet = self.app.wallet
- self.tx = tx
+ self.tx = tx # type: Transaction
self._action_button_fn = lambda btn: None
+ # if the wallet can populate the inputs with more info, do it now.
+ # as a result, e.g. we might learn an imported address tx is segwit,
+ # or that a beyond-gap-limit address is is_mine
+ tx.add_info_from_wallet(self.wallet)
+
def on_open(self):
self.update()
t@@ -150,6 +160,7 @@ class TxDialog(Factory.Popup):
self.date_label = ''
self.date_str = ''
+ self.can_sign = self.wallet.can_sign(self.tx)
if amount is None:
self.amount_str = _("Transaction unrelated to your wallet")
elif amount > 0:
t@@ -158,15 +169,18 @@ class TxDialog(Factory.Popup):
else:
self.is_mine = True
self.amount_str = format_amount(-amount)
- if fee is not None:
+ risk_of_burning_coins = (isinstance(self.tx, PartialTransaction)
+ and self.can_sign
+ and fee is not None
+ and self.tx.is_there_risk_of_burning_coins_as_fees())
+ if fee is not None and not risk_of_burning_coins:
self.fee_str = format_amount(fee)
fee_per_kb = fee / self.tx.estimated_size() * 1000
self.feerate_str = self.app.format_fee_rate(fee_per_kb)
else:
self.fee_str = _('unknown')
self.feerate_str = _('unknown')
- self.can_sign = self.wallet.can_sign(self.tx)
- self.ids.output_list.update(self.tx.get_outputs_for_UI())
+ self.ids.output_list.update(self.tx.outputs())
self.is_local_tx = tx_mined_status.height == TX_HEIGHT_LOCAL
self.update_action_button()
t@@ -252,10 +266,15 @@ class TxDialog(Factory.Popup):
def show_qr(self):
from electrum.bitcoin import base_encode, bfh
- raw_tx = str(self.tx)
- text = bfh(raw_tx)
+ original_raw_tx = str(self.tx)
+ tx = copy.deepcopy(self.tx) # make copy as we mutate tx
+ if isinstance(tx, PartialTransaction):
+ # this makes QR codes a lot smaller (or just possible in the first place!)
+ tx.convert_all_utxos_to_witness_utxos()
+
+ text = tx.serialize_as_bytes()
text = base_encode(text, base=43)
- self.app.qr_dialog(_("Raw Transaction"), text, text_for_clipboard=raw_tx)
+ self.app.qr_dialog(_("Raw Transaction"), text, text_for_clipboard=original_raw_tx)
def remove_local_tx(self):
txid = self.tx.txid()
DIR diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
t@@ -22,11 +22,10 @@ from kivy.lang import Builder
from kivy.factory import Factory
from kivy.utils import platform
-from electrum.bitcoin import TYPE_ADDRESS
from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat
from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
from electrum import bitcoin, constants
-from electrum.transaction import TxOutput, Transaction, tx_from_str
+from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput
from electrum.util import send_exception_to_crash_reporter, parse_URI, InvalidBitcoinURI
from electrum.util import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT, TxMinedInfo, get_request_status, pr_expiration_values
from electrum.plugin import run_hook
t@@ -276,8 +275,7 @@ class SendScreen(CScreen):
return
# try to decode as transaction
try:
- raw_tx = tx_from_str(data)
- tx = Transaction(raw_tx)
+ tx = tx_from_any(data)
tx.deserialize()
except:
tx = None
t@@ -313,7 +311,7 @@ class SendScreen(CScreen):
if not bitcoin.is_address(address):
self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
return
- outputs = [TxOutput(TYPE_ADDRESS, address, amount)]
+ outputs = [PartialTxOutput.from_address_and_value(address, amount)]
return self.app.wallet.create_invoice(outputs, message, self.payment_request, self.parsed_URI)
def do_save(self):
t@@ -353,11 +351,11 @@ class SendScreen(CScreen):
def _do_pay_onchain(self, invoice, rbf):
# make unsigned transaction
- outputs = invoice['outputs'] # type: List[TxOutput]
+ outputs = invoice['outputs'] # type: List[PartialTxOutput]
amount = sum(map(lambda x: x.value, outputs))
coins = self.app.wallet.get_spendable_coins(None)
try:
- tx = self.app.wallet.make_unsigned_transaction(coins, outputs, None)
+ tx = self.app.wallet.make_unsigned_transaction(coins=coins, outputs=outputs)
except NotEnoughFunds:
self.app.show_error(_("Not enough funds"))
return
DIR diff --git a/electrum/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py
t@@ -84,16 +84,20 @@ class AddressDialog(WindowModalDialog):
pubkey_e.setReadOnly(True)
vbox.addWidget(pubkey_e)
- try:
- redeem_script = self.wallet.pubkeys_to_redeem_script(pubkeys)
- except BaseException as e:
- redeem_script = None
+ redeem_script = self.wallet.get_redeem_script(address)
if redeem_script:
vbox.addWidget(QLabel(_("Redeem Script") + ':'))
redeem_e = ShowQRTextEdit(text=redeem_script)
redeem_e.addCopyButton(self.app)
vbox.addWidget(redeem_e)
+ witness_script = self.wallet.get_witness_script(address)
+ if witness_script:
+ vbox.addWidget(QLabel(_("Witness Script") + ':'))
+ witness_e = ShowQRTextEdit(text=witness_script)
+ witness_e.addCopyButton(self.app)
+ vbox.addWidget(witness_e)
+
vbox.addWidget(QLabel(_("History")))
addr_hist_model = AddressHistoryModel(self.parent, self.address)
self.hw = HistoryList(self.parent, addr_hist_model)
DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
t@@ -36,7 +36,7 @@ import base64
from functools import partial
import queue
import asyncio
-from typing import Optional, TYPE_CHECKING
+from typing import Optional, TYPE_CHECKING, Sequence, List, Union
from PyQt5.QtGui import QPixmap, QKeySequence, QIcon, QCursor, QFont
from PyQt5.QtCore import Qt, QRect, QStringListModel, QSize, pyqtSignal
t@@ -50,7 +50,7 @@ from PyQt5.QtWidgets import (QMessageBox, QComboBox, QSystemTrayIcon, QTabWidget
import electrum
from electrum import (keystore, simple_config, ecc, constants, util, bitcoin, commands,
coinchooser, paymentrequest)
-from electrum.bitcoin import COIN, is_address, TYPE_ADDRESS
+from electrum.bitcoin import COIN, is_address
from electrum.plugin import run_hook
from electrum.i18n import _
from electrum.util import (format_time, format_satoshis, format_fee_satoshis,
t@@ -64,7 +64,8 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis,
InvalidBitcoinURI, InvoiceError)
from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
from electrum.lnutil import PaymentFailure, SENT, RECEIVED
-from electrum.transaction import Transaction, TxOutput
+from electrum.transaction import (Transaction, PartialTxInput,
+ PartialTransaction, PartialTxOutput)
from electrum.address_synchronizer import AddTransactionException
from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet,
sweep_preparations, InternalAddressCorruption)
t@@ -922,7 +923,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def show_transaction(self, tx, *, invoice=None, tx_desc=None):
'''tx_desc is set only for txs created in the Send tab'''
- show_transaction(tx, self, invoice=invoice, desc=tx_desc)
+ show_transaction(tx, parent=self, invoice=invoice, desc=tx_desc)
def create_receive_tab(self):
# A 4-column grid layout. All the stretch is in the last column.
t@@ -1434,11 +1435,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def update_fee(self):
self.require_fee_update = True
- def get_payto_or_dummy(self):
- r = self.payto_e.get_recipient()
+ def get_payto_or_dummy(self) -> bytes:
+ r = self.payto_e.get_destination_scriptpubkey()
if r:
return r
- return (TYPE_ADDRESS, self.wallet.dummy_address())
+ return bfh(bitcoin.address_to_script(self.wallet.dummy_address()))
def do_update_fee(self):
'''Recalculate the fee. If the fee was manually input, retain it, but
t@@ -1461,13 +1462,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
coins = self.get_coins()
if not outputs:
- _type, addr = self.get_payto_or_dummy()
- outputs = [TxOutput(_type, addr, amount)]
+ scriptpubkey = self.get_payto_or_dummy()
+ outputs = [PartialTxOutput(scriptpubkey=scriptpubkey, value=amount)]
is_sweep = bool(self.tx_external_keypairs)
make_tx = lambda fee_est: \
self.wallet.make_unsigned_transaction(
- coins, outputs,
- fixed_fee=fee_est, is_sweep=is_sweep)
+ coins=coins,
+ outputs=outputs,
+ fee=fee_est,
+ is_sweep=is_sweep)
try:
tx = make_tx(fee_estimator)
self.not_enough_funds = False
t@@ -1546,7 +1549,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
menu.exec_(self.from_list.viewport().mapToGlobal(position))
- def set_pay_from(self, coins):
+ def set_pay_from(self, coins: Sequence[PartialTxInput]):
self.pay_from = list(coins)
self.redraw_from_list()
t@@ -1555,12 +1558,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.from_label.setHidden(len(self.pay_from) == 0)
self.from_list.setHidden(len(self.pay_from) == 0)
- def format(x):
- h = x.get('prevout_hash')
- return h[0:10] + '...' + h[-10:] + ":%d"%x.get('prevout_n') + '\t' + "%s"%x.get('address') + '\t'
+ def format(txin: PartialTxInput):
+ h = txin.prevout.txid.hex()
+ out_idx = txin.prevout.out_idx
+ addr = txin.address
+ return h[0:10] + '...' + h[-10:] + ":%d"%out_idx + '\t' + addr + '\t'
for coin in self.pay_from:
- item = QTreeWidgetItem([format(coin), self.format_amount(coin['value'])])
+ item = QTreeWidgetItem([format(coin), self.format_amount(coin.value_sats())])
item.setFont(0, QFont(MONOSPACE_FONT))
self.from_list.addTopLevelItem(item)
t@@ -1620,14 +1625,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
fee_estimator = None
return fee_estimator
- def read_outputs(self):
+ def read_outputs(self) -> List[PartialTxOutput]:
if self.payment_request:
outputs = self.payment_request.get_outputs()
else:
outputs = self.payto_e.get_outputs(self.max_button.isChecked())
return outputs
- def check_send_tab_onchain_outputs_and_show_errors(self, outputs) -> bool:
+ def check_send_tab_onchain_outputs_and_show_errors(self, outputs: List[PartialTxOutput]) -> bool:
"""Returns whether there are errors with outputs.
Also shows error dialog to user if so.
"""
t@@ -1636,12 +1641,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
return True
for o in outputs:
- if o.address is None:
+ if o.scriptpubkey is None:
self.show_error(_('Bitcoin Address is None'))
return True
- if o.type == TYPE_ADDRESS and not bitcoin.is_address(o.address):
- self.show_error(_('Invalid Bitcoin Address'))
- return True
if o.value is None:
self.show_error(_('Invalid Amount'))
return True
t@@ -1749,20 +1751,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
return
elif invoice['type'] == PR_TYPE_ONCHAIN:
message = invoice['message']
- outputs = invoice['outputs']
+ outputs = invoice['outputs'] # type: List[PartialTxOutput]
else:
raise Exception('unknown invoice type')
if run_hook('abort_send', self):
return
- outputs = [TxOutput(*x) for x in outputs]
+ for txout in outputs:
+ assert isinstance(txout, PartialTxOutput)
fee_estimator = self.get_send_fee_estimator()
coins = self.get_coins()
try:
is_sweep = bool(self.tx_external_keypairs)
tx = self.wallet.make_unsigned_transaction(
- coins, outputs, fixed_fee=fee_estimator,
+ coins=coins,
+ outputs=outputs,
+ fee=fee_estimator,
is_sweep=is_sweep)
except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
self.show_message(str(e))
t@@ -1837,7 +1842,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def sign_tx(self, tx, callback, password):
self.sign_tx_with_password(tx, callback, password)
- def sign_tx_with_password(self, tx, callback, password):
+ def sign_tx_with_password(self, tx: PartialTransaction, callback, password):
'''Sign the transaction in a separate thread. When done, calls
the callback with a success code of True or False.
'''
t@@ -1849,13 +1854,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success
if self.tx_external_keypairs:
# can sign directly
- task = partial(Transaction.sign, tx, self.tx_external_keypairs)
+ task = partial(tx.sign, self.tx_external_keypairs)
else:
task = partial(self.wallet.sign_transaction, tx, password)
msg = _('Signing transaction...')
WaitingDialog(self, msg, task, on_success, on_failure)
- def broadcast_transaction(self, tx, *, invoice=None, tx_desc=None):
+ def broadcast_transaction(self, tx: Transaction, *, invoice=None, tx_desc=None):
def broadcast_thread():
# non-GUI thread
t@@ -1879,7 +1884,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if pr:
self.payment_request = None
refund_address = self.wallet.get_receiving_address()
- coro = pr.send_payment_and_receive_paymentack(str(tx), refund_address)
+ coro = pr.send_payment_and_receive_paymentack(tx.serialize(), refund_address)
fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
ack_status, ack_msg = fut.result(timeout=20)
self.logger.info(f"Payment ACK: {ack_status}. Ack message: {ack_msg}")
t@@ -2077,7 +2082,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.utxo_list.update()
self.update_fee()
- def set_frozen_state_of_coins(self, utxos, freeze: bool):
+ def set_frozen_state_of_coins(self, utxos: Sequence[PartialTxInput], freeze: bool):
self.wallet.set_frozen_state_of_coins(utxos, freeze)
self.utxo_list.update()
self.update_fee()
t@@ -2124,7 +2129,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
else:
return self.wallet.get_spendable_coins(None)
- def spend_coins(self, coins):
+ def spend_coins(self, coins: Sequence[PartialTxInput]):
self.set_pay_from(coins)
self.set_onchain(len(coins) > 0)
self.show_send_tab()
t@@ -2527,7 +2532,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if not address:
return
try:
- pk, redeem_script = self.wallet.export_private_key(address, password)
+ pk = self.wallet.export_private_key(address, password)
except Exception as e:
self.logger.exception('')
self.show_message(repr(e))
t@@ -2542,11 +2547,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
keys_e = ShowQRTextEdit(text=pk)
keys_e.addCopyButton(self.app)
vbox.addWidget(keys_e)
- if redeem_script:
- vbox.addWidget(QLabel(_("Redeem Script") + ':'))
- rds_e = ShowQRTextEdit(text=redeem_script)
- rds_e.addCopyButton(self.app)
- vbox.addWidget(rds_e)
+ # if redeem_script:
+ # vbox.addWidget(QLabel(_("Redeem Script") + ':'))
+ # rds_e = ShowQRTextEdit(text=redeem_script)
+ # rds_e.addCopyButton(self.app)
+ # vbox.addWidget(rds_e)
vbox.addLayout(Buttons(CloseButton(d)))
d.setLayout(vbox)
d.exec_()
t@@ -2718,11 +2723,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
d = PasswordDialog(parent, msg)
return d.run()
- def tx_from_text(self, txt) -> Optional[Transaction]:
- from electrum.transaction import tx_from_str
+ def tx_from_text(self, data: Union[str, bytes]) -> Union[None, 'PartialTransaction', 'Transaction']:
+ from electrum.transaction import tx_from_any
try:
- tx = tx_from_str(txt)
- return Transaction(tx)
+ return tx_from_any(data)
except BaseException as e:
self.show_critical(_("Electrum was unable to parse your transaction") + ":\n" + repr(e))
return
t@@ -2741,25 +2745,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.pay_to_URI(data)
return
# else if the user scanned an offline signed tx
- try:
- data = bh2u(bitcoin.base_decode(data, length=None, base=43))
- except BaseException as e:
- self.show_error((_('Could not decode QR code')+':\n{}').format(repr(e)))
- return
tx = self.tx_from_text(data)
if not tx:
return
self.show_transaction(tx)
def read_tx_from_file(self) -> Optional[Transaction]:
- fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn")
+ fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn;;*.psbt")
if not fileName:
return
try:
with open(fileName, "r") as f:
- file_content = f.read()
+ file_content = f.read() # type: Union[str, bytes]
except (ValueError, IOError, os.error) as reason:
- self.show_critical(_("Electrum was unable to open your transaction file") + "\n" + str(reason), title=_("Unable to read file or no transaction found"))
+ self.show_critical(_("Electrum was unable to open your transaction file") + "\n" + str(reason),
+ title=_("Unable to read file or no transaction found"))
return
return self.tx_from_text(file_content)
t@@ -2831,7 +2831,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
time.sleep(0.1)
if done or cancelled:
break
- privkey = self.wallet.export_private_key(addr, password)[0]
+ privkey = self.wallet.export_private_key(addr, password)
private_keys[addr] = privkey
self.computing_privkeys_signal.emit()
if not cancelled:
t@@ -3130,7 +3130,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
vbox.addLayout(Buttons(CloseButton(d)))
d.exec_()
- def cpfp(self, parent_tx: Transaction, new_tx: Transaction) -> None:
+ def cpfp(self, parent_tx: Transaction, new_tx: PartialTransaction) -> None:
total_size = parent_tx.estimated_size() + new_tx.estimated_size()
parent_txid = parent_tx.txid()
assert parent_txid
t@@ -3257,7 +3257,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
new_tx.set_rbf(False)
self.show_transaction(new_tx, tx_desc=tx_label)
- def save_transaction_into_wallet(self, tx):
+ def save_transaction_into_wallet(self, tx: Transaction):
win = self.top_level_window()
try:
if not self.wallet.add_transaction(tx.txid(), tx):
DIR diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py
t@@ -25,13 +25,13 @@
import re
from decimal import Decimal
-from typing import NamedTuple, Sequence
+from typing import NamedTuple, Sequence, Optional, List
from PyQt5.QtGui import QFontMetrics
from electrum import bitcoin
from electrum.util import bfh
-from electrum.transaction import TxOutput, push_script
+from electrum.transaction import push_script, PartialTxOutput
from electrum.bitcoin import opcodes
from electrum.logging import Logger
from electrum.lnaddr import LnDecodeException
t@@ -65,12 +65,12 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
self.heightMax = 150
self.c = None
self.textChanged.connect(self.check_text)
- self.outputs = []
+ self.outputs = [] # type: List[PartialTxOutput]
self.errors = [] # type: Sequence[PayToLineError]
self.is_pr = False
self.is_alias = False
self.update_size()
- self.payto_address = None
+ self.payto_scriptpubkey = None # type: Optional[bytes]
self.lightning_invoice = None
self.previous_payto = ''
t@@ -86,19 +86,19 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
def setExpired(self):
self.setStyleSheet(util.ColorScheme.RED.as_stylesheet(True))
- def parse_address_and_amount(self, line):
+ def parse_address_and_amount(self, line) -> PartialTxOutput:
x, y = line.split(',')
- out_type, out = self.parse_output(x)
+ scriptpubkey = self.parse_output(x)
amount = self.parse_amount(y)
- return TxOutput(out_type, out, amount)
+ return PartialTxOutput(scriptpubkey=scriptpubkey, value=amount)
- def parse_output(self, x):
+ def parse_output(self, x) -> bytes:
try:
address = self.parse_address(x)
- return bitcoin.TYPE_ADDRESS, address
+ return bfh(bitcoin.address_to_script(address))
except:
script = self.parse_script(x)
- return bitcoin.TYPE_SCRIPT, script
+ return bfh(script)
def parse_script(self, x):
script = ''
t@@ -131,9 +131,9 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
return
# filter out empty lines
lines = [i for i in self.lines() if i]
- outputs = []
+ outputs = [] # type: List[PartialTxOutput]
total = 0
- self.payto_address = None
+ self.payto_scriptpubkey = None
self.lightning_invoice = None
if len(lines) == 1:
data = lines[0]
t@@ -152,10 +152,10 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
self.lightning_invoice = lower
return
try:
- self.payto_address = self.parse_output(data)
+ self.payto_scriptpubkey = self.parse_output(data)
except:
pass
- if self.payto_address:
+ if self.payto_scriptpubkey:
self.win.set_onchain(True)
self.win.lock_amount(False)
return
t@@ -177,7 +177,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
self.win.max_button.setChecked(is_max)
self.outputs = outputs
- self.payto_address = None
+ self.payto_scriptpubkey = None
if self.win.max_button.isChecked():
self.win.do_update_fee()
t@@ -188,18 +188,16 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
def get_errors(self) -> Sequence[PayToLineError]:
return self.errors
- def get_recipient(self):
- return self.payto_address
+ def get_destination_scriptpubkey(self) -> Optional[bytes]:
+ return self.payto_scriptpubkey
def get_outputs(self, is_max):
- if self.payto_address:
+ if self.payto_scriptpubkey:
if is_max:
amount = '!'
else:
amount = self.amount_edit.get_amount()
-
- _type, addr = self.payto_address
- self.outputs = [TxOutput(_type, addr, amount)]
+ self.outputs = [PartialTxOutput(scriptpubkey=self.payto_scriptpubkey, value=amount)]
return self.outputs[:]
DIR diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py
t@@ -26,12 +26,12 @@
import sys
import copy
import datetime
-import json
import traceback
-from typing import TYPE_CHECKING
+import time
+from typing import TYPE_CHECKING, Callable
from PyQt5.QtCore import QSize, Qt
-from PyQt5.QtGui import QTextCharFormat, QBrush, QFont
+from PyQt5.QtGui import QTextCharFormat, QBrush, QFont, QPixmap
from PyQt5.QtWidgets import (QDialog, QLabel, QPushButton, QHBoxLayout, QVBoxLayout,
QTextEdit, QFrame, QAction, QToolButton, QMenu)
import qrcode
t@@ -42,11 +42,12 @@ from electrum.i18n import _
from electrum.plugin import run_hook
from electrum import simple_config
from electrum.util import bfh
-from electrum.transaction import SerializationError, Transaction
+from electrum.transaction import SerializationError, Transaction, PartialTransaction, PartialTxInput
from electrum.logging import get_logger
-from .util import (MessageBoxMixin, read_QIcon, Buttons, CopyButton,
- MONOSPACE_FONT, ColorScheme, ButtonsLineEdit)
+from .util import (MessageBoxMixin, read_QIcon, Buttons, CopyButton, icon_path,
+ MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, text_dialog,
+ char_width_in_lineedit)
if TYPE_CHECKING:
from .main_window import ElectrumWindow
t@@ -60,9 +61,9 @@ _logger = get_logger(__name__)
dialogs = [] # Otherwise python randomly garbage collects the dialogs...
-def show_transaction(tx, parent, *, invoice=None, desc=None, prompt_if_unsaved=False):
+def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', invoice=None, desc=None, prompt_if_unsaved=False):
try:
- d = TxDialog(tx, parent, invoice, desc, prompt_if_unsaved)
+ d = TxDialog(tx, parent=parent, invoice=invoice, desc=desc, prompt_if_unsaved=prompt_if_unsaved)
except SerializationError as e:
_logger.exception('unable to deserialize the transaction')
parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e))
t@@ -73,7 +74,7 @@ def show_transaction(tx, parent, *, invoice=None, desc=None, prompt_if_unsaved=F
class TxDialog(QDialog, MessageBoxMixin):
- def __init__(self, tx: Transaction, parent: 'ElectrumWindow', invoice, desc, prompt_if_unsaved):
+ def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', invoice, desc, prompt_if_unsaved):
'''Transactions in the wallet will show their description.
Pass desc to give a description for txs not yet in the wallet.
'''
t@@ -96,8 +97,8 @@ class TxDialog(QDialog, MessageBoxMixin):
# if the wallet can populate the inputs with more info, do it now.
# as a result, e.g. we might learn an imported address tx is segwit,
- # in which case it's ok to display txid
- tx.add_inputs_info(self.wallet)
+ # or that a beyond-gap-limit address is is_mine
+ tx.add_info_from_wallet(self.wallet)
self.setMinimumWidth(950)
self.setWindowTitle(_("Transaction"))
t@@ -115,7 +116,15 @@ class TxDialog(QDialog, MessageBoxMixin):
self.add_tx_stats(vbox)
vbox.addSpacing(10)
- self.add_io(vbox)
+
+ self.inputs_header = QLabel()
+ vbox.addWidget(self.inputs_header)
+ self.inputs_textedit = QTextEditWithDefaultSize()
+ vbox.addWidget(self.inputs_textedit)
+ self.outputs_header = QLabel()
+ vbox.addWidget(self.outputs_header)
+ self.outputs_textedit = QTextEditWithDefaultSize()
+ vbox.addWidget(self.outputs_textedit)
self.sign_button = b = QPushButton(_("Sign"))
b.clicked.connect(self.sign)
t@@ -136,23 +145,35 @@ class TxDialog(QDialog, MessageBoxMixin):
b.clicked.connect(self.close)
b.setDefault(True)
- export_actions_menu = QMenu()
- action = QAction(_("Copy to clipboard"), self)
- action.triggered.connect(lambda: parent.app.clipboard().setText((lambda: str(self.tx))()))
- export_actions_menu.addAction(action)
- action = QAction(read_QIcon(qr_icon), _("Show as QR code"), self)
- action.triggered.connect(self.show_qr)
- export_actions_menu.addAction(action)
- action = QAction(_("Export to file"), self)
- action.triggered.connect(self.export)
- export_actions_menu.addAction(action)
+ self.export_actions_menu = export_actions_menu = QMenu()
+ self.add_export_actions_to_menu(export_actions_menu)
+ export_actions_menu.addSeparator()
+ if isinstance(tx, PartialTransaction):
+ export_for_coinjoin_submenu = export_actions_menu.addMenu(_("For CoinJoin; strip privates"))
+ self.add_export_actions_to_menu(export_for_coinjoin_submenu, gettx=self._gettx_for_coinjoin)
+
self.export_actions_button = QToolButton()
self.export_actions_button.setText(_("Export"))
self.export_actions_button.setMenu(export_actions_menu)
self.export_actions_button.setPopupMode(QToolButton.InstantPopup)
+ partial_tx_actions_menu = QMenu()
+ ptx_merge_sigs_action = QAction(_("Merge signatures from"), self)
+ ptx_merge_sigs_action.triggered.connect(self.merge_sigs)
+ partial_tx_actions_menu.addAction(ptx_merge_sigs_action)
+ ptx_join_txs_action = QAction(_("Join inputs/outputs"), self)
+ ptx_join_txs_action.triggered.connect(self.join_tx_with_another)
+ partial_tx_actions_menu.addAction(ptx_join_txs_action)
+ self.partial_tx_actions_button = QToolButton()
+ self.partial_tx_actions_button.setText(_("Combine"))
+ self.partial_tx_actions_button.setMenu(partial_tx_actions_menu)
+ self.partial_tx_actions_button.setPopupMode(QToolButton.InstantPopup)
+
# Action buttons
- self.buttons = [self.sign_button, self.broadcast_button, self.cancel_button]
+ self.buttons = []
+ if isinstance(tx, PartialTransaction):
+ self.buttons.append(self.partial_tx_actions_button)
+ self.buttons += [self.sign_button, self.broadcast_button, self.cancel_button]
# Transaction sharing buttons
self.sharing_buttons = [self.export_actions_button, self.save_button]
t@@ -189,8 +210,43 @@ class TxDialog(QDialog, MessageBoxMixin):
# Override escape-key to close normally (and invoke closeEvent)
self.close()
- def show_qr(self):
- text = bfh(str(self.tx))
+ def add_export_actions_to_menu(self, menu: QMenu, *, gettx: Callable[[], Transaction] = None) -> None:
+ if gettx is None:
+ gettx = lambda: None
+
+ action = QAction(_("Copy to clipboard"), self)
+ action.triggered.connect(lambda: self.copy_to_clipboard(tx=gettx()))
+ menu.addAction(action)
+
+ qr_icon = "qrcode_white.png" if ColorScheme.dark_scheme else "qrcode.png"
+ action = QAction(read_QIcon(qr_icon), _("Show as QR code"), self)
+ action.triggered.connect(lambda: self.show_qr(tx=gettx()))
+ menu.addAction(action)
+
+ action = QAction(_("Export to file"), self)
+ action.triggered.connect(lambda: self.export_to_file(tx=gettx()))
+ menu.addAction(action)
+
+ def _gettx_for_coinjoin(self) -> PartialTransaction:
+ if not isinstance(self.tx, PartialTransaction):
+ raise Exception("Can only export partial transactions for coinjoins.")
+ tx = copy.deepcopy(self.tx)
+ tx.prepare_for_export_for_coinjoin()
+ return tx
+
+ def copy_to_clipboard(self, *, tx: Transaction = None):
+ if tx is None:
+ tx = self.tx
+ self.main_window.app.clipboard().setText(str(tx))
+
+ def show_qr(self, *, tx: Transaction = None):
+ if tx is None:
+ tx = self.tx
+ tx = copy.deepcopy(tx) # make copy as we mutate tx
+ if isinstance(tx, PartialTransaction):
+ # this makes QR codes a lot smaller (or just possible in the first place!)
+ tx.convert_all_utxos_to_witness_utxos()
+ text = tx.serialize_as_bytes()
text = base_encode(text, base=43)
try:
self.main_window.show_qrcode(text, 'Transaction', parent=self)
t@@ -222,17 +278,68 @@ class TxDialog(QDialog, MessageBoxMixin):
self.saved = True
self.main_window.pop_top_level_window(self)
-
- def export(self):
- name = 'signed_%s.txn' % (self.tx.txid()[0:8]) if self.tx.is_complete() else 'unsigned.txn'
- fileName = self.main_window.getSaveFileName(_("Select where to save your signed transaction"), name, "*.txn")
- if fileName:
+ def export_to_file(self, *, tx: Transaction = None):
+ if tx is None:
+ tx = self.tx
+ if isinstance(tx, PartialTransaction):
+ tx.finalize_psbt()
+ if tx.is_complete():
+ name = 'signed_%s.txn' % (tx.txid()[0:8])
+ else:
+ name = self.wallet.basename() + time.strftime('-%Y%m%d-%H%M.psbt')
+ fileName = self.main_window.getSaveFileName(_("Select where to save your signed transaction"), name, "*.txn;;*.psbt")
+ if not fileName:
+ return
+ if tx.is_complete(): # network tx hex
with open(fileName, "w+") as f:
- f.write(json.dumps(self.tx.as_dict(), indent=4) + '\n')
- self.show_message(_("Transaction exported successfully"))
- self.saved = True
+ network_tx_hex = tx.serialize_to_network()
+ f.write(network_tx_hex + '\n')
+ else: # if partial: PSBT bytes
+ assert isinstance(tx, PartialTransaction)
+ with open(fileName, "wb+") as f:
+ f.write(tx.serialize_as_bytes())
+
+ self.show_message(_("Transaction exported successfully"))
+ self.saved = True
+
+ def merge_sigs(self):
+ if not isinstance(self.tx, PartialTransaction):
+ return
+ text = text_dialog(self, _('Input raw transaction'),
+ _("Transaction to merge signatures from") + ":",
+ _("Load transaction"))
+ if not text:
+ return
+ tx = self.main_window.tx_from_text(text)
+ if not tx:
+ return
+ try:
+ self.tx.combine_with_other_psbt(tx)
+ except Exception as e:
+ self.show_error(_("Error combining partial transactions") + ":\n" + repr(e))
+ return
+ self.update()
+
+ def join_tx_with_another(self):
+ if not isinstance(self.tx, PartialTransaction):
+ return
+ text = text_dialog(self, _('Input raw transaction'),
+ _("Transaction to join with") + " (" + _("add inputs and outputs") + "):",
+ _("Load transaction"))
+ if not text:
+ return
+ tx = self.main_window.tx_from_text(text)
+ if not tx:
+ return
+ try:
+ self.tx.join_with_other_psbt(tx)
+ except Exception as e:
+ self.show_error(_("Error joining partial transactions") + ":\n" + repr(e))
+ return
+ self.update()
def update(self):
+ self.update_io()
desc = self.desc
base_unit = self.main_window.base_unit()
format_amount = self.main_window.format_amount
t@@ -287,13 +394,17 @@ class TxDialog(QDialog, MessageBoxMixin):
feerate_warning = simple_config.FEERATE_WARNING_HIGH_FEE
if fee_rate > feerate_warning:
fee_str += ' - ' + _('Warning') + ': ' + _("high fee") + '!'
+ if isinstance(self.tx, PartialTransaction):
+ risk_of_burning_coins = (can_sign and fee is not None
+ and self.tx.is_there_risk_of_burning_coins_as_fees())
+ self.fee_warning_icon.setVisible(risk_of_burning_coins)
self.amount_label.setText(amount_str)
self.fee_label.setText(fee_str)
self.size_label.setText(size_str)
run_hook('transaction_dialog_update', self)
- def add_io(self, vbox):
- vbox.addWidget(QLabel(_("Inputs") + ' (%d)'%len(self.tx.inputs())))
+ def update_io(self):
+ self.inputs_header.setText(_("Inputs") + ' (%d)'%len(self.tx.inputs()))
ext = QTextCharFormat()
rec = QTextCharFormat()
rec.setBackground(QBrush(ColorScheme.GREEN.as_color(background=True)))
t@@ -315,39 +426,39 @@ class TxDialog(QDialog, MessageBoxMixin):
def format_amount(amt):
return self.main_window.format_amount(amt, whitespaces=True)
- i_text = QTextEditWithDefaultSize()
+ i_text = self.inputs_textedit
+ i_text.clear()
i_text.setFont(QFont(MONOSPACE_FONT))
i_text.setReadOnly(True)
cursor = i_text.textCursor()
- for x in self.tx.inputs():
- if x['type'] == 'coinbase':
+ for txin in self.tx.inputs():
+ if txin.is_coinbase():
cursor.insertText('coinbase')
else:
- prevout_hash = x.get('prevout_hash')
- prevout_n = x.get('prevout_n')
+ prevout_hash = txin.prevout.txid.hex()
+ prevout_n = txin.prevout.out_idx
cursor.insertText(prevout_hash + ":%-4d " % prevout_n, ext)
- addr = self.wallet.get_txin_address(x)
+ addr = self.wallet.get_txin_address(txin)
if addr is None:
addr = ''
cursor.insertText(addr, text_format(addr))
- if x.get('value'):
- cursor.insertText(format_amount(x['value']), ext)
+ if isinstance(txin, PartialTxInput) and txin.value_sats() is not None:
+ cursor.insertText(format_amount(txin.value_sats()), ext)
cursor.insertBlock()
- vbox.addWidget(i_text)
- vbox.addWidget(QLabel(_("Outputs") + ' (%d)'%len(self.tx.outputs())))
- o_text = QTextEditWithDefaultSize()
+ self.outputs_header.setText(_("Outputs") + ' (%d)'%len(self.tx.outputs()))
+ o_text = self.outputs_textedit
+ o_text.clear()
o_text.setFont(QFont(MONOSPACE_FONT))
o_text.setReadOnly(True)
cursor = o_text.textCursor()
- for o in self.tx.get_outputs_for_UI():
- addr, v = o.address, o.value
+ for o in self.tx.outputs():
+ addr, v = o.get_ui_address_str(), o.value
cursor.insertText(addr, text_format(addr))
if v is not None:
cursor.insertText('\t', ext)
cursor.insertText(format_amount(v), ext)
cursor.insertBlock()
- vbox.addWidget(o_text)
def add_tx_stats(self, vbox):
hbox_stats = QHBoxLayout()
t@@ -362,8 +473,24 @@ class TxDialog(QDialog, MessageBoxMixin):
vbox_left.addWidget(self.date_label)
self.amount_label = TxDetailLabel()
vbox_left.addWidget(self.amount_label)
+
+ fee_hbox = QHBoxLayout()
self.fee_label = TxDetailLabel()
- vbox_left.addWidget(self.fee_label)
+ fee_hbox.addWidget(self.fee_label)
+ self.fee_warning_icon = QLabel()
+ pixmap = QPixmap(icon_path("warning"))
+ pixmap_size = round(2 * char_width_in_lineedit())
+ pixmap = pixmap.scaled(pixmap_size, pixmap_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
+ self.fee_warning_icon.setPixmap(pixmap)
+ self.fee_warning_icon.setToolTip(_("Warning") + ": "
+ + _("The fee could not be verified. Signing non-segwit inputs is risky:\n"
+ "if this transaction was maliciously modified before you sign,\n"
+ "you might end up paying a higher mining fee than displayed."))
+ self.fee_warning_icon.setVisible(False)
+ fee_hbox.addWidget(self.fee_warning_icon)
+ fee_hbox.addStretch(1)
+ vbox_left.addLayout(fee_hbox)
+
vbox_left.addStretch(1)
hbox_stats.addLayout(vbox_left, 50)
DIR diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py
t@@ -840,13 +840,16 @@ def export_meta_gui(electrum_window, title, exporter):
def get_parent_main_window(widget):
"""Returns a reference to the ElectrumWindow this widget belongs to."""
from .main_window import ElectrumWindow
+ from .transaction_dialog import TxDialog
for _ in range(100):
if widget is None:
return None
- if not isinstance(widget, ElectrumWindow):
- widget = widget.parentWidget()
- else:
+ if isinstance(widget, ElectrumWindow):
return widget
+ elif isinstance(widget, TxDialog):
+ return widget.main_window
+ else:
+ widget = widget.parentWidget()
return None
DIR diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py
t@@ -23,7 +23,7 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
-from typing import Optional, List
+from typing import Optional, List, Dict
from enum import IntEnum
from PyQt5.QtCore import Qt
t@@ -31,9 +31,11 @@ from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont
from PyQt5.QtWidgets import QAbstractItemView, QMenu
from electrum.i18n import _
+from electrum.transaction import PartialTxInput
from .util import MyTreeView, ColorScheme, MONOSPACE_FONT
+
class UTXOList(MyTreeView):
class Columns(IntEnum):
t@@ -64,21 +66,21 @@ class UTXOList(MyTreeView):
def update(self):
self.wallet = self.parent.wallet
utxos = self.wallet.get_utxos()
- self.utxo_dict = {}
+ self.utxo_dict = {} # type: Dict[str, PartialTxInput]
self.model().clear()
self.update_headers(self.__class__.headers)
- for idx, x in enumerate(utxos):
- self.insert_utxo(idx, x)
+ for idx, utxo in enumerate(utxos):
+ self.insert_utxo(idx, utxo)
self.filter()
- def insert_utxo(self, idx, x):
- address = x['address']
- height = x.get('height')
- name = x.get('prevout_hash') + ":%d"%x.get('prevout_n')
- name_short = x.get('prevout_hash')[:16] + '...' + ":%d"%x.get('prevout_n')
- self.utxo_dict[name] = x
- label = self.wallet.get_label(x.get('prevout_hash'))
- amount = self.parent.format_amount(x['value'], whitespaces=True)
+ def insert_utxo(self, idx, utxo: PartialTxInput):
+ address = utxo.address
+ height = utxo.block_height
+ name = utxo.prevout.to_str()
+ name_short = utxo.prevout.txid.hex()[:16] + '...' + ":%d" % utxo.prevout.out_idx
+ self.utxo_dict[name] = utxo
+ label = self.wallet.get_label(utxo.prevout.txid.hex())
+ amount = self.parent.format_amount(utxo.value_sats(), whitespaces=True)
labels = [name_short, address, label, amount, '%d'%height]
utxo_item = [QStandardItem(x) for x in labels]
self.set_editability(utxo_item)
t@@ -89,7 +91,7 @@ class UTXOList(MyTreeView):
if self.wallet.is_frozen_address(address):
utxo_item[self.Columns.ADDRESS].setBackground(ColorScheme.BLUE.as_color(True))
utxo_item[self.Columns.ADDRESS].setToolTip(_('Address is frozen'))
- if self.wallet.is_frozen_coin(x):
+ if self.wallet.is_frozen_coin(utxo):
utxo_item[self.Columns.OUTPOINT].setBackground(ColorScheme.BLUE.as_color(True))
utxo_item[self.Columns.OUTPOINT].setToolTip(f"{name}\n{_('Coin is frozen')}")
else:
t@@ -114,26 +116,26 @@ class UTXOList(MyTreeView):
menu.addAction(_("Spend"), lambda: self.parent.spend_coins(coins))
assert len(coins) >= 1, len(coins)
if len(coins) == 1:
- utxo_dict = coins[0]
- addr = utxo_dict['address']
- txid = utxo_dict['prevout_hash']
+ utxo = coins[0]
+ addr = utxo.address
+ txid = utxo.prevout.txid.hex()
# "Details"
tx = self.wallet.db.get_transaction(txid)
if tx:
label = self.wallet.get_label(txid) or None # Prefer None if empty (None hides the Description: field in the window)
- menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, label))
+ menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, tx_desc=label))
# "Copy ..."
idx = self.indexAt(position)
if not idx.isValid():
return
self.add_copy_menu(menu, idx)
# "Freeze coin"
- if not self.wallet.is_frozen_coin(utxo_dict):
- menu.addAction(_("Freeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo_dict], True))
+ if not self.wallet.is_frozen_coin(utxo):
+ menu.addAction(_("Freeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo], True))
else:
menu.addSeparator()
menu.addAction(_("Coin is frozen"), lambda: None).setEnabled(False)
- menu.addAction(_("Unfreeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo_dict], False))
+ menu.addAction(_("Unfreeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo], False))
menu.addSeparator()
# "Freeze address"
if not self.wallet.is_frozen_address(addr):
t@@ -146,9 +148,9 @@ class UTXOList(MyTreeView):
else:
# multiple items selected
menu.addSeparator()
- addrs = [utxo_dict['address'] for utxo_dict in coins]
- is_coin_frozen = [self.wallet.is_frozen_coin(utxo_dict) for utxo_dict in coins]
- is_addr_frozen = [self.wallet.is_frozen_address(utxo_dict['address']) for utxo_dict in coins]
+ addrs = [utxo.address for utxo in coins]
+ is_coin_frozen = [self.wallet.is_frozen_coin(utxo) for utxo in coins]
+ is_addr_frozen = [self.wallet.is_frozen_address(utxo.address) for utxo in coins]
if not all(is_coin_frozen):
menu.addAction(_("Freeze Coins"), lambda: self.parent.set_frozen_state_of_coins(coins, True))
if any(is_coin_frozen):
DIR diff --git a/electrum/gui/stdio.py b/electrum/gui/stdio.py
t@@ -5,8 +5,8 @@ import logging
from electrum import WalletStorage, Wallet
from electrum.util import format_satoshis
-from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS
-from electrum.transaction import TxOutput
+from electrum.bitcoin import is_address, COIN
+from electrum.transaction import PartialTxOutput
from electrum.network import TxBroadcastError, BestEffortRequestFailed
from electrum.logging import console_stderr_handler
t@@ -197,8 +197,9 @@ class ElectrumGui:
if c == "n": return
try:
- tx = self.wallet.mktx([TxOutput(TYPE_ADDRESS, self.str_recipient, amount)],
- password, self.config, fee)
+ tx = self.wallet.mktx(outputs=[PartialTxOutput.from_address_and_value(self.str_recipient, amount)],
+ password=password,
+ fee=fee)
except Exception as e:
print(repr(e))
return
DIR diff --git a/electrum/gui/text.py b/electrum/gui/text.py
t@@ -9,8 +9,8 @@ import logging
import electrum
from electrum.util import format_satoshis
-from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS
-from electrum.transaction import TxOutput
+from electrum.bitcoin import is_address, COIN
+from electrum.transaction import PartialTxOutput
from electrum.wallet import Wallet
from electrum.storage import WalletStorage
from electrum.network import NetworkParameters, TxBroadcastError, BestEffortRequestFailed
t@@ -360,8 +360,9 @@ class ElectrumGui:
else:
password = None
try:
- tx = self.wallet.mktx([TxOutput(TYPE_ADDRESS, self.str_recipient, amount)],
- password, self.config, fee)
+ tx = self.wallet.mktx(outputs=[PartialTxOutput.from_address_and_value(self.str_recipient, amount)],
+ password=password,
+ fee=fee)
except Exception as e:
self.show_message(repr(e))
return
DIR diff --git a/electrum/json_db.py b/electrum/json_db.py
t@@ -28,7 +28,7 @@ import json
import copy
import threading
from collections import defaultdict
-from typing import Dict, Optional, List, Tuple, Set, Iterable, NamedTuple
+from typing import Dict, Optional, List, Tuple, Set, Iterable, NamedTuple, Sequence
from . import util, bitcoin
from .util import profiler, WalletFileException, multisig_type, TxMinedInfo
t@@ -40,15 +40,11 @@ from .logging import Logger
OLD_SEED_VERSION = 4 # electrum versions < 2.0
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
-FINAL_SEED_VERSION = 19 # electrum >= 2.7 will set this to prevent
+FINAL_SEED_VERSION = 20 # electrum >= 2.7 will set this to prevent
# old versions from overwriting new format
-class JsonDBJsonEncoder(util.MyEncoder):
- def default(self, obj):
- if isinstance(obj, Transaction):
- return str(obj)
- return super().default(obj)
+JsonDBJsonEncoder = util.MyEncoder
class TxFeesValue(NamedTuple):
t@@ -217,6 +213,7 @@ class JsonDB(Logger):
self._convert_version_17()
self._convert_version_18()
self._convert_version_19()
+ self._convert_version_20()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
self._after_upgrade_tasks()
t@@ -425,10 +422,10 @@ class JsonDB(Logger):
for txid, raw_tx in transactions.items():
tx = Transaction(raw_tx)
for txin in tx.inputs():
- if txin['type'] == 'coinbase':
+ if txin.is_coinbase():
continue
- prevout_hash = txin['prevout_hash']
- prevout_n = txin['prevout_n']
+ prevout_hash = txin.prevout.txid.hex()
+ prevout_n = txin.prevout.out_idx
spent_outpoints[prevout_hash][str(prevout_n)] = txid
self.put('spent_outpoints', spent_outpoints)
t@@ -448,10 +445,45 @@ class JsonDB(Logger):
self.put('tx_fees', None)
self.put('seed_version', 19)
- # def _convert_version_20(self):
- # TODO for "next" upgrade:
- # - move "pw_hash_version" from keystore to storage
- # pass
+ def _convert_version_20(self):
+ # store 'derivation' (prefix) and 'root_fingerprint' in all xpub-based keystores.
+ # store explicit None values if we cannot retroactively determine them
+ if not self._is_upgrade_method_needed(19, 19):
+ return
+
+ from .bip32 import BIP32Node, convert_bip32_intpath_to_strpath
+ # note: This upgrade method reimplements bip32.root_fp_and_der_prefix_from_xkey.
+ # This is done deliberately, to avoid introducing that method as a dependency to this upgrade.
+ for ks_name in ('keystore', *['x{}/'.format(i) for i in range(1, 16)]):
+ ks = self.get(ks_name, None)
+ if ks is None: continue
+ xpub = ks.get('xpub', None)
+ if xpub is None: continue
+ bip32node = BIP32Node.from_xkey(xpub)
+ # derivation prefix
+ derivation_prefix = ks.get('derivation', None)
+ if derivation_prefix is None:
+ assert bip32node.depth >= 0, bip32node.depth
+ if bip32node.depth == 0:
+ derivation_prefix = 'm'
+ elif bip32node.depth == 1:
+ child_number_int = int.from_bytes(bip32node.child_number, 'big')
+ derivation_prefix = convert_bip32_intpath_to_strpath([child_number_int])
+ ks['derivation'] = derivation_prefix
+ # root fingerprint
+ root_fingerprint = ks.get('ckcc_xfp', None)
+ if root_fingerprint is not None:
+ root_fingerprint = root_fingerprint.to_bytes(4, byteorder="little", signed=False).hex().lower()
+ if root_fingerprint is None:
+ if bip32node.depth == 0:
+ root_fingerprint = bip32node.calc_fingerprint_of_this_node().hex().lower()
+ elif bip32node.depth == 1:
+ root_fingerprint = bip32node.fingerprint.hex()
+ ks['root_fingerprint'] = root_fingerprint
+ ks.pop('ckcc_xfp', None)
+ self.put(ks_name, ks)
+
+ self.put('seed_version', 20)
def _convert_imported(self):
if not self._is_upgrade_method_needed(0, 13):
t@@ -758,16 +790,16 @@ class JsonDB(Logger):
@modifier
def add_change_address(self, addr):
- self._addr_to_addr_index[addr] = (True, len(self.change_addresses))
+ self._addr_to_addr_index[addr] = (1, len(self.change_addresses))
self.change_addresses.append(addr)
@modifier
def add_receiving_address(self, addr):
- self._addr_to_addr_index[addr] = (False, len(self.receiving_addresses))
+ self._addr_to_addr_index[addr] = (0, len(self.receiving_addresses))
self.receiving_addresses.append(addr)
@locked
- def get_address_index(self, address):
+ def get_address_index(self, address) -> Optional[Sequence[int]]:
return self._addr_to_addr_index.get(address)
@modifier
t@@ -801,11 +833,11 @@ class JsonDB(Logger):
self.data['addresses'][name] = []
self.change_addresses = self.data['addresses']['change']
self.receiving_addresses = self.data['addresses']['receiving']
- self._addr_to_addr_index = {} # key: address, value: (is_change, index)
+ self._addr_to_addr_index = {} # type: Dict[str, Sequence[int]] # key: address, value: (is_change, index)
for i, addr in enumerate(self.receiving_addresses):
- self._addr_to_addr_index[addr] = (False, i)
+ self._addr_to_addr_index[addr] = (0, i)
for i, addr in enumerate(self.change_addresses):
- self._addr_to_addr_index[addr] = (True, i)
+ self._addr_to_addr_index[addr] = (1, i)
@profiler
def _load_transactions(self):
DIR diff --git a/electrum/keystore.py b/electrum/keystore.py
t@@ -26,16 +26,17 @@
from unicodedata import normalize
import hashlib
-from typing import Tuple, TYPE_CHECKING, Union, Sequence
+import re
+from typing import Tuple, TYPE_CHECKING, Union, Sequence, Optional, Dict, List, NamedTuple
from . import bitcoin, ecc, constants, bip32
-from .bitcoin import (deserialize_privkey, serialize_privkey,
- public_key_to_p2pkh)
+from .bitcoin import deserialize_privkey, serialize_privkey
from .bip32 import (convert_bip32_path_to_list_of_uint32, BIP32_PRIME,
- is_xpub, is_xprv, BIP32Node)
+ is_xpub, is_xprv, BIP32Node, normalize_bip32_derivation,
+ convert_bip32_intpath_to_strpath)
from .ecc import string_to_number, number_to_string
from .crypto import (pw_decode, pw_encode, sha256, sha256d, PW_HASH_VERSION_LATEST,
- SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion)
+ SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion, hash_160)
from .util import (InvalidPassword, WalletFileException,
BitcoinException, bh2u, bfh, inv_dict)
from .mnemonic import Mnemonic, load_wordlist, seed_type, is_seed
t@@ -43,13 +44,14 @@ from .plugin import run_hook
from .logging import Logger
if TYPE_CHECKING:
- from .transaction import Transaction
+ from .transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
class KeyStore(Logger):
def __init__(self):
Logger.__init__(self)
+ self.is_requesting_to_be_rewritten_to_wallet_file = False # type: bool
def has_seed(self):
return False
t@@ -67,25 +69,19 @@ class KeyStore(Logger):
"""Returns whether the keystore can be encrypted with a password."""
raise NotImplementedError()
- def get_tx_derivations(self, tx):
+ def get_tx_derivations(self, tx: 'PartialTransaction') -> Dict[str, Union[Sequence[int], str]]:
keypairs = {}
for txin in tx.inputs():
- num_sig = txin.get('num_sig')
- if num_sig is None:
+ if txin.is_complete():
continue
- x_signatures = txin['signatures']
- signatures = [sig for sig in x_signatures if sig]
- if len(signatures) == num_sig:
- # input is complete
- continue
- for k, x_pubkey in enumerate(txin['x_pubkeys']):
- if x_signatures[k] is not None:
+ for pubkey in txin.pubkeys:
+ if pubkey in txin.part_sigs:
# this pubkey already signed
continue
- derivation = self.get_pubkey_derivation(x_pubkey)
+ derivation = self.get_pubkey_derivation(pubkey, txin)
if not derivation:
continue
- keypairs[x_pubkey] = derivation
+ keypairs[pubkey.hex()] = derivation
return keypairs
def can_sign(self, tx):
t@@ -108,9 +104,64 @@ class KeyStore(Logger):
def decrypt_message(self, sequence, message, password) -> bytes:
raise NotImplementedError() # implemented by subclasses
- def sign_transaction(self, tx: 'Transaction', password) -> None:
+ def sign_transaction(self, tx: 'PartialTransaction', password) -> None:
raise NotImplementedError() # implemented by subclasses
+ def get_pubkey_derivation(self, pubkey: bytes,
+ txinout: Union['PartialTxInput', 'PartialTxOutput'],
+ *, only_der_suffix=True) \
+ -> Union[Sequence[int], str, None]:
+ """Returns either a derivation int-list if the pubkey can be HD derived from this keystore,
+ the pubkey itself (hex) if the pubkey belongs to the keystore but not HD derived,
+ or None if the pubkey is unrelated.
+ """
+ def test_der_suffix_against_pubkey(der_suffix: Sequence[int], pubkey: bytes) -> bool:
+ if len(der_suffix) != 2:
+ return False
+ if pubkey.hex() != self.derive_pubkey(*der_suffix):
+ return False
+ return True
+
+ if hasattr(self, 'get_root_fingerprint'):
+ if pubkey not in txinout.bip32_paths:
+ return None
+ fp_found, path_found = txinout.bip32_paths[pubkey]
+ der_suffix = None
+ full_path = None
+ # try fp against our root
+ my_root_fingerprint_hex = self.get_root_fingerprint()
+ my_der_prefix_str = self.get_derivation_prefix()
+ ks_der_prefix = convert_bip32_path_to_list_of_uint32(my_der_prefix_str) if my_der_prefix_str else None
+ if (my_root_fingerprint_hex is not None and ks_der_prefix is not None and
+ fp_found.hex() == my_root_fingerprint_hex):
+ if path_found[:len(ks_der_prefix)] == ks_der_prefix:
+ der_suffix = path_found[len(ks_der_prefix):]
+ if not test_der_suffix_against_pubkey(der_suffix, pubkey):
+ der_suffix = None
+ # try fp against our intermediate fingerprint
+ if (der_suffix is None and hasattr(self, 'xpub') and
+ fp_found == BIP32Node.from_xkey(self.xpub).calc_fingerprint_of_this_node()):
+ der_suffix = path_found
+ if not test_der_suffix_against_pubkey(der_suffix, pubkey):
+ der_suffix = None
+ if der_suffix is None:
+ return None
+ if ks_der_prefix is not None:
+ full_path = ks_der_prefix + list(der_suffix)
+ return der_suffix if only_der_suffix else full_path
+ return None
+
+ def find_my_pubkey_in_txinout(
+ self, txinout: Union['PartialTxInput', 'PartialTxOutput'],
+ *, only_der_suffix: bool = False
+ ) -> Tuple[Optional[bytes], Optional[List[int]]]:
+ # note: we assume that this cosigner only has one pubkey in this txin/txout
+ for pubkey in txinout.bip32_paths:
+ path = self.get_pubkey_derivation(pubkey, txinout, only_der_suffix=only_der_suffix)
+ if path and not isinstance(path, (str, bytes)):
+ return pubkey, list(path)
+ return None, None
+
class Software_KeyStore(KeyStore):
t@@ -210,14 +261,10 @@ class Imported_KeyStore(Software_KeyStore):
raise InvalidPassword()
return privkey, compressed
- def get_pubkey_derivation(self, x_pubkey):
- if x_pubkey[0:2] in ['02', '03', '04']:
- if x_pubkey in self.keypairs.keys():
- return x_pubkey
- elif x_pubkey[0:2] == 'fd':
- addr = bitcoin.script_to_address(x_pubkey[2:])
- if addr in self.addresses:
- return self.addresses[addr].get('pubkey')
+ def get_pubkey_derivation(self, pubkey, txin, *, only_der_suffix=True):
+ if pubkey.hex() in self.keypairs:
+ return pubkey.hex()
+ return None
def update_password(self, old_password, new_password):
self.check_password(old_password)
t@@ -230,7 +277,6 @@ class Imported_KeyStore(Software_KeyStore):
self.pw_hash_version = PW_HASH_VERSION_LATEST
-
class Deterministic_KeyStore(Software_KeyStore):
def __init__(self, d):
t@@ -277,15 +323,85 @@ class Deterministic_KeyStore(Software_KeyStore):
class Xpub:
- def __init__(self):
+ def __init__(self, *, derivation_prefix: str = None, root_fingerprint: str = None):
self.xpub = None
self.xpub_receive = None
self.xpub_change = None
+ # "key origin" info (subclass should persist these):
+ self._derivation_prefix = derivation_prefix # type: Optional[str]
+ self._root_fingerprint = root_fingerprint # type: Optional[str]
+
def get_master_public_key(self):
return self.xpub
- def derive_pubkey(self, for_change, n):
+ def get_derivation_prefix(self) -> Optional[str]:
+ """Returns to bip32 path from some root node to self.xpub
+ Note that the return value might be None; if it is unknown.
+ """
+ return self._derivation_prefix
+
+ def get_root_fingerprint(self) -> Optional[str]:
+ """Returns the bip32 fingerprint of the top level node.
+ This top level node is the node at the beginning of the derivation prefix,
+ i.e. applying the derivation prefix to it will result self.xpub
+ Note that the return value might be None; if it is unknown.
+ """
+ return self._root_fingerprint
+
+ def get_fp_and_derivation_to_be_used_in_partial_tx(self, der_suffix: Sequence[int], *,
+ only_der_suffix: bool = True) -> Tuple[bytes, Sequence[int]]:
+ """Returns fingerprint and derivation path corresponding to a derivation suffix.
+ The fingerprint is either the root fp or the intermediate fp, depending on what is available
+ and 'only_der_suffix', and the derivation path is adjusted accordingly.
+ """
+ fingerprint_hex = self.get_root_fingerprint()
+ der_prefix_str = self.get_derivation_prefix()
+ if not only_der_suffix and fingerprint_hex is not None and der_prefix_str is not None:
+ # use root fp, and true full path
+ fingerprint_bytes = bfh(fingerprint_hex)
+ der_prefix_ints = convert_bip32_path_to_list_of_uint32(der_prefix_str)
+ else:
+ # use intermediate fp, and claim der suffix is the full path
+ fingerprint_bytes = BIP32Node.from_xkey(self.xpub).calc_fingerprint_of_this_node()
+ der_prefix_ints = convert_bip32_path_to_list_of_uint32('m')
+ der_full = der_prefix_ints + list(der_suffix)
+ return fingerprint_bytes, der_full
+
+ def get_xpub_to_be_used_in_partial_tx(self, *, only_der_suffix: bool) -> str:
+ assert self.xpub
+ fp_bytes, der_full = self.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix=[],
+ only_der_suffix=only_der_suffix)
+ bip32node = BIP32Node.from_xkey(self.xpub)
+ depth = len(der_full)
+ child_number_int = der_full[-1] if len(der_full) >= 1 else 0
+ child_number_bytes = child_number_int.to_bytes(length=4, byteorder="big")
+ fingerprint = bytes(4) if depth == 0 else bip32node.fingerprint
+ bip32node = bip32node._replace(depth=depth,
+ fingerprint=fingerprint,
+ child_number=child_number_bytes)
+ return bip32node.to_xpub()
+
+ def add_key_origin_from_root_node(self, *, derivation_prefix: str, root_node: BIP32Node):
+ assert self.xpub
+ # try to derive ourselves from what we were given
+ child_node1 = root_node.subkey_at_private_derivation(derivation_prefix)
+ child_pubkey_bytes1 = child_node1.eckey.get_public_key_bytes(compressed=True)
+ child_node2 = BIP32Node.from_xkey(self.xpub)
+ child_pubkey_bytes2 = child_node2.eckey.get_public_key_bytes(compressed=True)
+ if child_pubkey_bytes1 != child_pubkey_bytes2:
+ raise Exception("(xpub, derivation_prefix, root_node) inconsistency")
+ self.add_key_origin(derivation_prefix=derivation_prefix,
+ root_fingerprint=root_node.calc_fingerprint_of_this_node().hex().lower())
+
+ def add_key_origin(self, *, derivation_prefix: Optional[str], root_fingerprint: Optional[str]):
+ assert self.xpub
+ self._root_fingerprint = root_fingerprint
+ self._derivation_prefix = normalize_bip32_derivation(derivation_prefix)
+
+ def derive_pubkey(self, for_change, n) -> str:
+ for_change = int(for_change)
+ assert for_change in (0, 1)
xpub = self.xpub_change if for_change else self.xpub_receive
if xpub is None:
rootnode = BIP32Node.from_xkey(self.xpub)
t@@ -301,54 +417,13 @@ class Xpub:
node = BIP32Node.from_xkey(xpub).subkey_at_public_derivation(sequence)
return node.eckey.get_public_key_hex(compressed=True)
- def get_xpubkey(self, c, i):
- def encode_path_int(path_int) -> str:
- if path_int < 0xffff:
- hex = bitcoin.int_to_hex(path_int, 2)
- else:
- hex = 'ffff' + bitcoin.int_to_hex(path_int, 4)
- return hex
- s = ''.join(map(encode_path_int, (c, i)))
- return 'ff' + bh2u(bitcoin.DecodeBase58Check(self.xpub)) + s
-
- @classmethod
- def parse_xpubkey(self, pubkey):
- # type + xpub + derivation
- assert pubkey[0:2] == 'ff'
- pk = bfh(pubkey)
- # xpub:
- pk = pk[1:]
- xkey = bitcoin.EncodeBase58Check(pk[0:78])
- # derivation:
- dd = pk[78:]
- s = []
- while dd:
- # 2 bytes for derivation path index
- n = int.from_bytes(dd[0:2], byteorder="little")
- dd = dd[2:]
- # in case of overflow, drop these 2 bytes; and use next 4 bytes instead
- if n == 0xffff:
- n = int.from_bytes(dd[0:4], byteorder="little")
- dd = dd[4:]
- s.append(n)
- assert len(s) == 2
- return xkey, s
-
- def get_pubkey_derivation(self, x_pubkey):
- if x_pubkey[0:2] != 'ff':
- return
- xpub, derivation = self.parse_xpubkey(x_pubkey)
- if self.xpub != xpub:
- return
- return derivation
-
class BIP32_KeyStore(Deterministic_KeyStore, Xpub):
type = 'bip32'
def __init__(self, d):
- Xpub.__init__(self)
+ Xpub.__init__(self, derivation_prefix=d.get('derivation'), root_fingerprint=d.get('root_fingerprint'))
Deterministic_KeyStore.__init__(self, d)
self.xpub = d.get('xpub')
self.xprv = d.get('xprv')
t@@ -360,6 +435,8 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub):
d = Deterministic_KeyStore.dump(self)
d['xpub'] = self.xpub
d['xprv'] = self.xprv
+ d['derivation'] = self.get_derivation_prefix()
+ d['root_fingerprint'] = self.get_root_fingerprint()
return d
def get_master_private_key(self, password):
t@@ -388,14 +465,22 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub):
def is_watching_only(self):
return self.xprv is None
+ def add_xpub(self, xpub):
+ assert is_xpub(xpub)
+ self.xpub = xpub
+ root_fingerprint, derivation_prefix = bip32.root_fp_and_der_prefix_from_xkey(xpub)
+ self.add_key_origin(derivation_prefix=derivation_prefix, root_fingerprint=root_fingerprint)
+
def add_xprv(self, xprv):
+ assert is_xprv(xprv)
self.xprv = xprv
- self.xpub = bip32.xpub_from_xprv(xprv)
+ self.add_xpub(bip32.xpub_from_xprv(xprv))
def add_xprv_from_seed(self, bip32_seed, xtype, derivation):
rootnode = BIP32Node.from_rootseed(bip32_seed, xtype=xtype)
node = rootnode.subkey_at_private_derivation(derivation)
self.add_xprv(node.to_xprv())
+ self.add_key_origin_from_root_node(derivation_prefix=derivation, root_node=rootnode)
def get_private_key(self, sequence, password):
xprv = self.get_master_private_key(password)
t@@ -415,6 +500,7 @@ class Old_KeyStore(Deterministic_KeyStore):
def __init__(self, d):
Deterministic_KeyStore.__init__(self, d)
self.mpk = d.get('mpk')
+ self._root_fingerprint = None
def get_hex_seed(self, password):
return pw_decode(self.seed, password, version=self.pw_hash_version).encode('utf8')
t@@ -477,7 +563,7 @@ class Old_KeyStore(Deterministic_KeyStore):
public_key = master_public_key + z*ecc.generator()
return public_key.get_public_key_hex(compressed=False)
- def derive_pubkey(self, for_change, n):
+ def derive_pubkey(self, for_change, n) -> str:
return self.get_pubkey_from_mpk(self.mpk, for_change, n)
def get_private_key_from_stretched_exponent(self, for_change, n, secexp):
t@@ -508,31 +594,25 @@ class Old_KeyStore(Deterministic_KeyStore):
def get_master_public_key(self):
return self.mpk
- def get_xpubkey(self, for_change, n):
- s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (for_change, n)))
- return 'fe' + self.mpk + s
-
- @classmethod
- def parse_xpubkey(self, x_pubkey):
- assert x_pubkey[0:2] == 'fe'
- pk = x_pubkey[2:]
- mpk = pk[0:128]
- dd = pk[128:]
- s = []
- while dd:
- n = int(bitcoin.rev_hex(dd[0:4]), 16)
- dd = dd[4:]
- s.append(n)
- assert len(s) == 2
- return mpk, s
-
- def get_pubkey_derivation(self, x_pubkey):
- if x_pubkey[0:2] != 'fe':
- return
- mpk, derivation = self.parse_xpubkey(x_pubkey)
- if self.mpk != mpk:
- return
- return derivation
+ def get_derivation_prefix(self) -> str:
+ return 'm'
+
+ def get_root_fingerprint(self) -> str:
+ if self._root_fingerprint is None:
+ master_public_key = ecc.ECPubkey(bfh('04'+self.mpk))
+ xfp = hash_160(master_public_key.get_public_key_bytes(compressed=True))[0:4]
+ self._root_fingerprint = xfp.hex().lower()
+ return self._root_fingerprint
+
+ # TODO Old_KeyStore and Xpub could share a common baseclass?
+ def get_fp_and_derivation_to_be_used_in_partial_tx(self, der_suffix: Sequence[int], *,
+ only_der_suffix: bool = True) -> Tuple[bytes, Sequence[int]]:
+ fingerprint_hex = self.get_root_fingerprint()
+ der_prefix_str = self.get_derivation_prefix()
+ fingerprint_bytes = bfh(fingerprint_hex)
+ der_prefix_ints = convert_bip32_path_to_list_of_uint32(der_prefix_str)
+ der_full = der_prefix_ints + list(der_suffix)
+ return fingerprint_bytes, der_full
def update_password(self, old_password, new_password):
self.check_password(old_password)
t@@ -554,14 +634,13 @@ class Hardware_KeyStore(KeyStore, Xpub):
type = 'hardware'
def __init__(self, d):
- Xpub.__init__(self)
+ Xpub.__init__(self, derivation_prefix=d.get('derivation'), root_fingerprint=d.get('root_fingerprint'))
KeyStore.__init__(self)
# Errors and other user interaction is done through the wallet's
# handler. The handler is per-window and preserved across
# device reconnects
self.xpub = d.get('xpub')
self.label = d.get('label')
- self.derivation = d.get('derivation')
self.handler = None
run_hook('init_keystore', self)
t@@ -582,7 +661,8 @@ class Hardware_KeyStore(KeyStore, Xpub):
'type': self.type,
'hw_type': self.hw_type,
'xpub': self.xpub,
- 'derivation':self.derivation,
+ 'derivation': self.get_derivation_prefix(),
+ 'root_fingerprint': self.get_root_fingerprint(),
'label':self.label,
}
t@@ -624,6 +704,16 @@ class Hardware_KeyStore(KeyStore, Xpub):
def ready_to_sign(self):
return super().ready_to_sign() and self.has_usable_connection_with_device()
+ def opportunistically_fill_in_missing_info_from_device(self, client):
+ assert client is not None
+ if self._root_fingerprint is None:
+ # digitalbitbox (at least) does not reveal xpubs corresponding to unhardened paths
+ # so ask for a direct child, and read out fingerprint from that:
+ child_of_root_xpub = client.get_xpub("m/0'", xtype='standard')
+ root_fingerprint = BIP32Node.from_xkey(child_of_root_xpub).fingerprint.hex().lower()
+ self._root_fingerprint = root_fingerprint
+ self.is_requesting_to_be_rewritten_to_wallet_file = True
+
def bip39_normalize_passphrase(passphrase):
return normalize('NFKD', passphrase or '')
t@@ -684,16 +774,17 @@ PURPOSE48_SCRIPT_TYPES_INV = inv_dict(PURPOSE48_SCRIPT_TYPES)
def xtype_from_derivation(derivation: str) -> str:
"""Returns the script type to be used for this derivation."""
- if derivation.startswith("m/84'"):
- return 'p2wpkh'
- elif derivation.startswith("m/49'"):
- return 'p2wpkh-p2sh'
- elif derivation.startswith("m/44'"):
- return 'standard'
- elif derivation.startswith("m/45'"):
- return 'standard'
-
bip32_indices = convert_bip32_path_to_list_of_uint32(derivation)
+ if len(bip32_indices) >= 1:
+ if bip32_indices[0] == 84 + BIP32_PRIME:
+ return 'p2wpkh'
+ elif bip32_indices[0] == 49 + BIP32_PRIME:
+ return 'p2wpkh-p2sh'
+ elif bip32_indices[0] == 44 + BIP32_PRIME:
+ return 'standard'
+ elif bip32_indices[0] == 45 + BIP32_PRIME:
+ return 'standard'
+
if len(bip32_indices) >= 4:
if bip32_indices[0] == 48 + BIP32_PRIME:
# m / purpose' / coin_type' / account' / script_type' / change / address_index
t@@ -704,40 +795,6 @@ def xtype_from_derivation(derivation: str) -> str:
return 'standard'
-# extended pubkeys
-
-def is_xpubkey(x_pubkey):
- return x_pubkey[0:2] == 'ff'
-
-
-def parse_xpubkey(x_pubkey):
- assert x_pubkey[0:2] == 'ff'
- return BIP32_KeyStore.parse_xpubkey(x_pubkey)
-
-
-def xpubkey_to_address(x_pubkey):
- if x_pubkey[0:2] == 'fd':
- address = bitcoin.script_to_address(x_pubkey[2:])
- return x_pubkey, address
- if x_pubkey[0:2] in ['02', '03', '04']:
- pubkey = x_pubkey
- elif x_pubkey[0:2] == 'ff':
- xpub, s = BIP32_KeyStore.parse_xpubkey(x_pubkey)
- pubkey = BIP32_KeyStore.get_pubkey_from_xpub(xpub, s)
- elif x_pubkey[0:2] == 'fe':
- mpk, s = Old_KeyStore.parse_xpubkey(x_pubkey)
- pubkey = Old_KeyStore.get_pubkey_from_mpk(mpk, s[0], s[1])
- else:
- raise BitcoinException("Cannot parse pubkey. prefix: {}"
- .format(x_pubkey[0:2]))
- if pubkey:
- address = public_key_to_p2pkh(bfh(pubkey))
- return pubkey, address
-
-def xpubkey_to_pubkey(x_pubkey):
- pubkey, address = xpubkey_to_address(x_pubkey)
- return pubkey
-
hw_keystores = {}
def register_keystore(hw_type, constructor):
t@@ -770,7 +827,7 @@ def load_keystore(storage, name) -> KeyStore:
def is_old_mpk(mpk: str) -> bool:
try:
- int(mpk, 16)
+ int(mpk, 16) # test if hex string
except:
return False
if len(mpk) != 128:
t@@ -804,16 +861,18 @@ def is_private_key_list(text, *, allow_spaces_inside_key=True, raise_on_error=Fa
raise_on_error=raise_on_error))
-is_mpk = lambda x: is_old_mpk(x) or is_xpub(x)
-is_private = lambda x: is_seed(x) or is_xprv(x) or is_private_key_list(x)
-is_master_key = lambda x: is_old_mpk(x) or is_xprv(x) or is_xpub(x)
-is_private_key = lambda x: is_xprv(x) or is_private_key_list(x)
-is_bip32_key = lambda x: is_xprv(x) or is_xpub(x)
+def is_master_key(x):
+ return is_old_mpk(x) or is_bip32_key(x)
+
+
+def is_bip32_key(x):
+ return is_xprv(x) or is_xpub(x)
def bip44_derivation(account_id, bip43_purpose=44):
coin = constants.net.BIP44_COIN_TYPE
- return "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id))
+ der = "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id))
+ return normalize_bip32_derivation(der)
def purpose48_derivation(account_id: int, xtype: str) -> str:
t@@ -824,7 +883,8 @@ def purpose48_derivation(account_id: int, xtype: str) -> str:
script_type_int = PURPOSE48_SCRIPT_TYPES.get(xtype)
if script_type_int is None:
raise Exception('unknown xtype: {}'.format(xtype))
- return "m/%d'/%d'/%d'/%d'" % (bip43_purpose, coin, account_id, script_type_int)
+ der = "m/%d'/%d'/%d'/%d'" % (bip43_purpose, coin, account_id, script_type_int)
+ return normalize_bip32_derivation(der)
def from_seed(seed, passphrase, is_p2sh=False):
t@@ -861,14 +921,12 @@ def from_old_mpk(mpk):
def from_xpub(xpub):
k = BIP32_KeyStore({})
- k.xpub = xpub
+ k.add_xpub(xpub)
return k
def from_xprv(xprv):
- xpub = bip32.xpub_from_xprv(xprv)
k = BIP32_KeyStore({})
- k.xprv = xprv
- k.xpub = xpub
+ k.add_xprv(xprv)
return k
def from_master_key(text):
DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
t@@ -32,10 +32,9 @@ import time
from . import ecc
from .util import bfh, bh2u
-from .bitcoin import TYPE_SCRIPT, TYPE_ADDRESS
from .bitcoin import redeem_script_to_address
from .crypto import sha256, sha256d
-from .transaction import Transaction
+from .transaction import Transaction, PartialTransaction
from .logging import Logger
from .lnonion import decode_onion_error
t@@ -528,19 +527,19 @@ class Channel(Logger):
ctx = self.make_commitment(subject, point, ctn)
return secret, ctx
- def get_commitment(self, subject, ctn):
+ def get_commitment(self, subject, ctn) -> PartialTransaction:
secret, ctx = self.get_secret_and_commitment(subject, ctn)
return ctx
- def get_next_commitment(self, subject: HTLCOwner) -> Transaction:
+ def get_next_commitment(self, subject: HTLCOwner) -> PartialTransaction:
ctn = self.get_next_ctn(subject)
return self.get_commitment(subject, ctn)
- def get_latest_commitment(self, subject: HTLCOwner) -> Transaction:
+ def get_latest_commitment(self, subject: HTLCOwner) -> PartialTransaction:
ctn = self.get_latest_ctn(subject)
return self.get_commitment(subject, ctn)
- def get_oldest_unrevoked_commitment(self, subject: HTLCOwner) -> Transaction:
+ def get_oldest_unrevoked_commitment(self, subject: HTLCOwner) -> PartialTransaction:
ctn = self.get_oldest_unrevoked_ctn(subject)
return self.get_commitment(subject, ctn)
t@@ -603,7 +602,7 @@ class Channel(Logger):
self.hm.recv_fail(htlc_id)
def pending_local_fee(self):
- return self.constraints.capacity - sum(x[2] for x in self.get_next_commitment(LOCAL).outputs())
+ return self.constraints.capacity - sum(x.value for x in self.get_next_commitment(LOCAL).outputs())
def update_fee(self, feerate: int, from_us: bool):
# feerate uses sat/kw
t@@ -658,7 +657,7 @@ class Channel(Logger):
def __str__(self):
return str(self.serialize())
- def make_commitment(self, subject, this_point, ctn) -> Transaction:
+ def make_commitment(self, subject, this_point, ctn) -> PartialTransaction:
assert type(subject) is HTLCOwner
feerate = self.get_feerate(subject, ctn)
other = REMOTE if LOCAL == subject else LOCAL
t@@ -717,21 +716,20 @@ class Channel(Logger):
onchain_fees,
htlcs=htlcs)
- def get_local_index(self):
- return int(self.config[LOCAL].multisig_key.pubkey > self.config[REMOTE].multisig_key.pubkey)
-
def make_closing_tx(self, local_script: bytes, remote_script: bytes,
- fee_sat: int) -> Tuple[bytes, Transaction]:
+ fee_sat: int) -> Tuple[bytes, PartialTransaction]:
""" cooperative close """
- _, outputs = make_commitment_outputs({
+ _, outputs = make_commitment_outputs(
+ fees_per_participant={
LOCAL: fee_sat * 1000 if self.constraints.is_initiator else 0,
REMOTE: fee_sat * 1000 if not self.constraints.is_initiator else 0,
},
- self.balance(LOCAL),
- self.balance(REMOTE),
- (TYPE_SCRIPT, bh2u(local_script)),
- (TYPE_SCRIPT, bh2u(remote_script)),
- [], self.config[LOCAL].dust_limit_sat)
+ local_amount_msat=self.balance(LOCAL),
+ remote_amount_msat=self.balance(REMOTE),
+ local_script=bh2u(local_script),
+ remote_script=bh2u(remote_script),
+ htlcs=[],
+ dust_limit_sat=self.config[LOCAL].dust_limit_sat)
closing_tx = make_closing_tx(self.config[LOCAL].multisig_key.pubkey,
self.config[REMOTE].multisig_key.pubkey,
t@@ -744,25 +742,23 @@ class Channel(Logger):
sig = ecc.sig_string_from_der_sig(der_sig[:-1])
return sig, closing_tx
- def signature_fits(self, tx):
+ def signature_fits(self, tx: PartialTransaction):
remote_sig = self.config[LOCAL].current_commitment_signature
preimage_hex = tx.serialize_preimage(0)
- pre_hash = sha256d(bfh(preimage_hex))
+ msg_hash = sha256d(bfh(preimage_hex))
assert remote_sig
- res = ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, remote_sig, pre_hash)
+ res = ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, remote_sig, msg_hash)
return res
def force_close_tx(self):
tx = self.get_latest_commitment(LOCAL)
assert self.signature_fits(tx)
- tx = Transaction(str(tx))
- tx.deserialize(True)
tx.sign({bh2u(self.config[LOCAL].multisig_key.pubkey): (self.config[LOCAL].multisig_key.privkey, True)})
remote_sig = self.config[LOCAL].current_commitment_signature
remote_sig = ecc.der_sig_from_sig_string(remote_sig) + b"\x01"
- sigs = tx._inputs[0]["signatures"]
- none_idx = sigs.index(None)
- tx.add_signature_to_txin(0, none_idx, bh2u(remote_sig))
+ tx.add_signature_to_txin(txin_idx=0,
+ signing_pubkey=self.config[REMOTE].multisig_key.pubkey.hex(),
+ sig=remote_sig.hex())
assert tx.is_complete()
return tx
DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
t@@ -11,7 +11,7 @@ import asyncio
import os
import time
from functools import partial
-from typing import List, Tuple, Dict, TYPE_CHECKING, Optional, Callable
+from typing import List, Tuple, Dict, TYPE_CHECKING, Optional, Callable, Union
import traceback
import sys
from datetime import datetime
t@@ -24,7 +24,7 @@ from . import ecc
from .ecc import sig_string_from_r_and_s, get_r_and_s_from_sig_string, der_sig_from_sig_string
from . import constants
from .util import bh2u, bfh, log_exceptions, list_enabled_bits, ignore_exceptions, chunks, SilentTaskGroup
-from .transaction import Transaction, TxOutput
+from .transaction import Transaction, TxOutput, PartialTxOutput
from .logging import Logger
from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment,
process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage,
t@@ -48,7 +48,7 @@ from .interface import GracefulDisconnect, NetworkException
from .lnrouter import fee_for_edge_msat
if TYPE_CHECKING:
- from .lnworker import LNWorker
+ from .lnworker import LNWorker, LNGossip, LNWallet
from .lnrouter import RouteEdge
t@@ -62,7 +62,7 @@ def channel_id_from_funding_tx(funding_txid: str, funding_index: int) -> Tuple[b
class Peer(Logger):
- def __init__(self, lnworker: 'LNWorker', pubkey:bytes, transport: LNTransportBase):
+ def __init__(self, lnworker: Union['LNGossip', 'LNWallet'], pubkey:bytes, transport: LNTransportBase):
self.initialized = asyncio.Event()
self.querying = asyncio.Event()
self.transport = transport
t@@ -483,8 +483,8 @@ class Peer(Logger):
push_msat: int, temp_channel_id: bytes) -> Channel:
wallet = self.lnworker.wallet
# dry run creating funding tx to see if we even have enough funds
- funding_tx_test = wallet.mktx([TxOutput(bitcoin.TYPE_ADDRESS, wallet.dummy_address(), funding_sat)],
- password, nonlocal_only=True)
+ funding_tx_test = wallet.mktx(outputs=[PartialTxOutput.from_address_and_value(wallet.dummy_address(), funding_sat)],
+ password=password, nonlocal_only=True)
await asyncio.wait_for(self.initialized.wait(), LN_P2P_NETWORK_TIMEOUT)
feerate = self.lnworker.current_feerate_per_kw()
local_config = self.make_local_config(funding_sat, push_msat, LOCAL)
t@@ -563,8 +563,8 @@ class Peer(Logger):
# create funding tx
redeem_script = funding_output_script(local_config, remote_config)
funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script)
- funding_output = TxOutput(bitcoin.TYPE_ADDRESS, funding_address, funding_sat)
- funding_tx = wallet.mktx([funding_output], password, nonlocal_only=True)
+ funding_output = PartialTxOutput.from_address_and_value(funding_address, funding_sat)
+ funding_tx = wallet.mktx(outputs=[funding_output], password=password, nonlocal_only=True)
funding_txid = funding_tx.txid()
funding_index = funding_tx.outputs().index(funding_output)
# remote commitment transaction
t@@ -691,7 +691,7 @@ class Peer(Logger):
outp = funding_tx.outputs()[funding_idx]
redeem_script = funding_output_script(chan.config[REMOTE], chan.config[LOCAL])
funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script)
- if outp != TxOutput(bitcoin.TYPE_ADDRESS, funding_address, funding_sat):
+ if not (outp.address == funding_address and outp.value == funding_sat):
chan.set_state('DISCONNECTED')
raise Exception('funding outpoint mismatch')
t@@ -1485,11 +1485,13 @@ class Peer(Logger):
break
# TODO: negotiate better
our_fee = their_fee
- # index of our_sig
- i = chan.get_local_index()
# add signatures
- closing_tx.add_signature_to_txin(0, i, bh2u(der_sig_from_sig_string(our_sig) + b'\x01'))
- closing_tx.add_signature_to_txin(0, 1-i, bh2u(der_sig_from_sig_string(their_sig) + b'\x01'))
+ closing_tx.add_signature_to_txin(txin_idx=0,
+ signing_pubkey=chan.config[LOCAL].multisig_key.pubkey,
+ sig=bh2u(der_sig_from_sig_string(our_sig) + b'\x01'))
+ closing_tx.add_signature_to_txin(txin_idx=0,
+ signing_pubkey=chan.config[REMOTE].multisig_key.pubkey,
+ sig=bh2u(der_sig_from_sig_string(their_sig) + b'\x01'))
# broadcast
await self.network.broadcast_transaction(closing_tx)
return closing_tx.txid()
DIR diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py
t@@ -6,7 +6,7 @@ from typing import Optional, Dict, List, Tuple, TYPE_CHECKING, NamedTuple, Calla
from enum import Enum, auto
from .util import bfh, bh2u
-from .bitcoin import TYPE_ADDRESS, redeem_script_to_address, dust_threshold
+from .bitcoin import redeem_script_to_address, dust_threshold
from . import ecc
from .lnutil import (make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script,
derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey,
t@@ -15,7 +15,8 @@ from .lnutil import (make_commitment_output_to_remote_address, make_commitment_o
get_ordered_channel_configs, privkey_to_pubkey, get_per_commitment_secret_from_seed,
RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED,
map_htlcs_to_ctx_output_idxs, Direction)
-from .transaction import Transaction, TxOutput, construct_witness
+from .transaction import (Transaction, TxOutput, construct_witness, PartialTransaction, PartialTxInput,
+ PartialTxOutput, TxOutpoint)
from .simple_config import SimpleConfig
from .logging import get_logger
t@@ -254,7 +255,7 @@ def create_sweeptxs_for_our_ctx(*, chan: 'Channel', ctx: Transaction,
is_revocation=False,
config=chan.lnworker.config)
# side effect
- txs[htlc_tx.prevout(0)] = SweepInfo(name='first-stage-htlc',
+ txs[htlc_tx.inputs()[0].prevout.to_str()] = SweepInfo(name='first-stage-htlc',
csv_delay=0,
cltv_expiry=htlc_tx.locktime,
gen_tx=lambda: htlc_tx)
t@@ -336,7 +337,7 @@ def create_sweeptxs_for_their_ctx(*, chan: 'Channel', ctx: Transaction,
gen_tx = create_sweeptx_for_their_revoked_ctx(chan, ctx, per_commitment_secret, chan.sweep_address)
if gen_tx:
tx = gen_tx()
- txs[tx.prevout(0)] = SweepInfo(name='to_local_for_revoked_ctx',
+ txs[tx.inputs()[0].prevout.to_str()] = SweepInfo(name='to_local_for_revoked_ctx',
csv_delay=0,
cltv_expiry=0,
gen_tx=gen_tx)
t@@ -433,66 +434,58 @@ def create_htlctx_that_spends_from_our_ctx(chan: 'Channel', our_pcp: bytes,
local_htlc_sig = bfh(htlc_tx.sign_txin(0, local_htlc_privkey))
txin = htlc_tx.inputs()[0]
witness_program = bfh(Transaction.get_preimage_script(txin))
- txin['witness'] = bh2u(make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, preimage, witness_program))
+ txin.witness = make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, preimage, witness_program)
return witness_script, htlc_tx
def create_sweeptx_their_ctx_htlc(ctx: Transaction, witness_script: bytes, sweep_address: str,
preimage: Optional[bytes], output_idx: int,
privkey: bytes, is_revocation: bool,
- cltv_expiry: int, config: SimpleConfig) -> Optional[Transaction]:
+ cltv_expiry: int, config: SimpleConfig) -> Optional[PartialTransaction]:
assert type(cltv_expiry) is int
preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered)
val = ctx.outputs()[output_idx].value
- sweep_inputs = [{
- 'scriptSig': '',
- 'type': 'p2wsh',
- 'signatures': [],
- 'num_sig': 0,
- 'prevout_n': output_idx,
- 'prevout_hash': ctx.txid(),
- 'value': val,
- 'coinbase': False,
- 'preimage_script': bh2u(witness_script),
- }]
+ prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
+ txin = PartialTxInput(prevout=prevout)
+ txin._trusted_value_sats = val
+ txin.witness_script = witness_script
+ txin.script_sig = b''
+ sweep_inputs = [txin]
tx_size_bytes = 200 # TODO (depends on offered/received and is_revocation)
fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
outvalue = val - fee
if outvalue <= dust_threshold(): return None
- sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)]
- tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2, locktime=cltv_expiry)
+ sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)]
+ tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2, locktime=cltv_expiry)
sig = bfh(tx.sign_txin(0, privkey))
if not is_revocation:
witness = construct_witness([sig, preimage, witness_script])
else:
revocation_pubkey = privkey_to_pubkey(privkey)
witness = construct_witness([sig, revocation_pubkey, witness_script])
- tx.inputs()[0]['witness'] = witness
+ tx.inputs()[0].witness = bfh(witness)
assert tx.is_complete()
return tx
def create_sweeptx_their_ctx_to_remote(sweep_address: str, ctx: Transaction, output_idx: int,
our_payment_privkey: ecc.ECPrivkey,
- config: SimpleConfig) -> Optional[Transaction]:
+ config: SimpleConfig) -> Optional[PartialTransaction]:
our_payment_pubkey = our_payment_privkey.get_public_key_hex(compressed=True)
val = ctx.outputs()[output_idx].value
- sweep_inputs = [{
- 'type': 'p2wpkh',
- 'x_pubkeys': [our_payment_pubkey],
- 'num_sig': 1,
- 'prevout_n': output_idx,
- 'prevout_hash': ctx.txid(),
- 'value': val,
- 'coinbase': False,
- 'signatures': [None],
- }]
+ prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
+ txin = PartialTxInput(prevout=prevout)
+ txin._trusted_value_sats = val
+ txin.script_type = 'p2wpkh'
+ txin.pubkeys = [bfh(our_payment_pubkey)]
+ txin.num_sig = 1
+ sweep_inputs = [txin]
tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh
fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
outvalue = val - fee
if outvalue <= dust_threshold(): return None
- sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)]
- sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs)
+ sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)]
+ sweep_tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs)
sweep_tx.set_rbf(True)
sweep_tx.sign({our_payment_pubkey: (our_payment_privkey.get_secret_bytes(), True)})
if not sweep_tx.is_complete():
t@@ -502,7 +495,7 @@ def create_sweeptx_their_ctx_to_remote(sweep_address: str, ctx: Transaction, out
def create_sweeptx_ctx_to_local(*, sweep_address: str, ctx: Transaction, output_idx: int, witness_script: str,
privkey: bytes, is_revocation: bool, config: SimpleConfig,
- to_self_delay: int=None) -> Optional[Transaction]:
+ to_self_delay: int=None) -> Optional[PartialTransaction]:
"""Create a txn that sweeps the 'to_local' output of a commitment
transaction into our wallet.
t@@ -510,61 +503,51 @@ def create_sweeptx_ctx_to_local(*, sweep_address: str, ctx: Transaction, output_
is_revocation: tells us which ^
"""
val = ctx.outputs()[output_idx].value
- sweep_inputs = [{
- 'scriptSig': '',
- 'type': 'p2wsh',
- 'signatures': [],
- 'num_sig': 0,
- 'prevout_n': output_idx,
- 'prevout_hash': ctx.txid(),
- 'value': val,
- 'coinbase': False,
- 'preimage_script': witness_script,
- }]
+ prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
+ txin = PartialTxInput(prevout=prevout)
+ txin._trusted_value_sats = val
+ txin.script_sig = b''
+ txin.witness_script = bfh(witness_script)
+ sweep_inputs = [txin]
if not is_revocation:
assert isinstance(to_self_delay, int)
- sweep_inputs[0]['sequence'] = to_self_delay
+ sweep_inputs[0].nsequence = to_self_delay
tx_size_bytes = 121 # approx size of to_local -> p2wpkh
fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
outvalue = val - fee
if outvalue <= dust_threshold():
return None
- sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)]
- sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2)
+ sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)]
+ sweep_tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2)
sig = sweep_tx.sign_txin(0, privkey)
witness = construct_witness([sig, int(is_revocation), witness_script])
- sweep_tx.inputs()[0]['witness'] = witness
+ sweep_tx.inputs()[0].witness = bfh(witness)
return sweep_tx
def create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx(*,
htlc_tx: Transaction, htlctx_witness_script: bytes, sweep_address: str,
privkey: bytes, is_revocation: bool, to_self_delay: int,
- config: SimpleConfig) -> Optional[Transaction]:
+ config: SimpleConfig) -> Optional[PartialTransaction]:
val = htlc_tx.outputs()[0].value
- sweep_inputs = [{
- 'scriptSig': '',
- 'type': 'p2wsh',
- 'signatures': [],
- 'num_sig': 0,
- 'prevout_n': 0,
- 'prevout_hash': htlc_tx.txid(),
- 'value': val,
- 'coinbase': False,
- 'preimage_script': bh2u(htlctx_witness_script),
- }]
+ prevout = TxOutpoint(txid=bfh(htlc_tx.txid()), out_idx=0)
+ txin = PartialTxInput(prevout=prevout)
+ txin._trusted_value_sats = val
+ txin.script_sig = b''
+ txin.witness_script = htlctx_witness_script
+ sweep_inputs = [txin]
if not is_revocation:
assert isinstance(to_self_delay, int)
- sweep_inputs[0]['sequence'] = to_self_delay
+ sweep_inputs[0].nsequence = to_self_delay
tx_size_bytes = 200 # TODO
fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
outvalue = val - fee
if outvalue <= dust_threshold(): return None
- sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)]
- tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2)
+ sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)]
+ tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2)
sig = bfh(tx.sign_txin(0, privkey))
witness = construct_witness([sig, int(is_revocation), htlctx_witness_script])
- tx.inputs()[0]['witness'] = witness
+ tx.inputs()[0].witness = bfh(witness)
assert tx.is_complete()
return tx
DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py
t@@ -10,11 +10,11 @@ import re
from .util import bfh, bh2u, inv_dict
from .crypto import sha256
-from .transaction import Transaction
+from .transaction import (Transaction, PartialTransaction, PartialTxInput, TxOutpoint,
+ PartialTxOutput, opcodes, TxOutput)
from .ecc import CURVE_ORDER, sig_string_from_der_sig, ECPubkey, string_to_number
from . import ecc, bitcoin, crypto, transaction
-from .transaction import opcodes, TxOutput, Transaction
-from .bitcoin import push_script, redeem_script_to_address, TYPE_ADDRESS
+from .bitcoin import push_script, redeem_script_to_address, address_to_script
from . import segwit_addr
from .i18n import _
from .lnaddr import lndecode
t@@ -97,6 +97,7 @@ class ScriptHtlc(NamedTuple):
htlc: 'UpdateAddHtlc'
+# FIXME duplicate of TxOutpoint in transaction.py??
class Outpoint(NamedTuple("Outpoint", [('txid', str), ('output_index', int)])):
def to_str(self):
return "{}:{}".format(self.txid, self.output_index)
t@@ -287,7 +288,7 @@ def make_htlc_tx_output(amount_msat, local_feerate, revocationpubkey, local_dela
fee = fee // 1000 * 1000
final_amount_sat = (amount_msat - fee) // 1000
assert final_amount_sat > 0, final_amount_sat
- output = TxOutput(bitcoin.TYPE_ADDRESS, p2wsh, final_amount_sat)
+ output = PartialTxOutput.from_address_and_value(p2wsh, final_amount_sat)
return script, output
def make_htlc_tx_witness(remotehtlcsig: bytes, localhtlcsig: bytes,
t@@ -299,29 +300,23 @@ def make_htlc_tx_witness(remotehtlcsig: bytes, localhtlcsig: bytes,
return bfh(transaction.construct_witness([0, remotehtlcsig, localhtlcsig, payment_preimage, witness_script]))
def make_htlc_tx_inputs(htlc_output_txid: str, htlc_output_index: int,
- amount_msat: int, witness_script: str) -> List[dict]:
+ amount_msat: int, witness_script: str) -> List[PartialTxInput]:
assert type(htlc_output_txid) is str
assert type(htlc_output_index) is int
assert type(amount_msat) is int
assert type(witness_script) is str
- c_inputs = [{
- 'scriptSig': '',
- 'type': 'p2wsh',
- 'signatures': [],
- 'num_sig': 0,
- 'prevout_n': htlc_output_index,
- 'prevout_hash': htlc_output_txid,
- 'value': amount_msat // 1000,
- 'coinbase': False,
- 'sequence': 0x0,
- 'preimage_script': witness_script,
- }]
+ txin = PartialTxInput(prevout=TxOutpoint(txid=bfh(htlc_output_txid), out_idx=htlc_output_index),
+ nsequence=0)
+ txin.witness_script = bfh(witness_script)
+ txin.script_sig = b''
+ txin._trusted_value_sats = amount_msat // 1000
+ c_inputs = [txin]
return c_inputs
-def make_htlc_tx(*, cltv_expiry: int, inputs, output) -> Transaction:
+def make_htlc_tx(*, cltv_expiry: int, inputs: List[PartialTxInput], output: PartialTxOutput) -> PartialTransaction:
assert type(cltv_expiry) is int
c_outputs = [output]
- tx = Transaction.from_io(inputs, c_outputs, locktime=cltv_expiry, version=2)
+ tx = PartialTransaction.from_io(inputs, c_outputs, locktime=cltv_expiry, version=2)
return tx
def make_offered_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes,
t@@ -437,7 +432,7 @@ def map_htlcs_to_ctx_output_idxs(*, chan: 'Channel', ctx: Transaction, pcp: byte
def make_htlc_tx_with_open_channel(*, chan: 'Channel', pcp: bytes, subject: 'HTLCOwner',
htlc_direction: 'Direction', commit: Transaction, ctx_output_idx: int,
- htlc: 'UpdateAddHtlc', name: str = None) -> Tuple[bytes, Transaction]:
+ htlc: 'UpdateAddHtlc', name: str = None) -> Tuple[bytes, PartialTransaction]:
amount_msat, cltv_expiry, payment_hash = htlc.amount_msat, htlc.cltv_expiry, htlc.payment_hash
for_us = subject == LOCAL
conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=for_us)
t@@ -472,19 +467,15 @@ def make_htlc_tx_with_open_channel(*, chan: 'Channel', pcp: bytes, subject: 'HTL
return witness_script_of_htlc_tx_output, htlc_tx
def make_funding_input(local_funding_pubkey: bytes, remote_funding_pubkey: bytes,
- funding_pos: int, funding_txid: bytes, funding_sat: int):
+ funding_pos: int, funding_txid: str, funding_sat: int) -> PartialTxInput:
pubkeys = sorted([bh2u(local_funding_pubkey), bh2u(remote_funding_pubkey)])
# commitment tx input
- c_input = {
- 'type': 'p2wsh',
- 'x_pubkeys': pubkeys,
- 'signatures': [None, None],
- 'num_sig': 2,
- 'prevout_n': funding_pos,
- 'prevout_hash': funding_txid,
- 'value': funding_sat,
- 'coinbase': False,
- }
+ prevout = TxOutpoint(txid=bfh(funding_txid), out_idx=funding_pos)
+ c_input = PartialTxInput(prevout=prevout)
+ c_input.script_type = 'p2wsh'
+ c_input.pubkeys = [bfh(pk) for pk in pubkeys]
+ c_input.num_sig = 2
+ c_input._trusted_value_sats = funding_sat
return c_input
class HTLCOwner(IntFlag):
t@@ -504,18 +495,18 @@ RECEIVED = Direction.RECEIVED
LOCAL = HTLCOwner.LOCAL
REMOTE = HTLCOwner.REMOTE
-def make_commitment_outputs(fees_per_participant: Mapping[HTLCOwner, int], local_amount: int, remote_amount: int,
- local_tupl, remote_tupl, htlcs: List[ScriptHtlc], dust_limit_sat: int) -> Tuple[List[TxOutput], List[TxOutput]]:
- to_local_amt = local_amount - fees_per_participant[LOCAL]
- to_local = TxOutput(*local_tupl, to_local_amt // 1000)
- to_remote_amt = remote_amount - fees_per_participant[REMOTE]
- to_remote = TxOutput(*remote_tupl, to_remote_amt // 1000)
+def make_commitment_outputs(*, fees_per_participant: Mapping[HTLCOwner, int], local_amount_msat: int, remote_amount_msat: int,
+ local_script: str, remote_script: str, htlcs: List[ScriptHtlc], dust_limit_sat: int) -> Tuple[List[PartialTxOutput], List[PartialTxOutput]]:
+ to_local_amt = local_amount_msat - fees_per_participant[LOCAL]
+ to_local = PartialTxOutput(scriptpubkey=bfh(local_script), value=to_local_amt // 1000)
+ to_remote_amt = remote_amount_msat - fees_per_participant[REMOTE]
+ to_remote = PartialTxOutput(scriptpubkey=bfh(remote_script), value=to_remote_amt // 1000)
non_htlc_outputs = [to_local, to_remote]
htlc_outputs = []
for script, htlc in htlcs:
- htlc_outputs.append(TxOutput(bitcoin.TYPE_ADDRESS,
- bitcoin.redeem_script_to_address('p2wsh', bh2u(script)),
- htlc.amount_msat // 1000))
+ addr = bitcoin.redeem_script_to_address('p2wsh', bh2u(script))
+ htlc_outputs.append(PartialTxOutput(scriptpubkey=bfh(address_to_script(addr)),
+ value=htlc.amount_msat // 1000))
# trim outputs
c_outputs_filtered = list(filter(lambda x: x.value >= dust_limit_sat, non_htlc_outputs + htlc_outputs))
t@@ -533,13 +524,13 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey,
delayed_pubkey, to_self_delay, funding_txid,
funding_pos, funding_sat, local_amount, remote_amount,
dust_limit_sat, fees_per_participant,
- htlcs: List[ScriptHtlc]) -> Transaction:
+ htlcs: List[ScriptHtlc]) -> PartialTransaction:
c_input = make_funding_input(local_funding_pubkey, remote_funding_pubkey,
funding_pos, funding_txid, funding_sat)
obs = get_obscured_ctn(ctn, funder_payment_basepoint, fundee_payment_basepoint)
locktime = (0x20 << 24) + (obs & 0xffffff)
sequence = (0x80 << 24) + (obs >> 24)
- c_input['sequence'] = sequence
+ c_input.nsequence = sequence
c_inputs = [c_input]
t@@ -555,13 +546,19 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey,
htlcs = list(htlcs)
htlcs.sort(key=lambda x: x.htlc.cltv_expiry)
- htlc_outputs, c_outputs_filtered = make_commitment_outputs(fees_per_participant, local_amount, remote_amount,
- (bitcoin.TYPE_ADDRESS, local_address), (bitcoin.TYPE_ADDRESS, remote_address), htlcs, dust_limit_sat)
+ htlc_outputs, c_outputs_filtered = make_commitment_outputs(
+ fees_per_participant=fees_per_participant,
+ local_amount_msat=local_amount,
+ remote_amount_msat=remote_amount,
+ local_script=address_to_script(local_address),
+ remote_script=address_to_script(remote_address),
+ htlcs=htlcs,
+ dust_limit_sat=dust_limit_sat)
assert sum(x.value for x in c_outputs_filtered) <= funding_sat, (c_outputs_filtered, funding_sat)
# create commitment tx
- tx = Transaction.from_io(c_inputs, c_outputs_filtered, locktime=locktime, version=2)
+ tx = PartialTransaction.from_io(c_inputs, c_outputs_filtered, locktime=locktime, version=2)
return tx
def make_commitment_output_to_local_witness_script(
t@@ -578,11 +575,9 @@ def make_commitment_output_to_local_address(
def make_commitment_output_to_remote_address(remote_payment_pubkey: bytes) -> str:
return bitcoin.pubkey_to_address('p2wpkh', bh2u(remote_payment_pubkey))
-def sign_and_get_sig_string(tx, local_config, remote_config):
- pubkeys = sorted([bh2u(local_config.multisig_key.pubkey), bh2u(remote_config.multisig_key.pubkey)])
+def sign_and_get_sig_string(tx: PartialTransaction, local_config, remote_config):
tx.sign({bh2u(local_config.multisig_key.pubkey): (local_config.multisig_key.privkey, True)})
- sig_index = pubkeys.index(bh2u(local_config.multisig_key.pubkey))
- sig = bytes.fromhex(tx.inputs()[0]["signatures"][sig_index])
+ sig = tx.inputs()[0].part_sigs[local_config.multisig_key.pubkey]
sig_64 = sig_string_from_der_sig(sig[:-1])
return sig_64
t@@ -598,11 +593,11 @@ def get_obscured_ctn(ctn: int, funder: bytes, fundee: bytes) -> int:
mask = int.from_bytes(sha256(funder + fundee)[-6:], 'big')
return ctn ^ mask
-def extract_ctn_from_tx(tx, txin_index: int, funder_payment_basepoint: bytes,
+def extract_ctn_from_tx(tx: Transaction, txin_index: int, funder_payment_basepoint: bytes,
fundee_payment_basepoint: bytes) -> int:
tx.deserialize()
locktime = tx.locktime
- sequence = tx.inputs()[txin_index]['sequence']
+ sequence = tx.inputs()[txin_index].nsequence
obs = ((sequence & 0xffffff) << 24) + (locktime & 0xffffff)
return get_obscured_ctn(obs, funder_payment_basepoint, fundee_payment_basepoint)
t@@ -671,12 +666,12 @@ def get_compressed_pubkey_from_bech32(bech32_pubkey: str) -> bytes:
def make_closing_tx(local_funding_pubkey: bytes, remote_funding_pubkey: bytes,
- funding_txid: bytes, funding_pos: int, funding_sat: int,
- outputs: List[TxOutput]) -> Transaction:
+ funding_txid: str, funding_pos: int, funding_sat: int,
+ outputs: List[PartialTxOutput]) -> PartialTransaction:
c_input = make_funding_input(local_funding_pubkey, remote_funding_pubkey,
funding_pos, funding_txid, funding_sat)
- c_input['sequence'] = 0xFFFF_FFFF
- tx = Transaction.from_io([c_input], outputs, locktime=0, version=2)
+ c_input.nsequence = 0xFFFF_FFFF
+ tx = PartialTransaction.from_io([c_input], outputs, locktime=0, version=2)
return tx
DIR diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py
t@@ -77,9 +77,11 @@ class SweepStore(SqlDB):
return set([r[0] for r in c.fetchall()])
@sql
- def add_sweep_tx(self, funding_outpoint, ctn, prevout, tx):
+ def add_sweep_tx(self, funding_outpoint, ctn, prevout, tx: Transaction):
c = self.conn.cursor()
- c.execute("""INSERT INTO sweep_txs (funding_outpoint, ctn, prevout, tx) VALUES (?,?,?,?)""", (funding_outpoint, ctn, prevout, bfh(str(tx))))
+ assert tx.is_complete()
+ raw_tx = bfh(tx.serialize())
+ c.execute("""INSERT INTO sweep_txs (funding_outpoint, ctn, prevout, tx) VALUES (?,?,?,?)""", (funding_outpoint, ctn, prevout, raw_tx))
self.conn.commit()
@sql
DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
t@@ -375,7 +375,7 @@ class LNWallet(LNWorker):
for ctn in range(watchtower_ctn + 1, current_ctn):
sweeptxs = chan.create_sweeptxs(ctn)
for tx in sweeptxs:
- await watchtower.add_sweep_tx(outpoint, ctn, tx.prevout(0), str(tx))
+ await watchtower.add_sweep_tx(outpoint, ctn, tx.inputs()[0].prevout.to_str(), tx.serialize())
def start_network(self, network: 'Network'):
self.lnwatcher = LNWatcher(network)
DIR diff --git a/electrum/network.py b/electrum/network.py
t@@ -64,6 +64,7 @@ if TYPE_CHECKING:
from .channel_db import ChannelDB
from .lnworker import LNGossip
from .lnwatcher import WatchTower
+ from .transaction import Transaction
_logger = get_logger(__name__)
t@@ -887,11 +888,11 @@ class Network(Logger):
return await self.interface.session.send_request('blockchain.transaction.get_merkle', [tx_hash, tx_height])
@best_effort_reliable
- async def broadcast_transaction(self, tx, *, timeout=None) -> None:
+ async def broadcast_transaction(self, tx: 'Transaction', *, timeout=None) -> None:
if timeout is None:
timeout = self.get_network_timeout_seconds(NetworkTimeout.Urgent)
try:
- out = await self.interface.session.send_request('blockchain.transaction.broadcast', [str(tx)], timeout=timeout)
+ out = await self.interface.session.send_request('blockchain.transaction.broadcast', [tx.serialize()], timeout=timeout)
# note: both 'out' and exception messages are untrusted input from the server
except (RequestTimedOut, asyncio.CancelledError, asyncio.TimeoutError):
raise # pass-through
DIR diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py
t@@ -25,7 +25,7 @@
import hashlib
import sys
import time
-from typing import Optional
+from typing import Optional, List
import asyncio
import urllib.parse
t@@ -42,8 +42,8 @@ from . import bitcoin, ecc, util, transaction, x509, rsakey
from .util import bh2u, bfh, export_meta, import_meta, make_aiohttp_session
from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT
from .crypto import sha256
-from .bitcoin import TYPE_ADDRESS
-from .transaction import TxOutput
+from .bitcoin import address_to_script
+from .transaction import PartialTxOutput
from .network import Network
from .logging import get_logger, Logger
t@@ -128,7 +128,7 @@ class PaymentRequest:
return str(self.raw)
def parse(self, r):
- self.outputs = []
+ self.outputs = [] # type: List[PartialTxOutput]
if self.error:
return
self.id = bh2u(sha256(r)[0:16])
t@@ -141,12 +141,12 @@ class PaymentRequest:
self.details = pb2.PaymentDetails()
self.details.ParseFromString(self.data.serialized_payment_details)
for o in self.details.outputs:
- type_, addr = transaction.get_address_from_output_script(o.script)
- if type_ != TYPE_ADDRESS:
+ addr = transaction.get_address_from_output_script(o.script)
+ if not addr:
# TODO maybe rm restriction but then get_requestor and get_id need changes
self.error = "only addresses are allowed as outputs"
return
- self.outputs.append(TxOutput(type_, addr, o.amount))
+ self.outputs.append(PartialTxOutput.from_address_and_value(addr, o.amount))
self.memo = self.details.memo
self.payment_url = self.details.payment_url
t@@ -252,8 +252,9 @@ class PaymentRequest:
def get_address(self):
o = self.outputs[0]
- assert o.type == TYPE_ADDRESS
- return o.address
+ addr = o.address
+ assert addr
+ return addr
def get_requestor(self):
return self.requestor if self.requestor else self.get_address()
t@@ -278,7 +279,7 @@ class PaymentRequest:
paymnt.merchant_data = pay_det.merchant_data
paymnt.transactions.append(bfh(raw_tx))
ref_out = paymnt.refund_to.add()
- ref_out.script = util.bfh(transaction.Transaction.pay_script(TYPE_ADDRESS, refund_addr))
+ ref_out.script = util.bfh(address_to_script(refund_addr))
paymnt.memo = "Paid using Electrum"
pm = paymnt.SerializeToString()
payurl = urllib.parse.urlparse(pay_det.payment_url)
t@@ -326,7 +327,7 @@ def make_unsigned_request(req):
if amount is None:
amount = 0
memo = req['memo']
- script = bfh(Transaction.pay_script(TYPE_ADDRESS, addr))
+ script = bfh(address_to_script(addr))
outputs = [(script, amount)]
pd = pb2.PaymentDetails()
for script, amount in outputs:
DIR diff --git a/electrum/plugin.py b/electrum/plugin.py
t@@ -39,6 +39,7 @@ from .logging import get_logger, Logger
if TYPE_CHECKING:
from .plugins.hw_wallet import HW_PluginBase
+ from .keystore import Hardware_KeyStore
_logger = get_logger(__name__)
t@@ -442,20 +443,23 @@ class DeviceMgr(ThreadJob):
self.scan_devices()
return self.client_lookup(id_)
- def client_for_keystore(self, plugin, handler, keystore, force_pair):
+ def client_for_keystore(self, plugin, handler, keystore: 'Hardware_KeyStore', force_pair):
self.logger.info("getting client for keystore")
if handler is None:
raise Exception(_("Handler not found for") + ' ' + plugin.name + '\n' + _("A library is probably missing."))
handler.update_status(False)
devices = self.scan_devices()
xpub = keystore.xpub
- derivation = keystore.get_derivation()
+ derivation = keystore.get_derivation_prefix()
+ assert derivation is not None
client = self.client_by_xpub(plugin, xpub, handler, devices)
if client is None and force_pair:
info = self.select_device(plugin, handler, keystore, devices)
client = self.force_pair_xpub(plugin, handler, info, xpub, derivation, devices)
if client:
handler.update_status(True)
+ if client:
+ keystore.opportunistically_fill_in_missing_info_from_device(client)
self.logger.info("end client for keystore")
return client
DIR diff --git a/electrum/plugins/audio_modem/qt.py b/electrum/plugins/audio_modem/qt.py
t@@ -4,6 +4,7 @@ import json
from io import BytesIO
import sys
import platform
+from typing import TYPE_CHECKING
from PyQt5.QtWidgets import (QComboBox, QGridLayout, QLabel, QPushButton)
t@@ -12,6 +13,9 @@ from electrum.gui.qt.util import WaitingDialog, EnterButton, WindowModalDialog,
from electrum.i18n import _
from electrum.logging import get_logger
+if TYPE_CHECKING:
+ from electrum.gui.qt.transaction_dialog import TxDialog
+
_logger = get_logger(__name__)
t@@ -71,12 +75,12 @@ class Plugin(BasePlugin):
return bool(d.exec_())
@hook
- def transaction_dialog(self, dialog):
+ def transaction_dialog(self, dialog: 'TxDialog'):
b = QPushButton()
b.setIcon(read_QIcon("speaker.png"))
def handler():
- blob = json.dumps(dialog.tx.as_dict())
+ blob = dialog.tx.serialize()
self._send(parent=dialog, blob=blob)
b.clicked.connect(handler)
dialog.sharing_buttons.insert(-1, b)
DIR diff --git a/electrum/plugins/coldcard/basic_psbt.py b/electrum/plugins/coldcard/basic_psbt.py
t@@ -1,313 +0,0 @@
-#
-# basic_psbt.py - yet another PSBT parser/serializer but used only for test cases.
-#
-# - history: taken from coldcard-firmware/testing/psbt.py
-# - trying to minimize electrum code in here, and generally, dependancies.
-#
-import io
-import struct
-from base64 import b64decode
-from binascii import a2b_hex, b2a_hex
-from struct import pack, unpack
-
-from electrum.transaction import Transaction
-
-# BIP-174 (aka PSBT) defined values
-#
-PSBT_GLOBAL_UNSIGNED_TX = (0)
-PSBT_GLOBAL_XPUB = (1)
-
-PSBT_IN_NON_WITNESS_UTXO = (0)
-PSBT_IN_WITNESS_UTXO = (1)
-PSBT_IN_PARTIAL_SIG = (2)
-PSBT_IN_SIGHASH_TYPE = (3)
-PSBT_IN_REDEEM_SCRIPT = (4)
-PSBT_IN_WITNESS_SCRIPT = (5)
-PSBT_IN_BIP32_DERIVATION = (6)
-PSBT_IN_FINAL_SCRIPTSIG = (7)
-PSBT_IN_FINAL_SCRIPTWITNESS = (8)
-
-PSBT_OUT_REDEEM_SCRIPT = (0)
-PSBT_OUT_WITNESS_SCRIPT = (1)
-PSBT_OUT_BIP32_DERIVATION = (2)
-
-# Serialization/deserialization tools
-def ser_compact_size(l):
- r = b""
- if l < 253:
- r = struct.pack("B", l)
- elif l < 0x10000:
- r = struct.pack("<BH", 253, l)
- elif l < 0x100000000:
- r = struct.pack("<BI", 254, l)
- else:
- r = struct.pack("<BQ", 255, l)
- return r
-
-def deser_compact_size(f):
- try:
- nit = f.read(1)[0]
- except IndexError:
- return None # end of file
-
- if nit == 253:
- nit = struct.unpack("<H", f.read(2))[0]
- elif nit == 254:
- nit = struct.unpack("<I", f.read(4))[0]
- elif nit == 255:
- nit = struct.unpack("<Q", f.read(8))[0]
- return nit
-
-def my_var_int(l):
- # Bitcoin serialization of integers... directly into binary!
- if l < 253:
- return pack("B", l)
- elif l < 0x10000:
- return pack("<BH", 253, l)
- elif l < 0x100000000:
- return pack("<BI", 254, l)
- else:
- return pack("<BQ", 255, l)
-
-
-class PSBTSection:
-
- def __init__(self, fd=None, idx=None):
- self.defaults()
- self.my_index = idx
-
- if not fd: return
-
- while 1:
- ks = deser_compact_size(fd)
- if ks is None: break
- if ks == 0: break
-
- key = fd.read(ks)
- vs = deser_compact_size(fd)
- val = fd.read(vs)
-
- kt = key[0]
- self.parse_kv(kt, key[1:], val)
-
- def serialize(self, fd, my_idx):
-
- def wr(ktype, val, key=b''):
- fd.write(ser_compact_size(1 + len(key)))
- fd.write(bytes([ktype]) + key)
- fd.write(ser_compact_size(len(val)))
- fd.write(val)
-
- self.serialize_kvs(wr)
-
- fd.write(b'\0')
-
-class BasicPSBTInput(PSBTSection):
- def defaults(self):
- self.utxo = None
- self.witness_utxo = None
- self.part_sigs = {}
- self.sighash = None
- self.bip32_paths = {}
- self.redeem_script = None
- self.witness_script = None
- self.others = {}
-
- def __eq__(a, b):
- if a.sighash != b.sighash:
- if a.sighash is not None and b.sighash is not None:
- return False
-
- rv = a.utxo == b.utxo and \
- a.witness_utxo == b.witness_utxo and \
- a.redeem_script == b.redeem_script and \
- a.witness_script == b.witness_script and \
- a.my_index == b.my_index and \
- a.bip32_paths == b.bip32_paths and \
- sorted(a.part_sigs.keys()) == sorted(b.part_sigs.keys())
-
- # NOTE: equality test on signatures requires parsing DER stupidness
- # and some maybe understanding of R/S values on curve that I don't have.
-
- return rv
-
- def parse_kv(self, kt, key, val):
- if kt == PSBT_IN_NON_WITNESS_UTXO:
- self.utxo = val
- assert not key
- elif kt == PSBT_IN_WITNESS_UTXO:
- self.witness_utxo = val
- assert not key
- elif kt == PSBT_IN_PARTIAL_SIG:
- self.part_sigs[key] = val
- elif kt == PSBT_IN_SIGHASH_TYPE:
- assert len(val) == 4
- self.sighash = struct.unpack("<I", val)[0]
- assert not key
- elif kt == PSBT_IN_BIP32_DERIVATION:
- self.bip32_paths[key] = val
- elif kt == PSBT_IN_REDEEM_SCRIPT:
- self.redeem_script = val
- assert not key
- elif kt == PSBT_IN_WITNESS_SCRIPT:
- self.witness_script = val
- assert not key
- elif kt in ( PSBT_IN_REDEEM_SCRIPT,
- PSBT_IN_WITNESS_SCRIPT,
- PSBT_IN_FINAL_SCRIPTSIG,
- PSBT_IN_FINAL_SCRIPTWITNESS):
- assert not key
- self.others[kt] = val
- else:
- raise KeyError(kt)
-
- def serialize_kvs(self, wr):
- if self.utxo:
- wr(PSBT_IN_NON_WITNESS_UTXO, self.utxo)
- if self.witness_utxo:
- wr(PSBT_IN_WITNESS_UTXO, self.witness_utxo)
- if self.redeem_script:
- wr(PSBT_IN_REDEEM_SCRIPT, self.redeem_script)
- if self.witness_script:
- wr(PSBT_IN_WITNESS_SCRIPT, self.witness_script)
- for pk, val in sorted(self.part_sigs.items()):
- wr(PSBT_IN_PARTIAL_SIG, val, pk)
- if self.sighash is not None:
- wr(PSBT_IN_SIGHASH_TYPE, struct.pack('<I', self.sighash))
- for k in self.bip32_paths:
- wr(PSBT_IN_BIP32_DERIVATION, self.bip32_paths[k], k)
- for k in self.others:
- wr(k, self.others[k])
-
-class BasicPSBTOutput(PSBTSection):
- def defaults(self):
- self.redeem_script = None
- self.witness_script = None
- self.bip32_paths = {}
-
- def __eq__(a, b):
- return a.redeem_script == b.redeem_script and \
- a.witness_script == b.witness_script and \
- a.my_index == b.my_index and \
- a.bip32_paths == b.bip32_paths
-
- def parse_kv(self, kt, key, val):
- if kt == PSBT_OUT_REDEEM_SCRIPT:
- self.redeem_script = val
- assert not key
- elif kt == PSBT_OUT_WITNESS_SCRIPT:
- self.witness_script = val
- assert not key
- elif kt == PSBT_OUT_BIP32_DERIVATION:
- self.bip32_paths[key] = val
- else:
- raise ValueError(kt)
-
- def serialize_kvs(self, wr):
- if self.redeem_script:
- wr(PSBT_OUT_REDEEM_SCRIPT, self.redeem_script)
- if self.witness_script:
- wr(PSBT_OUT_WITNESS_SCRIPT, self.witness_script)
- for k in self.bip32_paths:
- wr(PSBT_OUT_BIP32_DERIVATION, self.bip32_paths[k], k)
-
-
-class BasicPSBT:
- "Just? parse and store"
-
- def __init__(self):
-
- self.txn = None
- self.filename = None
- self.parsed_txn = None
- self.xpubs = []
-
- self.inputs = []
- self.outputs = []
-
- def __eq__(a, b):
- return a.txn == b.txn and \
- len(a.inputs) == len(b.inputs) and \
- len(a.outputs) == len(b.outputs) and \
- all(a.inputs[i] == b.inputs[i] for i in range(len(a.inputs))) and \
- all(a.outputs[i] == b.outputs[i] for i in range(len(a.outputs))) and \
- sorted(a.xpubs) == sorted(b.xpubs)
-
- def parse(self, raw, filename=None):
- # auto-detect and decode Base64 and Hex.
- if raw[0:10].lower() == b'70736274ff':
- raw = a2b_hex(raw.strip())
- if raw[0:6] == b'cHNidP':
- raw = b64decode(raw)
- assert raw[0:5] == b'psbt\xff', "bad magic"
-
- self.filename = filename
-
- with io.BytesIO(raw[5:]) as fd:
-
- # globals
- while 1:
- ks = deser_compact_size(fd)
- if ks is None: break
-
- if ks == 0: break
-
- key = fd.read(ks)
- vs = deser_compact_size(fd)
- val = fd.read(vs)
-
- kt = key[0]
- if kt == PSBT_GLOBAL_UNSIGNED_TX:
- self.txn = val
-
- self.parsed_txn = Transaction(val.hex())
- num_ins = len(self.parsed_txn.inputs())
- num_outs = len(self.parsed_txn.outputs())
-
- elif kt == PSBT_GLOBAL_XPUB:
- # key=(xpub) => val=(path)
- self.xpubs.append( (key, val) )
- else:
- raise ValueError('unknown global key type: 0x%02x' % kt)
-
- assert self.txn, 'missing reqd section'
-
- self.inputs = [BasicPSBTInput(fd, idx) for idx in range(num_ins)]
- self.outputs = [BasicPSBTOutput(fd, idx) for idx in range(num_outs)]
-
- sep = fd.read(1)
- assert sep == b''
-
- return self
-
- def serialize(self, fd):
-
- def wr(ktype, val, key=b''):
- fd.write(ser_compact_size(1 + len(key)))
- fd.write(bytes([ktype]) + key)
- fd.write(ser_compact_size(len(val)))
- fd.write(val)
-
- fd.write(b'psbt\xff')
-
- wr(PSBT_GLOBAL_UNSIGNED_TX, self.txn)
-
- for k,v in self.xpubs:
- wr(PSBT_GLOBAL_XPUB, v, key=k)
-
- # sep
- fd.write(b'\0')
-
- for idx, inp in enumerate(self.inputs):
- inp.serialize(fd, idx)
-
- for idx, outp in enumerate(self.outputs):
- outp.serialize(fd, idx)
-
- def as_bytes(self):
- with io.BytesIO() as fd:
- self.serialize(fd)
- return fd.getvalue()
-
-# EOF
-
DIR diff --git a/electrum/plugins/coldcard/build_psbt.py b/electrum/plugins/coldcard/build_psbt.py
t@@ -1,397 +0,0 @@
-#
-# build_psbt.py - create a PSBT from (unsigned) transaction and keystore data.
-#
-import io
-import struct
-from binascii import a2b_hex, b2a_hex
-from struct import pack, unpack
-
-from electrum.transaction import (Transaction, multisig_script, parse_redeemScript_multisig,
- NotRecognizedRedeemScript)
-
-from electrum.logging import get_logger
-from electrum.wallet import Standard_Wallet, Multisig_Wallet, Abstract_Wallet
-from electrum.keystore import xpubkey_to_pubkey, Xpub
-from electrum.util import bfh, bh2u
-from electrum.crypto import hash_160, sha256
-from electrum.bitcoin import DecodeBase58Check
-from electrum.i18n import _
-
-from .basic_psbt import (
- PSBT_GLOBAL_UNSIGNED_TX, PSBT_GLOBAL_XPUB, PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_WITNESS_UTXO,
- PSBT_IN_SIGHASH_TYPE, PSBT_IN_REDEEM_SCRIPT, PSBT_IN_WITNESS_SCRIPT, PSBT_IN_PARTIAL_SIG,
- PSBT_IN_BIP32_DERIVATION, PSBT_OUT_BIP32_DERIVATION,
- PSBT_OUT_REDEEM_SCRIPT, PSBT_OUT_WITNESS_SCRIPT)
-from .basic_psbt import BasicPSBT
-
-
-_logger = get_logger(__name__)
-
-def xfp2str(xfp):
- # Standardized way to show an xpub's fingerprint... it's a 4-byte string
- # and not really an integer. Used to show as '0x%08x' but that's wrong endian.
- return b2a_hex(pack('<I', xfp)).decode('ascii').upper()
-
-def xfp_from_xpub(xpub):
- # sometime we need to BIP32 fingerprint value: 4 bytes of ripemd(sha256(pubkey))
- kk = bfh(Xpub.get_pubkey_from_xpub(xpub, []))
- assert len(kk) == 33
- xfp, = unpack('<I', hash_160(kk)[0:4])
- return xfp
-
-def packed_xfp_path(xfp, text_path, int_path=[]):
- # Convert text subkey derivation path into binary format needed for PSBT
- # - binary LE32 values, first one is the fingerprint
- rv = pack('<I', xfp)
-
- for x in text_path.split('/'):
- if x == 'm': continue
- if x.endswith("'"):
- x = int(x[:-1]) | 0x80000000
- else:
- x = int(x)
- rv += pack('<I', x)
-
- for x in int_path:
- rv += pack('<I', x)
-
- return rv
-
-def unpacked_xfp_path(xfp, text_path):
- # Convert text subkey derivation path into format needed for PSBT
- # - binary LE32 values, first one is the fingerprint
- # - but as ints, not bytes yet
- rv = [xfp]
-
- for x in text_path.split('/'):
- if x == 'm': continue
- if x.endswith("'"):
- x = int(x[:-1]) | 0x80000000
- else:
- x = int(x)
- rv.append(x)
-
- return rv
-
-def xfp_for_keystore(ks):
- # Need the fingerprint of the MASTER key for a keystore we're playing with.
- xfp = getattr(ks, 'ckcc_xfp', None)
-
- if xfp is None:
- xfp = xfp_from_xpub(ks.get_master_public_key())
- setattr(ks, 'ckcc_xfp', xfp)
-
- return xfp
-
-def packed_xfp_path_for_keystore(ks, int_path=[]):
- # Return XFP + common prefix path for keystore, as binary ready for PSBT
- derv = getattr(ks, 'derivation', 'm')
- return packed_xfp_path(xfp_for_keystore(ks), derv[2:] or 'm', int_path=int_path)
-
-# Serialization/deserialization tools
-def ser_compact_size(l):
- r = b""
- if l < 253:
- r = struct.pack("B", l)
- elif l < 0x10000:
- r = struct.pack("<BH", 253, l)
- elif l < 0x100000000:
- r = struct.pack("<BI", 254, l)
- else:
- r = struct.pack("<BQ", 255, l)
- return r
-
-def deser_compact_size(f):
- try:
- nit = f.read(1)[0]
- except IndexError:
- return None # end of file
-
- if nit == 253:
- nit = struct.unpack("<H", f.read(2))[0]
- elif nit == 254:
- nit = struct.unpack("<I", f.read(4))[0]
- elif nit == 255:
- nit = struct.unpack("<Q", f.read(8))[0]
- return nit
-
-def my_var_int(l):
- # Bitcoin serialization of integers... directly into binary!
- if l < 253:
- return pack("B", l)
- elif l < 0x10000:
- return pack("<BH", 253, l)
- elif l < 0x100000000:
- return pack("<BI", 254, l)
- else:
- return pack("<BQ", 255, l)
-
-def build_psbt(tx: Transaction, wallet: Abstract_Wallet):
- # Render a PSBT file, for possible upload to Coldcard.
- #
- # TODO this should be part of Wallet object, or maybe Transaction?
-
- if getattr(tx, 'raw_psbt', False):
- _logger.info('PSBT cache hit')
- return tx.raw_psbt
-
- inputs = tx.inputs()
- if 'prev_tx' not in inputs[0]:
- # fetch info about inputs, if needed?
- # - needed during export PSBT flow, not normal online signing
- wallet.add_hw_info(tx)
-
- # wallet.add_hw_info installs this attr
- assert tx.output_info is not None, 'need data about outputs'
-
- # Build a map of all pubkeys needed as derivation from master XFP, in PSBT binary format
- # 1) binary version of the common subpath for all keys
- # m/ => fingerprint LE32
- # a/b/c => ints
- #
- # 2) all used keys in transaction:
- # - for all inputs and outputs (when its change back)
- # - for all keystores, if multisig
- #
- subkeys = {}
- for ks in wallet.get_keystores():
-
- # XFP + fixed prefix for this keystore
- ks_prefix = packed_xfp_path_for_keystore(ks)
-
- # all pubkeys needed for input signing
- for xpubkey, derivation in ks.get_tx_derivations(tx).items():
- pubkey = xpubkey_to_pubkey(xpubkey)
-
- # assuming depth two, non-harded: change + index
- aa, bb = derivation
- assert 0 <= aa < 0x80000000 and 0 <= bb < 0x80000000
-
- subkeys[bfh(pubkey)] = ks_prefix + pack('<II', aa, bb)
-
- # all keys related to change outputs
- for o in tx.outputs():
- if o.address in tx.output_info:
- # this address "is_mine" but might not be change (if I send funds to myself)
- output_info = tx.output_info.get(o.address)
- if not output_info.is_change:
- continue
- chg_path = output_info.address_index
- assert chg_path[0] == 1 and len(chg_path) == 2, f"unexpected change path: {chg_path}"
- pubkey = ks.derive_pubkey(True, chg_path[1])
- subkeys[bfh(pubkey)] = ks_prefix + pack('<II', *chg_path)
-
- for txin in inputs:
- assert txin['type'] != 'coinbase', _("Coinbase not supported")
-
- if txin['type'] in ['p2sh', 'p2wsh-p2sh', 'p2wsh']:
- assert type(wallet) is Multisig_Wallet
-
- # Construct PSBT from start to finish.
- out_fd = io.BytesIO()
- out_fd.write(b'psbt\xff')
-
- def write_kv(ktype, val, key=b''):
- # serialize helper: write w/ size and key byte
- out_fd.write(my_var_int(1 + len(key)))
- out_fd.write(bytes([ktype]) + key)
-
- if isinstance(val, str):
- val = bfh(val)
-
- out_fd.write(my_var_int(len(val)))
- out_fd.write(val)
-
-
- # global section: just the unsigned txn
- class CustomTXSerialization(Transaction):
- @classmethod
- def input_script(cls, txin, estimate_size=False):
- return ''
-
- unsigned = bfh(CustomTXSerialization(tx.serialize()).serialize_to_network(witness=False))
- write_kv(PSBT_GLOBAL_UNSIGNED_TX, unsigned)
-
- if type(wallet) is Multisig_Wallet:
-
- # always put the xpubs into the PSBT, useful at least for checking
- for xp, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()):
- ks_prefix = packed_xfp_path_for_keystore(ks)
-
- write_kv(PSBT_GLOBAL_XPUB, ks_prefix, DecodeBase58Check(xp))
-
- # end globals section
- out_fd.write(b'\x00')
-
- # inputs section
- for txin in inputs:
- if Transaction.is_segwit_input(txin):
- utxo = txin['prev_tx'].outputs()[txin['prevout_n']]
- spendable = txin['prev_tx'].serialize_output(utxo)
- write_kv(PSBT_IN_WITNESS_UTXO, spendable)
- else:
- write_kv(PSBT_IN_NON_WITNESS_UTXO, str(txin['prev_tx']))
-
- pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
-
- pubkeys = [bfh(k) for k in pubkeys]
-
- if type(wallet) is Multisig_Wallet:
- # always need a redeem script for multisig
- scr = Transaction.get_preimage_script(txin)
-
- if Transaction.is_segwit_input(txin):
- # needed for both p2wsh-p2sh and p2wsh
- write_kv(PSBT_IN_WITNESS_SCRIPT, bfh(scr))
- else:
- write_kv(PSBT_IN_REDEEM_SCRIPT, bfh(scr))
-
- sigs = txin.get('signatures')
-
- for pk_pos, (pubkey, x_pubkey) in enumerate(zip(pubkeys, x_pubkeys)):
- if pubkey in subkeys:
- # faster? case ... calculated above
- write_kv(PSBT_IN_BIP32_DERIVATION, subkeys[pubkey], pubkey)
- else:
- # when an input is partly signed, tx.get_tx_derivations()
- # doesn't include that keystore's value and yet we need it
- # because we need to show a correct keypath...
- assert x_pubkey[0:2] == 'ff', x_pubkey
-
- for ks in wallet.get_keystores():
- d = ks.get_pubkey_derivation(x_pubkey)
- if d is not None:
- ks_path = packed_xfp_path_for_keystore(ks, d)
- write_kv(PSBT_IN_BIP32_DERIVATION, ks_path, pubkey)
- break
- else:
- raise AssertionError("no keystore for: %s" % x_pubkey)
-
- if txin['type'] == 'p2wpkh-p2sh':
- assert len(pubkeys) == 1, 'can be only one redeem script per input'
- pa = hash_160(pubkey)
- assert len(pa) == 20
- write_kv(PSBT_IN_REDEEM_SCRIPT, b'\x00\x14'+pa)
-
- # optional? insert (partial) signatures that we already have
- if sigs and sigs[pk_pos]:
- write_kv(PSBT_IN_PARTIAL_SIG, bfh(sigs[pk_pos]), pubkey)
-
- out_fd.write(b'\x00')
-
- # outputs section
- for o in tx.outputs():
- # can be empty, but must be present, and helpful to show change inputs
- # wallet.add_hw_info() adds some data about change outputs into tx.output_info
- if o.address in tx.output_info:
- # this address "is_mine" but might not be change (if I send funds to myself)
- output_info = tx.output_info.get(o.address)
- if output_info.is_change:
- pubkeys = [bfh(i) for i in wallet.get_public_keys(o.address)]
-
- # Add redeem/witness script?
- if type(wallet) is Multisig_Wallet:
- # always need a redeem script for multisig cases
- scr = bfh(multisig_script([bh2u(i) for i in sorted(pubkeys)], wallet.m))
-
- if output_info.script_type == 'p2wsh-p2sh':
- write_kv(PSBT_OUT_WITNESS_SCRIPT, scr)
- write_kv(PSBT_OUT_REDEEM_SCRIPT, b'\x00\x20' + sha256(scr))
- elif output_info.script_type == 'p2wsh':
- write_kv(PSBT_OUT_WITNESS_SCRIPT, scr)
- elif output_info.script_type == 'p2sh':
- write_kv(PSBT_OUT_REDEEM_SCRIPT, scr)
- else:
- raise ValueError(output_info.script_type)
-
- elif output_info.script_type == 'p2wpkh-p2sh':
- # need a redeem script when P2SH is used to wrap p2wpkh
- assert len(pubkeys) == 1
- pa = hash_160(pubkeys[0])
- write_kv(PSBT_OUT_REDEEM_SCRIPT, b'\x00\x14' + pa)
-
- # Document change output's bip32 derivation(s)
- for pubkey in pubkeys:
- sk = subkeys[pubkey]
- write_kv(PSBT_OUT_BIP32_DERIVATION, sk, pubkey)
-
- out_fd.write(b'\x00')
-
- # capture for later use
- tx.raw_psbt = out_fd.getvalue()
-
- return tx.raw_psbt
-
-
-def recover_tx_from_psbt(first: BasicPSBT, wallet: Abstract_Wallet) -> Transaction:
- # Take a PSBT object and re-construct the Electrum transaction object.
- # - does not include signatures, see merge_sigs_from_psbt
- # - any PSBT in the group could be used for this purpose; all must share tx details
-
- tx = Transaction(first.txn.hex())
- tx.deserialize(force_full_parse=True)
-
- # .. add back some data that's been preserved in the PSBT, but isn't part of
- # of the unsigned bitcoin txn
- tx.is_partial_originally = True
-
- for idx, inp in enumerate(tx.inputs()):
- scr = first.inputs[idx].redeem_script or first.inputs[idx].witness_script
-
- # XXX should use transaction.py parse_scriptSig() here!
- if scr:
- try:
- M, N, __, pubkeys, __ = parse_redeemScript_multisig(scr)
- except NotRecognizedRedeemScript:
- # limitation: we can only handle M-of-N multisig here
- raise ValueError("Cannot handle non M-of-N multisig input")
-
- inp['pubkeys'] = pubkeys
- inp['x_pubkeys'] = pubkeys
- inp['num_sig'] = M
- inp['type'] = 'p2wsh' if first.inputs[idx].witness_script else 'p2sh'
-
- # bugfix: transaction.py:parse_input() puts empty dict here, but need a list
- inp['signatures'] = [None] * N
-
- if 'prev_tx' not in inp:
- # fetch info about inputs' previous txn
- wallet.add_hw_info(tx)
-
- if 'value' not in inp:
- # we'll need to know the value of the outpts used as part
- # of the witness data, much later...
- inp['value'] = inp['prev_tx'].outputs()[inp['prevout_n']].value
-
- return tx
-
-def merge_sigs_from_psbt(tx: Transaction, psbt: BasicPSBT):
- # Take new signatures from PSBT, and merge into in-memory transaction object.
- # - "we trust everyone here" ... no validation/checks
-
- count = 0
- for inp_idx, inp in enumerate(psbt.inputs):
- if not inp.part_sigs:
- continue
-
- scr = inp.redeem_script or inp.witness_script
-
- # need to map from pubkey to signing position in redeem script
- M, N, _, pubkeys, _ = parse_redeemScript_multisig(scr)
- #assert (M, N) == (wallet.m, wallet.n)
-
- for sig_pk in inp.part_sigs:
- pk_pos = pubkeys.index(sig_pk.hex())
- tx.add_signature_to_txin(inp_idx, pk_pos, inp.part_sigs[sig_pk].hex())
- count += 1
-
- #print("#%d: sigs = %r" % (inp_idx, tx.inputs()[inp_idx]['signatures']))
-
- # reset serialization of TX
- tx.raw = tx.serialize()
- tx.raw_psbt = None
-
- return count
-
-# EOF
-
DIR diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py
t@@ -2,16 +2,18 @@
# Coldcard Electrum plugin main code.
#
#
-from struct import pack, unpack
-import os, sys, time, io
+import os, time, io
import traceback
+from typing import TYPE_CHECKING
+import struct
+from electrum import bip32
from electrum.bip32 import BIP32Node, InvalidMasterKeyVersionBytes
from electrum.i18n import _
from electrum.plugin import Device, hook
from electrum.keystore import Hardware_KeyStore
-from electrum.transaction import Transaction, multisig_script
-from electrum.wallet import Standard_Wallet, Multisig_Wallet
+from electrum.transaction import PartialTransaction
+from electrum.wallet import Standard_Wallet, Multisig_Wallet, Abstract_Wallet
from electrum.util import bfh, bh2u, versiontuple, UserFacingException
from electrum.base_wizard import ScriptTypeNotSupported
from electrum.logging import get_logger
t@@ -19,9 +21,9 @@ from electrum.logging import get_logger
from ..hw_wallet import HW_PluginBase
from ..hw_wallet.plugin import LibraryFoundButUnusable, only_hook_if_libraries_available
-from .basic_psbt import BasicPSBT
-from .build_psbt import (build_psbt, xfp2str, unpacked_xfp_path,
- merge_sigs_from_psbt, xfp_for_keystore)
+if TYPE_CHECKING:
+ from electrum.keystore import Xpub
+
_logger = get_logger(__name__)
t@@ -86,7 +88,7 @@ class CKCCClient:
return '<CKCCClient: xfp=%s label=%r>' % (xfp2str(self.dev.master_fingerprint),
self.label())
- def verify_connection(self, expected_xfp, expected_xpub=None):
+ def verify_connection(self, expected_xfp: int, expected_xpub=None):
ex = (expected_xfp, expected_xpub)
if self._expected_device == ex:
t@@ -213,7 +215,7 @@ class CKCCClient:
# poll device... if user has approved, will get tuple: (addr, sig) else None
return self.dev.send_recv(CCProtocolPacker.get_signed_msg(), timeout=None)
- def sign_transaction_start(self, raw_psbt, finalize=True):
+ def sign_transaction_start(self, raw_psbt: bytes, *, finalize: bool = False):
# Multiple steps to sign:
# - upload binary
# - start signing UX
t@@ -242,6 +244,8 @@ class Coldcard_KeyStore(Hardware_KeyStore):
hw_type = 'coldcard'
device = 'Coldcard'
+ plugin: 'ColdcardPlugin'
+
def __init__(self, d):
Hardware_KeyStore.__init__(self, d)
# Errors and other user interaction is done through the wallet's
t@@ -250,39 +254,22 @@ class Coldcard_KeyStore(Hardware_KeyStore):
self.force_watching_only = False
self.ux_busy = False
- # for multisig I need to know what wallet this keystore is part of
- # will be set by link_wallet
- self.my_wallet = None
-
- # Seems like only the derivation path and resulting **derived** xpub is stored in
- # the wallet file... however, we need to know at least the fingerprint of the master
- # xpub to verify against MiTM, and also so we can put the right value into the subkey paths
- # of PSBT files that might be generated offline.
- # - save the fingerprint of the master xpub, as "xfp"
- # - it's a LE32 int, but hex BE32 is more natural way to view it
+ # we need to know at least the fingerprint of the master xpub to verify against MiTM
# - device reports these value during encryption setup process
# - full xpub value now optional
lab = d['label']
- if hasattr(lab, 'xfp'):
- # initial setup
- self.ckcc_xfp = lab.xfp
- self.ckcc_xpub = getattr(lab, 'xpub', None)
- else:
- # wallet load: fatal if missing, we need them!
- self.ckcc_xfp = d['ckcc_xfp']
- self.ckcc_xpub = d.get('ckcc_xpub', None)
+ self.ckcc_xpub = getattr(lab, 'xpub', None) or d.get('ckcc_xpub', None)
def dump(self):
# our additions to the stored data about keystore -- only during creation?
d = Hardware_KeyStore.dump(self)
-
- d['ckcc_xfp'] = self.ckcc_xfp
d['ckcc_xpub'] = self.ckcc_xpub
-
return d
- def get_derivation(self):
- return self.derivation
+ def get_xfp_int(self) -> int:
+ xfp = self.get_root_fingerprint()
+ assert xfp is not None
+ return xfp_int_from_xfp_bytes(bfh(xfp))
def get_client(self):
# called when user tries to do something like view address, sign somthing.
t@@ -290,7 +277,8 @@ class Coldcard_KeyStore(Hardware_KeyStore):
# - will fail if indicated device can't produce the xpub (at derivation) expected
rv = self.plugin.get_client(self)
if rv:
- rv.verify_connection(self.ckcc_xfp, self.ckcc_xpub)
+ xfp_int = self.get_xfp_int()
+ rv.verify_connection(xfp_int, self.ckcc_xpub)
return rv
t@@ -332,7 +320,7 @@ class Coldcard_KeyStore(Hardware_KeyStore):
return b''
client = self.get_client()
- path = self.get_derivation() + ("/%d/%d" % sequence)
+ path = self.get_derivation_prefix() + ("/%d/%d" % sequence)
try:
cl = self.get_client()
try:
t@@ -372,28 +360,23 @@ class Coldcard_KeyStore(Hardware_KeyStore):
return b''
@wrap_busy
- def sign_transaction(self, tx: Transaction, password):
- # Build a PSBT in memory, upload it for signing.
+ def sign_transaction(self, tx, password):
+ # Upload PSBT for signing.
# - we can also work offline (without paired device present)
if tx.is_complete():
return
- assert self.my_wallet, "Not clear which wallet associated with this Coldcard"
-
client = self.get_client()
- assert client.dev.master_fingerprint == self.ckcc_xfp
+ assert client.dev.master_fingerprint == self.get_xfp_int()
- # makes PSBT required
- raw_psbt = build_psbt(tx, self.my_wallet)
-
- cc_finalize = not (type(self.my_wallet) is Multisig_Wallet)
+ raw_psbt = tx.serialize_as_bytes()
try:
try:
self.handler.show_message("Authorize Transaction...")
- client.sign_transaction_start(raw_psbt, cc_finalize)
+ client.sign_transaction_start(raw_psbt)
while 1:
# How to kill some time, without locking UI?
t@@ -420,18 +403,11 @@ class Coldcard_KeyStore(Hardware_KeyStore):
self.give_error(e, True)
return
- if cc_finalize:
- # We trust the coldcard to re-serialize final transaction ready to go
- tx.update(bh2u(raw_resp))
- else:
- # apply partial signatures back into txn
- psbt = BasicPSBT()
- psbt.parse(raw_resp, client.label())
-
- merge_sigs_from_psbt(tx, psbt)
-
- # caller's logic looks at tx now and if it's sufficiently signed,
- # will send it if that's the user's intent.
+ tx2 = PartialTransaction.from_raw_psbt(raw_resp)
+ # apply partial signatures back into txn
+ tx.combine_with_other_psbt(tx2)
+ # caller's logic looks at tx now and if it's sufficiently signed,
+ # will send it if that's the user's intent.
@staticmethod
def _encode_txin_type(txin_type):
t@@ -447,7 +423,7 @@ class Coldcard_KeyStore(Hardware_KeyStore):
@wrap_busy
def show_address(self, sequence, txin_type):
client = self.get_client()
- address_path = self.get_derivation()[2:] + "/%d/%d"%sequence
+ address_path = self.get_derivation_prefix()[2:] + "/%d/%d"%sequence
addr_fmt = self._encode_txin_type(txin_type)
try:
try:
t@@ -573,7 +549,7 @@ class ColdcardPlugin(HW_PluginBase):
xpub = client.get_xpub(derivation, xtype)
return xpub
- def get_client(self, keystore, force_pair=True):
+ def get_client(self, keystore, force_pair=True) -> 'CKCCClient':
# Acquire a connection to the hardware device (via USB)
devmgr = self.device_manager()
handler = keystore.handler
t@@ -586,9 +562,10 @@ class ColdcardPlugin(HW_PluginBase):
return client
@staticmethod
- def export_ms_wallet(wallet, fp, name):
+ def export_ms_wallet(wallet: Multisig_Wallet, fp, name):
# Build the text file Coldcard needs to understand the multisig wallet
# it is participating in. All involved Coldcards can share same file.
+ assert isinstance(wallet, Multisig_Wallet)
print('# Exported from Electrum', file=fp)
print(f'Name: {name:.20s}', file=fp)
t@@ -597,12 +574,12 @@ class ColdcardPlugin(HW_PluginBase):
xpubs = []
derivs = set()
- for xp, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()):
- xfp = xfp_for_keystore(ks)
- dd = getattr(ks, 'derivation', 'm')
-
- xpubs.append( (xfp2str(xfp), xp, dd) )
- derivs.add(dd)
+ for xpub, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()):
+ fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix=[], only_der_suffix=False)
+ fp_hex = fp_bytes.hex().upper()
+ der_prefix_str = bip32.convert_bip32_intpath_to_strpath(der_full)
+ xpubs.append( (fp_hex, xpub, der_prefix_str) )
+ derivs.add(der_prefix_str)
# Derivation doesn't matter too much to the Coldcard, since it
# uses key path data from PSBT or USB request as needed. However,
t@@ -613,14 +590,14 @@ class ColdcardPlugin(HW_PluginBase):
print('', file=fp)
assert len(xpubs) == wallet.n
- for xfp, xp, dd in xpubs:
+ for xfp, xpub, der_prefix in xpubs:
if derivs:
# show as a comment if unclear
- print(f'# derivation: {dd}', file=fp)
+ print(f'# derivation: {der_prefix}', file=fp)
- print(f'{xfp}: {xp}\n', file=fp)
+ print(f'{xfp}: {xpub}\n', file=fp)
- def show_address(self, wallet, address, keystore=None):
+ def show_address(self, wallet, address, keystore: 'Coldcard_KeyStore' = None):
if keystore is None:
keystore = wallet.get_keystore()
if not self.show_address_helper(wallet, address, keystore):
t@@ -633,50 +610,36 @@ class ColdcardPlugin(HW_PluginBase):
sequence = wallet.get_address_index(address)
keystore.show_address(sequence, txin_type)
elif type(wallet) is Multisig_Wallet:
+ assert isinstance(wallet, Multisig_Wallet) # only here for type-hints in IDE
# More involved for P2SH/P2WSH addresses: need M, and all public keys, and their
# derivation paths. Must construct script, and track fingerprints+paths for
# all those keys
- pubkeys = wallet.get_public_keys(address)
-
- xfps = []
- for xp, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()):
- path = "%s/%d/%d" % (getattr(ks, 'derivation', 'm'),
- *wallet.get_address_index(address))
-
- # need master XFP for each co-signers
- ks_xfp = xfp_for_keystore(ks)
- xfps.append(unpacked_xfp_path(ks_xfp, path))
+ pubkey_deriv_info = wallet.get_public_keys_with_deriv_info(address)
+ pubkeys = sorted([pk for pk in list(pubkey_deriv_info)])
+ xfp_paths = []
+ for pubkey_hex in pubkey_deriv_info:
+ ks, der_suffix = pubkey_deriv_info[pubkey_hex]
+ fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix, only_der_suffix=False)
+ xfp_int = xfp_int_from_xfp_bytes(fp_bytes)
+ xfp_paths.append([xfp_int] + list(der_full))
- # put into BIP45 (sorted) order
- pkx = list(sorted(zip(pubkeys, xfps)))
+ script = bfh(wallet.pubkeys_to_scriptcode(pubkeys))
- script = bfh(multisig_script([pk for pk,xfp in pkx], wallet.m))
-
- keystore.show_p2sh_address(wallet.m, script, [xfp for pk,xfp in pkx], txin_type)
+ keystore.show_p2sh_address(wallet.m, script, xfp_paths, txin_type)
else:
keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device))
return
- @classmethod
- def link_wallet(cls, wallet):
- # PROBLEM: wallet.sign_transaction() does not pass in the wallet to the individual
- # keystores, and we need to know about our co-signers at that time.
- # FIXME the keystore needs a reference to the wallet object because
- # it constructs a PSBT from an electrum tx object inside keystore.sign_transaction.
- # instead keystore.sign_transaction's API should be changed such that its input
- # *is* a PSBT and not an electrum tx object
- for ks in wallet.get_keystores():
- if type(ks) == Coldcard_KeyStore:
- if not ks.my_wallet:
- ks.my_wallet = wallet
-
- @hook
- def load_wallet(self, wallet, window):
- # make sure hook in superclass also runs:
- if hasattr(super(), 'load_wallet'):
- super().load_wallet(wallet, window)
- self.link_wallet(wallet)
+
+def xfp_int_from_xfp_bytes(fp_bytes: bytes) -> int:
+ return int.from_bytes(fp_bytes, byteorder="little", signed=False)
+
+
+def xfp2str(xfp: int) -> str:
+ # Standardized way to show an xpub's fingerprint... it's a 4-byte string
+ # and not really an integer. Used to show as '0x%08x' but that's wrong endian.
+ return struct.pack('<I', xfp).hex().lower()
# EOF
DIR diff --git a/electrum/plugins/coldcard/qt.py b/electrum/plugins/coldcard/qt.py
t@@ -1,25 +1,22 @@
import time, os
from functools import partial
+import copy
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import QPushButton, QLabel, QVBoxLayout, QWidget, QGridLayout
-from PyQt5.QtWidgets import QFileDialog
+
+from electrum.gui.qt.util import WindowModalDialog, CloseButton, get_parent_main_window, Buttons
+from electrum.gui.qt.transaction_dialog import TxDialog
from electrum.i18n import _
from electrum.plugin import hook
-from electrum.wallet import Standard_Wallet, Multisig_Wallet
-from electrum.gui.qt.util import WindowModalDialog, CloseButton, get_parent_main_window, Buttons
-from electrum.transaction import Transaction
+from electrum.wallet import Multisig_Wallet
+from electrum.transaction import PartialTransaction
from .coldcard import ColdcardPlugin, xfp2str
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
from ..hw_wallet.plugin import only_hook_if_libraries_available
-from binascii import a2b_hex
-from base64 import b64encode, b64decode
-
-from .basic_psbt import BasicPSBT
-from .build_psbt import build_psbt, merge_sigs_from_psbt, recover_tx_from_psbt
CC_DEBUG = False
t@@ -73,135 +70,29 @@ class Plugin(ColdcardPlugin, QtPluginBase):
ColdcardPlugin.export_ms_wallet(wallet, f, basename)
main_window.show_message(_("Wallet setup file exported successfully"))
- @only_hook_if_libraries_available
@hook
- def transaction_dialog(self, dia):
- # see gui/qt/transaction_dialog.py
-
+ def transaction_dialog(self, dia: TxDialog):
# if not a Coldcard wallet, hide feature
if not any(type(ks) == self.keystore_class for ks in dia.wallet.get_keystores()):
return
- # - add a new button, near "export"
- btn = QPushButton(_("Save PSBT"))
- btn.clicked.connect(lambda unused: self.export_psbt(dia))
- if dia.tx.is_complete():
- # but disable it for signed transactions (nothing to do if already signed)
- btn.setDisabled(True)
-
- dia.sharing_buttons.append(btn)
-
- def export_psbt(self, dia):
- # Called from hook in transaction dialog
- tx = dia.tx
-
- if tx.is_complete():
- # if they sign while dialog is open, it can transition from unsigned to signed,
- # which we don't support here, so do nothing
- return
-
- # convert to PSBT
- build_psbt(tx, dia.wallet)
+ def gettx_for_coldcard_export() -> PartialTransaction:
+ if not isinstance(dia.tx, PartialTransaction):
+ raise Exception("Can only export partial transactions for coinjoins.")
+ tx = copy.deepcopy(dia.tx)
+ tx.add_info_from_wallet(dia.wallet, include_xpubs_and_full_paths=True)
+ return tx
- name = (dia.wallet.basename() + time.strftime('-%y%m%d-%H%M.psbt'))\
- .replace(' ', '-').replace('.json', '')
- fileName = dia.main_window.getSaveFileName(_("Select where to save the PSBT file"),
- name, "*.psbt")
- if fileName:
- with open(fileName, "wb+") as f:
- f.write(tx.raw_psbt)
- dia.show_message(_("Transaction exported successfully"))
- dia.saved = True
+ # add a new "export" option
+ if isinstance(dia.tx, PartialTransaction):
+ export_submenu = dia.export_actions_menu.addMenu(_("For {}; include xpubs").format(self.device))
+ dia.add_export_actions_to_menu(export_submenu, gettx=gettx_for_coldcard_export)
def show_settings_dialog(self, window, keystore):
# When they click on the icon for CC we come here.
# - doesn't matter if device not connected, continue
CKCCSettingsDialog(window, self, keystore).exec_()
- @hook
- def init_menubar_tools(self, main_window, tools_menu):
- # add some PSBT-related tools to the "Load Transaction" menu.
- rt = main_window.raw_transaction_menu
- wallet = main_window.wallet
- rt.addAction(_("From &PSBT File or Files"), lambda: self.psbt_combiner(main_window, wallet))
-
- def psbt_combiner(self, window, wallet):
- title = _("Select the PSBT file to load or PSBT files to combine")
- directory = ''
- fnames, __ = QFileDialog.getOpenFileNames(window, title, directory, "PSBT Files (*.psbt)")
-
- psbts = []
- for fn in fnames:
- try:
- with open(fn, "rb") as f:
- raw = f.read()
-
- psbt = BasicPSBT()
- psbt.parse(raw, fn)
-
- psbts.append(psbt)
- except (AssertionError, ValueError, IOError, os.error) as reason:
- window.show_critical(_("Electrum was unable to open your PSBT file") + "\n" + str(reason), title=_("Unable to read file"))
- return
-
- warn = []
- if not psbts: return # user picked nothing
-
- # Consistency checks and warnings.
- try:
- first = psbts[0]
- for p in psbts:
- fn = os.path.split(p.filename)[1]
-
- assert (p.txn == first.txn), \
- "All must relate to the same unsigned transaction."
-
- for idx, inp in enumerate(p.inputs):
- if not inp.part_sigs:
- warn.append(fn + ':\n ' + _("No partial signatures found for input #%d") % idx)
-
- assert first.inputs[idx].redeem_script == inp.redeem_script, "Mismatched redeem scripts"
- assert first.inputs[idx].witness_script == inp.witness_script, "Mismatched witness"
-
- except AssertionError as exc:
- # Fatal errors stop here.
- window.show_critical(str(exc),
- title=_("Unable to combine PSBT files, check: ")+p.filename)
- return
-
- if warn:
- # Lots of potential warnings...
- window.show_warning('\n\n'.join(warn), title=_("PSBT warnings"))
-
- # Construct an Electrum transaction object from data in first PSBT file.
- try:
- tx = recover_tx_from_psbt(first, wallet)
- except BaseException as exc:
- if CC_DEBUG:
- from PyQt5.QtCore import pyqtRemoveInputHook; pyqtRemoveInputHook()
- import pdb; pdb.post_mortem()
- window.show_critical(str(exc), title=_("Unable to understand PSBT file"))
- return
-
- # Combine the signatures from all the PSBTS (may do nothing if unsigned PSBTs)
- for p in psbts:
- try:
- merge_sigs_from_psbt(tx, p)
- except BaseException as exc:
- if CC_DEBUG:
- from PyQt5.QtCore import pyqtRemoveInputHook; pyqtRemoveInputHook()
- import pdb; pdb.post_mortem()
- window.show_critical("Unable to merge signatures: " + str(exc),
- title=_("Unable to combine PSBT file: ") + p.filename)
- return
-
- # Display result, might not be complete yet, but hopefully it's ready to transmit!
- if len(psbts) == 1:
- desc = _("From PSBT file: ") + fn
- else:
- desc = _("Combined from %d PSBT files") % len(psbts)
-
- window.show_transaction(tx, tx_desc=desc)
class Coldcard_Handler(QtHandlerBase):
setup_signal = pyqtSignal()
t@@ -307,7 +198,7 @@ class CKCCSettingsDialog(WindowModalDialog):
def show_placeholders(self, unclear_arg):
# device missing, so hide lots of detail.
- self.xfp.setText('<tt>%s' % xfp2str(self.keystore.ckcc_xfp))
+ self.xfp.setText('<tt>%s' % self.keystore.get_root_fingerprint())
self.serial.setText('(not connected)')
self.fw_version.setText('')
self.fw_built.setText('')
DIR diff --git a/electrum/plugins/cosigner_pool/qt.py b/electrum/plugins/cosigner_pool/qt.py
t@@ -25,23 +25,25 @@
import time
from xmlrpc.client import ServerProxy
+from typing import TYPE_CHECKING, Union, List, Tuple
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtWidgets import QPushButton
from electrum import util, keystore, ecc, crypto
from electrum import transaction
+from electrum.transaction import Transaction, PartialTransaction, tx_from_any
from electrum.bip32 import BIP32Node
from electrum.plugin import BasePlugin, hook
from electrum.i18n import _
from electrum.wallet import Multisig_Wallet
from electrum.util import bh2u, bfh
-from electrum.gui.qt.transaction_dialog import show_transaction
+from electrum.gui.qt.transaction_dialog import show_transaction, TxDialog
from electrum.gui.qt.util import WaitingDialog
-import sys
-import traceback
+if TYPE_CHECKING:
+ from electrum.gui.qt.main_window import ElectrumWindow
server = ServerProxy('https://cosigner.electrum.org/', allow_none=True)
t@@ -97,8 +99,8 @@ class Plugin(BasePlugin):
self.listener = None
self.obj = QReceiveSignalObject()
self.obj.cosigner_receive_signal.connect(self.on_receive)
- self.keys = []
- self.cosigner_list = []
+ self.keys = [] # type: List[Tuple[str, str, ElectrumWindow]]
+ self.cosigner_list = [] # type: List[Tuple[ElectrumWindow, str, bytes, str]]
@hook
def init_qt(self, gui):
t@@ -116,10 +118,11 @@ class Plugin(BasePlugin):
def is_available(self):
return True
- def update(self, window):
+ def update(self, window: 'ElectrumWindow'):
wallet = window.wallet
if type(wallet) != Multisig_Wallet:
return
+ assert isinstance(wallet, Multisig_Wallet) # only here for type-hints in IDE
if self.listener is None:
self.logger.info("starting listener")
self.listener = Listener(self)
t@@ -131,7 +134,7 @@ class Plugin(BasePlugin):
self.keys = []
self.cosigner_list = []
for key, keystore in wallet.keystores.items():
- xpub = keystore.get_master_public_key()
+ xpub = keystore.get_master_public_key() # type: str
pubkey = BIP32Node.from_xkey(xpub).eckey.get_public_key_bytes(compressed=True)
_hash = bh2u(crypto.sha256d(pubkey))
if not keystore.is_watching_only():
t@@ -142,14 +145,14 @@ class Plugin(BasePlugin):
self.listener.set_keyhashes([t[1] for t in self.keys])
@hook
- def transaction_dialog(self, d):
+ def transaction_dialog(self, d: 'TxDialog'):
d.cosigner_send_button = b = QPushButton(_("Send to cosigner"))
b.clicked.connect(lambda: self.do_send(d.tx))
d.buttons.insert(0, b)
self.transaction_dialog_update(d)
@hook
- def transaction_dialog_update(self, d):
+ def transaction_dialog_update(self, d: 'TxDialog'):
if d.tx.is_complete() or d.wallet.can_sign(d.tx):
d.cosigner_send_button.hide()
return
t@@ -160,17 +163,14 @@ class Plugin(BasePlugin):
else:
d.cosigner_send_button.hide()
- def cosigner_can_sign(self, tx, cosigner_xpub):
- from electrum.keystore import is_xpubkey, parse_xpubkey
- xpub_set = set([])
- for txin in tx.inputs():
- for x_pubkey in txin['x_pubkeys']:
- if is_xpubkey(x_pubkey):
- xpub, s = parse_xpubkey(x_pubkey)
- xpub_set.add(xpub)
- return cosigner_xpub in xpub_set
-
- def do_send(self, tx):
+ def cosigner_can_sign(self, tx: Transaction, cosigner_xpub: str) -> bool:
+ if not isinstance(tx, PartialTransaction):
+ return False
+ if tx.is_complete():
+ return False
+ return cosigner_xpub in {bip32node.to_xpub() for bip32node in tx.xpubs}
+
+ def do_send(self, tx: Union[Transaction, PartialTransaction]):
def on_success(result):
window.show_message(_("Your transaction was sent to the cosigning pool.") + '\n' +
_("Open your cosigner wallet to retrieve it."))
t@@ -184,7 +184,7 @@ class Plugin(BasePlugin):
if not self.cosigner_can_sign(tx, xpub):
continue
# construct message
- raw_tx_bytes = bfh(str(tx))
+ raw_tx_bytes = tx.serialize_as_bytes()
public_key = ecc.ECPubkey(K)
message = public_key.encrypt_message(raw_tx_bytes).decode('ascii')
# send message
t@@ -223,12 +223,12 @@ class Plugin(BasePlugin):
return
try:
privkey = BIP32Node.from_xkey(xprv).eckey
- message = bh2u(privkey.decrypt_message(message))
+ message = privkey.decrypt_message(message)
except Exception as e:
self.logger.exception('')
window.show_error(_('Error decrypting message') + ':\n' + repr(e))
return
self.listener.clear(keyhash)
- tx = transaction.Transaction(message)
- show_transaction(tx, window, prompt_if_unsaved=True)
+ tx = tx_from_any(message)
+ show_transaction(tx, parent=window, prompt_if_unsaved=True)
DIR diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py
t@@ -14,20 +14,21 @@ import re
import struct
import sys
import time
+import copy
from electrum.crypto import sha256d, EncodeAES_base64, EncodeAES_bytes, DecodeAES_bytes, hmac_oneshot
from electrum.bitcoin import (TYPE_ADDRESS, push_script, var_int, public_key_to_p2pkh,
is_address)
-from electrum.bip32 import BIP32Node
+from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath, is_all_public_derivation
from electrum import ecc
from electrum.ecc import msg_magic
from electrum.wallet import Standard_Wallet
from electrum import constants
-from electrum.transaction import Transaction
+from electrum.transaction import Transaction, PartialTransaction, PartialTxInput
from electrum.i18n import _
from electrum.keystore import Hardware_KeyStore
from ..hw_wallet import HW_PluginBase
-from electrum.util import to_string, UserCancelled, UserFacingException
+from electrum.util import to_string, UserCancelled, UserFacingException, bfh
from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET
from electrum.network import Network
from electrum.logging import get_logger
t@@ -449,21 +450,13 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
hw_type = 'digitalbitbox'
device = 'DigitalBitbox'
+ plugin: 'DigitalBitboxPlugin'
def __init__(self, d):
Hardware_KeyStore.__init__(self, d)
self.force_watching_only = False
self.maxInputs = 14 # maximum inputs per single sign command
-
- def get_derivation(self):
- return str(self.derivation)
-
-
- def is_p2pkh(self):
- return self.derivation.startswith("m/44'/")
-
-
def give_error(self, message, clear_client = False):
if clear_client:
self.client = None
t@@ -478,7 +471,7 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
sig = None
try:
message = message.encode('utf8')
- inputPath = self.get_derivation() + "/%d/%d" % sequence
+ inputPath = self.get_derivation_prefix() + "/%d/%d" % sequence
msg_hash = sha256d(msg_magic(message))
inputHash = to_hexstr(msg_hash)
hasharray = []
t@@ -540,58 +533,50 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
try:
p2pkhTransaction = True
- derivations = self.get_tx_derivations(tx)
inputhasharray = []
hasharray = []
pubkeyarray = []
# Build hasharray from inputs
for i, txin in enumerate(tx.inputs()):
- if txin['type'] == 'coinbase':
+ if txin.is_coinbase():
self.give_error("Coinbase not supported") # should never happen
- if txin['type'] != 'p2pkh':
+ if txin.script_type != 'p2pkh':
p2pkhTransaction = False
- for x_pubkey in txin['x_pubkeys']:
- if x_pubkey in derivations:
- index = derivations.get(x_pubkey)
- inputPath = "%s/%d/%d" % (self.get_derivation(), index[0], index[1])
- inputHash = sha256d(binascii.unhexlify(tx.serialize_preimage(i)))
- hasharray_i = {'hash': to_hexstr(inputHash), 'keypath': inputPath}
- hasharray.append(hasharray_i)
- inputhasharray.append(inputHash)
- break
- else:
- self.give_error("No matching x_key for sign_transaction") # should never happen
+ my_pubkey, inputPath = self.find_my_pubkey_in_txinout(txin)
+ if not inputPath:
+ self.give_error("No matching pubkey for sign_transaction") # should never happen
+ inputPath = convert_bip32_intpath_to_strpath(inputPath)
+ inputHash = sha256d(bfh(tx.serialize_preimage(i)))
+ hasharray_i = {'hash': to_hexstr(inputHash), 'keypath': inputPath}
+ hasharray.append(hasharray_i)
+ inputhasharray.append(inputHash)
# Build pubkeyarray from outputs
- for o in tx.outputs():
- assert o.type == TYPE_ADDRESS
- info = tx.output_info.get(o.address)
- if info is not None:
- if info.is_change:
- index = info.address_index
- changePath = self.get_derivation() + "/%d/%d" % index
- changePubkey = self.derive_pubkey(index[0], index[1])
- pubkeyarray_i = {'pubkey': changePubkey, 'keypath': changePath}
- pubkeyarray.append(pubkeyarray_i)
+ for txout in tx.outputs():
+ assert txout.address
+ if txout.is_change:
+ changePubkey, changePath = self.find_my_pubkey_in_txinout(txout)
+ assert changePath
+ changePath = convert_bip32_intpath_to_strpath(changePath)
+ changePubkey = changePubkey.hex()
+ pubkeyarray_i = {'pubkey': changePubkey, 'keypath': changePath}
+ pubkeyarray.append(pubkeyarray_i)
# Special serialization of the unsigned transaction for
# the mobile verification app.
# At the moment, verification only works for p2pkh transactions.
if p2pkhTransaction:
- class CustomTXSerialization(Transaction):
- @classmethod
- def input_script(self, txin, estimate_size=False):
- if txin['type'] == 'p2pkh':
- return Transaction.get_preimage_script(txin)
- if txin['type'] == 'p2sh':
- # Multisig verification has partial support, but is disabled. This is the
- # expected serialization though, so we leave it here until we activate it.
- return '00' + push_script(Transaction.get_preimage_script(txin))
- raise Exception("unsupported type %s" % txin['type'])
- tx_dbb_serialized = CustomTXSerialization(tx.serialize()).serialize_to_network()
+ tx_copy = copy.deepcopy(tx)
+ # monkey-patch method of tx_copy instance to change serialization
+ def input_script(self, txin: PartialTxInput, *, estimate_size=False):
+ if txin.script_type == 'p2pkh':
+ return Transaction.get_preimage_script(txin)
+ raise Exception("unsupported type %s" % txin.script_type)
+ tx_copy.input_script = input_script.__get__(tx_copy, PartialTransaction)
+ tx_dbb_serialized = tx_copy.serialize_to_network()
else:
# We only need this for the signing echo / verification.
tx_dbb_serialized = None
t@@ -656,12 +641,9 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
if len(dbb_signatures) != len(tx.inputs()):
raise Exception("Incorrect number of transactions signed.") # Should never occur
for i, txin in enumerate(tx.inputs()):
- num = txin['num_sig']
- for pubkey in txin['pubkeys']:
- signatures = list(filter(None, txin['signatures']))
- if len(signatures) == num:
- break # txin is complete
- ii = txin['pubkeys'].index(pubkey)
+ for pubkey_bytes in txin.pubkeys:
+ if txin.is_complete():
+ break
signed = dbb_signatures[i]
if 'recid' in signed:
# firmware > v2.1.1
t@@ -673,20 +655,19 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
elif 'pubkey' in signed:
# firmware <= v2.1.1
pk = signed['pubkey']
- if pk != pubkey:
+ if pk != pubkey_bytes.hex():
continue
sig_r = int(signed['sig'][:64], 16)
sig_s = int(signed['sig'][64:], 16)
sig = ecc.der_sig_from_r_and_s(sig_r, sig_s)
sig = to_hexstr(sig) + '01'
- tx.add_signature_to_txin(i, ii, sig)
+ tx.add_signature_to_txin(txin_idx=i, signing_pubkey=pubkey_bytes.hex(), sig=sig)
except UserCancelled:
raise
except BaseException as e:
self.give_error(e, True)
else:
- _logger.info("Transaction is_complete {tx.is_complete()}")
- tx.raw = tx.serialize()
+ _logger.info(f"Transaction is_complete {tx.is_complete()}")
class DigitalBitboxPlugin(HW_PluginBase):
t@@ -760,6 +741,8 @@ class DigitalBitboxPlugin(HW_PluginBase):
def get_xpub(self, device_id, derivation, xtype, wizard):
if xtype not in self.SUPPORTED_XTYPES:
raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device))
+ if is_all_public_derivation(derivation):
+ raise Exception(f"The {self.device} does not reveal xpubs corresponding to non-hardened paths. (path: {derivation})")
devmgr = self.device_manager()
client = devmgr.client_by_id(device_id)
client.handler = self.create_handler(wizard)
t@@ -788,11 +771,11 @@ class DigitalBitboxPlugin(HW_PluginBase):
if not self.is_mobile_paired():
keystore.handler.show_error(_('This function is only available after pairing your {} with a mobile device.').format(self.device))
return
- if not keystore.is_p2pkh():
+ if wallet.get_txin_type(address) != 'p2pkh':
keystore.handler.show_error(_('This function is only available for p2pkh keystores when using {}.').format(self.device))
return
change, index = wallet.get_address_index(address)
- keypath = '%s/%d/%d' % (keystore.derivation, change, index)
+ keypath = '%s/%d/%d' % (keystore.get_derivation_prefix(), change, index)
xpub = self.get_client(keystore)._get_xpub(keypath)
verify_request_payload = {
"type": 'p2pkh',
DIR diff --git a/electrum/plugins/digitalbitbox/qt.py b/electrum/plugins/digitalbitbox/qt.py
t@@ -2,7 +2,7 @@ from functools import partial
from electrum.i18n import _
from electrum.plugin import hook
-from electrum.wallet import Standard_Wallet
+from electrum.wallet import Standard_Wallet, Abstract_Wallet
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
from ..hw_wallet.plugin import only_hook_if_libraries_available
t@@ -18,7 +18,7 @@ class Plugin(DigitalBitboxPlugin, QtPluginBase):
@only_hook_if_libraries_available
@hook
- def receive_menu(self, menu, addrs, wallet):
+ def receive_menu(self, menu, addrs, wallet: Abstract_Wallet):
if type(wallet) is not Standard_Wallet:
return
t@@ -29,12 +29,12 @@ class Plugin(DigitalBitboxPlugin, QtPluginBase):
if not self.is_mobile_paired():
return
- if not keystore.is_p2pkh():
- return
-
if len(addrs) == 1:
+ addr = addrs[0]
+ if wallet.get_txin_type(addr) != 'p2pkh':
+ return
def show_address():
- keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore))
+ keystore.thread.add(partial(self.show_address, wallet, addr, keystore))
menu.addAction(_("Show on {}").format(self.device), show_address)
DIR diff --git a/electrum/plugins/greenaddress_instant/qt.py b/electrum/plugins/greenaddress_instant/qt.py
t@@ -36,6 +36,7 @@ from electrum.network import Network
if TYPE_CHECKING:
from aiohttp import ClientResponse
+ from electrum.gui.qt.transaction_dialog import TxDialog
class Plugin(BasePlugin):
t@@ -43,13 +44,13 @@ class Plugin(BasePlugin):
button_label = _("Verify GA instant")
@hook
- def transaction_dialog(self, d):
+ def transaction_dialog(self, d: 'TxDialog'):
d.verify_button = QPushButton(self.button_label)
d.verify_button.clicked.connect(lambda: self.do_verify(d))
d.buttons.insert(0, d.verify_button)
self.transaction_dialog_update(d)
- def get_my_addr(self, d):
+ def get_my_addr(self, d: 'TxDialog'):
"""Returns the address for given tx which can be used to request
instant confirmation verification from GreenAddress"""
for o in d.tx.outputs():
t@@ -58,13 +59,13 @@ class Plugin(BasePlugin):
return None
@hook
- def transaction_dialog_update(self, d):
+ def transaction_dialog_update(self, d: 'TxDialog'):
if d.tx.is_complete() and self.get_my_addr(d):
d.verify_button.show()
else:
d.verify_button.hide()
- def do_verify(self, d):
+ def do_verify(self, d: 'TxDialog'):
tx = d.tx
wallet = d.wallet
window = d.main_window
DIR diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py
t@@ -24,11 +24,18 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
+from typing import TYPE_CHECKING, Dict, List, Union, Tuple, Sequence
+
from electrum.plugin import BasePlugin, hook
from electrum.i18n import _
from electrum.bitcoin import is_address, TYPE_SCRIPT, opcodes
from electrum.util import bfh, versiontuple, UserFacingException
-from electrum.transaction import TxOutput, Transaction
+from electrum.transaction import TxOutput, Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
+from electrum.bip32 import BIP32Node
+
+if TYPE_CHECKING:
+ from electrum.wallet import Abstract_Wallet
+ from electrum.keystore import Hardware_KeyStore
class HW_PluginBase(BasePlugin):
t@@ -65,7 +72,10 @@ class HW_PluginBase(BasePlugin):
"""
raise NotImplementedError()
- def show_address(self, wallet, address, keystore=None):
+ def get_client(self, keystore: 'Hardware_KeyStore', force_pair: bool = True):
+ raise NotImplementedError()
+
+ def show_address(self, wallet: 'Abstract_Wallet', address, keystore: 'Hardware_KeyStore' = None):
pass # implemented in child classes
def show_address_helper(self, wallet, address, keystore=None):
t@@ -132,20 +142,12 @@ class HW_PluginBase(BasePlugin):
return self._ignore_outdated_fw
-def is_any_tx_output_on_change_branch(tx: Transaction) -> bool:
- if not tx.output_info:
- return False
- for o in tx.outputs():
- info = tx.output_info.get(o.address)
- if info is not None:
- return info.is_change
- return False
+def is_any_tx_output_on_change_branch(tx: PartialTransaction) -> bool:
+ return any([txout.is_change for txout in tx.outputs()])
def trezor_validate_op_return_output_and_get_data(output: TxOutput) -> bytes:
- if output.type != TYPE_SCRIPT:
- raise Exception("Unexpected output type: {}".format(output.type))
- script = bfh(output.address)
+ script = output.scriptpubkey
if not (script[0] == opcodes.OP_RETURN and
script[1] == len(script) - 2 and script[1] <= 75):
raise UserFacingException(_("Only OP_RETURN scripts, with one constant push, are supported."))
t@@ -154,6 +156,25 @@ def trezor_validate_op_return_output_and_get_data(output: TxOutput) -> bytes:
return script[2:]
+def get_xpubs_and_der_suffixes_from_txinout(tx: PartialTransaction,
+ txinout: Union[PartialTxInput, PartialTxOutput]) \
+ -> List[Tuple[str, List[int]]]:
+ xfp_to_xpub_map = {xfp: bip32node for bip32node, (xfp, path)
+ in tx.xpubs.items()} # type: Dict[bytes, BIP32Node]
+ xfps = [txinout.bip32_paths[pubkey][0] for pubkey in txinout.pubkeys]
+ try:
+ xpubs = [xfp_to_xpub_map[xfp] for xfp in xfps]
+ except KeyError as e:
+ raise Exception(f"Partial transaction is missing global xpub for "
+ f"fingerprint ({str(e)}) in input/output") from e
+ xpubs_and_deriv_suffixes = []
+ for bip32node, pubkey in zip(xpubs, txinout.pubkeys):
+ xfp, path = txinout.bip32_paths[pubkey]
+ der_suffix = list(path)[bip32node.depth:]
+ xpubs_and_deriv_suffixes.append((bip32node.to_xpub(), der_suffix))
+ return xpubs_and_deriv_suffixes
+
+
def only_hook_if_libraries_available(func):
# note: this decorator must wrap @hook, not the other way around,
# as 'hook' uses the name of the function it wraps
DIR diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py
t@@ -1,19 +1,23 @@
from binascii import hexlify, unhexlify
import traceback
import sys
+from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING
from electrum.util import bfh, bh2u, UserCancelled, UserFacingException
from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT
from electrum.bip32 import BIP32Node
from electrum import constants
from electrum.i18n import _
-from electrum.transaction import deserialize, Transaction
-from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey
+from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
+from electrum.keystore import Hardware_KeyStore
from electrum.base_wizard import ScriptTypeNotSupported
from ..hw_wallet import HW_PluginBase
-from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data
+from ..hw_wallet.plugin import (is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data,
+ get_xpubs_and_der_suffixes_from_txinout)
+if TYPE_CHECKING:
+ from .client import KeepKeyClient
# TREZOR initialization methods
TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
t@@ -23,8 +27,7 @@ class KeepKey_KeyStore(Hardware_KeyStore):
hw_type = 'keepkey'
device = 'KeepKey'
- def get_derivation(self):
- return self.derivation
+ plugin: 'KeepKeyPlugin'
def get_client(self, force_pair=True):
return self.plugin.get_client(self, force_pair)
t@@ -34,7 +37,7 @@ class KeepKey_KeyStore(Hardware_KeyStore):
def sign_message(self, sequence, message, password):
client = self.get_client()
- address_path = self.get_derivation() + "/%d/%d"%sequence
+ address_path = self.get_derivation_prefix() + "/%d/%d"%sequence
address_n = client.expand_path(address_path)
msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message)
return msg_sig.signature
t@@ -44,22 +47,13 @@ class KeepKey_KeyStore(Hardware_KeyStore):
return
# previous transactions used as inputs
prev_tx = {}
- # path of the xpubs that are involved
- xpub_path = {}
for txin in tx.inputs():
- pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
- tx_hash = txin['prevout_hash']
- if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin):
- raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device))
- prev_tx[tx_hash] = txin['prev_tx']
- for x_pubkey in x_pubkeys:
- if not is_xpubkey(x_pubkey):
- continue
- xpub, s = parse_xpubkey(x_pubkey)
- if xpub == self.get_master_public_key():
- xpub_path[xpub] = self.get_derivation()
+ tx_hash = txin.prevout.txid.hex()
+ if txin.utxo is None and not Transaction.is_segwit_input(txin):
+ raise UserFacingException(_('Missing previous tx for legacy input.'))
+ prev_tx[tx_hash] = txin.utxo
- self.plugin.sign_transaction(self, tx, prev_tx, xpub_path)
+ self.plugin.sign_transaction(self, tx, prev_tx)
class KeepKeyPlugin(HW_PluginBase):
t@@ -164,7 +158,7 @@ class KeepKeyPlugin(HW_PluginBase):
return client
- def get_client(self, keystore, force_pair=True):
+ def get_client(self, keystore, force_pair=True) -> Optional['KeepKeyClient']:
devmgr = self.device_manager()
handler = keystore.handler
with devmgr.hid_lock:
t@@ -306,12 +300,11 @@ class KeepKeyPlugin(HW_PluginBase):
return self.types.PAYTOMULTISIG
raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))
- def sign_transaction(self, keystore, tx, prev_tx, xpub_path):
+ def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx):
self.prev_tx = prev_tx
- self.xpub_path = xpub_path
client = self.get_client(keystore)
- inputs = self.tx_inputs(tx, True)
- outputs = self.tx_outputs(keystore.get_derivation(), tx)
+ inputs = self.tx_inputs(tx, for_sig=True, keystore=keystore)
+ outputs = self.tx_outputs(tx, keystore=keystore)
signatures = client.sign_tx(self.get_coin_name(), inputs, outputs,
lock_time=tx.locktime, version=tx.version)[0]
signatures = [(bh2u(x) + '01') for x in signatures]
t@@ -326,137 +319,118 @@ class KeepKeyPlugin(HW_PluginBase):
if not client.atleast_version(1, 3):
keystore.handler.show_error(_("Your device firmware is too old"))
return
- change, index = wallet.get_address_index(address)
- derivation = keystore.derivation
- address_path = "%s/%d/%d"%(derivation, change, index)
+ deriv_suffix = wallet.get_address_index(address)
+ derivation = keystore.get_derivation_prefix()
+ address_path = "%s/%d/%d"%(derivation, *deriv_suffix)
address_n = client.expand_path(address_path)
+ script_type = self.get_keepkey_input_script_type(wallet.txin_type)
+
+ # prepare multisig, if available:
xpubs = wallet.get_master_public_keys()
- if len(xpubs) == 1:
- script_type = self.get_keepkey_input_script_type(wallet.txin_type)
- client.get_address(self.get_coin_name(), address_n, True, script_type=script_type)
- else:
- def f(xpub):
- return self._make_node_path(xpub, [change, index])
+ if len(xpubs) > 1:
pubkeys = wallet.get_public_keys(address)
# sort xpubs using the order of pubkeys
- sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs)))
- pubkeys = list(map(f, sorted_xpubs))
- multisig = self.types.MultisigRedeemScriptType(
- pubkeys=pubkeys,
- signatures=[b''] * wallet.n,
- m=wallet.m,
- )
- script_type = self.get_keepkey_input_script_type(wallet.txin_type)
- client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type)
-
- def tx_inputs(self, tx, for_sig=False):
+ sorted_pairs = sorted(zip(pubkeys, xpubs))
+ multisig = self._make_multisig(
+ wallet.m,
+ [(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs])
+ else:
+ multisig = None
+
+ client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type)
+
+ def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'KeepKey_KeyStore' = None):
inputs = []
for txin in tx.inputs():
txinputtype = self.types.TxInputType()
- if txin['type'] == 'coinbase':
+ if txin.is_coinbase():
prev_hash = b"\x00"*32
prev_index = 0xffffffff # signed int -1
else:
if for_sig:
- x_pubkeys = txin['x_pubkeys']
- if len(x_pubkeys) == 1:
- x_pubkey = x_pubkeys[0]
- xpub, s = parse_xpubkey(x_pubkey)
- xpub_n = self.client_class.expand_path(self.xpub_path[xpub])
- txinputtype.address_n.extend(xpub_n + s)
- txinputtype.script_type = self.get_keepkey_input_script_type(txin['type'])
+ assert isinstance(tx, PartialTransaction)
+ assert isinstance(txin, PartialTxInput)
+ assert keystore
+ if len(txin.pubkeys) > 1:
+ xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin)
+ multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes)
else:
- def f(x_pubkey):
- xpub, s = parse_xpubkey(x_pubkey)
- return self._make_node_path(xpub, s)
- pubkeys = list(map(f, x_pubkeys))
- multisig = self.types.MultisigRedeemScriptType(
- pubkeys=pubkeys,
- signatures=map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures')),
- m=txin.get('num_sig'),
- )
- script_type = self.get_keepkey_input_script_type(txin['type'])
- txinputtype = self.types.TxInputType(
- script_type=script_type,
- multisig=multisig
- )
- # find which key is mine
- for x_pubkey in x_pubkeys:
- if is_xpubkey(x_pubkey):
- xpub, s = parse_xpubkey(x_pubkey)
- if xpub in self.xpub_path:
- xpub_n = self.client_class.expand_path(self.xpub_path[xpub])
- txinputtype.address_n.extend(xpub_n + s)
- break
-
- prev_hash = unhexlify(txin['prevout_hash'])
- prev_index = txin['prevout_n']
-
- if 'value' in txin:
- txinputtype.amount = txin['value']
+ multisig = None
+ script_type = self.get_keepkey_input_script_type(txin.script_type)
+ txinputtype = self.types.TxInputType(
+ script_type=script_type,
+ multisig=multisig)
+ my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)
+ if full_path:
+ txinputtype.address_n.extend(full_path)
+
+ prev_hash = txin.prevout.txid
+ prev_index = txin.prevout.out_idx
+
+ if txin.value_sats() is not None:
+ txinputtype.amount = txin.value_sats()
txinputtype.prev_hash = prev_hash
txinputtype.prev_index = prev_index
- if txin.get('scriptSig') is not None:
- script_sig = bfh(txin['scriptSig'])
- txinputtype.script_sig = script_sig
+ if txin.script_sig is not None:
+ txinputtype.script_sig = txin.script_sig
- txinputtype.sequence = txin.get('sequence', 0xffffffff - 1)
+ txinputtype.sequence = txin.nsequence
inputs.append(txinputtype)
return inputs
- def tx_outputs(self, derivation, tx: Transaction):
+ def _make_multisig(self, m, xpubs):
+ if len(xpubs) == 1:
+ return None
+ pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs]
+ return self.types.MultisigRedeemScriptType(
+ pubkeys=pubkeys,
+ signatures=[b''] * len(pubkeys),
+ m=m)
+
+ def tx_outputs(self, tx: PartialTransaction, *, keystore: 'KeepKey_KeyStore'):
def create_output_by_derivation():
- script_type = self.get_keepkey_output_script_type(info.script_type)
- if len(xpubs) == 1:
- address_n = self.client_class.expand_path(derivation + "/%d/%d" % index)
- txoutputtype = self.types.TxOutputType(
- amount=amount,
- script_type=script_type,
- address_n=address_n,
- )
+ script_type = self.get_keepkey_output_script_type(txout.script_type)
+ if len(txout.pubkeys) > 1:
+ xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
+ multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes)
else:
- address_n = self.client_class.expand_path("/%d/%d" % index)
- pubkeys = [self._make_node_path(xpub, address_n) for xpub in xpubs]
- multisig = self.types.MultisigRedeemScriptType(
- pubkeys=pubkeys,
- signatures=[b''] * len(pubkeys),
- m=m)
- txoutputtype = self.types.TxOutputType(
- multisig=multisig,
- amount=amount,
- address_n=self.client_class.expand_path(derivation + "/%d/%d" % index),
- script_type=script_type)
+ multisig = None
+ my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout)
+ assert full_path
+ txoutputtype = self.types.TxOutputType(
+ multisig=multisig,
+ amount=txout.value,
+ address_n=full_path,
+ script_type=script_type)
return txoutputtype
def create_output_by_address():
txoutputtype = self.types.TxOutputType()
- txoutputtype.amount = amount
- if _type == TYPE_SCRIPT:
- txoutputtype.script_type = self.types.PAYTOOPRETURN
- txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o)
- elif _type == TYPE_ADDRESS:
+ txoutputtype.amount = txout.value
+ if address:
txoutputtype.script_type = self.types.PAYTOADDRESS
txoutputtype.address = address
+ else:
+ txoutputtype.script_type = self.types.PAYTOOPRETURN
+ txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(txout)
return txoutputtype
outputs = []
has_change = False
any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
- for o in tx.outputs():
- _type, address, amount = o.type, o.address, o.value
+ for txout in tx.outputs():
+ address = txout.address
use_create_by_derivation = False
- info = tx.output_info.get(address)
- if info is not None and not has_change:
- index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig
+ if txout.is_mine and not has_change:
# prioritise hiding outputs on the 'change' branch from user
# because no more than one change address allowed
- if info.is_change == any_output_on_change_branch:
+ if txout.is_change == any_output_on_change_branch:
use_create_by_derivation = True
has_change = True
t@@ -468,20 +442,20 @@ class KeepKeyPlugin(HW_PluginBase):
return outputs
- def electrum_tx_to_txtype(self, tx):
+ def electrum_tx_to_txtype(self, tx: Optional[Transaction]):
t = self.types.TransactionType()
if tx is None:
# probably for segwit input and we don't need this prev txn
return t
- d = deserialize(tx.raw)
- t.version = d['version']
- t.lock_time = d['lockTime']
+ tx.deserialize()
+ t.version = tx.version
+ t.lock_time = tx.locktime
inputs = self.tx_inputs(tx)
t.inputs.extend(inputs)
- for vout in d['outputs']:
+ for out in tx.outputs():
o = t.bin_outputs.add()
- o.amount = vout['value']
- o.script_pubkey = bfh(vout['scriptPubKey'])
+ o.amount = out.value
+ o.script_pubkey = out.scriptpubkey
return t
# This function is called from the TREZOR libraries (via tx_api)
DIR diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py
t@@ -4,11 +4,13 @@ import sys
import traceback
from electrum import ecc
-from electrum.bitcoin import TYPE_ADDRESS, int_to_hex, var_int, is_segwit_script_type
-from electrum.bip32 import BIP32Node
+from electrum import bip32
+from electrum.crypto import hash_160
+from electrum.bitcoin import int_to_hex, var_int, is_segwit_script_type
+from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath
from electrum.i18n import _
from electrum.keystore import Hardware_KeyStore
-from electrum.transaction import Transaction
+from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
from electrum.wallet import Standard_Wallet
from electrum.util import bfh, bh2u, versiontuple, UserFacingException
from electrum.base_wizard import ScriptTypeNotSupported
t@@ -78,9 +80,6 @@ class Ledger_Client():
def label(self):
return ""
- def i4b(self, x):
- return pack('>I', x)
-
def has_usable_connection_with_device(self):
try:
self.dongleObject.getFirmwareVersion()
t@@ -101,29 +100,27 @@ class Ledger_Client():
raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT)
if xtype in ['p2wpkh-p2sh', 'p2wsh-p2sh'] and not self.supports_segwit():
raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT)
- splitPath = bip32_path.split('/')
- if splitPath[0] == 'm':
- splitPath = splitPath[1:]
- bip32_path = bip32_path[2:]
- fingerprint = 0
- if len(splitPath) > 1:
- prevPath = "/".join(splitPath[0:len(splitPath) - 1])
+ bip32_path = bip32.normalize_bip32_derivation(bip32_path)
+ bip32_intpath = bip32.convert_bip32_path_to_list_of_uint32(bip32_path)
+ bip32_path = bip32_path[2:] # cut off "m/"
+ if len(bip32_intpath) >= 1:
+ prevPath = bip32.convert_bip32_intpath_to_strpath(bip32_intpath[:-1])[2:]
nodeData = self.dongleObject.getWalletPublicKey(prevPath)
publicKey = compress_public_key(nodeData['publicKey'])
- h = hashlib.new('ripemd160')
- h.update(hashlib.sha256(publicKey).digest())
- fingerprint = unpack(">I", h.digest()[0:4])[0]
+ fingerprint_bytes = hash_160(publicKey)[0:4]
+ childnum_bytes = bip32_intpath[-1].to_bytes(length=4, byteorder="big")
+ else:
+ fingerprint_bytes = bytes(4)
+ childnum_bytes = bytes(4)
nodeData = self.dongleObject.getWalletPublicKey(bip32_path)
publicKey = compress_public_key(nodeData['publicKey'])
- depth = len(splitPath)
- lastChild = splitPath[len(splitPath) - 1].split('\'')
- childnum = int(lastChild[0]) if len(lastChild) == 1 else 0x80000000 | int(lastChild[0])
+ depth = len(bip32_intpath)
return BIP32Node(xtype=xtype,
eckey=ecc.ECPubkey(publicKey),
chaincode=nodeData['chainCode'],
depth=depth,
- fingerprint=self.i4b(fingerprint),
- child_number=self.i4b(childnum)).to_xpub()
+ fingerprint=fingerprint_bytes,
+ child_number=childnum_bytes).to_xpub()
def has_detached_pin_support(self, client):
try:
t@@ -217,6 +214,8 @@ class Ledger_KeyStore(Hardware_KeyStore):
hw_type = 'ledger'
device = 'Ledger'
+ plugin: 'LedgerPlugin'
+
def __init__(self, d):
Hardware_KeyStore.__init__(self, d)
# Errors and other user interaction is done through the wallet's
t@@ -231,9 +230,6 @@ class Ledger_KeyStore(Hardware_KeyStore):
obj['cfg'] = self.cfg
return obj
- def get_derivation(self):
- return self.derivation
-
def get_client(self):
return self.plugin.get_client(self).dongleObject
t@@ -260,13 +256,6 @@ class Ledger_KeyStore(Hardware_KeyStore):
self.signing = False
return wrapper
- def address_id_stripped(self, address):
- # Strip the leading "m/"
- change, index = self.get_address_index(address)
- derivation = self.derivation
- address_path = "%s/%d/%d"%(derivation, change, index)
- return address_path[2:]
-
def decrypt_message(self, pubkey, message, password):
raise UserFacingException(_('Encryption and decryption are currently not supported for {}').format(self.device))
t@@ -277,7 +266,7 @@ class Ledger_KeyStore(Hardware_KeyStore):
message_hash = hashlib.sha256(message).hexdigest().upper()
# prompt for the PIN before displaying the dialog if necessary
client = self.get_client()
- address_path = self.get_derivation()[2:] + "/%d/%d"%sequence
+ address_path = self.get_derivation_prefix()[2:] + "/%d/%d"%sequence
self.handler.show_message("Signing message ...\r\nMessage hash: "+message_hash)
try:
info = self.get_client().signMessagePrepare(address_path, message)
t@@ -318,16 +307,13 @@ class Ledger_KeyStore(Hardware_KeyStore):
@test_pin_unlocked
@set_and_unset_signing
- def sign_transaction(self, tx: Transaction, password):
+ def sign_transaction(self, tx, password):
if tx.is_complete():
return
- client = self.get_client()
inputs = []
inputsPaths = []
- pubKeys = []
chipInputs = []
redeemScripts = []
- signatures = []
changePath = ""
output = None
p2shTransaction = False
t@@ -336,60 +322,52 @@ class Ledger_KeyStore(Hardware_KeyStore):
self.get_client() # prompt for the PIN before displaying the dialog if necessary
# Fetch inputs of the transaction to sign
- derivations = self.get_tx_derivations(tx)
for txin in tx.inputs():
- if txin['type'] == 'coinbase':
+ if txin.is_coinbase():
self.give_error("Coinbase not supported") # should never happen
- if txin['type'] in ['p2sh']:
+ if txin.script_type in ['p2sh']:
p2shTransaction = True
- if txin['type'] in ['p2wpkh-p2sh', 'p2wsh-p2sh']:
+ if txin.script_type in ['p2wpkh-p2sh', 'p2wsh-p2sh']:
if not self.get_client_electrum().supports_segwit():
self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT)
segwitTransaction = True
- if txin['type'] in ['p2wpkh', 'p2wsh']:
+ if txin.script_type in ['p2wpkh', 'p2wsh']:
if not self.get_client_electrum().supports_native_segwit():
self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT)
segwitTransaction = True
- pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
- for i, x_pubkey in enumerate(x_pubkeys):
- if x_pubkey in derivations:
- signingPos = i
- s = derivations.get(x_pubkey)
- hwAddress = "%s/%d/%d" % (self.get_derivation()[2:], s[0], s[1])
- break
- else:
- self.give_error("No matching x_key for sign_transaction") # should never happen
+ my_pubkey, full_path = self.find_my_pubkey_in_txinout(txin)
+ if not full_path:
+ self.give_error("No matching pubkey for sign_transaction") # should never happen
+ full_path = convert_bip32_intpath_to_strpath(full_path)[2:]
redeemScript = Transaction.get_preimage_script(txin)
- txin_prev_tx = txin.get('prev_tx')
+ txin_prev_tx = txin.utxo
if txin_prev_tx is None and not Transaction.is_segwit_input(txin):
- raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device))
- txin_prev_tx_raw = txin_prev_tx.raw if txin_prev_tx else None
+ raise UserFacingException(_('Missing previous tx for legacy input.'))
+ txin_prev_tx_raw = txin_prev_tx.serialize() if txin_prev_tx else None
inputs.append([txin_prev_tx_raw,
- txin['prevout_n'],
+ txin.prevout.out_idx,
redeemScript,
- txin['prevout_hash'],
- signingPos,
- txin.get('sequence', 0xffffffff - 1),
- txin.get('value')])
- inputsPaths.append(hwAddress)
- pubKeys.append(pubkeys)
+ txin.prevout.txid.hex(),
+ my_pubkey,
+ txin.nsequence,
+ txin.value_sats()])
+ inputsPaths.append(full_path)
# Sanity check
if p2shTransaction:
for txin in tx.inputs():
- if txin['type'] != 'p2sh':
+ if txin.script_type != 'p2sh':
self.give_error("P2SH / regular input mixed in same transaction not supported") # should never happen
txOutput = var_int(len(tx.outputs()))
for o in tx.outputs():
- output_type, addr, amount = o.type, o.address, o.value
- txOutput += int_to_hex(amount, 8)
- script = tx.pay_script(output_type, addr)
+ txOutput += int_to_hex(o.value, 8)
+ script = o.scriptpubkey.hex()
txOutput += var_int(len(script)//2)
txOutput += script
txOutput = bfh(txOutput)
t@@ -403,21 +381,21 @@ class Ledger_KeyStore(Hardware_KeyStore):
self.give_error("Transaction with more than 2 outputs not supported")
has_change = False
any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
- for o in tx.outputs():
- assert o.type == TYPE_ADDRESS
- info = tx.output_info.get(o.address)
- if (info is not None) and len(tx.outputs()) > 1 \
+ for txout in tx.outputs():
+ assert txout.address
+ if txout.is_mine and len(tx.outputs()) > 1 \
and not has_change:
- index = info.address_index
# prioritise hiding outputs on the 'change' branch from user
# because no more than one change address allowed
- if info.is_change == any_output_on_change_branch:
- changePath = self.get_derivation()[2:] + "/%d/%d"%index
+ if txout.is_change == any_output_on_change_branch:
+ my_pubkey, changePath = self.find_my_pubkey_in_txinout(txout)
+ assert changePath
+ changePath = convert_bip32_intpath_to_strpath(changePath)[2:]
has_change = True
else:
- output = o.address
+ output = txout.address
else:
- output = o.address
+ output = txout.address
self.handler.show_message(_("Confirm Transaction on your Ledger device..."))
try:
t@@ -467,7 +445,10 @@ class Ledger_KeyStore(Hardware_KeyStore):
singleInput, redeemScripts[inputIndex], version=tx.version)
inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime)
inputSignature[0] = 0x30 # force for 1.4.9+
- signatures.append(inputSignature)
+ my_pubkey = inputs[inputIndex][4]
+ tx.add_signature_to_txin(txin_idx=inputIndex,
+ signing_pubkey=my_pubkey.hex(),
+ sig=inputSignature.hex())
inputIndex = inputIndex + 1
else:
while inputIndex < len(inputs):
t@@ -488,7 +469,10 @@ class Ledger_KeyStore(Hardware_KeyStore):
# Sign input with the provided PIN
inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime)
inputSignature[0] = 0x30 # force for 1.4.9+
- signatures.append(inputSignature)
+ my_pubkey = inputs[inputIndex][4]
+ tx.add_signature_to_txin(txin_idx=inputIndex,
+ signing_pubkey=my_pubkey.hex(),
+ sig=inputSignature.hex())
inputIndex = inputIndex + 1
firstTransaction = False
except UserWarning:
t@@ -508,16 +492,11 @@ class Ledger_KeyStore(Hardware_KeyStore):
finally:
self.handler.finished()
- for i, txin in enumerate(tx.inputs()):
- signingPos = inputs[i][4]
- tx.add_signature_to_txin(i, signingPos, bh2u(signatures[i]))
- tx.raw = tx.serialize()
-
@test_pin_unlocked
@set_and_unset_signing
def show_address(self, sequence, txin_type):
client = self.get_client()
- address_path = self.get_derivation()[2:] + "/%d/%d"%sequence
+ address_path = self.get_derivation_prefix()[2:] + "/%d/%d"%sequence
self.handler.show_message(_("Showing address ..."))
segwit = is_segwit_script_type(txin_type)
segwitNative = txin_type == 'p2wpkh'
DIR diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py
t@@ -1,6 +1,7 @@
from binascii import hexlify, unhexlify
import traceback
import sys
+from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING
from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingException
from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT
t@@ -8,13 +9,16 @@ from electrum.bip32 import BIP32Node
from electrum import constants
from electrum.i18n import _
from electrum.plugin import Device
-from electrum.transaction import deserialize, Transaction
-from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey
+from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
+from electrum.keystore import Hardware_KeyStore
from electrum.base_wizard import ScriptTypeNotSupported
from ..hw_wallet import HW_PluginBase
-from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data
+from ..hw_wallet.plugin import (is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data,
+ get_xpubs_and_der_suffixes_from_txinout)
+if TYPE_CHECKING:
+ from .client import SafeTClient
# Safe-T mini initialization methods
TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
t@@ -24,8 +28,7 @@ class SafeTKeyStore(Hardware_KeyStore):
hw_type = 'safe_t'
device = 'Safe-T mini'
- def get_derivation(self):
- return self.derivation
+ plugin: 'SafeTPlugin'
def get_client(self, force_pair=True):
return self.plugin.get_client(self, force_pair)
t@@ -35,7 +38,7 @@ class SafeTKeyStore(Hardware_KeyStore):
def sign_message(self, sequence, message, password):
client = self.get_client()
- address_path = self.get_derivation() + "/%d/%d"%sequence
+ address_path = self.get_derivation_prefix() + "/%d/%d"%sequence
address_n = client.expand_path(address_path)
msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message)
return msg_sig.signature
t@@ -45,22 +48,13 @@ class SafeTKeyStore(Hardware_KeyStore):
return
# previous transactions used as inputs
prev_tx = {}
- # path of the xpubs that are involved
- xpub_path = {}
for txin in tx.inputs():
- pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
- tx_hash = txin['prevout_hash']
- if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin):
- raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device))
- prev_tx[tx_hash] = txin['prev_tx']
- for x_pubkey in x_pubkeys:
- if not is_xpubkey(x_pubkey):
- continue
- xpub, s = parse_xpubkey(x_pubkey)
- if xpub == self.get_master_public_key():
- xpub_path[xpub] = self.get_derivation()
+ tx_hash = txin.prevout.txid.hex()
+ if txin.utxo is None and not Transaction.is_segwit_input(txin):
+ raise UserFacingException(_('Missing previous tx for legacy input.'))
+ prev_tx[tx_hash] = txin.utxo
- self.plugin.sign_transaction(self, tx, prev_tx, xpub_path)
+ self.plugin.sign_transaction(self, tx, prev_tx)
class SafeTPlugin(HW_PluginBase):
t@@ -148,7 +142,7 @@ class SafeTPlugin(HW_PluginBase):
return client
- def get_client(self, keystore, force_pair=True):
+ def get_client(self, keystore, force_pair=True) -> Optional['SafeTClient']:
devmgr = self.device_manager()
handler = keystore.handler
with devmgr.hid_lock:
t@@ -302,12 +296,11 @@ class SafeTPlugin(HW_PluginBase):
return self.types.OutputScriptType.PAYTOMULTISIG
raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))
- def sign_transaction(self, keystore, tx, prev_tx, xpub_path):
+ def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx):
self.prev_tx = prev_tx
- self.xpub_path = xpub_path
client = self.get_client(keystore)
- inputs = self.tx_inputs(tx, True)
- outputs = self.tx_outputs(keystore.get_derivation(), tx)
+ inputs = self.tx_inputs(tx, for_sig=True, keystore=keystore)
+ outputs = self.tx_outputs(tx, keystore=keystore)
signatures = client.sign_tx(self.get_coin_name(), inputs, outputs,
lock_time=tx.locktime, version=tx.version)[0]
signatures = [(bh2u(x) + '01') for x in signatures]
t@@ -322,139 +315,120 @@ class SafeTPlugin(HW_PluginBase):
if not client.atleast_version(1, 0):
keystore.handler.show_error(_("Your device firmware is too old"))
return
- change, index = wallet.get_address_index(address)
- derivation = keystore.derivation
- address_path = "%s/%d/%d"%(derivation, change, index)
+ deriv_suffix = wallet.get_address_index(address)
+ derivation = keystore.get_derivation_prefix()
+ address_path = "%s/%d/%d"%(derivation, *deriv_suffix)
address_n = client.expand_path(address_path)
+ script_type = self.get_safet_input_script_type(wallet.txin_type)
+
+ # prepare multisig, if available:
xpubs = wallet.get_master_public_keys()
- if len(xpubs) == 1:
- script_type = self.get_safet_input_script_type(wallet.txin_type)
- client.get_address(self.get_coin_name(), address_n, True, script_type=script_type)
- else:
- def f(xpub):
- return self._make_node_path(xpub, [change, index])
+ if len(xpubs) > 1:
pubkeys = wallet.get_public_keys(address)
# sort xpubs using the order of pubkeys
- sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs)))
- pubkeys = list(map(f, sorted_xpubs))
- multisig = self.types.MultisigRedeemScriptType(
- pubkeys=pubkeys,
- signatures=[b''] * wallet.n,
- m=wallet.m,
- )
- script_type = self.get_safet_input_script_type(wallet.txin_type)
- client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type)
-
- def tx_inputs(self, tx, for_sig=False):
+ sorted_pairs = sorted(zip(pubkeys, xpubs))
+ multisig = self._make_multisig(
+ wallet.m,
+ [(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs])
+ else:
+ multisig = None
+
+ client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type)
+
+ def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'SafeTKeyStore' = None):
inputs = []
for txin in tx.inputs():
txinputtype = self.types.TxInputType()
- if txin['type'] == 'coinbase':
+ if txin.is_coinbase():
prev_hash = b"\x00"*32
prev_index = 0xffffffff # signed int -1
else:
if for_sig:
- x_pubkeys = txin['x_pubkeys']
- if len(x_pubkeys) == 1:
- x_pubkey = x_pubkeys[0]
- xpub, s = parse_xpubkey(x_pubkey)
- xpub_n = self.client_class.expand_path(self.xpub_path[xpub])
- txinputtype._extend_address_n(xpub_n + s)
- txinputtype.script_type = self.get_safet_input_script_type(txin['type'])
+ assert isinstance(tx, PartialTransaction)
+ assert isinstance(txin, PartialTxInput)
+ assert keystore
+ if len(txin.pubkeys) > 1:
+ xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin)
+ multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes)
else:
- def f(x_pubkey):
- xpub, s = parse_xpubkey(x_pubkey)
- return self._make_node_path(xpub, s)
- pubkeys = list(map(f, x_pubkeys))
- multisig = self.types.MultisigRedeemScriptType(
- pubkeys=pubkeys,
- signatures=list(map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures'))),
- m=txin.get('num_sig'),
- )
- script_type = self.get_safet_input_script_type(txin['type'])
- txinputtype = self.types.TxInputType(
- script_type=script_type,
- multisig=multisig
- )
- # find which key is mine
- for x_pubkey in x_pubkeys:
- if is_xpubkey(x_pubkey):
- xpub, s = parse_xpubkey(x_pubkey)
- if xpub in self.xpub_path:
- xpub_n = self.client_class.expand_path(self.xpub_path[xpub])
- txinputtype._extend_address_n(xpub_n + s)
- break
-
- prev_hash = unhexlify(txin['prevout_hash'])
- prev_index = txin['prevout_n']
-
- if 'value' in txin:
- txinputtype.amount = txin['value']
+ multisig = None
+ script_type = self.get_safet_input_script_type(txin.script_type)
+ txinputtype = self.types.TxInputType(
+ script_type=script_type,
+ multisig=multisig)
+ my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)
+ if full_path:
+ txinputtype._extend_address_n(full_path)
+
+ prev_hash = txin.prevout.txid
+ prev_index = txin.prevout.out_idx
+
+ if txin.value_sats() is not None:
+ txinputtype.amount = txin.value_sats()
txinputtype.prev_hash = prev_hash
txinputtype.prev_index = prev_index
- if txin.get('scriptSig') is not None:
- script_sig = bfh(txin['scriptSig'])
- txinputtype.script_sig = script_sig
+ if txin.script_sig is not None:
+ txinputtype.script_sig = txin.script_sig
- txinputtype.sequence = txin.get('sequence', 0xffffffff - 1)
+ txinputtype.sequence = txin.nsequence
inputs.append(txinputtype)
return inputs
- def tx_outputs(self, derivation, tx: Transaction):
+ def _make_multisig(self, m, xpubs):
+ if len(xpubs) == 1:
+ return None
+ pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs]
+ return self.types.MultisigRedeemScriptType(
+ pubkeys=pubkeys,
+ signatures=[b''] * len(pubkeys),
+ m=m)
+
+ def tx_outputs(self, tx: PartialTransaction, *, keystore: 'SafeTKeyStore'):
def create_output_by_derivation():
- script_type = self.get_safet_output_script_type(info.script_type)
- if len(xpubs) == 1:
- address_n = self.client_class.expand_path(derivation + "/%d/%d" % index)
- txoutputtype = self.types.TxOutputType(
- amount=amount,
- script_type=script_type,
- address_n=address_n,
- )
+ script_type = self.get_safet_output_script_type(txout.script_type)
+ if len(txout.pubkeys) > 1:
+ xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
+ multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes)
else:
- address_n = self.client_class.expand_path("/%d/%d" % index)
- pubkeys = [self._make_node_path(xpub, address_n) for xpub in xpubs]
- multisig = self.types.MultisigRedeemScriptType(
- pubkeys=pubkeys,
- signatures=[b''] * len(pubkeys),
- m=m)
- txoutputtype = self.types.TxOutputType(
- multisig=multisig,
- amount=amount,
- address_n=self.client_class.expand_path(derivation + "/%d/%d" % index),
- script_type=script_type)
+ multisig = None
+ my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout)
+ assert full_path
+ txoutputtype = self.types.TxOutputType(
+ multisig=multisig,
+ amount=txout.value,
+ address_n=full_path,
+ script_type=script_type)
return txoutputtype
def create_output_by_address():
txoutputtype = self.types.TxOutputType()
- txoutputtype.amount = amount
- if _type == TYPE_SCRIPT:
- txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN
- txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o)
- elif _type == TYPE_ADDRESS:
+ txoutputtype.amount = txout.value
+ if address:
txoutputtype.script_type = self.types.OutputScriptType.PAYTOADDRESS
txoutputtype.address = address
+ else:
+ txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN
+ txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(txout)
return txoutputtype
outputs = []
has_change = False
any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
- for o in tx.outputs():
- _type, address, amount = o.type, o.address, o.value
+ for txout in tx.outputs():
+ address = txout.address
use_create_by_derivation = False
- info = tx.output_info.get(address)
- if info is not None and not has_change:
- index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig
+ if txout.is_mine and not has_change:
# prioritise hiding outputs on the 'change' branch from user
# because no more than one change address allowed
# note: ^ restriction can be removed once we require fw
# that has https://github.com/trezor/trezor-mcu/pull/306
- if info.is_change == any_output_on_change_branch:
+ if txout.is_change == any_output_on_change_branch:
use_create_by_derivation = True
has_change = True
t@@ -466,20 +440,20 @@ class SafeTPlugin(HW_PluginBase):
return outputs
- def electrum_tx_to_txtype(self, tx):
+ def electrum_tx_to_txtype(self, tx: Optional[Transaction]):
t = self.types.TransactionType()
if tx is None:
# probably for segwit input and we don't need this prev txn
return t
- d = deserialize(tx.raw)
- t.version = d['version']
- t.lock_time = d['lockTime']
+ tx.deserialize()
+ t.version = tx.version
+ t.lock_time = tx.locktime
inputs = self.tx_inputs(tx)
t._extend_inputs(inputs)
- for vout in d['outputs']:
+ for out in tx.outputs():
o = t._add_bin_outputs()
- o.amount = vout['value']
- o.script_pubkey = bfh(vout['scriptPubKey'])
+ o.amount = out.value
+ o.script_pubkey = out.scriptpubkey
return t
# This function is called from the TREZOR libraries (via tx_api)
DIR diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py
t@@ -1,6 +1,6 @@
import traceback
import sys
-from typing import NamedTuple, Any
+from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING
from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingException
from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT
t@@ -8,14 +8,15 @@ from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as pa
from electrum import constants
from electrum.i18n import _
from electrum.plugin import Device
-from electrum.transaction import deserialize, Transaction
-from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey
+from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
+from electrum.keystore import Hardware_KeyStore
from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET
from electrum.logging import get_logger
from ..hw_wallet import HW_PluginBase
from ..hw_wallet.plugin import (is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data,
- LibraryFoundButUnusable, OutdatedHwFirmwareException)
+ LibraryFoundButUnusable, OutdatedHwFirmwareException,
+ get_xpubs_and_der_suffixes_from_txinout)
_logger = get_logger(__name__)
t@@ -53,8 +54,7 @@ class TrezorKeyStore(Hardware_KeyStore):
hw_type = 'trezor'
device = TREZOR_PRODUCT_KEY
- def get_derivation(self):
- return self.derivation
+ plugin: 'TrezorPlugin'
def get_client(self, force_pair=True):
return self.plugin.get_client(self, force_pair)
t@@ -64,7 +64,7 @@ class TrezorKeyStore(Hardware_KeyStore):
def sign_message(self, sequence, message, password):
client = self.get_client()
- address_path = self.get_derivation() + "/%d/%d"%sequence
+ address_path = self.get_derivation_prefix() + "/%d/%d"%sequence
msg_sig = client.sign_message(address_path, message)
return msg_sig.signature
t@@ -73,22 +73,13 @@ class TrezorKeyStore(Hardware_KeyStore):
return
# previous transactions used as inputs
prev_tx = {}
- # path of the xpubs that are involved
- xpub_path = {}
for txin in tx.inputs():
- pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin)
- tx_hash = txin['prevout_hash']
- if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin):
- raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device))
- prev_tx[tx_hash] = txin['prev_tx']
- for x_pubkey in x_pubkeys:
- if not is_xpubkey(x_pubkey):
- continue
- xpub, s = parse_xpubkey(x_pubkey)
- if xpub == self.get_master_public_key():
- xpub_path[xpub] = self.get_derivation()
+ tx_hash = txin.prevout.txid.hex()
+ if txin.utxo is None and not Transaction.is_segwit_input(txin):
+ raise UserFacingException(_('Missing previous tx for legacy input.'))
+ prev_tx[tx_hash] = txin.utxo
- self.plugin.sign_transaction(self, tx, prev_tx, xpub_path)
+ self.plugin.sign_transaction(self, tx, prev_tx)
class TrezorInitSettings(NamedTuple):
t@@ -172,7 +163,7 @@ class TrezorPlugin(HW_PluginBase):
# note that this call can still raise!
return TrezorClientBase(transport, handler, self)
- def get_client(self, keystore, force_pair=True):
+ def get_client(self, keystore, force_pair=True) -> Optional['TrezorClientBase']:
devmgr = self.device_manager()
handler = keystore.handler
with devmgr.hid_lock:
t@@ -327,11 +318,11 @@ class TrezorPlugin(HW_PluginBase):
return OutputScriptType.PAYTOMULTISIG
raise ValueError('unexpected txin type: {}'.format(electrum_txin_type))
- def sign_transaction(self, keystore, tx, prev_tx, xpub_path):
- prev_tx = { bfh(txhash): self.electrum_tx_to_txtype(tx, xpub_path) for txhash, tx in prev_tx.items() }
+ def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx):
+ prev_tx = { bfh(txhash): self.electrum_tx_to_txtype(tx) for txhash, tx in prev_tx.items() }
client = self.get_client(keystore)
- inputs = self.tx_inputs(tx, xpub_path, True)
- outputs = self.tx_outputs(keystore.get_derivation(), tx)
+ inputs = self.tx_inputs(tx, for_sig=True, keystore=keystore)
+ outputs = self.tx_outputs(tx, keystore=keystore)
details = SignTx(lock_time=tx.locktime, version=tx.version)
signatures, _ = client.sign_tx(self.get_coin_name(), inputs, outputs, details=details, prev_txes=prev_tx)
signatures = [(bh2u(x) + '01') for x in signatures]
t@@ -343,7 +334,7 @@ class TrezorPlugin(HW_PluginBase):
if not self.show_address_helper(wallet, address, keystore):
return
deriv_suffix = wallet.get_address_index(address)
- derivation = keystore.derivation
+ derivation = keystore.get_derivation_prefix()
address_path = "%s/%d/%d"%(derivation, *deriv_suffix)
script_type = self.get_trezor_input_script_type(wallet.txin_type)
t@@ -355,111 +346,107 @@ class TrezorPlugin(HW_PluginBase):
sorted_pairs = sorted(zip(pubkeys, xpubs))
multisig = self._make_multisig(
wallet.m,
- [(xpub, deriv_suffix) for _, xpub in sorted_pairs])
+ [(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs])
else:
multisig = None
client = self.get_client(keystore)
client.show_address(address_path, script_type, multisig)
- def tx_inputs(self, tx, xpub_path, for_sig=False):
+ def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'TrezorKeyStore' = None):
inputs = []
for txin in tx.inputs():
txinputtype = TxInputType()
- if txin['type'] == 'coinbase':
+ if txin.is_coinbase():
prev_hash = b"\x00"*32
prev_index = 0xffffffff # signed int -1
else:
if for_sig:
- x_pubkeys = txin['x_pubkeys']
- xpubs = [parse_xpubkey(x) for x in x_pubkeys]
- multisig = self._make_multisig(txin.get('num_sig'), xpubs, txin.get('signatures'))
- script_type = self.get_trezor_input_script_type(txin['type'])
+ assert isinstance(tx, PartialTransaction)
+ assert isinstance(txin, PartialTxInput)
+ assert keystore
+ if len(txin.pubkeys) > 1:
+ xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin)
+ multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes)
+ else:
+ multisig = None
+ script_type = self.get_trezor_input_script_type(txin.script_type)
txinputtype = TxInputType(
script_type=script_type,
multisig=multisig)
- # find which key is mine
- for xpub, deriv in xpubs:
- if xpub in xpub_path:
- xpub_n = parse_path(xpub_path[xpub])
- txinputtype.address_n = xpub_n + deriv
- break
-
- prev_hash = bfh(txin['prevout_hash'])
- prev_index = txin['prevout_n']
-
- if 'value' in txin:
- txinputtype.amount = txin['value']
+ my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)
+ if full_path:
+ txinputtype.address_n = full_path
+
+ prev_hash = txin.prevout.txid
+ prev_index = txin.prevout.out_idx
+
+ if txin.value_sats() is not None:
+ txinputtype.amount = txin.value_sats()
txinputtype.prev_hash = prev_hash
txinputtype.prev_index = prev_index
- if txin.get('scriptSig') is not None:
- script_sig = bfh(txin['scriptSig'])
- txinputtype.script_sig = script_sig
+ if txin.script_sig is not None:
+ txinputtype.script_sig = txin.script_sig
- txinputtype.sequence = txin.get('sequence', 0xffffffff - 1)
+ txinputtype.sequence = txin.nsequence
inputs.append(txinputtype)
return inputs
- def _make_multisig(self, m, xpubs, signatures=None):
+ def _make_multisig(self, m, xpubs):
if len(xpubs) == 1:
return None
-
pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs]
- if signatures is None:
- signatures = [b''] * len(pubkeys)
- elif len(signatures) != len(pubkeys):
- raise RuntimeError('Mismatched number of signatures')
- else:
- signatures = [bfh(x)[:-1] if x else b'' for x in signatures]
-
return MultisigRedeemScriptType(
pubkeys=pubkeys,
- signatures=signatures,
+ signatures=[b''] * len(pubkeys),
m=m)
- def tx_outputs(self, derivation, tx: Transaction):
+ def tx_outputs(self, tx: PartialTransaction, *, keystore: 'TrezorKeyStore'):
def create_output_by_derivation():
- script_type = self.get_trezor_output_script_type(info.script_type)
- deriv = parse_path("/%d/%d" % index)
- multisig = self._make_multisig(m, [(xpub, deriv) for xpub in xpubs])
+ script_type = self.get_trezor_output_script_type(txout.script_type)
+ if len(txout.pubkeys) > 1:
+ xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
+ multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes)
+ else:
+ multisig = None
+ my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout)
+ assert full_path
txoutputtype = TxOutputType(
multisig=multisig,
- amount=amount,
- address_n=parse_path(derivation + "/%d/%d" % index),
+ amount=txout.value,
+ address_n=full_path,
script_type=script_type)
return txoutputtype
def create_output_by_address():
txoutputtype = TxOutputType()
- txoutputtype.amount = amount
- if _type == TYPE_SCRIPT:
- txoutputtype.script_type = OutputScriptType.PAYTOOPRETURN
- txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o)
- elif _type == TYPE_ADDRESS:
+ txoutputtype.amount = txout.value
+ if address:
txoutputtype.script_type = OutputScriptType.PAYTOADDRESS
txoutputtype.address = address
+ else:
+ txoutputtype.script_type = OutputScriptType.PAYTOOPRETURN
+ txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(txout)
return txoutputtype
outputs = []
has_change = False
any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
- for o in tx.outputs():
- _type, address, amount = o.type, o.address, o.value
+ for txout in tx.outputs():
+ address = txout.address
use_create_by_derivation = False
- info = tx.output_info.get(address)
- if info is not None and not has_change:
- index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig
+ if txout.is_mine and not has_change:
# prioritise hiding outputs on the 'change' branch from user
# because no more than one change address allowed
# note: ^ restriction can be removed once we require fw
# that has https://github.com/trezor/trezor-mcu/pull/306
- if info.is_change == any_output_on_change_branch:
+ if txout.is_change == any_output_on_change_branch:
use_create_by_derivation = True
has_change = True
t@@ -471,17 +458,17 @@ class TrezorPlugin(HW_PluginBase):
return outputs
- def electrum_tx_to_txtype(self, tx, xpub_path):
+ def electrum_tx_to_txtype(self, tx: Optional[Transaction]):
t = TransactionType()
if tx is None:
# probably for segwit input and we don't need this prev txn
return t
- d = deserialize(tx.raw)
- t.version = d['version']
- t.lock_time = d['lockTime']
- t.inputs = self.tx_inputs(tx, xpub_path)
+ tx.deserialize()
+ t.version = tx.version
+ t.lock_time = tx.locktime
+ t.inputs = self.tx_inputs(tx)
t.bin_outputs = [
- TxOutputBinType(amount=vout['value'], script_pubkey=bfh(vout['scriptPubKey']))
- for vout in d['outputs']
+ TxOutputBinType(amount=o.value, script_pubkey=o.scriptpubkey)
+ for o in tx.outputs()
]
return t
DIR diff --git a/electrum/plugins/trustedcoin/cmdline.py b/electrum/plugins/trustedcoin/cmdline.py
t@@ -30,7 +30,7 @@ from .trustedcoin import TrustedCoinPlugin
class Plugin(TrustedCoinPlugin):
- def prompt_user_for_otp(self, wallet, tx):
+ def prompt_user_for_otp(self, wallet, tx): # FIXME this is broken
if not isinstance(wallet, self.wallet_class):
return
if not wallet.can_sign_without_server():
DIR diff --git a/electrum/plugins/trustedcoin/legacy_tx_format.py b/electrum/plugins/trustedcoin/legacy_tx_format.py
t@@ -0,0 +1,106 @@
+# Copyright (C) 2018 The Electrum developers
+# Distributed under the MIT software license, see the accompanying
+# file LICENCE or http://www.opensource.org/licenses/mit-license.php
+
+import copy
+from typing import Union
+
+from electrum import bitcoin
+from electrum.bitcoin import push_script, int_to_hex, var_int
+from electrum.transaction import (Transaction, PartialTransaction, PartialTxInput,
+ multisig_script, construct_witness)
+from electrum.keystore import BIP32_KeyStore
+from electrum.wallet import Multisig_Wallet
+
+
+ELECTRUM_PARTIAL_TXN_HEADER_MAGIC = b'EPTF\xff'
+PARTIAL_FORMAT_VERSION = b'\x00'
+NO_SIGNATURE = b'\xff'
+
+
+def get_xpubkey(keystore: BIP32_KeyStore, c, i) -> str:
+ def encode_path_int(path_int) -> str:
+ if path_int < 0xffff:
+ hex = bitcoin.int_to_hex(path_int, 2)
+ else:
+ hex = 'ffff' + bitcoin.int_to_hex(path_int, 4)
+ return hex
+
+ s = ''.join(map(encode_path_int, (c, i)))
+ return 'ff' + bitcoin.DecodeBase58Check(keystore.xpub).hex() + s
+
+
+def serialize_tx_in_legacy_format(tx: PartialTransaction, *, wallet: Multisig_Wallet) -> str:
+ assert isinstance(tx, PartialTransaction)
+
+ # copy tx so we don't mutate the input arg
+ # monkey-patch method of tx instance to change serialization
+ tx = copy.deepcopy(tx)
+
+ def get_siglist(txin: 'PartialTxInput', *, estimate_size=False):
+ if txin.prevout.is_coinbase():
+ return [], []
+ if estimate_size:
+ try:
+ pubkey_size = len(txin.pubkeys[0])
+ except IndexError:
+ pubkey_size = 33 # guess it is compressed
+ num_pubkeys = max(1, len(txin.pubkeys))
+ pk_list = ["00" * pubkey_size] * num_pubkeys
+ # we assume that signature will be 0x48 bytes long
+ num_sig = max(txin.num_sig, num_pubkeys)
+ sig_list = [ "00" * 0x48 ] * num_sig
+ else:
+ pk_list = ["" for pk in txin.pubkeys]
+ for ks in wallet.get_keystores():
+ my_pubkey, full_path = ks.find_my_pubkey_in_txinout(txin)
+ x_pubkey = get_xpubkey(ks, full_path[-2], full_path[-1])
+ pubkey_index = txin.pubkeys.index(my_pubkey)
+ pk_list[pubkey_index] = x_pubkey
+ assert all(pk_list)
+ sig_list = [txin.part_sigs.get(pubkey, NO_SIGNATURE).hex() for pubkey in txin.pubkeys]
+ return pk_list, sig_list
+
+ def input_script(self, txin: PartialTxInput, *, estimate_size=False) -> str:
+ assert estimate_size is False
+ pubkeys, sig_list = get_siglist(txin, estimate_size=estimate_size)
+ script = ''.join(push_script(x) for x in sig_list)
+ if txin.script_type == 'p2sh':
+ # put op_0 before script
+ script = '00' + script
+ redeem_script = multisig_script(pubkeys, txin.num_sig)
+ script += push_script(redeem_script)
+ return script
+ elif txin.script_type == 'p2wsh':
+ return ''
+ raise Exception(f"unexpected type {txin.script_type}")
+ tx.input_script = input_script.__get__(tx, PartialTransaction)
+
+ def serialize_witness(self, txin: PartialTxInput, *, estimate_size=False):
+ assert estimate_size is False
+ if txin.witness is not None:
+ return txin.witness.hex()
+ if txin.prevout.is_coinbase():
+ return ''
+ assert isinstance(txin, PartialTxInput)
+ if not self.is_segwit_input(txin):
+ return '00'
+ pubkeys, sig_list = get_siglist(txin, estimate_size=estimate_size)
+ if txin.script_type == 'p2wsh':
+ witness_script = multisig_script(pubkeys, txin.num_sig)
+ witness = construct_witness([0] + sig_list + [witness_script])
+ else:
+ raise Exception(f"unexpected type {txin.script_type}")
+ if txin.is_complete() or estimate_size:
+ partial_format_witness_prefix = ''
+ else:
+ input_value = int_to_hex(txin.value_sats(), 8)
+ witness_version = int_to_hex(0, 2)
+ partial_format_witness_prefix = var_int(0xffffffff) + input_value + witness_version
+ return partial_format_witness_prefix + witness
+ tx.serialize_witness = serialize_witness.__get__(tx, PartialTransaction)
+
+ buf = ELECTRUM_PARTIAL_TXN_HEADER_MAGIC.hex()
+ buf += PARTIAL_FORMAT_VERSION.hex()
+ buf += tx.serialize_to_network()
+ return buf
DIR diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py
t@@ -29,7 +29,7 @@ import base64
import time
import hashlib
from collections import defaultdict
-from typing import Dict, Union
+from typing import Dict, Union, Sequence, List
from urllib.parse import urljoin
from urllib.parse import quote
t@@ -39,7 +39,7 @@ from electrum import ecc, constants, keystore, version, bip32, bitcoin
from electrum.bitcoin import TYPE_ADDRESS
from electrum.bip32 import BIP32Node, xpub_type
from electrum.crypto import sha256
-from electrum.transaction import TxOutput
+from electrum.transaction import PartialTxOutput, PartialTxInput, PartialTransaction, Transaction
from electrum.mnemonic import Mnemonic, seed_type, is_any_2fa_seed_type
from electrum.wallet import Multisig_Wallet, Deterministic_Wallet
from electrum.i18n import _
t@@ -50,6 +50,8 @@ from electrum.network import Network
from electrum.base_wizard import BaseWizard, WizardWalletPasswordSetting
from electrum.logging import Logger
+from .legacy_tx_format import serialize_tx_in_legacy_format
+
def get_signing_xpub(xtype):
if not constants.net.TESTNET:
t@@ -259,6 +261,8 @@ server = TrustedCoinCosignerClient(user_agent="Electrum/" + version.ELECTRUM_VER
class Wallet_2fa(Multisig_Wallet):
+ plugin: 'TrustedCoinPlugin'
+
wallet_type = '2fa'
def __init__(self, storage, *, config):
t@@ -314,34 +318,35 @@ class Wallet_2fa(Multisig_Wallet):
raise Exception('too high trustedcoin fee ({} for {} txns)'.format(price, n))
return price
- def make_unsigned_transaction(self, coins, outputs, fixed_fee=None,
- change_addr=None, is_sweep=False):
+ def make_unsigned_transaction(self, *, coins: Sequence[PartialTxInput],
+ outputs: List[PartialTxOutput], fee=None,
+ change_addr: str = None, is_sweep=False) -> PartialTransaction:
mk_tx = lambda o: Multisig_Wallet.make_unsigned_transaction(
- self, coins, o, fixed_fee, change_addr)
- fee = self.extra_fee() if not is_sweep else 0
- if fee:
+ self, coins=coins, outputs=o, fee=fee, change_addr=change_addr)
+ extra_fee = self.extra_fee() if not is_sweep else 0
+ if extra_fee:
address = self.billing_info['billing_address_segwit']
- fee_output = TxOutput(TYPE_ADDRESS, address, fee)
+ fee_output = PartialTxOutput.from_address_and_value(address, extra_fee)
try:
tx = mk_tx(outputs + [fee_output])
except NotEnoughFunds:
# TrustedCoin won't charge if the total inputs is
# lower than their fee
tx = mk_tx(outputs)
- if tx.input_value() >= fee:
+ if tx.input_value() >= extra_fee:
raise
self.logger.info("not charging for this tx")
else:
tx = mk_tx(outputs)
return tx
- def on_otp(self, tx, otp):
+ def on_otp(self, tx: PartialTransaction, otp):
if not otp:
self.logger.info("sign_transaction: no auth code")
return
otp = int(otp)
long_user_id, short_id = self.get_user_id()
- raw_tx = tx.serialize()
+ raw_tx = serialize_tx_in_legacy_format(tx, wallet=self)
try:
r = server.sign(short_id, raw_tx, otp)
except TrustedCoinException as e:
t@@ -350,8 +355,9 @@ class Wallet_2fa(Multisig_Wallet):
else:
raise
if r:
- raw_tx = r.get('transaction')
- tx.update(raw_tx)
+ received_raw_tx = r.get('transaction')
+ received_tx = Transaction(received_raw_tx)
+ tx.combine_with_other_psbt(received_tx)
self.logger.info(f"twofactor: is complete {tx.is_complete()}")
# reset billing_info
self.billing_info = None
t@@ -457,15 +463,16 @@ class TrustedCoinPlugin(BasePlugin):
self.logger.info("twofactor: xpub3 not needed")
return
def wrapper(tx):
+ assert tx
self.prompt_user_for_otp(wallet, tx, on_success, on_failure)
return wrapper
@hook
- def get_tx_extra_fee(self, wallet, tx):
+ def get_tx_extra_fee(self, wallet, tx: Transaction):
if type(wallet) != Wallet_2fa:
return
for o in tx.outputs():
- if o.type == TYPE_ADDRESS and wallet.is_billing_address(o.address):
+ if wallet.is_billing_address(o.address):
return o.address, o.value
def finish_requesting(func):
DIR diff --git a/electrum/scripts/bip70.py b/electrum/scripts/bip70.py
t@@ -7,6 +7,7 @@ import tlslite
from electrum.transaction import Transaction
from electrum import paymentrequest
from electrum import paymentrequest_pb2 as pb2
+from electrum.bitcoin import address_to_script
chain_file = 'mychain.pem'
cert_file = 'mycert.pem'
t@@ -26,7 +27,7 @@ certificates.certificate.extend(map(lambda x: str(x.bytes), chain.x509List))
with open(cert_file, 'r') as f:
rsakey = tlslite.utils.python_rsakey.Python_RSAKey.parsePEM(f.read())
-script = Transaction.pay_script('address', address).decode('hex')
+script = address_to_script(address)
pr_string = paymentrequest.make_payment_request(amount, script, memo, rsakey)
DIR diff --git a/electrum/segwit_addr.py b/electrum/segwit_addr.py
t@@ -103,6 +103,8 @@ def convertbits(data, frombits, tobits, pad=True):
def decode(hrp, addr):
"""Decode a segwit address."""
+ if addr is None:
+ return (None, None)
hrpgot, data = bech32_decode(addr)
if hrpgot != hrp:
return (None, None)
DIR diff --git a/electrum/synchronizer.py b/electrum/synchronizer.py
t@@ -209,7 +209,7 @@ class Synchronizer(SynchronizerBase):
async def _get_transaction(self, tx_hash, *, allow_server_not_finding_tx=False):
self._requests_sent += 1
try:
- result = await self.network.get_transaction(tx_hash)
+ raw_tx = await self.network.get_transaction(tx_hash)
except UntrustedServerReturnedError as e:
# most likely, "No such mempool or blockchain transaction"
if allow_server_not_finding_tx:
t@@ -219,7 +219,7 @@ class Synchronizer(SynchronizerBase):
raise
finally:
self._requests_answered += 1
- tx = Transaction(result)
+ tx = Transaction(raw_tx)
try:
tx.deserialize() # see if raises
except Exception as e:
t@@ -233,7 +233,7 @@ class Synchronizer(SynchronizerBase):
raise SynchronizerFailure(f"received tx does not match expected txid ({tx_hash} != {tx.txid()})")
tx_height = self.requested_tx.pop(tx_hash)
self.wallet.receive_tx_callback(tx_hash, tx, tx_height)
- self.logger.info(f"received tx {tx_hash} height: {tx_height} bytes: {len(tx.raw)}")
+ self.logger.info(f"received tx {tx_hash} height: {tx_height} bytes: {len(raw_tx)}")
# callbacks
self.wallet.network.trigger_callback('new_transaction', self.wallet, tx)
DIR diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh
t@@ -147,7 +147,7 @@ if [[ $1 == "breach" ]]; then
echo "alice pays"
$alice lnpay $request
sleep 2
- ctx=$($alice get_channel_ctx $channel | jq '.hex' | tr -d '"')
+ ctx=$($alice get_channel_ctx $channel)
request=$($bob add_lightning_request 0.01 -m "blah2")
echo "alice pays again"
$alice lnpay $request
t@@ -224,7 +224,7 @@ if [[ $1 == "breach_with_unspent_htlc" ]]; then
echo "SETTLE_DELAY did not work, $settled != 0"
exit 1
fi
- ctx=$($alice get_channel_ctx $channel | jq '.hex' | tr -d '"')
+ ctx=$($alice get_channel_ctx $channel)
sleep 5
settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length')
if [[ "$settled" != "1" ]]; then
t@@ -251,7 +251,7 @@ if [[ $1 == "breach_with_spent_htlc" ]]; then
echo "alice pays bob"
invoice=$($bob add_lightning_request 0.05 -m "test")
$alice lnpay $invoice --timeout=1 || true
- ctx=$($alice get_channel_ctx $channel | jq '.hex' | tr -d '"')
+ ctx=$($alice get_channel_ctx $channel)
settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length')
if [[ "$settled" != "0" ]]; then
echo "SETTLE_DELAY did not work, $settled != 0"
DIR diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py
t@@ -12,7 +12,7 @@ from electrum.bitcoin import (public_key_to_p2pkh, address_from_private_key,
from electrum.bip32 import (BIP32Node, convert_bip32_intpath_to_strpath,
xpub_from_xprv, xpub_type, is_xprv, is_bip32_derivation,
is_xpub, convert_bip32_path_to_list_of_uint32,
- normalize_bip32_derivation)
+ normalize_bip32_derivation, is_all_public_derivation)
from electrum.crypto import sha256d, SUPPORTED_PW_HASH_VERSIONS
from electrum import ecc, crypto, constants
from electrum.ecc import number_to_string, string_to_number
t@@ -494,6 +494,14 @@ class Test_xprv_xpub(ElectrumTestCase):
self.assertEqual("m/0/2/1'", normalize_bip32_derivation("m/0/2/-1/"))
self.assertEqual("m/0/1'/1'/5'", normalize_bip32_derivation("m/0//-1/1'///5h"))
+ def test_is_all_public_derivation(self):
+ self.assertFalse(is_all_public_derivation("m/0/1'/1'"))
+ self.assertFalse(is_all_public_derivation("m/0/2/1'"))
+ self.assertFalse(is_all_public_derivation("m/0/1'/1'/5"))
+ self.assertTrue(is_all_public_derivation("m"))
+ self.assertTrue(is_all_public_derivation("m/0"))
+ self.assertTrue(is_all_public_derivation("m/75/22/3"))
+
def test_xtype_from_derivation(self):
self.assertEqual('standard', xtype_from_derivation("m/44'"))
self.assertEqual('standard', xtype_from_derivation("m/44'/"))
DIR diff --git a/electrum/tests/test_commands.py b/electrum/tests/test_commands.py
t@@ -159,3 +159,24 @@ class TestCommandsTestnet(TestCaseForTestnet):
for xkey1, xtype1 in xprvs:
for xkey2, xtype2 in xprvs:
self.assertEqual(xkey2, cmds._run('convert_xkey', (xkey1, xtype2)))
+
+ def test_serialize(self):
+ cmds = Commands(config=self.config)
+ jsontx = {
+ "inputs": [
+ {
+ "prevout_hash": "9d221a69ca3997cbeaf5624d723e7dc5f829b1023078c177d37bdae95f37c539",
+ "prevout_n": 1,
+ "value": 1000000,
+ "privkey": "p2wpkh:cVDXzzQg6RoCTfiKpe8MBvmm5d5cJc6JLuFApsFDKwWa6F5TVHpD"
+ }
+ ],
+ "outputs": [
+ {
+ "address": "tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd",
+ "value": 990000
+ }
+ ]
+ }
+ self.assertEqual("0200000000010139c5375fe9da7bd377c1783002b129f8c57d3e724d62f5eacb9739ca691a229d0100000000feffffff01301b0f0000000000160014ac0e2d229200bffb2167ed6fd196aef9d687d8bb02483045022100fa88a9e7930b2af269fd0a5cb7fbbc3d0a05606f3ac6ea8a40686ebf02fdd85802203dd19603b4ee8fdb81d40185572027686f70ea299c6a3e22bc2545e1396398b20121021f110909ded653828a254515b58498a6bafc96799fb0851554463ed44ca7d9da00000000",
+ cmds._run('serialize', (jsontx,)))
DIR diff --git a/electrum/tests/test_lnchannel.py b/electrum/tests/test_lnchannel.py
t@@ -170,7 +170,7 @@ class TestFee(ElectrumTestCase):
"""
def test_fee(self):
alice_channel, bob_channel = create_test_channels(253, 10000000000, 5000000000)
- self.assertIn(9999817, [x[2] for x in alice_channel.get_latest_commitment(LOCAL).outputs()])
+ self.assertIn(9999817, [x.value for x in alice_channel.get_latest_commitment(LOCAL).outputs()])
class TestChannel(ElectrumTestCase):
maxDiff = 999
DIR diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py
t@@ -9,7 +9,7 @@ from electrum.lnutil import (RevocationStore, get_per_commitment_secret_from_see
get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError,
ScriptHtlc, extract_nodeid, calc_onchain_fees, UpdateAddHtlc)
from electrum.util import bh2u, bfh
-from electrum.transaction import Transaction
+from electrum.transaction import Transaction, PartialTransaction
from . import ElectrumTestCase
t@@ -570,7 +570,7 @@ class TestLNUtil(ElectrumTestCase):
localhtlcsig=bfh(local_sig),
payment_preimage=htlc_payment_preimage if success else b'', # will put 00 on witness if timeout
witness_script=htlc)
- our_htlc_tx._inputs[0]['witness'] = bh2u(our_htlc_tx_witness)
+ our_htlc_tx._inputs[0].witness = our_htlc_tx_witness
return str(our_htlc_tx)
def test_commitment_tx_with_one_output(self):
t@@ -669,7 +669,7 @@ class TestLNUtil(ElectrumTestCase):
ref_commit_tx_str = '02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220'
self.assertEqual(str(our_commit_tx), ref_commit_tx_str)
- def sign_and_insert_remote_sig(self, tx, remote_pubkey, remote_signature, pubkey, privkey):
+ def sign_and_insert_remote_sig(self, tx: PartialTransaction, remote_pubkey, remote_signature, pubkey, privkey):
assert type(remote_pubkey) is bytes
assert len(remote_pubkey) == 33
assert type(remote_signature) is str
t@@ -678,10 +678,7 @@ class TestLNUtil(ElectrumTestCase):
assert len(pubkey) == 33
assert len(privkey) == 33
tx.sign({bh2u(pubkey): (privkey[:-1], True)})
- pubkeys, _x_pubkeys = tx.get_sorted_pubkeys(tx.inputs()[0])
- index_of_pubkey = pubkeys.index(bh2u(remote_pubkey))
- tx._inputs[0]["signatures"][index_of_pubkey] = remote_signature + "01"
- tx.raw = None
+ tx.add_signature_to_txin(txin_idx=0, signing_pubkey=remote_pubkey.hex(), sig=remote_signature + "01")
def test_get_compressed_pubkey_from_bech32(self):
self.assertEqual(b'\x03\x84\xef\x87\xd9d\xa2\xaaa7=\xff\xb8\xfe=t8[}>;\n\x13\xa8e\x8eo:\xf5Mi\xb5H',
DIR diff --git a/electrum/tests/test_psbt.py b/electrum/tests/test_psbt.py
t@@ -0,0 +1,269 @@
+from pprint import pprint
+import unittest
+
+from electrum import constants
+from electrum.transaction import (tx_from_any, PartialTransaction, BadHeaderMagic, UnexpectedEndOfStream,
+ SerializationError, PSBTInputConsistencyFailure)
+
+from . import ElectrumTestCase, TestCaseForTestnet
+
+
+class TestValidPSBT(TestCaseForTestnet):
+ # test cases from BIP-0174
+
+ def test_valid_psbt_001(self):
+ # Case: PSBT with one P2PKH input. Outputs are empty
parazyd.org:70 /git/electrum/commit/707b74d22b28d942c445754311736f158e505990.gph:7173: line too long