• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

spesmilo / electrum / 5735552722403328

16 May 2025 10:28AM UTC coverage: 59.722% (+0.002%) from 59.72%
5735552722403328

Pull #9833

CirrusCI

f321x
make lightning dns seed fetching async
Pull Request #9833: dns: use async dnspython interface

22 of 50 new or added lines in 7 files covered. (44.0%)

1107 existing lines in 11 files now uncovered.

21549 of 36082 relevant lines covered (59.72%)

2.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.45
/electrum/address_synchronizer.py
1
# Electrum - lightweight Bitcoin client
2
# Copyright (C) 2018 The Electrum Developers
3
#
4
# Permission is hereby granted, free of charge, to any person
5
# obtaining a copy of this software and associated documentation files
6
# (the "Software"), to deal in the Software without restriction,
7
# including without limitation the rights to use, copy, modify, merge,
8
# publish, distribute, sublicense, and/or sell copies of the Software,
9
# and to permit persons to whom the Software is furnished to do so,
10
# subject to the following conditions:
11
#
12
# The above copyright notice and this permission notice shall be
13
# included in all copies or substantial portions of the Software.
14
#
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
19
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
20
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
# SOFTWARE.
23

24
import asyncio
4✔
25
import threading
4✔
26
import itertools
4✔
27
from collections import defaultdict
4✔
28
from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple, NamedTuple, Sequence, List
4✔
29

30
from .crypto import sha256
4✔
31
from . import bitcoin, util
4✔
32
from .bitcoin import COINBASE_MATURITY
4✔
33
from .util import profiler, bfh, TxMinedInfo, UnrelatedTransactionException, with_lock, OldTaskGroup
4✔
34
from .transaction import Transaction, TxOutput, TxInput, PartialTxInput, TxOutpoint, PartialTransaction, tx_from_any
4✔
35
from .synchronizer import Synchronizer
4✔
36
from .verifier import SPV
4✔
37
from .blockchain import hash_header, Blockchain
4✔
38
from .i18n import _
4✔
39
from .logging import Logger
4✔
40
from .util import EventListener, event_listener
4✔
41

42
if TYPE_CHECKING:
4✔
43
    from .network import Network
×
44
    from .wallet_db import WalletDB
×
45
    from .simple_config import SimpleConfig
×
46

47

48
TX_HEIGHT_FUTURE = -3
4✔
49
TX_HEIGHT_LOCAL = -2
4✔
50
TX_HEIGHT_UNCONF_PARENT = -1
4✔
51
TX_HEIGHT_UNCONFIRMED = 0
4✔
52

53
TX_TIMESTAMP_INF = 999_999_999_999
4✔
54
TX_HEIGHT_INF = 10 ** 9
4✔
55

56

57
from enum import IntEnum, auto
4✔
58

59
class TxMinedDepth(IntEnum):
4✔
60
    """ IntEnum because we call min() in get_deepest_tx_mined_depth_for_txids """
61
    DEEP = auto()
4✔
62
    SHALLOW = auto()
4✔
63
    MEMPOOL = auto()
4✔
64
    FREE = auto()
4✔
65

66

67
class HistoryItem(NamedTuple):
4✔
68
    txid: str
4✔
69
    tx_mined_status: TxMinedInfo
4✔
70
    delta: int
4✔
71
    fee: Optional[int]
4✔
72
    balance: int
4✔
73

74

75
class AddressSynchronizer(Logger, EventListener):
4✔
76
    """ address database """
77

78
    network: Optional['Network']
4✔
79
    asyncio_loop: Optional['asyncio.AbstractEventLoop'] = None
4✔
80
    synchronizer: Optional['Synchronizer']
4✔
81
    verifier: Optional['SPV']
4✔
82

83
    def __init__(self, db: 'WalletDB', config: 'SimpleConfig', *, name: str = None):
4✔
84
        self.db = db
4✔
85
        self.config = config
4✔
86
        self.name = name
4✔
87
        self.network = None
4✔
88
        Logger.__init__(self)
4✔
89
        # verifier (SPV) and synchronizer are started in start_network
90
        self.synchronizer = None
4✔
91
        self.verifier = None
4✔
92
        # locks: if you need to take multiple ones, acquire them in the order they are defined here!
93
        self.lock = threading.RLock()
4✔
94
        self.transaction_lock = threading.RLock()
4✔
95
        self.future_tx = {}  # type: Dict[str, int]  # txid -> wanted (abs) height
4✔
96
        # Txs the server claims are mined but still pending verification:
97
        self.unverified_tx = defaultdict(int)  # type: Dict[str, int]  # txid -> height. Access with self.lock.
4✔
98
        # Txs the server claims are in the mempool:
99
        self.unconfirmed_tx = defaultdict(int)  # type: Dict[str, int]  # txid -> height. Access with self.lock.
4✔
100
        # thread local storage for caching stuff
101
        self.threadlocal_cache = threading.local()
4✔
102

103
        self._get_balance_cache = {}
4✔
104

105
        self.load_and_cleanup()
4✔
106

107
    def diagnostic_name(self):
4✔
108
        return self.name or ""
4✔
109

110
    def with_transaction_lock(func):
4✔
111
        def func_wrapper(self: 'AddressSynchronizer', *args, **kwargs):
4✔
112
            with self.transaction_lock:
4✔
113
                return func(self, *args, **kwargs)
4✔
114
        return func_wrapper
4✔
115

116
    def load_and_cleanup(self):
4✔
117
        self.load_local_history()
4✔
118
        self.check_history()
4✔
119
        self.load_unverified_transactions()
4✔
120
        self.remove_local_transactions_we_dont_have()
4✔
121

122
    def is_mine(self, address: Optional[str]) -> bool:
4✔
123
        """Returns whether an address is in our set.
124

125
        Differences between adb.is_mine and wallet.is_mine:
126
        - adb.is_mine: addrs that we are watching (e.g. via Synchronizer)
127
            - lnwatcher adds its own lightning-related addresses that are not part of the wallet
128
        - wallet.is_mine: addrs that are part of the wallet balance or the wallet might sign for
129
            - an offline wallet might learn from a PSBT about addrs beyond its gap limit
130
        Neither set is guaranteed to be a subset of the other.
131
        """
132
        if not address: return False
4✔
133
        return self.db.is_addr_in_history(address)
4✔
134

135
    def get_addresses(self):
4✔
136
        return sorted(self.db.get_history())
×
137

138
    def get_address_history(self, addr: str) -> Dict[str, int]:
4✔
139
        """Returns the history for the address, as a txid->height dict.
140
        In addition to what we have from the server, this includes local and future txns.
141

142
        Also see related method db.get_addr_history, which stores the response from the server,
143
        so that only includes txns the server sees.
144
        """
145
        h = {}
4✔
146
        # we need self.transaction_lock but get_tx_height will take self.lock
