mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-09-02 02:05:19 +00:00
fix #3345: do not require a wallet in order to sweep
This commit is contained in:
parent
61b69f3856
commit
2ea59aad14
4 changed files with 89 additions and 88 deletions
|
@ -2364,9 +2364,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
address_e.textChanged.connect(on_address)
|
address_e.textChanged.connect(on_address)
|
||||||
if not d.exec_():
|
if not d.exec_():
|
||||||
return
|
return
|
||||||
|
from electrum.wallet import sweep
|
||||||
try:
|
try:
|
||||||
tx = self.wallet.sweep(get_pk(), self.network, self.config, get_address(), None)
|
tx = sweep(get_pk(), self.network, self.config, get_address(), None)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
self.show_message(str(e))
|
self.show_message(str(e))
|
||||||
return
|
return
|
||||||
|
|
|
@ -383,16 +383,17 @@ class Commands:
|
||||||
raise BaseException('cannot verify alias', x)
|
raise BaseException('cannot verify alias', x)
|
||||||
return out['address']
|
return out['address']
|
||||||
|
|
||||||
@command('nw')
|
@command('n')
|
||||||
def sweep(self, privkey, destination, fee=None, nocheck=False, imax=100):
|
def sweep(self, privkey, destination, fee=None, nocheck=False, imax=100):
|
||||||
"""Sweep private keys. Returns a transaction that spends UTXOs from
|
"""Sweep private keys. Returns a transaction that spends UTXOs from
|
||||||
privkey to a destination address. The transaction is not
|
privkey to a destination address. The transaction is not
|
||||||
broadcasted."""
|
broadcasted."""
|
||||||
|
from .wallet import sweep
|
||||||
tx_fee = satoshis(fee)
|
tx_fee = satoshis(fee)
|
||||||
privkeys = privkey.split()
|
privkeys = privkey.split()
|
||||||
self.nocheck = nocheck
|
self.nocheck = nocheck
|
||||||
dest = self._resolver(destination)
|
#dest = self._resolver(destination)
|
||||||
tx = self.wallet.sweep(privkeys, self.network, self.config, dest, tx_fee, imax)
|
tx = sweep(privkeys, self.network, self.config, destination, tx_fee, imax)
|
||||||
return tx.as_dict() if tx else None
|
return tx.as_dict() if tx else None
|
||||||
|
|
||||||
@command('wp')
|
@command('wp')
|
||||||
|
@ -707,7 +708,7 @@ command_options = {
|
||||||
'nocheck': (None, "Do not verify aliases"),
|
'nocheck': (None, "Do not verify aliases"),
|
||||||
'imax': (None, "Maximum number of inputs"),
|
'imax': (None, "Maximum number of inputs"),
|
||||||
'fee': ("-f", "Transaction fee (in BTC)"),
|
'fee': ("-f", "Transaction fee (in BTC)"),
|
||||||
'from_addr': ("-F", "Source address. If it isn't in the wallet, it will ask for the private key unless supplied in the format public_key:private_key. It's not saved in the wallet."),
|
'from_addr': ("-F", "Source address (must be a wallet address; use sweep to spend from non-wallet address)."),
|
||||||
'change_addr': ("-c", "Change address. Default is a spare address, or the source address if it's not in the wallet"),
|
'change_addr': ("-c", "Change address. Default is a spare address, or the source address if it's not in the wallet"),
|
||||||
'nbits': (None, "Number of bits of entropy"),
|
'nbits': (None, "Number of bits of entropy"),
|
||||||
'entropy': (None, "Custom entropy"),
|
'entropy': (None, "Custom entropy"),
|
||||||
|
|
|
@ -249,6 +249,9 @@ class SimpleConfig(PrintError):
|
||||||
fee_rate = self.get('fee_per_kb', self.max_fee_rate()/2)
|
fee_rate = self.get('fee_per_kb', self.max_fee_rate()/2)
|
||||||
return fee_rate
|
return fee_rate
|
||||||
|
|
||||||
|
def estimate_fee(self, size):
|
||||||
|
return int(self.fee_per_kb() * size / 1000.)
|
||||||
|
|
||||||
def update_fee_estimates(self, key, value):
|
def update_fee_estimates(self, key, value):
|
||||||
self.fee_estimates[key] = value
|
self.fee_estimates[key] = value
|
||||||
self.fee_estimates_last_updated[key] = time.time()
|
self.fee_estimates_last_updated[key] = time.time()
|
||||||
|
|
161
lib/wallet.py
161
lib/wallet.py
|
@ -67,6 +67,81 @@ TX_STATUS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def relayfee(network):
|
||||||
|
RELAY_FEE = 5000
|
||||||
|
MAX_RELAY_FEE = 50000
|
||||||
|
f = network.relay_fee if network and network.relay_fee else RELAY_FEE
|
||||||
|
return min(f, MAX_RELAY_FEE)
|
||||||
|
|
||||||
|
def dust_threshold(network):
|
||||||
|
# Change <= dust threshold is added to the tx fee
|
||||||
|
return 182 * 3 * relayfee(network) / 1000
|
||||||
|
|
||||||
|
|
||||||
|
def append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax):
|
||||||
|
if txin_type != 'p2pk':
|
||||||
|
address = bitcoin.pubkey_to_address(txin_type, pubkey)
|
||||||
|
sh = bitcoin.address_to_scripthash(address)
|
||||||
|
else:
|
||||||
|
script = bitcoin.public_key_to_p2pk_script(pubkey)
|
||||||
|
sh = bitcoin.script_to_scripthash(script)
|
||||||
|
address = '(pubkey)'
|
||||||
|
u = network.synchronous_get(('blockchain.scripthash.listunspent', [sh]))
|
||||||
|
for item in u:
|
||||||
|
if len(inputs) >= imax:
|
||||||
|
break
|
||||||
|
item['address'] = address
|
||||||
|
item['type'] = txin_type
|
||||||
|
item['prevout_hash'] = item['tx_hash']
|
||||||
|
item['prevout_n'] = item['tx_pos']
|
||||||
|
item['pubkeys'] = [pubkey]
|
||||||
|
item['x_pubkeys'] = [pubkey]
|
||||||
|
item['signatures'] = [None]
|
||||||
|
item['num_sig'] = 1
|
||||||
|
inputs.append(item)
|
||||||
|
|
||||||
|
def sweep(privkeys, network, config, recipient, fee=None, imax=100):
|
||||||
|
|
||||||
|
def find_utxos_for_privkey(txin_type, privkey, compressed):
|
||||||
|
pubkey = bitcoin.public_key_from_private_key(privkey, compressed)
|
||||||
|
append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax)
|
||||||
|
keypairs[pubkey] = privkey, compressed
|
||||||
|
inputs = []
|
||||||
|
keypairs = {}
|
||||||
|
for sec in privkeys:
|
||||||
|
txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)
|
||||||
|
find_utxos_for_privkey(txin_type, privkey, compressed)
|
||||||
|
# do other lookups to increase support coverage
|
||||||
|
if is_minikey(sec):
|
||||||
|
# minikeys don't have a compressed byte
|
||||||
|
# we lookup both compressed and uncompressed pubkeys
|
||||||
|
find_utxos_for_privkey(txin_type, privkey, not compressed)
|
||||||
|
elif txin_type == 'p2pkh':
|
||||||
|
# WIF serialization does not distinguish p2pkh and p2pk
|
||||||
|
# we also search for pay-to-pubkey outputs
|
||||||
|
find_utxos_for_privkey('p2pk', privkey, compressed)
|
||||||
|
if not inputs:
|
||||||
|
raise BaseException(_('No inputs found. (Note that inputs need to be confirmed)'))
|
||||||
|
total = sum(i.get('value') for i in inputs)
|
||||||
|
if fee is None:
|
||||||
|
outputs = [(TYPE_ADDRESS, recipient, total)]
|
||||||
|
tx = Transaction.from_io(inputs, outputs)
|
||||||
|
fee = config.estimate_fee(tx.estimated_size())
|
||||||
|
if total - fee < 0:
|
||||||
|
raise BaseException(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d'%(total, fee))
|
||||||
|
if total - fee < dust_threshold(network):
|
||||||
|
raise BaseException(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d\nDust Threshold: %d'%(total, fee, dust_threshold(network)))
|
||||||
|
|
||||||
|
outputs = [(TYPE_ADDRESS, recipient, total - fee)]
|
||||||
|
locktime = network.get_local_height()
|
||||||
|
|
||||||
|
tx = Transaction.from_io(inputs, outputs, locktime=locktime)
|
||||||
|
tx.set_rbf(True)
|
||||||
|
tx.sign(keypairs)
|
||||||
|
return tx
|
||||||
|
|
||||||
|
|
||||||
class Abstract_Wallet(PrintError):
|
class Abstract_Wallet(PrintError):
|
||||||
"""
|
"""
|
||||||
Wallet classes are created to handle various address generation methods.
|
Wallet classes are created to handle various address generation methods.
|
||||||
|
@ -89,7 +164,6 @@ class Abstract_Wallet(PrintError):
|
||||||
self.multiple_change = storage.get('multiple_change', False)
|
self.multiple_change = storage.get('multiple_change', False)
|
||||||
self.labels = storage.get('labels', {})
|
self.labels = storage.get('labels', {})
|
||||||
self.frozen_addresses = set(storage.get('frozen_addresses',[]))
|
self.frozen_addresses = set(storage.get('frozen_addresses',[]))
|
||||||
self.stored_height = storage.get('stored_height', 0) # last known height (for offline mode)
|
|
||||||
self.history = storage.get('addr_history',{}) # address -> list(txid, height)
|
self.history = storage.get('addr_history',{}) # address -> list(txid, height)
|
||||||
|
|
||||||
self.load_keystore()
|
self.load_keystore()
|
||||||
|
@ -316,7 +390,7 @@ class Abstract_Wallet(PrintError):
|
||||||
|
|
||||||
def get_local_height(self):
|
def get_local_height(self):
|
||||||
""" return last known height if we are offline """
|
""" return last known height if we are offline """
|
||||||
return self.network.get_local_height() if self.network else self.stored_height
|
return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0)
|
||||||
|
|
||||||
def get_tx_height(self, tx_hash):
|
def get_tx_height(self, tx_hash):
|
||||||
""" return the height and timestamp of a verified transaction. """
|
""" return the height and timestamp of a verified transaction. """
|
||||||
|
@ -776,14 +850,10 @@ class Abstract_Wallet(PrintError):
|
||||||
return status, status_str
|
return status, status_str
|
||||||
|
|
||||||
def relayfee(self):
|
def relayfee(self):
|
||||||
RELAY_FEE = 5000
|
return relayfee(self.network)
|
||||||
MAX_RELAY_FEE = 50000
|
|
||||||
f = self.network.relay_fee if self.network and self.network.relay_fee else RELAY_FEE
|
|
||||||
return min(f, MAX_RELAY_FEE)
|
|
||||||
|
|
||||||
def dust_threshold(self):
|
def dust_threshold(self):
|
||||||
# Change <= dust threshold is added to the tx fee
|
return dust_threshold(self.network)
|
||||||
return 182 * 3 * self.relayfee() / 1000
|
|
||||||
|
|
||||||
def make_unsigned_transaction(self, inputs, outputs, config, fixed_fee=None, change_addr=None):
|
def make_unsigned_transaction(self, inputs, outputs, config, fixed_fee=None, change_addr=None):
|
||||||
# check outputs
|
# check outputs
|
||||||
|
@ -826,7 +896,7 @@ class Abstract_Wallet(PrintError):
|
||||||
|
|
||||||
# Fee estimator
|
# Fee estimator
|
||||||
if fixed_fee is None:
|
if fixed_fee is None:
|
||||||
fee_estimator = partial(self.estimate_fee, config)
|
fee_estimator = self.config.estimate_fee
|
||||||
else:
|
else:
|
||||||
fee_estimator = lambda size: fixed_fee
|
fee_estimator = lambda size: fixed_fee
|
||||||
|
|
||||||
|
@ -853,85 +923,12 @@ class Abstract_Wallet(PrintError):
|
||||||
run_hook('make_unsigned_transaction', self, tx)
|
run_hook('make_unsigned_transaction', self, tx)
|
||||||
return tx
|
return tx
|
||||||
|
|
||||||
def estimate_fee(self, config, size):
|
|
||||||
fee = int(config.fee_per_kb() * size / 1000.)
|
|
||||||
return fee
|
|
||||||
|
|
||||||
def mktx(self, outputs, password, config, fee=None, change_addr=None, domain=None):
|
def mktx(self, outputs, password, config, fee=None, change_addr=None, domain=None):
|
||||||
coins = self.get_spendable_coins(domain, config)
|
coins = self.get_spendable_coins(domain, config)
|
||||||
tx = self.make_unsigned_transaction(coins, outputs, config, fee, change_addr)
|
tx = self.make_unsigned_transaction(coins, outputs, config, fee, change_addr)
|
||||||
self.sign_transaction(tx, password)
|
self.sign_transaction(tx, password)
|
||||||
return tx
|
return tx
|
||||||
|
|
||||||
def _append_utxos_to_inputs(self, inputs, network, pubkey, txin_type, imax):
|
|
||||||
if txin_type != 'p2pk':
|
|
||||||
address = bitcoin.pubkey_to_address(txin_type, pubkey)
|
|
||||||
sh = bitcoin.address_to_scripthash(address)
|
|
||||||
else:
|
|
||||||
script = bitcoin.public_key_to_p2pk_script(pubkey)
|
|
||||||
sh = bitcoin.script_to_scripthash(script)
|
|
||||||
address = '(pubkey)'
|
|
||||||
u = network.synchronous_get(('blockchain.scripthash.listunspent', [sh]))
|
|
||||||
for item in u:
|
|
||||||
if len(inputs) >= imax:
|
|
||||||
break
|
|
||||||
item['address'] = address
|
|
||||||
item['type'] = txin_type
|
|
||||||
item['prevout_hash'] = item['tx_hash']
|
|
||||||
item['prevout_n'] = item['tx_pos']
|
|
||||||
item['pubkeys'] = [pubkey]
|
|
||||||
item['x_pubkeys'] = [pubkey]
|
|
||||||
item['signatures'] = [None]
|
|
||||||
item['num_sig'] = 1
|
|
||||||
inputs.append(item)
|
|
||||||
|
|
||||||
def sweep(self, privkeys, network, config, recipient, fee=None, imax=100):
|
|
||||||
|
|
||||||
def find_utxos_for_privkey(txin_type, privkey, compressed):
|
|
||||||
pubkey = bitcoin.public_key_from_private_key(privkey, compressed)
|
|
||||||
self._append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax)
|
|
||||||
keypairs[pubkey] = privkey, compressed
|
|
||||||
|
|
||||||
inputs = []
|
|
||||||
keypairs = {}
|
|
||||||
for sec in privkeys:
|
|
||||||
txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)
|
|
||||||
|
|
||||||
find_utxos_for_privkey(txin_type, privkey, compressed)
|
|
||||||
|
|
||||||
# do other lookups to increase support coverage
|
|
||||||
if is_minikey(sec):
|
|
||||||
# minikeys don't have a compressed byte
|
|
||||||
# we lookup both compressed and uncompressed pubkeys
|
|
||||||
find_utxos_for_privkey(txin_type, privkey, not compressed)
|
|
||||||
elif txin_type == 'p2pkh':
|
|
||||||
# WIF serialization does not distinguish p2pkh and p2pk
|
|
||||||
# we also search for pay-to-pubkey outputs
|
|
||||||
find_utxos_for_privkey('p2pk', privkey, compressed)
|
|
||||||
|
|
||||||
if not inputs:
|
|
||||||
raise BaseException(_('No inputs found. (Note that inputs need to be confirmed)'))
|
|
||||||
|
|
||||||
total = sum(i.get('value') for i in inputs)
|
|
||||||
if fee is None:
|
|
||||||
outputs = [(TYPE_ADDRESS, recipient, total)]
|
|
||||||
tx = Transaction.from_io(inputs, outputs)
|
|
||||||
fee = self.estimate_fee(config, tx.estimated_size())
|
|
||||||
|
|
||||||
if total - fee < 0:
|
|
||||||
raise BaseException(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d'%(total, fee))
|
|
||||||
|
|
||||||
if total - fee < self.dust_threshold():
|
|
||||||
raise BaseException(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d\nDust Threshold: %d'%(total, fee, self.dust_threshold()))
|
|
||||||
|
|
||||||
outputs = [(TYPE_ADDRESS, recipient, total - fee)]
|
|
||||||
locktime = self.get_local_height()
|
|
||||||
|
|
||||||
tx = Transaction.from_io(inputs, outputs, locktime=locktime)
|
|
||||||
tx.set_rbf(True)
|
|
||||||
tx.sign(keypairs)
|
|
||||||
return tx
|
|
||||||
|
|
||||||
def is_frozen(self, addr):
|
def is_frozen(self, addr):
|
||||||
return addr in self.frozen_addresses
|
return addr in self.frozen_addresses
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue