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

spesmilo / electrum / 5707590002278400

25 Apr 2025 10:46AM UTC coverage: 60.343% (+0.05%) from 60.296%
5707590002278400

Pull #9751

CirrusCI

ecdsa
fixes
Pull Request #9751: Txbatcher without password in memory

42 of 67 new or added lines in 4 files covered. (62.69%)

1196 existing lines in 9 files now uncovered.

21659 of 35893 relevant lines covered (60.34%)

3.01 hits per line

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

64.57
/electrum/wallet.py
1
# Electrum - lightweight Bitcoin client
2
# Copyright (C) 2015 Thomas Voegtlin
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
# Wallet classes:
25
#   - Imported_Wallet: imported addresses or single keys, 0 or 1 keystore
26
#   - Standard_Wallet: one HD keystore, P2PKH-like scripts
27
#   - Multisig_Wallet: several HD keystores, M-of-N OP_CHECKMULTISIG scripts
28

29
import os
5✔
30
import random
5✔
31
import time
5✔
32
import copy
5✔
33
import math
5✔
34
from functools import partial
5✔
35
from collections import defaultdict
5✔
36
from decimal import Decimal
5✔
37
from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence, Dict, Any, Set, Iterable, Mapping
5✔
38
from abc import ABC, abstractmethod
5✔
39
import itertools
5✔
40
import threading
5✔
41
import enum
5✔
42
import asyncio
5✔
43

44
import electrum_ecc as ecc
5✔
45
from aiorpcx import ignore_after, run_in_thread
5✔
46

47
from .i18n import _
5✔
48
from .bip32 import BIP32Node, convert_bip32_intpath_to_strpath, convert_bip32_strpath_to_intpath
5✔
49
from . import util
5✔
50
from .lntransport import extract_nodeid
5✔
51
from .util import (
5✔
52
    NotEnoughFunds, UserCancelled, profiler, OldTaskGroup, format_fee_satoshis,
53
    WalletFileException, BitcoinException, InvalidPassword, format_time, timestamp_to_datetime,
54
    Satoshis, Fiat, TxMinedInfo, quantize_feerate, OrderedDictWithIndex
55
)
56
from .simple_config import SimpleConfig
5✔
57
from .fee_policy import FeePolicy, FixedFeePolicy, FEE_RATIO_HIGH_WARNING, FEERATE_WARNING_HIGH_FEE
5✔
58
from .lnutil import MIN_FUNDING_SAT
5✔
59
from .bitcoin import COIN, is_address, is_minikey, relayfee, dust_threshold
5✔
60
from .bitcoin import DummyAddress, DummyAddressUsedInTxException
5✔
61
from . import keystore
5✔
62
from .keystore import (load_keystore, Hardware_KeyStore, KeyStore, KeyStoreWithMPK,
5✔
63
                       AddressIndexGeneric, CannotDerivePubkey)
64
from .util import multisig_type, parse_max_spend
5✔
65
from .storage import StorageEncryptionVersion, WalletStorage
5✔
66
from .wallet_db import WalletDB
5✔
67
from . import transaction, bitcoin, coinchooser, bip32
5✔
68
from .transaction import (
5✔
69
    Transaction, TxInput, TxOutput, PartialTransaction, PartialTxInput,
70
    PartialTxOutput, TxOutpoint, Sighash
71
)
72
from .plugin import run_hook
5✔
73
from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
5✔
74
                                   TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE, TX_TIMESTAMP_INF)
75
from .invoices import BaseInvoice, Invoice, Request
5✔
76
from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_UNCONFIRMED, PR_INFLIGHT
5✔
77
from .contacts import Contacts
5✔
78
from .mnemonic import Mnemonic
5✔
79
from .logging import get_logger, Logger
5✔
80
from .lnworker import LNWallet
5✔
81
from .util import read_json_file, write_json_file, UserFacingException, FileImportFailed
5✔
82
from .util import EventListener, event_listener
5✔
83
from . import descriptor
5✔
84
from .descriptor import Descriptor
5✔
85
from .util import OnchainHistoryItem
5✔
86
from .txbatcher import TxBatcher
5✔
87

88
if TYPE_CHECKING:
5✔
UNCOV
89
    from .network import Network
×
UNCOV
90
    from .exchange_rate import FxThread
×
UNCOV
91
    from .submarine_swaps import SwapData
×
UNCOV
92
    from .lnchannel import AbstractChannel
×
UNCOV
93
    from .lnsweep import SweepInfo
×
94

95

96
_logger = get_logger(__name__)
5✔
97

98
TX_STATUS = [
5✔
99
    _('Unconfirmed'),
100
    _('Unconfirmed parent'),
101
    _('Not Verified'),
102
    _('Local'),
103
]
104

105

106
async def _append_utxos_to_inputs(
5✔
107
    *,
108
    inputs: List[PartialTxInput],
109
    network: 'Network',
110
    script_descriptor: 'descriptor.Descriptor',
111
    imax: int,
112
) -> None:
113
    script = script_descriptor.expand().output_script
5✔
114
    scripthash = bitcoin.script_to_scripthash(script)
5✔
115

116
    async def append_single_utxo(item):
5✔
117
        prev_tx_raw = await network.get_transaction(item['tx_hash'])
5✔
118
        prev_tx = Transaction(prev_tx_raw)
5✔
119
        prev_txout = prev_tx.outputs()[item['tx_pos']]
5✔
120
        if scripthash != bitcoin.script_to_scripthash(prev_txout.scriptpubkey):
5✔
UNCOV
121
            raise Exception('scripthash mismatch when sweeping')
×
122
        prevout_str = item['tx_hash'] + ':%d' % item['tx_pos']
5✔
123
        prevout = TxOutpoint.from_str(prevout_str)
5✔
124
        txin = PartialTxInput(prevout=prevout)
5✔
125
        txin.utxo = prev_tx
5✔
126
        txin.block_height = int(item['height'])
5✔
127
        txin.script_descriptor = script_descriptor
5✔
128
        inputs.append(txin)
5✔
129

130
    u = await network.listunspent_for_scripthash(scripthash)
5✔
131
    async with OldTaskGroup() as group:
5✔
132
        for item in u:
5✔
133
            if len(inputs) >= imax:
5✔
UNCOV
134
                break
×
135
            await group.spawn(append_single_utxo(item))
5✔
136

137

138
async def sweep_preparations(
5✔
139
    privkeys: Iterable[str], network: 'Network', imax=100,
140
) -> Tuple[Sequence[PartialTxInput], Mapping[bytes, bytes]]:
141

142
    async def find_utxos_for_privkey(txin_type: str, privkey: bytes, compressed: bool):
5✔
143
        pubkey = ecc.ECPrivkey(privkey).get_public_key_bytes(compressed=compressed)
5✔
144
        desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=txin_type)
5✔
145
        await _append_utxos_to_inputs(
5✔
146
            inputs=inputs,
147
            network=network,
148
            script_descriptor=desc,
149
            imax=imax)
150
        keypairs[pubkey] = privkey
5✔
151

152
    inputs = []  # type: List[PartialTxInput]
5✔
153
    keypairs = {}  # type: Dict[bytes, bytes]
5✔
154
    async with OldTaskGroup() as group:
5✔
155
        for sec in privkeys:
5✔
156
            txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)
5✔
157
            await group.spawn(find_utxos_for_privkey(txin_type, privkey, compressed))
5✔
158
            # do other lookups to increase support coverage
159
            if is_minikey(sec):
5✔
160
                # minikeys don't have a compressed byte
161
                # we lookup both compressed and uncompressed pubkeys
UNCOV
162
                await group.spawn(find_utxos_for_privkey(txin_type, privkey, not compressed))
×
163
            elif txin_type == 'p2pkh':
5✔
164
                # WIF serialization does not distinguish p2pkh and p2pk
165
                # we also search for pay-to-pubkey outputs
166
                await group.spawn(find_utxos_for_privkey('p2pk', privkey, compressed))
5✔
167
    if not inputs:
5✔
UNCOV
168
        raise UserFacingException(_('No inputs found.'))
×
169
    return inputs, keypairs
5✔
170

171

172
async def sweep(
5✔
173
        privkeys: Iterable[str],
174
        *,
175
        network: 'Network',
176
        to_address: str,
177
        fee_policy: FeePolicy,
178
        imax=100,
179
        locktime=None,
180
        tx_version=None) -> PartialTransaction:
181

182
    inputs, keypairs = await sweep_preparations(privkeys, network, imax)
5✔
183
    total = sum(txin.value_sats() for txin in inputs)
5✔
184
    outputs = [PartialTxOutput(scriptpubkey=bitcoin.address_to_script(to_address), value=total)]
5✔
185
    tx = PartialTransaction.from_io(inputs, outputs)
5✔
186
    fee = fee_policy.estimate_fee(tx.estimated_size(), network=network)
5✔
187
    if total - fee < 0:
5✔
UNCOV
188
        raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d'%(total, fee))
×
189
    if total - fee < dust_threshold(network):
5✔
UNCOV
190
        raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d\nDust Threshold: %d'%(total, fee, dust_threshold(network)))
×
191
    outputs = [PartialTxOutput(scriptpubkey=bitcoin.address_to_script(to_address), value=total - fee)]
5✔
192
    if locktime is None:
5✔
UNCOV
193
        locktime = get_locktime_for_new_transaction(network)
×
194
    tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime, version=tx_version)
5✔
195
    tx.set_rbf(True)
5✔
196
    tx.sign(keypairs)
5✔
197
    return tx
5✔
198

199

200
def get_locktime_for_new_transaction(
5✔
201
    network: 'Network',
202
    *,
203
    include_random_component: bool = True,
204
) -> int:
205
    # if no network or not up to date, just set locktime to zero
206
    if not network:
5✔
207
        return 0
5✔
208
    chain = network.blockchain()
5✔
209
    if chain.is_tip_stale():
5✔
210
        return 0
5✔
211
    # figure out current block height
212
    chain_height = chain.height()  # learnt from all connected servers, SPV-checked
×
UNCOV
213
    server_height = network.get_server_height()  # height claimed by main server, unverified
×
214
    # note: main server might be lagging (either is slow, is malicious, or there is an SPV-invisible-hard-fork)
215
    #       - if it's lagging too much, it is the network's job to switch away
216
    if server_height < chain_height - 10:
×
217
        # the diff is suspiciously large... give up and use something non-fingerprintable
UNCOV
218
        return 0
×
219
    # discourage "fee sniping"
220
    locktime = min(chain_height, server_height)
×
221
    # sometimes pick locktime a bit further back, to help privacy
222
    # of setups that need more time (offline/multisig/coinjoin/...)
223
    if include_random_component:
×
UNCOV
224
        if random.randint(0, 9) == 0:
×
UNCOV
225
            locktime = max(0, locktime - random.randint(0, 99))
×
UNCOV
226
    locktime = max(0, locktime)
×
UNCOV
227
    return locktime
×
228

229

230
class CannotRBFTx(Exception): pass
5✔
231

232

233
class CannotBumpFee(CannotRBFTx):
5✔
234
    def __str__(self):
5✔
UNCOV
235
        return _('Cannot bump fee') + ':\n\n' + Exception.__str__(self)
×
236

237

238
class CannotDoubleSpendTx(CannotRBFTx):
5✔
239
    def __str__(self):
5✔
UNCOV
240
        return _('Cannot cancel transaction') + ':\n\n' + Exception.__str__(self)
×
241

242

243
class CannotCPFP(Exception):
5✔
244
    def __str__(self):
5✔
UNCOV
245
        return _('Cannot create child transaction') + ':\n\n' + Exception.__str__(self)
×
246

247

248
class InternalAddressCorruption(Exception):
5✔
249
    def __str__(self):
5✔
UNCOV
250
        return _("Wallet file corruption detected. "
×
251
                 "Please restore your wallet from seed, and compare the addresses in both files")
252

253

254
class TransactionPotentiallyDangerousException(Exception): pass
5✔
255

256

257
class TransactionDangerousException(TransactionPotentiallyDangerousException): pass
5✔
258

259

260
class TxSighashRiskLevel(enum.IntEnum):
5✔
261
    # higher value -> more risk
262
    SAFE = 0
5✔
263
    FEE_WARNING_SKIPCONFIRM = 1  # show warning icon (ignored for CLI)
5✔
264
    FEE_WARNING_NEEDCONFIRM = 2  # prompt user for confirmation
5✔
265
    WEIRD_SIGHASH = 3            # prompt user for confirmation
5✔
266
    INSANE_SIGHASH = 4           # reject
5✔
267

268

269
class TxSighashDanger:
5✔
270

271
    def __init__(
5✔
272
        self,
273
        *,
274
        risk_level: TxSighashRiskLevel = TxSighashRiskLevel.SAFE,
275
        short_message: str = None,
276
        messages: List[str] = None,
277
    ):
278
        self.risk_level = risk_level
5✔
279
        self.short_message = short_message
5✔
280
        self._messages = messages or []
5✔
281

282
    def needs_confirm(self) -> bool:
5✔
283
        """If True, the user should be prompted for explicit confirmation before signing."""
284
        return self.risk_level >= TxSighashRiskLevel.FEE_WARNING_NEEDCONFIRM
5✔
285

286
    def needs_reject(self) -> bool:
5✔
287
        """If True, the transaction should be rejected, i.e. abort signing."""
288
        return self.risk_level >= TxSighashRiskLevel.INSANE_SIGHASH
5✔
289

290
    def get_long_message(self) -> str:
5✔
291
        """Returns a description of the potential dangers of signing the tx that can be shown to the user.
292
        Empty string if there are none.
293
        """
294
        if self.short_message:
5✔
295
            header = [self.short_message]
5✔
296
        else:
UNCOV
297
            header = []
×
298
        return "\n".join(header + self._messages)
5✔
299

300
    def combine(*args: 'TxSighashDanger') -> 'TxSighashDanger':
5✔
301
        max_danger = max(args, key=lambda sighash_danger: sighash_danger.risk_level)  # type: TxSighashDanger
5✔
302
        messages = [msg for sighash_danger in args for msg in sighash_danger._messages]
5✔
303
        return TxSighashDanger(
5✔
304
            risk_level=max_danger.risk_level,
305
            short_message=max_danger.short_message,
306
            messages=messages,
307
        )
308

309
    def __repr__(self):
5✔
UNCOV
310
        return (f"<{self.__class__.__name__} risk_level={self.risk_level} "
×
311
                f"short_message={self.short_message!r} _messages={self._messages!r}>")
312

313

314
class BumpFeeStrategy(enum.Enum):
5✔
315
    PRESERVE_PAYMENT = enum.auto()
5✔
316
    DECREASE_PAYMENT = enum.auto()
5✔
317

318
    @classmethod
5✔
319
    def all(cls) -> Sequence['BumpFeeStrategy']:
5✔
320
        return list(BumpFeeStrategy.__members__.values())
×
321

322
    def text(self) -> str:
5✔
UNCOV
323
        if self == self.PRESERVE_PAYMENT:
×
UNCOV
324
            return _('Preserve payment')
×
UNCOV
325
        elif self == self.DECREASE_PAYMENT:
×
UNCOV
326
            return _('Decrease payment')
×
327
        else:
UNCOV
328
            raise Exception(f"unknown strategy: {self=}")
×
329

330

331
class ReceiveRequestHelp(NamedTuple):
5✔
332
    # help texts (warnings/errors):
333
    address_help: str
5✔
334
    URI_help: str
5✔
335
    ln_help: str
5✔
336
    # whether the texts correspond to an error (or just a warning):
337
    address_is_error: bool
5✔
338
    URI_is_error: bool
5✔
339
    ln_is_error: bool
5✔
340

341
    ln_swap_suggestion: Optional[Any] = None
5✔
342
    ln_rebalance_suggestion: Optional[Any] = None
5✔
343
    ln_zeroconf_suggestion: bool = False
5✔
344

345
    def can_swap(self) -> bool:
5✔
UNCOV
346
        return bool(self.ln_swap_suggestion)
×
347

348
    def can_rebalance(self) -> bool:
5✔
UNCOV
349
        return bool(self.ln_rebalance_suggestion)
×
350

351
    def can_zeroconf(self) -> bool:
5✔
UNCOV
352
        return self.ln_zeroconf_suggestion
×
353

354

355
class TxWalletDelta(NamedTuple):
5✔
356
    is_relevant: bool  # "related to wallet?"
5✔
357
    is_any_input_ismine: bool
5✔
358
    is_all_input_ismine: bool
5✔
359
    delta: int
5✔
360
    fee: Optional[int]
5✔
361

362
class TxWalletDetails(NamedTuple):
5✔
363
    txid: Optional[str]
5✔
364
    status: str
5✔
365
    label: str
5✔
366
    can_broadcast: bool
5✔
367
    can_bump: bool
5✔
368
    can_cpfp: bool
5✔
369
    can_dscancel: bool  # whether user can double-spend to self
5✔
370
    can_save_as_local: bool
5✔
371
    amount: Optional[int]
5✔
372
    fee: Optional[int]
5✔
373
    tx_mined_status: TxMinedInfo
5✔
374
    mempool_depth_bytes: Optional[int]
5✔
375
    can_remove: bool  # whether user should be allowed to delete tx
5✔
376
    is_lightning_funding_tx: bool
5✔
377
    is_related_to_wallet: bool
5✔
378

379

380
class Abstract_Wallet(ABC, Logger, EventListener):
5✔
381
    """
382
    Wallet classes are created to handle various address generation methods.
383
    Completion states (watching-only, single account, no seed, etc) are handled inside classes.
384
    """
385

386
    LOGGING_SHORTCUT = 'w'
5✔
387
    max_change_outputs = 3
5✔
388
    gap_limit_for_change = 10
5✔
389

390
    txin_type: str
5✔
391
    wallet_type: str
5✔
392
    lnworker: Optional['LNWallet']
5✔
393
    network: Optional['Network']
5✔
394

395
    def __init__(self, db: WalletDB, *, config: SimpleConfig):
5✔
396
        self.config = config
5✔
397
        assert self.config is not None, "config must not be None"
5✔
398
        self.db = db
5✔
399
        self.storage = db.storage  # type: Optional[WalletStorage]
5✔
400
        # load addresses needs to be called before constructor for sanity checks
401
        db.load_addresses(self.wallet_type)
5✔
402
        self.keystore = None  # type: Optional[KeyStore]  # will be set by load_keystore
5✔
403
        self._password_in_memory = None  # see self.unlock
5✔
404
        Logger.__init__(self)
5✔
405

406
        self.network = None
5✔
407
        self.adb = AddressSynchronizer(db, config, name=self.diagnostic_name())
5✔
408
        for addr in self.get_addresses():
5✔
409
            self.adb.add_address(addr)
5✔
410
        self.lock = self.adb.lock
5✔
411
        self.transaction_lock = self.adb.transaction_lock
5✔
412
        self._last_full_history = None
5✔
413
        self._tx_parents_cache = {}
5✔
414
        self._default_labels = {}
5✔
415
        self._accounting_addresses = set() # addresses counted as ours after successful sweep
5✔
416

417
        self.taskgroup = OldTaskGroup()
5✔
418

419
        # saved fields
420
        self.use_change            = db.get('use_change', True)
5✔
421
        self.multiple_change       = db.get('multiple_change', False)
5✔
422
        self._labels                = db.get_dict('labels')
5✔
423
        self._frozen_addresses      = set(db.get('frozen_addresses', []))
5✔
424
        self._frozen_coins          = db.get_dict('frozen_coins')  # type: Dict[str, bool]
5✔
425
        self.fiat_value            = db.get_dict('fiat_value')
5✔
426
        self._receive_requests      = db.get_dict('payment_requests')  # type: Dict[str, Request]
5✔
427
        self._invoices              = db.get_dict('invoices')  # type: Dict[str, Invoice]
5✔
428
        self._reserved_addresses   = set(db.get('reserved_addresses', []))
5✔
429
        self._num_parents          = db.get_dict('num_parents')
5✔
430

431
        self._freeze_lock = threading.RLock()  # for mutating/iterating frozen_{addresses,coins}
5✔
432

433
        self.load_keystore()
5✔
434
        self.txbatcher = TxBatcher(self)
5✔
435
        self._init_lnworker()
5✔
436
        self._init_requests_rhash_index()
5✔
437
        self._prepare_onchain_invoice_paid_detection()
5✔
438
        self.calc_unused_change_addresses()
5✔
439
        # save wallet type the first time
440
        if self.db.get('wallet_type') is None:
5✔
441
            self.db.put('wallet_type', self.wallet_type)
5✔
442
        self.contacts = Contacts(self.db)
5✔
443
        self._coin_price_cache = {}
5✔
444

445
        # true when synchronized. this is stricter than adb.is_up_to_date():
446
        # to-be-generated (HD) addresses are also considered here (gap-limit-roll-forward)
447
        self._up_to_date = False
5✔
448

449
        self.test_addresses_sanity()
5✔
450
        if self.storage and self.has_storage_encryption():
5✔
451
            if (se := self.storage.get_encryption_version()) != (ae := self.get_available_storage_encryption_version()):
5✔
UNCOV
452
                raise WalletFileException(f"unexpected storage encryption type. found: {se!r}. allowed: {ae!r}")
×
453

454
        self.register_callbacks()
5✔
455

456
    def _init_lnworker(self):
5✔
457
        self.lnworker = None
5✔
458

459
    async def main_loop(self):
5✔
460
        self.logger.info("starting taskgroup.")
5✔
461
        try:
5✔
462
            async with self.taskgroup as group:
5✔
463
                await group.spawn(asyncio.Event().wait)  # run forever (until cancel)
5✔
464
                await group.spawn(self.do_synchronize_loop())
5✔
465
                await group.spawn(self.txbatcher.run())
5✔
466
        except Exception as e:
5✔
UNCOV
467
            self.logger.exception("taskgroup died.")
×
468
        finally:
469
            util.trigger_callback('wallet_updated', self)
5✔
470
            self.logger.info("taskgroup stopped.")
5✔
471

472
    async def do_synchronize_loop(self):
5✔
473
        """Generates new deterministic addresses if needed (gap limit roll-forward),
474
        and sets up_to_date.
475
        """
476
        while True:
5✔
477
            # polling.
478
            # TODO if adb had "up_to_date_changed" asyncio.Event(), we could *also* trigger on that.
479
            #      The polling would still be useful as often need to gen new addrs while adb.is_up_to_date() is False
480
            await asyncio.sleep(0.1)
5✔
481
            # note: we only generate new HD addresses if the existing ones
482
            #       have history that are mined and SPV-verified.
483
            await run_in_thread(self.synchronize)
5✔
484

485
    def save_db(self):
5✔
486
        if self.db.storage:
5✔
487
            self.db.write()
5✔
488

489
    def save_backup(self, backup_dir):
5✔
490
        new_path = os.path.join(backup_dir, self.basename() + '.backup')
×
491
        new_storage = WalletStorage(new_path)
×
492
        new_storage._encryption_version = self.storage._encryption_version
×
493
        new_storage.pubkey = self.storage.pubkey
×
494

495
        new_db = WalletDB(self.db.dump(), storage=new_storage, upgrade=True)
×
496
        if self.lnworker:
×
497
            channel_backups = new_db.get_dict('imported_channel_backups')
×
UNCOV
498
            for chan_id, chan in self.lnworker.channels.items():
×
UNCOV
499
                channel_backups[chan_id.hex()] = self.lnworker.create_channel_backup(chan_id)
×
UNCOV
500
            new_db.put('channels', None)
×
UNCOV
501
            new_db.put('lightning_privkey2', None)
×
UNCOV
502
        new_db.set_modified(True)
×
UNCOV
503
        new_db.write()
×
UNCOV
504
        return new_path
×
505

506
    def has_lightning(self) -> bool:
5✔
507
        return bool(self.lnworker)
5✔
508

509
    def has_channels(self):
5✔
UNCOV
510
        return self.lnworker is not None and len(self.lnworker._channels) > 0
×
511

512
    def can_have_lightning(self) -> bool:
5✔
513
        """ whether this wallet can create new channels """
514
        # we want static_remotekey to be a wallet address
515
        if not self.txin_type == 'p2wpkh':
×
516
            return False
×
517
        if self.config.ENABLE_ANCHOR_CHANNELS:
×
518
            # exclude watching-only wallets
UNCOV
519
            if not self.keystore:
×
520
                return False
×
521
            # exclude hardware wallets
522
            if not self.keystore.may_have_password():
×
UNCOV
523
                return False
×
UNCOV
524
        return True
×
525

526
    def can_have_deterministic_lightning(self) -> bool:
5✔
527
        if not self.can_have_lightning():
×
528
            return False
×
529
        return self.keystore.can_have_deterministic_lightning_xprv()
×
530

531
    def init_lightning(self, *, password) -> None:
5✔
UNCOV
532
        assert self.can_have_lightning()
×
533
        assert self.db.get('lightning_xprv') is None
×
534
        assert self.db.get('lightning_privkey2') is None
×
535
        if self.can_have_deterministic_lightning():
×
536
            assert isinstance(self.keystore, keystore.BIP32_KeyStore)
×
537
            ln_xprv = self.keystore.get_lightning_xprv(password)
×
538
            self.db.put('lightning_xprv', ln_xprv)
×
539
        else:
540
            seed = os.urandom(32)
×
UNCOV
541
            node = BIP32Node.from_rootseed(seed, xtype='standard')
×
UNCOV
542
            ln_xprv = node.to_xprv()
×
UNCOV
543
            self.db.put('lightning_privkey2', ln_xprv)
×
UNCOV
544
        self.lnworker = LNWallet(self, ln_xprv)
×
UNCOV
545
        self.save_db()
×
UNCOV
546
        if self.network:
×
UNCOV
547
            self._start_network_lightning()
×
548

549
    async def stop(self):
5✔
550
        """Stop all networking and save DB to disk."""
551
        self.unregister_callbacks()
5✔
552
        try:
5✔
553
            async with ignore_after(5):
5✔
554
                if self.network:
5✔
UNCOV
555
                    if self.lnworker:
×
UNCOV
556
                        await self.lnworker.stop()
×
UNCOV
557
                        self.lnworker = None
×
558
                await self.adb.stop()
5✔
559
                await self.taskgroup.cancel_remaining()
5✔
560
        finally:  # even if we get cancelled
561
            if any([ks.is_requesting_to_be_rewritten_to_wallet_file for ks in self.get_keystores()]):
5✔
562
                self.save_keystore()
5✔
563
            self.save_db()
5✔
564

565
    def is_up_to_date(self) -> bool:
5✔
UNCOV
566
        if self.taskgroup.joined:  # either stop() was called, or the taskgroup died
×
UNCOV
567
            return False
×
UNCOV
568
        return self._up_to_date
×
569

570
    def tx_is_related(self, tx):
5✔
571
        is_mine = any([self.is_mine(out.address) for out in tx.outputs()])
5✔
572
        is_mine |= any([self.is_mine(self.adb.get_txin_address(txin)) for txin in tx.inputs()])
5✔
573
        return is_mine
5✔
574

575
    def clear_tx_parents_cache(self):
