Browse Source

Persist LNWatcher transactions in wallet file:

- separate AddressSynchronizer from Wallet and LNWatcher
 - the AddressSynchronizer class is referred to as 'adb' (address database)
 - Use callbacks to replace overloaded methods
master
ThomasV 4 years ago
parent
commit
121d8732f1
  1. 37
      electrum/address_synchronizer.py
  2. 12
      electrum/commands.py
  3. 4
      electrum/gui/kivy/main_window.py
  4. 2
      electrum/gui/kivy/uix/dialogs/addresses.py
  5. 2
      electrum/gui/kivy/uix/dialogs/request_dialog.py
  6. 2
      electrum/gui/kivy/uix/screens.py
  7. 4
      electrum/gui/qt/address_list.py
  8. 2
      electrum/gui/qt/channels_list.py
  9. 6
      electrum/gui/qt/history_list.py
  10. 8
      electrum/gui/qt/main_window.py
  11. 6
      electrum/gui/qt/transaction_dialog.py
  12. 7
      electrum/lnchannel.py
  13. 2
      electrum/lnpeer.py
  14. 105
      electrum/lnwatcher.py
  15. 19
      electrum/lnworker.py
  16. 2
      electrum/network.py
  17. 9
      electrum/submarine_swaps.py
  18. 39
      electrum/synchronizer.py
  19. 8
      electrum/tests/test_commands.py
  20. 8
      electrum/tests/test_lnpeer.py
  21. 15
      electrum/tests/test_wallet.py
  22. 292
      electrum/tests/test_wallet_vertical.py
  23. 214
      electrum/wallet.py

37
electrum/address_synchronizer.py

