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
patch-4
ThomasV 3 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):
"""
inherited by wallet
"""
""" address database """
network: Optional['Network']
asyncio_loop: Optional['asyncio.AbstractEventLoop'] = None
@ -207,7 +205,7 @@ class AddressSynchronizer(Logger):
self.db.put('stored_height', self.get_local_height())
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.set_up_to_date(False)
if self.synchronizer:
@ -341,6 +339,7 @@ class AddressSynchronizer(Logger):
# save
self.db.add_transaction(tx_hash, tx)
self.db.add_num_inputs_to_tx(tx_hash, len(tx.inputs()))
util.trigger_callback('adb_added_tx', self, tx_hash)
return True
def remove_transaction(self, tx_hash: str) -> None:
@ -504,10 +503,7 @@ class AddressSynchronizer(Logger):
@with_lock
@with_transaction_lock
@with_local_height_cached
def get_history(self, *, domain=None) -> Sequence[HistoryItem]:
# get domain
if domain is None:
domain = self.get_addresses()
def get_history(self, domain) -> Sequence[HistoryItem]:
domain = set(domain)
# 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
@ -536,7 +532,7 @@ class AddressSynchronizer(Logger):
fee=fee,
balance=balance))
# sanity check
c, u, x = self.get_balance(domain=domain)
c, u, x = self.get_balance(domain)
if balance != c + u + x:
raise Exception("wallet.get_history() failed balance sanity-check")
return h2
@ -607,8 +603,7 @@ class AddressSynchronizer(Logger):
with self.lock:
self.unverified_tx.pop(tx_hash, None)
self.db.add_verified_tx(tx_hash, info)
tx_mined_status = self.get_tx_height(tx_hash)
util.trigger_callback('verified', self, tx_hash, tx_mined_status)
util.trigger_callback('adb_added_verified_tx', self, tx_hash)
def get_unverified_txs(self) -> Dict[str, int]:
'''Returns a map from tx hash to transaction height'''
@ -637,6 +632,9 @@ class AddressSynchronizer(Logger):
# a status update, that will overwrite it.
self.unverified_tx[tx_hash] = tx_height
txs.add(tx_hash)
for tx_hash in txs:
util.trigger_callback('adb_removed_verified_tx', self, tx_hash)
return txs
def get_local_height(self) -> int:
@ -688,7 +686,7 @@ class AddressSynchronizer(Logger):
if self.verifier:
self.verifier.reset_request_counters()
# fire triggers
util.trigger_callback('status')
util.trigger_callback('adb_set_up_to_date', self)
if status_changed:
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)
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
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]:
"""Return the balance of a set of addresses:
confirmed and matured, unconfirmed, unmatured
"""
if domain is None:
domain = self.get_addresses()
if excluded_addresses is None:
excluded_addresses = set()
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
def get_utxos(
self,
domain=None,
domain,
*,
excluded_addresses=None,
mature_only: bool = False,
@ -926,8 +919,6 @@ class AddressSynchronizer(Logger):
else:
block_height = self.get_local_height()
coins = []
if domain is None:
domain = self.get_addresses()
domain = set(domain)
if excluded_addresses:
domain = set(domain) - set(excluded_addresses)
@ -957,7 +948,3 @@ class AddressSynchronizer(Logger):
def is_empty(self, address: str) -> bool:
coins = self.get_addr_utxo(address)
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
if change and not wallet.is_change(addr):
continue
if unused and wallet.is_used(addr):
if unused and wallet.adb.is_used(addr):
continue
if funded and wallet.is_empty(addr):
if funded and wallet.adb.is_empty(addr):
continue
item = addr
if labels or balance:
@ -965,7 +965,7 @@ class Commands:
async def addtransaction(self, tx, wallet: Abstract_Wallet = None):
""" Add a transaction to the wallet history """
tx = Transaction(tx)
if not wallet.add_transaction(tx):
if not wallet.adb.add_transaction(tx):
return False
wallet.save_db()
return tx.txid()
@ -1040,11 +1040,11 @@ class Commands:
"""
if not is_hash256_str(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:
raise Exception(f'Only local transactions can be removed. '
f'This tx has height: {height} != {TX_HEIGHT_LOCAL}')
wallet.remove_transaction(txid)
wallet.adb.remove_transaction(txid)
wallet.save_db()
@command('wn')
@ -1057,7 +1057,7 @@ class Commands:
if not wallet.db.get_transaction(txid):
raise Exception("Transaction not in wallet.")
return {
"confirmations": wallet.get_tx_height(txid).conf,
"confirmations": wallet.adb.get_tx_height(txid).conf,
}
@command('')

4
electrum/gui/kivy/main_window.py

@ -957,7 +957,7 @@ class ElectrumWindow(App, Logger):
server_height = self.network.get_server_height()
server_lag = self.num_blocks - server_height
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]"
.format(_("Synchronizing..."), num_answered, num_sent))
elif server_lag > 1:
@ -1164,7 +1164,7 @@ class ElectrumWindow(App, Logger):
def show_transaction(self, txid):
tx = self.wallet.db.get_transaction(txid)
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:
self.tx_dialog(tx)
else:

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

@ -264,7 +264,7 @@ class AddressesDialog(Factory.Popup):
for address in _list:
label = wallet.get_label(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):
continue
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()
if not address:
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')
self.warning = warning

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

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

4
electrum/gui/qt/address_list.py

@ -167,7 +167,7 @@ class AddressList(MyTreeView):
for address in addr_list:
c, u, x = self.wallet.get_addr_balance(address)
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):
continue
if self.show_used == AddressUsageStateFilter.FUNDED and balance == 0:
@ -219,7 +219,7 @@ class AddressList(MyTreeView):
def refresh_row(self, key, row):
address = key
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)
balance = c + u + x
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()
if 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:
menu.addAction(_("View closing transaction"), lambda: self.parent.show_transaction(closing_tx))
menu.addSeparator()

6
electrum/gui/qt/history_list.py

@ -717,7 +717,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
return
tx_hash = tx_item['txid']
if tx_item.get('lightning'):
tx = self.wallet.lnworker.lnwatcher.db.get_transaction(tx_hash)
tx = self.wallet.adb.get_transaction(tx_hash)
else:
tx = self.wallet.db.get_transaction(tx_hash)
if not tx:
@ -760,7 +760,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
menu.exec_(self.viewport().mapToGlobal(position))
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?")
if num_child_txs > 0:
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,
title=_("Please confirm")):
return
self.wallet.remove_transaction(tx_hash)
self.wallet.adb.remove_transaction(tx_hash)
self.wallet.save_db()
# need to update at least: history_list, utxo_list, address_list
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.
# Display the synchronizing message in that case.
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 = ("{} ({}/{})"
.format(_("Synchronizing..."), num_answered, num_sent))
icon = read_QIcon("status_waiting.png")
@ -1510,7 +1510,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def update_receive_address_styling(self):
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.setToolTip(_("This address has already been used. "
"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()
parent_txid = parent_tx.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:
self.show_error(_("Can't CPFP: unknown fee for parent transaction."))
return
@ -3699,7 +3699,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def save_transaction_into_wallet(self, tx: Transaction):
win = self.top_level_window()
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" +
_("It conflicts with current history."))
return False

6
electrum/gui/qt/transaction_dialog.py

@ -449,7 +449,7 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
item = lnworker_history[txid]
ln_amount = item['amount_msat'] / 1000
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:
ln_amount = None
self.broadcast_button.setEnabled(tx_details.can_broadcast)
@ -618,11 +618,11 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
prevout_hash = txin.prevout.txid.hex()
prevout_n = txin.prevout.out_idx
cursor.insertText(prevout_hash + ":%-4d " % prevout_n, ext)
addr = self.wallet.get_txin_address(txin)
addr = self.wallet.adb.get_txin_address(txin)
if addr is None:
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:
cursor.insertText(format_amount(txin_value), ext)
cursor.insertBlock()

7
electrum/lnchannel.py

@ -281,7 +281,7 @@ class AbstractChannel(Logger, ABC):
if spender_txid is None:
continue
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:
self.logger.info(f'channel is double spent {inputs}')
self.set_state(ChannelState.REDEEMED)
@ -486,7 +486,8 @@ class ChannelBackup(AbstractChannel):
def get_capacity(self):
lnwatcher = self.lnworker.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
def is_backup(self):
@ -1549,7 +1550,7 @@ class Channel(AbstractChannel):
return False
assert conf > 0
# 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:
self.logger.info(f"no funding_tx {funding_txid}")
return False

2
electrum/lnpeer.py

@ -2175,7 +2175,7 @@ class Peer(Logger):
sig=bh2u(der_sig_from_sig_string(their_sig) + b'\x01'))
# save local transaction and set state
try:
self.lnworker.wallet.add_transaction(closing_tx)
self.lnworker.wallet.adb.add_transaction(closing_tx)
except UnrelatedTransactionException:
pass # this can happen if (~all the balance goes to REMOTE)
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 match_script_against_template
from .lnutil import WITNESS_TEMPLATE_RECEIVED_HTLC, WITNESS_TEMPLATE_OFFERED_HTLC
from .logging import Logger
if TYPE_CHECKING:
@ -135,24 +136,34 @@ class SweepStore(SqlDB):
class LNWatcher(AddressSynchronizer):
class LNWatcher(Logger):
LOGGING_SHORTCUT = 'W'
def __init__(self, network: 'Network'):
AddressSynchronizer.__init__(self, WalletDB({}, manual_upgrades=False))
def __init__(self, adb, network: 'Network'):
Logger.__init__(self)
self.adb = adb
self.config = network.config
self.callbacks = {} # address -> lambda: coroutine
self.network = network
util.register_callback(
self.on_network_update,
['network_updated', 'blockchain_updated', 'verified', 'wallet_updated', 'fee'])
util.register_callback(self.on_fee, ['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
self.channel_status = {}
async def stop(self):
await super().stop()
util.unregister_callback(self.on_network_update)
util.unregister_callback(self.on_fee)
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):
return self.channel_status.get(outpoint, 'unknown')
@ -171,15 +182,31 @@ class LNWatcher(AddressSynchronizer):
self.callbacks.pop(address, None)
def add_callback(self, address, callback):
self.add_address(address)
self.adb.add_address(address)
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
async def on_network_update(self, event, *args):
if event in ('verified', 'wallet_updated'):
if args[0] != self:
return
if not self.synchronizer:
async def trigger_callbacks(self):
if not self.adb.synchronizer:
self.logger.info("synchronizer not set yet")
return
for address, callback in list(self.callbacks.items()):
@ -187,18 +214,18 @@ class LNWatcher(AddressSynchronizer):
async def check_onchain_situation(self, address, funding_outpoint):
# early return if address has not been added yet
if not self.is_mine(address):
if not self.adb.is_mine(address):
return
spenders = self.inspect_tx_candidate(funding_outpoint, 0)
# 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
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_height = self.get_tx_height(closing_txid)
closing_height = self.adb.get_tx_height(closing_txid)
if closing_txid:
closing_tx = self.db.get_transaction(closing_txid)
closing_tx = self.adb.get_transaction(closing_txid)
if closing_tx:
keep_watching = await self.do_breach_remedy(funding_outpoint, closing_tx, spenders)
else:
@ -233,18 +260,18 @@ class LNWatcher(AddressSynchronizer):
n==2 => outpoint is a second-stage htlc
"""
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}
if n == 0:
if spender_txid is None:
self.channel_status[outpoint] = 'open'
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:
self.channel_status[outpoint] = 'closed (deep)'
if spender_txid is None:
return result
spender_tx = self.db.get_transaction(spender_txid)
spender_tx = self.adb.get_transaction(spender_txid)
if n == 1:
# if tx input is not a first-stage HTLC, we can stop recursion
if len(spender_tx.inputs()) != 1:
@ -263,8 +290,8 @@ class LNWatcher(AddressSynchronizer):
for i, o in enumerate(spender_tx.outputs()):
if o.address is None:
continue
if not self.is_mine(o.address):
self.add_address(o.address)
if not self.adb.is_mine(o.address):
self.adb.add_address(o.address)
elif n < 2:
r = self.inspect_tx_candidate(spender_txid+':%d'%i, n+1)
result.update(r)
@ -273,7 +300,7 @@ class LNWatcher(AddressSynchronizer):
def get_tx_mined_depth(self, txid: str):
if not txid:
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
if conf > 100:
return TxMinedDepth.DEEP
@ -298,13 +325,19 @@ class WatchTower(LNWatcher):
LOGGING_SHORTCUT = 'W'
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.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,
# and a queue for seeing which txs are being published
self.tx_progress = {} # type: Dict[str, ListenerItem]
async def stop(self):
await super().stop()
await self.adb.stop()
def diagnostic_name(self):
return "local_tower"
@ -327,7 +360,7 @@ class WatchTower(LNWatcher):
return keep_watching
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:
return
try:
@ -379,7 +412,7 @@ class LNWalletWatcher(LNWatcher):
def __init__(self, lnworker: 'LNWallet', network: 'Network'):
self.network = network
self.lnworker = lnworker
LNWatcher.__init__(self, network)
LNWatcher.__init__(self, lnworker.wallet.adb, network)
def diagnostic_name(self):
return f"{self.lnworker.wallet.diagnostic_name()}-LNW"
@ -412,7 +445,7 @@ class LNWalletWatcher(LNWatcher):
name = sweep_info.name + ' ' + chan.get_id_for_log()
spender_txid = spenders.get(prevout)
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:
keep_watching = True
continue
@ -446,7 +479,7 @@ class LNWalletWatcher(LNWatcher):
broadcast = False
reason = 'waiting for {}: CLTV ({} > {}), prevout {}'.format(name, local_height, sweep_info.cltv_expiry, prevout)
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
if prev_height.height <= 0 or wanted_height - local_height > 0:
broadcast = False
@ -460,24 +493,16 @@ class LNWalletWatcher(LNWatcher):
if broadcast:
await self.network.try_broadcasting(tx, name)
else:
if txid in self.lnworker.wallet.future_tx:
if txid in self.adb.future_tx:
return
self.logger.debug(f'(chan {chan_id_for_log}) trying to redeem {name}: {prevout}')
self.logger.info(reason)
# it's OK to add local transaction, the fee will be recomputed
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:
self.logger.info(f'could not add future tx: {name}. prevout: {prevout} {str(e)}')
tx_was_added = False
if tx_was_added:
self.logger.info(f'added future tx: {name}. prevout: {prevout}')
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'):
super().start_network(network)
self.lnwatcher = LNWalletWatcher(self, network)
self.lnwatcher.start_network(network)
self.swap_manager.start_network(network=network, lnwatcher=self.lnwatcher)
self.lnrater = LNRater(self, network)
@ -745,7 +744,7 @@ class LNWallet(LNWorker):
for coro in [
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.sync_with_local_watchtower(),
self.sync_with_remote_watchtower(),
@ -850,7 +849,7 @@ class LNWallet(LNWorker):
return out
def get_onchain_history(self):
current_height = self.wallet.get_local_height()
current_height = self.wallet.adb.get_local_height()
out = {}
# add funding events
for chan in self.channels.values():
@ -860,7 +859,7 @@ class LNWallet(LNWorker):
if not self.lnwatcher:
continue # lnwatcher not available with --offline (its data is not persisted)
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 = {
'channel_id': bh2u(chan.channel_id),
'type': 'channel_opening',
@ -880,7 +879,7 @@ class LNWallet(LNWorker):
if item is None:
continue
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 = {
'channel_id': bh2u(chan.channel_id),
'txid': closing_txid,
@ -912,7 +911,7 @@ class LNWallet(LNWorker):
label = 'Reverse swap' if swap.is_reverse else 'Forward swap'
delta = current_height - swap.locktime
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:
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:
@ -985,13 +984,13 @@ class LNWallet(LNWorker):
peer = self._peers.get(chan.node_id)
if peer:
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)
elif chan.get_state() == ChannelState.FORCE_CLOSING:
force_close_tx = chan.force_close_tx()
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:
self.logger.info('REBROADCASTING CLOSING TX')
await self.network.try_broadcasting(force_close_tx, 'force-close')
@ -1013,7 +1012,7 @@ class LNWallet(LNWorker):
temp_channel_id=os.urandom(32))
chan, funding_tx = await asyncio.wait_for(coro, LN_P2P_NETWORK_TIMEOUT)
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.set_label(funding_tx.txid(), _('Open channel'))
if funding_tx.is_complete():
@ -2311,7 +2310,7 @@ class LNWallet(LNWorker):
chan.set_state(ChannelState.FORCE_CLOSING)
# Add local tx to wallet to also allow manual rebroadcasts.
try:
self.wallet.add_transaction(tx)
self.wallet.adb.add_transaction(tx)
except UnrelatedTransactionException:
pass # this can happen if (~all the balance goes to REMOTE)
return tx

2
electrum/network.py

@ -348,7 +348,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
if self.config.get('run_watchtower', False):
from . import lnwatcher
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())
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:
assert self.network
assert self.lnwatcher
if not self.lnwatcher.is_up_to_date():
if not self.lnwatcher.adb.is_up_to_date():
return
current_height = self.network.get_local_height()
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():
if swap.is_reverse and txin.value_sats() < swap.onchain_amount:
self.logger.info('amount too low, we should not reveal the preimage')
@ -200,7 +200,7 @@ class SwapManager(Logger):
swap.is_redeemed = True
elif spent_height == TX_HEIGHT_LOCAL:
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}')
await self.network.broadcast_transaction(tx)
# already in mempool
@ -230,8 +230,7 @@ class SwapManager(Logger):
)
self.sign_tx(tx, swap)
self.logger.info(f'adding claim tx {tx.txid()}')
self.wallet.add_transaction(tx)
self.lnwatcher.add_transaction(tx)
self.wallet.adb.add_transaction(tx)
def get_claim_fee(self):
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
data of any transactions the wallet doesn't have.
'''
def __init__(self, wallet: 'AddressSynchronizer'):
self.wallet = wallet
SynchronizerBase.__init__(self, wallet.network)
def __init__(self, adb: 'AddressSynchronizer'):
self.adb = adb
SynchronizerBase.__init__(self, adb.network)
def _reset(self):
super()._reset()
@ -146,7 +146,7 @@ class Synchronizer(SynchronizerBase):
self._stale_histories = dict() # type: Dict[str, asyncio.Task]
def diagnostic_name(self):
return self.wallet.diagnostic_name()
return self.adb.diagnostic_name()
def is_up_to_date(self):
return (not self.requested_addrs
@ -155,7 +155,7 @@ class Synchronizer(SynchronizerBase):
and not self._stale_histories)
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:
return
# No point in requesting history twice for the same announced status.
@ -189,7 +189,7 @@ class Synchronizer(SynchronizerBase):
else:
self._stale_histories.pop(addr, asyncio.Future()).cancel()
# 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
await self._request_missing_txs(hist)
@ -202,7 +202,7 @@ class Synchronizer(SynchronizerBase):
for tx_hash, tx_height in hist:
if tx_hash in self.requested_tx:
continue
tx = self.wallet.db.get_transaction(tx_hash)
tx = self.adb.db.get_transaction(tx_hash)
if tx and not isinstance(tx, PartialTransaction):
continue # already have complete tx
transaction_hashes.append(tx_hash)
@ -231,39 +231,32 @@ class Synchronizer(SynchronizerBase):
if 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)
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)}")
# callbacks
util.trigger_callback('new_transaction', self.wallet, tx)
async def main(self):
self.wallet.set_up_to_date(False)
self.adb.set_up_to_date(False)
# request missing txns, if any
for addr in random_shuffled_copy(self.wallet.db.get_history()):
history = self.wallet.db.get_addr_history(addr)
for addr in random_shuffled_copy(self.adb.db.get_history()):
history = self.adb.db.get_addr_history(addr)
# Old electrum servers returned ['*'] when all history for the address
# was pruned. This no longer happens but may remain in old wallets.
if history == ['*']: continue
await self._request_missing_txs(history, allow_server_not_finding_tx=True)
# 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)
# main loop
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.
hist_done = self.is_up_to_date()
spv_done = self.wallet.verifier.is_up_to_date() if self.wallet.verifier else True
num_new_addrs = await run_in_thread(self.wallet.synchronize)
up_to_date = hist_done and spv_done and num_new_addrs == 0
spv_done = self.adb.verifier.is_up_to_date() if self.adb.verifier else True
up_to_date = hist_done and spv_done
# 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):
self._processed_some_notifications = False
self.wallet.set_up_to_date(up_to_date)
util.trigger_callback('wallet_updated', self.wallet)
self.adb.set_up_to_date(up_to_date)
class Notifier(SynchronizerBase):

8
electrum/tests/test_commands.py

@ -217,7 +217,7 @@ class TestCommandsTestnet(TestCaseForTestnet):
funding_tx = Transaction('0200000000010165806607dd458280cb57bf64a16cf4be85d053145227b98c28932e953076b8e20000000000fdffffff02ac150700000000001600147e3ddfe6232e448a8390f3073c7a3b2044fd17eb102908000000000016001427fbe3707bc57e5bb63d6f15733ec88626d8188a02473044022049ce9efbab88808720aa563e2d9bc40226389ab459c4390ea3e89465665d593502206c1c7c30a2f640af1e463e5107ee4cfc0ee22664cfae3f2606a95303b54cdef80121026269e54d06f7070c1f967eb2874ba60de550dfc327a945c98eb773672d9411fd77181e00')
funding_txid = funding_tx.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)
tx_str = cmds._run(
@ -245,7 +245,7 @@ class TestCommandsTestnet(TestCaseForTestnet):
funding_tx = Transaction('02000000000101f59876b1c65bbe3e182ccc7ea7224fe397bb9b70aadcbbf4f4074c75c8a074840000000000fdffffff021f351f00000000001600144eec851dd980cc36af1f629a32325f511604d6af56732d000000000016001439267bc7f3e3fabeae3bc3f73880de22d8b01ba50247304402207eac5f639806a00878488d58ca651d690292145bca5511531845ae21fab309d102207162708bd344840cc1bacff1092e426eb8484f83f5c068ba4ca579813de324540121020e0798c267ff06ee8b838cd465f3cfa6c843a122a04917364ce000c29ca205cae5f31f00')
funding_txid = funding_tx.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)
tx_str = cmds._run(
@ -283,7 +283,7 @@ class TestCommandsTestnet(TestCaseForTestnet):
funding_txid = funding_tx.txid()
funding_output_value = 1000000
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)
@ -301,7 +301,7 @@ class TestCommandsTestnet(TestCaseForTestnet):
funding_tx = Transaction("02000000000102789e8aa8caa79d87241ff9df0e3fd757a07c85a30195d76e8efced1d57c56b670000000000fdffffff7ee2b6abd52b332f797718ae582f8d3b979b83b1799e0a3bfb2c90c6e070c29e0100000000fdffffff020820000000000000160014c0eb720c93a61615d2d66542d381be8943ca553950c3000000000000160014d7dbd0196a2cbd76420f14a19377096cf6cddb75024730440220485b491ad8d3ce3b4da034a851882da84a06ec9800edff0d3fd6aa42eeba3b440220359ea85d32a05932ac417125e133fa54e54e7e9cd20ebc54b883576b8603fd65012103860f1fbf8a482b9d35d7d4d04be8fb33d856a514117cd8b73e372d36895feec60247304402206c2ca56cc030853fa59b4b3cb293f69a3378ead0f10cb76f640f8c2888773461022079b7055d0f6af6952a48e5b97218015b0723462d667765c142b41bd35e3d9c0a01210359e303f57647094a668d69e8ff0bd46c356d00aa7da6dc533c438e71c057f0793e721f00")
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)
tx = "02000000000101b9723dfc69af058ef6613539a000d2cd098a2c8a74e802b6d8739db708ba8c9a0100000000fdffffff02a00f00000000000016001429e1fd187f0cac845946ae1b11dc136c536bfc0fe8b2000000000000160014100611bcb3aee7aad176936cf4ed56ade03027aa02473044022063c05e2347f16251922830ccc757231247b3c2970c225f988e9204844a1ab7b802204652d2c4816707e3d3bea2609b83b079001a435bad2a99cc2e730f276d07070c012102ee3f00141178006c78b0b458aab21588388335078c655459afe544211f15aee050721f00"

8
electrum/tests/test_lnpeer.py

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

15
electrum/tests/test_wallet.py

@ -24,7 +24,8 @@ from . import ElectrumTestCase
class FakeSynchronizer(object):
def __init__(self):
def __init__(self, db):
self.db = db
self.store = []
def add(self, address):
@ -100,18 +101,20 @@ class FakeFxThread:
ccy_amount_str = FxThread.ccy_amount_str
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:
def __init__(self, fiat_value):
super().__init__()
self.fiat_value = fiat_value
self.db = WalletDB("{}", manual_upgrades=True)
self.adb = FakeADB()
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
price_at_timestamp = Abstract_Wallet.price_at_timestamp
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 threading
import enum
import asyncio
from aiorpcx import timeout_after, TaskTimeout, ignore_after
@ -260,7 +261,7 @@ class TxWalletDetails(NamedTuple):
is_lightning_funding_tx: bool
class Abstract_Wallet(AddressSynchronizer, ABC):
class Abstract_Wallet(ABC):
"""
Wallet classes are created to handle various address generation methods.
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
db.load_addresses(self.wallet_type)
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
self.use_change = db.get('use_change', True)
@ -309,6 +316,24 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self._coin_price_cache = {}
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):
if self.storage:
@ -367,9 +392,13 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
async def stop(self):
"""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:
async with ignore_after(5):
await super().stop()
await self.adb.stop()
if self.network:
if self.lnworker:
await self.lnworker.stop()
@ -379,28 +408,56 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self.save_keystore()
self.save_db()
def set_up_to_date(self, b):
super().set_up_to_date(b)
if b: self.save_db()
def is_up_to_date(self):
return self.adb.is_up_to_date()
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):
super().clear_history()
self.adb.clear_history()
self.save_db()
def start_network(self, network):
AddressSynchronizer.start_network(self, network)
self.network = network
if network:
asyncio.run_coroutine_threadsafe(self.main(), self.network.asyncio_loop)
self.adb.start_network(network)
if self.lnworker:
self.lnworker.start_network(network)
# only start gossiping when we already have channels
if self.db.get('channels'):
self.network.start_gossip()
def load_and_cleanup(self):
self.load_keystore()
self.test_addresses_sanity()
super().load_and_cleanup()
@abstractmethod
def load_keystore(self) -> None:
pass
@ -450,7 +507,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self._not_old_change_addresses = [addr for addr in self._not_old_change_addresses
if not self.address_is_old(addr)]
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
def is_deterministic(self) -> bool:
@ -537,6 +594,10 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
return False
return self.get_address_index(address)[0] == 1
@abstractmethod
def get_addresses(self) -> Sequence[str]:
pass
@abstractmethod
def get_address_index(self, address: str) -> Optional[AddressIndexGeneric]:
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
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_any_input_ismine = tx_wallet_delta.is_any_input_ismine
is_swap = self.is_swap_tx(tx)
@ -606,11 +667,11 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
can_cpfp = False
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)
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
and (tx_we_already_have_in_db is None or not tx_we_already_have_in_db.is_complete()))
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])
# otherwise 'height' is unreliable (typically LOCAL):
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):
status = _('Unconfirmed')
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():
size = tx.estimated_size()
fee_per_byte = fee / size
@ -682,11 +743,22 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
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]:
confirmed_only = self.config.get('confirmed_only', False)
with self._freeze_lock:
frozen_addresses = self._frozen_addresses.copy()
utxos = self.get_utxos(domain,
utxos = self.get_utxos(
excluded_addresses=frozen_addresses,
mature_only=True,
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()
if self.is_frozen_coin(utxo)}
if not frozen_coins: # shortcut
return self.get_balance(frozen_addresses)
return self.adb.get_balance(frozen_addresses)
c1, u1, x1 = self.get_balance()
c2, u2, x2 = self.get_balance(
excluded_addresses=frozen_addresses,
@ -739,7 +811,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
def balance_at_timestamp(self, domain, target_timestamp):
# we assume that get_history returns items ordered by block height
# 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
for hist_item in h:
balance = hist_item.balance
@ -749,8 +821,10 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
return balance
def get_onchain_history(self, *, domain=None):
if domain is None:
domain = self.get_addresses()
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))
yield {
'txid': hist_item.txid,
@ -768,7 +842,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
}
def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> Invoice:
height = self.get_local_height()
height = self.adb.get_local_height()
if pr:
return Invoice.from_bip70_payreq(pr, height=height)
amount_msat = 0
@ -801,7 +875,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
key = self.get_key_for_outgoing_invoice(invoice)
if not invoice.is_lightning():
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:
for txout in invoice.get_outputs():
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)
total_received = 0
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:
continue
if tx_height.conf < conf:
@ -917,14 +991,15 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self.set_label(tx_hash, "; ".join(labels))
return bool(labels)
def add_transaction(self, tx, *, allow_unrelated=False):
is_known = bool(self.db.get_transaction(tx.txid()))
tx_was_added = super().add_transaction(tx, allow_unrelated=allow_unrelated)
if tx_was_added and not is_known:
self._maybe_set_tx_label_based_on_invoices(tx)
if self.lnworker:
self.lnworker.maybe_add_backup_from_tx(tx)
return tx_was_added
# fixme: this needs a callback
#def add_transaction(self, tx, *, allow_unrelated=False):
# is_known = bool(self.db.get_transaction(tx.txid()))
# tx_was_added = self.adb.add_transaction(tx, allow_unrelated=allow_unrelated)
# if tx_was_added and not is_known:
# self._maybe_set_tx_label_based_on_invoices(tx)
# if self.lnworker:
# self.lnworker.maybe_add_backup_from_tx(tx)
# return tx_was_added
@profiler
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']
start_coins = self.get_utxos(
domain=None,
block_height=start_height,
confirmed_funding_only=True,
confirmed_spending_only=True,
nonlocal_only=True)
end_coins = self.get_utxos(
domain=None,
block_height=end_height,
confirmed_funding_only=True,
confirmed_spending_only=True,
@ -1130,7 +1203,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
}
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):
p = price_func(timestamp)
@ -1204,7 +1277,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
is_final = tx and tx.is_final()
if not is_final:
extra.append('rbf')
fee = self.get_tx_fee(tx_hash)
fee = self.adb.get_tx_fee(tx_hash)
if fee is not None:
size = tx.estimated_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]:
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
if hist_item.tx_mined_status.conf > 0: continue
# 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())]):
continue
# 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
# do not mutate LN funding txs, as that would change their 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:
# 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()]
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.add_info_from_wallet(self)
base_tx_fee = base_tx.get_fee()
@ -1512,7 +1586,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
# we should typically have the funding tx available;
# might not have it e.g. while not up_to_date
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()):
return False
return True
@ -1565,12 +1639,12 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
needs_spv_check = not self.config.get("skipmerklecheck", False)
for tx_hash, tx_height in h:
if needs_spv_check:
tx_age = self.get_tx_height(tx_hash).conf
tx_age = self.adb.get_tx_height(tx_hash).conf
else:
if tx_height <= 0:
tx_age = 0
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)
return max_conf >= req_conf
@ -1835,7 +1909,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
break
else:
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))
if not item:
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."))
# grab all ismine 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])
# figure out output 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.
address = address or txin.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())
if item:
txin_value = item[1]
@ -1949,7 +2023,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
only_der_suffix: bool = False,
ignore_network_issues: bool = True,
) -> None:
address = self.get_txin_address(txin)
address = self.adb.get_txin_address(txin)
# note: we add input utxos regardless of is_mine
self._add_input_utxo_info(txin, ignore_network_issues=ignore_network_issues, address=address)
is_mine = self.is_mine(address)
@ -2006,8 +2080,8 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
raw_tx = self.network.run_from_another_thread(
self.network.get_transaction(tx_hash, timeout=10))
except NetworkException as e:
self.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')
_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')
if not ignore_network_issues:
raise e
else:
@ -2082,7 +2156,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
# TODO we should index receive_requests by id
# add lightning requests. (use as key)
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]
@check_returned_address_for_corruption
@ -2105,7 +2179,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
raise Exception("no receiving addresses in wallet?!")
choice = domain[0]
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():
return addr
else:
@ -2128,12 +2202,12 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
def get_onchain_request_status(self, r: Invoice) -> Tuple[bool, Optional[int]]:
address = r.get_address()
amount = int(r.get_amount_sat() or 0)
received, sent = self.get_addr_io(address)
received, sent = self.adb.get_addr_io(address)
l = []
for txo, x in received.items():
h, v, is_cb = x
txid, n = txo.split(':')
tx_height = self.get_tx_height(txid)
tx_height = self.adb.get_tx_height(txid)
height = tx_height.height
if height > 0 and height <= r.height:
continue
@ -2276,19 +2350,6 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
d['bip70'] = x.bip70
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:
# 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
@ -2310,7 +2371,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
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
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(
outputs=outputs,
message=message,
@ -2502,7 +2563,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
def price_at_timestamp(self, txid, price_func):
"""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())
def average_price(self, txid, price_func, ccy) -> Decimal:
@ -2663,6 +2724,9 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
else:
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):
# wallet with a single keystore
@ -2766,7 +2830,7 @@ class Imported_Wallet(Simple_Wallet):
continue
good_addr.append(address)
self.db.add_imported_address(address, {})
self.add_address(address)
self.adb.add_address(address)
if write_to_disk:
self.save_db()
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
with self.lock:
for addr in self.db.get_history():
details = self.get_address_history(addr)
details = self.adb.get_address_history(addr)
if addr == address:
for tx_hash, height in details:
transactions_to_remove.add(tx_hash)
@ -2797,7 +2861,7 @@ class Imported_Wallet(Simple_Wallet):
transactions_to_remove -= transactions_new
self.db.remove_addr_history(address)
for tx_hash in transactions_to_remove:
self._remove_transaction(tx_hash)
self.adb._remove_transaction(tx_hash)
self.set_label(address, None)
self.remove_payment_request(address)
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]:
with self.lock:
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
def is_mine(self, address) -> bool:
@ -2865,7 +2929,7 @@ class Imported_Wallet(Simple_Wallet):
addr = bitcoin.pubkey_to_address(txin_type, pubkey)
good_addr.append(addr)
self.db.add_imported_address(addr, {'type':txin_type, 'pubkey':pubkey})
self.add_address(addr)
self.adb.add_address(addr)
self.save_keystore()
if write_to_disk:
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()
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.add_address(address)
self.adb.add_address(address)
if for_change:
# note: if it's actually "old", it will get filtered later
self._not_old_change_addresses.append(address)
@ -3081,7 +3145,7 @@ class Deterministic_Wallet(Abstract_Wallet):
break
return count
@AddressSynchronizer.with_local_height_cached
#@AddressSynchronizer.with_local_height_cached FIXME
def synchronize(self):
count = 0
with self.lock:

Loading…
Cancel
Save