5✔
576
        with self.lock, self.transaction_lock:
5✔
577
            self._tx_parents_cache.clear()
5✔
578
            self._num_parents.clear()
5✔
579
            self._last_full_history = None
5✔
580

581
    @event_listener
5✔
582
    async def on_event_adb_set_up_to_date(self, adb):
5✔
583
        if self.adb != adb:
5✔
584
            return
5✔
585
        num_new_addrs = await run_in_thread(self.synchronize)
5✔
586
        up_to_date = self.adb.is_up_to_date() and num_new_addrs == 0
5✔
587
        with self.lock:
5✔
588
            status_changed = self._up_to_date != up_to_date
5✔
589
            self._up_to_date = up_to_date
5✔
590
        if up_to_date:
5✔
591
            self.adb.reset_netrequest_counters()  # sync progress indicator
×
UNCOV
592
            self.save_db()
×
593
        # fire triggers
594
        if status_changed or up_to_date:  # suppress False->False transition, as it is spammy
5✔
UNCOV
595
            util.trigger_callback('wallet_updated', self)
×
UNCOV
596
            util.trigger_callback('status')
×
597
        if status_changed:
5✔
UNCOV
598
            self.logger.info(f'set_up_to_date: {up_to_date}')
×
599

600
    @event_listener
5✔
601
    def on_event_adb_added_tx(self, adb, tx_hash: str, tx: Transaction):
5✔
602
        if self.adb != adb:
5✔
603
            return
5✔
604
        if not self.tx_is_related(tx):
5✔
605
            return
5✔
606
        self.clear_tx_parents_cache()
5✔
607
        if self.lnworker:
5✔
608
            self.lnworker.maybe_add_backup_from_tx(tx)
5✔
609
        self._update_invoices_and_reqs_touched_by_tx(tx_hash)
5✔
610
        util.trigger_callback('new_transaction', self, tx)
5✔
611

612
    @event_listener
5✔
613
    def on_event_adb_removed_tx(self, adb, txid: str, tx: Transaction):
5✔
614
        if self.adb != adb:
5✔
615
            return
5✔
616
        if not tx or not self.tx_is_related(tx):
5✔
UNCOV
617
            return
×
618
        self.clear_tx_parents_cache()
5✔
619
        util.trigger_callback('removed_transaction', self, tx)
5✔
620

621
    @event_listener
5✔
622
    def on_event_adb_added_verified_tx(self, adb, tx_hash):
5✔
623
        if adb != self.adb:
5✔
624
            return
5✔
625
        self._update_invoices_and_reqs_touched_by_tx(tx_hash)
5✔
626
        tx_mined_status = self.adb.get_tx_height(tx_hash)
5✔
627
        util.trigger_callback('verified', self, tx_hash, tx_mined_status)
5✔
628

629
    @event_listener
5✔
630
    def on_event_adb_removed_verified_tx(self, adb, tx_hash):
5✔
UNCOV
631
        if adb != self.adb:
×
UNCOV
632
            return
×
UNCOV
633
        self._update_invoices_and_reqs_touched_by_tx(tx_hash)
×
634

635
    def clear_history(self):
5✔
UNCOV
636
        self.adb.clear_history()
×
UNCOV
637
        self.save_db()
×
638

639
    def start_network(self, network: 'Network'):
5✔
640
        assert self.network is None, "already started"
5✔
641
        self.network = network
5✔
642
        if network:
5✔
643
            asyncio.run_coroutine_threadsafe(self.main_loop(), self.network.asyncio_loop)
5✔
644
            self.adb.start_network(network)
5✔
645
            if self.lnworker:
5✔
646
                self._start_network_lightning()
×
647

648
    def _start_network_lightning(self):
5✔
UNCOV
649
        assert self.lnworker
×
UNCOV
650
        assert self.lnworker.network is None, 'lnworker network already initialized'
×
651
        self.lnworker.start_network(self.network)
×
652
        # only start gossiping when we already have channels
UNCOV
653
        if self.db.get('channels'):
×
UNCOV
654
            self.network.start_gossip()
×
655

656
    @abstractmethod
5✔
657
    def load_keystore(self) -> None:
5✔
UNCOV
658
        pass
×
659

660
    def diagnostic_name(self):
5✔
661
        return self.basename()
5✔
662

663
    def __str__(self):
5✔
664
        return self.basename()
5✔
665

666
    def get_master_public_key(self):
5✔
UNCOV
667
        return None
×
668

669
    def get_master_public_keys(self):
5✔
UNCOV
670
        return []
×
671

672
    def basename(self) -> str:
5✔
673
        return self.storage.basename() if self.storage else 'no_name'
5✔
674

675
    def test_addresses_sanity(self) -> None:
5✔
676
        addrs = self.get_receiving_addresses()
5✔
677
        if len(addrs) > 0:
5✔
678
            addr = str(addrs[0])
5✔
679
            if not bitcoin.is_address(addr):
5✔
UNCOV
680
                neutered_addr = addr[:5] + '..' + addr[-2:]
×
UNCOV
681
                raise WalletFileException(f'The addresses in this wallet are not bitcoin addresses.\n'
×
682
                                          f'e.g. {neutered_addr} (length: {len(addr)})')
683

684
    def check_returned_address_for_corruption(func):
5✔
685
        def wrapper(self, *args, **kwargs):
5✔
686
            addr = func(self, *args, **kwargs)
5✔
687
            self.check_address_for_corruption(addr)
5✔
688
            return addr
5✔
689
        return wrapper
5✔
690

691
    def calc_unused_change_addresses(self) -> Sequence[str]:
5✔
692
        """Returns a list of change addresses to choose from, for usage in e.g. new transactions.
693
        The caller should give priority to earlier ones in the list.
694
        """
695
        with self.lock:
5✔
696
            # We want a list of unused change addresses.
697
            # As a performance optimisation, to avoid checking all addresses every time,
698
            # we maintain a list of "not old" addresses ("old" addresses have deeply confirmed history),
699
            # and only check those.
700
            if not hasattr(self, '_not_old_change_addresses'):
5✔
701
                self._not_old_change_addresses = self.get_change_addresses()
5✔
702
            self._not_old_change_addresses = [addr for addr in self._not_old_change_addresses
5✔
703
                                              if not self.adb.address_is_old(addr)]
704
            unused_addrs = [addr for addr in self._not_old_change_addresses
5✔
705
                            if not self.adb.is_used(addr) and not self.is_address_reserved(addr)]
706
            return unused_addrs
5✔
707

708
    def is_deterministic(self) -> bool:
5✔
709
        return self.keystore.is_deterministic()
×
710

711
    def _set_label(self, key: str, value: Optional[str]) -> None:
5✔
UNCOV
712
        with self.lock:
×
713
            if value is None:
×
UNCOV
714
                self._labels.pop(key, None)
×
715
            else:
UNCOV
716
                self._labels[key] = value
×
717

718
    def set_label(self, name: str, text: str = None) -> bool:
5✔
719
        if not name:
5✔
UNCOV
720
            return False
×
721
        changed = False
5✔
722
        with self.lock:
5✔
723
            old_text = self._labels.get(name)
5✔
724
            if text:
5✔
725
                text = text.replace("\n", " ")
5✔
726
                if old_text != text:
5✔
727
                    self._labels[name] = text
5✔
728
                    changed = True
5✔
729
            else:
730
                if old_text is not None:
5✔
731
                    self._labels.pop(name)
×
732
                    changed = True
×
733
        if changed:
5✔
734
            run_hook('set_label', self, name, text)
5✔
735
        return changed
5✔
736

737
    def import_labels(self, path):
5✔
UNCOV
738
        data = read_json_file(path)
×
UNCOV
739
        for key, value in data.items():
×
740
            self.set_label(key, value)
×
741

742
    def export_labels(self, path):
5✔
UNCOV
743
        write_json_file(path, self.get_all_labels())
×
744

745
    def set_fiat_value(self, txid, ccy, text, fx, value_sat):
5✔
746
        if not self.db.get_transaction(txid):
5✔
UNCOV
747
            return
×
748
        # since fx is inserting the thousands separator,
749
        # and not util, also have fx remove it
750
        text = fx.remove_thousands_separator(text)
5✔
751
        def_fiat = self.default_fiat_value(txid, fx, value_sat)
5✔
752
        formatted = fx.ccy_amount_str(def_fiat, add_thousands_sep=False)
5✔
753
        def_fiat_rounded = Decimal(formatted)
5✔
754
        reset = not text
5✔
755
        if not reset:
5✔
756
            try:
5✔
757
                text_dec = Decimal(text)
5✔
758
                text_dec_rounded = Decimal(fx.ccy_amount_str(text_dec, add_thousands_sep=False))
5✔
759
                reset = text_dec_rounded == def_fiat_rounded
5✔
760
            except Exception:
5✔
761
                # garbage. not resetting, but not saving either
762
                return False
5✔
763
        if reset:
5✔
764
            d = self.fiat_value.get(ccy, {})
5✔
765
            if d and txid in d:
5✔
766
                d.pop(txid)
5✔
767
            else:
768
                # avoid saving empty dict
769
                return True
5✔
770
        else:
771
            if ccy not in self.fiat_value:
5✔
772
                self.fiat_value[ccy] = {}
5✔
773
            self.fiat_value[ccy][txid] = text
5✔
774
        return reset
5✔
775

776
    def get_fiat_value(self, txid, ccy):
5✔
UNCOV
777
        fiat_value = self.fiat_value.get(ccy, {}).get(txid)
×
UNCOV
778
        try:
×
UNCOV
779
            return Decimal(fiat_value)
×
UNCOV
780
        except Exception:
×
UNCOV
781
            return
×
782

783
    def is_mine(self, address) -> bool:
5✔
784
        if not address: return False
5✔
785
        return bool(self.get_address_index(address))
5✔
786

787
    def is_change(self, address) -> bool:
5✔
788
        if not self.is_mine(address):
5✔
789
            return False
5✔
790
        return self.get_address_index(address)[0] == 1
5✔
791

792
    @abstractmethod
5✔
793
    def get_addresses(self) -> Sequence[str]:
5✔
UNCOV
794
        pass
×
795

796
    @abstractmethod
5✔
797
    def get_address_index(self, address: str) -> Optional[AddressIndexGeneric]:
5✔
798
        pass
×
799

800
    @abstractmethod
5✔
801
    def get_address_path_str(self, address: str) -> Optional[str]:
5✔
802
        """Returns derivation path str such as "m/0/5" to address,
803
        or None if not applicable.
804
        """
805
        pass
×
806

807
    def get_redeem_script(self, address: str) -> Optional[str]:
5✔
808
        desc = self.get_script_descriptor_for_address(address)
×
809
        if desc is None: return None
×
810
        redeem_script = desc.expand().redeem_script
×
811
        if redeem_script:
×
812
            return redeem_script.hex()
×
813

814
    def get_witness_script(self, address: str) -> Optional[str]:
5✔
UNCOV
815
        desc = self.get_script_descriptor_for_address(address)
×
UNCOV
816
        if desc is None: return None
×
817
        witness_script = desc.expand().witness_script
×
UNCOV
818
        if witness_script:
×
UNCOV
819
            return witness_script.hex()
×
820

821
    @abstractmethod
5✔
822
    def get_txin_type(self, address: str) -> str:
5✔
823
        """Return script type of wallet address."""
UNCOV
824
        pass
×
825

826
    def export_private_key(self, address: str, password: Optional[str]) -> str:
5✔
827
        if self.is_watching_only():
5✔
UNCOV
828
            raise UserFacingException(_("This is a watching-only wallet"))
×
829
        if not is_address(address):
5✔
830
            raise UserFacingException(_('Invalid bitcoin address: {}').format(address))
5✔
831
        if not self.is_mine(address):
5✔
832
            raise UserFacingException(_('Address not in wallet: {}').format(address))
5✔
833
        index = self.get_address_index(address)
5✔
834
        pk, compressed = self.keystore.get_private_key(index, password)
5✔
835
        txin_type = self.get_txin_type(address)
5✔
836
        serialized_privkey = bitcoin.serialize_privkey(pk, compressed, txin_type)
5✔
837
        return serialized_privkey
5✔
838

839
    def export_private_key_for_path(self, path: Union[Sequence[int], str], password: Optional[str]) -> str:
5✔
UNCOV
840
        raise UserFacingException("this wallet is not deterministic")
×
841

842
    @abstractmethod
5✔
843
    def get_public_keys(self, address: str) -> Sequence[str]:
5✔
UNCOV
844
        pass
×
845

846
    def get_public_keys_with_deriv_info(self, address: str) -> Dict[bytes, Tuple[KeyStoreWithMPK, Sequence[int]]]:
5✔
847
        """Returns a map: pubkey -> (keystore, derivation_suffix)"""
848
        return {}
×
849

850
    def is_lightning_funding_tx(self, txid: Optional[str]) -> bool:
5✔
851
        if not self.lnworker or txid is None:
5✔
852
            return False
5✔
853
        if any([chan.funding_outpoint.txid == txid
5✔
854
                for chan in self.lnworker.channels.values()]):
UNCOV
855
            return True
×
856
        if any([chan.funding_outpoint.txid == txid
5✔
857
                for chan in self.lnworker.channel_backups.values()]):
858
            return True
×
859
        return False
5✔
860

861
    def get_swaps_by_claim_tx(self, tx: Transaction) -> Iterable['SwapData']:
5✔
862
        return self.lnworker.swap_manager.get_swaps_by_claim_tx(tx) if self.lnworker else []
5✔
863

864
    def get_swaps_by_funding_tx(self, tx: Transaction) -> Iterable['SwapData']:
5✔
UNCOV
865
        return self.lnworker.swap_manager.get_swaps_by_funding_tx(tx) if self.lnworker else []
×
866

867
    def is_accounting_address(self, addr):
5✔
868
        """
869
        Addresses from which we have been able to sweep funds.
870
        We consider them 'ours' for accounting purposes, so that the
871
        wallet history does not show funds going in and out of the wallet.
872
        """
873
        # must be a sweep utxo AND we swept (spending tx is a wallet tx)
874
        return addr in self._accounting_addresses
5✔
875

876
    def get_wallet_delta(self, tx: Transaction) -> TxWalletDelta:
5✔
877
        """Return the effect a transaction has on the wallet.
878
        This method must use self.is_mine, not self.adb.is_mine()
879
        """
880
        is_relevant = False  # "related to wallet?"
5✔
881
        num_input_ismine = 0
5✔
882
        v_in = v_in_mine = v_out = v_out_mine = 0
5✔
883
        with self.lock, self.transaction_lock:
5✔
884
            for txin in tx.inputs():
5✔
885
                addr = self.adb.get_txin_address(txin)
5✔
886
                value = self.adb.get_txin_value(txin, address=addr)
5✔
887
                if self.is_mine(addr) or self.is_accounting_address(addr):
5✔
888
                    num_input_ismine += 1
5✔
889
                    is_relevant = True
5✔
890
                    assert value is not None
5✔
891
                    v_in_mine += value
5✔
892
                if value is None:
5✔
UNCOV
893
                    v_in = None
×
894
                elif v_in is not None:
5✔
895
                    v_in += value
5✔
896
            for txout in tx.outputs():
5✔
897
                v_out += txout.value
5✔
898
                if self.is_mine(txout.address) or self.is_accounting_address(txout.address):
5✔
899
                    v_out_mine += txout.value
5✔
900
                    is_relevant = True
5✔
901
        delta = v_out_mine - v_in_mine
5✔
902
        if v_in is not None:
5✔
903
            fee = v_in - v_out
5✔
904
        else:
UNCOV
905
            fee = None
×
906
        if fee is None and isinstance(tx, PartialTransaction):
5✔
UNCOV
907
            fee = tx.get_fee()
×
908
        return TxWalletDelta(
5✔
909
            is_relevant=is_relevant,
910
            is_any_input_ismine=num_input_ismine > 0,
911
            is_all_input_ismine=num_input_ismine == len(tx.inputs()),
912
            delta=delta,
913
            fee=fee,
914
        )
915

916
    def get_tx_info(self, tx: Transaction) -> TxWalletDetails:
5✔
917
        tx_wallet_delta = self.get_wallet_delta(tx)
5✔
918
        is_relevant = tx_wallet_delta.is_relevant
5✔
919
        is_any_input_ismine = tx_wallet_delta.is_any_input_ismine
5✔
920
        is_swap = bool(self.get_swaps_by_claim_tx(tx))
5✔
921
        fee = tx_wallet_delta.fee
5✔
922
        exp_n = None
5✔
923
        can_broadcast = False
5✔
924
        can_bump = False
5✔
925
        can_cpfp = False
5✔
926
        tx_hash = tx.txid()  # note: txid can be None! e.g. when called from GUI tx dialog
5✔
927
        is_lightning_funding_tx = self.is_lightning_funding_tx(tx_hash)
5✔
928
        tx_we_already_have_in_db = self.adb.db.get_transaction(tx_hash)
5✔
929
        can_save_as_local = (is_relevant and tx.txid() is not None
5✔
930
                             and (tx_we_already_have_in_db is None or not tx_we_already_have_in_db.is_complete()))
931
        label = ''
5✔
932
        tx_mined_status = self.adb.get_tx_height(tx_hash)
5✔
933
        can_remove = ((tx_mined_status.height in [TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL])
5✔
934
                      # otherwise 'height' is unreliable (typically LOCAL):
935
                      and is_relevant
936
                      # don't offer during common signing flow, e.g. when watch-only wallet starts creating a tx:
937
                      and bool(tx_we_already_have_in_db))
938
        can_dscancel = False
5✔
939
        if tx.is_complete():
5✔
940
            if tx_we_already_have_in_db:
5✔
941
                label = self.get_label_for_txid(tx_hash)
5✔
942
                if tx_mined_status.height > 0:
5✔
943
                    if tx_mined_status.conf:
×
UNCOV
944
                        status = _("{} confirmations").format(tx_mined_status.conf)
×
945
                    else:
946
                        status = _('Not verified')
×
947
                elif tx_mined_status.height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED):
5✔
948
                    status = _('Unconfirmed')
5✔
949
                    if fee is None:
5✔
UNCOV
950
                        fee = self.adb.get_tx_fee(tx_hash)
×
951
                    if fee and self.network and self.network.has_fee_mempool():
5✔
UNCOV
952
                        size = tx.estimated_size()
×
UNCOV
953
                        fee_per_byte = fee / size
×
UNCOV
954
                        exp_n = self.network.mempool_fees.fee_to_depth(fee_per_byte)
×
955
                    can_bump = (is_any_input_ismine or is_swap) and self.can_rbf_tx(tx)
5✔
956
                    can_dscancel = (is_any_input_ismine and self.can_rbf_tx(tx, is_dscancel=True)
5✔
957
                                    and not all([self.is_mine(txout.address) for txout in tx.outputs()]))
958
                    try:
5✔
959
                        self.cpfp(tx, 0)
5✔
960
                        can_cpfp = True
5✔
961
                    except Exception:
5✔
962
                        can_cpfp = False
5✔
963
                else:
UNCOV
964
                    status = _('Local')
×
965
                    if tx_mined_status.height == TX_HEIGHT_FUTURE:
×
966
                        num_blocks_remainining = tx_mined_status.wanted_height - self.adb.get_local_height()
×
UNCOV
967
                        num_blocks_remainining = max(0, num_blocks_remainining)
×
968
                        status = _('Local (future: {})').format(_('in {} blocks').format(num_blocks_remainining))
×
969
                    can_broadcast = self.network is not None
×
970
                    can_bump = (is_any_input_ismine or is_swap) and self.can_rbf_tx(tx)
×
971
            else:
UNCOV
972
                status = _("Signed")
×
UNCOV
973
                can_broadcast = self.network is not None
×
974
        else:
UNCOV
975
            assert isinstance(tx, PartialTransaction)
×
UNCOV
976
            s, r = tx.signature_count()
×
977
            status = _("Unsigned") if s == 0 else _('Partially signed') + ' (%d/%d)'%(s,r)
×
978

979
        if is_relevant:
5✔
980
            if tx_wallet_delta.is_all_input_ismine:
5✔
981
                assert fee is not None
5✔
982
                amount = tx_wallet_delta.delta + fee
5✔
983
            else:
UNCOV
984
                amount = tx_wallet_delta.delta
×
985
        else:
UNCOV
986
            amount = None
×
987

988
        if is_lightning_funding_tx:
5✔
UNCOV
989
            assert not can_bump  # would change txid
×
990

991
        return TxWalletDetails(
5✔
992
            txid=tx_hash,
993
            status=status,
994
            label=label,
995
            can_broadcast=can_broadcast,
996
            can_bump=can_bump,
997
            can_cpfp=can_cpfp,
998
            can_dscancel=can_dscancel,
999
            can_save_as_local=can_save_as_local,
1000
            amount=amount,
1001
            fee=fee,
1002
            tx_mined_status=tx_mined_status,
1003
            mempool_depth_bytes=exp_n,
1004
            can_remove=can_remove,
1005
            is_lightning_funding_tx=is_lightning_funding_tx,
1006
            is_related_to_wallet=is_relevant,
1007
        )
1008

1009
    def get_num_parents(self, txid: str) -> Optional[int]:
5✔
UNCOV
1010
        if not self.is_up_to_date():
×
UNCOV
1011
            return
×
UNCOV
1012
        if txid not in self._num_parents:
×
UNCOV
1013
            self._num_parents[txid] = len(self.get_tx_parents(txid))
×
1014
        return self._num_parents[txid]
×
1015

1016
    def get_tx_parents(self, txid: str) -> Dict[str, Tuple[List[str], List[str]]]:
5✔
1017
        """
1018
        returns a flat dict:
1019
        txid -> list of parent txids
1020
        """
1021
        with self.lock, self.transaction_lock:
×
UNCOV
1022
            if self._last_full_history is None:
×
1023
                self._last_full_history = self.get_onchain_history()
×
1024
                # populate cache in chronological order (confirmed tx only)
1025
                # todo: get_full_history should return unconfirmed tx topologically sorted
1026
                for _txid, tx_item in self._last_full_history.items():
×
1027
                    if tx_item.tx_mined_status.height > 0:
×
1028
                        self.get_tx_parents(_txid)
×
1029

1030
            result = self._tx_parents_cache.get(txid, None)
×
1031
            if result is not None:
×
1032
                return result
×
1033
            result = {}   # type: Dict[str, Tuple[List[str], List[str]]]
×
UNCOV
1034
            parents = []  # type: List[str]
×
1035
            uncles = []   # type: List[str]
×
1036
            tx = self.adb.get_transaction(txid)
×
1037
            assert tx, f"cannot find {txid} in db"
×
1038
            for i, txin in enumerate(tx.inputs()):
×
1039
                _txid = txin.prevout.txid.hex()
×
1040
                parents.append(_txid)
×
1041
                # detect address reuse
1042
                addr = self.adb.get_txin_address(txin)
×
1043
                if addr is None:
×
1044
                    continue
×
1045
                received, sent = self.adb.get_addr_io(addr)
×
1046
                if len(sent) > 1:
×
1047
                    my_txid, my_height, my_pos = sent[txin.prevout.to_str()]
×
1048
                    assert my_txid == txid
×
1049
                    for k, v in sent.items():
×
UNCOV
1050
                        if k != txin.prevout.to_str():
×
1051
                            reuse_txid, reuse_height, reuse_pos = v
×
1052
                            if reuse_height <= 0:  # exclude not-yet-mined (we need topological ordering)
×
1053
                                continue
×
1054
                            if (reuse_height, reuse_pos) < (my_height, my_pos):
×
1055
                                uncle_txid, uncle_index = k.split(':')
×
1056
                                uncles.append(uncle_txid)
×
1057

UNCOV
1058
            for _txid in parents + uncles:
×
UNCOV
1059
                if _txid in self._last_full_history.keys():
×
UNCOV
1060
                    result.update(self.get_tx_parents(_txid))
×
UNCOV
1061
            result[txid] = parents, uncles
×
UNCOV
1062
            self._tx_parents_cache[txid] = result
×
1063
            return result
×
1064

1065
    def get_balance(self, **kwargs):
5✔
1066
        domain = self.get_addresses()
5✔
1067
        return self.adb.get_balance(domain, **kwargs)
5✔
1068

1069
    def anchor_reserve(self) -> int:
5✔
UNCOV
1070
        if self.lnworker is None or not isinstance(self.lnworker, LNWallet):
×
UNCOV
1071
            return 0
×
UNCOV
1072
        if not self.lnworker.has_anchor_channels():
×
UNCOV
1073
            return 0
×
1074
        return self.config.LN_UTXO_RESERVE
×
1075

1076
    def get_spendable_balance_sat(
5✔
1077
        self,
1078
        deduct_anchor_reserve: bool = True,
1079
        **kwargs
1080
    ) -> int:
UNCOV
1081
        anchor_reserve = self.anchor_reserve() if deduct_anchor_reserve else 0
×
UNCOV
1082
        spendable_coins = self.get_spendable_coins(**kwargs)
×
UNCOV
1083
        oc_balance = sum([coin.value_sats() for coin in spendable_coins]) - anchor_reserve
×
UNCOV
1084
        return max(0, oc_balance)
×
1085

1086
    def get_addr_balance(self, address):
5✔
UNCOV
1087
        return self.adb.get_balance([address])
×
1088

1089
    def get_utxos(
5✔
1090
            self,
1091
            domain: Optional[Iterable[str]] = None,
1092
            **kwargs,
1093
    ):
1094
        if domain is None:
5✔
1095
            domain = self.get_addresses()
5✔
1096
        return self.adb.get_utxos(domain=domain, **kwargs)
5✔
1097

1098
    def get_spendable_coins(
5✔
1099
            self,
1100
            domain: Optional[Iterable[str]] = None,
1101
            *,
1102
            nonlocal_only: bool = False,
1103
            confirmed_only: bool = None,
1104
    ) -> Sequence[PartialTxInput]:
1105
        with self._freeze_lock:
5✔
1106
            frozen_addresses = self._frozen_addresses.copy()
5✔
1107
        if confirmed_only is None:
5✔
1108
            confirmed_only = self.config.WALLET_SPEND_CONFIRMED_ONLY
5✔
1109
        utxos = self.get_utxos(
5✔
1110
            domain=domain,
1111
            excluded_addresses=frozen_addresses,
1112
            mature_only=True,
1113
            confirmed_funding_only=confirmed_only,
1114
            nonlocal_only=nonlocal_only,
1115
        )
1116
        utxos = [utxo for utxo in utxos if not self.is_frozen_coin(utxo)]
5✔
1117
        return utxos
5✔
1118

1119
    @abstractmethod
5✔
1120
    def get_receiving_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence[str]:
5✔
UNCOV
1121
        pass
×
1122

1123
    @abstractmethod
5✔
1124
    def get_change_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence[str]:
5✔
1125
        pass
×
1126

1127
    def dummy_address(self):
5✔
1128
        # first receiving address
1129
        return self.get_receiving_addresses(slice_start=0, slice_stop=1)[0]
×
1130

1131
    def get_frozen_balance(self):
5✔
1132
        with self._freeze_lock:
×
1133
            frozen_addresses = self._frozen_addresses.copy()
×
1134
        # note: for coins, use is_frozen_coin instead of _frozen_coins,
1135
        #       as latter only contains *manually* frozen ones
UNCOV
1136
        frozen_coins = {utxo.prevout.to_str() for utxo in self.get_utxos()
×
1137
                        if self.is_frozen_coin(utxo)}
1138
        if not frozen_coins:  # shortcut
×
UNCOV
1139
            return self.adb.get_balance(frozen_addresses)
×
UNCOV
1140
        c1, u1, x1 = self.get_balance()
×
UNCOV
1141
        c2, u2, x2 = self.get_balance(
×
1142
            excluded_addresses=frozen_addresses,
1143
            excluded_coins=frozen_coins,
1144
        )
1145
        return c1-c2, u1-u2, x1-x2
×
1146

1147
    def get_balances_for_piechart(self):
5✔
1148
        # return only positive values
1149
        # todo: add lightning frozen
1150
        c, u, x = self.get_balance()
×
1151
        fc, fu, fx = self.get_frozen_balance()
×
1152
        lightning = self.lnworker.get_balance() if self.has_lightning() else 0
×
UNCOV
1153
        f_lightning = self.lnworker.get_balance(frozen=True) if self.has_lightning() else 0
×
1154
        # subtract frozen funds
UNCOV
1155
        cc = c - fc
×
UNCOV
1156
        uu = u - fu
×
1157
        xx = x - fx
×
1158
        frozen = fc + fu + fx
×
1159
        return cc, uu, xx, frozen, lightning - f_lightning, f_lightning
×
1160

1161
    def balance_at_timestamp(self, domain, target_timestamp):
5✔
1162
        # we assume that get_history returns items ordered by block height
1163
        # we also assume that block timestamps are monotonic (which is false...!)
1164
        h = self.adb.get_history(domain=domain)
×
UNCOV
1165
        balance = 0
×
UNCOV
1166
        for hist_item in h:
×
UNCOV
1167
            balance = hist_item.balance
×
UNCOV
1168
            if hist_item.tx_mined_status.timestamp is None or hist_item.tx_mined_status.timestamp > target_timestamp:
×
UNCOV
1169
                return balance - hist_item.delta
×
1170
        # return last balance
UNCOV
1171
        return balance
×
1172

1173
    def get_onchain_history(
5✔
1174
            self, *,
1175
            domain=None,
1176
            from_timestamp=None,
1177
            to_timestamp=None,
1178
            from_height=None,
1179
            to_height=None) -> Dict[str, OnchainHistoryItem]:
1180
        # sanity check
1181
        if (from_timestamp is not None or to_timestamp is not None) \
×
1182
                and (from_height is not None or to_height is not None):
1183
            raise UserFacingException('timestamp and block height based filtering cannot be used together')
×
1184
        # call lnworker first, because it adds accounting addresses
1185
        groups = self.lnworker.get_groups_for_onchain_history() if self.lnworker else {}
×
1186
        if domain is None:
×
1187
            domain = self.get_addresses()
×
1188
            domain += list(self._accounting_addresses)
×
1189

1190
        now = time.time()
×
1191
        transactions = OrderedDictWithIndex()
×
1192
        monotonic_timestamp = 0
×
1193
        for hist_item in self.adb.get_history(domain=domain):
×
1194
            timestamp = (hist_item.tx_mined_status.timestamp or TX_TIMESTAMP_INF)
×
1195
            height = hist_item.tx_mined_status
×
1196
            if from_timestamp and (timestamp or now) < from_timestamp:
×
1197
                continue
×
1198
            if to_timestamp and (timestamp or now) >= to_timestamp:
×
1199
                continue
×
1200
            if from_height is not None and from_height > height > 0:
×
1201
                continue
×
UNCOV
1202
            if to_height is not None and (height >= to_height or height <= 0):
×
UNCOV
1203
                continue
×
UNCOV
1204
            monotonic_timestamp = max(monotonic_timestamp, timestamp)
×
UNCOV
1205
            txid = hist_item.txid
×
UNCOV
1206
            group_id = groups.get(txid)
×
UNCOV
1207
            label = self.get_label_for_txid(txid)
×
UNCOV
1208
            tx_item = OnchainHistoryItem(
×
1209
                txid=hist_item.txid,
1210
                amount_sat=hist_item.delta,
1211
                fee_sat=hist_item.fee,
1212
                balance_sat=hist_item.balance,
1213
                tx_mined_status=hist_item.tx_mined_status,
1214
                label=label,
1215
                monotonic_timestamp=monotonic_timestamp,
1216
                group_id=group_id,
1217
            )
1218
            transactions[hist_item.txid] = tx_item
×
1219

UNCOV
1220
        return transactions
×
1221

1222
    def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> Invoice:
5✔
1223
        height = self.adb.get_local_height()
5✔
1224
        if pr:
5✔
1225
            return Invoice.from_bip70_payreq(pr, height=height)
×
1226
        amount_msat = 0
5✔
1227
        for x in outputs:
5✔
1228
            if parse_max_spend(x.value):
5✔
1229
                amount_msat = '!'
5✔
1230
                break
5✔
1231
            else:
UNCOV
1232
                assert isinstance(x.value, int), f"{x.value!r}"
×
UNCOV
1233
                amount_msat += x.value * 1000
×
1234
        timestamp = None
5✔
1235
        exp = None
5✔
1236
        if URI:
5✔
1237
            timestamp = URI.get('time')
5✔
1238
            exp = URI.get('exp')
5✔
1239
        timestamp = timestamp or int(Invoice._get_cur_time())
5✔
1240
        exp = exp or 0
5✔
1241
        invoice = Invoice(
5✔
1242
            amount_msat=amount_msat,
1243
            message=message,
1244
            time=timestamp,
1245
            exp=exp,
1246
            outputs=outputs,
1247
            bip70=None,
1248
            height=height,
1249
            lightning_invoice=None,
1250
        )
1251
        return invoice
5✔
1252

1253
    def save_invoice(self, invoice: Invoice, *, write_to_disk: bool = True) -> None:
5✔
1254
        key = invoice.get_id()
×
1255
        if not invoice.is_lightning():
×
1256
            if self.is_onchain_invoice_paid(invoice)[0]:
×
UNCOV
1257
                _logger.info("saving invoice... but it is already paid!")
×
UNCOV
1258
            with self.transaction_lock:
×
1259
                for txout in invoice.get_outputs():
×
1260
                    self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(key)
×
UNCOV
1261
        self._invoices[key] = invoice
×
UNCOV
1262
        if write_to_disk:
×
1263
            self.save_db()
×
1264

1265
    def clear_invoices(self):
5✔
UNCOV
1266
        self._invoices.clear()
×
UNCOV
1267
        self.save_db()
×
1268

1269
    def clear_requests(self):
5✔
1270
        self._receive_requests.clear()
×
UNCOV
1271
        self._requests_addr_to_key.clear()
×
UNCOV
1272
        self.save_db()
×
1273

1274
    def get_invoices(self) -> List[Invoice]:
5✔
UNCOV
1275
        out = list(self._invoices.values())
×
UNCOV
1276
        out.sort(key=lambda x:x.time)
×
1277
        return out
×
1278

1279
    def get_unpaid_invoices(self) -> List[Invoice]:
5✔
1280
        invoices = self.get_invoices()
×
1281
        return [x for x in invoices if self.get_invoice_status(x) != PR_PAID]
×
1282

1283
    def get_invoice(self, invoice_id):
5✔
1284
        return self._invoices.get(invoice_id)
×
1285

1286
    def import_requests(self, path):
5✔
1287
        data = read_json_file(path)
×
UNCOV
1288
        for x in data:
×
UNCOV
1289
            try:
×
UNCOV
1290
                req = Request(**x)
×
1291
            except Exception:
×
UNCOV
1292
                raise FileImportFailed(_("Invalid invoice format"))
×
UNCOV
1293
            self.add_payment_request(req, write_to_disk=False)
×
1294
        self.save_db()
×
1295

1296
    def export_requests(self, path):
5✔
1297
        # note: this does not export preimages for LN bolt11 invoices
1298
        write_json_file(path, list(self._receive_requests.values()))
×
1299

1300
    def import_invoices(self, path):
5✔
1301
        data = read_json_file(path)
×
UNCOV
1302
        for x in data:
×
UNCOV
1303
            try:
×
1304
                invoice = Invoice(**x)
×
UNCOV
1305
            except Exception:
×
UNCOV
1306
                raise FileImportFailed(_("Invalid invoice format"))
×
UNCOV
1307
            self.save_invoice(invoice, write_to_disk=False)
×
1308
        self.save_db()
×
1309

1310
    def export_invoices(self, path):
5✔
UNCOV
1311
        write_json_file(path, list(self._invoices.values()))
×
1312

1313
    def get_relevant_invoices_for_tx(self, tx_hash: Optional[str]) -> Sequence[Invoice]:
5✔
1314
        if not tx_hash:
5✔
UNCOV
1315
            return []
×
1316
        invoice_keys = self._invoices_from_txid_map.get(tx_hash, set())
5✔
1317
        invoices = [self.get_invoice(key) for key in invoice_keys]
5✔
1318
        invoices = [inv for inv in invoices if inv]  # filter out None
5✔
1319
        for inv in invoices:
5✔
UNCOV
1320
            assert isinstance(inv, Invoice), f"unexpected type {type(inv)}"
×
1321
        return invoices
5✔
1322

1323
    def _init_requests_rhash_index(self):
5✔
1324
        # self._requests_addr_to_key may contain addresses that can be reused
1325
        # this is checked in get_request_by_address
1326
        self._requests_addr_to_key = defaultdict(set)  # type: Dict[str, Set[str]]
5✔
1327
        for req in self._receive_requests.values():
5✔
1328
            if addr := req.get_address():
5✔
1329
                self._requests_addr_to_key[addr].add(req.get_id())
5✔
1330

1331
    def _prepare_onchain_invoice_paid_detection(self):
5✔
1332
        self._invoices_from_txid_map = defaultdict(set)  # type: Dict[str, Set[str]]
5✔
1333
        self._invoices_from_scriptpubkey_map = defaultdict(set)  # type: Dict[bytes, Set[str]]
5✔
1334
        self._update_onchain_invoice_paid_detection(self._invoices.keys())
5✔
1335

1336
    def _update_onchain_invoice_paid_detection(self, invoice_keys: Iterable[str]) -> None:
5✔
1337
        for invoice_key in invoice_keys:
5✔
1338
            invoice = self._invoices.get(invoice_key)
5✔
1339
            if not invoice:
5✔
UNCOV
1340
                continue
×
1341
            if invoice.is_lightning() and not invoice.get_address():
5✔
1342
                continue
5✔
1343
            if invoice.is_lightning() and self.lnworker and self.lnworker.get_invoice_status(invoice) == PR_PAID:
5✔
1344
                continue
5✔
1345
            is_paid, conf_needed, relevant_txs = self._is_onchain_invoice_paid(invoice)
5✔
1346
            if is_paid:
5✔
1347
                for txid in relevant_txs:
5✔
1348
                    self._invoices_from_txid_map[txid].add(invoice_key)
5✔
1349
            for txout in invoice.get_outputs():
5✔
1350
                self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key)
5✔
1351
            # update invoice status
1352
            status = self.get_invoice_status(invoice)
5✔
1353
            util.trigger_callback('invoice_status', self, invoice_key, status)
5✔
1354

1355
    def _is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional[int], Sequence[str]]:
5✔
1356
        """Returns whether on-chain invoice/request is satisfied, num confs required txs have,
1357
        and list of relevant TXIDs.
1358
        """
1359
        outputs = invoice.get_outputs()
5✔
1360
        if not outputs:  # e.g. lightning-only
5✔
1361
            return False, None, []
5✔
1362
        invoice_amounts = defaultdict(int)  # type: Dict[bytes, int]  # scriptpubkey -> value_sats
5✔
1363
        for txo in outputs:  # type: PartialTxOutput
5✔
1364
            invoice_amounts[txo.scriptpubkey] += 1 if parse_max_spend(txo.value) else txo.value
5✔
1365
        relevant_txs = set()
5✔
1366
        is_paid = True
5✔
1367
        conf_needed = None  # type: Optional[int]
5✔
1368
        with self.lock, self.transaction_lock:
5✔
1369
            for invoice_scriptpubkey, invoice_amt in invoice_amounts.items():
5✔
1370
                scripthash = bitcoin.script_to_scripthash(invoice_scriptpubkey)
5✔
1371
                prevouts_and_values = self.db.get_prevouts_by_scripthash(scripthash)
5✔
1372
                confs_and_values = []
5✔
1373
                for prevout, v in prevouts_and_values:
5✔
1374
                    relevant_txs.add(prevout.txid.hex())
5✔
1375
                    tx_height = self.adb.get_tx_height(prevout.txid.hex())
5✔
1376
                    if 0 < tx_height.height <= invoice.height:  # exclude txs older than invoice
5✔
1377
                        continue
5✔
1378
                    confs_and_values.append((tx_height.conf or 0, v))
5✔
1379
                # check that there is at least one TXO, and that they pay enough.
1380
                # note: "at least one TXO" check is needed for zero amount invoice (e.g. OP_RETURN)
1381
                vsum = 0
5✔
1382
                for conf, v in reversed(sorted(confs_and_values)):
5✔
1383
                    vsum += v
5✔
1384
                    if vsum >= invoice_amt:
5✔
1385
                        conf_needed = min(conf_needed, conf) if conf_needed is not None else conf
5✔
1386
                        break
5✔
1387
                else:
1388
                    is_paid = False
5✔
1389
        return is_paid, conf_needed, list(relevant_txs)
5✔
1390

1391
    def is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional[int]]:
5✔
1392
        is_paid, conf_needed, relevant_txs = self._is_onchain_invoice_paid(invoice)
5✔
1393
        return is_paid, conf_needed
5✔
1394

1395
    @profiler
5✔
1396
    def get_full_history(self, fx=None, *, onchain_domain=None, include_lightning=True, include_fiat=False) -> dict:
5✔
1397
        """
1398
        includes both onchain and lightning
1399
        includes grouping information
1400
        """
UNCOV
1401
        transactions_tmp = OrderedDictWithIndex()
×
1402
        # add on-chain txns
1403
        onchain_history = self.get_onchain_history(domain=onchain_domain)
×
1404
        for tx_item in onchain_history.values():
×
1405
            txid = tx_item.txid
×
1406
            transactions_tmp[txid] = tx_item.to_dict()
×
1407
            transactions_tmp[txid]['lightning'] = False
×
1408

1409
        # add lightning_transactions
UNCOV
1410
        lightning_history = self.lnworker.get_lightning_history() if self.lnworker and include_lightning else {}
×
1411
        for tx_item in lightning_history.values():
×
1412
            key = tx_item.payment_hash or 'ln:' + tx_item.group_id
×
1413
            transactions_tmp[key] = tx_item.to_dict()
×
1414
            transactions_tmp[key]['lightning'] = True
×
1415

1416
        # sort on-chain and LN stuff into new dict, by timestamp
1417
        # (we rely on this being a *stable* sort)
1418
        def sort_key(x):
×
1419
            txid, tx_item = x
×
1420
            ts = tx_item.get('monotonic_timestamp') or tx_item.get('timestamp') or float('inf')
×
1421
            height = self.adb.tx_height_to_sort_height(tx_item.get('height'))
×
1422
            return ts, height
×
1423
        # create groups
1424
        transactions = OrderedDictWithIndex()
×
1425
        for k, tx_item in sorted(list(transactions_tmp.items()), key=sort_key):
×
UNCOV
1426
            if 'ln_value' not in tx_item:
×
1427
                tx_item['ln_value'] = Satoshis(0)
×
1428
            if 'bc_value' not in tx_item:
×
1429
                tx_item['bc_value'] = Satoshis(0)
×
1430
            group_id = tx_item.get('group_id')
×
1431
            if not group_id:
×
UNCOV
1432
                transactions[k] = tx_item
×
1433
            else:
UNCOV
1434
                key = 'group:' + group_id
×
UNCOV
1435
                parent = transactions.get(key)
×
UNCOV
1436
                group_label = self.get_label_for_group(group_id)
×
UNCOV
1437
                if parent is None:
×
UNCOV
1438
                    parent = {
×
1439
                        'label': group_label,
1440
                        'fiat_value': Fiat(Decimal(0), fx.ccy) if fx else None,
1441
                        'bc_value': Satoshis(0),
1442
                        'ln_value': Satoshis(0),
1443
                        'value': Satoshis(0),
1444
                        'children': [],
1445
                        'timestamp': 0,
1446
                        'date': timestamp_to_datetime(0),
1447
                        'fee_sat': 0,
1448
                        # fixme: there is no guarantee that there will be an onchain tx in the group
1449
                        'height': 0,
1450
                        'confirmations': 0,
1451
                        'txid': '----',
1452
                    }
1453
                    transactions[key] = parent
×
1454
                parent['bc_value'] += tx_item['bc_value']
×
1455
                parent['ln_value'] += tx_item['ln_value']
×
1456
                parent['value'] = parent['bc_value'] + parent['ln_value']
×
1457
                if 'fiat_value' in tx_item:
×
1458
                    parent['fiat_value'] += tx_item['fiat_value']
×
1459
                if tx_item.get('txid') == group_id:
×
1460
                    parent['lightning'] = False
×
UNCOV
1461
                    parent['txid'] = tx_item['txid']
×
1462
                    parent['timestamp'] = tx_item['timestamp']
×
1463
                    parent['date'] = timestamp_to_datetime(tx_item['timestamp'])
×
1464
                    parent['height'] = tx_item['height']
×
1465
                    parent['confirmations'] = tx_item['confirmations']
×
1466
                    parent['wanted_height'] = tx_item.get('wanted_height')
×
UNCOV
1467
                parent['children'].append(tx_item)
×
1468

1469
        now = time.time()
×
1470
        for key, item in transactions.items():
×
1471
            children = item.get('children', [])
×
1472
            if len(children) == 1:
×
1473
                transactions[key] = children[0]
×
1474
            # add on-chain and lightning values
1475
            # note: 'value' has msat precision (as LN has msat precision)
1476
            item['value'] = item.get('bc_value', Satoshis(0)) + item.get('ln_value', Satoshis(0))
×
1477
            for child in item.get('children', []):
×
UNCOV
1478
                child['value'] = child.get('bc_value', Satoshis(0)) + child.get('ln_value', Satoshis(0))
×
1479
            if include_fiat:
×
1480
                value = item['value'].value
×
1481
                txid = item.get('txid')
×
1482
                if not item.get('lightning') and txid:
×
1483
                    fiat_fields = self.get_tx_item_fiat(tx_hash=txid, amount_sat=value, fx=fx, tx_fee=item['fee_sat'])
×
UNCOV
1484
                    item.update(fiat_fields)
×
1485
                else:
UNCOV
1486
                    timestamp = item['timestamp'] or now
×
UNCOV
1487
                    fiat_value = value / Decimal(bitcoin.COIN) * fx.timestamp_rate(timestamp)
×
UNCOV
1488
                    item['fiat_value'] = Fiat(fiat_value, fx.ccy)
×
1489
                    item['fiat_default'] = True
×
1490
        return transactions
×
1491

1492
    @profiler
5✔
1493
    def get_onchain_capital_gains(self, fx, **kwargs):
5✔
1494
        # History with capital gains, using utxo pricing
1495
        # FIXME: Lightning capital gains would requires FIFO
1496
        from_timestamp = kwargs.get('from_timestamp')
×
1497
        to_timestamp = kwargs.get('to_timestamp')
×
1498
        history = self.get_onchain_history(**kwargs)
×
1499
        show_fiat = fx and fx.is_enabled() and fx.has_history()
×
1500
        out = []
×
1501
        income = 0
×
1502
        expenditures = 0
×
1503
        capital_gains = Decimal(0)
×
1504
        fiat_income = Decimal(0)
×
1505
        fiat_expenditures = Decimal(0)
×
UNCOV
1506
        for txid, hitem in history.items():
×
1507
            item = hitem.to_dict()
×
1508
            if item['bc_value'].value == 0:
×
1509
                continue
×
UNCOV
1510
            timestamp = item['timestamp']
×
1511
            tx_hash = item['txid']
×
UNCOV
1512
            tx_fee = item['fee_sat']
×
1513
            # fixme: use in and out values
1514
            value = item['bc_value'].value
×
1515
            if value < 0:
×
1516
                expenditures += -value
×
1517
            else:
1518
                income += value
×
1519
            # fiat computations
1520
            if show_fiat:
×
1521
                fiat_fields = self.get_tx_item_fiat(tx_hash=tx_hash, amount_sat=value, fx=fx, tx_fee=tx_fee)
×
UNCOV
1522
                fiat_value = fiat_fields['fiat_value'].value
×
1523
                if value < 0:
×
1524
                    capital_gains += fiat_fields['capital_gain'].value
×
1525
                    fiat_expenditures += -fiat_value
×
1526
                else:
1527
                    fiat_income += fiat_value
×
UNCOV
1528
            out.append(item)
×
1529
        # add summary
1530
        if out:
×
1531
            first_item = out[0]
×
1532
            last_item = out[-1]
×
UNCOV
1533
            start_height = first_item['height'] - 1
×
1534
            end_height = last_item['height']
×
1535

1536
            b = first_item['bc_balance'].value
×
UNCOV
1537
            v = first_item['bc_value'].value
×
1538
            start_balance = None if b is None or v is None else b - v
×
1539
            end_balance = last_item['bc_balance'].value
×
1540

1541
            if from_timestamp is not None and to_timestamp is not None:
×
UNCOV
1542
                start_timestamp = from_timestamp
×
UNCOV
1543
                end_timestamp = to_timestamp
×
1544
            else:
UNCOV
1545
                start_timestamp = first_item['timestamp']
×
1546
                end_timestamp = last_item['timestamp']
×
1547

UNCOV
1548
            start_coins = self.get_utxos(
×
1549
                block_height=start_height,
1550
                confirmed_funding_only=True,
1551
                confirmed_spending_only=True,
1552
                nonlocal_only=True)
1553
            end_coins = self.get_utxos(
×
1554
                block_height=end_height,
1555
                confirmed_funding_only=True,
1556
                confirmed_spending_only=True,
1557
                nonlocal_only=True)
1558

1559
            def summary_point(timestamp, height, balance, coins):
×
1560
                date = timestamp_to_datetime(timestamp)
×
1561
                out = {
×
1562
                    'date': date,
1563
                    'block_height': height,
1564
                    'BTC_balance': Satoshis(balance),
1565
                }
1566
                if show_fiat:
×
1567
                    ap = self.acquisition_price(coins, fx.timestamp_rate, fx.ccy)
×
UNCOV
1568
                    lp = self.liquidation_price(coins, fx.timestamp_rate, timestamp)
×
1569
                    out['acquisition_price'] = Fiat(ap, fx.ccy)
×
1570
                    out['liquidation_price'] = Fiat(lp, fx.ccy)
×
1571
                    out['unrealized_gains'] = Fiat(lp - ap, fx.ccy)
×
UNCOV
1572
                    out['fiat_balance'] = Fiat(fx.historical_value(balance, date), fx.ccy)
×
UNCOV
1573
                    out['BTC_fiat_price'] = Fiat(fx.historical_value(COIN, date), fx.ccy)
×
UNCOV
1574
                return out
×
1575

1576
            summary_start = summary_point(start_timestamp, start_height, start_balance, start_coins)
×
1577
            summary_end = summary_point(end_timestamp, end_height, end_balance, end_coins)
×
1578
            flow = {
×
1579
                'BTC_incoming': Satoshis(income),
1580
                'BTC_outgoing': Satoshis(expenditures)
1581
            }
UNCOV
1582
            if show_fiat:
×
UNCOV
1583
                flow['fiat_currency'] = fx.ccy
×
UNCOV
1584
                flow['fiat_incoming'] = Fiat(fiat_income, fx.ccy)
×
UNCOV
1585
                flow['fiat_outgoing'] = Fiat(fiat_expenditures, fx.ccy)
×
UNCOV
1586
                flow['realized_capital_gains'] = Fiat(capital_gains, fx.ccy)
×
1587
            summary = {
×
1588
                'begin': summary_start,
1589
                'end': summary_end,
1590
                'flow': flow,
1591
            }
1592

1593
        else:
1594
            summary = {}
×
1595
        return summary
×
1596

1597
    def acquisition_price(self, coins, price_func, ccy):
5✔
UNCOV
1598
        return Decimal(sum(self.coin_price(coin.prevout.txid.hex(), price_func, ccy, self.adb.get_txin_value(coin)) for coin in coins))
×
1599

1600
    def liquidation_price(self, coins, price_func, timestamp):
5✔
UNCOV
1601
        p = price_func(timestamp)
×
UNCOV
1602
        return sum([coin.value_sats() for coin in coins]) * p / Decimal(COIN)
×
1603

1604
    def default_fiat_value(self, tx_hash, fx, value_sat):
5✔
1605
        return value_sat / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate)
5✔
1606

1607
    def get_tx_item_fiat(
5✔
1608
            self,
1609
            *,
1610
            tx_hash: str,
1611
            amount_sat: int,
1612
            fx: 'FxThread',
1613
            tx_fee: Optional[int],
1614
    ) -> Dict[str, Any]:
1615
        item = {}
×
1616
        fiat_value = self.get_fiat_value(tx_hash, fx.ccy)
×
1617
        fiat_default = fiat_value is None
×
1618
        fiat_rate = self.price_at_timestamp(tx_hash, fx.timestamp_rate)
×
1619
        fiat_value = fiat_value if fiat_value is not None else self.default_fiat_value(tx_hash, fx, amount_sat)
×
1620
        fiat_fee = tx_fee / Decimal(COIN) * fiat_rate if tx_fee is not None else None
×
1621
        item['fiat_currency'] = fx.ccy
×
1622
        item['fiat_rate'] = Fiat(fiat_rate, fx.ccy)
×
1623
        item['fiat_value'] = Fiat(fiat_value, fx.ccy)
×
1624
        item['fiat_fee'] = Fiat(fiat_fee, fx.ccy) if fiat_fee is not None else None
×
1625
        item['fiat_default'] = fiat_default
×
UNCOV
1626
        if amount_sat < 0:
×
UNCOV
1627
            acquisition_price = - amount_sat / Decimal(COIN) * self.average_price(tx_hash, fx.timestamp_rate, fx.ccy)
×
UNCOV
1628
            liquidation_price = - fiat_value
×
1629
            item['acquisition_price'] = Fiat(acquisition_price, fx.ccy)
×
UNCOV
1630
            cg = liquidation_price - acquisition_price
×
UNCOV
1631
            item['capital_gain'] = Fiat(cg, fx.ccy)
×
UNCOV
1632
        return item
×
1633

1634
    def _get_label(self, key: str) -> str:
5✔
1635
        # key is typically: address / txid / LN-payment-hash-hex
UNCOV
1636
        return self._labels.get(key) or ''
×
1637

1638
    def get_label_for_address(self, addr: str) -> str:
5✔
1639
        label = self._labels.get(addr) or ''
5✔
1640
        if not label and (request := self.get_request_by_addr(addr)):
5✔
UNCOV
1641
            label = request.get_message()
×
1642
        return label
5✔
1643

1644
    def set_default_label(self, key:str, value:str):
5✔
UNCOV
1645
        self._default_labels[key] = value
×
1646

1647
    def get_label_for_outpoint(self, outpoint:str) -> str:
5✔
1648
        return self._labels.get(outpoint) or self._get_default_label_for_outpoint(outpoint)
5✔
1649

1650
    def _get_default_label_for_outpoint(self, outpoint: str) -> str:
5✔
1651
        return self._default_labels.get(outpoint)
5✔
1652

1653
    def get_label_for_group(self, group_id: str) -> str:
5✔
UNCOV
1654
        return self._default_labels.get('group:' + group_id)
×
1655

1656
    def set_group_label(self, group_id: str, label: str):
5✔
1657
        self._default_labels['group:' + group_id] = label
×
1658

1659
    def get_label_for_txid(self, tx_hash: str) -> str:
5✔
1660
        return self._labels.get(tx_hash) or self._get_default_label_for_txid(tx_hash)
5✔
1661

1662
    def _get_default_label_for_txid(self, tx_hash: str) -> str:
5✔
1663
        if label := self._default_labels.get(tx_hash):
5✔
1664
            return label
×
1665
        labels = []
5✔
1666
        tx = self.adb.get_transaction(tx_hash)
5✔
1667
        if tx:
5✔
1668
            for i in range(len(tx.outputs())):
5✔
1669
                outpoint = tx_hash + f':{i}'
5✔
1670
                if label := self.get_label_for_outpoint(outpoint):
5✔
UNCOV
1671
                    labels.append(label)
×
1672
            for txin in tx.inputs():
5✔
1673
                outpoint = txin.prevout.to_str()
5✔
1674
                if label := self.get_label_for_outpoint(outpoint):
5✔
1675
                    labels.append(label)
×
1676

1677
        # note: we don't deserialize tx as the history calls us for every tx, and that would be slow
1678
        if not self.db.get_txi_addresses(tx_hash):
5✔
1679
            # no inputs are ismine -> likely incoming payment -> concat labels of output addresses
1680
            for addr in self.db.get_txo_addresses(tx_hash):
×
1681
                label = self.get_label_for_address(addr)
×
UNCOV
1682
                if label:
×
UNCOV
1683
                    labels.append(label)
×
1684
        else:
1685
            # some inputs are ismine -> likely outgoing payment
1686
            for invoice in self.get_relevant_invoices_for_tx(tx_hash):
5✔
1687
                if invoice.message:
×
1688
                    labels.append(invoice.message)
×
1689
        #if not labels and self.lnworker and (label:= self.lnworker.get_label_for_txid(tx_hash)):
1690
        #    labels.append(label)
1691
        return ', '.join(labels)
5✔
1692

1693
    def _get_default_label_for_rhash(self, rhash: str) -> str:
5✔
1694
        req = self.get_request(rhash)
×
1695
        return req.get_message() if req else ''
×
1696

1697
    def get_label_for_rhash(self, rhash: str) -> str:
5✔
UNCOV
1698
        return self._labels.get(rhash) or self._get_default_label_for_rhash(rhash)
×
1699

1700
    def get_all_labels(self) -> Dict[str, str]:
5✔
UNCOV
1701
        with self.lock:
×
UNCOV
1702
            return copy.copy(self._labels)
×
1703

1704
    def get_tx_status(self, tx_hash: str, tx_mined_info: TxMinedInfo):
5✔
1705
        extra = []
5✔
1706
        height = tx_mined_info.height
5✔
1707
        conf = tx_mined_info.conf
5✔
1708
        timestamp = tx_mined_info.timestamp
5✔
1709
        if height == TX_HEIGHT_FUTURE:
5✔
UNCOV
1710
            num_blocks_remainining = tx_mined_info.wanted_height - self.adb.get_local_height()
×
UNCOV
1711
            num_blocks_remainining = max(0, num_blocks_remainining)
×
UNCOV
1712
            return 2, _('in {} blocks').format(num_blocks_remainining)
×
1713
        if conf == 0:
5✔
1714
            tx = self.db.get_transaction(tx_hash)
5✔
1715
            if not tx:
5✔
UNCOV
1716
                return 2, _("unknown")
×
1717
            if not tx.is_complete():
5✔
1718
                tx.add_info_from_wallet(self)  # needed for estimated_size(), for txin size calc
5✔
1719
            fee = self.adb.get_tx_fee(tx_hash)
5✔
1720
            if fee is not None:
5✔
1721
                size = tx.estimated_size()
5✔
1722
                fee_per_byte = fee / size
5✔
1723
                extra.append(format_fee_satoshis(fee_per_byte) + f" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VB}")
5✔
1724
            if fee is not None and height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED) \
5✔
1725
               and self.network and self.network.has_fee_mempool():
1726
                exp_n = self.network.mempool_fees.fee_to_depth(fee_per_byte)
×
1727
                if exp_n is not None:
×
UNCOV
1728
                    extra.append(FeePolicy.get_depth_mb_str(exp_n))
×
1729
            if height == TX_HEIGHT_LOCAL:
5✔
1730
                status = 3
5✔
1731
            elif height == TX_HEIGHT_UNCONF_PARENT:
×
UNCOV
1732
                status = 1
×
UNCOV
1733
            elif height == TX_HEIGHT_UNCONFIRMED:
×
UNCOV
1734
                status = 0
×
1735
            else:
UNCOV
1736
                status = 2  # not SPV verified
×
1737
        else:
UNCOV
1738
            status = 3 + min(conf, 6)
×
1739
        time_str = format_time(timestamp) if timestamp else _("unknown")
5✔
1740
        status_str = TX_STATUS[status] if status < 4 else time_str
5✔
1741
        if extra:
5✔
1742
            status_str += ' [%s]'%(', '.join(extra))
5✔
1743
        return status, status_str
5✔
1744

1745
    def relayfee(self):
5✔
1746
        return relayfee(self.network)
5✔
1747

1748
    def dust_threshold(self):
5✔
1749
        return dust_threshold(self.network)
5✔
1750

1751
    def get_candidates_for_batching(self, outputs, coins) -> Sequence[Transaction]:
5✔
1752
        # do not batch if we spend max (not supported by make_unsigned_transaction)
1753
        if any([parse_max_spend(o.value) is not None for o in outputs]):
5✔
UNCOV
1754
            return []
×
1755
        candidates = []
5✔
1756
        domain = self.get_addresses()
5✔
1757
        for hist_item in self.adb.get_history(domain):
5✔
1758
            # tx should not be mined yet
1759
            if hist_item.tx_mined_status.conf > 0: continue
5✔
1760
            # conservative future proofing of code: only allow known unconfirmed types
1761
            if hist_item.tx_mined_status.height not in (
5✔
1762
                    TX_HEIGHT_UNCONFIRMED,
1763
                    TX_HEIGHT_UNCONF_PARENT,
1764
                    TX_HEIGHT_LOCAL):
UNCOV
1765
                continue
×
1766
            # tx should be "outgoing" from wallet
1767
            if hist_item.delta >= 0:
5✔
1768
                continue
5✔
1769
            tx = self.db.get_transaction(hist_item.txid)
5✔
1770
            if not tx:
5✔
UNCOV
1771
                continue
×
1772
            txid = tx.txid()
5✔
1773
            # tx should not belong to tx batcher
1774
            if self.txbatcher.is_mine(txid):
5✔
UNCOV
1775
                continue
×
1776
            # is_mine outputs should not be spent yet
1777
            # to avoid cancelling our own dependent transactions
1778
            if any([self.is_mine(o.address) and self.db.get_spent_outpoint(txid, output_idx)
5✔
1779
                    for output_idx, o in enumerate(tx.outputs())]):
UNCOV
1780
                continue
×
1781
            # all inputs should be is_mine
1782
            if not all([self.is_mine(self.adb.get_txin_address(txin)) for txin in tx.inputs()]):
5✔
UNCOV
1783
                continue
×
1784
            # tx must have opted-in for RBF (even if local, for consistency)
1785
            if not self.can_rbf_tx(tx):
5✔
1786
                continue
×
1787
            # reject merge if we need to spend outputs from the base tx
1788
            remaining_amount = sum(c.value_sats() for c in coins if c.prevout.txid.hex() != tx.txid())
5✔
1789
            change_amount = sum(o.value for o in tx.outputs() if self.is_change(o.address))
5✔
1790
            output_amount = sum(o.value for o in outputs)
5✔
1791
            if output_amount > remaining_amount + change_amount:
5✔
1792
                continue
5✔
UNCOV
1793
            candidates.append(tx)
×
1794
        return candidates
5✔
1795

1796
    def get_change_addresses_for_new_transaction(
5✔
1797
            self, preferred_change_addr=None, *, allow_reusing_used_change_addrs: bool = True,
1798
    ) -> List[str]:
1799
        change_addrs = []
5✔
1800
        if preferred_change_addr:
5✔
1801
            if isinstance(preferred_change_addr, (list, tuple)):
5✔
1802
                change_addrs = list(preferred_change_addr)
5✔
1803
            else:
UNCOV
1804
                change_addrs = [preferred_change_addr]
×
1805
        elif self.use_change:
5✔
1806
            # Recalc and get unused change addresses
1807
            addrs = self.calc_unused_change_addresses()
5✔
1808
            # New change addresses are created only after a few
1809
            # confirmations.
1810
            if addrs:
5✔
1811
                # if there are any unused, select all
1812
                change_addrs = addrs
5✔
1813
            else:
1814
                # if there are none, take one randomly from the last few
1815
                if not allow_reusing_used_change_addrs:
5✔
1816
                    return []
5✔
UNCOV
1817
                addrs = self.get_change_addresses(slice_start=-self.gap_limit_for_change)
×
UNCOV
1818
                change_addrs = [random.choice(addrs)] if addrs else []
×
1819
        for addr in change_addrs:
5✔
1820
            assert is_address(addr), f"not valid bitcoin address: {addr}"
5✔
1821
            # note that change addresses are not necessarily ismine
1822
            # in which case this is a no-op
1823
            self.check_address_for_corruption(addr)
5✔
1824
        max_change = self.max_change_outputs if self.multiple_change else 1
5✔
1825
        return change_addrs[:max_change]
5✔
1826

1827
    def get_single_change_address_for_new_transaction(
5✔
1828
            self, preferred_change_addr=None, *, allow_reusing_used_change_addrs: bool = True,
1829
    ) -> Optional[str]:
1830
        addrs = self.get_change_addresses_for_new_transaction(
5✔
1831
            preferred_change_addr=preferred_change_addr,
1832
            allow_reusing_used_change_addrs=allow_reusing_used_change_addrs,
1833
        )
1834
        if addrs:
5✔
1835
            return addrs[0]
5✔
1836
        return None
×
1837

1838
    @check_returned_address_for_corruption
5✔
1839
    def get_new_sweep_address_for_channel(self) -> str:
5✔
1840
        # Recalc and get unused change addresses
1841
        addrs = self.calc_unused_change_addresses()
×
UNCOV
1842
        if addrs:
×
1843
            selected_addr = addrs[0]
×
1844
        else:
1845
            # if there are none, take one randomly from the last few
UNCOV
1846
            addrs = self.get_change_addresses(slice_start=-self.gap_limit_for_change)
×
UNCOV
1847
            if addrs:
×
UNCOV
1848
                selected_addr = random.choice(addrs)
×
1849
            else:  # fallback for e.g. imported wallets
UNCOV
1850
                selected_addr = self.get_receiving_address()
×
UNCOV
1851
        assert is_address(selected_addr), f"not valid bitcoin address: {selected_addr}"
×
UNCOV
1852
        return selected_addr
×
1853

1854
    def should_keep_reserve_utxo(
5✔
1855
            self,
1856
            tx_inputs: List[PartialTxInput],
1857
            tx_outputs: List[PartialTxOutput],
1858
            is_anchor_channel_opening: bool,
1859
    ) -> bool:
1860
        channels_need_reserve = self.lnworker and self.lnworker.has_anchor_channels()
5✔
1861
        # note: is_anchor_channel_opening is used in unit tests, without lnworker
1862
        is_reserve_needed = is_anchor_channel_opening or channels_need_reserve
5✔
1863
        if not is_reserve_needed:
5✔
1864
            return False
5✔
1865

1866
        coins_in_wallet = self.get_spendable_coins(nonlocal_only=False, confirmed_only=False)
5✔
1867
        prevout_coins_in_wallet = set(c.prevout for c in coins_in_wallet)
5✔
1868
        amount_in_wallet = sum(c.value_sats() for c in coins_in_wallet)
5✔
1869

1870
        amount_consumed = sum(c.value_sats() for c in tx_inputs if c.prevout in prevout_coins_in_wallet)
5✔
1871
        amount_retained = sum(o.value for o in tx_outputs if self.is_mine(o.address))
5✔
1872
        to_be_spent_sat = amount_consumed - amount_retained
5✔
1873

1874
        assert amount_in_wallet - to_be_spent_sat >= 0
5✔
1875
        if amount_in_wallet - to_be_spent_sat >= self.config.LN_UTXO_RESERVE:
5✔
1876
            # there will be enough remaining after we send
1877
            return False
5✔
1878
        # we will need to subtract the reserve
1879
        self.logger.info(f'we should keep a reserve: {to_be_spent_sat=}, {amount_in_wallet=}')
5✔
1880
        return True
5✔
1881

1882
    def is_low_reserve(self) -> bool:
5✔
UNCOV
1883
        return self.should_keep_reserve_utxo([], [], False)
×
1884

1885
    @profiler(min_threshold=0.1)
5✔
1886
    def make_unsigned_transaction(
5✔
1887
            self, *,
1888
            coins: Optional[Sequence[PartialTxInput]] = None,
1889
            outputs: List[PartialTxOutput],
1890
            inputs: Optional[List[PartialTxInput]] = None,
1891
            fee_policy: FeePolicy,
1892
            change_addr: str = None,
1893
            is_sweep: bool = False,  # used by Wallet_2fa subclass
1894
            rbf: bool = True,
1895
            BIP69_sort: Optional[bool] = True,
1896
            base_tx: Optional[Transaction] = None,
1897
            send_change_to_lightning: bool = False,
1898
            merge_duplicate_outputs: bool = False,
1899
            locktime: Optional[int] = None,
1900
            tx_version: Optional[int] = None,
1901
            is_anchor_channel_opening: bool = False,
1902
    ) -> PartialTransaction:
1903
        """Can raise NotEnoughFunds or NoDynamicFeeEstimates."""
1904

1905
        if coins is None:
5✔
1906
            coins = self.get_spendable_coins()
5✔
1907
        if not inputs and not coins:  # any bitcoin tx must have at least 1 input by consensus
5✔
1908
            raise NotEnoughFunds()
5✔
1909
        if any([c.already_has_some_signatures() for c in coins]):
5✔
UNCOV
1910
            raise Exception("Some inputs already contain signatures!")
×
1911
        if inputs is None:
5✔
1912
            inputs = []
5✔
1913
        # make sure inputs and coins do not overlap
1914
        if inputs:
5✔
1915
            input_set = set(txin.prevout for txin in inputs)
5✔
1916
            coins = [coin for coin in coins if (coin.prevout not in input_set)]
5✔
1917

1918
        # prevent side-effect with '!'
1919
        outputs = copy.deepcopy(outputs)
5✔
1920

1921
        # check outputs for "max" amount
1922
        i_max = []
5✔
1923
        i_max_sum = 0
5✔
1924
        for i, o in enumerate(outputs):
5✔
1925
            weight = parse_max_spend(o.value)
5✔
1926
            if weight:
5✔
1927
                i_max_sum += weight
5✔
1928
                i_max.append((weight, i))
5✔
1929

1930
        for txin in coins:
5✔
1931
            self.add_input_info(txin)
5✔
1932
            nSequence = 0xffffffff - (2 if rbf else 1)
5✔
1933
            txin.nsequence = nSequence
5✔
1934

1935
        fee_estimator = partial(fee_policy.estimate_fee, network=self.network)
5✔
1936

1937
        # set if we merge with another transaction
1938
        rbf_merge_txid = None
5✔
1939

1940
        if len(i_max) == 0:
5✔
1941
            # Let the coin chooser select the coins to spend
1942
            coin_chooser = coinchooser.get_coin_chooser(self.config)
5✔
1943
            # If there is an unconfirmed RBF tx, merge with it
1944
            if base_tx:
5✔
1945
                # make sure we don't try to spend change from the tx-to-be-replaced:
1946
                coins = [c for c in coins if c.prevout.txid.hex() != base_tx.txid()]
5✔
1947
                is_local = self.adb.get_tx_height(base_tx.txid()).height == TX_HEIGHT_LOCAL
5✔
1948
                if not isinstance(base_tx, PartialTransaction):
5✔
UNCOV
1949
                    base_tx = PartialTransaction.from_tx(base_tx)
×
UNCOV
1950
                    base_tx.add_info_from_wallet(self)
×
1951
                else:
1952
                    # don't cast PartialTransaction, because it removes make_witness
1953
                    base_tx.remove_signatures()
5✔
1954
                base_tx_fee = base_tx.get_fee()
5✔
1955
                base_feerate = Decimal(base_tx_fee)/base_tx.estimated_size()
5✔
1956
                relayfeerate = Decimal(self.relayfee()) / 1000
5✔
1957
                original_fee_estimator = fee_estimator
5✔
1958
                def fee_estimator(size: Union[int, float, Decimal]) -> int:
5✔
1959
                    size = Decimal(size)
5✔
1960
                    lower_bound_relayfee = int(base_tx_fee + round(size * relayfeerate)) if not is_local else 0
5✔
1961
                    lower_bound_feerate = int(base_feerate * size) + 1
5✔
1962
                    lower_bound = max(lower_bound_feerate, lower_bound_relayfee)
5✔
1963
                    return max(lower_bound, original_fee_estimator(size))
5✔
1964
                txi = base_tx.inputs() + list(inputs)
5✔
1965
                txo = list(filter(lambda o: not self.is_change(o.address), base_tx.outputs())) + list(outputs)
5✔
1966
                old_change_addrs = [o.address for o in base_tx.outputs() if self.is_change(o.address)]
5✔
1967
                rbf_merge_txid = base_tx.txid()
5✔
1968
            else:
1969
                txi = list(inputs)
5✔
1970
                txo = list(outputs)
5✔
1971
                old_change_addrs = []
5✔
1972
            # change address. if empty, coin_chooser will set it
1973
            change_addrs = self.get_change_addresses_for_new_transaction(change_addr or old_change_addrs)
5✔
1974
            if merge_duplicate_outputs:
5✔
1975
                txo = transaction.merge_duplicate_tx_outputs(txo)
5✔
1976
            if len(txo) == 0 or (self.lnworker and send_change_to_lightning):
5✔
1977
                # even if the option use multiple change outputs is enabled there should be only
1978
                # one change address if there are 0 txos as this is a sweep tx, or if we want to swap change to ln
1979
                change_addrs = change_addrs[0:1]
5✔
1980
            tx = coin_chooser.make_tx(
5✔
1981
                coins=coins,
1982
                inputs=txi,
1983
                outputs=txo,
1984
                change_addrs=change_addrs,
1985
                fee_estimator_vb=fee_estimator,
1986
                dust_threshold=self.dust_threshold(),
1987
                BIP69_sort=BIP69_sort)
1988
            if self.lnworker and send_change_to_lightning:
5✔
UNCOV
1989
                change = tx.get_change_outputs()
×
UNCOV
1990
                if len(change) == 1:
×
UNCOV
1991
                    amount = change[0].value
×
UNCOV
1992
                    if amount <= self.lnworker.num_sats_can_receive():
×
UNCOV
1993
                        tx.replace_output_address(change[0].address, DummyAddress.SWAP)
×
1994
            if self.should_keep_reserve_utxo(tx.inputs(), tx.outputs(), is_anchor_channel_opening):
5✔
1995
                raise NotEnoughFunds()
5✔
1996
            self.logger.debug(f'coinchooser returned tx with {len(tx.inputs())} inputs and {len(tx.outputs())} outputs')
5✔
1997

1998
        else:
1999
            # "spend max" branch
2000
            # note: This *will* spend inputs with negative effective value (if there are any).
2001
            #       Given as the user is spending "max", and so might be abandoning the wallet,
2002
            #       try to include all UTXOs, otherwise leftover might remain in the UTXO set
2003
            #       forever. see #5433
2004
            # note: Actually, it might be the case that not all UTXOs from the wallet are
2005
            #       being spent if the user manually selected UTXOs.
2006
            def distribute_amount(amount):
5✔
2007
                if amount < 0:
5✔
UNCOV
2008
                    raise NotEnoughFunds()
×
2009
                distr_amount = 0
5✔
2010
                for (weight, i) in i_max:
5✔
2011
                    # fixme: this does not check that value >= dust_threshold
2012
                    val = int((amount/i_max_sum) * weight)
5✔
2013
                    outputs[i].value = val
5✔
2014
                    distr_amount += val
5✔
2015
                (x,i) = i_max[-1]
5✔
2016
                outputs[i].value += (amount - distr_amount)
5✔
2017

2018
            tx_inputs = inputs + coins # these do not overlap, see above
5✔
2019
            distribute_amount(0)
5✔
2020
            tx = PartialTransaction.from_io(list(tx_inputs), list(outputs))
5✔
2021
            fee = fee_estimator(tx.estimated_size())
5✔
2022

2023
            input_amount = sum(c.value_sats() for c in tx_inputs)
5✔
2024
            allocated_amount = sum(o.value for o in outputs if not parse_max_spend(o.value))
5✔
2025
            to_distribute = input_amount - allocated_amount
5✔
2026
            distribute_amount(to_distribute - fee)
5✔
2027

2028
            if self.should_keep_reserve_utxo(tx_inputs, outputs, is_anchor_channel_opening):
5✔
2029
                self.logger.info(f'Adding change output to meet utxo reserve requirements')
5✔
2030
                change_addr = self.get_change_addresses_for_new_transaction(change_addr)[0]
5✔
2031
                change = PartialTxOutput.from_address_and_value(change_addr, self.config.LN_UTXO_RESERVE)
5✔
2032
                change.is_utxo_reserve = True # for GUI
5✔
2033
                outputs.append(change)
5✔
2034
                to_distribute -= change.value
5✔
2035
                tx = PartialTransaction.from_io(list(tx_inputs), list(outputs))
5✔
2036
                fee = fee_estimator(tx.estimated_size())
5✔
2037
                distribute_amount(to_distribute - fee)
5✔
2038

2039
            tx = PartialTransaction.from_io(list(tx_inputs), list(outputs))
5✔
2040

2041
        assert len(tx.outputs()) > 0, "any bitcoin tx must have at least 1 output by consensus"
5✔
2042
        if locktime is None:
5✔
2043
            # Timelock tx to current height.
2044
            locktime = get_locktime_for_new_transaction(self.network)
5✔
2045
        tx.locktime = locktime
5✔
2046
        if tx_version is not None:
5✔
2047
            tx.version = tx_version
5✔
2048
        tx.rbf_merge_txid = rbf_merge_txid
5✔
2049
        tx.add_info_from_wallet(self)
5✔
2050
        run_hook('make_unsigned_transaction', self, tx)
5✔
2051
        return tx
5✔
2052

2053
    def is_frozen_address(self, addr: str) -> bool:
5✔
UNCOV
2054
        return addr in self._frozen_addresses
×
2055

2056
    def is_frozen_coin(self, utxo: PartialTxInput) -> bool:
5✔
2057
        prevout_str = utxo.prevout.to_str()
5✔
2058
        frozen = self._frozen_coins.get(prevout_str, None)
5✔
2059
        # note: there are three possible states for 'frozen':
2060
        #       True/False if the user explicitly set it,
2061
        #       None otherwise
2062
        if frozen is not None:  # user has explicitly set the state
5✔
2063
            return bool(frozen)
5✔
2064
        # State not set. We implicitly mark certain coins as frozen:
2065
        tx_mined_status = self.adb.get_tx_height(utxo.prevout.txid.hex())
5✔
2066
        if tx_mined_status.height == TX_HEIGHT_FUTURE:
5✔
UNCOV
2067
            return True
×
2068
        if self._is_coin_small_and_unconfirmed(utxo):
5✔
UNCOV
2069
            return True
×
2070
        addr = utxo.address
5✔
2071
        assert addr is not None
5✔
2072
        if self.config.WALLET_FREEZE_REUSED_ADDRESS_UTXOS and self.adb.is_used_as_from_address(addr):
5✔
UNCOV
2073
            return True
×
2074
        return False
5✔
2075

2076
    def _is_coin_small_and_unconfirmed(self, utxo: PartialTxInput) -> bool:
5✔
2077
        """If true, the coin should not be spent.
2078
        The idea here is that an attacker might send us a UTXO in a
2079
        large low-fee unconfirmed tx that will ~never confirm. If we
2080
        spend it as part of a tx ourselves, that too will not confirm
2081
        (unless we use a high fee, but that might not be worth it for
2082
        a small value UTXO).
2083
        In particular, this test triggers for large "dusting transactions"
2084
        that are used for advertising purposes by some entities.
2085
        see #6960
2086
        """
2087
        # confirmed UTXOs are fine; check this first for performance:
2088
        block_height = utxo.block_height
5✔
2089
        assert block_height is not None
5✔
2090
        if block_height > 0:
5✔
2091
            return False
5✔
2092
        # exempt large value UTXOs
2093
        value_sats = utxo.value_sats()
5✔
2094
        assert value_sats is not None
5✔
2095
        threshold = self.config.WALLET_UNCONF_UTXO_FREEZE_THRESHOLD_SAT
5✔
2096
        if value_sats >= threshold:
5✔
2097
            return False
5✔
2098
        # if funding tx has any is_mine input, then UTXO is fine
2099
        funding_tx = self.db.get_transaction(utxo.prevout.txid.hex())
5✔
2100
        if funding_tx is None:
5✔
2101
            # we should typically have the funding tx available;
2102
            # might not have it e.g. while not up_to_date
UNCOV
2103
            return True
×
2104
        if any(self.is_mine(self.adb.get_txin_address(txin))
5✔
2105
               for txin in funding_tx.inputs()):
2106
            return False
5✔
UNCOV
2107
        return True
×
2108

2109
    def set_frozen_state_of_addresses(
5✔
2110
        self,
2111
        addrs: Iterable[str],
2112
        freeze: bool,
2113
        *,
2114
        write_to_disk: bool = True,
2115
    ) -> bool:
2116
        """Set frozen state of the addresses to FREEZE, True or False"""
2117
        if all(self.is_mine(addr) for addr in addrs):
5✔
2118
            with self._freeze_lock:
5✔
2119
                if freeze:
5✔
2120
                    self._frozen_addresses |= set(addrs)
5✔
2121
                else:
2122
                    self._frozen_addresses -= set(addrs)
5✔
2123
                self.db.put('frozen_addresses', list(self._frozen_addresses))
