integrate PSBT support natively. WIP
This commit is contained in:
@@ -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
|
||||
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
|
||||
@@ -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
|
||||
@@ -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 = []
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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):
|
||||
@@ -788,11 +769,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',
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user