147
        # so we need to take that too here, to enforce order of locks
148
        with self.lock, self.transaction_lock:
4✔
149
            related_txns = self._history_local.get(addr, set())
4✔
150
            for tx_hash in related_txns:
4✔
151
                tx_height = self.get_tx_height(tx_hash).height
4✔
152
                h[tx_hash] = tx_height
4✔
153
        return h
4✔
154

155
    def get_address_history_len(self, addr: str) -> int:
4✔
156
        """Return number of transactions where address is involved."""
157
        return len(self._history_local.get(addr, ()))
4✔
158

159
    def get_txin_address(self, txin: TxInput) -> Optional[str]:
4✔
160
        if txin.address:
4✔
161
            return txin.address
4✔
162
        prevout_hash = txin.prevout.txid.hex()
4✔
163
        prevout_n = txin.prevout.out_idx
4✔
164
        for addr in self.db.get_txo_addresses(prevout_hash):
4✔
165
            d = self.db.get_txo_addr(prevout_hash, addr)
4✔
166
            if prevout_n in d:
4✔
167
                return addr
4✔
168
        tx = self.db.get_transaction(prevout_hash)
4✔
169
        if tx:
4✔
170
            return tx.outputs()[prevout_n].address
4✔
171
        return None
4✔
172

173
    def get_txin_value(self, txin: TxInput, *, address: str = None) -> Optional[int]:
4✔
174
        if txin.value_sats() is not None:
4✔
175
            return txin.value_sats()
4✔
176
        prevout_hash = txin.prevout.txid.hex()
4✔
177
        prevout_n = txin.prevout.out_idx
4✔
178
        if address is None:
4✔
179
            address = self.get_txin_address(txin)
×
180
        if address:
4✔
181
            d = self.db.get_txo_addr(prevout_hash, address)
4✔
182
            try:
4✔
183
                v, cb = d[prevout_n]
4✔
184
                return v
4✔
185
            except KeyError:
×
186
                pass
×
187
        tx = self.db.get_transaction(prevout_hash)
×
188
        if tx:
×
189
            return tx.outputs()[prevout_n].value
×
190
        return None
×
191

192
    def load_unverified_transactions(self):
4✔
193
        # review transactions that are in the history
194
        for addr in self.db.get_history():
4✔
195
            hist = self.db.get_addr_history(addr)
4✔
196
            for tx_hash, tx_height in hist:
4✔
197
                # add it in case it was previously unconfirmed
198
                self.add_unverified_or_unconfirmed_tx(tx_hash, tx_height)
4✔
199

200
    def start_network(self, network: Optional['Network']) -> None:
4✔
201
        assert self.network is None, "already started"
4✔
202
        self.network = network
4✔
203
        if self.network is not None:
4✔
204
            self.synchronizer = Synchronizer(self)
4✔
205
            self.verifier = SPV(self.network, self)
4✔
206
            self.asyncio_loop = network.asyncio_loop
4✔
207
            self.register_callbacks()
4✔
208

209
    @event_listener
4✔
210
    def on_event_blockchain_updated(self, *args):
4✔
211
        self._get_balance_cache = {}  # invalidate cache
×
212
        self.db.put('stored_height', self.get_local_height())
×
213

214
    async def stop(self):
4✔
215
        if self.network:
4✔
216
            try:
×
217
                async with OldTaskGroup() as group:
×
218
                    if self.synchronizer:
×
219
                        await group.spawn(self.synchronizer.stop())
×
220
                    if self.verifier:
×
221
                        await group.spawn(self.verifier.stop())
×
222
            finally:  # even if we get cancelled
223
                self.synchronizer = None
×
224
                self.verifier = None
×
225
                self.unregister_callbacks()
×
226

227
    def add_address(self, address):
4✔
228
        if address not in self.db.history:
4✔
229
            self.db.history[address] = []
4✔
230
        if self.synchronizer:
4✔
231
            self.synchronizer.add(address)
×
232
        self.up_to_date_changed()
4✔
233

234
    def get_conflicting_transactions(self, tx: Transaction, *, include_self: bool = False) -> Set[str]:
4✔
235
        """Returns a set of transaction hashes from the wallet history that are
236
        directly conflicting with tx, i.e. they have common outpoints being
237
        spent with tx.
238

239
        include_self specifies whether the tx itself should be reported as a
240
        conflict (if already in wallet history)
241
        """
242
        conflicting_txns = set()
4✔
243
        with self.transaction_lock:
4✔
244
            for txin in tx.inputs():
4✔
245
                if txin.is_coinbase_input():
4✔
246
                    continue
×
247
                prevout_hash = txin.prevout.txid.hex()
4✔
248
                prevout_n = txin.prevout.out_idx
4✔
249
                spending_tx_hash = self.db.get_spent_outpoint(prevout_hash, prevout_n)
4✔
250
                if spending_tx_hash is None:
4✔
251
                    continue
4✔
252
                # this outpoint has already been spent, by spending_tx
253
                # annoying assert that has revealed several bugs over time:
254
                assert self.db.get_transaction(spending_tx_hash), "spending tx not in wallet db"
4✔
255
                conflicting_txns |= {spending_tx_hash}
4✔
256
            if tx_hash := tx.txid():
4✔
257
                if tx_hash in conflicting_txns:
4✔
258
                    # this tx is already in history, so it conflicts with itself
259
                    if len(conflicting_txns) > 1:
4✔
260
                        raise Exception('Found conflicting transactions already in wallet history.')
×
261
                    if not include_self:
4✔
262
                        conflicting_txns -= {tx_hash}
4✔
263
            return conflicting_txns
4✔
264

265
    def get_transaction(self, txid: str) -> Optional[Transaction]:
4✔
266
        tx = self.db.get_transaction(txid)
4✔
267
        if tx:
4✔
268
            tx.deserialize()
4✔
269
            for txin in tx._inputs:
4✔
270
                tx_mined_info = self.get_tx_height(txin.prevout.txid.hex())
4✔
271
                txin.block_height = tx_mined_info.height  # not SPV-ed
4✔
272
                txin.block_txpos = tx_mined_info.txpos
4✔
273
        return tx
4✔
274

275
    def add_transaction(self, tx: Transaction, *, allow_unrelated=False, is_new=True) -> bool:
4✔
276
        """
277
        Returns whether the tx was successfully added to the wallet history.
278
        Note that a transaction may need to be added several times, if our
279
        list of addresses has increased. This will return True even if the
280
        transaction was already in self.db.
281
        """
282
        assert tx, tx
4✔
283
        # note: tx.is_complete() is not necessarily True; tx might be partial
284
        # but it *needs* to have a txid:
285
        tx_hash = tx.txid()
4✔
286
        if tx_hash is None:
4✔
287
            raise Exception("cannot add tx without txid to wallet history")
×
288
        # For sanity, try to serialize and deserialize tx early:
289
        tx_from_any(str(tx))  # see if raises (no-side-effects)
4✔
290
        # we need self.transaction_lock but get_tx_height will take self.lock
291
        # so we need to take that too here, to enforce order of locks
292
        with self.lock, self.transaction_lock:
4✔
293
            # NOTE: returning if tx in self.transactions might seem like a good idea
294
            # BUT we track is_mine inputs in a txn, and during subsequent calls
295
            # of add_transaction tx, we might learn of more-and-more inputs of
296
            # being is_mine, as we roll the gap_limit forward
297
            is_coinbase = tx.inputs()[0].is_coinbase_input()
4✔
298
            tx_height = self.get_tx_height(tx_hash).height
4✔
299
            if not allow_unrelated:
4✔
300
                # note that during sync, if the transactions are not properly sorted,
301
                # it could happen that we think tx is unrelated but actually one of the inputs is is_mine.
302
                # this is the main motivation for allow_unrelated
303
                is_mine = any([self.is_mine(self.get_txin_address(txin)) for txin in tx.inputs()])
4✔
304
                is_for_me = any([self.is_mine(txo.address) for txo in tx.outputs()])
4✔
305
                if not is_mine and not is_for_me:
4✔
306
                    raise UnrelatedTransactionException()
4✔
307
            # Find all conflicting transactions.
308
            # In case of a conflict,
309
            #     1. confirmed > mempool > local
310
            #     2. this new txn has priority over existing ones
311
            # When this method exits, there must NOT be any conflict, so
312
            # either keep this txn and remove all conflicting (along with dependencies)
313
            #     or drop this txn
314
            conflicting_txns = self.get_conflicting_transactions(tx)
4✔
315
            if conflicting_txns:
4✔
316
                existing_mempool_txn = any(
4✔
317
                    self.get_tx_height(tx_hash2).height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT)
318
                    for tx_hash2 in conflicting_txns)
319
                existing_confirmed_txn = any(
4✔
320
                    self.get_tx_height(tx_hash2).height > 0
321
                    for tx_hash2 in conflicting_txns)
322
                if existing_confirmed_txn and tx_height <= 0:
4✔
323
                    # this is a non-confirmed tx that conflicts with confirmed txns; drop.
324
                    return False
×
325
                if existing_mempool_txn and tx_height == TX_HEIGHT_LOCAL:
4✔
326
                    # this is a local tx that conflicts with non-local txns; drop.
327
                    return False
4✔
328
                # keep this txn and remove all conflicting
329
                for tx_hash2 in conflicting_txns:
4✔
330
                    self.remove_transaction(tx_hash2)
4✔
331
            # add inputs
332
            def add_value_from_prev_output():
4✔
333
                # note: this takes linear time in num is_mine outputs of prev_tx
334
                addr = self.get_txin_address(txi)
4✔
335
                if addr and self.is_mine(addr):
4✔
336
                    outputs = self.db.get_txo_addr(prevout_hash, addr)
4✔
337
                    try:
4✔
338
                        v, is_cb = outputs[prevout_n]
4✔
339
                    except KeyError:
×
340
                        pass
×
341
                    else:
342
                        self.db.add_txi_addr(tx_hash, addr, ser, v)
4✔
343
                        self._get_balance_cache.clear()  # invalidate cache
4✔
344
            for txi in tx.inputs():
4✔
345
                if txi.is_coinbase_input():
4✔
346
                    continue
×
347
                prevout_hash = txi.prevout.txid.hex()
4✔
348
                prevout_n = txi.prevout.out_idx
4✔
349
                ser = txi.prevout.to_str()
4✔
350
                self.db.set_spent_outpoint(prevout_hash, prevout_n, tx_hash)
4✔
351
                add_value_from_prev_output()
4✔
352
            # add outputs
353
            for n, txo in enumerate(tx.outputs()):
4✔
354
                v = txo.value
4✔
355
                ser = tx_hash + ':%d'%n
4✔
356
                scripthash = bitcoin.script_to_scripthash(txo.scriptpubkey)
4✔
357
                self.db.add_prevout_by_scripthash(scripthash, prevout=TxOutpoint.from_str(ser), value=v)
4✔
358
                addr = txo.address
4✔
359
                if addr and self.is_mine(addr):
4✔
360
                    self.db.add_txo_addr(tx_hash, addr, n, v, is_coinbase)
4✔
361
                    self._get_balance_cache.clear()  # invalidate cache
4✔
362
                    # give v to txi that spends me
363
                    next_tx = self.db.get_spent_outpoint(tx_hash, n)
4✔
364
                    if next_tx is not None:
4✔
365
                        self.db.add_txi_addr(next_tx, addr, ser, v)
4✔
366
                        self._add_tx_to_local_history(next_tx)
4✔
367
            # add to local history
368
            self._add_tx_to_local_history(tx_hash)
4✔
369
            # save
370
            self.db.add_transaction(tx_hash, tx)
4✔
371
            self.db.add_num_inputs_to_tx(tx_hash, len(tx.inputs()))
4✔
372
            if is_new:
4✔
373
                util.trigger_callback('adb_added_tx', self, tx_hash, tx)
4✔
374
            return True
4✔
375

376
    def remove_transaction(self, tx_hash: str) -> None:
4✔
377
        """Removes a transaction AND all its dependents/children
378
        from the wallet history.
379
        """
380
        with self.lock, self.transaction_lock:
4✔
381
            to_remove = {tx_hash}
4✔
382
            to_remove |= self.get_depending_transactions(tx_hash)
4✔
383
            for txid in to_remove:
4✔
384
                self._remove_transaction(txid)
4✔
385

386
    def _remove_transaction(self, tx_hash: str) -> None:
4✔
387
        """Removes a single transaction from the wallet history, and attempts
388
         to undo all effects of the tx (spending inputs, creating outputs, etc).
389
        """
390
        def remove_from_spent_outpoints():
4✔
391
            # undo spends in spent_outpoints
392
            if tx is not None:
4✔
393
                # if we have the tx, this branch is faster
394
                for txin in tx.inputs():
4✔
395
                    if txin.is_coinbase_input():
4✔
396
                        continue
×
397
                    prevout_hash = txin.prevout.txid.hex()
4✔
398
                    prevout_n = txin.prevout.out_idx
4✔
399
                    self.db.remove_spent_outpoint(prevout_hash, prevout_n)
4✔
400
            else:
401
                # expensive but always works
402
                for prevout_hash, prevout_n in self.db.list_spent_outpoints():
×
403
                    spending_txid = self.db.get_spent_outpoint(prevout_hash, prevout_n)
×
404
                    if spending_txid == tx_hash:
×
405
                        self.db.remove_spent_outpoint(prevout_hash, prevout_n)
×
406

407
        with self.lock, self.transaction_lock:
4✔
408
            self.logger.info(f"removing tx from history {tx_hash}")