5✔
2124
            util.trigger_callback('status')
5✔
2125
            if write_to_disk:
5✔
2126
                self.save_db()
5✔
2127
            return True
5✔
UNCOV
2128
        return False
×
2129

2130
    def set_frozen_state_of_coins(
5✔
2131
        self,
2132
        utxos: Iterable[str],
2133
        freeze: Optional[bool],  # tri-state
2134
        *,
2135
        write_to_disk: bool = True,
2136
    ) -> None:
2137
        """Set frozen state of the utxos to `freeze`, True or False (or None).
2138
        A value of True/False means the user explicitly set if the coin should be frozen.
2139
        In contrast, None is the default "unset" state. If unset, is_frozen_coin()
2140
        can decide whether a coin should be frozen.
2141
        """
2142
        # basic sanity check that input is not garbage: (see if raises)
2143
        [TxOutpoint.from_str(utxo) for utxo in utxos]
5✔
2144
        assert freeze in (None, False, True), f"{freeze=!r}"
5✔
2145
        with self._freeze_lock:
5✔
2146
            for utxo in utxos:
5✔
2147
                if freeze is None:
5✔
2148
                    self._frozen_coins.pop(utxo, None)
5✔
2149
                else:
2150
                    self._frozen_coins[utxo] = bool(freeze)
5✔
2151
        util.trigger_callback('status')
5✔
2152
        if write_to_disk:
5✔
2153
            self.save_db()
5✔
2154

2155
    def is_address_reserved(self, addr: str) -> bool:
5✔
2156
        # note: atm 'reserved' status is only taken into consideration for 'change addresses'
2157
        return addr in self._reserved_addresses
5✔
2158

2159
    def set_reserved_state_of_address(self, addr: str, *, reserved: bool) -> None:
5✔
2160
        if not self.is_mine(addr):
5✔
2161
            # silently ignore non-ismine addresses
UNCOV
2162
            return
×
2163
        with self.lock:
5✔
2164
            has_changed = (addr in self._reserved_addresses) != reserved
5✔
2165
            if reserved:
5✔
2166
                self._reserved_addresses.add(addr)
5✔
2167
            else:
UNCOV
2168
                self._reserved_addresses.discard(addr)
×
2169
            if has_changed:
5✔
2170
                self.db.put('reserved_addresses', list(self._reserved_addresses))
×
2171

2172
    def set_reserved_addresses_for_chan(self, chan: 'AbstractChannel', *, reserved: bool) -> None:
5✔
2173
        for addr in chan.get_wallet_addresses_channel_might_want_reserved():
5✔
2174
            self.set_reserved_state_of_address(addr, reserved=reserved)
5✔
2175

2176
    def can_export(self):
5✔
UNCOV
2177
        return not self.is_watching_only() and hasattr(self.keystore, 'get_private_key')
×
2178

2179
    def get_bumpfee_strategies_for_tx(
5✔
2180
        self,
2181
        *,
2182
        tx: Transaction,
2183
    ) -> Tuple[Sequence[BumpFeeStrategy], int]:
2184
        """Returns tuple(list of available strategies, idx of recommended option among those)."""
2185
        all_strats = BumpFeeStrategy.all()
×
2186
        # are we paying max?
UNCOV
2187
        invoices = self.get_relevant_invoices_for_tx(tx.txid())
×
2188
        if len(invoices) == 1 and len(invoices[0].outputs) == 1:
×
UNCOV
2189
            if invoices[0].outputs[0].value == '!':
×
UNCOV
2190
                return all_strats, all_strats.index(BumpFeeStrategy.DECREASE_PAYMENT)
×
2191
        # do not decrease payment if it is a swap
UNCOV
2192
        if self.get_swaps_by_funding_tx(tx):
×
UNCOV
2193
            return [BumpFeeStrategy.PRESERVE_PAYMENT], 0
×
2194
        # default
UNCOV
2195
        return all_strats, all_strats.index(BumpFeeStrategy.PRESERVE_PAYMENT)
×
2196

2197
    def bump_fee(
5✔
2198
            self,
2199
            *,
2200
            tx: Transaction,
2201
            new_fee_rate: Union[int, float, Decimal],
2202
            coins: Sequence[PartialTxInput] = None,
2203
            strategy: BumpFeeStrategy = BumpFeeStrategy.PRESERVE_PAYMENT,
2204
    ) -> PartialTransaction:
2205
        """Increase the miner fee of 'tx'.
2206
        'new_fee_rate' is the target min rate in sat/vbyte
2207
        'coins' is a list of UTXOs we can choose from as potential new inputs to be added
2208

2209
        note: it is the caller's responsibility to have already called tx.add_info_from_network().
2210
              Without that, all txins must be ismine.
2211
        """
2212
        assert tx
5✔
2213
        if not isinstance(tx, PartialTransaction):
5✔
2214
            tx = PartialTransaction.from_tx(tx)
5✔
2215
        assert isinstance(tx, PartialTransaction)
5✔
2216
        tx.remove_signatures()
5✔
2217
        if not self.can_rbf_tx(tx):
5✔
UNCOV
2218
            raise CannotBumpFee(_('Transaction is final'))
×
2219
        new_fee_rate = quantize_feerate(new_fee_rate)  # strip excess precision
5✔
2220
        tx.add_info_from_wallet(self)
5✔
2221
        if tx.is_missing_info_from_network():
5✔
UNCOV
2222
            raise Exception("tx missing info from network")
×
2223
        old_tx_size = tx.estimated_size()
5✔
2224
        old_fee = tx.get_fee()
5✔
2225
        assert old_fee is not None
5✔
2226
        old_fee_rate = old_fee / old_tx_size  # sat/vbyte
5✔
2227
        if new_fee_rate <= old_fee_rate:
5✔
UNCOV
2228
            raise CannotBumpFee(_("The new fee rate needs to be higher than the old fee rate."))
×
2229

2230
        if strategy == BumpFeeStrategy.PRESERVE_PAYMENT:
5✔
2231
            # FIXME: we should try decreasing change first,
2232
            # but it requires updating a bunch of unit tests
2233
            try:
5✔
2234
                tx_new = self._bump_fee_through_coinchooser(
5✔
2235
                    tx=tx,
2236
                    new_fee_rate=new_fee_rate,
2237
                    coins=coins,
2238
                )
2239
            except CannotBumpFee as e:
5✔
2240
                tx_new = self._bump_fee_through_decreasing_change(
5✔
2241
                    tx=tx, new_fee_rate=new_fee_rate)
2242
        elif strategy == BumpFeeStrategy.DECREASE_PAYMENT:
5✔
2243
            tx_new = self._bump_fee_through_decreasing_payment(
5✔
2244
                tx=tx, new_fee_rate=new_fee_rate)
2245
        else:
UNCOV
2246
            raise Exception(f"unknown strategy: {strategy=}")
×
2247

2248
        target_min_fee = new_fee_rate * tx_new.estimated_size()
5✔
2249
        actual_fee = tx_new.get_fee()
5✔
2250
        if actual_fee + 1 < target_min_fee:
5✔
UNCOV
2251
            raise CannotBumpFee(
×
2252
                f"bump_fee fee target was not met. "
2253
                f"got {actual_fee}, expected >={target_min_fee}. "
2254
                f"target rate was {new_fee_rate}")
2255
        tx_new.locktime = get_locktime_for_new_transaction(self.network)
5✔
2256
        tx_new.set_rbf(True)
5✔
2257
        tx_new.add_info_from_wallet(self)
5✔
2258
        return tx_new
5✔
2259

2260
    def _bump_fee_through_coinchooser(
5✔
2261
            self,
2262
            *,
2263
            tx: PartialTransaction,
2264
            new_fee_rate: Union[int, Decimal],
2265
            coins: Sequence[PartialTxInput] = None,
2266
    ) -> PartialTransaction:
2267
        """Increase the miner fee of 'tx'.
2268

2269
        - keeps all inputs
2270
        - keeps all not is_mine outputs,
2271
        - allows adding new inputs
2272
        """
2273
        tx = copy.deepcopy(tx)
5✔
2274
        tx.add_info_from_wallet(self)
5✔
2275
        assert tx.get_fee() is not None
5✔
2276
        old_inputs = list(tx.inputs())
5✔
2277
        old_outputs = list(tx.outputs())
5✔
2278
        # change address
2279
        old_change_addrs = [o.address for o in old_outputs if self.is_change(o.address)]
5✔
2280
        change_addrs = self.get_change_addresses_for_new_transaction(old_change_addrs)
5✔
2281
        # which outputs to keep?
2282
        if old_change_addrs:
5✔
2283
            fixed_outputs = list(filter(lambda o: not self.is_change(o.address), old_outputs))
5✔
2284
        else:
2285
            if all(self.is_mine(o.address) for o in old_outputs):
5✔
2286
                # all outputs are is_mine and none of them are change.
2287
                # we bail out as it's unclear what the user would want!
2288
                # the coinchooser bump fee method is probably not a good idea in this case
2289
                raise CannotBumpFee(_('All outputs are non-change is_mine'))
5✔
2290
            old_not_is_mine = list(filter(lambda o: not self.is_mine(o.address), old_outputs))
5✔
2291
            if old_not_is_mine:
5✔
2292
                fixed_outputs = old_not_is_mine
5✔
2293
            else:
UNCOV
2294
                fixed_outputs = old_outputs
×
2295
        if not fixed_outputs:
5✔
2296
            raise CannotBumpFee(_('Could not figure out which outputs to keep'))
5✔
2297

2298
        if coins is None:
5✔
2299
            coins = self.get_spendable_coins(None)
5✔
2300
        # make sure we don't try to spend output from the tx-to-be-replaced:
2301
        coins = [c for c in coins
5✔
2302
                 if c.prevout.txid.hex() not in self.adb.get_conflicting_transactions(tx, include_self=True)]
2303
        for item in coins:
5✔
2304
            self.add_input_info(item)
5✔
2305
        def fee_estimator(size):
5✔
2306
            return FeePolicy.estimate_fee_for_feerate(fee_per_kb=new_fee_rate*1000, size=size)
5✔
2307
        coin_chooser = coinchooser.get_coin_chooser(self.config)
5✔
2308
        try:
5✔
2309
            return coin_chooser.make_tx(
5✔
2310
                coins=coins,
2311
                inputs=old_inputs,
2312
                outputs=fixed_outputs,
2313
                change_addrs=change_addrs,
2314
                fee_estimator_vb=fee_estimator,
2315
                dust_threshold=self.dust_threshold())
2316
        except NotEnoughFunds as e:
5✔
2317
            raise CannotBumpFee(e)
5✔
2318

2319
    def _bump_fee_through_decreasing_change(
5✔
2320
            self,
2321
            *,
2322
            tx: PartialTransaction,
2323
            new_fee_rate: Union[int, Decimal],
2324
    ) -> PartialTransaction:
2325
        """Increase the miner fee of 'tx'.
2326

2327
        - keeps all inputs
2328
        - no new inputs are added
2329
        - change outputs are decreased or removed
2330
        """
2331
        tx = copy.deepcopy(tx)
5✔
2332
        tx.add_info_from_wallet(self)
5✔
2333
        assert tx.get_fee() is not None
5✔
2334
        inputs = tx.inputs()
5✔
2335
        outputs = tx._outputs  # note: we will mutate this directly
5✔
2336

2337
        # use own outputs
2338
        s = list(filter(lambda o: self.is_mine(o.address), outputs))
5✔
2339
        if not s:
5✔
UNCOV
2340
            raise CannotBumpFee('No suitable output')
×
2341

2342
        # prioritize low value outputs, to get rid of dust
2343
        s = sorted(s, key=lambda o: o.value)
5✔
2344
        for o in s:
5✔
2345
            target_fee = int(math.ceil(tx.estimated_size() * new_fee_rate))
5✔
2346
            delta = target_fee - tx.get_fee()
5✔
2347
            if delta <= 0:
5✔
UNCOV
2348
                break
×
2349
            i = outputs.index(o)
5✔
2350
            if o.value - delta >= self.dust_threshold():
5✔
2351
                new_output_value = o.value - delta
5✔
2352
                assert isinstance(new_output_value, int)
5✔
2353
                outputs[i].value = new_output_value
5✔
2354
                delta = 0
5✔
2355
                break
5✔
2356
            else:
2357
                del outputs[i]
5✔
2358
                # note: we mutated the outputs of tx, which will affect
2359
                #       tx.estimated_size() in the next iteration
2360
        else:
2361
            # recompute delta if there was no next iteration
2362
            target_fee = int(math.ceil(tx.estimated_size() * new_fee_rate))
5✔
2363
            delta = target_fee - tx.get_fee()
5✔
2364

2365
        if delta > 0:
5✔
2366
            raise CannotBumpFee(_('Could not find suitable outputs'))
5✔
2367

2368
        return PartialTransaction.from_io(inputs, outputs)
5✔
2369

2370
    def _bump_fee_through_decreasing_payment(
5✔
2371
            self,
2372
            *,
2373
            tx: PartialTransaction,
2374
            new_fee_rate: Union[int, Decimal],
2375
    ) -> PartialTransaction:
2376
        """
2377
        Increase the miner fee of 'tx' by decreasing amount paid.
2378
        This should be used for transactions that pay "Max".
2379

2380
        - keeps all inputs
2381
        - no new inputs are added
2382
        - Each non-ismine output is decreased proportionally to their byte-size.
2383
        """
2384
        tx = copy.deepcopy(tx)
5✔
2385
        tx.add_info_from_wallet(self)
5✔
2386
        assert tx.get_fee() is not None
5✔
2387
        inputs = tx.inputs()
5✔
2388
        outputs = tx.outputs()
5✔
2389

2390
        # select non-ismine outputs
2391
        s = [(idx, out) for (idx, out) in enumerate(outputs)
5✔
2392
             if not self.is_mine(out.address)]
2393
        s = [(idx, out) for (idx, out) in s if self._is_rbf_allowed_to_touch_tx_output(out)]
5✔
2394
        if not s:
5✔
UNCOV
2395
            raise CannotBumpFee("Cannot find payment output")
×
2396

2397
        del_out_idxs = set()
5✔
2398
        tx_size = tx.estimated_size()
5✔
2399
        cur_fee = tx.get_fee()
5✔
2400
        # Main loop. Each iteration decreases value of all selected outputs.
2401
        # The number of iterations is bounded by len(s) as only the final iteration
2402
        # can *not remove* any output.
2403
        for __ in range(len(s) + 1):
5✔
2404
            target_fee = int(math.ceil(tx_size * new_fee_rate))
5✔
2405
            delta_total = target_fee - cur_fee
5✔
2406
            if delta_total <= 0:
5✔
2407
                break
5✔
2408
            out_size_total = sum(Transaction.estimated_output_size_for_script(out.scriptpubkey)
5✔
2409
                                 for (idx, out) in s if idx not in del_out_idxs)
2410
            if out_size_total == 0:  # no outputs left to decrease
5✔
2411
                raise CannotBumpFee(_('Could not find suitable outputs'))
5✔
2412
            for idx, out in s:
5✔
2413
                out_size = Transaction.estimated_output_size_for_script(out.scriptpubkey)
5✔
2414
                delta = int(math.ceil(delta_total * out_size / out_size_total))
5✔
2415
                if out.value - delta >= self.dust_threshold():
5✔
2416
                    new_output_value = out.value - delta
5✔
2417
                    assert isinstance(new_output_value, int)
5✔
2418
                    outputs[idx].value = new_output_value
5✔
2419
                    cur_fee += delta
5✔
2420
                else:  # remove output
2421
                    tx_size -= out_size
5✔
2422
                    cur_fee += out.value
5✔
2423
                    del_out_idxs.add(idx)
5✔
2424
        if delta_total > 0:
5✔
UNCOV
2425
            raise CannotBumpFee(_('Could not find suitable outputs'))
×
2426

2427
        outputs = [out for (idx, out) in enumerate(outputs) if idx not in del_out_idxs]
5✔
2428
        return PartialTransaction.from_io(inputs, outputs)
5✔
2429

2430
    def _is_rbf_allowed_to_touch_tx_output(self, txout: TxOutput) -> bool:
5✔
2431
        # 2fa fee outputs if present, should not be removed or have their value decreased
2432
        if self.is_billing_address(txout.address):
5✔
UNCOV
2433
            return False
×
2434
        # submarine swap funding outputs must not be decreased
2435
        if self.lnworker and self.lnworker.swap_manager.is_lockup_address_for_a_swap(txout.address):
5✔
UNCOV
2436
            return False
×
2437
        return True
5✔
2438

2439
    def can_rbf_tx(self, tx: Transaction, *, is_dscancel: bool = False) -> bool:
5✔
2440
        # do not mutate LN funding txs, as that would change their txid
2441
        if not is_dscancel and self.is_lightning_funding_tx(tx.txid()):
5✔
UNCOV
2442
            return False
×
2443
        return tx.is_rbf_enabled()
5✔
2444

2445
    def cpfp(self, tx: Transaction, fee: int) -> Optional[PartialTransaction]:
5✔
2446
        assert tx
5✔
2447
        txid = tx.txid()
5✔
2448
        for i, o in enumerate(tx.outputs()):
5✔
2449
            address, value = o.address, o.value
5✔
2450
            if self.is_mine(address):
5✔
2451
                break
5✔
2452
        else:
2453
            raise CannotCPFP(_("Could not find suitable output"))
5✔
2454
        coins = self.adb.get_addr_utxo(address)
5✔
2455
        item = coins.get(TxOutpoint.from_str(txid+':%d'%i))
5✔
2456
        if not item:
5✔
2457
            raise CannotCPFP(_("Could not find coins for output"))
×
2458
        inputs = [item]
5✔
2459
        out_address = (self.get_single_change_address_for_new_transaction(allow_reusing_used_change_addrs=False)
5✔
2460
                       or self.get_unused_address()
2461
                       or address)
2462
        output_value = value - fee
5✔
2463
        if output_value < self.dust_threshold():
5✔
UNCOV
2464
            raise CannotCPFP(_("The output value remaining after fee is too low."))
×
2465
        outputs = [PartialTxOutput.from_address_and_value(out_address, output_value)]
5✔
2466
        locktime = get_locktime_for_new_transaction(self.network)
5✔
2467
        tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
5✔
2468
        tx_new.set_rbf(True)
5✔
2469
        tx_new.add_info_from_wallet(self)
5✔
2470
        return tx_new
5✔
2471

2472
    def dscancel(
5✔
2473
            self, *, tx: Transaction, new_fee_rate: Union[int, float, Decimal]
2474
    ) -> PartialTransaction:
2475
        """Double-Spend-Cancel: cancel an unconfirmed tx by double-spending
2476
        its inputs, paying ourselves.
2477
        'new_fee_rate' is the target min rate in sat/vbyte
2478

2479
        note: it is the caller's responsibility to have already called tx.add_info_from_network().
2480
              Without that, all txins must be ismine.
2481
        """
2482
        assert tx
5✔
2483
        if not isinstance(tx, PartialTransaction):
5✔
2484
            tx = PartialTransaction.from_tx(tx)
5✔
2485
        assert isinstance(tx, PartialTransaction)
5✔
2486
        tx.remove_signatures()
5✔
2487

2488
        if not self.can_rbf_tx(tx, is_dscancel=True):
5✔
UNCOV
2489
            raise CannotDoubleSpendTx(_('Transaction is final'))
×
2490
        new_fee_rate = quantize_feerate(new_fee_rate)  # strip excess precision
5✔
2491
        tx.add_info_from_wallet(self)
5✔
2492
        if tx.is_missing_info_from_network():
5✔
UNCOV
2493
            raise Exception("tx missing info from network")
×
2494
        old_tx_size = tx.estimated_size()
5✔
2495
        old_fee = tx.get_fee()
5✔
2496
        assert old_fee is not None
5✔
2497
        old_fee_rate = old_fee / old_tx_size  # sat/vbyte
5✔
2498
        if new_fee_rate <= old_fee_rate:
5✔
UNCOV
2499
            raise CannotDoubleSpendTx(_("The new fee rate needs to be higher than the old fee rate."))
×
2500
        # grab all ismine inputs
2501
        inputs = [txin for txin in tx.inputs()
5✔
2502
                  if self.is_mine(self.adb.get_txin_address(txin))]
2503
        value = sum([txin.value_sats() for txin in inputs])
5✔
2504
        # figure out output address
2505
        old_change_addrs = [o.address for o in tx.outputs() if self.is_mine(o.address)]
5✔
2506
        out_address = (self.get_single_change_address_for_new_transaction(old_change_addrs)
5✔
2507
                       or self.get_receiving_address())
2508
        locktime = get_locktime_for_new_transaction(self.network)
5✔
2509
        outputs = [PartialTxOutput.from_address_and_value(out_address, value)]
5✔
2510
        tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
5✔
2511
        new_tx_size = tx_new.estimated_size()
5✔
2512
        new_fee = max(
5✔
2513
            new_fee_rate * new_tx_size,
2514
            old_fee + self.relayfee() * new_tx_size / Decimal(1000),  # BIP-125 rules 3 and 4
2515
        )
2516
        new_fee = int(math.ceil(new_fee))
5✔
2517
        output_value = value - new_fee
5✔
2518
        if output_value < self.dust_threshold():
5✔
UNCOV
2519
            raise CannotDoubleSpendTx(_("The output value remaining after fee is too low."))
×
2520
        outputs = [PartialTxOutput.from_address_and_value(out_address, value - new_fee)]
5✔
2521
        tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
5✔
2522
        tx_new.set_rbf(True)
5✔
2523
        tx_new.add_info_from_wallet(self)
5✔
2524
        return tx_new
5✔
2525

2526
    def _add_txinout_derivation_info(self, txinout: Union[PartialTxInput, PartialTxOutput],
5✔
2527
                                     address: str, *, only_der_suffix: bool) -> None:
2528
        pass  # implemented by subclasses
5✔
2529

2530
    def _add_input_utxo_info(
5✔
2531
            self,
2532
            txin: PartialTxInput,
2533
            *,
2534
            address: str = None,
2535
    ) -> None:
2536
        # - We prefer to include UTXO (full tx), even for segwit inputs (see #6198).
2537
        # - For witness v0 inputs, we include *both* UTXO and WITNESS_UTXO. UTXO is a strict superset,
2538
        #   so this is redundant, but it is (implied to be) "expected" from bip-0174 (see #8039).
2539
        #   Regardless, this might improve compatibility with some other software.
2540
        # - For witness v1, witness_utxo will be enough though (bip-0341 sighash fixes known prior issues).
2541
        # - We cannot include UTXO if the prev tx is not signed yet (chain of unsigned txs).
2542
        address = address or txin.address
5✔
2543
        # add witness_utxo
2544
        if txin.witness_utxo is None and txin.is_segwit() and address:
5✔
2545
            received, spent = self.adb.get_addr_io(address)
5✔
2546
            item = received.get(txin.prevout.to_str())
5✔
2547
            if item:
5✔
2548
                txin_value = item[2]
5✔
2549
                txin.witness_utxo = TxOutput.from_address_and_value(address, txin_value)
5✔
2550
        # add utxo
2551
        if txin.utxo is None:
5✔
2552
            txin.utxo = self.db.get_transaction(txin.prevout.txid.hex())
5✔
2553
        # Maybe remove witness_utxo. witness_utxo should not be present for non-segwit inputs.
2554
        # If it is present, it might be because another electrum instance added it when sharing the psbt via QR code.
2555
        # If we have the full utxo available, we can remove it without loss of information.
2556
        if txin.witness_utxo and not txin.is_segwit() and txin.utxo:
5✔
2557
            txin.witness_utxo = None
5✔
2558

2559
    def _learn_derivation_path_for_address_from_txinout(self, txinout: Union[PartialTxInput, PartialTxOutput],
5✔
2560
                                                        address: str) -> bool:
2561
        """Tries to learn the derivation path for an address (potentially beyond gap limit)
2562
        using data available in given txin/txout.
2563
        Returns whether the address was found to be is_mine.
2564
        """
2565
        return False  # implemented by subclasses
5✔
2566

2567
    def add_input_info(
5✔
2568
            self,
2569
            txin: TxInput,
2570
            *,
2571
            only_der_suffix: bool = False,
2572
    ) -> None:
2573
        """Populates the txin, using info the wallet already has.
2574
        That is, network requests are *not* done to fetch missing prev txs!
2575
        For that, use txin.add_info_from_network.
2576
        """
2577
        # note: we add input utxos regardless of is_mine
2578
        if txin.utxo is None:
5✔
2579
            txin.utxo = self.db.get_transaction(txin.prevout.txid.hex())
5✔
2580
        if not isinstance(txin, PartialTxInput):
5✔
2581
            return
5✔
2582
        address = self.adb.get_txin_address(txin)
5✔
2583
        self._add_input_utxo_info(txin, address=address)
5✔
2584
        is_mine = self.is_mine(address)
5✔
2585
        if not is_mine:
5✔
2586
            is_mine = self._learn_derivation_path_for_address_from_txinout(txin, address)
5✔
2587
        if not is_mine:
5✔
2588
            return
5✔
2589
        txin.script_descriptor = self.get_script_descriptor_for_address(address)
5✔
2590
        txin.is_mine = True
5✔
2591
        self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix)
5✔
2592
        txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height
5✔
2593

2594
    def has_support_for_slip_19_ownership_proofs(self) -> bool:
5✔
2595
        return False
×
2596

2597
    def add_slip_19_ownership_proofs_to_tx(self, tx: PartialTransaction) -> None:
5✔
UNCOV
2598
        raise NotImplementedError()
×
2599

2600
    def get_script_descriptor_for_address(self, address: str) -> Optional[Descriptor]:
5✔
2601
        if not self.is_mine(address):
5✔
UNCOV
2602
            return None
×
2603
        script_type = self.get_txin_type(address)
5✔
2604
        if script_type in ('address', 'unknown'):
5✔
2605
            return None
5✔
2606
        addr_index = self.get_address_index(address)
5✔
2607
        if addr_index is None:
5✔
UNCOV
2608
            return None
×
2609
        pubkeys = [ks.get_pubkey_provider(addr_index) for ks in self.get_keystores()]
5✔
2610
        if not pubkeys:
5✔
UNCOV
2611
            return None
×
2612
        if script_type == 'p2pk':
5✔
UNCOV
2613
            return descriptor.PKDescriptor(pubkey=pubkeys[0])
×
2614
        elif script_type == 'p2pkh':
5✔
2615
            return descriptor.PKHDescriptor(pubkey=pubkeys[0])
5✔
2616
        elif script_type == 'p2wpkh':
5✔
2617
            return descriptor.WPKHDescriptor(pubkey=pubkeys[0])
5✔
2618
        elif script_type == 'p2wpkh-p2sh':
5✔
2619
            wpkh = descriptor.WPKHDescriptor(pubkey=pubkeys[0])
5✔
2620
            return descriptor.SHDescriptor(subdescriptor=wpkh)
5✔
2621
        elif script_type == 'p2sh':
5✔
2622
            multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)
5✔
2623
            return descriptor.SHDescriptor(subdescriptor=multi)