@ -66,9 +66,7 @@ class TxWalletDelta(NamedTuple):
class AddressSynchronizer(Logger): class AddressSynchronizer(Logger):
""" """ address database """
inherited by wallet
"""
network: Optional['Network'] network: Optional['Network']
asyncio_loop: Optional['asyncio.AbstractEventLoop'] = None asyncio_loop: Optional['asyncio.AbstractEventLoop'] = None
@ -207,7 +205,7 @@ class AddressSynchronizer(Logger):
self.db.put('stored_height', self.get_local_height()) self.db.put('stored_height', self.get_local_height())
def add_address(self, address): def add_address(self, address):
if not self.db.get_addr_history(address): if address not in self.db.history:
self.db.history[address] = [] self.db.history[address] = []
self.set_up_to_date(False) self.set_up_to_date(False)
if self.synchronizer: if self.synchronizer:
@ -341,6 +339,7 @@ class AddressSynchronizer(Logger):
# save # save
self.db.add_transaction(tx_hash, tx) self.db.add_transaction(tx_hash, tx)
self.db.add_num_inputs_to_tx(tx_hash, len(tx.inputs())) self.db.add_num_inputs_to_tx(tx_hash, len(tx.inputs()))
util.trigger_callback('adb_added_tx', self, tx_hash)
return True return True
def remove_transaction(self, tx_hash: str) -> None: def remove_transaction(self, tx_hash: str) -> None:
@ -504,10 +503,7 @@ class AddressSynchronizer(Logger):
@with_lock @with_lock
@with_transaction_lock @with_transaction_lock
@with_local_height_cached @with_local_height_cached
def get_history(self, *, domain=None) -> Sequence[HistoryItem]: def get_history(self, domain) -> Sequence[HistoryItem]:
# get domain
if domain is None:
domain = self.get_addresses()
domain = set(domain) domain = set(domain)
# 1. Get the history of each address in the domain, maintain the # 1. Get the history of each address in the domain, maintain the
# delta of a tx as the sum of its deltas on domain addresses # delta of a tx as the sum of its deltas on domain addresses
@ -536,7 +532,7 @@ class AddressSynchronizer(Logger):
fee=fee, fee=fee,
balance=balance)) balance=balance))
# sanity check # sanity check
c, u, x = self.get_balance(domain=domain) c, u, x = self.get_balance(domain)
if balance != c + u + x: if balance != c + u + x:
raise Exception("wallet.get_history() failed balance sanity-check") raise Exception("wallet.get_history() failed balance sanity-check")
return h2 return h2
@ -607,8 +603,7 @@ class AddressSynchronizer(Logger):
with self.lock: with self.lock:
self.unverified_tx.pop(tx_hash, None) self.unverified_tx.pop(tx_hash, None)
self.db.add_verified_tx(tx_hash, info) self.db.add_verified_tx(tx_hash, info)
tx_mined_status = self.get_tx_height(tx_hash) util.trigger_callback('adb_added_verified_tx', self, tx_hash)
util.trigger_callback('verified', self, tx_hash, tx_mined_status)
def get_unverified_txs(self) -> Dict[str, int]: def get_unverified_txs(self) -> Dict[str, int]:
'''Returns a map from tx hash to transaction height''' '''Returns a map from tx hash to transaction height'''
@ -637,6 +632,9 @@ class AddressSynchronizer(Logger):
# a status update, that will overwrite it. # a status update, that will overwrite it.
self.unverified_tx[tx_hash] = tx_height self.unverified_tx[tx_hash] = tx_height
txs.add(tx_hash) txs.add(tx_hash)
for tx_hash in txs:
util.trigger_callback('adb_removed_verified_tx', self, tx_hash)
return txs return txs
def get_local_height(self) -> int: def get_local_height(self) -> int:
@ -688,7 +686,7 @@ class AddressSynchronizer(Logger):
if self.verifier: if self.verifier:
self.verifier.reset_request_counters() self.verifier.reset_request_counters()
# fire triggers # fire triggers
util.trigger_callback('status') util.trigger_callback('adb_set_up_to_date', self)
if status_changed: if status_changed:
self.logger.info(f'set_up_to_date: {up_to_date}') self.logger.info(f'set_up_to_date: {up_to_date}')
@ -837,17 +835,12 @@ class AddressSynchronizer(Logger):
received, sent = self.get_addr_io(address) received, sent = self.get_addr_io(address)
return sum([v for height, v, is_cb in received.values()]) return sum([v for height, v, is_cb in received.values()])
def get_addr_balance(self, address):
return self.get_balance([address])
@with_local_height_cached @with_local_height_cached
def get_balance(self, domain=None, *, excluded_addresses: Set[str] = None, def get_balance(self, domain, *, excluded_addresses: Set[str] = None,
excluded_coins: Set[str] = None) -> Tuple[int, int, int]: excluded_coins: Set[str] = None) -> Tuple[int, int, int]:
"""Return the balance of a set of addresses: """Return the balance of a set of addresses:
confirmed and matured, unconfirmed, unmatured confirmed and matured, unconfirmed, unmatured
""" """
if domain is None:
domain = self.get_addresses()
if excluded_addresses is None: if excluded_addresses is None:
excluded_addresses = set() excluded_addresses = set()
assert isinstance(excluded_addresses, set), f"excluded_addresses should be set, not {type(excluded_addresses)}" assert isinstance(excluded_addresses, set), f"excluded_addresses should be set, not {type(excluded_addresses)}"
@ -909,7 +902,7 @@ class AddressSynchronizer(Logger):
@with_local_height_cached @with_local_height_cached
def get_utxos( def get_utxos(
self, self,
domain=None, domain,
*, *,
excluded_addresses=None, excluded_addresses=None,
mature_only: bool = False, mature_only: bool = False,
@ -926,8 +919,6 @@ class AddressSynchronizer(Logger):
else: else:
block_height = self.get_local_height() block_height = self.get_local_height()
coins = [] coins = []
if domain is None:
domain = self.get_addresses()
domain = set(domain) domain = set(domain)
if excluded_addresses: if excluded_addresses:
domain = set(domain) - set(excluded_addresses) domain = set(domain) - set(excluded_addresses)
@ -957,7 +948,3 @@ class AddressSynchronizer(Logger):
def is_empty(self, address: str) -> bool: def is_empty(self, address: str) -> bool:
coins = self.get_addr_utxo(address) coins = self.get_addr_utxo(address)
return not bool(coins) return not bool(coins)
def synchronize(self) -> int:
"""Returns the number of new addresses we generated."""
return 0

12
electrum/commands.py

@ -826,9 +826,9 @@ class Commands:
continue continue
if change and not wallet.is_change(addr): if change and not wallet.is_change(addr):
continue continue
if unused and wallet.is_used(addr): if unused and wallet.adb.is_used(addr):
continue continue
if funded and wallet.is_empty(addr): if funded and wallet.adb.is_empty(addr):
continue continue
item = addr item = addr
if labels or balance: if labels or balance:
@ -965,7 +965,7 @@ class Commands:
async def addtransaction(self, tx, wallet: Abstract_Wallet = None): async def addtransaction(self, tx, wallet: Abstract_Wallet = None):
""" Add a transaction to the wallet history """ """ Add a transaction to the wallet history """
tx = Transaction(tx) tx = Transaction(tx)
if not wallet.add_transaction(tx): if not wallet.adb.add_transaction(tx):
return False return False
wallet.save_db() wallet.save_db()
return tx.txid() return tx.txid()
@ -1040,11 +1040,11 @@ class Commands:
""" """
if not is_hash256_str(txid): if not is_hash256_str(txid):
raise Exception(f"{repr(txid)} is not a txid") raise Exception(f"{repr(txid)} is not a txid")
height = wallet.get_tx_height(txid).height height = wallet.adb.get_tx_height(txid).height
if height != TX_HEIGHT_LOCAL: if height != TX_HEIGHT_LOCAL:
raise Exception(f'Only local transactions can be removed. ' raise Exception(f'Only local transactions can be removed. '
f'This tx has height: {height} != {TX_HEIGHT_LOCAL}') f'This tx has height: {height} != {TX_HEIGHT_LOCAL}')
wallet.remove_transaction(txid) wallet.adb.remove_transaction(txid)
wallet.save_db() wallet.save_db()
@command('wn') @command('wn')
@ -1057,7 +1057,7 @@ class Commands:
if not wallet.db.get_transaction(txid): if not wallet.db.get_transaction(txid):
raise Exception("Transaction not in wallet.") raise Exception("Transaction not in wallet.")
return { return {
"confirmations": wallet.get_tx_height(txid).conf, "confirmations": wallet.adb.get_tx_height(txid).conf,
} }
@command('') @command('')

4
electrum/gui/kivy/main_window.py

@ -957,7 +957,7 @@ class ElectrumWindow(App, Logger):
server_height = self.network.get_server_height() server_height = self.network.get_server_height()
server_lag = self.num_blocks - server_height server_lag = self.num_blocks - server_height
if not self.wallet.is_up_to_date() or server_height == 0: if not self.wallet.is_up_to_date() or server_height == 0:
num_sent, num_answered = self.wallet.get_history_sync_state_details() num_sent, num_answered = self.wallet.adb.get_history_sync_state_details()
status = ("{} [size=18dp]({}/{})[/size]" status = ("{} [size=18dp]({}/{})[/size]"
.format(_("Synchronizing..."), num_answered, num_sent)) .format(_("Synchronizing..."), num_answered, num_sent))
elif server_lag > 1: elif server_lag > 1:
@ -1164,7 +1164,7 @@ class ElectrumWindow(App, Logger):
def show_transaction(self, txid): def show_transaction(self, txid):
tx = self.wallet.db.get_transaction(txid) tx = self.wallet.db.get_transaction(txid)
if not tx and self.wallet.lnworker: if not tx and self.wallet.lnworker:
tx = self.wallet.lnworker.lnwatcher.db.get_transaction(txid) tx = self.wallet.adb.get_transaction(txid)
if tx: if tx:
self.tx_dialog(tx) self.tx_dialog(tx)
else: else:

2
electrum/gui/kivy/uix/dialogs/addresses.py

@ -264,7 +264,7 @@ class AddressesDialog(Factory.Popup):
for address in _list: for address in _list:
label = wallet.get_label(address) label = wallet.get_label(address)
balance = sum(wallet.get_addr_balance(address)) balance = sum(wallet.get_addr_balance(address))
is_used_and_empty = wallet.is_used(address) and balance == 0 is_used_and_empty = wallet.adb.is_used(address) and balance == 0
if self.show_used == 1 and (balance or is_used_and_empty): if self.show_used == 1 and (balance or is_used_and_empty):
continue continue
if self.show_used == 2 and balance == 0: if self.show_used == 2 and balance == 0:

2
electrum/gui/kivy/uix/dialogs/request_dialog.py

@ -168,7 +168,7 @@ class RequestDialog(Factory.Popup):
address = req.get_address() address = req.get_address()
if not address: if not address:
warning = _('Warning') + ': ' + _('This request cannot be paid on-chain') warning = _('Warning') + ': ' + _('This request cannot be paid on-chain')
elif self.app.wallet.is_used(address): elif self.app.wallet.adb.is_used(address):
warning = _('Warning') + ': ' + _('This address is being reused') warning = _('Warning') + ': ' + _('This address is being reused')
self.warning = warning self.warning = warning

2
electrum/gui/kivy/uix/screens.py

@ -102,7 +102,7 @@ class HistoryScreen(CScreen):
self.app.lightning_tx_dialog(tx_item) self.app.lightning_tx_dialog(tx_item)
return return
if tx_item.get('lightning'): if tx_item.get('lightning'):
tx = self.app.wallet.lnworker.lnwatcher.db.get_transaction(key) tx = self.app.wallet.adb.get_transaction(key)
else: else:
tx = self.app.wallet.db.get_transaction(key) tx = self.app.wallet.db.get_transaction(key)
if not tx: if not tx:

4
electrum/gui/qt/address_list.py

@ -167,7 +167,7 @@ class AddressList(MyTreeView):
for address in addr_list: for address in addr_list:
c, u, x = self.wallet.get_addr_balance(address) c, u, x = self.wallet.get_addr_balance(address)
balance = c + u + x balance = c + u + x
is_used_and_empty = self.wallet.is_used(address) and balance == 0 is_used_and_empty = self.wallet.adb.is_used(address) and balance == 0
if self.show_used == AddressUsageStateFilter.UNUSED and (balance or is_used_and_empty): if self.show_used == AddressUsageStateFilter.UNUSED and (balance or is_used_and_empty):
continue continue
if self.show_used == AddressUsageStateFilter.FUNDED and balance == 0: if self.show_used == AddressUsageStateFilter.FUNDED and balance == 0:
@ -219,7 +219,7 @@ class AddressList(MyTreeView):
def refresh_row(self, key, row): def refresh_row(self, key, row):
address = key address = key
label = self.wallet.get_label(address) label = self.wallet.get_label(address)
num = self.wallet.get_address_history_len(address) num = self.wallet.adb.get_address_history_len(address)
c, u, x = self.wallet.get_addr_balance(address) c, u, x = self.wallet.get_addr_balance(address)
balance = c + u + x balance = c + u + x
balance_text = self.parent.format_amount(balance, whitespaces=True) balance_text = self.parent.format_amount(balance, whitespaces=True)

2
electrum/gui/qt/channels_list.py

@ -258,7 +258,7 @@ class ChannelsList(MyTreeView):
item = chan.get_closing_height() item = chan.get_closing_height()
if item: if item:
txid, height, timestamp = item txid, height, timestamp = item
closing_tx = self.lnworker.lnwatcher.db.get_transaction(txid) closing_tx = self.parent.wallet.db.get_transaction(txid)
if closing_tx: if closing_tx:
menu.addAction(_("View closing transaction"), lambda: self.parent.show_transaction(closing_tx)) menu.addAction(_("View closing transaction"), lambda: self.parent.show_transaction(closing_tx))
menu.addSeparator() menu.addSeparator()

6
electrum/gui/qt/history_list.py

@ -717,7 +717,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
return return
tx_hash = tx_item['txid'] tx_hash = tx_item['txid']
if tx_item.get('lightning'): if tx_item.get('lightning'):
tx = self.wallet.lnworker.lnwatcher.db.get_transaction(tx_hash) tx = self.wallet.adb.get_transaction(tx_hash)
else: else:
tx = self.wallet.db.get_transaction(tx_hash) tx = self.wallet.db.get_transaction(tx_hash)
if not tx: if not tx:
@ -760,7 +760,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
menu.exec_(self.viewport().mapToGlobal(position)) menu.exec_(self.viewport().mapToGlobal(position))
def remove_local_tx(self, tx_hash: str): def remove_local_tx(self, tx_hash: str):
num_child_txs = len(self.wallet.get_depending_transactions(tx_hash)) num_child_txs = len(self.wallet.adb.get_depending_transactions(tx_hash))
question = _("Are you sure you want to remove this transaction?") question = _("Are you sure you want to remove this transaction?")
if num_child_txs > 0: if num_child_txs > 0:
question = (_("Are you sure you want to remove this transaction and {} child transactions?") question = (_("Are you sure you want to remove this transaction and {} child transactions?")
@ -768,7 +768,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
if not self.parent.question(msg=question, if not self.parent.question(msg=question,
title=_("Please confirm")): title=_("Please confirm")):
return return
self.wallet.remove_transaction(tx_hash) self.wallet.adb.remove_transaction(tx_hash)
self.wallet.save_db() self.wallet.save_db()
# need to update at least: history_list, utxo_list, address_list # need to update at least: history_list, utxo_list, address_list
self.parent.need_update.set() self.parent.need_update.set()

8
electrum/gui/qt/main_window.py

@ -1015,7 +1015,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
# until we get a headers subscription request response. # until we get a headers subscription request response.
# Display the synchronizing message in that case. # Display the synchronizing message in that case.
if not self.wallet.is_up_to_date() or server_height == 0: if not self.wallet.is_up_to_date() or server_height == 0:
num_sent, num_answered = self.wallet.get_history_sync_state_details() num_sent, num_answered = self.wallet.adb.get_history_sync_state_details()
network_text = ("{} ({}/{})" network_text = ("{} ({}/{})"
.format(_("Synchronizing..."), num_answered, num_sent)) .format(_("Synchronizing..."), num_answered, num_sent))
icon = read_QIcon("status_waiting.png") icon = read_QIcon("status_waiting.png")
@ -1510,7 +1510,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def update_receive_address_styling(self): def update_receive_address_styling(self):
addr = str(self.receive_address_e.text()) addr = str(self.receive_address_e.text())
if is_address(addr) and self.wallet.is_used(addr): if is_address(addr) and self.wallet.adb.is_used(addr):
self.receive_address_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) self.receive_address_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True))
self.receive_address_e.setToolTip(_("This address has already been used. " self.receive_address_e.setToolTip(_("This address has already been used. "
"For better privacy, do not reuse it for new payments.")) "For better privacy, do not reuse it for new payments."))
@ -3577,7 +3577,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
total_size = parent_tx.estimated_size() + new_tx.estimated_size() total_size = parent_tx.estimated_size() + new_tx.estimated_size()
parent_txid = parent_tx.txid() parent_txid = parent_tx.txid()
assert parent_txid assert parent_txid
parent_fee = self.wallet.get_tx_fee(parent_txid) parent_fee = self.wallet.adb.get_tx_fee(parent_txid)
if parent_fee is None: if parent_fee is None:
self.show_error(_("Can't CPFP: unknown fee for parent transaction.")) self.show_error(_("Can't CPFP: unknown fee for parent transaction."))
return return
@ -3699,7 +3699,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def save_transaction_into_wallet(self, tx: Transaction): def save_transaction_into_wallet(self, tx: Transaction):
win = self.top_level_window() win = self.top_level_window()
try: try:
if not self.wallet.add_transaction(tx): if not self.wallet.adb.add_transaction(tx):
win.show_error(_("Transaction could not be saved.") + "\n" + win.show_error(_("Transaction could not be saved.") + "\n" +
_("It conflicts with current history.")) _("It conflicts with current history."))
return False return False

6
electrum/gui/qt/transaction_dialog.py

@ -449,7 +449,7 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
item = lnworker_history[txid] item = lnworker_history[txid]
ln_amount = item['amount_msat'] / 1000 ln_amount = item['amount_msat'] / 1000
if amount is None: if amount is None:
tx_mined_status = self.wallet.lnworker.lnwatcher.get_tx_height(txid) tx_mined_status = self.wallet.lnworker.lnwatcher.adb.get_tx_height(txid)
else: else:
ln_amount = None ln_amount = None
self.broadcast_button.setEnabled(tx_details.can_broadcast) self.broadcast_button.setEnabled(tx_details.can_broadcast)
@ -618,11 +618,11 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
prevout_hash = txin.prevout.txid.hex() prevout_hash = txin.prevout.txid.hex()
prevout_n = txin.prevout.out_idx prevout_n = txin.prevout.out_idx
cursor.insertText(prevout_hash + ":%-4d " % prevout_n, ext) cursor.insertText(prevout_hash + ":%-4d " % prevout_n, ext)
addr = self.wallet.get_txin_address(txin) addr = self.wallet.adb.get_txin_address(txin)
if addr is None: if addr is None:
addr = '' addr = ''
cursor.insertText(addr, text_format(addr)) cursor.insertText(addr, text_format(addr))
txin_value = self.wallet.get_txin_value(txin) txin_value = self.wallet.adb.get_txin_value(txin)
if txin_value is not None: if txin_value is not None:
cursor.insertText(format_amount(txin_value), ext) cursor.insertText(format_amount(txin_value), ext)
cursor.insertBlock() cursor.insertBlock()

7
electrum/lnchannel.py

@ -281,7 +281,7 @@ class AbstractChannel(Logger, ABC):
if spender_txid is None: if spender_txid is None:
continue continue
if spender_txid != self.funding_outpoint.txid: if spender_txid != self.funding_outpoint.txid:
tx_mined_height = self.lnworker.wallet.get_tx_height(spender_txid) tx_mined_height = self.lnworker.wallet.adb.get_tx_height(spender_txid)
if tx_mined_height.conf > lnutil.REDEEM_AFTER_DOUBLE_SPENT_DELAY: if tx_mined_height.conf > lnutil.REDEEM_AFTER_DOUBLE_SPENT_DELAY:
self.logger.info(f'channel is double spent {inputs}') self.logger.info(f'channel is double spent {inputs}')
self.set_state(ChannelState.REDEEMED) self.set_state(ChannelState.REDEEMED)
@ -486,7 +486,8 @@ class ChannelBackup(AbstractChannel):
def get_capacity(self): def get_capacity(self):
lnwatcher = self.lnworker.lnwatcher lnwatcher = self.lnworker.lnwatcher
if lnwatcher: if lnwatcher:
return lnwatcher.get_tx_delta(self.funding_outpoint.txid, self.cb.funding_address) # fixme: we should probably not call that method here
return lnwatcher.adb.get_tx_delta(self.funding_outpoint.txid, self.cb.funding_address)
return None return None
def is_backup(self): def is_backup(self):
@ -1549,7 +1550,7 @@ class Channel(AbstractChannel):
return False return False
assert conf > 0 assert conf > 0
# check funding_tx amount and script # check funding_tx amount and script
funding_tx = self.lnworker.lnwatcher.db.get_transaction(funding_txid) funding_tx = self.lnworker.lnwatcher.adb.get_transaction(funding_txid)
if not funding_tx: if not funding_tx:
self.logger.info(f"no funding_tx {funding_txid}") self.logger.info(f"no funding_tx {funding_txid}")
return False return False

2
electrum/lnpeer.py

@ -2175,7 +2175,7 @@ class Peer(Logger):
sig=bh2u(der_sig_from_sig_string(their_sig) + b'\x01')) sig=bh2u(der_sig_from_sig_string(their_sig) + b'\x01'))
# save local transaction and set state # save local transaction and set state
try: try:
self.lnworker.wallet.add_transaction(closing_tx) self.lnworker.wallet.adb.add_transaction(closing_tx)
except UnrelatedTransactionException: except UnrelatedTransactionException:
pass # this can happen if (~all the balance goes to REMOTE) pass # this can happen if (~all the balance goes to REMOTE)
chan.set_state(ChannelState.CLOSING) chan.set_state(ChannelState.CLOSING)