4✔
409
            tx = self.db.remove_transaction(tx_hash)
4✔
410
            remove_from_spent_outpoints()
4✔
411
            self._remove_tx_from_local_history(tx_hash)
4✔
412
            for addr in itertools.chain(self.db.get_txi_addresses(tx_hash), self.db.get_txo_addresses(tx_hash)):
4✔
413
                self._get_balance_cache.clear()  # invalidate cache
4✔
414
            self.db.remove_txi(tx_hash)
4✔
415
            self.db.remove_txo(tx_hash)
4✔
416
            self.db.remove_tx_fee(tx_hash)
4✔
417
            self.db.remove_verified_tx(tx_hash)
4✔
418
            self.unverified_tx.pop(tx_hash, None)
4✔
419
            self.unconfirmed_tx.pop(tx_hash, None)
4✔
420
            if tx:
4✔
421
                for idx, txo in enumerate(tx.outputs()):
4✔
422
                    scripthash = bitcoin.script_to_scripthash(txo.scriptpubkey)
4✔
423
                    prevout = TxOutpoint(bfh(tx_hash), idx)
4✔
424
                    self.db.remove_prevout_by_scripthash(scripthash, prevout=prevout, value=txo.value)
4✔
425
        util.trigger_callback('adb_removed_tx', self, tx_hash, tx)
4✔
426

427
    def get_depending_transactions(self, tx_hash: str) -> Set[str]:
4✔
428
        """Returns all (grand-)children of tx_hash in this wallet."""
429
        with self.transaction_lock:
4✔
430
            children = set()
4✔
431
            for n in self.db.get_spent_outpoints(tx_hash):
4✔
432
                other_hash = self.db.get_spent_outpoint(tx_hash, n)
×
433
                children.add(other_hash)
×
434
                children |= self.get_depending_transactions(other_hash)
×
435
            return children
4✔
436

437
    def receive_tx_callback(self, tx: Transaction, tx_height: int) -> None:
4✔
438
        txid = tx.txid()
4✔
439
        assert txid is not None
4✔
440
        self.add_unverified_or_unconfirmed_tx(txid, tx_height)
4✔
441
        self.add_transaction(tx, allow_unrelated=True)
4✔
442

443
    def receive_history_callback(self, addr: str, hist, tx_fees: Dict[str, int]):
4✔
444
        with self.lock:
4✔
445
            old_hist = self.get_address_history(addr)
4✔
446
            for tx_hash, height in old_hist.items():
4✔
447
                if (tx_hash, height) not in hist:
4✔
448
                    # make tx local
449
                    self.unverified_tx.pop(tx_hash, None)
4✔
450
                    self.unconfirmed_tx.pop(tx_hash, None)
4✔
451
                    self.db.remove_verified_tx(tx_hash)
4✔
452
                    if self.verifier:
4✔
UNCOV
453
                        self.verifier.remove_spv_proof_for_tx(tx_hash)
×
454
            self.db.set_addr_history(addr, hist)
4✔
455

456
        for tx_hash, tx_height in hist:
4✔
457
            # add it in case it was previously unconfirmed
458
            self.add_unverified_or_unconfirmed_tx(tx_hash, tx_height)
4✔
459
            # if addr is new, we have to recompute txi and txo
460
            tx = self.db.get_transaction(tx_hash)
4✔
461
            if tx is None:
4✔
UNCOV
462
                continue
×
463
            self.add_transaction(tx, allow_unrelated=True, is_new=False)
4✔
464
            # if we already had this tx, see if its height changed (e.g. local->unconfirmed)
465
            old_height = old_hist.get(tx_hash, None)
4✔
466
            if old_height is not None and old_height != tx_height:
4✔
467
                util.trigger_callback('adb_tx_height_changed', self, tx_hash, old_height, tx_height)
4✔
468

469
        # Store fees
470
        for tx_hash, fee_sat in tx_fees.items():
4✔
UNCOV
471
            self.db.add_tx_fee_from_server(tx_hash, fee_sat)
×
472

473
    @profiler
4✔
474
    def load_local_history(self):
4✔
475
        self._history_local = {}  # type: Dict[str, Set[str]]  # address -> set(txid)
4✔
476
        self._address_history_changed_events = defaultdict(asyncio.Event)  # address -> Event
4✔
477
        for txid in itertools.chain(self.db.list_txi(), self.db.list_txo()):
4✔
478
            self._add_tx_to_local_history(txid)
4✔
479

480
    @profiler
4✔
481
    def check_history(self):
4✔
482
        hist_addrs_mine = list(filter(lambda k: self.is_mine(k), self.db.get_history()))
4✔
483
        hist_addrs_not_mine = list(filter(lambda k: not self.is_mine(k), self.db.get_history()))
4✔
484
        for addr in hist_addrs_not_mine:
4✔
UNCOV
485
            self.db.remove_addr_history(addr)
×
486
        for addr in hist_addrs_mine:
4✔
487
            hist = self.db.get_addr_history(addr)
4✔
488
            for tx_hash, tx_height in hist:
4✔
489
                if self.db.get_txi_addresses(tx_hash) or self.db.get_txo_addresses(tx_hash):
4✔
490
                    continue
4✔
491
                tx = self.db.get_transaction(tx_hash)
4✔
492
                if tx is not None:
4✔
UNCOV
493
                    self.add_transaction(tx, allow_unrelated=True)
×
494

495
    def remove_local_transactions_we_dont_have(self):
4✔
496
        for txid in itertools.chain(self.db.list_txi(), self.db.list_txo()):
4✔
497
            tx_height = self.get_tx_height(txid).height
4✔
498
            if tx_height == TX_HEIGHT_LOCAL and not self.db.get_transaction(txid):
4✔
UNCOV
499
                self.remove_transaction(txid)
×
500

501
    def clear_history(self):
4✔
UNCOV
502
        with self.lock:
×
UNCOV
503
            with self.transaction_lock:
×
504
                self.db.clear_history()
×
505
                self._history_local.clear()
×
506
                self._get_balance_cache.clear()  # invalidate cache
×
507

508
    def _get_tx_sort_key(self, tx_hash: str) -> Tuple[int, int]:
4✔
509
        """Returns a key to be used for sorting txs."""
510
        with self.lock:
4✔
511
            tx_mined_info = self.get_tx_height(tx_hash)
4✔
512
            height = self.tx_height_to_sort_height(tx_mined_info.height)
4✔
513
            txpos = tx_mined_info.txpos or -1
4✔
514
            return height, txpos
4✔
515

516
    @classmethod
4✔
517
    def tx_height_to_sort_height(cls, height: int = None):
4✔
518
        """Return a height-like value to be used for sorting txs."""
519
        if height is not None:
4✔
520
            if height > 0:
4✔
UNCOV
521
                return height
×
522
            if height == TX_HEIGHT_UNCONFIRMED:
4✔
523
                return TX_HEIGHT_INF