5✔
2624
        elif script_type == 'p2wsh':
5✔
2625
            multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)
5✔
2626
            return descriptor.WSHDescriptor(subdescriptor=multi)
5✔
2627
        elif script_type == 'p2wsh-p2sh':
5✔
2628
            multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)
5✔
2629
            wsh = descriptor.WSHDescriptor(subdescriptor=multi)
5✔
2630
            return descriptor.SHDescriptor(subdescriptor=wsh)
5✔
2631
        else:
UNCOV
2632
            raise NotImplementedError(f"unexpected {script_type=}")
×
2633

2634
    def can_sign(self, tx: Transaction) -> bool:
5✔
2635
        if not isinstance(tx, PartialTransaction):
5✔
UNCOV
2636
            return False
×
2637
        if tx.is_complete():
5✔
UNCOV
2638
            return False
×
2639
        # add info to inputs if we can; otherwise we might return a false negative:
2640
        tx.add_info_from_wallet(self)
5✔
2641
        for txin in tx.inputs():
5✔
2642
            # note: is_mine check needed to avoid false positives.
2643
            #       just because keystore could sign, txin does not necessarily belong to wallet.
2644
            #       Example: we have p2pkh-like addresses and txin is a multisig that involves our pubkey.
2645
            if not self.is_mine(txin.address):
5✔
2646
                continue
5✔
UNCOV
2647
            for k in self.get_keystores():
×
UNCOV
2648
                if k.can_sign_txin(txin):
×
UNCOV
2649
                    return True
×
2650
        return False
5✔
2651

2652
    def add_output_info(self, txout: PartialTxOutput, *, only_der_suffix: bool = False) -> None:
5✔
2653
        address = txout.address
5✔
2654
        if not self.is_mine(address):
5✔
2655
            is_mine = self._learn_derivation_path_for_address_from_txinout(txout, address)
5✔
2656
            if not is_mine:
5✔
2657
                return
5✔
2658
        txout.script_descriptor = self.get_script_descriptor_for_address(address)
5✔
2659
        txout.is_mine = True
5✔
2660
        txout.is_change = self.is_change(address)
5✔
2661
        self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix)
5✔
2662

2663
    def sign_transaction(self, tx: Transaction, password, *, ignore_warnings: bool = False) -> Optional[PartialTransaction]:
5✔
2664
        """ returns tx if successful else None """
2665
        if self.is_watching_only():
5✔
UNCOV
2666
            return
×
2667
        if not isinstance(tx, PartialTransaction):
5✔
UNCOV
2668
            return
×
2669
        if any(DummyAddress.is_dummy_address(txout.address) for txout in tx.outputs()):
5✔
2670
            raise DummyAddressUsedInTxException("tried to sign tx with dummy address!")
5✔
2671

2672
        # check if signing is dangerous
2673
        sh_danger = self.check_sighash(tx)
5✔
2674
        if sh_danger.needs_reject():
5✔
2675
            raise TransactionDangerousException('Not signing transaction:\n' + sh_danger.get_long_message())
5✔
2676
        if sh_danger.needs_confirm() and not ignore_warnings:
5✔
2677
            raise TransactionPotentiallyDangerousException('Not signing transaction:\n' + sh_danger.get_long_message())
5✔
2678

2679
        # find out if we are replacing a txbatcher transaction
2680
        prevout_str = tx.inputs()[0].prevout.to_str()
5✔
2681
        batch = self.txbatcher.find_batch_by_prevout(prevout_str)
5✔
2682
        if batch:
5✔
2683
            batch.add_sweep_info_to_tx(tx)
5✔
2684

2685
        # sign with make_witness
2686
        for i, txin in enumerate(tx.inputs()):
5✔
2687
            if hasattr(txin, 'make_witness'):
5✔
2688
                self.logger.info(f'sign_transaction: adding witness using make_witness')
5✔
2689
                privkey = txin.privkey
5✔
2690
                sig = tx.sign_txin(i, privkey)
5✔
2691
                txin.script_sig = b''
5✔
2692
                txin.witness = txin.make_witness(sig)
5✔
2693
                assert txin.is_complete()
5✔
2694

2695
        # add info to a temporary tx copy; including xpubs
2696
        # and full derivation paths as hw keystores might want them
2697
        tmp_tx = copy.deepcopy(tx)
5✔
2698
        tmp_tx.add_info_from_wallet(self, include_xpubs=True)
5✔
2699
        # sign. start with ready keystores.
2700
        # note: ks.ready_to_sign() side-effect: we trigger pairings with potential hw devices.
2701
        #       We only do this once, before the loop, however we could rescan after each iteration,
2702
        #       to see if the user connected/disconnected devices in the meantime.
2703
        for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True):
5✔
2704
            try:
5✔
2705
                if k.can_sign(tmp_tx):
5✔
2706
                    k.sign_transaction(tmp_tx, password)
5✔
2707
            except UserCancelled:
×
UNCOV
2708
                continue
×
2709
        # remove sensitive info; then copy back details from temporary tx
2710
        tmp_tx.remove_xpubs_and_bip32_paths()
5✔
2711
        tx.combine_with_other_psbt(tmp_tx)
5✔
2712
        tx.add_info_from_wallet(self, include_xpubs=False)
5✔
2713
        return tx
5✔
2714

2715
    def try_detecting_internal_addresses_corruption(self) -> None:
5✔
UNCOV
2716
        pass
×
2717

2718
    def check_address_for_corruption(self, addr: str) -> None:
5✔
UNCOV
2719
        pass
×
2720

2721
    def get_unused_addresses(self) -> Sequence[str]:
5✔
2722
        domain = self.get_receiving_addresses()
5✔
2723
        return [addr for addr in domain if not self.adb.is_used(addr) and not self.get_request_by_addr(addr)]
5✔
2724

2725
    @check_returned_address_for_corruption
5✔
2726
    def get_unused_address(self) -> Optional[str]:
5✔
2727
        """Get an unused receiving address, if there is one.
2728
        Note: there might NOT be one available!
2729
        """
2730
        addrs = self.get_unused_addresses()
5✔
2731
        if addrs:
5✔
2732
            return addrs[0]
5✔
2733

2734
    @check_returned_address_for_corruption
5✔
2735
    def get_receiving_address(self) -> str:
5✔
2736
        """Get a receiving address. Guaranteed to always return an address."""
2737
        unused_addr = self.get_unused_address()
5✔
2738
        if unused_addr:
5✔
2739
            return unused_addr
5✔
2740
        domain = self.get_receiving_addresses()
×
2741
        if not domain:
×
2742
            raise Exception("no receiving addresses in wallet?!")
×
2743
        choice = domain[0]
×
2744
        for addr in domain:
×
2745
            if not self.adb.is_used(addr):
×
2746
                if self.get_request_by_addr(addr) is None:
×
UNCOV
2747
                    return addr
×
2748
                else:
2749
                    choice = addr
×
UNCOV
2750
        return choice
×
2751

2752
    def create_new_address(self, for_change: bool = False):
5✔
UNCOV
2753
        raise UserFacingException("this wallet cannot generate new addresses")
×
2754

2755
    def import_address(self, address: str) -> str:
5✔
UNCOV
2756
        raise UserFacingException("this wallet cannot import addresses")
×
2757