105
electrum/lnwatcher.py

@ -16,6 +16,7 @@ from .address_synchronizer import AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGH
from .transaction import Transaction, TxOutpoint from .transaction import Transaction, TxOutpoint
from .transaction import match_script_against_template from .transaction import match_script_against_template
from .lnutil import WITNESS_TEMPLATE_RECEIVED_HTLC, WITNESS_TEMPLATE_OFFERED_HTLC from .lnutil import WITNESS_TEMPLATE_RECEIVED_HTLC, WITNESS_TEMPLATE_OFFERED_HTLC
from .logging import Logger
if TYPE_CHECKING: if TYPE_CHECKING:
@ -135,24 +136,34 @@ class SweepStore(SqlDB):
class LNWatcher(AddressSynchronizer): class LNWatcher(Logger):
LOGGING_SHORTCUT = 'W' LOGGING_SHORTCUT = 'W'
def __init__(self, network: 'Network'): def __init__(self, adb, network: 'Network'):
AddressSynchronizer.__init__(self, WalletDB({}, manual_upgrades=False))
Logger.__init__(self)
self.adb = adb
self.config = network.config self.config = network.config
self.callbacks = {} # address -> lambda: coroutine self.callbacks = {} # address -> lambda: coroutine
self.network = network self.network = network
util.register_callback(
self.on_network_update, util.register_callback(self.on_fee, ['fee'])
['network_updated', 'blockchain_updated', 'verified', 'wallet_updated', 'fee']) util.register_callback(self.on_blockchain_updated, ['blockchain_updated'])
util.register_callback(self.on_network_updated, ['network_updated'])
util.register_callback(self.on_adb_added_verified_tx, ['adb_added_verified_tx'])
util.register_callback(self.on_adb_set_up_to_date, ['adb_set_up_to_date'])
# status gets populated when we run # status gets populated when we run
self.channel_status = {} self.channel_status = {}
async def stop(self): async def stop(self):
await super().stop() util.unregister_callback(self.on_fee)
util.unregister_callback(self.on_network_update) util.unregister_callback(self.on_blockchain_updated)
util.unregister_callback(self.on_network_updated)
util.unregister_callback(self.on_adb_added_verified_tx)
util.unregister_callback(self.on_adb_set_up_to_date)
def get_channel_status(self, outpoint): def get_channel_status(self, outpoint):
return self.channel_status.get(outpoint, 'unknown') return self.channel_status.get(outpoint, 'unknown')
@ -171,15 +182,31 @@ class LNWatcher(AddressSynchronizer):
self.callbacks.pop(address, None) self.callbacks.pop(address, None)
def add_callback(self, address, callback): def add_callback(self, address, callback):
self.add_address(address) self.adb.add_address(address)
self.callbacks[address] = callback self.callbacks[address] = callback
async def on_fee(self, event, *args):
await self.trigger_callbacks()
async def on_network_updated(self, event, *args):
await self.trigger_callbacks()
async def on_blockchain_updated(self, event, *args):
await self.trigger_callbacks()
async def on_adb_added_verified_tx(self, event, adb, tx_hash):
if adb != self.adb:
return
await self.trigger_callbacks()
async def on_adb_set_up_to_date(self, event, adb):
if adb != self.adb:
return
await self.trigger_callbacks()
@log_exceptions @log_exceptions
async def on_network_update(self, event, *args): async def trigger_callbacks(self):
if event in ('verified', 'wallet_updated'): if not self.adb.synchronizer:
if args[0] != self:
return
if not self.synchronizer:
self.logger.info("synchronizer not set yet") self.logger.info("synchronizer not set yet")
return return
for address, callback in list(self.callbacks.items()): for address, callback in list(self.callbacks.items()):
@ -187,18 +214,18 @@ class LNWatcher(AddressSynchronizer):
async def check_onchain_situation(self, address, funding_outpoint): async def check_onchain_situation(self, address, funding_outpoint):
# early return if address has not been added yet # early return if address has not been added yet
if not self.is_mine(address): if not self.adb.is_mine(address):
return return
spenders = self.inspect_tx_candidate(funding_outpoint, 0) spenders = self.inspect_tx_candidate(funding_outpoint, 0)
# inspect_tx_candidate might have added new addresses, in which case we return ealy # inspect_tx_candidate might have added new addresses, in which case we return ealy
if not self.is_up_to_date(): if not self.adb.is_up_to_date():
return return
funding_txid = funding_outpoint.split(':')[0] funding_txid = funding_outpoint.split(':')[0]
funding_height = self.get_tx_height(funding_txid) funding_height = self.adb.get_tx_height(funding_txid)
closing_txid = spenders.get(funding_outpoint) closing_txid = spenders.get(funding_outpoint)
closing_height = self.get_tx_height(closing_txid) closing_height = self.adb.get_tx_height(closing_txid)
if closing_txid: if closing_txid:
closing_tx = self.db.get_transaction(closing_txid) closing_tx = self.adb.get_transaction(closing_txid)
if closing_tx: if closing_tx:
keep_watching = await self.do_breach_remedy(funding_outpoint, closing_tx, spenders) keep_watching = await self.do_breach_remedy(funding_outpoint, closing_tx, spenders)
else: else:
@ -233,18 +260,18 @@ class LNWatcher(AddressSynchronizer):
n==2 => outpoint is a second-stage htlc n==2 => outpoint is a second-stage htlc
""" """
prev_txid, index = outpoint.split(':') prev_txid, index = outpoint.split(':')
spender_txid = self.db.get_spent_outpoint(prev_txid, int(index)) spender_txid = self.adb.db.get_spent_outpoint(prev_txid, int(index))
result = {outpoint:spender_txid} result = {outpoint:spender_txid}
if n == 0: if n == 0:
if spender_txid is None: if spender_txid is None:
self.channel_status[outpoint] = 'open' self.channel_status[outpoint] = 'open'
elif not self.is_deeply_mined(spender_txid): elif not self.is_deeply_mined(spender_txid):
self.channel_status[outpoint] = 'closed (%d)' % self.get_tx_height(spender_txid).conf self.channel_status[outpoint] = 'closed (%d)' % self.adb.get_tx_height(spender_txid).conf
else: else:
self.channel_status[outpoint] = 'closed (deep)' self.channel_status[outpoint] = 'closed (deep)'
if spender_txid is None: if spender_txid is None:
return result return result
spender_tx = self.db.get_transaction(spender_txid) spender_tx = self.adb.get_transaction(spender_txid)
if n == 1: if n == 1:
# if tx input is not a first-stage HTLC, we can stop recursion # if tx input is not a first-stage HTLC, we can stop recursion
if len(spender_tx.inputs()) != 1: if len(spender_tx.inputs()) != 1:
@ -263,8 +290,8 @@ class LNWatcher(AddressSynchronizer):
for i, o in enumerate(spender_tx.outputs()): for i, o in enumerate(spender_tx.outputs()):
if o.address is None: if o.address is None:
continue continue
if not self.is_mine(o.address): if not self.adb.is_mine(o.address):
self.add_address(o.address) self.adb.add_address(o.address)
elif n < 2: elif n < 2:
r = self.inspect_tx_candidate(spender_txid+':%d'%i, n+1) r = self.inspect_tx_candidate(spender_txid+':%d'%i, n+1)
result.update(r) result.update(r)
@ -273,7 +300,7 @@ class LNWatcher(AddressSynchronizer):
def get_tx_mined_depth(self, txid: str): def get_tx_mined_depth(self, txid: str):
if not txid: if not txid:
return TxMinedDepth.FREE return TxMinedDepth.FREE
tx_mined_depth = self.get_tx_height(txid) tx_mined_depth = self.adb.get_tx_height(txid)
height, conf = tx_mined_depth.height, tx_mined_depth.conf height, conf = tx_mined_depth.height, tx_mined_depth.conf
if conf > 100: if conf > 100:
return TxMinedDepth.DEEP return TxMinedDepth.DEEP
@ -298,13 +325,19 @@ class WatchTower(LNWatcher):
LOGGING_SHORTCUT = 'W' LOGGING_SHORTCUT = 'W'
def __init__(self, network): def __init__(self, network):
LNWatcher.__init__(self, network) adb = AddressSynchronizer(WalletDB({}, manual_upgrades=False))
adb.start_network(network)
LNWatcher.__init__(self, adb, network)
self.network = network self.network = network
self.sweepstore = SweepStore(os.path.join(self.network.config.path, "watchtower_db"), network) self.sweepstore = SweepStore(os.path.join(self.network.config.path, "watchtower_db"), network)
# this maps funding_outpoints to ListenerItems, which have an event for when the watcher is done, # this maps funding_outpoints to ListenerItems, which have an event for when the watcher is done,
# and a queue for seeing which txs are being published # and a queue for seeing which txs are being published
self.tx_progress = {} # type: Dict[str, ListenerItem] self.tx_progress = {} # type: Dict[str, ListenerItem]
async def stop(self):
await super().stop()
await self.adb.stop()
def diagnostic_name(self): def diagnostic_name(self):
return "local_tower" return "local_tower"
@ -327,7 +360,7 @@ class WatchTower(LNWatcher):
return keep_watching return keep_watching
async def broadcast_or_log(self, funding_outpoint: str, tx: Transaction): async def broadcast_or_log(self, funding_outpoint: str, tx: Transaction):
height = self.get_tx_height(tx.txid()).height height = self.adb.get_tx_height(tx.txid()).height
if height != TX_HEIGHT_LOCAL: if height != TX_HEIGHT_LOCAL:
return return
try: try:
@ -379,7 +412,7 @@ class LNWalletWatcher(LNWatcher):
def __init__(self, lnworker: 'LNWallet', network: 'Network'): def __init__(self, lnworker: 'LNWallet', network: 'Network'):
self.network = network self.network = network
self.lnworker = lnworker self.lnworker = lnworker
LNWatcher.__init__(self, network) LNWatcher.__init__(self, lnworker.wallet.adb, network)
def diagnostic_name(self): def diagnostic_name(self):
return f"{self.lnworker.wallet.diagnostic_name()}-LNW" return f"{self.lnworker.wallet.diagnostic_name()}-LNW"
@ -412,7 +445,7 @@ class LNWalletWatcher(LNWatcher):
name = sweep_info.name + ' ' + chan.get_id_for_log() name = sweep_info.name + ' ' + chan.get_id_for_log()
spender_txid = spenders.get(prevout) spender_txid = spenders.get(prevout)
if spender_txid is not None: if spender_txid is not None:
spender_tx = self.db.get_transaction(spender_txid) spender_tx = self.adb.get_transaction(spender_txid)
if not spender_tx: if not spender_tx:
keep_watching = True keep_watching = True
continue continue
@ -446,7 +479,7 @@ class LNWalletWatcher(LNWatcher):
broadcast = False broadcast = False
reason = 'waiting for {}: CLTV ({} > {}), prevout {}'.format(name, local_height, sweep_info.cltv_expiry, prevout) reason = 'waiting for {}: CLTV ({} > {}), prevout {}'.format(name, local_height, sweep_info.cltv_expiry, prevout)
if sweep_info.csv_delay: if sweep_info.csv_delay:
prev_height = self.get_tx_height(prev_txid) prev_height = self.adb.get_tx_height(prev_txid)
wanted_height = sweep_info.csv_delay + prev_height.height - 1 wanted_height = sweep_info.csv_delay + prev_height.height - 1
if prev_height.height <= 0 or wanted_height - local_height > 0: if prev_height.height <= 0 or wanted_height - local_height > 0:
broadcast = False broadcast = False
@ -460,24 +493,16 @@ class LNWalletWatcher(LNWatcher):
if broadcast: if broadcast:
await self.network.try_broadcasting(tx, name) await self.network.try_broadcasting(tx, name)
else: else:
if txid in self.lnworker.wallet.future_tx: if txid in self.adb.future_tx:
return return
self.logger.debug(f'(chan {chan_id_for_log}) trying to redeem {name}: {prevout}') self.logger.debug(f'(chan {chan_id_for_log}) trying to redeem {name}: {prevout}')
self.logger.info(reason) self.logger.info(reason)
# it's OK to add local transaction, the fee will be recomputed # it's OK to add local transaction, the fee will be recomputed
try: try:
tx_was_added = self.lnworker.wallet.add_future_tx(tx, wanted_height) tx_was_added = self.adb.add_future_tx(tx, wanted_height)
except Exception as e: except Exception as e:
self.logger.info(f'could not add future tx: {name}. prevout: {prevout} {str(e)}') self.logger.info(f'could not add future tx: {name}. prevout: {prevout} {str(e)}')
tx_was_added = False tx_was_added = False
if tx_was_added: if tx_was_added:
self.logger.info(f'added future tx: {name}. prevout: {prevout}') self.logger.info(f'added future tx: {name}. prevout: {prevout}')
util.trigger_callback('wallet_updated', self.lnworker.wallet) util.trigger_callback('wallet_updated', self.lnworker.wallet)
def add_verified_tx(self, tx_hash: str, info: TxMinedInfo):
# this method is overloaded so that we have the GUI refreshed
# TODO: LNWatcher should not be an AddressSynchronizer,
# we should use the existing wallet instead, and results would be persisted
super().add_verified_tx(tx_hash, info)
tx_mined_status = self.get_tx_height(tx_hash)
util.trigger_callback('verified', self.lnworker.wallet, tx_hash, tx_mined_status)