4✔
UNCOV
524
            if height == TX_HEIGHT_UNCONF_PARENT:
×
UNCOV
525
                return TX_HEIGHT_INF + 1
×
526
            if height == TX_HEIGHT_FUTURE:
×
527
                return TX_HEIGHT_INF + 2
×
528
            if height == TX_HEIGHT_LOCAL:
×
529
                return TX_HEIGHT_INF + 3
×
530
        return TX_HEIGHT_INF + 100
×
531

532
    def with_local_height_cached(func):
4✔
533
        # get local height only once, as it's relatively expensive.
534
        # take care that nested calls work as expected
535
        def f(self, *args, **kwargs):
4✔
536
            orig_val = getattr(self.threadlocal_cache, 'local_height', None)
4✔
537
            self.threadlocal_cache.local_height = orig_val or self.get_local_height()
4✔
538
            try:
4✔
539
                return func(self, *args, **kwargs)
4✔
540
            finally:
541
                self.threadlocal_cache.local_height = orig_val
4✔
542
        return f
4✔
543

544
    @with_lock
4✔
545
    @with_transaction_lock
4✔
546
    @with_local_height_cached
4✔
547
    def get_history(self, domain) -> Sequence[HistoryItem]:
4✔
548
        domain = set(domain)
4✔
549
        # 1. Get the history of each address in the domain, maintain the
550
        #    delta of a tx as the sum of its deltas on domain addresses
551
        tx_deltas = defaultdict(int)  # type: Dict[str, int]
4✔
552
        for addr in domain:
4✔
553
            h = self.get_address_history(addr).items()
4✔
554
            for tx_hash, height in h:
4✔
555
                tx_deltas[tx_hash] += self.get_tx_delta(tx_hash, addr)
4✔
556
        # 2. create sorted history
557
        history = []
4✔
558
        for tx_hash in tx_deltas:
4✔
559
            delta = tx_deltas[tx_hash]
4✔
560
            tx_mined_status = self.get_tx_height(tx_hash)
4✔
561
            fee = self.get_tx_fee(tx_hash)
4✔
562
            history.append((tx_hash, tx_mined_status, delta, fee))
4✔
563
        history.sort(key = lambda x: self._get_tx_sort_key(x[0]))
4✔
564
        # 3. add balance
565
        h2 = []
4✔
566
        balance = 0
4✔
567
        for tx_hash, tx_mined_status, delta, fee in history:
4✔
568
            balance += delta
4✔
569
            h2.append(HistoryItem(
4✔
570
                txid=tx_hash,
571
                tx_mined_status=tx_mined_status,
572
                delta=delta,
573
                fee=fee,
574
                balance=balance))
575
        # sanity check
576
        c, u, x = self.get_balance(domain)
4✔
577
        if balance != c + u + x:
4✔
UNCOV
578
            self.logger.error(f'sanity check failed! c={c},u={u},x={x} while history balance={balance}')
×
UNCOV
579
            raise Exception("wallet.get_history() failed balance sanity-check")
×
580
        return h2
4✔
581

582
    def _add_tx_to_local_history(self, txid):
4✔
583
        with self.transaction_lock:
4✔
584
            for addr in itertools.chain(self.db.get_txi_addresses(txid), self.db.get_txo_addresses(txid)):
4✔
585
                cur_hist = self._history_local.get(addr, set())
4✔
586
                cur_hist.add(txid)
4✔
587
                self._history_local[addr] = cur_hist
4✔
588
                self._mark_address_history_changed(addr)
4✔
589

590
    def _remove_tx_from_local_history(self, txid):
4✔
591
        with self.transaction_lock:
4✔
592
            for addr in itertools.chain(self.db.get_txi_addresses(txid), self.db.get_txo_addresses(txid)):
4✔
593
                cur_hist = self._history_local.get(addr, set())
4✔
594
                try:
4✔
595
                    cur_hist.remove(txid)
4✔
UNCOV
596
                except KeyError:
×
UNCOV
597
                    pass
×
598
                else:
599
                    self._history_local[addr] = cur_hist
4✔
600
                    self._mark_address_history_changed(addr)
4✔
601

602
    def _mark_address_history_changed(self, addr: str) -> None:
4✔
603
        def set_and_clear():
4✔
604
            event = self._address_history_changed_events[addr]
4✔
605
            # history for this address changed, wake up coroutines:
606
            event.set()
4✔
607
            # clear event immediately so that coroutines can wait() for the next change:
608
            event.clear()
4✔
609
        if self.asyncio_loop:
4✔
610
            self.asyncio_loop.call_soon_threadsafe(set_and_clear)
4✔
611

612
    async def wait_for_address_history_to_change(self, addr: str) -> None:
4✔
613
        """Wait until the server tells us about a new transaction related to addr.
614

615
        Unconfirmed and confirmed transactions are not distinguished, and so e.g. SPV
616
        is not taken into account.
617
        """
UNCOV
618
        assert self.is_mine(addr), "address needs to be is_mine to be watched"
×
UNCOV
619
        await self._address_history_changed_events[addr].wait()
×
620

621
    def add_unverified_or_unconfirmed_tx(self, tx_hash, tx_height):
4✔
622
        if self.db.is_in_verified_tx(tx_hash):
4✔
623
            if tx_height <= 0:
4✔
624
                # tx was previously SPV-verified but now in mempool (probably reorg)
UNCOV
625
                with self.lock:
×
UNCOV
626
                    self.db.remove_verified_tx(tx_hash)
×
627
                    self.unconfirmed_tx[tx_hash] = tx_height
×
628
                if self.verifier:
×
629
                    self.verifier.remove_spv_proof_for_tx(tx_hash)
×
630
        else:
631
            with self.lock:
4✔
632
                if tx_height > 0:
4✔
633
                    self.unverified_tx[tx_hash] = tx_height
4✔
634
                else:
635
                    self.unconfirmed_tx[tx_hash] = tx_height
4✔
636

637
    def remove_unverified_tx(self, tx_hash, tx_height):
4✔
UNCOV
638
        with self.lock:
×
UNCOV
639
            new_height = self.unverified_tx.get(tx_hash)
×
640
            if new_height == tx_height:
×
641
                self.unverified_tx.pop(tx_hash, None)
×
642

643
    def add_verified_tx(self, tx_hash: str, info: TxMinedInfo):
4✔
644
        # Remove from the unverified map and add to the verified map
645
        with self.lock:
4✔
646
            self.unverified_tx.pop(tx_hash, None)
4✔
647
            self.db.add_verified_tx(tx_hash, info)
4✔
648
        util.trigger_callback('adb_added_verified_tx', self, tx_hash)
4✔
649

650
    def get_unverified_txs(self) -> Dict[str, int]:
4✔
651
        '''Returns a map from tx hash to transaction height'''
UNCOV
652
        with self.lock:
×
UNCOV
653
            return dict(self.unverified_tx)  # copy
×
654

655
    def undo_verifications(self, blockchain: Blockchain, above_height: int) -> Set[str]:
4✔
656
        '''Used by the verifier when a reorg has happened'''
UNCOV
657
        txs = set()
×
UNCOV
658
        with self.lock:
×
659
            for tx_hash in self.db.list_verified_tx():
×
660
                info = self.db.get_verified_tx(tx_hash)
×
661
                tx_height = info.height
×
662
                if tx_height > above_height:
×
663
                    header = blockchain.read_header(tx_height)
×
664
                    if not header or hash_header(header) != info.header_hash:
×
665
                        self.db.remove_verified_tx(tx_hash)
×
666
                        # NOTE: we should add these txns to self.unverified_tx,
667
                        # but with what height?
668
                        # If on the new fork after the reorg, the txn is at the
669
                        # same height, we will not get a status update for the
670
                        # address. If the txn is not mined or at a diff height,
671
                        # we should get a status update. Unless we put tx into
672
                        # unverified_tx, it will turn into local. So we put it
673
                        # into unverified_tx with the old height, and if we get
674
                        # a status update, that will overwrite it.
UNCOV
675
                        self.unverified_tx[tx_hash] = tx_height
×
UNCOV
676
                        txs.add(tx_hash)
×
677

678
        for tx_hash in txs:
×
UNCOV
679
            util.trigger_callback('adb_removed_verified_tx', self, tx_hash)
×
680
        return txs
×
681

682
    def get_local_height(self) -> int:
4✔
683
        """ return last known height if we are offline """
684
        cached_local_height = getattr(self.threadlocal_cache, 'local_height', None)
4✔
685
        if cached_local_height is not None:
4✔
686
            return cached_local_height
4✔
687
        return self.network.get_local_height() if self.network else self.db.get('stored_height', 0)
4✔
688

689
    def set_future_tx(self, txid: str, *, wanted_height: int):
4✔
690
        """Mark a local tx as "future" (encumbered by a timelock).
691
        wanted_height is the min (abs) block height at which the tx can get into the mempool (be broadcast).
692
                      note: tx becomes consensus-valid to be mined in a block at height wanted_height+1
693
        In case of a CSV-locked tx with unconfirmed inputs, the wanted_height is a best-case guess.
694
        """
UNCOV
695
        with self.lock:
×
UNCOV
696
            old_height = self.future_tx.get(txid) or None
×
697
            self.future_tx[txid] = wanted_height
×
698
        if old_height != wanted_height:
×
699
            util.trigger_callback('adb_set_future_tx', self, txid)
×
700

701
    def get_tx_height(self, tx_hash: str) -> TxMinedInfo:
4✔
702
        if tx_hash is None:  # ugly backwards compat...
4✔
UNCOV
703
            return TxMinedInfo(height=TX_HEIGHT_LOCAL, conf=0)
×
704
        with self.lock:
4✔
705
            verified_tx_mined_info = self.db.get_verified_tx(tx_hash)
4✔
706
            if verified_tx_mined_info:
4✔
707
                conf = max(self.get_local_height() - verified_tx_mined_info.height + 1, 0)
4✔
708
                return verified_tx_mined_info._replace(conf=conf)
4✔
709
            elif tx_hash in self.unverified_tx:
4✔
710
                height = self.unverified_tx[tx_hash]
4✔
711
                return TxMinedInfo(height=height, conf=0)
4✔
712
            elif tx_hash in self.unconfirmed_tx:
4✔
713
                height = self.unconfirmed_tx[tx_hash]
4✔
714
                return TxMinedInfo(height=height, conf=0)
4✔
715
            elif wanted_height := self.future_tx.get(tx_hash):
4✔
UNCOV
716
                if wanted_height > self.get_local_height():
×
UNCOV
717
                    return TxMinedInfo(height=TX_HEIGHT_FUTURE, conf=0, wanted_height=wanted_height)
×
718
                else:
719
                    return TxMinedInfo(height=TX_HEIGHT_LOCAL, conf=0)
×
720
            else:
721
                # local transaction
722
                return TxMinedInfo(height=TX_HEIGHT_LOCAL, conf=0)
4✔
723

724
    def up_to_date_changed(self) -> None:
4✔
725
        # fire triggers
726
        util.trigger_callback('adb_set_up_to_date', self)
4✔
727

728
    def is_up_to_date(self):
4✔
729
        if not self.synchronizer or not self.verifier:
4✔
730
            return False
4✔
731
        return self.synchronizer.is_up_to_date() and self.verifier.is_up_to_date()
4✔
732

733
    def reset_netrequest_counters(self) -> None:
4✔
UNCOV
734
        if self.synchronizer:
×
UNCOV
735
            self.synchronizer.reset_request_counters()
×
736
        if self.verifier:
×
737
            self.verifier.reset_request_counters()
×
738

739
    def get_history_sync_state_details(self) -> Tuple[int, int]:
4✔
UNCOV
740
        nsent, nans = 0, 0
×
UNCOV
741
        if self.synchronizer:
×
742
            n1, n2 = self.synchronizer.num_requests_sent_and_answered()
×
743
            nsent += n1
×
744
            nans += n2
×
745
        if self.verifier:
×
746
            n1, n2 = self.verifier.num_requests_sent_and_answered()
×
747
            nsent += n1
×
748
            nans += n2
×
749
        return nsent, nans
×
750

751
    @with_transaction_lock
4✔
752
    def get_tx_delta(self, tx_hash: str, address: str) -> int:
4✔
753
        """effect of tx on address"""
754
        delta = 0
4✔
755
        # subtract the value of coins sent from address
756
        d = self.db.get_txi_addr(tx_hash, address)
4✔
757
        for n, v in d:
4✔
758
            delta -= v
4✔
759
        # add the value of the coins received at address
760
        d = self.db.get_txo_addr(tx_hash, address)
4✔
761
        for n, (v, cb) in d.items():
4✔
762
            delta += v
4✔
763
        return delta
4✔
764

765
    def get_tx_fee(self, txid: str) -> Optional[int]:
4✔
766
        """Returns tx_fee or None. Use server fee only if tx is unconfirmed and not mine.
767

768
        Note: being fast is prioritised over completeness here. We try to avoid deserializing
769
              the tx, as that is expensive if we are called for the whole history. We sometimes
770
              incorrectly early-exit and return None, e.g. for not-all-ismine-input txs,
771
              where we could calculate the fee if we deserialized (but to see if we have all
772
              the parent txs available, we would have to deserialize first).
773
              More expensive but more complete alternative: wallet.get_tx_info(tx).fee
774
        """
775
        # check if stored fee is available
776
        fee = self.db.get_tx_fee(txid, trust_server=False)
4✔
777
        if fee is not None:
4✔
UNCOV
778
            return fee