2758
    def import_addresses(self, addresses: List[str], *,
5✔
2759
                         write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:
UNCOV
2760
        raise UserFacingException("this wallet cannot import addresses")
×
2761

2762
    def delete_address(self, address: str) -> None:
5✔
UNCOV
2763
        raise UserFacingException("this wallet cannot delete addresses")
×
2764

2765
    def get_request_URI(self, req: Request) -> Optional[str]:
5✔
UNCOV
2766
        return req.get_bip21_URI(lightning_invoice=None)
×
2767

2768
    def check_expired_status(self, r: BaseInvoice, status):
5✔
2769
        #if r.is_lightning() and r.exp == 0:
2770
        #    status = PR_EXPIRED  # for BOLT-11 invoices, exp==0 means 0 seconds
2771
        if status == PR_UNPAID and r.has_expired():
5✔
2772
            status = PR_EXPIRED
5✔
2773
        return status
5✔
2774

2775
    def get_invoice_status(self, invoice: BaseInvoice):
5✔
2776
        """Returns status of (incoming) request or (outgoing) invoice."""
2777
        # lightning invoices can be paid onchain
2778
        if invoice.is_lightning() and self.lnworker:
5✔
2779
            status = self.lnworker.get_invoice_status(invoice)
5✔
2780
            if status != PR_UNPAID:
5✔
2781
                return self.check_expired_status(invoice, status)
5✔
2782
        paid, conf = self.is_onchain_invoice_paid(invoice)
5✔
2783
        if not paid:
5✔
2784
            if isinstance(invoice, Invoice):
5✔
2785
                if status:=invoice.get_broadcasting_status():
5✔
UNCOV
2786
                    return status
×
2787
            status = PR_UNPAID
5✔
2788
        elif conf == 0:
5✔
2789
            status = PR_UNCONFIRMED
5✔
2790
        else:
2791
            assert conf >= 1, conf
5✔
2792
            status = PR_PAID
5✔
2793
        return self.check_expired_status(invoice, status)
5✔
2794

2795
    def get_request_by_addr(self, addr: str) -> Optional[Request]:
5✔
2796
        """Returns a relevant request for address, from an on-chain PoV.
2797
        (One that has been paid on-chain or is pending)
2798

2799
        Called in get_label_for_address and update_invoices_and_reqs_touched_by_tx
2800
        Returns None if the address can be reused (i.e. was paid by lightning or has expired)
2801
        """
2802
        keys = self._requests_addr_to_key.get(addr) or []
5✔
2803
        reqs = [self._receive_requests.get(key) for key in keys]
5✔
2804
        reqs = [req for req in reqs if req]  # filter None
5✔
2805
        if not reqs:
5✔
2806
            return
5✔
2807
        # filter out expired
2808
        reqs = [req for req in reqs if self.get_invoice_status(req) != PR_EXPIRED]
5✔
2809
        # filter out paid-with-lightning
2810
        if self.lnworker:
5✔
2811
            reqs = [req for req in reqs
5✔
2812
                    if not req.is_lightning() or self.lnworker.get_invoice_status(req) == PR_UNPAID]
2813
        if not reqs:
5✔
2814
            return None
5✔
2815
        # note: There typically should not be more than one relevant request for an address.
2816
        #       If there's multiple, return the one created last (see #8113). Consider:
2817
        #       - there is an old expired req1, and a newer unpaid req2, reusing the same addr (and same amount),
2818
        #       - now req2 gets paid. however, get_invoice_status will say both req1 and req2 are PAID. (see #8061)
2819
        #       - as a workaround, we return the request with the larger creation time.
2820
        reqs.sort(key=lambda req: req.get_time())
5✔
2821
        return reqs[-1]
5✔
2822

2823
    def get_request(self, request_id: str) -> Optional[Request]:
5✔
2824
        return self._receive_requests.get(request_id)
5✔
2825

2826
    def get_formatted_request(self, request_id):
5✔
2827
        x = self.get_request(request_id)
×
2828
        if x:
×
UNCOV
2829
            return self.export_request(x)
×
2830

2831
    def export_request(self, x: Request) -> Dict[str, Any]:
5✔
2832
        key = x.get_id()
×
2833
        status = self.get_invoice_status(x)
×
2834
        d = x.as_dict(status)
×
2835
        d['request_id'] = d.pop('id')
×
2836
        if x.is_lightning():
×
2837
            d['rhash'] = x.rhash
×
2838
            d['lightning_invoice'] = self.get_bolt11_invoice(x)
×
2839
            if self.lnworker and status == PR_UNPAID:
×
2840
                d['can_receive'] = self.lnworker.can_receive_invoice(x)
×
2841
        if address := x.get_address():
×
2842
            d['address'] = address
×
UNCOV
2843
            d['URI'] = self.get_request_URI(x)
×
2844
            # if request was paid onchain, add relevant fields
2845
            # note: addr is reused when getting paid on LN! so we check for that.
2846
            _, conf, tx_hashes = self._is_onchain_invoice_paid(x)
×
2847
            if not x.is_lightning() or not self.lnworker or self.lnworker.get_invoice_status(x) != PR_PAID:
×
2848
                if conf is not None:
×
2849
                    d['confirmations'] = conf
×
2850
                d['tx_hashes'] = tx_hashes
×
2851
        run_hook('wallet_export_request', d, key)
×
UNCOV
2852
        return d
×
2853

2854
    def export_invoice(self, x: Invoice) -> Dict[str, Any]:
5✔
2855
        key = x.get_id()
×
2856
        status = self.get_invoice_status(x)
×
2857
        d = x.as_dict(status)
×
2858
        d['invoice_id'] = d.pop('id')
×
2859
        if x.is_lightning():
×
2860
            d['lightning_invoice'] = x.lightning_invoice
×
2861
            if self.lnworker and status == PR_UNPAID:
×
UNCOV
2862
                d['can_pay'] = self.lnworker.can_pay_invoice(x)
×
2863
        else:
2864
            amount_sat = x.get_amount_sat()
×
2865
            assert isinstance(amount_sat, (int, str, type(None)))
×
2866
            d['outputs'] = [y.to_legacy_tuple() for y in x.get_outputs()]
×
2867
            if x.bip70:
×
2868
                d['bip70'] = x.bip70
×
UNCOV
2869
        return d
×
2870

2871
    def get_invoices_and_requests_touched_by_tx(self, tx):
5✔
2872
        request_keys = set()
5✔
2873
        invoice_keys = set()
5✔
2874
        with self.lock, self.transaction_lock:
5✔
2875
            for txo in tx.outputs():
5✔
2876
                addr = txo.address
5✔
2877
                if request:=self.get_request_by_addr(addr):
5✔
2878
                    request_keys.add(request.get_id())
5✔
2879
                for invoice_key in self._invoices_from_scriptpubkey_map.get(txo.scriptpubkey, set()):
5✔
UNCOV
2880
                    invoice_keys.add(invoice_key)
×
2881
        return request_keys, invoice_keys
5✔
2882

2883
    def _update_invoices_and_reqs_touched_by_tx(self, tx_hash: str) -> None:
5✔
2884
        # FIXME in some cases if tx2 replaces unconfirmed tx1 in the mempool, we are not called.
2885
        #       For a given receive request, if tx1 touches it but tx2 does not, then
2886
        #       we were called when tx1 was added, but we will not get called when tx2 replaces tx1.
2887
        tx = self.db.get_transaction(tx_hash)
5✔
2888
        if tx is None:
5✔
UNCOV
2889
            return
×
2890
        request_keys, invoice_keys = self.get_invoices_and_requests_touched_by_tx(tx)
5✔
2891
        for key in request_keys:
5✔
2892
            request = self.get_request(key)
5✔
2893
            if not request:
5✔
UNCOV
2894
                continue
×
2895
            status = self.get_invoice_status(request)
5✔
2896
            util.trigger_callback('request_status', self, request.get_id(), status)
5✔
2897
        self._update_onchain_invoice_paid_detection(invoice_keys)
5✔
2898

2899
    def set_broadcasting(self, tx: Transaction, *, broadcasting_status: Optional[int]):
5✔
2900
        request_keys, invoice_keys = self.get_invoices_and_requests_touched_by_tx(tx)
×
2901
        for key in invoice_keys:
×
2902
            invoice = self._invoices.get(key)
×
2903
            if not invoice:
×
2904
                continue
×
2905
            invoice._broadcasting_status = broadcasting_status
×
2906
            status = self.get_invoice_status(invoice)
×
UNCOV
2907
            util.trigger_callback('invoice_status', self, key, status)
×
2908

2909
    def get_bolt11_invoice(self, req: Request) -> str:
5✔
2910
        if not self.lnworker:
×
2911
            return ''
×
2912
        if (payment_hash := req.payment_hash) is None:  # e.g. req might have been generated before enabling LN
×
2913
            return ''
×
2914
        amount_msat = req.get_amount_msat() or None
×
2915
        assert (amount_msat is None or amount_msat > 0), amount_msat
×
UNCOV
2916
        lnaddr, invoice = self.lnworker.get_bolt11_invoice(
×
2917
            payment_hash=payment_hash,
2918
            amount_msat=amount_msat,
2919
            message=req.message,
2920
            expiry=req.exp,
2921
            fallback_address=None)
UNCOV
2922
        return invoice
×
2923

2924
    def create_request(self, amount_sat: Optional[int], message: Optional[str], exp_delay: Optional[int], address: Optional[str]):
5✔
2925
        """ will create a lightning request if address is None """
2926
        # for receiving
2927
        amount_sat = amount_sat or 0
5✔
2928
        assert isinstance(amount_sat, int), f"{amount_sat!r}"
5✔
2929
        amount_msat = None if not amount_sat else amount_sat * 1000  # amount_sat in [None, 0] implies undefined.
5✔
2930
        message = message or ''
5✔
2931
        address = address or None  # converts "" to None
5✔
2932
        exp_delay = exp_delay or 0
5✔
2933
        timestamp = int(Request._get_cur_time())
5✔
2934
        if address is None:
5✔
2935
            assert self.has_lightning()
5✔
2936
            payment_hash = self.lnworker.create_payment_info(amount_msat=amount_msat, write_to_disk=False)
5✔
2937
        else:
2938
            payment_hash = None
5✔
2939
        outputs = [PartialTxOutput.from_address_and_value(address, amount_sat)] if address else []
5✔
2940
        height = self.adb.get_local_height()
5✔
2941
        req = Request(
5✔
2942
            outputs=outputs,
2943
            message=message,
2944
            time=timestamp,
2945
            amount_msat=amount_msat,
2946
            exp=exp_delay,
2947
            height=height,
2948
            bip70=None,
2949
            payment_hash=payment_hash,
2950
        )
2951
        key = self.add_payment_request(req)
5✔
2952
        return key
5✔
2953

2954
    def add_payment_request(self, req: Request, *, write_to_disk: bool = True):
5✔
2955
        request_id = req.get_id()
5✔
2956
        self._receive_requests[request_id] = req
5✔
2957
        if addr:=req.get_address():
5✔
2958
            self._requests_addr_to_key[addr].add(request_id)
5✔
2959
        if write_to_disk:
5✔
2960
            self.save_db()
5✔
2961
        return request_id
5✔
2962

2963
    def delete_request(self, request_id, *, write_to_disk: bool = True):
5✔
2964
        """ lightning or on-chain """
2965
        req = self.get_request(request_id)
×
2966
        if req is None:
×
2967
            return
×
2968
        self._receive_requests.pop(request_id, None)
×
2969
        if addr:=req.get_address():
×
2970
            self._requests_addr_to_key[addr].discard(request_id)
×
2971
        if req.is_lightning() and self.lnworker:
×
2972
            self.lnworker.delete_payment_info(req.rhash)
×
2973
        if write_to_disk:
×
UNCOV
2974
            self.save_db()
×
2975

2976
    def delete_invoice(self, invoice_id, *, write_to_disk: bool = True):
5✔
2977
        """ lightning or on-chain """
2978
        inv = self._invoices.pop(invoice_id, None)
×
2979
        if inv is None:
×
2980
            return
×
2981
        if inv.is_lightning() and self.lnworker:
×
2982
            self.lnworker.delete_payment_info(inv.rhash)
×
2983
        if write_to_disk:
×
UNCOV
2984
            self.save_db()
×
2985

2986
    def get_requests(self) -> List[Request]:
5✔
2987
        out = [self.get_request(x) for x in self._receive_requests.keys()]
×
2988
        out = [x for x in out if x is not None]
×
UNCOV
2989
        return out
×
2990

2991
    def get_sorted_requests(self) -> List[Request]:
5✔
2992
        """ sorted by timestamp """
2993
        out = self.get_requests()
×
2994
        out.sort(key=lambda x: x.time)
×
UNCOV
2995
        return out
×
2996

2997
    def get_unpaid_requests(self) -> List[Request]:
5✔
2998
        out = [x for x in self._receive_requests.values() if self.get_invoice_status(x) != PR_PAID]
×
2999
        out.sort(key=lambda x: x.time)
×
UNCOV
3000
        return out
×
3001

3002
    def delete_expired_requests(self):
5✔
3003
        keys = [k for k, v in self._receive_requests.items() if self.get_invoice_status(v) == PR_EXPIRED]
×
3004
        self.delete_requests(keys)
×
UNCOV
3005
        return keys
×
3006

3007
    def delete_requests(self, keys):
5✔
3008
        for key in keys:
×
3009
            self.delete_request(key, write_to_disk=False)
×
3010
        if keys:
×
UNCOV
3011
            self.save_db()
×
3012

3013
    @abstractmethod
5✔
3014
    def get_fingerprint(self) -> str:
5✔
3015
        """Returns a string that can be used to identify this wallet.
3016
        Used e.g. by Labels plugin, and LN channel backups.
3017
        Returns empty string "" for wallets that don't have an ID.
3018
        """
UNCOV
3019
        pass
×
3020

3021
    def can_import_privkey(self):
5✔
UNCOV
3022
        return False
×
3023

3024
    def can_import_address(self):
5✔
UNCOV
3025
        return False
×
3026

3027
    def can_delete_address(self):
5✔
UNCOV
3028
        return False
×
3029

3030
    def has_password(self) -> bool:
5✔
3031
        return self.has_keystore_encryption() or self.has_storage_encryption()
5✔
3032

3033
    def can_have_keystore_encryption(self):
5✔
3034
        return self.keystore and self.keystore.may_have_password()
5✔
3035

3036
    def get_available_storage_encryption_version(self) -> StorageEncryptionVersion:
5✔
3037
        """Returns the type of storage encryption offered to the user.
3038

3039
        A wallet file (storage) is either encrypted with this version
3040
        or is stored in plaintext.
3041
        """
3042
        if isinstance(self.keystore, Hardware_KeyStore):
5✔
UNCOV
3043
            return StorageEncryptionVersion.XPUB_PASSWORD
×
3044
        else:
3045
            return StorageEncryptionVersion.USER_PASSWORD
5✔
3046

3047
    def has_keystore_encryption(self) -> bool:
5✔
3048
        """Returns whether encryption is enabled for the keystore.
3049

3050
        If True, e.g. signing a transaction will require a password.
3051
        """
3052
        if self.can_have_keystore_encryption():
5✔
3053
            return bool(self.db.get('use_encryption', False))
5✔
3054
        return False
5✔
3055

3056
    def has_storage_encryption(self) -> bool:
5✔
3057
        """Returns whether encryption is enabled for the wallet file on disk."""
3058
        return bool(self.storage) and self.storage.is_encrypted()
5✔
3059

3060
    @classmethod
5✔
3061
    def may_have_password(cls):
5✔
UNCOV
3062
        return True
×
3063

3064
    def check_password(self, password: Optional[str]) -> None:
5✔
3065
        """Raises an InvalidPassword exception on invalid password"""
3066
        if not self.has_password():
5✔
3067
            if password is not None:
5✔
3068
                raise InvalidPassword("password given but wallet has no password")
5✔
3069
            return
5✔
3070
        if self.has_keystore_encryption():
5✔
3071
            self.keystore.check_password(password)
5✔
3072
        if self.has_storage_encryption():
5✔
3073
            self.storage.check_password(password)
5✔
3074

3075
    def update_password(self, old_pw, new_pw, *, encrypt_storage: bool = True):
5✔
3076
        if old_pw is None and self.has_password():
5✔
UNCOV
3077
            raise InvalidPassword()
×
3078
        self.check_password(old_pw)
5✔
3079
        if self.storage:
5✔
3080
            if encrypt_storage:
5✔
3081
                enc_version = self.get_available_storage_encryption_version()
5✔
3082
            else:
3083
                enc_version = StorageEncryptionVersion.PLAINTEXT
5✔
3084
            self.storage.set_password(new_pw, enc_version)
5✔
3085
        # make sure next storage.write() saves changes
3086
        self.db.set_modified(True)
5✔
3087

3088
        # note: Encrypting storage with a hw device is currently only
3089
        #       allowed for non-multisig wallets. Further,
3090
        #       Hardware_KeyStore.may_have_password() == False.
3091
        #       If these were not the case,
3092
        #       extra care would need to be taken when encrypting keystores.
3093
        self._update_password_for_keystore(old_pw, new_pw)
5✔
3094
        encrypt_keystore = self.can_have_keystore_encryption()
5✔
3095
        self.db.set_keystore_encryption(bool(new_pw) and encrypt_keystore)
5✔
3096
        # save changes. force full rewrite to rm remnants of old password
3097
        if self.storage and self.storage.file_exists():
5✔
3098
            self.db.write_and_force_consolidation()
5✔
3099
        # if wallet was previously unlocked, reset password_in_memory
3100
        self.lock_wallet()
5✔
3101

3102
    @abstractmethod
5✔
3103
    def _update_password_for_keystore(self, old_pw: Optional[str], new_pw: Optional[str]) -> None:
5✔
UNCOV
3104
        pass
×
3105

3106
    def sign_message(self, address: str, message: str, password) -> bytes:
5✔
3107
        index = self.get_address_index(address)
×
3108
        script_type = self.get_txin_type(address)
×
3109
        assert script_type != "address"
×
UNCOV
3110
        return self.keystore.sign_message(index, message, password, script_type=script_type)
×
3111

3112
    def decrypt_message(self, pubkey: str, message, password) -> bytes:
5✔
3113
        addr = self.pubkeys_to_address([pubkey])
×
3114
        index = self.get_address_index(addr)
×
UNCOV
3115
        return self.keystore.decrypt_message(index, message, password)
×
3116

3117
    @abstractmethod
5✔
3118
    def pubkeys_to_address(self, pubkeys: Sequence[str]) -> Optional[str]:
5✔
UNCOV
3119
        pass
×
3120

3121
    def price_at_timestamp(self, txid, price_func):
5✔
3122
        """Returns fiat price of bitcoin at the time tx got confirmed."""
3123
        timestamp = self.adb.get_tx_height(txid).timestamp
5✔
3124
        return price_func(timestamp if timestamp else time.time())
5✔
3125

3126
    def average_price(self, txid, price_func, ccy) -> Decimal:
5✔
3127
        """ Average acquisition price of the inputs of a transaction """
3128
        input_value = 0
×
3129
        total_price = 0
×
3130
        txi_addresses = self.db.get_txi_addresses(txid)
×
3131
        if not txi_addresses:
×
3132
            return Decimal('NaN')
×
3133
        for addr in txi_addresses:
×
3134
            d = self.db.get_txi_addr(txid, addr)
×
3135
            for ser, v in d:
×
3136
                input_value += v
×
3137
                total_price += self.coin_price(ser.split(':')[0], price_func, ccy, v)
×
UNCOV
3138
        return total_price / (input_value/Decimal(COIN))
×
3139

3140
    def clear_coin_price_cache(self):
5✔
UNCOV
3141
        self._coin_price_cache = {}
×
3142

3143
    def coin_price(self, txid, price_func, ccy, txin_value) -> Decimal:
5✔
3144
        """
3145
        Acquisition price of a coin.
3146
        This assumes that either all inputs are mine, or no input is mine.
3147
        """
3148
        if txin_value is None:
×
3149
            return Decimal('NaN')
×
3150
        cache_key = "{}:{}:{}".format(str(txid), str(ccy), str(txin_value))
×
3151
        result = self._coin_price_cache.get(cache_key, None)
×
3152
        if result is not None:
×
3153
            return result
×
3154
        if self.db.get_txi_addresses(txid):
×
3155
            result = self.average_price(txid, price_func, ccy) * txin_value/Decimal(COIN)
×
3156
            self._coin_price_cache[cache_key] = result
×
UNCOV
3157
            return result
×
3158
        else:
3159
            fiat_value = self.get_fiat_value(txid, ccy)
×
3160
            if fiat_value is not None:
×
UNCOV
3161
                return fiat_value
×
3162
            else:
3163
                p = self.price_at_timestamp(txid, price_func)
×
UNCOV
3164
                return p * txin_value/Decimal(COIN)
×
3165

3166
    def is_billing_address(self, addr):
5✔
3167
        # overridden for TrustedCoin wallets
3168
        return False
5✔
3169

3170
    @abstractmethod
5✔
3171
    def is_watching_only(self) -> bool:
5✔
UNCOV
3172
        pass
×
3173

3174
    def get_keystore(self) -> Optional[KeyStore]:
5✔
UNCOV
3175
        return self.keystore
×
3176

3177
    def get_keystores(self) -> Sequence[KeyStore]:
5✔
3178
        return [self.keystore] if self.keystore else []
5✔
3179

3180
    @abstractmethod
5✔
3181
    def save_keystore(self):
5✔
UNCOV
3182
        pass
×
3183

3184
    @abstractmethod
5✔
3185
    def has_seed(self) -> bool:
5✔
UNCOV
3186
        pass
×
3187

3188
    def get_seed_type(self) -> Optional[str]:
5✔
UNCOV
3189
        return None
×
3190

3191
    @abstractmethod
5✔
3192
    def get_all_known_addresses_beyond_gap_limit(self) -> Set[str]:
5✔
UNCOV
3193
        pass
×
3194

3195
    def _check_risk_of_burning_coins_as_fees(self, tx: 'PartialTransaction') -> TxSighashDanger:
5✔
3196
        """Helper method to check if there is risk of burning coins as fees if we sign.
3197
        Note that if not all inputs are ismine, e.g. coinjoin, the risk is not just about fees.
3198

3199
        Note:
3200
            - legacy sighash does not commit to any input amounts
3201
            - BIP-0143 sighash only commits to the *corresponding* input amount
3202
            - BIP-taproot sighash commits to *all* input amounts
3203
        """
3204
        assert isinstance(tx, PartialTransaction)
5✔
3205
        rl = TxSighashRiskLevel
5✔
3206
        short_message = _("Warning") + ": " + _("The fee could not be verified!")
5✔
3207
        # check that all inputs use SIGHASH_ALL
3208
        if not all(txin.sighash in (None, Sighash.ALL) for txin in tx.inputs()):
5✔
3209
            messages = [(_("Warning") + ": "
5✔
3210
                         + _("Some inputs use non-default sighash flags, which might affect the fee."))]
3211
            return TxSighashDanger(risk_level=rl.FEE_WARNING_NEEDCONFIRM, short_message=short_message, messages=messages)
5✔
3212
        # if we have all full previous txs, we *know* all the input amounts -> fine
3213
        if all([txin.utxo for txin in tx.inputs()]):
5✔
3214
            return TxSighashDanger(risk_level=rl.SAFE)
5✔
3215
        # a single segwit input -> fine
3216
        if len(tx.inputs()) == 1 and tx.inputs()[0].is_segwit() and tx.inputs()[0].witness_utxo:
5✔
3217
            return TxSighashDanger(risk_level=rl.SAFE)
5✔
3218
        # coinjoin or similar
3219
        if any([not self.is_mine(txin.address) for txin in tx.inputs()]):
5✔
UNCOV
3220
            messages = [(_("Warning") + ": "
×
3221
                         + _("The input amounts could not be verified as the previous transactions are missing.\n"
3222
                             "The amount of money being spent CANNOT be verified."))]
UNCOV
3223
            return TxSighashDanger(risk_level=rl.FEE_WARNING_NEEDCONFIRM, short_message=short_message, messages=messages)
×
3224
        # some inputs are legacy
3225
        if any([not txin.is_segwit() for txin in tx.inputs()]):
5✔
3226
            messages = [(_("Warning") + ": "
5✔
3227
                         + _("The fee could not be verified. Signing non-segwit inputs is risky:\n"
3228
                             "if this transaction was maliciously modified before you sign,\n"
3229
                             "you might end up paying a higher mining fee than displayed."))]
3230
            return TxSighashDanger(risk_level=rl.FEE_WARNING_NEEDCONFIRM, short_message=short_message, messages=messages)
5✔
3231
        # all inputs are segwit
3232
        # https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-August/014843.html
UNCOV
3233
        messages = [(_("Warning") + ": "
×
3234
                     + _("If you received this transaction from an untrusted device, "
3235
                         "do not accept to sign it more than once,\n"
3236
                         "otherwise you could end up paying a different fee."))]
UNCOV
3237
        return TxSighashDanger(risk_level=rl.FEE_WARNING_SKIPCONFIRM, short_message=short_message, messages=messages)
×
3238

3239
    def check_sighash(self, tx: 'PartialTransaction') -> TxSighashDanger:
5✔
3240
        """Checks the Sighash for my inputs and considers if the tx is safe to sign."""
3241
        assert isinstance(tx, PartialTransaction)
5✔
3242
        rl = TxSighashRiskLevel
5✔
3243
        hintmap = {
5✔
3244
            0:                    (rl.SAFE,           None),
3245
            Sighash.NONE:         (rl.INSANE_SIGHASH, _('Input {} is marked SIGHASH_NONE.')),
3246
            Sighash.SINGLE:       (rl.WEIRD_SIGHASH,  _('Input {} is marked SIGHASH_SINGLE.')),
3247
            Sighash.ALL:          (rl.SAFE,           None),
3248
            Sighash.ANYONECANPAY: (rl.WEIRD_SIGHASH,  _('Input {} is marked SIGHASH_ANYONECANPAY.')),
3249
        }
3250
        sighash_danger = TxSighashDanger()
5✔
3251
        for txin_idx, txin in enumerate(tx.inputs()):
5✔
3252
            if txin.sighash in (None, Sighash.ALL):
5✔
3253
                continue  # None will get converted to Sighash.ALL, so these values are safe
5✔
3254
            # found interesting sighash flag
3255
            addr = self.adb.get_txin_address(txin)
5✔
3256
            if self.is_mine(addr):
5✔
3257
                sh_base = txin.sighash & (Sighash.ANYONECANPAY ^ 0xff)
5✔
3258
                sh_acp = txin.sighash & Sighash.ANYONECANPAY
5✔
3259
                for sh in [sh_base, sh_acp]:
5✔
3260
                    if msg := hintmap[sh][1]:
5✔
3261
                        risk_level = hintmap[sh][0]
5✔
3262
                        header = _('Fatal') if TxSighashDanger(risk_level=risk_level).needs_reject() else _('Warning')
5✔
3263
                        shd = TxSighashDanger(
5✔
3264
                            risk_level=risk_level,
3265
                            short_message=_('Danger! This transaction uses non-default sighash flags!'),
3266
                            messages=[f"{header}: {msg.format(txin_idx)}"],
3267
                        )
3268
                        sighash_danger = sighash_danger.combine(shd)
5✔
3269
        if sighash_danger.needs_reject():  # no point for further tests
5✔
3270
            return sighash_danger
5✔
3271
        # if we show any fee to the user, check now how reliable that is:
3272
        if self.get_wallet_delta(tx).fee is not None:
5✔
3273
            shd = self._check_risk_of_burning_coins_as_fees(tx)
5✔
3274
            sighash_danger = sighash_danger.combine(shd)
5✔
3275
        return sighash_danger
5✔
3276

3277
    def get_tx_fee_warning(
5✔
3278
            self, *,
3279
            invoice_amt: int,
3280
            tx_size: int,
3281
            fee: int,
3282
            txid: Optional[str]) -> Optional[Tuple[bool, str, str]]:
3283

3284
        assert invoice_amt >= 0, f"{invoice_amt=!r} must be non-negative satoshis"
×
3285
        assert fee >= 0, f"{fee=!r} must be non-negative satoshis"
×
3286
        is_future_tx = txid is not None and txid in self.adb.future_tx
×
3287
        feerate = Decimal(fee) / tx_size  # sat/byte
×
3288
        fee_ratio = Decimal(fee) / invoice_amt if invoice_amt else 0
×
3289
        long_warning = None
×
3290
        short_warning = None
×
3291
        allow_send = True
×
3292
        if feerate < self.relayfee() / 1000 and not is_future_tx:
×
UNCOV
3293
            long_warning = ' '.join([
×
3294
                _("This transaction requires a higher fee, or it will not be propagated by your current server."),
3295
                _("Try to raise your transaction fee, or use a server with a lower relay fee.")
3296
            ])
3297
            short_warning = _("below relay fee") + "!"
×
3298
            allow_send = False
×
3299
        elif fee_ratio >= FEE_RATIO_HIGH_WARNING:
×
UNCOV
3300
            long_warning = ' '.join([
×
3301
                _("The fee for this transaction seems unusually high."),
3302
                _("({}% of amount)").format(f'{fee_ratio*100:.2f}')
3303
            ])
3304
            short_warning = _("high fee ratio") + "!"
×
3305
        elif feerate > FEERATE_WARNING_HIGH_FEE / 1000:
×
UNCOV
3306
            long_warning = ' '.join([
×
3307
                _("The fee for this transaction seems unusually high."),
3308
                _("(feerate: {})").format(self.config.format_fee_rate(1000 * feerate))
3309
            ])
3310
            short_warning = _("high fee rate") + "!"
×
3311
        if long_warning is None:
×
UNCOV
3312
            return None
×
3313
        else:
UNCOV
3314
            return allow_send, long_warning, short_warning
×
3315

3316
    def get_help_texts_for_receive_request(self, req: Request) -> ReceiveRequestHelp:
5✔
3317
        key = req.get_id()
×
3318
        addr = req.get_address() or ''
×
3319
        amount_sat = req.get_amount_sat() or 0
×
3320
        address_help = ''
×
3321
        URI_help = ''
×
3322
        ln_help = ''
×
3323
        address_is_error = False
×
3324
        URI_is_error = False
×
3325
        ln_is_error = False
×
3326
        ln_swap_suggestion = None
×
3327
        ln_rebalance_suggestion = None
×
3328
        ln_zeroconf_suggestion = False
×
3329
        URI = self.get_request_URI(req) or ''
×
UNCOV
3330
        lightning_has_channels = (
×
3331
            self.lnworker and len([chan for chan in self.lnworker.channels.values() if chan.is_open()]) > 0
3332
        )
3333
        lightning_online = self.lnworker and self.lnworker.num_peers() > 0
×
3334
        can_receive_lightning = self.lnworker and amount_sat <= self.lnworker.num_sats_can_receive()
×
3335
        try:
×
3336
            zeroconf_nodeid = extract_nodeid(self.config.ZEROCONF_TRUSTED_NODE)[0]
×
3337
        except Exception:
×
3338
            zeroconf_nodeid = None
×
UNCOV
3339
        can_get_zeroconf_channel = (self.lnworker and self.config.ACCEPT_ZEROCONF_CHANNELS
×
3340
                                        and zeroconf_nodeid in self.lnworker.peers)
UNCOV
3341
        status = self.get_invoice_status(req)
×
3342

3343
        if status == PR_EXPIRED:
×
UNCOV
3344
            address_help = URI_help = ln_help = _('This request has expired')
×
3345

3346
        is_amt_too_small_for_onchain = amount_sat and amount_sat < self.dust_threshold()
×
3347
        if not addr:
×
3348
            address_is_error = True
×
3349
            address_help = _('This request cannot be paid on-chain')
×
3350
            if is_amt_too_small_for_onchain:
×
3351
                address_help = _('Amount too small to be received onchain')
×
3352
        if not URI:
×
3353
            URI_is_error = True
×
3354
            URI_help = _('This request cannot be paid on-chain')
×
3355
            if is_amt_too_small_for_onchain:
×
3356
                URI_help = _('Amount too small to be received onchain')
×
3357
        if not req.is_lightning():
×
3358
            ln_is_error = True
×
UNCOV
3359
            ln_help = _('This request does not have a Lightning invoice.')
×
3360

3361
        if status == PR_UNPAID:
×
3362
            if self.adb.is_used(addr):
×
UNCOV
3363
                address_help = URI_help = (_("This address has already been used. "
×
3364
                                             "For better privacy, do not reuse it for new payments."))
3365
            if req.is_lightning():
×
3366
                if not lightning_has_channels and not can_get_zeroconf_channel:
×
3367
                    ln_is_error = True
×
3368
                    ln_help = _("You must have an open Lightning channel to receive payments.")
×
3369
                elif not lightning_online:
×
3370
                    ln_is_error = True
×
3371
                    ln_help = _('You must be online to receive Lightning payments.')
×
3372
                elif not can_receive_lightning or (amount_sat <= 0 and not lightning_has_channels):
×
3373
                    ln_rebalance_suggestion = self.lnworker.suggest_rebalance_to_receive(amount_sat)
×
UNCOV
3374
                    ln_swap_suggestion = self.lnworker.suggest_swap_to_receive(amount_sat)
×
3375
                    # prefer to use swaps over JIT channels if possible
3376
                    if can_get_zeroconf_channel and not bool(ln_rebalance_suggestion) and not bool(ln_swap_suggestion):
×
3377
                        if amount_sat < MIN_FUNDING_SAT:
×
3378
                            ln_is_error = True
×
UNCOV
3379
                            ln_help = (_('Cannot receive this payment. Request at least {} '
×
3380
                                        'to purchase a Lightning channel from your service provider.')
3381
                                       .format(self.config.format_amount_and_units(amount_sat=MIN_FUNDING_SAT)))
3382
                        else:
3383
                            ln_zeroconf_suggestion = True
×
UNCOV
3384
                            ln_help = _(f'Receiving this payment will purchase a payment channel from your '
×
3385
                                        f'service provider. Service fees are deducted from the incoming payment.')
3386
                    else:
3387
                        ln_is_error = True
×
3388
                        ln_help = _('You do not have the capacity to receive this amount with Lightning.')
×
3389
                        if bool(ln_rebalance_suggestion):
×
3390
                            ln_help += '\n\n' + _('You may have that capacity if you rebalance your channels.')
×
3391
                        elif bool(ln_swap_suggestion):
×
UNCOV
3392
                            ln_help += '\n\n' + _('You may have that capacity if you swap some of your funds.')
×
3393
                # for URI that has LN part but no onchain part, copy error:
3394
                if not addr and ln_is_error:
×
3395
                    URI_is_error = ln_is_error
×
3396
                    URI_help = ln_help
×
UNCOV
3397
        return ReceiveRequestHelp(
×
3398
            address_help=address_help,
3399
            URI_help=URI_help,
3400
            ln_help=ln_help,
3401
            address_is_error=address_is_error,
3402
            URI_is_error=URI_is_error,
3403
            ln_is_error=ln_is_error,
3404
            ln_rebalance_suggestion=ln_rebalance_suggestion,
3405
            ln_swap_suggestion=ln_swap_suggestion,
3406
            ln_zeroconf_suggestion=ln_zeroconf_suggestion
3407
        )
3408

3409

3410
    def synchronize(self) -> int:
5✔
3411
        """Returns the number of new addresses we generated."""
3412
        return 0
5✔
3413

3414
    def unlock(self, password):
5✔
3415
        self.logger.info(f'unlocking wallet')
×
UNCOV
3416
        self.check_password(password)
×
UNCOV
3417
        self._password_in_memory = password
×
3418

3419
    def lock_wallet(self):
5✔
3420
        self._password_in_memory = None
5✔
3421

3422
    def get_unlocked_password(self):
5✔
UNCOV
3423
        return self._password_in_memory
×
3424

3425
    def get_text_not_enough_funds_mentioning_frozen(
5✔
3426
            self,
3427
            *,
3428
            for_amount: Optional[Union[int, str]] = None,
3429
            hint: Optional[str] = None
3430
    ) -> str:
3431
        """Generate 'Not enough funds' text.
3432
        Include mention of frozen coins (and append optional hint), iff unfreezing would satisfy for_amount
3433
        """
3434
        text = _('Not enough funds')
×
3435
        if for_amount is not None:
×
3436
            if frozen_bal := sum(self.get_frozen_balance()):
×
3437
                frozen_str = None
×
3438
                if isinstance(for_amount, int):
×
3439
                    if frozen_bal + self.get_spendable_balance_sat() > for_amount:
×
3440
                        frozen_str = self.config.format_amount_and_units(frozen_bal)
×
3441
                elif for_amount == '!':
×
3442
                    frozen_str = self.config.format_amount_and_units(frozen_bal)
×
3443
                if frozen_str:
×
3444
                    text = _('Not enough funds') + " " + _('({} are frozen)').format(frozen_str)
×
3445
                if hint:
×
UNCOV
3446
                    text += '. ' + hint
×
UNCOV
3447
        return text
×
3448

3449
    def get_frozen_balance_str(self) -> Optional[str]:
5✔
3450
        frozen_bal = sum(self.get_frozen_balance())
×
3451
        if not frozen_bal:
×
UNCOV
3452
            return None
×
UNCOV
3453
        return self.config.format_amount_and_units(frozen_bal)
×
3454

3455
    def add_future_tx(self, sweep_info: 'SweepInfo', wanted_height: int):
5✔
3456
        """ add local tx to provide user feedback """
3457
        txin = copy.deepcopy(sweep_info.txin)
×
3458
        prevout = txin.prevout.to_str()
×
UNCOV
3459
        prev_txid, index = prevout.split(':')
×
UNCOV
3460
        if txid := self.adb.db.get_spent_outpoint(prev_txid, int(index)):
×
3461
            # set future tx of existing spender because it is not persisted
3462
            # (and wanted_height can change if input of CSV was not mined before)
3463
            self.adb.set_future_tx(txid, wanted_height=wanted_height)
×
UNCOV
3464
            return
×
3465
        name = sweep_info.name
×
3466
        # outputs = [] will send coins to a change address
UNCOV
3467
        tx = self.make_unsigned_transaction(
×
3468
            inputs=[txin],
3469
            outputs=[],
3470
            fee_policy=FixedFeePolicy(0),
3471
        )
3472
        try:
×
3473
            self.adb.add_transaction(tx)
×
3474
        except Exception as e:
×
3475
            self.logger.info(f'could not add future tx: {name}. prevout: {prevout} {str(e)}')
×
3476
            return
×
3477
        self.logger.info(f'added future tx: {name}. prevout: {prevout}')
×
UNCOV
3478
        util.trigger_callback('wallet_updated', self)
×
UNCOV
3479
        self.adb.set_future_tx(tx.txid(), wanted_height=wanted_height)
×
3480

3481

3482
class Simple_Wallet(Abstract_Wallet):
5✔
3483
    # wallet with a single keystore
3484

3485
    def is_watching_only(self):
5✔
3486
        return self.keystore.is_watching_only()
5✔
3487

3488
    def _update_password_for_keystore(self, old_pw, new_pw):
5✔
3489
        if self.keystore and self.keystore.may_have_password():
5✔
3490
            self.keystore.update_password(old_pw, new_pw)
5✔
3491
            self.save_keystore()
5✔
3492

3493
    def save_keystore(self):
5✔
3494
        self.db.put('keystore', self.keystore.dump())
5✔
3495

3496
    @abstractmethod
5✔
3497
    def get_public_key(self, address: str) -> Optional[str]:
5✔
UNCOV
3498
        pass
×
3499

3500
    def get_public_keys(self, address: str) -> Sequence[str]:
5✔
UNCOV
3501
        pk = self.get_public_key(address)
×
UNCOV
3502
        return [pk] if pk else []
×
3503

3504

3505
class Imported_Wallet(Simple_Wallet):
5✔
3506
    # wallet made of imported addresses
3507

3508
    wallet_type = 'imported'
5✔
3509
    txin_type = 'address'
5✔
3510

3511
    def __init__(self, db, *, config):
5✔
3512
        Abstract_Wallet.__init__(self, db, config=config)
5✔
3513
        self.use_change = db.get('use_change', False)
5✔
3514

3515
    def is_watching_only(self):
5✔
3516
        return self.keystore is None
5✔
3517

3518
    def can_import_privkey(self):
5✔
3519
        return bool(self.keystore)
5✔
3520

3521
    def load_keystore(self):
5✔
3522
        self.keystore = load_keystore(self.db, 'keystore') if self.db.get('keystore') else None
5✔
3523

3524
    def save_keystore(self):
5✔
3525
        self.db.put('keystore', self.keystore.dump())
5✔
3526

3527
    def can_import_address(self):
5✔
UNCOV
3528
        return self.is_watching_only()
×
3529

3530
    def can_delete_address(self):
5✔
UNCOV
3531
        return True
×
3532

3533
    def has_seed(self):
5✔
3534
        return False
5✔
3535

3536
    def is_deterministic(self):
5✔
UNCOV
3537
        return False
×
3538

3539
    def is_change(self, address):
5✔
3540
        return False
5✔
3541

3542
    def get_all_known_addresses_beyond_gap_limit(self) -> Set[str]:
5✔
UNCOV
3543
        return set()
×
3544

3545
    def get_fingerprint(self):
5✔
UNCOV
3546
        return ''
×
3547

3548
    def get_addresses(self):
5✔
3549
        # note: overridden so that the history can be cleared
3550
        return self.db.get_imported_addresses()
5✔
3551

3552
    def get_receiving_addresses(self, **kwargs):
5✔
3553
        return self.get_addresses()
5✔
3554

3555
    def get_change_addresses(self, **kwargs):
5✔
3556
        return self.get_addresses()
5✔
3557

3558
    def import_addresses(self, addresses: List[str], *,
5✔
3559
                         write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:
3560
        good_addr = []  # type: List[str]
5✔
3561
        bad_addr = []  # type: List[Tuple[str, str]]
5✔
3562
        for address in addresses:
5✔
3563
            if not bitcoin.is_address(address):
5✔
UNCOV
3564
                bad_addr.append((address, _('invalid address')))
×
3565
                continue
×
3566
            if self.db.has_imported_address(address):
5✔
UNCOV
3567
                bad_addr.append((address, _('address already in wallet')))
×
UNCOV
3568
                continue
×
3569
            good_addr.append(address)
5✔
3570
            self.db.add_imported_address(address, {})
5✔
3571
            self.adb.add_address(address)
5✔
3572
        if write_to_disk:
5✔
3573
            self.save_db()
5✔
3574
        return good_addr, bad_addr
5✔
3575

3576
    def import_address(self, address: str) -> str:
5✔
3577
        good_addr, bad_addr = self.import_addresses([address])
5✔
3578
        if good_addr and good_addr[0] == address:
5✔
3579
            return address
5✔
3580
        else:
UNCOV
3581
            raise BitcoinException(str(bad_addr[0][1]))
×
3582

3583
    def delete_address(self, address: str) -> None:
5✔
3584
        if not self.db.has_imported_address(address):
5✔
UNCOV
3585
            return
×
3586
        if len(self.get_addresses()) <= 1:
5✔
3587
            raise UserFacingException(_('Cannot delete last remaining address from wallet'))
5✔
3588
        transactions_to_remove = set()  # only referred to by this address
5✔
3589
        transactions_new = set()  # txs that are not only referred to by address
5✔
3590
        with self.lock:
5✔
3591
            for addr in self.db.get_history():
5✔
3592
                details = self.adb.get_address_history(addr).items()
5✔
3593
                if addr == address:
5✔
3594
                    for tx_hash, height in details:
5✔
3595
                        transactions_to_remove.add(tx_hash)
5✔
3596
                else:
3597
                    for tx_hash, height in details:
5✔
3598
                        transactions_new.add(tx_hash)
5✔
3599
            transactions_to_remove -= transactions_new
5✔
3600
            self.db.remove_addr_history(address)
5✔
3601
            for tx_hash in transactions_to_remove:
5✔
3602
                self.adb._remove_transaction(tx_hash)
5✔
3603
        self.set_label(address, None)
5✔
3604
        if req:= self.get_request_by_addr(address):
5✔
UNCOV
3605
            self.delete_request(req.get_id())
×
3606
        self.set_frozen_state_of_addresses([address], False, write_to_disk=False)
5✔
3607
        pubkey = self.get_public_key(address)
5✔
3608
        self.db.remove_imported_address(address)
5✔
3609
        if pubkey:
5✔
3610
            # delete key iff no other address uses it (e.g. p2pkh and p2wpkh for same key)
3611
            for txin_type in bitcoin.WIF_SCRIPT_TYPES.keys():
5✔
3612
                try:
5✔
3613
                    addr2 = bitcoin.pubkey_to_address(txin_type, pubkey)
5✔
3614
                except NotImplementedError:
5✔
3615
                    pass
5✔
3616
                else:
3617
                    if self.db.has_imported_address(addr2):
5✔
UNCOV
3618
                        break
×
3619
            else:
3620
                self.keystore.delete_imported_key(pubkey)
5✔
3621
                self.save_keystore()
5✔
3622
        self.save_db()
5✔
3623

3624
    def get_change_addresses_for_new_transaction(self, *args, **kwargs) -> List[str]:
5✔
3625
        # for an imported wallet, if all "change addresses" are already used,
3626
        # it is probably better to send change back to the "from address", than to
3627
        # send it to another random used address and link them together, hence
3628
        # we force "allow_reusing_used_change_addrs=False"
3629
        return super().get_change_addresses_for_new_transaction(
5✔
3630
            *args,
3631
            **{**kwargs, "allow_reusing_used_change_addrs": False},
3632
        )
3633

3634
    def calc_unused_change_addresses(self) -> Sequence[str]:
5✔
3635
        with self.lock:
5✔
3636
            unused_addrs = [addr for addr in self.get_change_addresses()
5✔
3637
                            if not self.adb.is_used(addr) and not self.is_address_reserved(addr)]
3638
            return unused_addrs
5✔
3639

3640
    def is_mine(self, address) -> bool:
5✔
3641
        if not address: return False
5✔
3642
        return self.db.has_imported_address(address)
5✔
3643

3644
    def get_address_index(self, address) -> Optional[str]:
5✔
3645
        # returns None if address is not mine
3646
        return self.get_public_key(address)
5✔
3647

3648
    def get_address_path_str(self, address):
5✔
UNCOV
3649
        return None
×
3650

3651
    def get_public_key(self, address) -> Optional[str]:
5✔
3652
        x = self.db.get_imported_address(address)
5✔
3653
        return x.get('pubkey') if x else None
5✔
3654

3655
    def import_private_keys(self, keys: List[str], password: Optional[str], *,
5✔
3656
                            write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:
3657
        good_addr = []  # type: List[str]
5✔
3658
        bad_keys = []  # type: List[Tuple[str, str]]
5✔
3659
        for key in keys:
5✔
3660
            try:
5✔
3661
                txin_type, pubkey = self.keystore.import_privkey(key, password)
5✔
3662
            except Exception as e:
5✔
3663
                bad_keys.append((key, _('invalid private key') + f': {e}'))
5✔
3664
                continue
5✔
3665
            if txin_type not in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
5✔
3666
                bad_keys.append((key, _('not implemented type') + f': {txin_type}'))
×
UNCOV
3667
                continue
×
3668
            addr = bitcoin.pubkey_to_address(txin_type, pubkey)
5✔
3669
            good_addr.append(addr)
5✔
3670
            self.db.add_imported_address(addr, {'type':txin_type, 'pubkey':pubkey})
5✔
3671
            self.adb.add_address(addr)
5✔
3672
        self.save_keystore()
5✔
3673
        if write_to_disk:
5✔
3674
            self.save_db()
5✔
3675
        return good_addr, bad_keys
5✔
3676

3677
    def import_private_key(self, key: str, password: Optional[str]) -> str:
5✔
3678
        good_addr, bad_keys = self.import_private_keys([key], password=password)
5✔
3679
        if good_addr:
5✔
3680
            return good_addr[0]
5✔
3681
        else:
3682
            raise BitcoinException(str(bad_keys[0][1]))
5✔
3683

3684
    def get_txin_type(self, address):
5✔
3685
        return self.db.get_imported_address(address).get('type', 'address')
5✔
3686

3687
    @profiler
5✔
3688
    def try_detecting_internal_addresses_corruption(self):
5✔
3689
        # we check only a random sample, for performance
UNCOV
3690
        addresses_all = self.get_addresses()
×
3691
        # some random *used* addresses (note: we likely have not synced yet)
3692
        addresses_used = [addr for addr in addresses_all if self.adb.is_used(addr)]
×
UNCOV
3693
        sample1 = random.sample(addresses_used, min(len(addresses_used), 10))
×
3694
        # some random *unused* addresses
3695
        addresses_unused = [addr for addr in addresses_all if not self.adb.is_used(addr)]
×
3696
        sample2 = random.sample(addresses_unused, min(len(addresses_unused), 10))
×
3697
        for addr_found in itertools.chain(sample1, sample2):
×
UNCOV
3698
            self.check_address_for_corruption(addr_found)
×
3699

3700
    def check_address_for_corruption(self, addr):
5✔
3701
        if addr and self.is_mine(addr):
5✔
3702
            pubkey = self.get_public_key(addr)
5✔
3703
            if not pubkey:
5✔
UNCOV
3704
                return
×
3705
            txin_type = self.get_txin_type(addr)
5✔
3706
            if txin_type == 'address':
5✔
UNCOV
3707
                return
×
3708
            if addr != bitcoin.pubkey_to_address(txin_type, pubkey):
5✔
UNCOV
3709
                raise InternalAddressCorruption()
×
3710

3711
    def pubkeys_to_address(self, pubkeys):
5✔
UNCOV
3712
        pubkey = pubkeys[0]
×
3713
        # FIXME This is slow.
3714
        #       Ideally we would re-derive the address from the pubkey and the txin_type,
3715
        #       but we don't know the txin_type, and we only have an addr->txin_type map.
3716
        #       so instead a linear search of reverse-lookups is done...
3717
        for addr in self.db.get_imported_addresses():
×
3718
            if self.db.get_imported_address(addr)['pubkey'] == pubkey:
×
3719
                return addr
×
UNCOV
3720
        return None
×
3721

3722
    def decrypt_message(self, pubkey: str, message, password) -> bytes:
5✔
3723
        # this is significantly faster than the implementation in the superclass
3724
        return self.keystore.decrypt_message(pubkey, message, password)
5✔
3725

3726

3727
class Deterministic_Wallet(Abstract_Wallet):
5✔
3728

3729
    def __init__(self, db, *, config):
5✔
3730
        self._ephemeral_addr_to_addr_index = {}  # type: Dict[str, Sequence[int]]
5✔
3731
        Abstract_Wallet.__init__(self, db, config=config)
5✔
3732
        self.gap_limit = db.get('gap_limit', 20)
5✔
3733
        # generate addresses now. note that without libsecp this might block
3734
        # for a few seconds!
3735
        self.synchronize()
5✔
3736

3737
    def _init_lnworker(self):
5✔
3738
        # lightning_privkey2 is not deterministic (legacy wallets, bip39)
3739
        ln_xprv = self.db.get('lightning_xprv') or self.db.get('lightning_privkey2')
5✔
3740
        # lnworker can only be initialized once receiving addresses are available
3741
        # therefore we instantiate lnworker in DeterministicWallet
3742
        self.lnworker = LNWallet(self, ln_xprv) if ln_xprv else None
5✔
3743

3744
    def has_seed(self):
5✔
3745
        return self.keystore.has_seed()
5✔
3746

3747
    def get_addresses(self):
5✔
3748
        # note: overridden so that the history can be cleared.
3749
        # addresses are ordered based on derivation
3750
        out = self.get_receiving_addresses()
5✔
3751
        out += self.get_change_addresses()
5✔
3752
        return out
5✔
3753

3754
    def get_receiving_addresses(self, *, slice_start=None, slice_stop=None):
5✔
3755
        return self.db.get_receiving_addresses(slice_start=slice_start, slice_stop=slice_stop)
5✔
3756

3757
    def get_change_addresses(self, *, slice_start=None, slice_stop=None):
5✔
3758
        return self.db.get_change_addresses(slice_start=slice_start, slice_stop=slice_stop)
5✔
3759

3760
    @profiler
5✔
3761
    def try_detecting_internal_addresses_corruption(self):
5✔
UNCOV
3762
        addresses_all = self.get_addresses()
×
3763
        # first few addresses
3764
        nfirst_few = 10
×
UNCOV
3765
        sample1 = addresses_all[:nfirst_few]
×
3766
        # some random *used* addresses (note: we likely have not synced yet)
3767
        addresses_used = [addr for addr in addresses_all[nfirst_few:] if self.adb.is_used(addr)]
×
UNCOV
3768
        sample2 = random.sample(addresses_used, min(len(addresses_used), 10))
×
3769
        # some random *unused* addresses
3770
        addresses_unused = [addr for addr in addresses_all[nfirst_few:] if not self.adb.is_used(addr)]
×
3771
        sample3 = random.sample(addresses_unused, min(len(addresses_unused), 10))
×
3772
        for addr_found in itertools.chain(sample1, sample2, sample3):
×
UNCOV
3773
            self.check_address_for_corruption(addr_found)
×
3774

3775
    def check_address_for_corruption(self, addr):
5✔
3776
        if addr and self.is_mine(addr):
5✔
3777
            if addr != self.derive_address(*self.get_address_index(addr)):
5✔
UNCOV
3778
                raise InternalAddressCorruption()
×
3779

3780
    def get_seed(self, password):
5✔
3781
        return self.keystore.get_seed(password)
5✔
3782

3783
    def get_seed_type(self) -> Optional[str]:
5✔
3784
        if not self.has_seed():
×
3785
            return None
×
3786
        assert isinstance(self.keystore, keystore.Deterministic_KeyStore), type(self.keystore)
×
UNCOV
3787
        return self.keystore.get_seed_type()
×
3788

3789
    def change_gap_limit(self, value):
5✔
3790
        '''This method is not called in the code, it is kept for console use'''
3791
        value = int(value)
×
3792
        if value >= self.min_acceptable_gap():
×
3793
            self.gap_limit = value
×
3794
            self.db.put('gap_limit', self.gap_limit)
×
3795
            self.save_db()
×
UNCOV
3796
            return True
×
3797
        else:
UNCOV
3798
            return False
×
3799

3800
    def num_unused_trailing_addresses(self, addresses):
5✔
3801
        k = 0
×
3802
        for addr in addresses[::-1]:
×
3803
            if self.db.get_addr_history(addr):
×
3804
                break
×
3805
            k += 1
×
UNCOV
3806
        return k
×
3807

3808
    def min_acceptable_gap(self) -> int:
5✔
3809
        # fixme: this assumes wallet is synchronized
3810
        n = 0
×
3811
        nmax = 0
×
3812
        addresses = self.get_receiving_addresses()
×
3813
        k = self.num_unused_trailing_addresses(addresses)
×
3814
        for addr in addresses[0:-k]:
×
3815
            if self.adb.address_is_old(addr):
×
UNCOV
3816
                n = 0
×
3817
            else:
3818
                n += 1
×
3819
                nmax = max(nmax, n)
×
UNCOV
3820
        return nmax + 1
×
3821

3822
    @abstractmethod
5✔
3823
    def derive_pubkeys(self, c: int, i: int) -> Sequence[str]:
5✔
UNCOV
3824
        pass
×
3825

3826
    def derive_address(self, for_change: int, n: int) -> str:
5✔
3827
        for_change = int(for_change)
5✔
3828
        pubkeys = self.derive_pubkeys(for_change, n)
5✔
3829
        return self.pubkeys_to_address(pubkeys)
5✔
3830

3831
    def export_private_key_for_path(self, path: Union[Sequence[int], str], password: Optional[str]) -> str:
5✔
3832
        if isinstance(path, str):
5✔
3833
            path = convert_bip32_strpath_to_intpath(path)
5✔
3834
        pk, compressed = self.keystore.get_private_key(path, password)
5✔
3835
        txin_type = self.get_txin_type()  # assumes no mixed-scripts in wallet
5✔
3836
        return bitcoin.serialize_privkey(pk, compressed, txin_type)
5✔
3837

3838
    def get_public_keys_with_deriv_info(self, address: str):
5✔
3839
        der_suffix = self.get_address_index(address)
5✔
3840
        der_suffix = [int(x) for x in der_suffix]
5✔
3841
        return {k.derive_pubkey(*der_suffix): (k, der_suffix)
5✔
3842
                for k in self.get_keystores()}
3843

3844
    def _add_txinout_derivation_info(self, txinout, address, *, only_der_suffix):
5✔
3845
        if not self.is_mine(address):
5✔
UNCOV
3846
            return
×
3847
        pubkey_deriv_info = self.get_public_keys_with_deriv_info(address)
5✔
3848
        for pubkey in pubkey_deriv_info:
5✔
3849
            ks, der_suffix = pubkey_deriv_info[pubkey]
5✔
3850
            fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix,
5✔
3851
                                                                                   only_der_suffix=only_der_suffix)
3852
            txinout.bip32_paths[pubkey] = (fp_bytes, der_full)
5✔
3853

3854
    def create_new_address(self, for_change: bool = False):
5✔
3855
        assert type(for_change) is bool
5✔
3856
        with self.lock:
5✔
3857
            n = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses()
5✔
3858
            address = self.derive_address(int(for_change), n)
5✔
3859
            self.db.add_change_address(address) if for_change else self.db.add_receiving_address(address)
5✔
3860
            self.adb.add_address(address)
5✔
3861
            if for_change:
5✔
3862
                # note: if it's actually "old", it will get filtered later
3863
                self._not_old_change_addresses.append(address)
5✔
3864
            return address
5✔
3865

3866
    def synchronize_sequence(self, for_change: bool) -> int:
5✔
3867
        count = 0  # num new addresses we generated
5✔
3868
        limit = self.gap_limit_for_change if for_change else self.gap_limit
5✔
3869
        while True:
5✔
3870
            num_addr = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses()
5✔
3871
            if num_addr < limit:
5✔
3872
                count += 1
5✔
3873
                self.create_new_address(for_change)
5✔
3874
                continue
5✔
3875
            if for_change:
5✔
3876
                last_few_addresses = self.get_change_addresses(slice_start=-limit)
5✔
3877
            else:
3878
                last_few_addresses = self.get_receiving_addresses(slice_start=-limit)
5✔
3879
            if any(map(self.adb.address_is_old, last_few_addresses)):
5✔
3880
                count += 1
5✔
3881
                self.create_new_address(for_change)
5✔
3882
            else:
3883
                break
5✔
3884
        return count
5✔
3885

3886
    def synchronize(self):
5✔
3887
        count = 0
5✔
3888
        with self.lock:
5✔
3889
            count += self.synchronize_sequence(False)
5✔
3890
            count += self.synchronize_sequence(True)
5✔
3891
        return count
5✔
3892

3893
    def get_all_known_addresses_beyond_gap_limit(self):
5✔
3894
        # note that we don't stop at first large gap
UNCOV
3895
        found = set()
×
3896

3897
        def process_addresses(addrs, gap_limit):
×
3898
            rolling_num_unused = 0
×
3899
            for addr in addrs:
×
3900
                if self.db.get_addr_history(addr):
×
UNCOV
3901
                    rolling_num_unused = 0
×
3902
                else:
3903
                    if rolling_num_unused >= gap_limit:
×
3904
                        found.add(addr)
×
UNCOV
3905
                    rolling_num_unused += 1
×
3906

3907
        process_addresses(self.get_receiving_addresses(), self.gap_limit)
×
3908
        process_addresses(self.get_change_addresses(), self.gap_limit_for_change)
×
UNCOV
3909
        return found
×
3910

3911
    def get_address_index(self, address) -> Optional[Sequence[int]]:
5✔
3912
        return self.db.get_address_index(address) or self._ephemeral_addr_to_addr_index.get(address)
5✔
3913

3914
    def get_address_path_str(self, address):
5✔
3915
        intpath = self.get_address_index(address)
×
3916
        if intpath is None:
×
3917
            return None
×
UNCOV
3918
        return convert_bip32_intpath_to_strpath(intpath)
×
3919

3920
    def _learn_derivation_path_for_address_from_txinout(self, txinout, address):
5✔
3921
        for ks in self.get_keystores():
5✔
3922
            pubkey, der_suffix = ks.find_my_pubkey_in_txinout(txinout, only_der_suffix=True)
5✔
3923
            if der_suffix is not None:
5✔
3924
                # note: we already know the pubkey belongs to the keystore,
3925
                #       but the script template might be different
3926
                if len(der_suffix) != 2: continue
5✔
3927
                try:
5✔
3928
                    my_address = self.derive_address(*der_suffix)
5✔
3929
                except CannotDerivePubkey:
×
3930
                    my_address = None
×
3931
                if my_address == address:
5✔
3932
                    self._ephemeral_addr_to_addr_index[address] = list(der_suffix)
5✔
3933
                    return True
5✔
3934
        return False
5✔
3935

3936
    def get_master_public_keys(self):
5✔
3937
        return [self.get_master_public_key()]
×
3938

3939
    def get_fingerprint(self):
5✔
3940
        return self.get_master_public_key()
5✔
3941

3942
    def get_txin_type(self, address=None):
5✔
3943
        return self.txin_type
5✔
3944

3945

3946
class Standard_Wallet(Simple_Wallet, Deterministic_Wallet):
5✔
3947
    """ Deterministic Wallet with a single pubkey per address """
3948
    wallet_type = 'standard'
5✔
3949

3950
    def __init__(self, db, *, config):
5✔
3951
        Deterministic_Wallet.__init__(self, db, config=config)
5✔
3952

3953
    def get_public_key(self, address):
5✔
3954
        sequence = self.get_address_index(address)
×
3955
        pubkeys = self.derive_pubkeys(*sequence)
×
3956
        return pubkeys[0]
×
3957

3958
    def load_keystore(self):
5✔
3959
        self.keystore = load_keystore(self.db, 'keystore')  # type: KeyStoreWithMPK
5✔
3960
        try:
5✔
3961
            xtype = bip32.xpub_type(self.keystore.xpub)
5✔
3962
        except Exception:
5✔
3963
            xtype = 'standard'
5✔
3964
        self.txin_type = 'p2pkh' if xtype == 'standard' else xtype
5✔
3965

3966
    def get_master_public_key(self):
5✔
3967
        return self.keystore.get_master_public_key()
5✔
3968

3969
    def derive_pubkeys(self, c, i):
5✔
3970
        return [self.keystore.derive_pubkey(c, i).hex()]
5✔
3971

3972
    def pubkeys_to_address(self, pubkeys):
5✔
3973
        pubkey = pubkeys[0]
5✔
3974
        return bitcoin.pubkey_to_address(self.txin_type, pubkey)
5✔
3975

3976
    def has_support_for_slip_19_ownership_proofs(self) -> bool:
5✔
3977
        return self.keystore.has_support_for_slip_19_ownership_proofs()
×
3978

3979
    def add_slip_19_ownership_proofs_to_tx(self, tx: PartialTransaction) -> None:
5✔
3980
        tx.add_info_from_wallet(self)
×
3981
        self.keystore.add_slip_19_ownership_proofs_to_tx(tx=tx, password=None)
×
3982

3983

3984
class Multisig_Wallet(Deterministic_Wallet):
5✔
3985
    # generic m of n
3986

3987
    def __init__(self, db, *, config):
5✔
3988
        self.wallet_type = db.get('wallet_type')
5✔
3989
        self.m, self.n = multisig_type(self.wallet_type)
5✔
3990
        Deterministic_Wallet.__init__(self, db, config=config)
5✔
3991
        # sanity checks
3992
        for ks in self.get_keystores():
5✔
3993
            if not isinstance(ks, keystore.Xpub):
5✔
3994
                raise Exception(f"unexpected keystore type={type(ks)} in multisig")
5✔
3995
            if bip32.xpub_type(self.keystore.xpub) != bip32.xpub_type(ks.xpub):
5✔
3996
                raise Exception(f"multisig wallet needs to have homogeneous xpub types")
5✔
3997
        bip32_nodes = set({ks.get_bip32_node_for_xpub() for ks in self.get_keystores()})
5✔
3998
        if len(bip32_nodes) != len(self.get_keystores()):
5✔
3999
            raise Exception(f"duplicate xpubs in multisig")
5✔
4000

4001
    def get_public_keys(self, address):
5✔
4002
        return [pk.hex() for pk in self.get_public_keys_with_deriv_info(address)]
×
4003

4004
    def pubkeys_to_address(self, pubkeys):
5✔
4005
        redeem_script = self.pubkeys_to_scriptcode(pubkeys)
5✔
4006
        return bitcoin.redeem_script_to_address(self.txin_type, redeem_script)
5✔
4007

4008
    def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> bytes:
5✔
4009
        return transaction.multisig_script(sorted(pubkeys), self.m)
5✔
4010

4011
    def derive_pubkeys(self, c, i):
5✔
4012
        return [k.derive_pubkey(c, i).hex() for k in self.get_keystores()]
5✔
4013

4014
    def load_keystore(self):
5✔
4015
        self.keystores = {}
5✔
4016
        for i in range(self.n):
5✔
4017
            name = 'x%d'%(i+1)
5✔
4018
            self.keystores[name] = load_keystore(self.db, name)
5✔
4019
        self.keystore = self.keystores['x1']
5✔
4020
        xtype = bip32.xpub_type(self.keystore.xpub)
5✔
4021
        self.txin_type = 'p2sh' if xtype == 'standard' else xtype
5✔
4022

4023
    def save_keystore(self):
5✔
4024
        for name, k in self.keystores.items():
×
4025
            self.db.put(name, k.dump())
×
4026

4027
    def get_keystore(self):
5✔
4028
        return self.keystores.get('x1')
×
4029

4030
    def get_keystores(self):
5✔
4031
        return [self.keystores[i] for i in sorted(self.keystores.keys())]
5✔
4032

4033
    def can_have_keystore_encryption(self):
5✔
4034
        return any([k.may_have_password() for k in self.get_keystores()])
×
4035

4036
    def _update_password_for_keystore(self, old_pw, new_pw):
5✔
4037
        for name, keystore in self.keystores.items():
×
4038
            if keystore.may_have_password():
×
4039
                keystore.update_password(old_pw, new_pw)
×
4040
                self.db.put(name, keystore.dump())
×
4041

4042
    def check_password(self, password):
5✔
4043
        for name, keystore in self.keystores.items():
×
4044
            if keystore.may_have_password():
×
4045
                keystore.check_password(password)
×
4046
        if self.has_storage_encryption():
×
4047
            self.storage.check_password(password)
×
4048

4049
    def get_available_storage_encryption_version(self):
5✔
4050
        # multisig wallets are not offered hw device encryption
4051
        return StorageEncryptionVersion.USER_PASSWORD
×
4052

4053
    def has_seed(self):
5✔
4054
        return self.keystore.has_seed()
×
4055

4056
    def is_watching_only(self):
5✔
4057
        return all([k.is_watching_only() for k in self.get_keystores()])
5✔
4058

4059
    def get_master_public_key(self):
5✔
4060
        return self.keystore.get_master_public_key()
×
4061

4062
    def get_master_public_keys(self):
5✔
4063
        return [k.get_master_public_key() for k in self.get_keystores()]
×
4064

4065
    def get_fingerprint(self):
5✔
4066
        return ''.join(sorted(self.get_master_public_keys()))
×
4067

4068

4069
wallet_types = ['standard', 'multisig', 'imported']
5✔
4070

4071
def register_wallet_type(category):
5✔
4072
    wallet_types.append(category)
5✔
4073

4074
wallet_constructors = {
5✔
4075
    'standard': Standard_Wallet,
4076
    'old': Standard_Wallet,
4077
    'xpub': Standard_Wallet,
4078
    'imported': Imported_Wallet
4079
}
4080

4081
def register_constructor(wallet_type, constructor):
5✔
4082
    wallet_constructors[wallet_type] = constructor
5✔
4083

4084
# former WalletFactory
4085
class Wallet(object):
5✔
4086
    """The main wallet "entry point".
4087
    This class is actually a factory that will return a wallet of the correct
4088
    type when passed a WalletStorage instance."""
4089

4090
    def __new__(self, db: 'WalletDB', *, config: SimpleConfig):
5✔
4091
        wallet_type = db.get('wallet_type')
5✔
4092
        WalletClass = Wallet.wallet_class(wallet_type)
5✔
4093
        wallet = WalletClass(db, config=config)
5✔
4094
        return wallet
5✔
4095

4096
    @staticmethod
5✔
4097
    def wallet_class(wallet_type):
5✔
4098
        if multisig_type(wallet_type):
5✔
4099
            return Multisig_Wallet
5✔
4100
        if wallet_type in wallet_constructors:
5✔
4101
            return wallet_constructors[wallet_type]
5✔
UNCOV
4102
        raise WalletFileException("Unknown wallet type: " + str(wallet_type))
×
4103

4104

4105
def create_new_wallet(*, path, config: SimpleConfig, passphrase=None, password=None,
5✔
4106
                      encrypt_file=True, seed_type=None, gap_limit=None) -> dict:
4107
    """Create a new wallet"""
4108
    storage = WalletStorage(path)
5✔
4109
    if storage.file_exists():
5✔
UNCOV
4110
        raise UserFacingException("Remove the existing wallet first!")
×
4111
    db = WalletDB('', storage=storage, upgrade=True)
5✔
4112

4113
    seed = Mnemonic('en').make_seed(seed_type=seed_type)
5✔
4114
    k = keystore.from_seed(seed, passphrase=passphrase)
5✔
4115
    db.put('keystore', k.dump())
5✔
4116
    db.put('wallet_type', 'standard')
5✔
4117
    if k.can_have_deterministic_lightning_xprv():
5✔
4118
        db.put('lightning_xprv', k.get_lightning_xprv(None))
5✔
4119
    if gap_limit is not None:
5✔
4120
        db.put('gap_limit', gap_limit)
5✔
4121
    wallet = Wallet(db, config=config)
5✔
4122
    wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)
5✔
4123
    wallet.synchronize()
5✔
4124
    msg = "Please keep your seed in a safe place; if you lose it, you will not be able to restore your wallet."
5✔
4125
    wallet.save_db()
5✔
4126
    return {'seed': seed, 'wallet': wallet, 'msg': msg}
5✔
4127

4128

4129
def restore_wallet_from_text(
5✔
4130
    text: str,
4131
    *,
4132
    path: Optional[str],
4133
    config: SimpleConfig,
4134
    passphrase: Optional[str] = None,
4135
    password: Optional[str] = None,
4136
    encrypt_file: Optional[bool] = None,
4137
    gap_limit: Optional[int] = None,
4138
) -> dict:
4139
    """Restore a wallet from text. Text can be a seed phrase, a master
4140
    public key, a master private key, a list of bitcoin addresses
4141
    or bitcoin private keys."""
4142
    if path is None:  # create wallet in-memory
5✔
4143
        storage = None
5✔
4144
    else:
4145
        storage = WalletStorage(path)
5✔
4146
        if storage.file_exists():
5✔
UNCOV
4147
            raise UserFacingException("Remove the existing wallet first!")
×
4148
    if encrypt_file is None:
5✔
4149
        encrypt_file = True
5✔
4150
    db = WalletDB('', storage=storage, upgrade=True)
5✔
4151
    text = text.strip()
5✔
4152
    if keystore.is_address_list(text):
5✔
4153
        wallet = Imported_Wallet(db, config=config)
5✔
4154
        addresses = text.split()
5✔
4155
        good_inputs, bad_inputs = wallet.import_addresses(addresses, write_to_disk=False)
5✔
4156
        # FIXME tell user about bad_inputs
4157
        if not good_inputs:
5✔
UNCOV
4158
            raise UserFacingException("None of the given addresses can be imported")
×
4159
    elif keystore.is_private_key_list(text, allow_spaces_inside_key=False):
5✔
4160
        k = keystore.Imported_KeyStore({})
5✔
4161
        db.put('keystore', k.dump())
5✔
4162
        wallet = Imported_Wallet(db, config=config)
5✔
4163
        keys = keystore.get_private_keys(text, allow_spaces_inside_key=False)
5✔
4164
        good_inputs, bad_inputs = wallet.import_private_keys(keys, None, write_to_disk=False)
5✔
4165
        # FIXME tell user about bad_inputs
4166
        if not good_inputs:
5✔
UNCOV
4167
            raise UserFacingException("None of the given privkeys can be imported")
×
4168
    else:
4169
        if keystore.is_master_key(text):
5✔
4170
            k = keystore.from_master_key(text)
5✔
4171
        elif keystore.is_seed(text):
5✔
4172
            k = keystore.from_seed(text, passphrase=passphrase)
5✔
4173
            if k.can_have_deterministic_lightning_xprv():
5✔
4174
                db.put('lightning_xprv', k.get_lightning_xprv(None))
5✔
4175
        else:
UNCOV
4176
            raise UserFacingException("Seed or key not recognized")
×
4177
        db.put('keystore', k.dump())
5✔
4178
        db.put('wallet_type', 'standard')
5✔
4179
        if gap_limit is not None:
5✔
4180
            db.put('gap_limit', gap_limit)
5✔
4181
        wallet = Wallet(db, config=config)
5✔
4182
    if db.storage:
5✔
4183
        assert not db.storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk"
5✔
4184
    wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)
5✔
4185
    wallet.synchronize()
5✔
4186
    msg = ("This wallet was restored offline. It may contain more addresses than displayed. "
5✔
4187
           "Start a daemon and use load_wallet to sync its history.")
4188
    wallet.save_db()
5✔
4189
    return {'wallet': wallet, 'msg': msg}
5✔
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

© 2026 Coveralls, Inc