19
electrum/lnworker.py

@ -734,7 +734,6 @@ class LNWallet(LNWorker):
def start_network(self, network: 'Network'): def start_network(self, network: 'Network'):
super().start_network(network) super().start_network(network)
self.lnwatcher = LNWalletWatcher(self, network) self.lnwatcher = LNWalletWatcher(self, network)
self.lnwatcher.start_network(network)
self.swap_manager.start_network(network=network, lnwatcher=self.lnwatcher) self.swap_manager.start_network(network=network, lnwatcher=self.lnwatcher)
self.lnrater = LNRater(self, network) self.lnrater = LNRater(self, network)
@ -745,7 +744,7 @@ class LNWallet(LNWorker):
for coro in [ for coro in [
self.maybe_listen(), self.maybe_listen(),
self.lnwatcher.on_network_update('network_updated'), # shortcut (don't block) if funding tx locked and verified self.lnwatcher.trigger_callbacks(), # shortcut (don't block) if funding tx locked and verified
self.reestablish_peers_and_channels(), self.reestablish_peers_and_channels(),
self.sync_with_local_watchtower(), self.sync_with_local_watchtower(),
self.sync_with_remote_watchtower(), self.sync_with_remote_watchtower(),
@ -850,7 +849,7 @@ class LNWallet(LNWorker):
return out return out
def get_onchain_history(self): def get_onchain_history(self):
current_height = self.wallet.get_local_height() current_height = self.wallet.adb.get_local_height()
out = {} out = {}
# add funding events # add funding events
for chan in self.channels.values(): for chan in self.channels.values():
@ -860,7 +859,7 @@ class LNWallet(LNWorker):
if not self.lnwatcher: if not self.lnwatcher:
continue # lnwatcher not available with --offline (its data is not persisted) continue # lnwatcher not available with --offline (its data is not persisted)
funding_txid, funding_height, funding_timestamp = item funding_txid, funding_height, funding_timestamp = item
tx_height = self.lnwatcher.get_tx_height(funding_txid) tx_height = self.lnwatcher.adb.get_tx_height(funding_txid)
item = { item = {
'channel_id': bh2u(chan.channel_id), 'channel_id': bh2u(chan.channel_id),
'type': 'channel_opening', 'type': 'channel_opening',
@ -880,7 +879,7 @@ class LNWallet(LNWorker):
if item is None: if item is None:
continue continue
closing_txid, closing_height, closing_timestamp = item closing_txid, closing_height, closing_timestamp = item
tx_height = self.lnwatcher.get_tx_height(closing_txid) tx_height = self.lnwatcher.adb.get_tx_height(closing_txid)
item = { item = {
'channel_id': bh2u(chan.channel_id), 'channel_id': bh2u(chan.channel_id),
'txid': closing_txid, 'txid': closing_txid,
@ -912,7 +911,7 @@ class LNWallet(LNWorker):
label = 'Reverse swap' if swap.is_reverse else 'Forward swap' label = 'Reverse swap' if swap.is_reverse else 'Forward swap'
delta = current_height - swap.locktime delta = current_height - swap.locktime
if self.lnwatcher: if self.lnwatcher:
tx_height = self.lnwatcher.get_tx_height(swap.funding_txid) tx_height = self.lnwatcher.adb.get_tx_height(swap.funding_txid)
if swap.is_reverse and tx_height.height <= 0: if swap.is_reverse and tx_height.height <= 0:
label += ' (%s)' % _('waiting for funding tx confirmation') label += ' (%s)' % _('waiting for funding tx confirmation')
if not swap.is_reverse and not swap.is_redeemed and swap.spending_txid is None and delta < 0: if not swap.is_reverse and not swap.is_redeemed and swap.spending_txid is None and delta < 0:
@ -985,13 +984,13 @@ class LNWallet(LNWorker):
peer = self._peers.get(chan.node_id) peer = self._peers.get(chan.node_id)
if peer: if peer:
await peer.maybe_update_fee(chan) await peer.maybe_update_fee(chan)
conf = self.lnwatcher.get_tx_height(chan.funding_outpoint.txid).conf conf = self.lnwatcher.adb.get_tx_height(chan.funding_outpoint.txid).conf
peer.on_network_update(chan, conf) peer.on_network_update(chan, conf)
elif chan.get_state() == ChannelState.FORCE_CLOSING: elif chan.get_state() == ChannelState.FORCE_CLOSING:
force_close_tx = chan.force_close_tx() force_close_tx = chan.force_close_tx()
txid = force_close_tx.txid() txid = force_close_tx.txid()
height = self.lnwatcher.get_tx_height(txid).height height = self.lnwatcher.adb.get_tx_height(txid).height
if height == TX_HEIGHT_LOCAL: if height == TX_HEIGHT_LOCAL:
self.logger.info('REBROADCASTING CLOSING TX') self.logger.info('REBROADCASTING CLOSING TX')
await self.network.try_broadcasting(force_close_tx, 'force-close') await self.network.try_broadcasting(force_close_tx, 'force-close')
@ -1013,7 +1012,7 @@ class LNWallet(LNWorker):
temp_channel_id=os.urandom(32)) temp_channel_id=os.urandom(32))
chan, funding_tx = await asyncio.wait_for(coro, LN_P2P_NETWORK_TIMEOUT) chan, funding_tx = await asyncio.wait_for(coro, LN_P2P_NETWORK_TIMEOUT)
util.trigger_callback('channels_updated', self.wallet) util.trigger_callback('channels_updated', self.wallet)
self.wallet.add_transaction(funding_tx) # save tx as local into the wallet self.wallet.adb.add_transaction(funding_tx) # save tx as local into the wallet
self.wallet.sign_transaction(funding_tx, password) self.wallet.sign_transaction(funding_tx, password)
self.wallet.set_label(funding_tx.txid(), _('Open channel')) self.wallet.set_label(funding_tx.txid(), _('Open channel'))
if funding_tx.is_complete(): if funding_tx.is_complete():
@ -2311,7 +2310,7 @@ class LNWallet(LNWorker):
chan.set_state(ChannelState.FORCE_CLOSING) chan.set_state(ChannelState.FORCE_CLOSING)
# Add local tx to wallet to also allow manual rebroadcasts. # Add local tx to wallet to also allow manual rebroadcasts.
try: try:
self.wallet.add_transaction(tx) self.wallet.adb.add_transaction(tx)
except UnrelatedTransactionException: except UnrelatedTransactionException:
pass # this can happen if (~all the balance goes to REMOTE) pass # this can happen if (~all the balance goes to REMOTE)
return tx return tx

2
electrum/network.py

@ -348,7 +348,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
if self.config.get('run_watchtower', False): if self.config.get('run_watchtower', False):
from . import lnwatcher from . import lnwatcher
self.local_watchtower = lnwatcher.WatchTower(self) self.local_watchtower = lnwatcher.WatchTower(self)
self.local_watchtower.start_network(self) self.local_watchtower.adb.start_network(self)
asyncio.ensure_future(self.local_watchtower.start_watching()) asyncio.ensure_future(self.local_watchtower.start_watching())
def has_internet_connection(self) -> bool: def has_internet_connection(self) -> bool:

9
electrum/submarine_swaps.py

@ -178,11 +178,11 @@ class SwapManager(Logger):
async def _claim_swap(self, swap: SwapData) -> None: async def _claim_swap(self, swap: SwapData) -> None:
assert self.network assert self.network
assert self.lnwatcher assert self.lnwatcher
if not self.lnwatcher.is_up_to_date(): if not self.lnwatcher.adb.is_up_to_date():
return return
current_height = self.network.get_local_height() current_height = self.network.get_local_height()
delta = current_height - swap.locktime delta = current_height - swap.locktime
txos = self.lnwatcher.get_addr_outputs(swap.lockup_address) txos = self.lnwatcher.adb.get_addr_outputs(swap.lockup_address)
for txin in txos.values(): for txin in txos.values():
if swap.is_reverse and txin.value_sats() < swap.onchain_amount: if swap.is_reverse and txin.value_sats() < swap.onchain_amount:
self.logger.info('amount too low, we should not reveal the preimage') self.logger.info('amount too low, we should not reveal the preimage')
@ -200,7 +200,7 @@ class SwapManager(Logger):
swap.is_redeemed = True swap.is_redeemed = True
elif spent_height == TX_HEIGHT_LOCAL: elif spent_height == TX_HEIGHT_LOCAL:
if txin.block_height > 0 or self.wallet.config.get('allow_instant_swaps', False): if txin.block_height > 0 or self.wallet.config.get('allow_instant_swaps', False):
tx = self.lnwatcher.get_transaction(txin.spent_txid) tx = self.lnwatcher.adb.get_transaction(txin.spent_txid)
self.logger.info(f'broadcasting tx {txin.spent_txid}') self.logger.info(f'broadcasting tx {txin.spent_txid}')
await self.network.broadcast_transaction(tx) await self.network.broadcast_transaction(tx)
# already in mempool # already in mempool
@ -230,8 +230,7 @@ class SwapManager(Logger):
) )
self.sign_tx(tx, swap) self.sign_tx(tx, swap)
self.logger.info(f'adding claim tx {tx.txid()}') self.logger.info(f'adding claim tx {tx.txid()}')
self.wallet.add_transaction(tx) self.wallet.adb.add_transaction(tx)
self.lnwatcher.add_transaction(tx)
def get_claim_fee(self): def get_claim_fee(self):
return self.wallet.config.estimate_fee(136, allow_fallback_to_static_rates=True) return self.wallet.config.estimate_fee(136, allow_fallback_to_static_rates=True)

39
electrum/synchronizer.py

@ -135,9 +135,9 @@ class Synchronizer(SynchronizerBase):
we don't have the full history of, and requests binary transaction we don't have the full history of, and requests binary transaction
data of any transactions the wallet doesn't have. data of any transactions the wallet doesn't have.
''' '''
def __init__(self, wallet: 'AddressSynchronizer'): def __init__(self, adb: 'AddressSynchronizer'):
self.wallet = wallet self.adb = adb
SynchronizerBase.__init__(self, wallet.network) SynchronizerBase.__init__(self, adb.network)
def _reset(self): def _reset(self):
super()._reset() super()._reset()
@ -146,7 +146,7 @@ class Synchronizer(SynchronizerBase):
self._stale_histories = dict() # type: Dict[str, asyncio.Task] self._stale_histories = dict() # type: Dict[str, asyncio.Task]
def diagnostic_name(self): def diagnostic_name(self):
return self.wallet.diagnostic_name() return self.adb.diagnostic_name()
def is_up_to_date(self): def is_up_to_date(self):
return (not self.requested_addrs return (not self.requested_addrs
@ -155,7 +155,7 @@ class Synchronizer(SynchronizerBase):
and not self._stale_histories) and not self._stale_histories)
async def _on_address_status(self, addr, status): async def _on_address_status(self, addr, status):
history = self.wallet.db.get_addr_history(addr) history = self.adb.db.get_addr_history(addr)
if history_status(history) == status: if history_status(history) == status:
return return
# No point in requesting history twice for the same announced status. # No point in requesting history twice for the same announced status.
@ -189,7 +189,7 @@ class Synchronizer(SynchronizerBase):
else: else:
self._stale_histories.pop(addr, asyncio.Future()).cancel() self._stale_histories.pop(addr, asyncio.Future()).cancel()
# Store received history # Store received history
self.wallet.receive_history_callback(addr, hist, tx_fees) self.adb.receive_history_callback(addr, hist, tx_fees)
# Request transactions we don't have # Request transactions we don't have
await self._request_missing_txs(hist) await self._request_missing_txs(hist)
@ -202,7 +202,7 @@ class Synchronizer(SynchronizerBase):
for tx_hash, tx_height in hist: for tx_hash, tx_height in hist:
if tx_hash in self.requested_tx: if tx_hash in self.requested_tx:
continue continue
tx = self.wallet.db.get_transaction(tx_hash) tx = self.adb.db.get_transaction(tx_hash)
if tx and not isinstance(tx, PartialTransaction): if tx and not isinstance(tx, PartialTransaction):
continue # already have complete tx continue # already have complete tx
transaction_hashes.append(tx_hash) transaction_hashes.append(tx_hash)
@ -231,39 +231,32 @@ class Synchronizer(SynchronizerBase):
if tx_hash != tx.txid(): if tx_hash != tx.txid():
raise SynchronizerFailure(f"received tx does not match expected txid ({tx_hash} != {tx.txid()})") raise SynchronizerFailure(f"received tx does not match expected txid ({tx_hash} != {tx.txid()})")
tx_height = self.requested_tx.pop(tx_hash) tx_height = self.requested_tx.pop(tx_hash)
self.wallet.receive_tx_callback(tx_hash, tx, tx_height) self.adb.receive_tx_callback(tx_hash, tx, tx_height)
self.logger.info(f"received tx {tx_hash} height: {tx_height} bytes: {len(raw_tx)}") self.logger.info(f"received tx {tx_hash} height: {tx_height} bytes: {len(raw_tx)}")
# callbacks
util.trigger_callback('new_transaction', self.wallet, tx)
async def main(self): async def main(self):
self.wallet.set_up_to_date(False) self.adb.set_up_to_date(False)
# request missing txns, if any # request missing txns, if any
for addr in random_shuffled_copy(self.wallet.db.get_history()): for addr in random_shuffled_copy(self.adb.db.get_history()):
history = self.wallet.db.get_addr_history(addr) history = self.adb.db.get_addr_history(addr)
# Old electrum servers returned ['*'] when all history for the address # Old electrum servers returned ['*'] when all history for the address
# was pruned. This no longer happens but may remain in old wallets. # was pruned. This no longer happens but may remain in old wallets.
if history == ['*']: continue if history == ['*']: continue
await self._request_missing_txs(history, allow_server_not_finding_tx=True) await self._request_missing_txs(history, allow_server_not_finding_tx=True)
# add addresses to bootstrap # add addresses to bootstrap
for addr in random_shuffled_copy(self.wallet.get_addresses()): for addr in random_shuffled_copy(self.adb.get_addresses()):
await self._add_address(addr) await self._add_address(addr)
# main loop # main loop
while True: while True:
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
# note: we only generate new HD addresses if the existing ones
# have history that are mined and SPV-verified. This inherently couples
# the Sychronizer and the Verifier.
hist_done = self.is_up_to_date() hist_done = self.is_up_to_date()
spv_done = self.wallet.verifier.is_up_to_date() if self.wallet.verifier else True spv_done = self.adb.verifier.is_up_to_date() if self.adb.verifier else True
num_new_addrs = await run_in_thread(self.wallet.synchronize) up_to_date = hist_done and spv_done
up_to_date = hist_done and spv_done and num_new_addrs == 0
# see if status changed # see if status changed
if (up_to_date != self.wallet.is_up_to_date() if (up_to_date != self.adb.is_up_to_date()
or up_to_date and self._processed_some_notifications): or up_to_date and self._processed_some_notifications):
self._processed_some_notifications = False self._processed_some_notifications = False
self.wallet.set_up_to_date(up_to_date) self.adb.set_up_to_date(up_to_date)
util.trigger_callback('wallet_updated', self.wallet)
class Notifier(SynchronizerBase): class Notifier(SynchronizerBase):

8
electrum/tests/test_commands.py

@ -217,7 +217,7 @@ class TestCommandsTestnet(TestCaseForTestnet):
funding_tx = Transaction('0200000000010165806607dd458280cb57bf64a16cf4be85d053145227b98c28932e953076b8e20000000000fdffffff02ac150700000000001600147e3ddfe6232e448a8390f3073c7a3b2044fd17eb102908000000000016001427fbe3707bc57e5bb63d6f15733ec88626d8188a02473044022049ce9efbab88808720aa563e2d9bc40226389ab459c4390ea3e89465665d593502206c1c7c30a2f640af1e463e5107ee4cfc0ee22664cfae3f2606a95303b54cdef80121026269e54d06f7070c1f967eb2874ba60de550dfc327a945c98eb773672d9411fd77181e00') funding_tx = Transaction('0200000000010165806607dd458280cb57bf64a16cf4be85d053145227b98c28932e953076b8e20000000000fdffffff02ac150700000000001600147e3ddfe6232e448a8390f3073c7a3b2044fd17eb102908000000000016001427fbe3707bc57e5bb63d6f15733ec88626d8188a02473044022049ce9efbab88808720aa563e2d9bc40226389ab459c4390ea3e89465665d593502206c1c7c30a2f640af1e463e5107ee4cfc0ee22664cfae3f2606a95303b54cdef80121026269e54d06f7070c1f967eb2874ba60de550dfc327a945c98eb773672d9411fd77181e00')
funding_txid = funding_tx.txid() funding_txid = funding_tx.txid()
self.assertEqual('ede61d39e501d65ccf34e6300da439419c43393f793bb9a8a4b06b2d0d80a8a0', funding_txid) self.assertEqual('ede61d39e501d65ccf34e6300da439419c43393f793bb9a8a4b06b2d0d80a8a0', funding_txid)
wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet.adb.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
cmds = Commands(config=self.config) cmds = Commands(config=self.config)
tx_str = cmds._run( tx_str = cmds._run(
@ -245,7 +245,7 @@ class TestCommandsTestnet(TestCaseForTestnet):
funding_tx = Transaction('02000000000101f59876b1c65bbe3e182ccc7ea7224fe397bb9b70aadcbbf4f4074c75c8a074840000000000fdffffff021f351f00000000001600144eec851dd980cc36af1f629a32325f511604d6af56732d000000000016001439267bc7f3e3fabeae3bc3f73880de22d8b01ba50247304402207eac5f639806a00878488d58ca651d690292145bca5511531845ae21fab309d102207162708bd344840cc1bacff1092e426eb8484f83f5c068ba4ca579813de324540121020e0798c267ff06ee8b838cd465f3cfa6c843a122a04917364ce000c29ca205cae5f31f00') funding_tx = Transaction('02000000000101f59876b1c65bbe3e182ccc7ea7224fe397bb9b70aadcbbf4f4074c75c8a074840000000000fdffffff021f351f00000000001600144eec851dd980cc36af1f629a32325f511604d6af56732d000000000016001439267bc7f3e3fabeae3bc3f73880de22d8b01ba50247304402207eac5f639806a00878488d58ca651d690292145bca5511531845ae21fab309d102207162708bd344840cc1bacff1092e426eb8484f83f5c068ba4ca579813de324540121020e0798c267ff06ee8b838cd465f3cfa6c843a122a04917364ce000c29ca205cae5f31f00')
funding_txid = funding_tx.txid() funding_txid = funding_tx.txid()
self.assertEqual('e8e977bd9c857d84ec1b8f154ae2ee5dfa49fffb7688942a586196c1ad15de15', funding_txid) self.assertEqual('e8e977bd9c857d84ec1b8f154ae2ee5dfa49fffb7688942a586196c1ad15de15', funding_txid)
wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet.adb.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
cmds = Commands(config=self.config) cmds = Commands(config=self.config)
tx_str = cmds._run( tx_str = cmds._run(
@ -283,7 +283,7 @@ class TestCommandsTestnet(TestCaseForTestnet):
funding_txid = funding_tx.txid() funding_txid = funding_tx.txid()
funding_output_value = 1000000 funding_output_value = 1000000
self.assertEqual('add2535aedcbb5ba79cc2260868bb9e57f328738ca192937f2c92e0e94c19203', funding_txid) self.assertEqual('add2535aedcbb5ba79cc2260868bb9e57f328738ca192937f2c92e0e94c19203', funding_txid)
wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet.adb.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
cmds = Commands(config=self.config) cmds = Commands(config=self.config)
@ -301,7 +301,7 @@ class TestCommandsTestnet(TestCaseForTestnet):
funding_tx = Transaction("02000000000102789e8aa8caa79d87241ff9df0e3fd757a07c85a30195d76e8efced1d57c56b670000000000fdffffff7ee2b6abd52b332f797718ae582f8d3b979b83b1799e0a3bfb2c90c6e070c29e0100000000fdffffff020820000000000000160014c0eb720c93a61615d2d66542d381be8943ca553950c3000000000000160014d7dbd0196a2cbd76420f14a19377096cf6cddb75024730440220485b491ad8d3ce3b4da034a851882da84a06ec9800edff0d3fd6aa42eeba3b440220359ea85d32a05932ac417125e133fa54e54e7e9cd20ebc54b883576b8603fd65012103860f1fbf8a482b9d35d7d4d04be8fb33d856a514117cd8b73e372d36895feec60247304402206c2ca56cc030853fa59b4b3cb293f69a3378ead0f10cb76f640f8c2888773461022079b7055d0f6af6952a48e5b97218015b0723462d667765c142b41bd35e3d9c0a01210359e303f57647094a668d69e8ff0bd46c356d00aa7da6dc533c438e71c057f0793e721f00") funding_tx = Transaction("02000000000102789e8aa8caa79d87241ff9df0e3fd757a07c85a30195d76e8efced1d57c56b670000000000fdffffff7ee2b6abd52b332f797718ae582f8d3b979b83b1799e0a3bfb2c90c6e070c29e0100000000fdffffff020820000000000000160014c0eb720c93a61615d2d66542d381be8943ca553950c3000000000000160014d7dbd0196a2cbd76420f14a19377096cf6cddb75024730440220485b491ad8d3ce3b4da034a851882da84a06ec9800edff0d3fd6aa42eeba3b440220359ea85d32a05932ac417125e133fa54e54e7e9cd20ebc54b883576b8603fd65012103860f1fbf8a482b9d35d7d4d04be8fb33d856a514117cd8b73e372d36895feec60247304402206c2ca56cc030853fa59b4b3cb293f69a3378ead0f10cb76f640f8c2888773461022079b7055d0f6af6952a48e5b97218015b0723462d667765c142b41bd35e3d9c0a01210359e303f57647094a668d69e8ff0bd46c356d00aa7da6dc533c438e71c057f0793e721f00")
funding_txid = funding_tx.txid() funding_txid = funding_tx.txid()
wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) wallet.adb.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED)
cmds = Commands(config=self.config) cmds = Commands(config=self.config)
tx = "02000000000101b9723dfc69af058ef6613539a000d2cd098a2c8a74e802b6d8739db708ba8c9a0100000000fdffffff02a00f00000000000016001429e1fd187f0cac845946ae1b11dc136c536bfc0fe8b2000000000000160014100611bcb3aee7aad176936cf4ed56ade03027aa02473044022063c05e2347f16251922830ccc757231247b3c2970c225f988e9204844a1ab7b802204652d2c4816707e3d3bea2609b83b079001a435bad2a99cc2e730f276d07070c012102ee3f00141178006c78b0b458aab21588388335078c655459afe544211f15aee050721f00" tx = "02000000000101b9723dfc69af058ef6613539a000d2cd098a2c8a74e802b6d8739db708ba8c9a0100000000fdffffff02a00f00000000000016001429e1fd187f0cac845946ae1b11dc136c536bfc0fe8b2000000000000160014100611bcb3aee7aad176936cf4ed56ade03027aa02473044022063c05e2347f16251922830ccc757231247b3c2970c225f988e9204844a1ab7b802204652d2c4816707e3d3bea2609b83b079001a435bad2a99cc2e730f276d07070c012102ee3f00141178006c78b0b458aab21588388335078c655459afe544211f15aee050721f00"

8
electrum/tests/test_lnpeer.py

@ -97,8 +97,13 @@ class MockBlockchain:
return False return False
class MockADB:
def add_transaction(self, tx):
pass
class MockWallet: class MockWallet:
receive_requests = {} receive_requests = {}
adb = MockADB()
def set_label(self, x, y): def set_label(self, x, y):
pass pass
@ -106,9 +111,6 @@ class MockWallet:
def save_db(self): def save_db(self):
pass pass
def add_transaction(self, tx):
pass
def is_lightning_backup(self): def is_lightning_backup(self):
return False return False

15
electrum/tests/test_wallet.py

@ -24,7 +24,8 @@ from . import ElectrumTestCase
class FakeSynchronizer(object): class FakeSynchronizer(object):
def __init__(self): def __init__(self, db):
self.db = db
self.store = [] self.store = []
def add(self, address): def add(self, address):
@ -100,18 +101,20 @@ class FakeFxThread:
ccy_amount_str = FxThread.ccy_amount_str ccy_amount_str = FxThread.ccy_amount_str
history_rate = FxThread.history_rate history_rate = FxThread.history_rate
class FakeADB:
def get_tx_height(self, txid):
# because we use a current timestamp, and history is empty,
# FxThread.history_rate will use spot prices
return TxMinedInfo(height=10, conf=10, timestamp=int(time.time()), header_hash='def')
class FakeWallet: class FakeWallet:
def __init__(self, fiat_value): def __init__(self, fiat_value):
super().__init__() super().__init__()
self.fiat_value = fiat_value self.fiat_value = fiat_value
self.db = WalletDB("{}", manual_upgrades=True) self.db = WalletDB("{}", manual_upgrades=True)
self.adb = FakeADB()
self.db.transactions = self.db.verified_tx = {'abc':'Tx'} self.db.transactions = self.db.verified_tx = {'abc':'Tx'}
def get_tx_height(self, txid):
# because we use a current timestamp, and history is empty,
# FxThread.history_rate will use spot prices
return TxMinedInfo(height=10, conf=10, timestamp=int(time.time()), header_hash='def')
default_fiat_value = Abstract_Wallet.default_fiat_value default_fiat_value = Abstract_Wallet.default_fiat_value
price_at_timestamp = Abstract_Wallet.price_at_timestamp price_at_timestamp = Abstract_Wallet.price_at_timestamp
class storage: class storage:

292
electrum/tests/test_wallet_vertical.py

File diff suppressed because one or more lines are too long

214
electrum/wallet.py

@ -45,6 +45,7 @@ from abc import ABC, abstractmethod
import itertools import itertools
import threading import threading
import enum import enum
import asyncio
from aiorpcx import timeout_after, TaskTimeout, ignore_after from aiorpcx import timeout_after, TaskTimeout, ignore_after
@ -260,7 +261,7 @@ class TxWalletDetails(NamedTuple):
is_lightning_funding_tx: bool is_lightning_funding_tx: bool
class Abstract_Wallet(AddressSynchronizer, ABC): class Abstract_Wallet(ABC):
""" """
Wallet classes are created to handle various address generation methods. Wallet classes are created to handle various address generation methods.
Completion states (watching-only, single account, no seed, etc) are handled inside classes. Completion states (watching-only, single account, no seed, etc) are handled inside classes.
@ -285,7 +286,13 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
# load addresses needs to be called before constructor for sanity checks # load addresses needs to be called before constructor for sanity checks
db.load_addresses(self.wallet_type) db.load_addresses(self.wallet_type)
self.keystore = None # type: Optional[KeyStore] # will be set by load_keystore self.keystore = None # type: Optional[KeyStore] # will be set by load_keystore
AddressSynchronizer.__init__(self, db)
self.network = None
self.adb = AddressSynchronizer(db)
for addr in self.get_addresses():
self.adb.add_address(addr)
self.lock = self.adb.lock
self.transaction_lock = self.adb.transaction_lock
# saved fields # saved fields
self.use_change = db.get('use_change', True) self.use_change = db.get('use_change', True)
@ -309,6 +316,24 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self._coin_price_cache = {} self._coin_price_cache = {}
self.lnworker = None self.lnworker = None
self.load_keystore()
self.test_addresses_sanity()
# callbacks
util.register_callback(self.on_adb_set_up_to_date, ['adb_set_up_to_date'])
util.register_callback(self.on_adb_added_tx, ['adb_added_tx'])
util.register_callback(self.on_adb_added_verified_tx, ['adb_added_verified_tx'])
util.register_callback(self.on_adb_removed_verified_tx, ['adb_removed_verified_tx'])
async def main(self):
from aiorpcx import run_in_thread
# calls synchronize
while True:
await asyncio.sleep(0.1)
# note: we only generate new HD addresses if the existing ones
# have history that are mined and SPV-verified. This inherently couples
# the Sychronizer and the Verifier.
num_new_addrs = await run_in_thread(self.synchronize)
up_to_date = self.adb.is_up_to_date() and num_new_addrs == 0
def save_db(self): def save_db(self):
if self.storage: if self.storage:
@ -367,9 +392,13 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
async def stop(self): async def stop(self):
"""Stop all networking and save DB to disk.""" """Stop all networking and save DB to disk."""
util.unregister_callback(self.on_adb_set_up_to_date)
util.unregister_callback(self.on_adb_added_tx)
util.unregister_callback(self.on_adb_added_verified_tx)
util.unregister_callback(self.on_adb_removed_verified_tx)
try: try:
async with ignore_after(5): async with ignore_after(5):
await super().stop() await self.adb.stop()
if self.network: if self.network:
if self.lnworker: if self.lnworker:
await self.lnworker.stop() await self.lnworker.stop()
@ -379,28 +408,56 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self.save_keystore() self.save_keystore()
self.save_db() self.save_db()
def set_up_to_date(self, b): def is_up_to_date(self):
super().set_up_to_date(b) return self.adb.is_up_to_date()
if b: self.save_db()
def on_adb_set_up_to_date(self, event, adb):
if adb != self.adb:
return
if adb.is_up_to_date():
self.save_db()
util.trigger_callback('wallet_updated', self)
util.trigger_callback('status')
def on_adb_added_tx(self, event, adb, tx_hash):
if self.adb != adb:
return
tx = self.db.get_transaction(tx_hash)
if not tx:
raise Exception(tx_hash)
self._maybe_set_tx_label_based_on_invoices(tx)
if self.lnworker:
self.lnworker.maybe_add_backup_from_tx(tx)
self._update_request_statuses_touched_by_tx(tx_hash)
util.trigger_callback('new_transaction', self, tx)
def on_adb_added_verified_tx(self, event, adb, tx_hash):
if adb != self.adb:
return
self._update_request_statuses_touched_by_tx(tx_hash)
tx_mined_status = self.adb.get_tx_height(tx_hash)
util.trigger_callback('verified', self, tx_hash, tx_mined_status)
def on_adb_removed_verified_tx(self, event, adb, tx_hash):
if adb != self.adb:
return
self._update_request_statuses_touched_by_tx(tx_hash)
def clear_history(self): def clear_history(self):
super().clear_history() self.adb.clear_history()
self.save_db() self.save_db()
def start_network(self, network): def start_network(self, network):
AddressSynchronizer.start_network(self, network) self.network = network
if network: if network:
asyncio.run_coroutine_threadsafe(self.main(), self.network.asyncio_loop)
self.adb.start_network(network)
if self.lnworker: if self.lnworker:
self.lnworker.start_network(network) self.lnworker.start_network(network)
# only start gossiping when we already have channels # only start gossiping when we already have channels
if self.db.get('channels'): if self.db.get('channels'):
self.network.start_gossip() self.network.start_gossip()
def load_and_cleanup(self):
self.load_keystore()
self.test_addresses_sanity()
super().load_and_cleanup()
@abstractmethod @abstractmethod
def load_keystore(self) -> None: def load_keystore(self) -> None:
pass pass
@ -450,7 +507,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self._not_old_change_addresses = [addr for addr in self._not_old_change_addresses self._not_old_change_addresses = [addr for addr in self._not_old_change_addresses
if not self.address_is_old(addr)] if not self.address_is_old(addr)]
unused_addrs = [addr for addr in self._not_old_change_addresses unused_addrs = [addr for addr in self._not_old_change_addresses
if not self.is_used(addr) and not self.is_address_reserved(addr)] if not self.adb.is_used(addr) and not self.is_address_reserved(addr)]
return unused_addrs return unused_addrs
def is_deterministic(self) -> bool: def is_deterministic(self) -> bool:
@ -537,6 +594,10 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
return False return False
return self.get_address_index(address)[0] == 1 return self.get_address_index(address)[0] == 1
@abstractmethod
def get_addresses(self) -> Sequence[str]:
pass
@abstractmethod @abstractmethod
def get_address_index(self, address: str) -> Optional[AddressIndexGeneric]: def get_address_index(self, address: str) -> Optional[AddressIndexGeneric]:
pass pass
@ -595,7 +656,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
return bool(self.lnworker.swap_manager.get_swap_by_tx(tx)) if self.lnworker else False return bool(self.lnworker.swap_manager.get_swap_by_tx(tx)) if self.lnworker else False
def get_tx_info(self, tx: Transaction) -> TxWalletDetails: def get_tx_info(self, tx: Transaction) -> TxWalletDetails:
tx_wallet_delta = self.get_wallet_delta(tx) tx_wallet_delta = self.adb.get_wallet_delta(tx)
is_relevant = tx_wallet_delta.is_relevant is_relevant = tx_wallet_delta.is_relevant
is_any_input_ismine = tx_wallet_delta.is_any_input_ismine is_any_input_ismine = tx_wallet_delta.is_any_input_ismine
is_swap = self.is_swap_tx(tx) is_swap = self.is_swap_tx(tx)
@ -606,11 +667,11 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
can_cpfp = False can_cpfp = False
tx_hash = tx.txid() # note: txid can be None! e.g. when called from GUI tx dialog tx_hash = tx.txid() # note: txid can be None! e.g. when called from GUI tx dialog
is_lightning_funding_tx = self.is_lightning_funding_tx(tx_hash) is_lightning_funding_tx = self.is_lightning_funding_tx(tx_hash)
tx_we_already_have_in_db = self.db.get_transaction(tx_hash) tx_we_already_have_in_db = self.adb.db.get_transaction(tx_hash)
can_save_as_local = (is_relevant and tx.txid() is not None can_save_as_local = (is_relevant and tx.txid() is not None
and (tx_we_already_have_in_db is None or not tx_we_already_have_in_db.is_complete())) and (tx_we_already_have_in_db is None or not tx_we_already_have_in_db.is_complete()))
label = '' label = ''
tx_mined_status = self.get_tx_height(tx_hash) tx_mined_status = self.adb.get_tx_height(tx_hash)
can_remove = ((tx_mined_status.height in [TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL]) can_remove = ((tx_mined_status.height in [TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL])
# otherwise 'height' is unreliable (typically LOCAL): # otherwise 'height' is unreliable (typically LOCAL):
and is_relevant and is_relevant
@ -628,7 +689,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
elif tx_mined_status.height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED): elif tx_mined_status.height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED):
status = _('Unconfirmed') status = _('Unconfirmed')
if fee is None: if fee is None:
fee = self.get_tx_fee(tx_hash) fee = self.adb.get_tx_fee(tx_hash)
if fee and self.network and self.config.has_fee_mempool(): if fee and self.network and self.config.has_fee_mempool():
size = tx.estimated_size() size = tx.estimated_size()
fee_per_byte = fee / size fee_per_byte = fee / size
@ -682,11 +743,22 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
is_lightning_funding_tx=is_lightning_funding_tx, is_lightning_funding_tx=is_lightning_funding_tx,
) )
def get_balance(self, **kwargs):
domain = self.get_addresses()
return self.adb.get_balance(domain, **kwargs)
def get_addr_balance(self, address):
return self.adb.get_balance([address])
def get_utxos(self, **kwargs):
domain = self.get_addresses()
return self.adb.get_utxos(domain=domain, **kwargs)
def get_spendable_coins(self, domain, *, nonlocal_only=False) -> Sequence[PartialTxInput]: def get_spendable_coins(self, domain, *, nonlocal_only=False) -> Sequence[PartialTxInput]:
confirmed_only = self.config.get('confirmed_only', False) confirmed_only = self.config.get('confirmed_only', False)
with self._freeze_lock: with self._freeze_lock:
frozen_addresses = self._frozen_addresses.copy() frozen_addresses = self._frozen_addresses.copy()
utxos = self.get_utxos(domain, utxos = self.get_utxos(
excluded_addresses=frozen_addresses, excluded_addresses=frozen_addresses,
mature_only=True, mature_only=True,
confirmed_funding_only=confirmed_only, confirmed_funding_only=confirmed_only,
@ -714,7 +786,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
frozen_coins = {utxo.prevout.to_str() for utxo in self.get_utxos() frozen_coins = {utxo.prevout.to_str() for utxo in self.get_utxos()
if self.is_frozen_coin(utxo)} if self.is_frozen_coin(utxo)}
if not frozen_coins: # shortcut if not frozen_coins: # shortcut
return self.get_balance(frozen_addresses) return self.adb.get_balance(frozen_addresses)
c1, u1, x1 = self.get_balance() c1, u1, x1 = self.get_balance()
c2, u2, x2 = self.get_balance( c2, u2, x2 = self.get_balance(
excluded_addresses=frozen_addresses, excluded_addresses=frozen_addresses,
@ -739,7 +811,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
def balance_at_timestamp(self, domain, target_timestamp): def balance_at_timestamp(self, domain, target_timestamp):
# we assume that get_history returns items ordered by block height # we assume that get_history returns items ordered by block height
# we also assume that block timestamps are monotonic (which is false...!) # we also assume that block timestamps are monotonic (which is false...!)
h = self.get_history(domain=domain) h = self.adb.get_history(domain=domain)
balance = 0 balance = 0
for hist_item in h: for hist_item in h:
balance = hist_item.balance balance = hist_item.balance
@ -749,8 +821,10 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
return balance return balance
def get_onchain_history(self, *, domain=None): def get_onchain_history(self, *, domain=None):
if domain is None:
domain = self.get_addresses()
monotonic_timestamp = 0 monotonic_timestamp = 0
for hist_item in self.get_history(domain=domain): for hist_item in self.adb.get_history(domain=domain):
monotonic_timestamp = max(monotonic_timestamp, (hist_item.tx_mined_status.timestamp or 999_999_999_999)) monotonic_timestamp = max(monotonic_timestamp, (hist_item.tx_mined_status.timestamp or 999_999_999_999))
yield { yield {
'txid': hist_item.txid, 'txid': hist_item.txid,
@ -768,7 +842,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
} }
def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> Invoice: def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> Invoice:
height = self.get_local_height() height = self.adb.get_local_height()
if pr: if pr:
return Invoice.from_bip70_payreq(pr, height=height) return Invoice.from_bip70_payreq(pr, height=height)
amount_msat = 0 amount_msat = 0
@ -801,7 +875,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
key = self.get_key_for_outgoing_invoice(invoice) key = self.get_key_for_outgoing_invoice(invoice)
if not invoice.is_lightning(): if not invoice.is_lightning():
if self.is_onchain_invoice_paid(invoice, 0): if self.is_onchain_invoice_paid(invoice, 0):
self.logger.info("saving invoice... but it is already paid!") _logger.info("saving invoice... but it is already paid!")
with self.transaction_lock: with self.transaction_lock:
for txout in invoice.get_outputs(): for txout in invoice.get_outputs():
self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(key) self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(key)
@ -888,7 +962,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
prevouts_and_values = self.db.get_prevouts_by_scripthash(scripthash) prevouts_and_values = self.db.get_prevouts_by_scripthash(scripthash)
total_received = 0 total_received = 0
for prevout, v in prevouts_and_values: for prevout, v in prevouts_and_values:
tx_height = self.get_tx_height(prevout.txid.hex()) tx_height = self.adb.get_tx_height(prevout.txid.hex())
if tx_height.height > 0 and tx_height.height <= invoice.height: if tx_height.height > 0 and tx_height.height <= invoice.height:
continue continue
if tx_height.conf < conf: if tx_height.conf < conf:
@ -917,14 +991,15 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self.set_label(tx_hash, "; ".join(labels)) self.set_label(tx_hash, "; ".join(labels))
return bool(labels) return bool(labels)
def add_transaction(self, tx, *, allow_unrelated=False): # fixme: this needs a callback
is_known = bool(self.db.get_transaction(tx.txid())) #def add_transaction(self, tx, *, allow_unrelated=False):
tx_was_added = super().add_transaction(tx, allow_unrelated=allow_unrelated) # is_known = bool(self.db.get_transaction(tx.txid()))
if tx_was_added and not is_known: # tx_was_added = self.adb.add_transaction(tx, allow_unrelated=allow_unrelated)
self._maybe_set_tx_label_based_on_invoices(tx) # if tx_was_added and not is_known:
if self.lnworker: # self._maybe_set_tx_label_based_on_invoices(tx)
self.lnworker.maybe_add_backup_from_tx(tx) # if self.lnworker:
return tx_was_added # self.lnworker.maybe_add_backup_from_tx(tx)
# return tx_was_added
@profiler @profiler
def get_full_history(self, fx=None, *, onchain_domain=None, include_lightning=True): def get_full_history(self, fx=None, *, onchain_domain=None, include_lightning=True):
@ -1076,13 +1151,11 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
end_timestamp = last_item['timestamp'] end_timestamp = last_item['timestamp']
start_coins = self.get_utxos( start_coins = self.get_utxos(
domain=None,
block_height=start_height, block_height=start_height,
confirmed_funding_only=True, confirmed_funding_only=True,
confirmed_spending_only=True, confirmed_spending_only=True,
nonlocal_only=True) nonlocal_only=True)
end_coins = self.get_utxos( end_coins = self.get_utxos(
domain=None,
block_height=end_height, block_height=end_height,
confirmed_funding_only=True, confirmed_funding_only=True,
confirmed_spending_only=True, confirmed_spending_only=True,
@ -1130,7 +1203,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
} }
def acquisition_price(self, coins, price_func, ccy): def acquisition_price(self, coins, price_func, ccy):
return Decimal(sum(self.coin_price(coin.prevout.txid.hex(), price_func, ccy, self.get_txin_value(coin)) for coin in coins)) return Decimal(sum(self.coin_price(coin.prevout.txid.hex(), price_func, ccy, self.adb.get_txin_value(coin)) for coin in coins))
def liquidation_price(self, coins, price_func, timestamp): def liquidation_price(self, coins, price_func, timestamp):
p = price_func(timestamp) p = price_func(timestamp)
@ -1204,7 +1277,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
is_final = tx and tx.is_final() is_final = tx and tx.is_final()
if not is_final: if not is_final:
extra.append('rbf') extra.append('rbf')
fee = self.get_tx_fee(tx_hash) fee = self.adb.get_tx_fee(tx_hash)
if fee is not None: if fee is not None:
size = tx.estimated_size() size = tx.estimated_size()
fee_per_byte = fee / size fee_per_byte = fee / size
@ -1238,7 +1311,8 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
def get_unconfirmed_base_tx_for_batching(self) -> Optional[Transaction]: def get_unconfirmed_base_tx_for_batching(self) -> Optional[Transaction]:
candidate = None candidate = None
for hist_item in self.get_history(): domain = self.get_addresses()
for hist_item in self.adb.get_history(domain):
# tx should not be mined yet # tx should not be mined yet
if hist_item.tx_mined_status.conf > 0: continue if hist_item.tx_mined_status.conf > 0: continue
# conservative future proofing of code: only allow known unconfirmed types # conservative future proofing of code: only allow known unconfirmed types
@ -1259,7 +1333,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
for output_idx, o in enumerate(tx.outputs())]): for output_idx, o in enumerate(tx.outputs())]):
continue continue
# all inputs should be is_mine # all inputs should be is_mine
if not all([self.is_mine(self.get_txin_address(txin)) for txin in tx.inputs()]): if not all([self.is_mine(self.adb.get_txin_address(txin)) for txin in tx.inputs()]):
continue continue
# do not mutate LN funding txs, as that would change their txid # do not mutate LN funding txs, as that would change their txid
if self.is_lightning_funding_tx(txid): if self.is_lightning_funding_tx(txid):
@ -1392,7 +1466,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
if self.config.get('batch_rbf', False) and base_tx: if self.config.get('batch_rbf', False) and base_tx:
# make sure we don't try to spend change from the tx-to-be-replaced: # make sure we don't try to spend change from the tx-to-be-replaced:
coins = [c for c in coins if c.prevout.txid.hex() != base_tx.txid()] coins = [c for c in coins if c.prevout.txid.hex() != base_tx.txid()]
is_local = self.get_tx_height(base_tx.txid()).height == TX_HEIGHT_LOCAL is_local = self.adb.get_tx_height(base_tx.txid()).height == TX_HEIGHT_LOCAL
base_tx = PartialTransaction.from_tx(base_tx) base_tx = PartialTransaction.from_tx(base_tx)
base_tx.add_info_from_wallet(self) base_tx.add_info_from_wallet(self)
base_tx_fee = base_tx.get_fee() base_tx_fee = base_tx.get_fee()
@ -1512,7 +1586,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
# we should typically have the funding tx available; # we should typically have the funding tx available;
# might not have it e.g. while not up_to_date # might not have it e.g. while not up_to_date
return True return True
if any(self.is_mine(self.get_txin_address(txin)) if any(self.is_mine(self.adb.get_txin_address(txin))
for txin in funding_tx.inputs()): for txin in funding_tx.inputs()):
return False return False
return True return True
@ -1565,12 +1639,12 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
needs_spv_check = not self.config.get("skipmerklecheck", False) needs_spv_check = not self.config.get("skipmerklecheck", False)
for tx_hash, tx_height in h: for tx_hash, tx_height in h:
if needs_spv_check: if needs_spv_check:
tx_age = self.get_tx_height(tx_hash).conf tx_age = self.adb.get_tx_height(tx_hash).conf
else: else:
if tx_height <= 0: if tx_height <= 0:
tx_age = 0 tx_age = 0
else: else:
tx_age = self.get_local_height() - tx_height + 1 tx_age = self.adb.get_local_height() - tx_height + 1
max_conf = max(max_conf, tx_age) max_conf = max(max_conf, tx_age)
return max_conf >= req_conf return max_conf >= req_conf
@ -1835,7 +1909,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
break break
else: else:
raise CannotCPFP(_("Could not find suitable output")) raise CannotCPFP(_("Could not find suitable output"))
coins = self.get_addr_utxo(address) coins = self.adb.get_addr_utxo(address)
item = coins.get(TxOutpoint.from_str(txid+':%d'%i)) item = coins.get(TxOutpoint.from_str(txid+':%d'%i))
if not item: if not item:
raise CannotCPFP(_("Could not find coins for output")) raise CannotCPFP(_("Could not find coins for output"))
@ -1881,7 +1955,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
raise CannotDoubleSpendTx(_("The new fee rate needs to be higher than the old fee rate.")) raise CannotDoubleSpendTx(_("The new fee rate needs to be higher than the old fee rate."))
# grab all ismine inputs # grab all ismine inputs
inputs = [txin for txin in tx.inputs() inputs = [txin for txin in tx.inputs()
if self.is_mine(self.get_txin_address(txin))] if self.is_mine(self.adb.get_txin_address(txin))]
value = sum([txin.value_sats() for txin in inputs]) value = sum([txin.value_sats() for txin in inputs])
# figure out output address # figure out output address
old_change_addrs = [o.address for o in tx.outputs() if self.is_mine(o.address)] old_change_addrs = [o.address for o in tx.outputs() if self.is_mine(o.address)]
@ -1925,7 +1999,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
# in which case we might include a WITNESS_UTXO. # in which case we might include a WITNESS_UTXO.
address = address or txin.address address = address or txin.address
if txin.witness_utxo is None and txin.is_segwit() and address: if txin.witness_utxo is None and txin.is_segwit() and address:
received, spent = self.get_addr_io(address) received, spent = self.adb.get_addr_io(address)
item = received.get(txin.prevout.to_str()) item = received.get(txin.prevout.to_str())
if item: if item:
txin_value = item[1] txin_value = item[1]
@ -1949,7 +2023,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
only_der_suffix: bool = False, only_der_suffix: bool = False,
ignore_network_issues: bool = True, ignore_network_issues: bool = True,
) -> None: ) -> None:
address = self.get_txin_address(txin) address = self.adb.get_txin_address(txin)
# note: we add input utxos regardless of is_mine # note: we add input utxos regardless of is_mine
self._add_input_utxo_info(txin, ignore_network_issues=ignore_network_issues, address=address) self._add_input_utxo_info(txin, ignore_network_issues=ignore_network_issues, address=address)
is_mine = self.is_mine(address) is_mine = self.is_mine(address)
@ -2006,8 +2080,8 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
raw_tx = self.network.run_from_another_thread( raw_tx = self.network.run_from_another_thread(
self.network.get_transaction(tx_hash, timeout=10)) self.network.get_transaction(tx_hash, timeout=10))
except NetworkException as e: except NetworkException as e:
self.logger.info(f'got network error getting input txn. err: {repr(e)}. txid: {tx_hash}. ' _logger.info(f'got network error getting input txn. err: {repr(e)}. txid: {tx_hash}. '
f'if you are intentionally offline, consider using the --offline flag') f'if you are intentionally offline, consider using the --offline flag')
if not ignore_network_issues: if not ignore_network_issues:
raise e raise e
else: else:
@ -2082,7 +2156,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
# TODO we should index receive_requests by id # TODO we should index receive_requests by id
# add lightning requests. (use as key) # add lightning requests. (use as key)
in_use_by_request = set(self.receive_requests.keys()) in_use_by_request = set(self.receive_requests.keys())
return [addr for addr in domain if not self.is_used(addr) return [addr for addr in domain if not self.adb.is_used(addr)
and addr not in in_use_by_request] and addr not in in_use_by_request]
@check_returned_address_for_corruption @check_returned_address_for_corruption
@ -2105,7 +2179,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
raise Exception("no receiving addresses in wallet?!") raise Exception("no receiving addresses in wallet?!")
choice = domain[0] choice = domain[0]
for addr in domain: for addr in domain:
if not self.is_used(addr): if not self.adb.is_used(addr):
if addr not in self.receive_requests.keys(): if addr not in self.receive_requests.keys():
return addr return addr
else: else:
@ -2128,12 +2202,12 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
def get_onchain_request_status(self, r: Invoice) -> Tuple[bool, Optional[int]]: def get_onchain_request_status(self, r: Invoice) -> Tuple[bool, Optional[int]]:
address = r.get_address() address = r.get_address()
amount = int(r.get_amount_sat() or 0) amount = int(r.get_amount_sat() or 0)
received, sent = self.get_addr_io(address) received, sent = self.adb.get_addr_io(address)
l = [] l = []
for txo, x in received.items(): for txo, x in received.items():
h, v, is_cb = x h, v, is_cb = x
txid, n = txo.split(':') txid, n = txo.split(':')
tx_height = self.get_tx_height(txid) tx_height = self.adb.get_tx_height(txid)
height = tx_height.height height = tx_height.height
if height > 0 and height <= r.height: if height > 0 and height <= r.height:
continue continue
@ -2276,19 +2350,6 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
d['bip70'] = x.bip70 d['bip70'] = x.bip70
return d return d
def receive_tx_callback(self, tx_hash, tx, tx_height):
super().receive_tx_callback(tx_hash, tx, tx_height)
self._update_request_statuses_touched_by_tx(tx_hash)
def add_verified_tx(self, tx_hash, info):
super().add_verified_tx(tx_hash, info)
self._update_request_statuses_touched_by_tx(tx_hash)
def undo_verifications(self, blockchain, above_height):
reorged_txids = super().undo_verifications(blockchain, above_height)
for txid in reorged_txids:
self._update_request_statuses_touched_by_tx(txid)
def _update_request_statuses_touched_by_tx(self, tx_hash: str) -> None: def _update_request_statuses_touched_by_tx(self, tx_hash: str) -> None:
# FIXME in some cases if tx2 replaces unconfirmed tx1 in the mempool, we are not called. # FIXME in some cases if tx2 replaces unconfirmed tx1 in the mempool, we are not called.
# For a given receive request, if tx1 touches it but tx2 does not, then # For a given receive request, if tx1 touches it but tx2 does not, then
@ -2310,7 +2371,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
fallback_address = address if self.config.get('bolt11_fallback', True) else None fallback_address = address if self.config.get('bolt11_fallback', True) else None
lightning_invoice = self.lnworker.add_request(amount_sat, message, exp_delay, fallback_address) if lightning else None lightning_invoice = self.lnworker.add_request(amount_sat, message, exp_delay, fallback_address) if lightning else None
outputs = [ PartialTxOutput.from_address_and_value(address, amount_sat)] if address else [] outputs = [ PartialTxOutput.from_address_and_value(address, amount_sat)] if address else []
height = self.get_local_height() height = self.adb.get_local_height()
req = Invoice( req = Invoice(
outputs=outputs, outputs=outputs,
message=message, message=message,
@ -2502,7 +2563,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
def price_at_timestamp(self, txid, price_func): def price_at_timestamp(self, txid, price_func):
"""Returns fiat price of bitcoin at the time tx got confirmed.""" """Returns fiat price of bitcoin at the time tx got confirmed."""
timestamp = self.get_tx_height(txid).timestamp timestamp = self.adb.get_tx_height(txid).timestamp
return price_func(timestamp if timestamp else time.time()) return price_func(timestamp if timestamp else time.time())
def average_price(self, txid, price_func, ccy) -> Decimal: def average_price(self, txid, price_func, ccy) -> Decimal:
@ -2663,6 +2724,9 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
else: else:
return allow_send, long_warning, short_warning return allow_send, long_warning, short_warning
def synchronize(self) -> int:
"""Returns the number of new addresses we generated."""
return 0
class Simple_Wallet(Abstract_Wallet): class Simple_Wallet(Abstract_Wallet):
# wallet with a single keystore # wallet with a single keystore
@ -2766,7 +2830,7 @@ class Imported_Wallet(Simple_Wallet):
continue continue
good_addr.append(address) good_addr.append(address)
self.db.add_imported_address(address, {}) self.db.add_imported_address(address, {})
self.add_address(address) self.adb.add_address(address)
if write_to_disk: if write_to_disk:
self.save_db() self.save_db()
return good_addr, bad_addr return good_addr, bad_addr
@ -2787,7 +2851,7 @@ class Imported_Wallet(Simple_Wallet):
transactions_new = set() # txs that are not only referred to by address transactions_new = set() # txs that are not only referred to by address
with self.lock: with self.lock:
for addr in self.db.get_history(): for addr in self.db.get_history():
details = self.get_address_history(addr) details = self.adb.get_address_history(addr)
if addr == address: if addr == address:
for tx_hash, height in details: for tx_hash, height in details:
transactions_to_remove.add(tx_hash) transactions_to_remove.add(tx_hash)
@ -2797,7 +2861,7 @@ class Imported_Wallet(Simple_Wallet):
transactions_to_remove -= transactions_new transactions_to_remove -= transactions_new
self.db.remove_addr_history(address) self.db.remove_addr_history(address)
for tx_hash in transactions_to_remove: for tx_hash in transactions_to_remove:
self._remove_transaction(tx_hash) self.adb._remove_transaction(tx_hash)
self.set_label(address, None) self.set_label(address, None)
self.remove_payment_request(address) self.remove_payment_request(address)
self.set_frozen_state_of_addresses([address], False) self.set_frozen_state_of_addresses([address], False)
@ -2831,7 +2895,7 @@ class Imported_Wallet(Simple_Wallet):
def calc_unused_change_addresses(self) -> Sequence[str]: def calc_unused_change_addresses(self) -> Sequence[str]:
with self.lock: with self.lock:
unused_addrs = [addr for addr in self.get_change_addresses() unused_addrs = [addr for addr in self.get_change_addresses()
if not self.is_used(addr) and not self.is_address_reserved(addr)] if not self.adb.is_used(addr) and not self.is_address_reserved(addr)]
return unused_addrs return unused_addrs
def is_mine(self, address) -> bool: def is_mine(self, address) -> bool:
@ -2865,7 +2929,7 @@ class Imported_Wallet(Simple_Wallet):
addr = bitcoin.pubkey_to_address(txin_type, pubkey) addr = bitcoin.pubkey_to_address(txin_type, pubkey)
good_addr.append(addr) good_addr.append(addr)
self.db.add_imported_address(addr, {'type':txin_type, 'pubkey':pubkey}) self.db.add_imported_address(addr, {'type':txin_type, 'pubkey':pubkey})
self.add_address(addr) self.adb.add_address(addr)
self.save_keystore() self.save_keystore()
if write_to_disk: if write_to_disk:
self.save_db() self.save_db()
@ -3055,7 +3119,7 @@ class Deterministic_Wallet(Abstract_Wallet):
n = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses() n = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses()
address = self.derive_address(int(for_change), n) address = self.derive_address(int(for_change), n)
self.db.add_change_address(address) if for_change else self.db.add_receiving_address(address) self.db.add_change_address(address) if for_change else self.db.add_receiving_address(address)
self.add_address(address) self.adb.add_address(address)
if for_change: if for_change:
# note: if it's actually "old", it will get filtered later # note: if it's actually "old", it will get filtered later
self._not_old_change_addresses.append(address) self._not_old_change_addresses.append(address)
@ -3081,7 +3145,7 @@ class Deterministic_Wallet(Abstract_Wallet):
break break
return count return count
@AddressSynchronizer.with_local_height_cached #@AddressSynchronizer.with_local_height_cached FIXME
def synchronize(self): def synchronize(self):
count = 0 count = 0
with self.lock: with self.lock:

Loading…
Cancel
Save