×
779
        # delete server-sent fee for confirmed txns
780
        confirmed = self.get_tx_height(txid).conf > 0
4✔
781
        if confirmed:
4✔
UNCOV
782
            self.db.add_tx_fee_from_server(txid, None)
×
783
        # if all inputs are ismine, try to calc fee now;
784
        # otherwise, return stored value
785
        num_all_inputs = self.db.get_num_all_inputs_of_tx(txid)
4✔
786
        if num_all_inputs is not None:
4✔
787
            # check if tx is mine
788
            num_ismine_inputs = self.db.get_num_ismine_inputs_of_tx(txid)
4✔
789
            assert num_ismine_inputs <= num_all_inputs, (num_ismine_inputs, num_all_inputs)
4✔
790
            # trust server if tx is unconfirmed and not mine
791
            if num_ismine_inputs < num_all_inputs:
4✔
792
                return None if confirmed else self.db.get_tx_fee(txid, trust_server=True)
4✔
793
        # lookup tx and deserialize it.
794
        # note that deserializing is expensive, hence above hacks
795
        tx = self.db.get_transaction(txid)
4✔
796
        if not tx:
4✔
UNCOV
797
            return None
×
798
        # compute fee if possible
799
        v_in = v_out = 0
4✔
800
        with self.lock, self.transaction_lock:
4✔
801
            for txin in tx.inputs():
4✔
802
                addr = self.get_txin_address(txin)
4✔
803
                value = self.get_txin_value(txin, address=addr)
4✔
804
                if value is None:
4✔
UNCOV
805
                    v_in = None
×
806
                elif v_in is not None:
4✔
807
                    v_in += value
4✔
808
            for txout in tx.outputs():
4✔
809
                v_out += txout.value
4✔
810
        if v_in is not None:
4✔
811
            fee = v_in - v_out
4✔
812
        else:
UNCOV
813
            fee = None
×
814
        # save result
815
        self.db.add_tx_fee_we_calculated(txid, fee)
4✔
816
        self.db.add_num_inputs_to_tx(txid, len(tx.inputs()))
4✔
817
        return fee
4✔
818

819
    def get_addr_io(self, address: str):
4✔
820
        with self.lock, self.transaction_lock:
4✔
821
            h = self.get_address_history(address).items()
4✔
822
            received = {}
4✔
823
            sent = {}
4✔
824
            for tx_hash, height in h:
4✔
825
                tx_mined_info = self.get_tx_height(tx_hash)
4✔
826
                txpos = tx_mined_info.txpos if tx_mined_info.txpos is not None else -1
4✔
827
                d = self.db.get_txo_addr(tx_hash, address)
4✔
828
                for n, (v, is_cb) in d.items():
4✔
829
                    received[tx_hash + ':%d'%n] = (height, txpos, v, is_cb)
4✔
830
                l = self.db.get_txi_addr(tx_hash, address)
4✔
831
                for txi, v in l:
4✔
832
                    sent[txi] = tx_hash, height, txpos
4✔
833
        return received, sent
4✔
834

835
    def get_addr_outputs(self, address: str) -> Dict[TxOutpoint, PartialTxInput]:
4✔
836
        received, sent = self.get_addr_io(address)
4✔
837
        out = {}
4✔
838
        for prevout_str, v in received.items():
4✔
839
            tx_height, tx_pos, value, is_cb = v
4✔
840
            prevout = TxOutpoint.from_str(prevout_str)
4✔
841
            utxo = PartialTxInput(prevout=prevout, is_coinbase_output=is_cb)
4✔
842
            utxo._trusted_address = address
4✔
843
            utxo._trusted_value_sats = value
4✔
844
            utxo.block_height = tx_height
4✔
845
            utxo.block_txpos = tx_pos
4✔
846
            if prevout_str in sent:
4✔
847
                txid, height, pos = sent[prevout_str]
4✔
848
                utxo.spent_txid = txid
4✔
849
                utxo.spent_height = height
4✔
850
            else:
851
                utxo.spent_txid = None
4✔
852
                utxo.spent_height = None
4✔
853
            out[prevout] = utxo
4✔
854
        return out
4✔
855

856
    def get_addr_utxo(self, address: str) -> Dict[TxOutpoint, PartialTxInput]:
4✔
857
        out = self.get_addr_outputs(address)
4✔
858
        for k, v in list(out.items()):
4✔
859
            if v.spent_height is not None:
4✔
UNCOV
860
                out.pop(k)
×
861
        return out
4✔
862

863
    # return the total amount ever received by an address
864
    def get_addr_received(self, address):
4✔
UNCOV
865
        received, sent = self.get_addr_io(address)
×
UNCOV
866
        return sum([value for height, pos, value, is_cb in received.values()])
×
867

868
    @with_lock
4✔
869
    @with_transaction_lock
4✔
870
    @with_local_height_cached
4✔
871
    def get_balance(self, domain, *, excluded_addresses: Set[str] = None,
4✔
872
                    excluded_coins: Set[str] = None) -> Tuple[int, int, int]:
873
        """Return the balance of a set of addresses:
874
        confirmed and matured, unconfirmed, unmatured
875
        Note: intended for display-purposes. would need extreme care for "has enough funds" checks (see #8835)
876
        """
877
        if excluded_addresses is None:
4✔
878
            excluded_addresses = set()
4✔
879
        assert isinstance(excluded_addresses, set), f"excluded_addresses should be set, not {type(excluded_addresses)}"
4✔
880
        domain = set(domain) - excluded_addresses
4✔
881
        if excluded_coins is None:
4✔
882
            excluded_coins = set()
4✔
883
        assert isinstance(excluded_coins, set), f"excluded_coins should be set, not {type(excluded_coins)}"
4✔
884

885
        cache_key = sha256(','.join(sorted(domain)) + ';'
4✔
886
                           + ','.join(sorted(excluded_coins)))
887
        cached_value = self._get_balance_cache.get(cache_key)
4✔
888
        if cached_value:
4✔
UNCOV
889
            return cached_value
×
890

891
        coins = {}
4✔
892
        for address in domain:
4✔
893
            coins.update(self.get_addr_outputs(address))
4✔
894

895
        c = u = x = 0
4✔
896
        mempool_height = self.get_local_height() + 1  # height of next block
4✔
897
        for utxo in coins.values():  # type: PartialTxInput
4✔
898
            if utxo.spent_height is not None:
4✔
899
                continue
4✔
900
            if utxo.prevout.to_str() in excluded_coins:
4✔
UNCOV
901
                continue
×
902
            v = utxo.value_sats()
4✔
903
            tx_height = utxo.block_height
4✔
904
            is_cb = utxo.is_coinbase_output()
4✔
905
            if is_cb and tx_height + COINBASE_MATURITY > mempool_height:
4✔
UNCOV
906
                x += v
×
907
            elif tx_height > 0:
4✔
908
                c += v
4✔
909
            else:
910
                txid = utxo.prevout.txid.hex()
4✔
911
                tx = self.db.get_transaction(txid)
4✔
912
                assert tx is not None # txid comes from get_addr_io
4✔
913
                # we look at the outputs that are spent by this transaction
914
                # if those outputs are ours and confirmed, we count this coin as confirmed
915
                confirmed_spent_amount = 0
4✔
916
                for txin in tx.inputs():
4✔
917
                    if txin.prevout in coins:
4✔
918
                        coin = coins[txin.prevout]
4✔
919
                        if coin.block_height > 0:
4✔
UNCOV
920
                            confirmed_spent_amount += coin.value_sats()
×
921
                # Compare amount, in case tx has confirmed and unconfirmed inputs, or is a coinjoin.
922
                # (fixme: tx may have multiple change outputs)
923
                if confirmed_spent_amount >= v:
4✔
UNCOV
924
                    c += v
×
925
                else:
926
                    c += confirmed_spent_amount
4✔
927
                    u += v - confirmed_spent_amount
4✔
928
        result = c, u, x
4✔
929
        # cache result.
930
        # Cache needs to be invalidated if a transaction is added to/
931
        # removed from history; or on new blocks (maturity...)
932
        self._get_balance_cache[cache_key] = result
4✔
933
        return result
4✔
934

935
    @with_local_height_cached
4✔
936
    def get_utxos(
4✔
937
            self,
938
            domain,
939
            *,
940
            excluded_addresses=None,
941
            mature_only: bool = False,
942
            confirmed_funding_only: bool = False,
943
            confirmed_spending_only: bool = False,
944
            nonlocal_only: bool = False,
945
            block_height: int = None,
946
    ) -> Sequence[PartialTxInput]:
947
        if block_height is not None:
4✔
948
            # caller wants the UTXOs we had at a given height; check other parameters
UNCOV
949
            assert confirmed_funding_only
×
UNCOV
950
            assert confirmed_spending_only
×
951
            assert nonlocal_only
×
952
        else:
953
            block_height = self.get_local_height()
4✔
954
        coins = []
4✔
955
        domain = set(domain)
4✔
956
        if excluded_addresses:
4✔
957
            domain = set(domain) - set(excluded_addresses)
4✔
958
        mempool_height = block_height + 1  # height of next block
4✔
959
        for addr in domain:
4✔
960
            txos = self.get_addr_outputs(addr)
4✔
961
            for txo in txos.values():
4✔
962
                if txo.spent_height is not None:
4✔
963
                    if not confirmed_spending_only:
4✔
964
                        continue
4✔
UNCOV
965
                    if confirmed_spending_only and 0 < txo.spent_height <= block_height:
×
UNCOV
966
                        continue
×
967
                if confirmed_funding_only and not (0 < txo.block_height <= block_height):
4✔
968
                    continue
4✔
969
                if nonlocal_only and txo.block_height in (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE):
4✔
UNCOV
970
                    continue
×
971
                if (mature_only and txo.is_coinbase_output()
4✔
972
                        and txo.block_height + COINBASE_MATURITY > mempool_height):
UNCOV
973
                    continue
×
974
                coins.append(txo)
4✔
975
                continue
4✔
976
        return coins
4✔
977

978
    def is_used(self, address: str) -> bool:
4✔
979
        """Whether any tx ever touched `address`."""
980
        return self.get_address_history_len(address) != 0
4✔
981

982
    def is_used_as_from_address(self, address: str) -> bool:
4✔
983
        """Whether any tx ever spent from `address`."""
UNCOV
984
        received, sent = self.get_addr_io(address)
×
UNCOV
985
        return len(sent) > 0
×
986

987
    def is_empty(self, address: str) -> bool:
4✔
UNCOV
988
        coins = self.get_addr_utxo(address)
×
UNCOV
989
        return not bool(coins)
×
990

991
    @with_local_height_cached
4✔
992
    def address_is_old(self, address: str, *, req_conf: int = 3) -> bool:
4✔
993
        """Returns whether address has any history that is deeply confirmed.
994
        Used for reorg-safe(ish) gap limit roll-forward.
995
        """
996
        max_conf = -1
4✔
997
        h = self.db.get_addr_history(address)
4✔
998
        needs_spv_check = not self.config.NETWORK_SKIPMERKLECHECK
4✔
999
        for tx_hash, tx_height in h:
4✔
1000
            if needs_spv_check:
4✔
1001
                tx_age = self.get_tx_height(tx_hash).conf
4✔
1002
            else:
1003
                if tx_height <= 0:
4✔
UNCOV
1004
                    tx_age = 0
×
1005
                else:
1006
                    tx_age = self.get_local_height() - tx_height + 1
4✔
1007
            max_conf = max(max_conf, tx_age)
4✔
1008
        return max_conf >= req_conf
4✔
1009

1010
    def get_spender(self, outpoint: str) -> str:
4✔
1011
        """
1012
        returns txid spending outpoint.
1013
        subscribes to addresses as a side effect.
1014
        """
UNCOV
1015
        prev_txid, index = outpoint.split(':')
×
UNCOV
1016
        spender_txid = self.db.get_spent_outpoint(prev_txid, int(index))
×
1017
        # discard local spenders
1018
        tx_mined_status = self.get_tx_height(spender_txid)
×
UNCOV
1019
        if tx_mined_status.height in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]:
×
1020
            spender_txid = None
×
1021
        if not spender_txid:
×
1022
            return
×
1023
        spender_tx = self.get_transaction(spender_txid)
×
1024
        for i, o in enumerate(spender_tx.outputs()):
×
1025
            if o.address is None:
×
1026
                continue
×
1027
            if not self.is_mine(o.address):
×
1028
                self.add_address(o.address)
×
1029
        return spender_txid
×
1030

1031
    def get_tx_mined_depth(self, txid: str):
4✔
UNCOV
1032
        if not txid:
×
UNCOV
1033
            return TxMinedDepth.FREE
×
1034
        tx_mined_depth = self.get_tx_height(txid)
×
1035
        height, conf = tx_mined_depth.height, tx_mined_depth.conf
×
1036
        if conf > 20:
×
1037
            return TxMinedDepth.DEEP
×
1038
        elif conf > 0:
×
1039
            return TxMinedDepth.SHALLOW
×
1040
        elif height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT):
×
1041
            return TxMinedDepth.MEMPOOL
×
1042
        elif height in (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE):
×
1043
            return TxMinedDepth.FREE
×
1044
        elif height > 0 and conf == 0:
×
1045
            # unverified but claimed to be mined
1046
            return TxMinedDepth.MEMPOOL
×
1047
        else:
1048
            raise NotImplementedError()
×
1049

1050
    def is_deeply_mined(self, txid):
4✔
UNCOV
1051
        return self.get_tx_mined_depth(txid) == TxMinedDepth.DEEP
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc