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

spesmilo / electrum / 4878529344569344

04 Mar 2025 10:05AM UTC coverage: 60.716% (-0.02%) from 60.731%
4878529344569344

Pull #9587

CirrusCI

f321x
disable mpp flags in invoice creation if jit channel is required, check against available liquidity if we need a jit channel
Pull Request #9587: Disable mpp flags in invoice creation if jit channel is required and consider available liquidity

5 of 15 new or added lines in 2 files covered. (33.33%)

847 existing lines in 6 files now uncovered.

20678 of 34057 relevant lines covered (60.72%)

3.03 hits per line

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

63.81
/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 sys
5✔
31
import random
5✔
32
import time
5✔
33
import json
5✔
34
import copy
5✔
35
import errno
5✔
36
import traceback
5✔
37
import operator
5✔
38
import math
5✔
39
from functools import partial
5✔
40
from collections import defaultdict
5✔
41
from numbers import Number
5✔
42
from decimal import Decimal
5✔
43
from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence, Dict, Any, Set, Iterable, Mapping
5✔
44
from abc import ABC, abstractmethod
5✔
45
import itertools
5✔
46
import threading
5✔
47
import enum
5✔
48
import asyncio
5✔
49

50
import electrum_ecc as ecc
5✔
51
from aiorpcx import timeout_after, TaskTimeout, ignore_after, run_in_thread
5✔
52

53
from .i18n import _
5✔
54
from .bip32 import BIP32Node, convert_bip32_intpath_to_strpath, convert_bip32_strpath_to_intpath
5✔
55
from .crypto import sha256
5✔
56
from . import util
5✔
57
from .util import (NotEnoughFunds, UserCancelled, profiler, OldTaskGroup, ignore_exceptions,
5✔
58
                   format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates,
59
                   WalletFileException, BitcoinException,
60
                   InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
61
                   Fiat, bfh, TxMinedInfo, quantize_feerate, OrderedDictWithIndex)
62
from .simple_config import SimpleConfig, FEE_RATIO_HIGH_WARNING, FEERATE_WARNING_HIGH_FEE
5✔
63
from .bitcoin import COIN, TYPE_ADDRESS
5✔
64
from .bitcoin import is_address, address_to_script, is_minikey, relayfee, dust_threshold
5✔
65
from .bitcoin import DummyAddress, DummyAddressUsedInTxException
5✔
66
from .crypto import sha256d
5✔
67
from . import keystore
5✔
68
from .keystore import (load_keystore, Hardware_KeyStore, KeyStore, KeyStoreWithMPK,
5✔
69
                       AddressIndexGeneric, CannotDerivePubkey)
70
from .util import multisig_type, parse_max_spend
5✔
71
from .storage import StorageEncryptionVersion, WalletStorage
5✔
72
from .wallet_db import WalletDB
5✔
73
from . import transaction, bitcoin, coinchooser, paymentrequest, bip32
5✔
74
from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput,
5✔
75
                          PartialTransaction, PartialTxInput, PartialTxOutput, TxOutpoint, Sighash)
76
from .plugin import run_hook
5✔
77
from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
5✔
78
                                   TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE, TX_TIMESTAMP_INF)
79
from .invoices import BaseInvoice, Invoice, Request
5✔
80
from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_UNCONFIRMED, PR_INFLIGHT
5✔
81
from .contacts import Contacts
5✔
82
from .interface import NetworkException
5✔
83
from .mnemonic import Mnemonic
5✔
84
from .logging import get_logger, Logger
5✔
85
from .lnworker import LNWallet
5✔
86
from .paymentrequest import PaymentRequest
5✔
87
from .util import read_json_file, write_json_file, UserFacingException, FileImportFailed
5✔
88
from .util import EventListener, event_listener
5✔
89
from . import descriptor
5✔
90
from .descriptor import Descriptor
5✔
91
from .util import OnchainHistoryItem, LightningHistoryItem
5✔
92

93
if TYPE_CHECKING:
5✔
94
    from .network import Network
×
95
    from .exchange_rate import FxThread
×
96
    from .submarine_swaps import SwapData
×
97
    from .lnchannel import AbstractChannel
×
98

99

100
_logger = get_logger(__name__)
5✔
101

102
TX_STATUS = [
5✔
103
    _('Unconfirmed'),
104
    _('Unconfirmed parent'),
105
    _('Not Verified'),
106
    _('Local'),
107
]
108

109

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

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

134
    u = await network.listunspent_for_scripthash(scripthash)
5✔
135
    async with OldTaskGroup() as group:
5✔
136
        for item in u:
5✔
137
            if len(inputs) >= imax:
5✔
138
                break
×
139
            await group.spawn(append_single_utxo(item))
5✔
140

141

142
async def sweep_preparations(
5✔
143
    privkeys: Iterable[str], network: 'Network', imax=100,
144
) -> Tuple[Sequence[PartialTxInput], Mapping[bytes, bytes]]:
145

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

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

175

176
async def sweep(
5✔
177
        privkeys: Iterable[str],
178
        *,
179
        network: 'Network',
180
        config: 'SimpleConfig',
181
        to_address: str,
182
        fee: int = None,
183
        imax=100,
184
        locktime=None,
185
        tx_version=None) -> PartialTransaction:
186

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

199
    outputs = [PartialTxOutput(scriptpubkey=bitcoin.address_to_script(to_address),
5✔
200
                               value=total - fee)]
201
    if locktime is None:
5✔
202
        locktime = get_locktime_for_new_transaction(network)
×
203

204
    tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime, version=tx_version)
5✔
205
    tx.set_rbf(True)
5✔
206
    tx.sign(keypairs)
5✔
207
    return tx
5✔
208

209

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

239

240
class CannotRBFTx(Exception): pass
5✔
241

242

243
class CannotBumpFee(CannotRBFTx):
5✔
244
    def __str__(self):
5✔
245
        return _('Cannot bump fee') + ':\n\n' + Exception.__str__(self)
×
246

247

248
class CannotDoubleSpendTx(CannotRBFTx):
5✔
249
    def __str__(self):
5✔
250
        return _('Cannot cancel transaction') + ':\n\n' + Exception.__str__(self)
×
251

252

253
class CannotCPFP(Exception):
5✔
254
    def __str__(self):
5✔
255
        return _('Cannot create child transaction') + ':\n\n' + Exception.__str__(self)
×
256

257

258
class InternalAddressCorruption(Exception):
5✔
259
    def __str__(self):
5✔
260
        return _("Wallet file corruption detected. "
×
261
                 "Please restore your wallet from seed, and compare the addresses in both files")
262

263

264
class TransactionPotentiallyDangerousException(Exception): pass
5✔
265

266

267
class TransactionDangerousException(TransactionPotentiallyDangerousException): pass
5✔
268

269

270
class TxSighashRiskLevel(enum.IntEnum):
5✔
271
    # higher value -> more risk
272
    SAFE = 0
5✔
273
    FEE_WARNING_SKIPCONFIRM = 1  # show warning icon (ignored for CLI)
5✔
274
    FEE_WARNING_NEEDCONFIRM = 2  # prompt user for confirmation
5✔
275
    WEIRD_SIGHASH = 3            # prompt user for confirmation
5✔
276
    INSANE_SIGHASH = 4           # reject
5✔
277

278

279
class TxSighashDanger:
5✔
280

281
    def __init__(
5✔
282
        self,
283
        *,
284
        risk_level: TxSighashRiskLevel = TxSighashRiskLevel.SAFE,
285
        short_message: str = None,
286
        messages: List[str] = None,
287
    ):
288
        self.risk_level = risk_level
5✔
289
        self.short_message = short_message
5✔
290
        self._messages = messages or []
5✔
291

292
    def needs_confirm(self) -> bool:
5✔
293
        """If True, the user should be prompted for explicit confirmation before signing."""
294
        return self.risk_level >= TxSighashRiskLevel.FEE_WARNING_NEEDCONFIRM
5✔
295

296
    def needs_reject(self) -> bool:
5✔
297
        """If True, the transaction should be rejected, i.e. abort signing."""
298
        return self.risk_level >= TxSighashRiskLevel.INSANE_SIGHASH
5✔
299

300
    def get_long_message(self) -> str:
5✔
301
        """Returns a description of the potential dangers of signing the tx that can be shown to the user.
302
        Empty string if there are none.
303
        """
304
        if self.short_message:
5✔
305
            header = [self.short_message]
5✔
306
        else:
307
            header = []
×
308
        return "\n".join(header + self._messages)
5✔
309

310
    def combine(*args: 'TxSighashDanger') -> 'TxSighashDanger':
5✔
311
        max_danger = max(args, key=lambda sighash_danger: sighash_danger.risk_level)  # type: TxSighashDanger
5✔
312
        messages = [msg for sighash_danger in args for msg in sighash_danger._messages]
5✔
313
        return TxSighashDanger(
5✔
314
            risk_level=max_danger.risk_level,
315
            short_message=max_danger.short_message,
316
            messages=messages,
317
        )
318

319
    def __repr__(self):
5✔
320
        return (f"<{self.__class__.__name__} risk_level={self.risk_level} "
×
321
                f"short_message={self.short_message!r} _messages={self._messages!r}>")
322

323

324
class BumpFeeStrategy(enum.Enum):
5✔
325
    PRESERVE_PAYMENT = enum.auto()
5✔
326
    DECREASE_PAYMENT = enum.auto()
5✔
327

328
    @classmethod
5✔
329
    def all(cls) -> Sequence['BumpFeeStrategy']:
5✔
330
        return list(BumpFeeStrategy.__members__.values())
×
331

332
    def text(self) -> str:
5✔
333
        if self == self.PRESERVE_PAYMENT:
×
334
            return _('Preserve payment')
×
335
        elif self == self.DECREASE_PAYMENT:
×
336
            return _('Decrease payment')
×
337
        else:
338
            raise Exception(f"unknown strategy: {self=}")
×
339

340

341
class ReceiveRequestHelp(NamedTuple):
5✔
342
    # help texts (warnings/errors):
343
    address_help: str
5✔
344
    URI_help: str
5✔
345
    ln_help: str
5✔
346
    # whether the texts correspond to an error (or just a warning):
347
    address_is_error: bool
5✔
348
    URI_is_error: bool
5✔
349
    ln_is_error: bool
5✔
350

351
    ln_swap_suggestion: Optional[Any] = None
5✔
352
    ln_rebalance_suggestion: Optional[Any] = None
5✔
353

354
    def can_swap(self) -> bool:
5✔
355
        return bool(self.ln_swap_suggestion)
×
356

357
    def can_rebalance(self) -> bool:
5✔
358
        return bool(self.ln_rebalance_suggestion)
×
359

360

361
class TxWalletDelta(NamedTuple):
5✔
362
    is_relevant: bool  # "related to wallet?"
5✔
363
    is_any_input_ismine: bool
5✔
364
    is_all_input_ismine: bool
5✔
365
    delta: int
5✔
366
    fee: Optional[int]
5✔
367

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

385

386
class Abstract_Wallet(ABC, Logger, EventListener):
5✔
387
    """
388
    Wallet classes are created to handle various address generation methods.
389
    Completion states (watching-only, single account, no seed, etc) are handled inside classes.
390
    """
391

392
    LOGGING_SHORTCUT = 'w'
5✔
393
    max_change_outputs = 3
5✔
394
    gap_limit_for_change = 10
5✔
395

396
    txin_type: str
5✔
397
    wallet_type: str
5✔
398
    lnworker: Optional['LNWallet']
5✔
399
    network: Optional['Network']
5✔
400

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

412
        self.network = None
5✔
413
        self.adb = AddressSynchronizer(db, config, name=self.diagnostic_name())
5✔
414
        for addr in self.get_addresses():
5✔
415
            self.adb.add_address(addr)
5✔
416
        self.lock = self.adb.lock
5✔
417
        self.transaction_lock = self.adb.transaction_lock
5✔
418
        self._last_full_history = None
5✔
419
        self._tx_parents_cache = {}
5✔
420
        self._default_labels = {}
5✔
421

422
        self.taskgroup = OldTaskGroup()
5✔
423

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

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

438
        self.load_keystore()
5✔
439
        self._init_lnworker()
5✔
440
        self._init_requests_rhash_index()
5✔
441
        self._prepare_onchain_invoice_paid_detection()
5✔
442
        self.calc_unused_change_addresses()
5✔
443
        # save wallet type the first time
444
        if self.db.get('wallet_type') is None:
5✔
445
            self.db.put('wallet_type', self.wallet_type)
5✔
446
        self.contacts = Contacts(self.db)
5✔
447
        self._coin_price_cache = {}
5✔
448

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

453
        self.test_addresses_sanity()
5✔
454
        if self.storage and self.has_storage_encryption():
5✔
455
            if (se := self.storage.get_encryption_version()) != (ae := self.get_available_storage_encryption_version()):
5✔
456
                raise WalletFileException(f"unexpected storage encryption type. found: {se!r}. allowed: {ae!r}")
×
457

458
        self.register_callbacks()
5✔
459

460
    def _init_lnworker(self):
5✔
461
        self.lnworker = None
5✔
462

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

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

488
    def save_db(self):
5✔
489
        if self.db.storage:
5✔
490
            self.db.write()
5✔
491

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

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

509
    def has_lightning(self) -> bool:
5✔
510
        return bool(self.lnworker)
5✔
511

512
    def has_channels(self):
5✔
513
        return self.lnworker is not None and len(self.lnworker._channels) > 0
×
514

515
    def requires_unlock(self):
5✔
516
        return self.config.ENABLE_ANCHOR_CHANNELS and self.has_channels()
5✔
517

518
    def can_have_lightning(self) -> bool:
5✔
519
        if self.config.ENABLE_ANCHOR_CHANNELS:
×
520
            # this excludes hardware wallets, watching-only wallets
521
            return self.can_have_deterministic_lightning()
×
522
        else:
523
            # we want static_remotekey to be a wallet address
524
            return self.txin_type == 'p2wpkh'
×
525

526
    def can_have_deterministic_lightning(self) -> bool:
5✔
527
        if not self.txin_type == 'p2wpkh':
×
528
            return False
×
529
        if not self.keystore:
×
530
            return False
×
531
        return self.keystore.can_have_deterministic_lightning_xprv()
×
532

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

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

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

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

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

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

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

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

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

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

637
    def clear_history(self):
5✔
638
        self.adb.clear_history()
×
639
        self.save_db()
×
640

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

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

658
    @abstractmethod
5✔
659
    def load_keystore(self) -> None:
5✔
660
        pass
×
661

662
    def diagnostic_name(self):
5✔
663
        return self.basename()
5✔
664

665
    def __str__(self):
5✔
666
        return self.basename()
×
667

668
    def get_master_public_key(self):
5✔
669
        return None
×
670

671
    def get_master_public_keys(self):
5✔
672
        return []
×
673

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

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

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

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

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

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

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

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

744
    def export_labels(self, path):
5✔
745
        write_json_file(path, self.get_all_labels())
×
746

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

863
    def get_swap_by_claim_tx(self, tx: Transaction) -> Optional['SwapData']:
5✔
864
        return self.lnworker.swap_manager.get_swap_by_claim_tx(tx) if self.lnworker else None
5✔
865

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

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

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

972
        if is_relevant:
5✔
973
            if tx_wallet_delta.is_all_input_ismine:
5✔
974
                assert fee is not None
5✔
975
                amount = tx_wallet_delta.delta + fee
5✔
976
            else:
977
                amount = tx_wallet_delta.delta
×
978
        else:
979
            amount = None
×
980

981
        if is_lightning_funding_tx:
5✔
982
            assert not can_bump  # would change txid
×
983

984
        return TxWalletDetails(
5✔
985
            txid=tx_hash,
986
            status=status,
987
            label=label,
988
            can_broadcast=can_broadcast,
989
            can_bump=can_bump,
990
            can_cpfp=can_cpfp,
991
            can_dscancel=can_dscancel,
992
            can_save_as_local=can_save_as_local,
993
            amount=amount,
994
            fee=fee,
995
            tx_mined_status=tx_mined_status,
996
            mempool_depth_bytes=exp_n,
997
            can_remove=can_remove,
998
            is_lightning_funding_tx=is_lightning_funding_tx,
999
            is_related_to_wallet=is_relevant,
1000
        )
1001

1002
    def get_num_parents(self, txid: str) -> Optional[int]:
5✔
1003
        if not self.is_up_to_date():
×
1004
            return
×
1005
        if txid not in self._num_parents:
×
1006
            self._num_parents[txid] = len(self.get_tx_parents(txid))
×
1007
        return self._num_parents[txid]
×
1008

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

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

1051
            for _txid in parents + uncles:
×
1052
                if _txid in self._last_full_history.keys():
×
1053
                    result.update(self.get_tx_parents(_txid))
×
1054
            result[txid] = parents, uncles
×
1055
            self._tx_parents_cache[txid] = result
×
1056
            return result
×
1057

1058
    def get_balance(self, **kwargs):
5✔
1059
        domain = self.get_addresses()
5✔
1060
        return self.adb.get_balance(domain, **kwargs)
5✔
1061

1062
    def get_addr_balance(self, address):
5✔
1063
        return self.adb.get_balance([address])
×
1064

1065
    def get_utxos(
5✔
1066
            self,
1067
            domain: Optional[Iterable[str]] = None,
1068
            **kwargs,
1069
    ):
1070
        if domain is None:
5✔
1071
            domain = self.get_addresses()
5✔
1072
        return self.adb.get_utxos(domain=domain, **kwargs)
5✔
1073

1074
    def get_spendable_coins(
5✔
1075
            self,
1076
            domain: Optional[Iterable[str]] = None,
1077
            *,
1078
            nonlocal_only: bool = False,
1079
            confirmed_only: bool = None,
1080
    ) -> Sequence[PartialTxInput]:
1081
        with self._freeze_lock:
5✔
1082
            frozen_addresses = self._frozen_addresses.copy()
5✔
1083
        if confirmed_only is None:
5✔
1084
            confirmed_only = self.config.WALLET_SPEND_CONFIRMED_ONLY
5✔
1085
        utxos = self.get_utxos(
5✔
1086
            domain=domain,
1087
            excluded_addresses=frozen_addresses,
1088
            mature_only=True,
1089
            confirmed_funding_only=confirmed_only,
1090
            nonlocal_only=nonlocal_only,
1091
        )
1092
        utxos = [utxo for utxo in utxos if not self.is_frozen_coin(utxo)]
5✔
1093
        return utxos
5✔
1094

1095
    @abstractmethod
5✔
1096
    def get_receiving_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence[str]:
5✔
1097
        pass
×
1098

1099
    @abstractmethod
5✔
1100
    def get_change_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence[str]:
5✔
1101
        pass
×
1102

1103
    def dummy_address(self):
5✔
1104
        # first receiving address
1105
        return self.get_receiving_addresses(slice_start=0, slice_stop=1)[0]
×
1106

1107
    def get_frozen_balance(self):
5✔
1108
        with self._freeze_lock:
×
1109
            frozen_addresses = self._frozen_addresses.copy()
×
1110
        # note: for coins, use is_frozen_coin instead of _frozen_coins,
1111
        #       as latter only contains *manually* frozen ones
1112
        frozen_coins = {utxo.prevout.to_str() for utxo in self.get_utxos()
×
1113
                        if self.is_frozen_coin(utxo)}
1114
        if not frozen_coins:  # shortcut
×
1115
            return self.adb.get_balance(frozen_addresses)
×
1116
        c1, u1, x1 = self.get_balance()
×
1117
        c2, u2, x2 = self.get_balance(
×
1118
            excluded_addresses=frozen_addresses,
1119
            excluded_coins=frozen_coins,
1120
        )
1121
        return c1-c2, u1-u2, x1-x2
×
1122

1123
    def get_balances_for_piechart(self):
5✔
1124
        # return only positive values
1125
        # todo: add lightning frozen
1126
        c, u, x = self.get_balance()
×
1127
        fc, fu, fx = self.get_frozen_balance()
×
1128
        lightning = self.lnworker.get_balance() if self.has_lightning() else 0
×
1129
        f_lightning = self.lnworker.get_balance(frozen=True) if self.has_lightning() else 0
×
1130
        # subtract frozen funds
1131
        cc = c - fc
×
1132
        uu = u - fu
×
1133
        xx = x - fx
×
1134
        frozen = fc + fu + fx
×
1135
        return cc, uu, xx, frozen, lightning - f_lightning, f_lightning
×
1136

1137
    def balance_at_timestamp(self, domain, target_timestamp):
5✔
1138
        # we assume that get_history returns items ordered by block height
1139
        # we also assume that block timestamps are monotonic (which is false...!)
1140
        h = self.adb.get_history(domain=domain)
×
1141
        balance = 0
×
1142
        for hist_item in h:
×
1143
            balance = hist_item.balance
×
1144
            if hist_item.tx_mined_status.timestamp is None or hist_item.tx_mined_status.timestamp > target_timestamp:
×
1145
                return balance - hist_item.delta
×
1146
        # return last balance
1147
        return balance
×
1148

1149
    def get_onchain_history(
5✔
1150
            self, *,
1151
            domain=None,
1152
            from_timestamp=None,
1153
            to_timestamp=None,
1154
            from_height=None,
1155
            to_height=None) -> Dict[str, OnchainHistoryItem]:
1156
        # sanity check
1157
        if (from_timestamp is not None or to_timestamp is not None) \
×
1158
                and (from_height is not None or to_height is not None):
1159
            raise UserFacingException('timestamp and block height based filtering cannot be used together')
×
1160
        # call lnworker first, because it adds accounting addresses
1161
        groups = self.lnworker.get_groups_for_onchain_history() if self.lnworker else {}
×
1162
        if domain is None:
×
1163
            domain = self.get_addresses()
×
1164

1165
        now = time.time()
×
1166
        transactions = OrderedDictWithIndex()
×
1167
        monotonic_timestamp = 0
×
1168
        for hist_item in self.adb.get_history(domain=domain):
×
1169
            timestamp = (hist_item.tx_mined_status.timestamp or TX_TIMESTAMP_INF)
×
1170
            height = hist_item.tx_mined_status
×
1171
            if from_timestamp and (timestamp or now) < from_timestamp:
×
1172
                continue
×
1173
            if to_timestamp and (timestamp or now) >= to_timestamp:
×
1174
                continue
×
1175
            if from_height is not None and from_height > height > 0:
×
1176
                continue
×
1177
            if to_height is not None and (height >= to_height or height <= 0):
×
1178
                continue
×
1179
            monotonic_timestamp = max(monotonic_timestamp, timestamp)
×
1180
            txid = hist_item.txid
×
1181
            group_id = groups.get(txid)
×
1182
            label = self.get_label_for_txid(txid)
×
1183
            tx_item = OnchainHistoryItem(
×
1184
                txid=hist_item.txid,
1185
                amount_sat=hist_item.delta,
1186
                fee_sat=hist_item.fee,
1187
                balance_sat=hist_item.balance,
1188
                tx_mined_status=hist_item.tx_mined_status,
1189
                label=label,
1190
                monotonic_timestamp=monotonic_timestamp,
1191
                group_id=group_id,
1192
            )
1193
            transactions[hist_item.txid] = tx_item
×
1194

1195
        return transactions
×
1196

1197
    def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> Invoice:
5✔
1198
        height = self.adb.get_local_height()
5✔
1199
        if pr:
5✔
1200
            return Invoice.from_bip70_payreq(pr, height=height)
×
1201
        amount_msat = 0
5✔
1202
        for x in outputs:
5✔
1203
            if parse_max_spend(x.value):
5✔
1204
                amount_msat = '!'
5✔
1205
                break
5✔
1206
            else:
1207
                assert isinstance(x.value, int), f"{x.value!r}"
×
1208
                amount_msat += x.value * 1000
×
1209
        timestamp = None
5✔
1210
        exp = None
5✔
1211
        if URI:
5✔
1212
            timestamp = URI.get('time')
5✔
1213
            exp = URI.get('exp')
5✔
1214
        timestamp = timestamp or int(Invoice._get_cur_time())
5✔
1215
        exp = exp or 0
5✔
1216
        invoice = Invoice(
5✔
1217
            amount_msat=amount_msat,
1218
            message=message,
1219
            time=timestamp,
1220
            exp=exp,
1221
            outputs=outputs,
1222
            bip70=None,
1223
            height=height,
1224
            lightning_invoice=None,
1225
        )
1226
        return invoice
5✔
1227

1228
    def save_invoice(self, invoice: Invoice, *, write_to_disk: bool = True) -> None:
5✔
1229
        key = invoice.get_id()
×
1230
        if not invoice.is_lightning():
×
1231
            if self.is_onchain_invoice_paid(invoice)[0]:
×
1232
                _logger.info("saving invoice... but it is already paid!")
×
1233
            with self.transaction_lock:
×
1234
                for txout in invoice.get_outputs():
×
1235
                    self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(key)
×
1236
        self._invoices[key] = invoice
×
1237
        if write_to_disk:
×
1238
            self.save_db()
×
1239

1240
    def clear_invoices(self):
5✔
1241
        self._invoices.clear()
×
1242
        self.save_db()
×
1243

1244
    def clear_requests(self):
5✔
1245
        self._receive_requests.clear()
×
1246
        self._requests_addr_to_key.clear()
×
1247
        self.save_db()
×
1248

1249
    def get_invoices(self) -> List[Invoice]:
5✔
1250
        out = list(self._invoices.values())
×
1251
        out.sort(key=lambda x:x.time)
×
1252
        return out
×
1253

1254
    def get_unpaid_invoices(self) -> List[Invoice]:
5✔
1255
        invoices = self.get_invoices()
×
1256
        return [x for x in invoices if self.get_invoice_status(x) != PR_PAID]
×
1257

1258
    def get_invoice(self, invoice_id):
5✔
1259
        return self._invoices.get(invoice_id)
×
1260

1261
    def import_requests(self, path):
5✔
1262
        data = read_json_file(path)
×
1263
        for x in data:
×
1264
            try:
×
1265
                req = Request(**x)
×
1266
            except Exception:
×
1267
                raise FileImportFailed(_("Invalid invoice format"))
×
1268
            self.add_payment_request(req, write_to_disk=False)
×
1269
        self.save_db()
×
1270

1271
    def export_requests(self, path):
5✔
1272
        # note: this does not export preimages for LN bolt11 invoices
1273
        write_json_file(path, list(self._receive_requests.values()))
×
1274

1275
    def import_invoices(self, path):
5✔
1276
        data = read_json_file(path)
×
1277
        for x in data:
×
1278
            try:
×
1279
                invoice = Invoice(**x)
×
1280
            except Exception:
×
1281
                raise FileImportFailed(_("Invalid invoice format"))
×
1282
            self.save_invoice(invoice, write_to_disk=False)
×
1283
        self.save_db()
×
1284

1285
    def export_invoices(self, path):
5✔
1286
        write_json_file(path, list(self._invoices.values()))
×
1287

1288
    def get_relevant_invoices_for_tx(self, tx_hash: Optional[str]) -> Sequence[Invoice]:
5✔
1289
        if not tx_hash:
5✔
1290
            return []
×
1291
        invoice_keys = self._invoices_from_txid_map.get(tx_hash, set())
5✔
1292
        invoices = [self.get_invoice(key) for key in invoice_keys]
5✔
1293
        invoices = [inv for inv in invoices if inv]  # filter out None
5✔
1294
        for inv in invoices:
5✔
1295
            assert isinstance(inv, Invoice), f"unexpected type {type(inv)}"
×
1296
        return invoices
5✔
1297

1298
    def _init_requests_rhash_index(self):
5✔
1299
        # self._requests_addr_to_key may contain addresses that can be reused
1300
        # this is checked in get_request_by_address
1301
        self._requests_addr_to_key = defaultdict(set)  # type: Dict[str, Set[str]]
5✔
1302
        for req in self._receive_requests.values():
5✔
1303
            if addr := req.get_address():
5✔
1304
                self._requests_addr_to_key[addr].add(req.get_id())
5✔
1305

1306
    def _prepare_onchain_invoice_paid_detection(self):
5✔
1307
        self._invoices_from_txid_map = defaultdict(set)  # type: Dict[str, Set[str]]
5✔
1308
        self._invoices_from_scriptpubkey_map = defaultdict(set)  # type: Dict[bytes, Set[str]]
5✔
1309
        self._update_onchain_invoice_paid_detection(self._invoices.keys())
5✔
1310

1311
    def _update_onchain_invoice_paid_detection(self, invoice_keys: Iterable[str]) -> None:
5✔
1312
        for invoice_key in invoice_keys:
5✔
1313
            invoice = self._invoices.get(invoice_key)
5✔
1314
            if not invoice:
5✔
1315
                continue
×
1316
            if invoice.is_lightning() and not invoice.get_address():
5✔
1317
                continue
5✔
1318
            if invoice.is_lightning() and self.lnworker and self.lnworker.get_invoice_status(invoice) == PR_PAID:
5✔
1319
                continue
5✔
1320
            is_paid, conf_needed, relevant_txs = self._is_onchain_invoice_paid(invoice)
5✔
1321
            if is_paid:
5✔
1322
                for txid in relevant_txs:
5✔
1323
                    self._invoices_from_txid_map[txid].add(invoice_key)
5✔
1324
            for txout in invoice.get_outputs():
5✔
1325
                self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key)
5✔
1326
            # update invoice status
1327
            status = self.get_invoice_status(invoice)
5✔
1328
            util.trigger_callback('invoice_status', self, invoice_key, status)
5✔
1329

1330
    def _is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional[int], Sequence[str]]:
5✔
1331
        """Returns whether on-chain invoice/request is satisfied, num confs required txs have,
1332
        and list of relevant TXIDs.
1333
        """
1334
        outputs = invoice.get_outputs()
5✔
1335
        if not outputs:  # e.g. lightning-only
5✔
1336
            return False, None, []
5✔
1337
        invoice_amounts = defaultdict(int)  # type: Dict[bytes, int]  # scriptpubkey -> value_sats
5✔
1338
        for txo in outputs:  # type: PartialTxOutput
5✔
1339
            invoice_amounts[txo.scriptpubkey] += 1 if parse_max_spend(txo.value) else txo.value
5✔
1340
        relevant_txs = set()
5✔
1341
        is_paid = True
5✔
1342
        conf_needed = None  # type: Optional[int]
5✔
1343
        with self.lock, self.transaction_lock:
5✔
1344
            for invoice_scriptpubkey, invoice_amt in invoice_amounts.items():
5✔
1345
                scripthash = bitcoin.script_to_scripthash(invoice_scriptpubkey)
5✔
1346
                prevouts_and_values = self.db.get_prevouts_by_scripthash(scripthash)
5✔
1347
                confs_and_values = []
5✔
1348
                for prevout, v in prevouts_and_values:
5✔
1349
                    relevant_txs.add(prevout.txid.hex())
5✔
1350
                    tx_height = self.adb.get_tx_height(prevout.txid.hex())
5✔
1351
                    if 0 < tx_height.height <= invoice.height:  # exclude txs older than invoice
5✔
1352
                        continue
5✔
1353
                    confs_and_values.append((tx_height.conf or 0, v))
5✔
1354
                # check that there is at least one TXO, and that they pay enough.
1355
                # note: "at least one TXO" check is needed for zero amount invoice (e.g. OP_RETURN)
1356
                vsum = 0
5✔
1357
                for conf, v in reversed(sorted(confs_and_values)):
5✔
1358
                    vsum += v
5✔
1359
                    if vsum >= invoice_amt:
5✔
1360
                        conf_needed = min(conf_needed, conf) if conf_needed is not None else conf
5✔
1361
                        break
5✔
1362
                else:
1363
                    is_paid = False
5✔
1364
        return is_paid, conf_needed, list(relevant_txs)
5✔
1365

1366
    def is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional[int]]:
5✔
1367
        is_paid, conf_needed, relevant_txs = self._is_onchain_invoice_paid(invoice)
5✔
1368
        return is_paid, conf_needed
5✔
1369

1370
    @profiler
5✔
1371
    def get_full_history(self, fx=None, *, onchain_domain=None, include_lightning=True, include_fiat=False) -> dict:
5✔
1372
        """
1373
        includes both onchain and lightning
1374
        includes grouping information
1375
        """
1376
        transactions_tmp = OrderedDictWithIndex()
×
1377
        # add on-chain txns
1378
        onchain_history = self.get_onchain_history(domain=onchain_domain)
×
1379
        for tx_item in onchain_history.values():
×
1380
            txid = tx_item.txid
×
1381
            transactions_tmp[txid] = tx_item.to_dict()
×
1382
            transactions_tmp[txid]['lightning'] = False
×
1383

1384
        # add lightning_transactions
1385
        lightning_history = self.lnworker.get_lightning_history() if self.lnworker and include_lightning else {}
×
1386
        for tx_item in lightning_history.values():
×
1387
            key = tx_item.payment_hash or 'ln:' + tx_item.group_id
×
1388
            transactions_tmp[key] = tx_item.to_dict()
×
1389
            transactions_tmp[key]['lightning'] = True
×
1390

1391
        # sort on-chain and LN stuff into new dict, by timestamp
1392
        # (we rely on this being a *stable* sort)
1393
        def sort_key(x):
×
1394
            txid, tx_item = x
×
1395
            ts = tx_item.get('monotonic_timestamp') or tx_item.get('timestamp') or float('inf')
×
1396
            height = self.adb.tx_height_to_sort_height(tx_item.get('height'))
×
1397
            return ts, height
×
1398
        # create groups
1399
        transactions = OrderedDictWithIndex()
×
1400
        for k, tx_item in sorted(list(transactions_tmp.items()), key=sort_key):
×
1401
            group_id = tx_item.get('group_id')
×
1402
            if not group_id:
×
1403
                transactions[k] = tx_item
×
1404
            else:
1405
                key = 'group:' + group_id
×
1406
                parent = transactions.get(key)
×
1407
                group_label = self.get_label_for_group(group_id)
×
UNCOV
1408
                if parent is None:
×
1409
                    parent = {
×
1410
                        'label': group_label,
1411
                        'fiat_value': Fiat(Decimal(0), fx.ccy) if fx else None,
1412
                        'bc_value': Satoshis(0),
1413
                        'ln_value': Satoshis(0),
1414
                        'value': Satoshis(0),
1415
                        'children': [],
1416
                        'timestamp': 0,
1417
                        'date': timestamp_to_datetime(0),
1418
                        'fee_sat': 0,
1419
                        # fixme: there is no guarantee that there will be an onchain tx in the group
1420
                        'height': 0,
1421
                        'confirmations': 0,
1422
                        'txid': '----',
1423
                    }
UNCOV
1424
                    transactions[key] = parent
×
UNCOV
1425
                if 'bc_value' in tx_item:
×
UNCOV
1426
                    parent['bc_value'] += tx_item['bc_value']
×
UNCOV
1427
                if 'ln_value' in tx_item:
×
1428
                    parent['ln_value'] += tx_item['ln_value']
×
1429
                parent['value'] = parent['bc_value'] + parent['ln_value']
×
1430
                if 'fiat_value' in tx_item:
×
1431
                    parent['fiat_value'] += tx_item['fiat_value']
×
1432
                if tx_item.get('txid') == group_id:
×
1433
                    parent['lightning'] = False
×
1434
                    parent['txid'] = tx_item['txid']
×
1435
                    parent['timestamp'] = tx_item['timestamp']
×
1436
                    parent['date'] = timestamp_to_datetime(tx_item['timestamp'])
×
1437
                    parent['height'] = tx_item['height']
×
1438
                    parent['confirmations'] = tx_item['confirmations']
×
1439
                    parent['wanted_height'] = tx_item.get('wanted_height')
×
1440
                parent['children'].append(tx_item)
×
1441

1442
        now = time.time()
×
UNCOV
1443
        for key, item in transactions.items():
×
1444
            children = item.get('children', [])
×
1445
            if len(children) == 1:
×
1446
                transactions[key] = children[0]
×
1447
            # add on-chain and lightning values
1448
            # note: 'value' has msat precision (as LN has msat precision)
UNCOV
1449
            item['value'] = item.get('bc_value', Satoshis(0)) + item.get('ln_value', Satoshis(0))
×
UNCOV
1450
            for child in item.get('children', []):
×
1451
                child['value'] = child.get('bc_value', Satoshis(0)) + child.get('ln_value', Satoshis(0))
×
1452
            if include_fiat:
×
1453
                value = item['value'].value
×
1454
                txid = item.get('txid')
×
1455
                if not item.get('lightning') and txid:
×
1456
                    fiat_fields = self.get_tx_item_fiat(tx_hash=txid, amount_sat=value, fx=fx, tx_fee=item['fee_sat'])
×
1457
                    item.update(fiat_fields)
×
1458
                else:
1459
                    timestamp = item['timestamp'] or now
×
UNCOV
1460
                    fiat_value = value / Decimal(bitcoin.COIN) * fx.timestamp_rate(timestamp)
×
1461
                    item['fiat_value'] = Fiat(fiat_value, fx.ccy)
×
1462
                    item['fiat_default'] = True
×
1463
        return transactions
×
1464

1465
    @profiler
5✔
1466
    def get_onchain_capital_gains(self, fx, **kwargs):
5✔
1467
        # History with capital gains, using utxo pricing
1468
        # FIXME: Lightning capital gains would requires FIFO
UNCOV
1469
        from_timestamp = kwargs.get('from_timestamp')
×
UNCOV
1470
        to_timestamp = kwargs.get('to_timestamp')
×
1471
        history = self.get_onchain_history(**kwargs)
×
1472
        show_fiat = fx and fx.is_enabled() and fx.has_history()
×
1473
        out = []
×
1474
        income = 0
×
1475
        expenditures = 0
×
1476
        capital_gains = Decimal(0)
×
1477
        fiat_income = Decimal(0)
×
1478
        fiat_expenditures = Decimal(0)
×
1479
        for txid, hitem in history.items():
×
1480
            item = hitem.to_dict()
×
1481
            if item['bc_value'].value == 0:
×
1482
                continue
×
1483
            timestamp = item['timestamp']
×
1484
            tx_hash = item['txid']
×
1485
            tx_fee = item['fee_sat']
×
1486
            # fixme: use in and out values
1487
            value = item['bc_value'].value
×
UNCOV
1488
            if value < 0:
×
1489
                expenditures += -value
×
1490
            else:
1491
                income += value
×
1492
            # fiat computations
1493
            if show_fiat:
×
UNCOV
1494
                fiat_fields = self.get_tx_item_fiat(tx_hash=tx_hash, amount_sat=value, fx=fx, tx_fee=tx_fee)
×
1495
                fiat_value = fiat_fields['fiat_value'].value
×
1496
                if value < 0:
×
1497
                    capital_gains += fiat_fields['capital_gain'].value
×
1498
                    fiat_expenditures += -fiat_value
×
1499
                else:
1500
                    fiat_income += fiat_value
×
UNCOV
1501
            out.append(item)
×
1502
        # add summary
1503
        if out:
×
UNCOV
1504
            first_item = out[0]
×
1505
            last_item = out[-1]
×
1506
            start_height = first_item['height'] - 1
×
1507
            end_height = last_item['height']
×
1508

1509
            b = first_item['bc_balance'].value
×
UNCOV
1510
            v = first_item['bc_value'].value
×
1511
            start_balance = None if b is None or v is None else b - v
×
1512
            end_balance = last_item['bc_balance'].value
×
1513

1514
            if from_timestamp is not None and to_timestamp is not None:
×
UNCOV
1515
                start_timestamp = from_timestamp
×
1516
                end_timestamp = to_timestamp
×
1517
            else:
1518
                start_timestamp = first_item['timestamp']
×
UNCOV
1519
                end_timestamp = last_item['timestamp']
×
1520

1521
            start_coins = self.get_utxos(
×
1522
                block_height=start_height,
1523
                confirmed_funding_only=True,
1524
                confirmed_spending_only=True,
1525
                nonlocal_only=True)
UNCOV
1526
            end_coins = self.get_utxos(
×
1527
                block_height=end_height,
1528
                confirmed_funding_only=True,
1529
                confirmed_spending_only=True,
1530
                nonlocal_only=True)
1531

UNCOV
1532
            def summary_point(timestamp, height, balance, coins):
×
UNCOV
1533
                date = timestamp_to_datetime(timestamp)
×
1534
                out = {
×
1535
                    'date': date,
1536
                    'block_height': height,
1537
                    'BTC_balance': Satoshis(balance),
1538
                }
UNCOV
1539
                if show_fiat:
×
UNCOV
1540
                    ap = self.acquisition_price(coins, fx.timestamp_rate, fx.ccy)
×
1541
                    lp = self.liquidation_price(coins, fx.timestamp_rate, timestamp)
×
1542
                    out['acquisition_price'] = Fiat(ap, fx.ccy)
×
1543
                    out['liquidation_price'] = Fiat(lp, fx.ccy)
×
1544
                    out['unrealized_gains'] = Fiat(lp - ap, fx.ccy)
×
1545
                    out['fiat_balance'] = Fiat(fx.historical_value(balance, date), fx.ccy)
×
1546
                    out['BTC_fiat_price'] = Fiat(fx.historical_value(COIN, date), fx.ccy)
×
1547
                return out
×
1548

1549
            summary_start = summary_point(start_timestamp, start_height, start_balance, start_coins)
×
UNCOV
1550
            summary_end = summary_point(end_timestamp, end_height, end_balance, end_coins)
×
1551
            flow = {
×
1552
                'BTC_incoming': Satoshis(income),
1553
                'BTC_outgoing': Satoshis(expenditures)
1554
            }
UNCOV
1555
            if show_fiat:
×
UNCOV
1556
                flow['fiat_currency'] = fx.ccy
×
1557
                flow['fiat_incoming'] = Fiat(fiat_income, fx.ccy)
×
1558
                flow['fiat_outgoing'] = Fiat(fiat_expenditures, fx.ccy)
×
1559
                flow['realized_capital_gains'] = Fiat(capital_gains, fx.ccy)
×
1560
            summary = {
×
1561
                'begin': summary_start,
1562
                'end': summary_end,
1563
                'flow': flow,
1564
            }
1565

1566
        else:
UNCOV
1567
            summary = {}
×
UNCOV
1568
        return summary
×
1569

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

1573
    def liquidation_price(self, coins, price_func, timestamp):
5✔
UNCOV
1574
        p = price_func(timestamp)
×
UNCOV
1575
        return sum([coin.value_sats() for coin in coins]) * p / Decimal(COIN)
×
1576

1577
    def default_fiat_value(self, tx_hash, fx, value_sat):
5✔
1578
        return value_sat / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate)
5✔
1579

1580
    def get_tx_item_fiat(
5✔
1581
            self,
1582
            *,
1583
            tx_hash: str,
1584
            amount_sat: int,
1585
            fx: 'FxThread',
1586
            tx_fee: Optional[int],
1587
    ) -> Dict[str, Any]:
UNCOV
1588
        item = {}
×
UNCOV
1589
        fiat_value = self.get_fiat_value(tx_hash, fx.ccy)
×
1590
        fiat_default = fiat_value is None
×
1591
        fiat_rate = self.price_at_timestamp(tx_hash, fx.timestamp_rate)
×
1592
        fiat_value = fiat_value if fiat_value is not None else self.default_fiat_value(tx_hash, fx, amount_sat)
×
1593
        fiat_fee = tx_fee / Decimal(COIN) * fiat_rate if tx_fee is not None else None
×
1594
        item['fiat_currency'] = fx.ccy
×
1595
        item['fiat_rate'] = Fiat(fiat_rate, fx.ccy)
×
1596
        item['fiat_value'] = Fiat(fiat_value, fx.ccy)
×
1597
        item['fiat_fee'] = Fiat(fiat_fee, fx.ccy) if fiat_fee is not None else None
×
1598
        item['fiat_default'] = fiat_default
×
1599
        if amount_sat < 0:
×
1600
            acquisition_price = - amount_sat / Decimal(COIN) * self.average_price(tx_hash, fx.timestamp_rate, fx.ccy)
×
1601
            liquidation_price = - fiat_value
×
1602
            item['acquisition_price'] = Fiat(acquisition_price, fx.ccy)
×
1603
            cg = liquidation_price - acquisition_price
×
1604
            item['capital_gain'] = Fiat(cg, fx.ccy)
×
1605
        return item
×
1606

1607
    def _get_label(self, key: str) -> str:
5✔
1608
        # key is typically: address / txid / LN-payment-hash-hex
UNCOV
1609
        return self._labels.get(key) or ''
×
1610

1611
    def get_label_for_address(self, addr: str) -> str:
5✔
UNCOV
1612
        label = self._labels.get(addr) or ''
×
UNCOV
1613
        if not label and (request := self.get_request_by_addr(addr)):
×
1614
            label = request.get_message()
×
1615
        return label
×
1616

1617
    def set_default_label(self, key:str, value:str):
5✔
UNCOV
1618
        self._default_labels[key] = value
×
1619

1620
    def get_label_for_outpoint(self, outpoint:str) -> str:
5✔
1621
        return self._labels.get(outpoint) or self._get_default_label_for_outpoint(outpoint)
5✔
1622

1623
    def _get_default_label_for_outpoint(self, outpoint: str) -> str:
5✔
1624
        return self._default_labels.get(outpoint)
5✔
1625

1626
    def get_label_for_group(self, group_id: str) -> str:
5✔
UNCOV
1627
        return self._default_labels.get('group:' + group_id)
×
1628

1629
    def set_group_label(self, group_id: str, label: str):
5✔
UNCOV
1630
        self._default_labels['group:' + group_id] = label
×
1631

1632
    def get_label_for_txid(self, tx_hash: str) -> str:
5✔
1633
        return self._labels.get(tx_hash) or self._get_default_label_for_txid(tx_hash)
5✔
1634

1635
    def _get_default_label_for_txid(self, tx_hash: str) -> str:
5✔
1636
        if label := self._default_labels.get(tx_hash):
5✔
UNCOV
1637
            return label
×
1638
        labels = []
5✔
1639
        tx = self.adb.get_transaction(tx_hash)
5✔
1640
        if tx:
5✔
1641
            for i in range(len(tx.outputs())):
5✔
1642
                outpoint = tx_hash + f':{i}'
5✔
1643
                if label := self.get_label_for_outpoint(outpoint):
5✔
UNCOV
1644
                    labels.append(label)
×
1645
            for txin in tx.inputs():
5✔
1646
                outpoint = txin.prevout.to_str()
5✔
1647
                if label := self.get_label_for_outpoint(outpoint):
5✔
UNCOV
1648
                    labels.append(label)
×
1649

1650
        # note: we don't deserialize tx as the history calls us for every tx, and that would be slow
1651
        if not self.db.get_txi_addresses(tx_hash):
5✔
1652
            # no inputs are ismine -> likely incoming payment -> concat labels of output addresses
UNCOV
1653
            for addr in self.db.get_txo_addresses(tx_hash):
×
UNCOV
1654
                label = self.get_label_for_address(addr)
×
1655
                if label:
×
1656
                    labels.append(label)
×
1657
        else:
1658
            # some inputs are ismine -> likely outgoing payment
1659
            for invoice in self.get_relevant_invoices_for_tx(tx_hash):
5✔
UNCOV
1660
                if invoice.message:
×
UNCOV
1661
                    labels.append(invoice.message)
×
1662
        #if not labels and self.lnworker and (label:= self.lnworker.get_label_for_txid(tx_hash)):
1663
        #    labels.append(label)
1664
        return ', '.join(labels)
5✔
1665

1666
    def _get_default_label_for_rhash(self, rhash: str) -> str:
5✔
UNCOV
1667
        req = self.get_request(rhash)
×
UNCOV
1668
        return req.get_message() if req else ''
×
1669

1670
    def get_label_for_rhash(self, rhash: str) -> str:
5✔
UNCOV
1671
        return self._labels.get(rhash) or self._get_default_label_for_rhash(rhash)
×
1672

1673
    def get_all_labels(self) -> Dict[str, str]:
5✔
UNCOV
1674
        with self.lock:
×
UNCOV
1675
            return copy.copy(self._labels)
×
1676

1677
    def get_tx_status(self, tx_hash: str, tx_mined_info: TxMinedInfo):
5✔
1678
        extra = []
5✔
1679
        height = tx_mined_info.height
5✔
1680
        conf = tx_mined_info.conf
5✔
1681
        timestamp = tx_mined_info.timestamp
5✔
1682
        if height == TX_HEIGHT_FUTURE:
5✔
UNCOV
1683
            num_blocks_remainining = tx_mined_info.wanted_height - self.adb.get_local_height()
×
UNCOV
1684
            num_blocks_remainining = max(0, num_blocks_remainining)
×
1685
            return 2, _('in {} blocks').format(num_blocks_remainining)
×
1686
        if conf == 0:
5✔
1687
            tx = self.db.get_transaction(tx_hash)
5✔
1688
            if not tx:
5✔
UNCOV
1689
                return 2, _("unknown")
×
1690
            if not tx.is_complete():
5✔
1691
                tx.add_info_from_wallet(self)  # needed for estimated_size(), for txin size calc
5✔
1692
            fee = self.adb.get_tx_fee(tx_hash)
5✔
1693
            if fee is not None:
5✔
1694
                size = tx.estimated_size()
5✔
1695
                fee_per_byte = fee / size
5✔
1696
                extra.append(format_fee_satoshis(fee_per_byte) + f" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VB}")
5✔
1697
            if fee is not None and height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED) \
5✔
1698
               and self.config.has_fee_mempool():
UNCOV
1699
                exp_n = self.config.fee_to_depth(fee_per_byte)
×
UNCOV
1700
                if exp_n is not None:
×
1701
                    extra.append(self.config.get_depth_mb_str(exp_n))
×
1702
            if height == TX_HEIGHT_LOCAL:
5✔
1703
                status = 3
5✔
UNCOV
1704
            elif height == TX_HEIGHT_UNCONF_PARENT:
×
UNCOV
1705
                status = 1
×
1706
            elif height == TX_HEIGHT_UNCONFIRMED:
×
1707
                status = 0
×
1708
            else:
1709
                status = 2  # not SPV verified
×
1710
        else:
1711
            status = 3 + min(conf, 6)
×
1712
        time_str = format_time(timestamp) if timestamp else _("unknown")
5✔
1713
        status_str = TX_STATUS[status] if status < 4 else time_str
5✔
1714
        if extra:
5✔
1715
            status_str += ' [%s]'%(', '.join(extra))
5✔
1716
        return status, status_str
5✔
1717

1718
    def relayfee(self):
5✔
1719
        return relayfee(self.network)
5✔
1720

1721
    def dust_threshold(self):
5✔
1722
        return dust_threshold(self.network)
5✔
1723

1724
    def get_unconfirmed_base_tx_for_batching(self, outputs, coins) -> Optional[Transaction]:
5✔
1725
        candidate = None
5✔
1726
        domain = self.get_addresses()
5✔
1727
        for hist_item in self.adb.get_history(domain):
5✔
1728
            # tx should not be mined yet
1729
            if hist_item.tx_mined_status.conf > 0: continue
5✔
1730
            # conservative future proofing of code: only allow known unconfirmed types
1731
            if hist_item.tx_mined_status.height not in (TX_HEIGHT_UNCONFIRMED,
5✔
1732
                                                        TX_HEIGHT_UNCONF_PARENT,
1733
                                                        TX_HEIGHT_LOCAL):
UNCOV
1734
                continue
×
1735
            # tx should be "outgoing" from wallet
1736
            if hist_item.delta >= 0:
5✔
1737
                continue
5✔
1738
            tx = self.db.get_transaction(hist_item.txid)
5✔
1739
            if not tx:
5✔
UNCOV
1740
                continue
×
1741
            # is_mine outputs should not be spent yet
1742
            # to avoid cancelling our own dependent transactions
1743
            txid = tx.txid()
5✔
1744
            if any([self.is_mine(o.address) and self.db.get_spent_outpoint(txid, output_idx)
5✔
1745
                    for output_idx, o in enumerate(tx.outputs())]):
UNCOV
1746
                continue
×
1747
            # all inputs should be is_mine
1748
            if not all([self.is_mine(self.adb.get_txin_address(txin)) for txin in tx.inputs()]):
5✔
UNCOV
1749
                continue
×
1750
            # tx must have opted-in for RBF (even if local, for consistency)
1751
            if not self.can_rbf_tx(tx):
5✔
UNCOV
1752
                continue
×
1753
            # reject merge if we need to spend outputs from the base tx
1754
            remaining_amount = sum(c.value_sats() for c in coins if c.prevout.txid.hex() != tx.txid())
5✔
1755
            change_amount = sum(o.value for o in tx.outputs() if self.is_change(o.address))
5✔
1756
            output_amount = sum(o.value for o in outputs)
5✔
1757
            if output_amount > remaining_amount + change_amount:
5✔
1758
                continue
5✔
1759
            # prefer txns already in mempool (vs local)
1760
            if hist_item.tx_mined_status.height == TX_HEIGHT_LOCAL:
5✔
UNCOV
1761
                candidate = tx
×
UNCOV
1762
                continue
×
1763
            return tx
5✔
1764
        return candidate
5✔
1765

1766
    def get_change_addresses_for_new_transaction(
5✔
1767
            self, preferred_change_addr=None, *, allow_reusing_used_change_addrs: bool = True,
1768
    ) -> List[str]:
1769
        change_addrs = []
5✔
1770
        if preferred_change_addr:
5✔
1771
            if isinstance(preferred_change_addr, (list, tuple)):
5✔
1772
                change_addrs = list(preferred_change_addr)
5✔
1773
            else:
UNCOV
1774
                change_addrs = [preferred_change_addr]
×
1775
        elif self.use_change:
5✔
1776
            # Recalc and get unused change addresses
1777
            addrs = self.calc_unused_change_addresses()
5✔
1778
            # New change addresses are created only after a few
1779
            # confirmations.
1780
            if addrs:
5✔
1781
                # if there are any unused, select all
1782
                change_addrs = addrs
5✔
1783
            else:
1784
                # if there are none, take one randomly from the last few
1785
                if not allow_reusing_used_change_addrs:
5✔
1786
                    return []
5✔
UNCOV
1787
                addrs = self.get_change_addresses(slice_start=-self.gap_limit_for_change)
×
UNCOV
1788
                change_addrs = [random.choice(addrs)] if addrs else []
×
1789
        for addr in change_addrs:
5✔
1790
            assert is_address(addr), f"not valid bitcoin address: {addr}"
5✔
1791
            # note that change addresses are not necessarily ismine
1792
            # in which case this is a no-op
1793
            self.check_address_for_corruption(addr)
5✔
1794
        max_change = self.max_change_outputs if self.multiple_change else 1
5✔
1795
        return change_addrs[:max_change]
5✔
1796

1797
    def get_single_change_address_for_new_transaction(
5✔
1798
            self, preferred_change_addr=None, *, allow_reusing_used_change_addrs: bool = True,
1799
    ) -> Optional[str]:
1800
        addrs = self.get_change_addresses_for_new_transaction(
5✔
1801
            preferred_change_addr=preferred_change_addr,
1802
            allow_reusing_used_change_addrs=allow_reusing_used_change_addrs,
1803
        )
1804
        if addrs:
5✔
1805
            return addrs[0]
5✔
UNCOV
1806
        return None
×
1807

1808
    @check_returned_address_for_corruption
5✔
1809
    def get_new_sweep_address_for_channel(self) -> str:
5✔
1810
        # Recalc and get unused change addresses
UNCOV
1811
        addrs = self.calc_unused_change_addresses()
×
UNCOV
1812
        if addrs:
×
1813
            selected_addr = addrs[0]
×
1814
        else:
1815
            # if there are none, take one randomly from the last few
UNCOV
1816
            addrs = self.get_change_addresses(slice_start=-self.gap_limit_for_change)
×
UNCOV
1817
            if addrs:
×
1818
                selected_addr = random.choice(addrs)
×
1819
            else:  # fallback for e.g. imported wallets
1820
                selected_addr = self.get_receiving_address()
×
UNCOV
1821
        assert is_address(selected_addr), f"not valid bitcoin address: {selected_addr}"
×
1822
        return selected_addr
×
1823

1824
    def can_pay_onchain(self, outputs, coins=None):
5✔
UNCOV
1825
        fee = partial(self.config.estimate_fee, allow_fallback_to_static_rates=True)  # to avoid NoDynamicFeeEstimates
×
UNCOV
1826
        try:
×
1827
            self.make_unsigned_transaction(
×
1828
                coins=coins,
1829
                outputs=outputs,
1830
                fee=fee)
UNCOV
1831
        except NotEnoughFunds:
×
UNCOV
1832
            return False
×
1833
        return True
×
1834

1835
    @profiler(min_threshold=0.1)
5✔
1836
    def make_unsigned_transaction(
5✔
1837
            self, *,
1838
            coins: Sequence[PartialTxInput],
1839
            outputs: List[PartialTxOutput],
1840
            inputs: Optional[List[PartialTxInput]] = None,
1841
            fee=None,
1842
            change_addr: str = None,
1843
            is_sweep: bool = False,  # used by Wallet_2fa subclass
1844
            rbf: Optional[bool] = True,
1845
            BIP69_sort: Optional[bool] = True,
1846
            base_tx: Optional[PartialTransaction] = None,
1847
            send_change_to_lightning: Optional[bool] = None,
1848
    ) -> PartialTransaction:
1849
        """Can raise NotEnoughFunds or NoDynamicFeeEstimates."""
1850

1851
        if not inputs and not coins:  # any bitcoin tx must have at least 1 input by consensus
5✔
1852
            raise NotEnoughFunds()
5✔
1853
        if any([c.already_has_some_signatures() for c in coins]):
5✔
UNCOV
1854
            raise Exception("Some inputs already contain signatures!")
×
1855
        if inputs is None:
5✔
1856
            inputs = []
5✔
1857
        if inputs:
5✔
UNCOV
1858
            input_set = set(txin.prevout for txin in inputs)
×
UNCOV
1859
            coins = [coin for coin in coins if (coin.prevout not in input_set)]
×
1860
        if base_tx is None and self.config.WALLET_BATCH_RBF:
5✔
1861
            base_tx = self.get_unconfirmed_base_tx_for_batching(outputs, coins)
5✔
1862
        if send_change_to_lightning is None:
5✔
1863
            send_change_to_lightning = self.config.WALLET_SEND_CHANGE_TO_LIGHTNING
5✔
1864

1865
        # prevent side-effect with '!'
1866
        outputs = copy.deepcopy(outputs)
5✔
1867

1868
        # check outputs for "max" amount
1869
        i_max = []
5✔
1870
        i_max_sum = 0
5✔
1871
        for i, o in enumerate(outputs):
5✔
1872
            weight = parse_max_spend(o.value)
5✔
1873
            if weight:
5✔
1874
                i_max_sum += weight
5✔
1875
                i_max.append((weight, i))
5✔
1876

1877
        if fee is None and self.config.fee_per_kb() is None:
5✔
UNCOV
1878
            raise NoDynamicFeeEstimates()
×
1879

1880
        for txin in coins:
5✔
1881
            self.add_input_info(txin)
5✔
1882
            nSequence = 0xffffffff - (2 if rbf else 1)
5✔
1883
            txin.nsequence = nSequence
5✔
1884

1885
        # Fee estimator
1886
        if fee is None:
5✔
UNCOV
1887
            fee_estimator = self.config.estimate_fee
×
1888
        elif isinstance(fee, Number):
5✔
1889
            fee_estimator = lambda size: fee
5✔
1890
        elif callable(fee):
5✔
1891
            fee_estimator = fee
5✔
1892
        else:
UNCOV
1893
            raise Exception(f'Invalid argument fee: {fee}')
×
1894

1895
        # set if we merge with another transaction
1896
        rbf_merge_txid = None
5✔
1897

1898
        if len(i_max) == 0:
5✔
1899
            # Let the coin chooser select the coins to spend
1900
            coin_chooser = coinchooser.get_coin_chooser(self.config)
5✔
1901
            # If there is an unconfirmed RBF tx, merge with it
1902
            if base_tx:
5✔
1903
                # make sure we don't try to spend change from the tx-to-be-replaced:
1904
                coins = [c for c in coins if c.prevout.txid.hex() != base_tx.txid()]
5✔
1905
                is_local = self.adb.get_tx_height(base_tx.txid()).height == TX_HEIGHT_LOCAL
5✔
1906
                if not isinstance(base_tx, PartialTransaction):
5✔
1907
                    base_tx = PartialTransaction.from_tx(base_tx)
5✔
1908
                    base_tx.add_info_from_wallet(self)
5✔
1909
                else:
1910
                    # don't cast PartialTransaction, because it removes make_witness
UNCOV
1911
                    for txin in base_tx.inputs():
×
UNCOV
1912
                        txin.witness = None
×
1913
                base_tx_fee = base_tx.get_fee()
5✔
1914
                base_feerate = Decimal(base_tx_fee)/base_tx.estimated_size()
5✔
1915
                relayfeerate = Decimal(self.relayfee()) / 1000
5✔
1916
                original_fee_estimator = fee_estimator
5✔
1917
                def fee_estimator(size: Union[int, float, Decimal]) -> int:
5✔
1918
                    size = Decimal(size)
5✔
1919
                    lower_bound_relayfee = int(base_tx_fee + round(size * relayfeerate)) if not is_local else 0
5✔
1920
                    lower_bound_feerate = int(base_feerate * size) + 1
5✔
1921
                    lower_bound = max(lower_bound_feerate, lower_bound_relayfee)
5✔
1922
                    return max(lower_bound, original_fee_estimator(size))
5✔
1923
                txi = base_tx.inputs() + list(inputs)
5✔
1924
                txo = list(filter(lambda o: not self.is_change(o.address), base_tx.outputs())) + list(outputs)
5✔
1925
                old_change_addrs = [o.address for o in base_tx.outputs() if self.is_change(o.address)]
5✔
1926
                rbf_merge_txid = base_tx.txid()
5✔
1927
            else:
1928
                txi = list(inputs)
5✔
1929
                txo = list(outputs)
5✔
1930
                old_change_addrs = []
5✔
1931
            # change address. if empty, coin_chooser will set it
1932
            change_addrs = self.get_change_addresses_for_new_transaction(change_addr or old_change_addrs)
5✔
1933
            if self.config.WALLET_MERGE_DUPLICATE_OUTPUTS:
5✔
1934
                txo = transaction.merge_duplicate_tx_outputs(txo)
5✔
1935
            tx = coin_chooser.make_tx(
5✔
1936
                coins=coins,
1937
                inputs=txi,
1938
                outputs=txo,
1939
                change_addrs=change_addrs,
1940
                fee_estimator_vb=fee_estimator,
1941
                dust_threshold=self.dust_threshold(),
1942
                BIP69_sort=BIP69_sort)
1943
            if self.lnworker and send_change_to_lightning:
5✔
UNCOV
1944
                change = tx.get_change_outputs()
×
1945
                # do not use multiple change addresses
1946
                if len(change) == 1:
×
UNCOV
1947
                    amount = change[0].value
×
1948
                    if amount <= self.lnworker.num_sats_can_receive():
×
1949
                        tx.replace_output_address(change[0].address, DummyAddress.SWAP)
×
1950
        else:
1951
            # "spend max" branch
1952
            # note: This *will* spend inputs with negative effective value (if there are any).
1953
            #       Given as the user is spending "max", and so might be abandoning the wallet,
1954
            #       try to include all UTXOs, otherwise leftover might remain in the UTXO set
1955
            #       forever. see #5433
1956
            # note: Actually, it might be the case that not all UTXOs from the wallet are
1957
            #       being spent if the user manually selected UTXOs.
1958
            sendable = sum(map(lambda c: c.value_sats(), coins))
5✔
1959
            for (_,i) in i_max:
5✔
1960
                outputs[i].value = 0
5✔
1961
            tx = PartialTransaction.from_io(list(coins), list(outputs))
5✔
1962
            fee = fee_estimator(tx.estimated_size())
5✔
1963
            amount = sendable - tx.output_value() - fee
5✔
1964
            if amount < 0:
5✔
UNCOV
1965
                raise NotEnoughFunds()
×
1966
            distr_amount = 0
5✔
1967
            for (weight, i) in i_max:
5✔
1968
                val = int((amount/i_max_sum) * weight)
5✔
1969
                outputs[i].value = val
5✔
1970
                distr_amount += val
5✔
1971

1972
            (x,i) = i_max[-1]
5✔
1973
            outputs[i].value += (amount - distr_amount)
5✔
1974
            tx = PartialTransaction.from_io(list(coins), list(outputs))
5✔
1975

1976
        # Timelock tx to current height.
1977
        tx.locktime = get_locktime_for_new_transaction(self.network)
5✔
1978
        tx.rbf_merge_txid = rbf_merge_txid
5✔
1979
        tx.add_info_from_wallet(self)
5✔
1980
        run_hook('make_unsigned_transaction', self, tx)
5✔
1981
        return tx
5✔
1982

1983
    def is_frozen_address(self, addr: str) -> bool:
5✔
UNCOV
1984
        return addr in self._frozen_addresses
×
1985

1986
    def is_frozen_coin(self, utxo: PartialTxInput) -> bool:
5✔
1987
        prevout_str = utxo.prevout.to_str()
5✔
1988
        frozen = self._frozen_coins.get(prevout_str, None)
5✔
1989
        # note: there are three possible states for 'frozen':
1990
        #       True/False if the user explicitly set it,
1991
        #       None otherwise
1992
        if frozen is None:
5✔
1993
            return self._is_coin_small_and_unconfirmed(utxo)
5✔
UNCOV
1994
        return bool(frozen)
×
1995

1996
    def _is_coin_small_and_unconfirmed(self, utxo: PartialTxInput) -> bool:
5✔
1997
        """If true, the coin should not be spent.
1998
        The idea here is that an attacker might send us a UTXO in a
1999
        large low-fee unconfirmed tx that will ~never confirm. If we
2000
        spend it as part of a tx ourselves, that too will not confirm
2001
        (unless we use a high fee, but that might not be worth it for
2002
        a small value UTXO).
2003
        In particular, this test triggers for large "dusting transactions"
2004
        that are used for advertising purposes by some entities.
2005
        see #6960
2006
        """
2007
        # confirmed UTXOs are fine; check this first for performance:
2008
        block_height = utxo.block_height
5✔
2009
        assert block_height is not None
5✔
2010
        if block_height > 0:
5✔
UNCOV
2011
            return False
×
2012
        # exempt large value UTXOs
2013
        value_sats = utxo.value_sats()
5✔
2014
        assert value_sats is not None
5✔
2015
        threshold = self.config.WALLET_UNCONF_UTXO_FREEZE_THRESHOLD_SAT
5✔
2016
        if value_sats >= threshold:
5✔
2017
            return False
5✔
2018
        # if funding tx has any is_mine input, then UTXO is fine
2019
        funding_tx = self.db.get_transaction(utxo.prevout.txid.hex())
5✔
2020
        if funding_tx is None:
5✔
2021
            # we should typically have the funding tx available;
2022
            # might not have it e.g. while not up_to_date
UNCOV
2023
            return True
×
2024
        if any(self.is_mine(self.adb.get_txin_address(txin))
5✔
2025
               for txin in funding_tx.inputs()):
2026
            return False
5✔
UNCOV
2027
        return True
×
2028

2029
    def set_frozen_state_of_addresses(
5✔
2030
        self,
2031
        addrs: Iterable[str],
2032
        freeze: bool,
2033
        *,
2034
        write_to_disk: bool = True,
2035
    ) -> bool:
2036
        """Set frozen state of the addresses to FREEZE, True or False"""
2037
        if all(self.is_mine(addr) for addr in addrs):
5✔
2038
            with self._freeze_lock:
5✔
2039
                if freeze:
5✔
2040
                    self._frozen_addresses |= set(addrs)
5✔
2041
                else:
2042
                    self._frozen_addresses -= set(addrs)
5✔
2043
                self.db.put('frozen_addresses', list(self._frozen_addresses))
5✔
2044
            util.trigger_callback('status')
5✔
2045
            if write_to_disk:
5✔
2046
                self.save_db()
5✔
2047
            return True
5✔
UNCOV
2048
        return False
×
2049

2050
    def set_frozen_state_of_coins(
5✔
2051
        self,
2052
        utxos: Iterable[str],
2053
        freeze: bool,
2054
        *,
2055
        write_to_disk: bool = True,
2056
    ) -> None:
2057
        """Set frozen state of the utxos to FREEZE, True or False"""
2058
        # basic sanity check that input is not garbage: (see if raises)
UNCOV
2059
        [TxOutpoint.from_str(utxo) for utxo in utxos]
×
UNCOV
2060
        with self._freeze_lock:
×
2061
            for utxo in utxos:
×
2062
                self._frozen_coins[utxo] = bool(freeze)
×
2063
        util.trigger_callback('status')
×
2064
        if write_to_disk:
×
2065
            self.save_db()
×
2066

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

2071
    def set_reserved_state_of_address(self, addr: str, *, reserved: bool) -> None:
5✔
2072
        if not self.is_mine(addr):
5✔
2073
            # silently ignore non-ismine addresses
UNCOV
2074
            return
×
2075
        with self.lock:
5✔
2076
            has_changed = (addr in self._reserved_addresses) != reserved
5✔
2077
            if reserved:
5✔
2078
                self._reserved_addresses.add(addr)
5✔
2079
            else:
UNCOV
2080
                self._reserved_addresses.discard(addr)
×
2081
            if has_changed:
5✔
2082
                self.db.put('reserved_addresses', list(self._reserved_addresses))
×
2083

2084
    def set_reserved_addresses_for_chan(self, chan: 'AbstractChannel', *, reserved: bool) -> None:
5✔
2085
        for addr in chan.get_wallet_addresses_channel_might_want_reserved():
5✔
2086
            self.set_reserved_state_of_address(addr, reserved=reserved)
5✔
2087

2088
    def can_export(self):
5✔
UNCOV
2089
        return not self.is_watching_only() and hasattr(self.keystore, 'get_private_key')
×
2090

2091
    def get_bumpfee_strategies_for_tx(
5✔
2092
        self,
2093
        *,
2094
        tx: Transaction,
2095
    ) -> Tuple[Sequence[BumpFeeStrategy], int]:
2096
        """Returns tuple(list of available strategies, idx of recommended option among those)."""
UNCOV
2097
        all_strats = BumpFeeStrategy.all()
×
2098
        # are we paying max?
2099
        invoices = self.get_relevant_invoices_for_tx(tx.txid())
×
UNCOV
2100
        if len(invoices) == 1 and len(invoices[0].outputs) == 1:
×
2101
            if invoices[0].outputs[0].value == '!':
×
2102
                return all_strats, all_strats.index(BumpFeeStrategy.DECREASE_PAYMENT)
×
2103
        # do not decrease payment if it is a swap
2104
        if self.get_swaps_by_funding_tx(tx):
×
UNCOV
2105
            return [BumpFeeStrategy.PRESERVE_PAYMENT], 0
×
2106
        # default
2107
        return all_strats, all_strats.index(BumpFeeStrategy.PRESERVE_PAYMENT)
×
2108

2109
    def bump_fee(
5✔
2110
            self,
2111
            *,
2112
            tx: Transaction,
2113
            new_fee_rate: Union[int, float, Decimal],
2114
            coins: Sequence[PartialTxInput] = None,
2115
            strategy: BumpFeeStrategy = BumpFeeStrategy.PRESERVE_PAYMENT,
2116
    ) -> PartialTransaction:
2117
        """Increase the miner fee of 'tx'.
2118
        'new_fee_rate' is the target min rate in sat/vbyte
2119
        'coins' is a list of UTXOs we can choose from as potential new inputs to be added
2120

2121
        note: it is the caller's responsibility to have already called tx.add_info_from_network().
2122
              Without that, all txins must be ismine.
2123
        """
2124
        assert tx
5✔
2125
        if not isinstance(tx, PartialTransaction):
5✔
2126
            tx = PartialTransaction.from_tx(tx)
5✔
2127
        assert isinstance(tx, PartialTransaction)
5✔
2128
        tx.remove_signatures()
5✔
2129
        if not self.can_rbf_tx(tx):
5✔
UNCOV
2130
            raise CannotBumpFee(_('Transaction is final'))
×
2131
        new_fee_rate = quantize_feerate(new_fee_rate)  # strip excess precision
5✔
2132
        tx.add_info_from_wallet(self)
5✔
2133
        if tx.is_missing_info_from_network():
5✔
UNCOV
2134
            raise Exception("tx missing info from network")
×
2135
        old_tx_size = tx.estimated_size()
5✔
2136
        old_fee = tx.get_fee()
5✔
2137
        assert old_fee is not None
5✔
2138
        old_fee_rate = old_fee / old_tx_size  # sat/vbyte
5✔
2139
        if new_fee_rate <= old_fee_rate:
5✔
UNCOV
2140
            raise CannotBumpFee(_("The new fee rate needs to be higher than the old fee rate."))
×
2141

2142
        if strategy == BumpFeeStrategy.PRESERVE_PAYMENT:
5✔
2143
            # FIXME: we should try decreasing change first,
2144
            # but it requires updating a bunch of unit tests
2145
            try:
5✔
2146
                tx_new = self._bump_fee_through_coinchooser(
5✔
2147
                    tx=tx,
2148
                    new_fee_rate=new_fee_rate,
2149
                    coins=coins,
2150
                )
2151
            except CannotBumpFee as e:
5✔
2152
                tx_new = self._bump_fee_through_decreasing_change(
5✔
2153
                    tx=tx, new_fee_rate=new_fee_rate)
2154
        elif strategy == BumpFeeStrategy.DECREASE_PAYMENT:
5✔
2155
            tx_new = self._bump_fee_through_decreasing_payment(
5✔
2156
                tx=tx, new_fee_rate=new_fee_rate)
2157
        else:
UNCOV
2158
            raise Exception(f"unknown strategy: {strategy=}")
×
2159

2160
        target_min_fee = new_fee_rate * tx_new.estimated_size()
5✔
2161
        actual_fee = tx_new.get_fee()
5✔
2162
        if actual_fee + 1 < target_min_fee:
5✔
UNCOV
2163
            raise CannotBumpFee(
×
2164
                f"bump_fee fee target was not met. "
2165
                f"got {actual_fee}, expected >={target_min_fee}. "
2166
                f"target rate was {new_fee_rate}")
2167
        tx_new.locktime = get_locktime_for_new_transaction(self.network)
5✔
2168
        tx_new.set_rbf(True)
5✔
2169
        tx_new.add_info_from_wallet(self)
5✔
2170
        return tx_new
5✔
2171

2172
    def _bump_fee_through_coinchooser(
5✔
2173
            self,
2174
            *,
2175
            tx: PartialTransaction,
2176
            new_fee_rate: Union[int, Decimal],
2177
            coins: Sequence[PartialTxInput] = None,
2178
    ) -> PartialTransaction:
2179
        """Increase the miner fee of 'tx'.
2180

2181
        - keeps all inputs
2182
        - keeps all not is_mine outputs,
2183
        - allows adding new inputs
2184
        """
2185
        tx = copy.deepcopy(tx)
5✔
2186
        tx.add_info_from_wallet(self)
5✔
2187
        assert tx.get_fee() is not None
5✔
2188
        old_inputs = list(tx.inputs())
5✔
2189
        old_outputs = list(tx.outputs())
5✔
2190
        # change address
2191
        old_change_addrs = [o.address for o in old_outputs if self.is_change(o.address)]
5✔
2192
        change_addrs = self.get_change_addresses_for_new_transaction(old_change_addrs)
5✔
2193
        # which outputs to keep?
2194
        if old_change_addrs:
5✔
2195
            fixed_outputs = list(filter(lambda o: not self.is_change(o.address), old_outputs))
5✔
2196
        else:
2197
            if all(self.is_mine(o.address) for o in old_outputs):
5✔
2198
                # all outputs are is_mine and none of them are change.
2199
                # we bail out as it's unclear what the user would want!
2200
                # the coinchooser bump fee method is probably not a good idea in this case
2201
                raise CannotBumpFee(_('All outputs are non-change is_mine'))
5✔
2202
            old_not_is_mine = list(filter(lambda o: not self.is_mine(o.address), old_outputs))
5✔
2203
            if old_not_is_mine:
5✔
2204
                fixed_outputs = old_not_is_mine
5✔
2205
            else:
UNCOV
2206
                fixed_outputs = old_outputs
×
2207
        if not fixed_outputs:
5✔
2208
            raise CannotBumpFee(_('Could not figure out which outputs to keep'))
5✔
2209

2210
        if coins is None:
5✔
2211
            coins = self.get_spendable_coins(None)
5✔
2212
        # make sure we don't try to spend output from the tx-to-be-replaced:
2213
        coins = [c for c in coins
5✔
2214
                 if c.prevout.txid.hex() not in self.adb.get_conflicting_transactions(tx, include_self=True)]
2215
        for item in coins:
5✔
2216
            self.add_input_info(item)
5✔
2217
        def fee_estimator(size):
5✔
2218
            return self.config.estimate_fee_for_feerate(fee_per_kb=new_fee_rate*1000, size=size)
5✔
2219
        coin_chooser = coinchooser.get_coin_chooser(self.config)
5✔
2220
        try:
5✔
2221
            return coin_chooser.make_tx(
5✔
2222
                coins=coins,
2223
                inputs=old_inputs,
2224
                outputs=fixed_outputs,
2225
                change_addrs=change_addrs,
2226
                fee_estimator_vb=fee_estimator,
2227
                dust_threshold=self.dust_threshold())
2228
        except NotEnoughFunds as e:
5✔
2229
            raise CannotBumpFee(e)
5✔
2230

2231
    def _bump_fee_through_decreasing_change(
5✔
2232
            self,
2233
            *,
2234
            tx: PartialTransaction,
2235
            new_fee_rate: Union[int, Decimal],
2236
    ) -> PartialTransaction:
2237
        """Increase the miner fee of 'tx'.
2238

2239
        - keeps all inputs
2240
        - no new inputs are added
2241
        - change outputs are decreased or removed
2242
        """
2243
        tx = copy.deepcopy(tx)
5✔
2244
        tx.add_info_from_wallet(self)
5✔
2245
        assert tx.get_fee() is not None
5✔
2246
        inputs = tx.inputs()
5✔
2247
        outputs = tx._outputs  # note: we will mutate this directly
5✔
2248

2249
        # use own outputs
2250
        s = list(filter(lambda o: self.is_mine(o.address), outputs))
5✔
2251
        if not s:
5✔
UNCOV
2252
            raise CannotBumpFee('No suitable output')
×
2253

2254
        # prioritize low value outputs, to get rid of dust
2255
        s = sorted(s, key=lambda o: o.value)
5✔
2256
        for o in s:
5✔
2257
            target_fee = int(math.ceil(tx.estimated_size() * new_fee_rate))
5✔
2258
            delta = target_fee - tx.get_fee()
5✔
2259
            if delta <= 0:
5✔
UNCOV
2260
                break
×
2261
            i = outputs.index(o)
5✔
2262
            if o.value - delta >= self.dust_threshold():
5✔
2263
                new_output_value = o.value - delta
5✔
2264
                assert isinstance(new_output_value, int)
5✔
2265
                outputs[i].value = new_output_value
5✔
2266
                delta = 0
5✔
2267
                break
5✔
2268
            else:
2269
                del outputs[i]
5✔
2270
                # note: we mutated the outputs of tx, which will affect
2271
                #       tx.estimated_size() in the next iteration
2272
        else:
2273
            # recompute delta if there was no next iteration
2274
            target_fee = int(math.ceil(tx.estimated_size() * new_fee_rate))
5✔
2275
            delta = target_fee - tx.get_fee()
5✔
2276

2277
        if delta > 0:
5✔
2278
            raise CannotBumpFee(_('Could not find suitable outputs'))
5✔
2279

2280
        return PartialTransaction.from_io(inputs, outputs)
5✔
2281

2282
    def _bump_fee_through_decreasing_payment(
5✔
2283
            self,
2284
            *,
2285
            tx: PartialTransaction,
2286
            new_fee_rate: Union[int, Decimal],
2287
    ) -> PartialTransaction:
2288
        """
2289
        Increase the miner fee of 'tx' by decreasing amount paid.
2290
        This should be used for transactions that pay "Max".
2291

2292
        - keeps all inputs
2293
        - no new inputs are added
2294
        - Each non-ismine output is decreased proportionally to their byte-size.
2295
        """
2296
        tx = copy.deepcopy(tx)
5✔
2297
        tx.add_info_from_wallet(self)
5✔
2298
        assert tx.get_fee() is not None
5✔
2299
        inputs = tx.inputs()
5✔
2300
        outputs = tx.outputs()
5✔
2301

2302
        # select non-ismine outputs
2303
        s = [(idx, out) for (idx, out) in enumerate(outputs)
5✔
2304
             if not self.is_mine(out.address)]
2305
        s = [(idx, out) for (idx, out) in s if self._is_rbf_allowed_to_touch_tx_output(out)]
5✔
2306
        if not s:
5✔
UNCOV
2307
            raise CannotBumpFee("Cannot find payment output")
×
2308

2309
        del_out_idxs = set()
5✔
2310
        tx_size = tx.estimated_size()
5✔
2311
        cur_fee = tx.get_fee()
5✔
2312
        # Main loop. Each iteration decreases value of all selected outputs.
2313
        # The number of iterations is bounded by len(s) as only the final iteration
2314
        # can *not remove* any output.
2315
        for __ in range(len(s) + 1):
5✔
2316
            target_fee = int(math.ceil(tx_size * new_fee_rate))
5✔
2317
            delta_total = target_fee - cur_fee
5✔
2318
            if delta_total <= 0:
5✔
2319
                break
5✔
2320
            out_size_total = sum(Transaction.estimated_output_size_for_script(out.scriptpubkey)
5✔
2321
                                 for (idx, out) in s if idx not in del_out_idxs)
2322
            if out_size_total == 0:  # no outputs left to decrease
5✔
2323
                raise CannotBumpFee(_('Could not find suitable outputs'))
5✔
2324
            for idx, out in s:
5✔
2325
                out_size = Transaction.estimated_output_size_for_script(out.scriptpubkey)
5✔
2326
                delta = int(math.ceil(delta_total * out_size / out_size_total))
5✔
2327
                if out.value - delta >= self.dust_threshold():
5✔
2328
                    new_output_value = out.value - delta
5✔
2329
                    assert isinstance(new_output_value, int)
5✔
2330
                    outputs[idx].value = new_output_value
5✔
2331
                    cur_fee += delta
5✔
2332
                else:  # remove output
2333
                    tx_size -= out_size
5✔
2334
                    cur_fee += out.value
5✔
2335
                    del_out_idxs.add(idx)
5✔
2336
        if delta_total > 0:
5✔
UNCOV
2337
            raise CannotBumpFee(_('Could not find suitable outputs'))
×
2338

2339
        outputs = [out for (idx, out) in enumerate(outputs) if idx not in del_out_idxs]
5✔
2340
        return PartialTransaction.from_io(inputs, outputs)
5✔
2341

2342
    def _is_rbf_allowed_to_touch_tx_output(self, txout: TxOutput) -> bool:
5✔
2343
        # 2fa fee outputs if present, should not be removed or have their value decreased
2344
        if self.is_billing_address(txout.address):
5✔
UNCOV
2345
            return False
×
2346
        # submarine swap funding outputs must not be decreased
2347
        if self.lnworker and self.lnworker.swap_manager.is_lockup_address_for_a_swap(txout.address):
5✔
UNCOV
2348
            return False
×
2349
        return True
5✔
2350

2351
    def can_rbf_tx(self, tx: Transaction, *, is_dscancel: bool = False) -> bool:
5✔
2352
        # do not mutate LN funding txs, as that would change their txid
2353
        if not is_dscancel and self.is_lightning_funding_tx(tx.txid()):
5✔
UNCOV
2354
            return False
×
2355
        return tx.is_rbf_enabled()
5✔
2356

2357
    def cpfp(self, tx: Transaction, fee: int) -> Optional[PartialTransaction]:
5✔
2358
        assert tx
5✔
2359
        txid = tx.txid()
5✔
2360
        for i, o in enumerate(tx.outputs()):
5✔
2361
            address, value = o.address, o.value
5✔
2362
            if self.is_mine(address):
5✔
2363
                break
5✔
2364
        else:
2365
            raise CannotCPFP(_("Could not find suitable output"))
5✔
2366
        coins = self.adb.get_addr_utxo(address)
5✔
2367
        item = coins.get(TxOutpoint.from_str(txid+':%d'%i))
5✔
2368
        if not item:
5✔
UNCOV
2369
            raise CannotCPFP(_("Could not find coins for output"))
×
2370
        inputs = [item]
5✔
2371
        out_address = (self.get_single_change_address_for_new_transaction(allow_reusing_used_change_addrs=False)
5✔
2372
                       or self.get_unused_address()
2373
                       or address)
2374
        output_value = value - fee
5✔
2375
        if output_value < self.dust_threshold():
5✔
UNCOV
2376
            raise CannotCPFP(_("The output value remaining after fee is too low."))
×
2377
        outputs = [PartialTxOutput.from_address_and_value(out_address, output_value)]
5✔
2378
        locktime = get_locktime_for_new_transaction(self.network)
5✔
2379
        tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
5✔
2380
        tx_new.set_rbf(True)
5✔
2381
        tx_new.add_info_from_wallet(self)
5✔
2382
        return tx_new
5✔
2383

2384
    def dscancel(
5✔
2385
            self, *, tx: Transaction, new_fee_rate: Union[int, float, Decimal]
2386
    ) -> PartialTransaction:
2387
        """Double-Spend-Cancel: cancel an unconfirmed tx by double-spending
2388
        its inputs, paying ourselves.
2389
        'new_fee_rate' is the target min rate in sat/vbyte
2390

2391
        note: it is the caller's responsibility to have already called tx.add_info_from_network().
2392
              Without that, all txins must be ismine.
2393
        """
2394
        assert tx
5✔
2395
        if not isinstance(tx, PartialTransaction):
5✔
2396
            tx = PartialTransaction.from_tx(tx)
5✔
2397
        assert isinstance(tx, PartialTransaction)
5✔
2398
        tx.remove_signatures()
5✔
2399

2400
        if not self.can_rbf_tx(tx, is_dscancel=True):
5✔
UNCOV
2401
            raise CannotDoubleSpendTx(_('Transaction is final'))
×
2402
        new_fee_rate = quantize_feerate(new_fee_rate)  # strip excess precision
5✔
2403
        tx.add_info_from_wallet(self)
5✔
2404
        if tx.is_missing_info_from_network():
5✔
UNCOV
2405
            raise Exception("tx missing info from network")
×
2406
        old_tx_size = tx.estimated_size()
5✔
2407
        old_fee = tx.get_fee()
5✔
2408
        assert old_fee is not None
5✔
2409
        old_fee_rate = old_fee / old_tx_size  # sat/vbyte
5✔
2410
        if new_fee_rate <= old_fee_rate:
5✔
UNCOV
2411
            raise CannotDoubleSpendTx(_("The new fee rate needs to be higher than the old fee rate."))
×
2412
        # grab all ismine inputs
2413
        inputs = [txin for txin in tx.inputs()
5✔
2414
                  if self.is_mine(self.adb.get_txin_address(txin))]
2415
        value = sum([txin.value_sats() for txin in inputs])
5✔
2416
        # figure out output address
2417
        old_change_addrs = [o.address for o in tx.outputs() if self.is_mine(o.address)]
5✔
2418
        out_address = (self.get_single_change_address_for_new_transaction(old_change_addrs)
5✔
2419
                       or self.get_receiving_address())
2420
        locktime = get_locktime_for_new_transaction(self.network)
5✔
2421
        outputs = [PartialTxOutput.from_address_and_value(out_address, value)]
5✔
2422
        tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
5✔
2423
        new_tx_size = tx_new.estimated_size()
5✔
2424
        new_fee = max(
5✔
2425
            new_fee_rate * new_tx_size,
2426
            old_fee + self.relayfee() * new_tx_size / Decimal(1000),  # BIP-125 rules 3 and 4
2427
        )
2428
        new_fee = int(math.ceil(new_fee))
5✔
2429
        output_value = value - new_fee
5✔
2430
        if output_value < self.dust_threshold():
5✔
UNCOV
2431
            raise CannotDoubleSpendTx(_("The output value remaining after fee is too low."))
×
2432
        outputs = [PartialTxOutput.from_address_and_value(out_address, value - new_fee)]
5✔
2433
        tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
5✔
2434
        tx_new.set_rbf(True)
5✔
2435
        tx_new.add_info_from_wallet(self)
5✔
2436
        return tx_new
5✔
2437

2438
    def _add_txinout_derivation_info(self, txinout: Union[PartialTxInput, PartialTxOutput],
5✔
2439
                                     address: str, *, only_der_suffix: bool) -> None:
2440
        pass  # implemented by subclasses
5✔
2441

2442
    def _add_input_utxo_info(
5✔
2443
            self,
2444
            txin: PartialTxInput,
2445
            *,
2446
            address: str = None,
2447
    ) -> None:
2448
        # - We prefer to include UTXO (full tx), even for segwit inputs (see #6198).
2449
        # - For witness v0 inputs, we include *both* UTXO and WITNESS_UTXO. UTXO is a strict superset,
2450
        #   so this is redundant, but it is (implied to be) "expected" from bip-0174 (see #8039).
2451
        #   Regardless, this might improve compatibility with some other software.
2452
        # - For witness v1, witness_utxo will be enough though (bip-0341 sighash fixes known prior issues).
2453
        # - We cannot include UTXO if the prev tx is not signed yet (chain of unsigned txs).
2454
        address = address or txin.address
5✔
2455
        # add witness_utxo
2456
        if txin.witness_utxo is None and txin.is_segwit() and address:
5✔
2457
            received, spent = self.adb.get_addr_io(address)
5✔
2458
            item = received.get(txin.prevout.to_str())
5✔
2459
            if item:
5✔
2460
                txin_value = item[2]
5✔
2461
                txin.witness_utxo = TxOutput.from_address_and_value(address, txin_value)
5✔
2462
        # add utxo
2463
        if txin.utxo is None:
5✔
2464
            txin.utxo = self.db.get_transaction(txin.prevout.txid.hex())
5✔
2465
        # Maybe remove witness_utxo. witness_utxo should not be present for non-segwit inputs.
2466
        # If it is present, it might be because another electrum instance added it when sharing the psbt via QR code.
2467
        # If we have the full utxo available, we can remove it without loss of information.
2468
        if txin.witness_utxo and not txin.is_segwit() and txin.utxo:
5✔
2469
            txin.witness_utxo = None
5✔
2470

2471
    def _learn_derivation_path_for_address_from_txinout(self, txinout: Union[PartialTxInput, PartialTxOutput],
5✔
2472
                                                        address: str) -> bool:
2473
        """Tries to learn the derivation path for an address (potentially beyond gap limit)
2474
        using data available in given txin/txout.
2475
        Returns whether the address was found to be is_mine.
2476
        """
2477
        return False  # implemented by subclasses
5✔
2478

2479
    def add_input_info(
5✔
2480
            self,
2481
            txin: TxInput,
2482
            *,
2483
            only_der_suffix: bool = False,
2484
    ) -> None:
2485
        """Populates the txin, using info the wallet already has.
2486
        That is, network requests are *not* done to fetch missing prev txs!
2487
        For that, use txin.add_info_from_network.
2488
        """
2489
        # note: we add input utxos regardless of is_mine
2490
        if txin.utxo is None:
5✔
2491
            txin.utxo = self.db.get_transaction(txin.prevout.txid.hex())
5✔
2492
        if not isinstance(txin, PartialTxInput):
5✔
2493
            return
5✔
2494
        address = self.adb.get_txin_address(txin)
5✔
2495
        self._add_input_utxo_info(txin, address=address)
5✔
2496
        is_mine = self.is_mine(address)
5✔
2497
        if not is_mine:
5✔
2498
            is_mine = self._learn_derivation_path_for_address_from_txinout(txin, address)
5✔
2499
        if not is_mine:
5✔
2500
            return
5✔
2501
        txin.script_descriptor = self.get_script_descriptor_for_address(address)
5✔
2502
        txin.is_mine = True
5✔
2503
        self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix)
5✔
2504
        txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height
5✔
2505

2506
    def has_support_for_slip_19_ownership_proofs(self) -> bool:
5✔
UNCOV
2507
        return False
×
2508

2509
    def add_slip_19_ownership_proofs_to_tx(self, tx: PartialTransaction) -> None:
5✔
UNCOV
2510
        raise NotImplementedError()
×
2511

2512
    def get_script_descriptor_for_address(self, address: str) -> Optional[Descriptor]:
5✔
2513
        if not self.is_mine(address):
5✔
UNCOV
2514
            return None
×
2515
        script_type = self.get_txin_type(address)
5✔
2516
        if script_type in ('address', 'unknown'):
5✔
2517
            return None
5✔
2518
        addr_index = self.get_address_index(address)
5✔
2519
        if addr_index is None:
5✔
UNCOV
2520
            return None
×
2521
        pubkeys = [ks.get_pubkey_provider(addr_index) for ks in self.get_keystores()]
5✔
2522
        if not pubkeys:
5✔
UNCOV
2523
            return None
×
2524
        if script_type == 'p2pk':
5✔
2525
            return descriptor.PKDescriptor(pubkey=pubkeys[0])
×
2526
        elif script_type == 'p2pkh':
5✔
2527
            return descriptor.PKHDescriptor(pubkey=pubkeys[0])
5✔
2528
        elif script_type == 'p2wpkh':
5✔
2529
            return descriptor.WPKHDescriptor(pubkey=pubkeys[0])
5✔
2530
        elif script_type == 'p2wpkh-p2sh':
5✔
2531
            wpkh = descriptor.WPKHDescriptor(pubkey=pubkeys[0])
5✔
2532
            return descriptor.SHDescriptor(subdescriptor=wpkh)
5✔
2533
        elif script_type == 'p2sh':
5✔
2534
            multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)
5✔
2535
            return descriptor.SHDescriptor(subdescriptor=multi)
5✔
2536
        elif script_type == 'p2wsh':
5✔
2537
            multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)
5✔
2538
            return descriptor.WSHDescriptor(subdescriptor=multi)
5✔
2539
        elif script_type == 'p2wsh-p2sh':
5✔
2540
            multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)
5✔
2541
            wsh = descriptor.WSHDescriptor(subdescriptor=multi)
5✔
2542
            return descriptor.SHDescriptor(subdescriptor=wsh)
5✔
2543
        else:
UNCOV
2544
            raise NotImplementedError(f"unexpected {script_type=}")
×
2545

2546
    def can_sign(self, tx: Transaction) -> bool:
5✔
2547
        if not isinstance(tx, PartialTransaction):
5✔
UNCOV
2548
            return False
×
2549
        if tx.is_complete():
5✔
2550
            return False
×
2551
        # add info to inputs if we can; otherwise we might return a false negative:
2552
        tx.add_info_from_wallet(self)
5✔
2553
        for txin in tx.inputs():
5✔
2554
            # note: is_mine check needed to avoid false positives.
2555
            #       just because keystore could sign, txin does not necessarily belong to wallet.
2556
            #       Example: we have p2pkh-like addresses and txin is a multisig that involves our pubkey.
2557
            if not self.is_mine(txin.address):
5✔
2558
                continue
5✔
UNCOV
2559
            for k in self.get_keystores():
×
UNCOV
2560
                if k.can_sign_txin(txin):
×
2561
                    return True
×
2562
        if self.get_swap_by_claim_tx(tx):
5✔
2563
            return True
×
2564
        return False
5✔
2565

2566
    def add_output_info(self, txout: PartialTxOutput, *, only_der_suffix: bool = False) -> None:
5✔
2567
        address = txout.address
5✔
2568
        if not self.is_mine(address):
5✔
2569
            is_mine = self._learn_derivation_path_for_address_from_txinout(txout, address)
5✔
2570
            if not is_mine:
5✔
2571
                return
5✔
2572
        txout.script_descriptor = self.get_script_descriptor_for_address(address)
5✔
2573
        txout.is_mine = True
5✔
2574
        txout.is_change = self.is_change(address)
5✔
2575
        self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix)
5✔
2576

2577
    def sign_transaction(self, tx: Transaction, password, *, ignore_warnings: bool = False) -> Optional[PartialTransaction]:
5✔
2578
        """ returns tx if successful else None """
2579
        if self.is_watching_only():
5✔
2580
            return
5✔
2581
        if not isinstance(tx, PartialTransaction):
5✔
UNCOV
2582
            return
×
2583
        if any(DummyAddress.is_dummy_address(txout.address) for txout in tx.outputs()):
5✔
2584
            raise DummyAddressUsedInTxException("tried to sign tx with dummy address!")
5✔
2585

2586
        # check if signing is dangerous
2587
        sh_danger = self.check_sighash(tx)
5✔
2588
        if sh_danger.needs_reject():
5✔
2589
            raise TransactionDangerousException('Not signing transaction:\n' + sh_danger.get_long_message())
5✔
2590
        if sh_danger.needs_confirm() and not ignore_warnings:
5✔
2591
            raise TransactionPotentiallyDangerousException('Not signing transaction:\n' + sh_danger.get_long_message())
5✔
2592

2593
        # sign with make_witness
2594
        for i, txin in enumerate(tx.inputs()):
5✔
2595
            if hasattr(txin, 'make_witness'):
5✔
UNCOV
2596
                self.logger.info(f'sign_transaction: adding witness using make_witness')
×
UNCOV
2597
                privkey = txin.privkey
×
2598
                sig = tx.sign_txin(i, privkey)
×
2599
                txin.script_sig = b''
×
2600
                txin.witness = txin.make_witness(sig)
×
2601
                assert txin.is_complete()
×
2602

2603
        # add info to a temporary tx copy; including xpubs
2604
        # and full derivation paths as hw keystores might want them
2605
        tmp_tx = copy.deepcopy(tx)
5✔
2606
        tmp_tx.add_info_from_wallet(self, include_xpubs=True)
5✔
2607
        # sign. start with ready keystores.
2608
        # note: ks.ready_to_sign() side-effect: we trigger pairings with potential hw devices.
2609
        #       We only do this once, before the loop, however we could rescan after each iteration,
2610
        #       to see if the user connected/disconnected devices in the meantime.
2611
        for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True):
5✔
2612
            try:
5✔
2613
                if k.can_sign(tmp_tx):
5✔
2614
                    k.sign_transaction(tmp_tx, password)
5✔
UNCOV
2615
            except UserCancelled:
×
UNCOV
2616
                continue
×
2617
        # remove sensitive info; then copy back details from temporary tx
2618
        tmp_tx.remove_xpubs_and_bip32_paths()
5✔
2619
        tx.combine_with_other_psbt(tmp_tx)
5✔
2620
        tx.add_info_from_wallet(self, include_xpubs=False)
5✔
2621
        return tx
5✔
2622

2623
    def try_detecting_internal_addresses_corruption(self) -> None:
5✔
UNCOV
2624
        pass
×
2625

2626
    def check_address_for_corruption(self, addr: str) -> None:
5✔
UNCOV
2627
        pass
×
2628

2629
    def get_unused_addresses(self) -> Sequence[str]:
5✔
2630
        domain = self.get_receiving_addresses()
5✔
2631
        return [addr for addr in domain if not self.adb.is_used(addr) and not self.get_request_by_addr(addr)]
5✔
2632

2633
    @check_returned_address_for_corruption
5✔
2634
    def get_unused_address(self) -> Optional[str]:
5✔
2635
        """Get an unused receiving address, if there is one.
2636
        Note: there might NOT be one available!
2637
        """
2638
        addrs = self.get_unused_addresses()
5✔
2639
        if addrs:
5✔
2640
            return addrs[0]
5✔
2641

2642
    @check_returned_address_for_corruption
5✔
2643
    def get_receiving_address(self) -> str:
5✔
2644
        """Get a receiving address. Guaranteed to always return an address."""
2645
        unused_addr = self.get_unused_address()
5✔
2646
        if unused_addr:
5✔
2647
            return unused_addr
5✔
UNCOV
2648
        domain = self.get_receiving_addresses()
×
UNCOV
2649
        if not domain:
×
2650
            raise Exception("no receiving addresses in wallet?!")
×
2651
        choice = domain[0]
×
2652
        for addr in domain:
×
2653
            if not self.adb.is_used(addr):
×
2654
                if self.get_request_by_addr(addr) is None:
×
2655
                    return addr
×
2656
                else:
2657
                    choice = addr
×
UNCOV
2658
        return choice
×
2659

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

2663
    def import_address(self, address: str) -> str:
5✔
UNCOV
2664
        raise UserFacingException("this wallet cannot import addresses")
×
2665

2666
    def import_addresses(self, addresses: List[str], *,
5✔
2667
                         write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:
UNCOV
2668
        raise UserFacingException("this wallet cannot import addresses")
×
2669

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

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

2676
    def check_expired_status(self, r: BaseInvoice, status):
5✔
2677
        #if r.is_lightning() and r.exp == 0:
2678
        #    status = PR_EXPIRED  # for BOLT-11 invoices, exp==0 means 0 seconds
2679
        if status == PR_UNPAID and r.has_expired():
5✔
2680
            status = PR_EXPIRED
5✔
2681
        return status
5✔
2682

2683
    def get_invoice_status(self, invoice: BaseInvoice):
5✔
2684
        """Returns status of (incoming) request or (outgoing) invoice."""
2685
        # lightning invoices can be paid onchain
2686
        if invoice.is_lightning() and self.lnworker:
5✔
2687
            status = self.lnworker.get_invoice_status(invoice)
5✔
2688
            if status != PR_UNPAID:
5✔
2689
                return self.check_expired_status(invoice, status)
5✔
2690
        paid, conf = self.is_onchain_invoice_paid(invoice)
5✔
2691
        if not paid:
5✔
2692
            if isinstance(invoice, Invoice):
5✔
2693
                if status:=invoice.get_broadcasting_status():
5✔
UNCOV
2694
                    return status
×
2695
            status = PR_UNPAID
5✔
2696
        elif conf == 0:
5✔
2697
            status = PR_UNCONFIRMED
5✔
2698
        else:
2699
            assert conf >= 1, conf
5✔
2700
            status = PR_PAID
5✔
2701
        return self.check_expired_status(invoice, status)
5✔
2702

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

2707
        Called in get_label_for_address and update_invoices_and_reqs_touched_by_tx
2708
        Returns None if the address can be reused (i.e. was paid by lightning or has expired)
2709
        """
2710
        keys = self._requests_addr_to_key.get(addr) or []
5✔
2711
        reqs = [self._receive_requests.get(key) for key in keys]
5✔
2712
        reqs = [req for req in reqs if req]  # filter None
5✔
2713
        if not reqs:
5✔
2714
            return
5✔
2715
        # filter out expired
2716
        reqs = [req for req in reqs if self.get_invoice_status(req) != PR_EXPIRED]
5✔
2717
        # filter out paid-with-lightning
2718
        if self.lnworker:
5✔
2719
            reqs = [req for req in reqs
5✔
2720
                    if not req.is_lightning() or self.lnworker.get_invoice_status(req) == PR_UNPAID]
2721
        if not reqs:
5✔
2722
            return None
5✔
2723
        # note: There typically should not be more than one relevant request for an address.
2724
        #       If there's multiple, return the one created last (see #8113). Consider:
2725
        #       - there is an old expired req1, and a newer unpaid req2, reusing the same addr (and same amount),
2726
        #       - now req2 gets paid. however, get_invoice_status will say both req1 and req2 are PAID. (see #8061)
2727
        #       - as a workaround, we return the request with the larger creation time.
2728
        reqs.sort(key=lambda req: req.get_time())
5✔
2729
        return reqs[-1]
5✔
2730

2731
    def get_request(self, request_id: str) -> Optional[Request]:
5✔
2732
        return self._receive_requests.get(request_id)
5✔
2733

2734
    def get_formatted_request(self, request_id):
5✔
UNCOV
2735
        x = self.get_request(request_id)
×
UNCOV
2736
        if x:
×
2737
            return self.export_request(x)
×
2738

2739
    def export_request(self, x: Request) -> Dict[str, Any]:
5✔
UNCOV
2740
        key = x.get_id()
×
UNCOV
2741
        status = self.get_invoice_status(x)
×
2742
        d = x.as_dict(status)
×
2743
        d['request_id'] = d.pop('id')
×
2744
        if x.is_lightning():
×
2745
            d['rhash'] = x.rhash
×
2746
            d['lightning_invoice'] = self.get_bolt11_invoice(x)
×
2747
            if self.lnworker and status == PR_UNPAID:
×
2748
                d['can_receive'] = self.lnworker.can_receive_invoice(x)
×
2749
        if address := x.get_address():
×
2750
            d['address'] = address
×
2751
            d['URI'] = self.get_request_URI(x)
×
2752
            # if request was paid onchain, add relevant fields
2753
            # note: addr is reused when getting paid on LN! so we check for that.
UNCOV
2754
            _, conf, tx_hashes = self._is_onchain_invoice_paid(x)
×
UNCOV
2755
            if not x.is_lightning() or not self.lnworker or self.lnworker.get_invoice_status(x) != PR_PAID:
×
2756
                if conf is not None:
×
2757
                    d['confirmations'] = conf
×
2758
                d['tx_hashes'] = tx_hashes
×
2759
        run_hook('wallet_export_request', d, key)
×
2760
        return d
×
2761

2762
    def export_invoice(self, x: Invoice) -> Dict[str, Any]:
5✔
UNCOV
2763
        key = x.get_id()
×
UNCOV
2764
        status = self.get_invoice_status(x)
×
2765
        d = x.as_dict(status)
×
2766
        d['invoice_id'] = d.pop('id')
×
2767
        if x.is_lightning():
×
2768
            d['lightning_invoice'] = x.lightning_invoice
×
2769
            if self.lnworker and status == PR_UNPAID:
×
2770
                d['can_pay'] = self.lnworker.can_pay_invoice(x)
×
2771
        else:
2772
            amount_sat = x.get_amount_sat()
×
UNCOV
2773
            assert isinstance(amount_sat, (int, str, type(None)))
×
2774
            d['outputs'] = [y.to_legacy_tuple() for y in x.get_outputs()]
×
2775
            if x.bip70:
×
2776
                d['bip70'] = x.bip70
×
2777
        return d
×
2778

2779
    def get_invoices_and_requests_touched_by_tx(self, tx):
5✔
2780
        request_keys = set()
5✔
2781
        invoice_keys = set()
5✔
2782
        with self.lock, self.transaction_lock:
5✔
2783
            for txo in tx.outputs():
5✔
2784
                addr = txo.address
5✔
2785
                if request:=self.get_request_by_addr(addr):
5✔
2786
                    request_keys.add(request.get_id())
5✔
2787
                for invoice_key in self._invoices_from_scriptpubkey_map.get(txo.scriptpubkey, set()):
5✔
UNCOV
2788
                    invoice_keys.add(invoice_key)
×
2789
        return request_keys, invoice_keys
5✔
2790

2791
    def _update_invoices_and_reqs_touched_by_tx(self, tx_hash: str) -> None:
5✔
2792
        # FIXME in some cases if tx2 replaces unconfirmed tx1 in the mempool, we are not called.
2793
        #       For a given receive request, if tx1 touches it but tx2 does not, then
2794
        #       we were called when tx1 was added, but we will not get called when tx2 replaces tx1.
2795
        tx = self.db.get_transaction(tx_hash)
5✔
2796
        if tx is None:
5✔
UNCOV
2797
            return
×
2798
        request_keys, invoice_keys = self.get_invoices_and_requests_touched_by_tx(tx)
5✔
2799
        for key in request_keys:
5✔
2800
            request = self.get_request(key)
5✔
2801
            if not request:
5✔
UNCOV
2802
                continue
×
2803
            status = self.get_invoice_status(request)
5✔
2804
            util.trigger_callback('request_status', self, request.get_id(), status)
5✔
2805
        self._update_onchain_invoice_paid_detection(invoice_keys)
5✔
2806

2807
    def set_broadcasting(self, tx: Transaction, *, broadcasting_status: Optional[int]):
5✔
UNCOV
2808
        request_keys, invoice_keys = self.get_invoices_and_requests_touched_by_tx(tx)
×
UNCOV
2809
        for key in invoice_keys:
×
2810
            invoice = self._invoices.get(key)
×
2811
            if not invoice:
×
2812
                continue
×
2813
            invoice._broadcasting_status = broadcasting_status
×
2814
            status = self.get_invoice_status(invoice)
×
2815
            util.trigger_callback('invoice_status', self, key, status)
×
2816

2817
    def get_bolt11_invoice(self, req: Request) -> str:
5✔
UNCOV
2818
        if not self.lnworker:
×
UNCOV
2819
            return ''
×
2820
        if (payment_hash := req.payment_hash) is None:  # e.g. req might have been generated before enabling LN
×
2821
            return ''
×
2822
        amount_msat = req.get_amount_msat() or None
×
2823
        assert (amount_msat is None or amount_msat > 0), amount_msat
×
2824
        lnaddr, invoice = self.lnworker.get_bolt11_invoice(
×
2825
            payment_hash=payment_hash,
2826
            amount_msat=amount_msat,
2827
            message=req.message,
2828
            expiry=req.exp,
2829
            fallback_address=None)
UNCOV
2830
        return invoice
×
2831

2832
    def create_request(self, amount_sat: Optional[int], message: Optional[str], exp_delay: Optional[int], address: Optional[str]):
5✔
2833
        """ will create a lightning request if address is None """
2834
        # for receiving
2835
        amount_sat = amount_sat or 0
5✔
2836
        assert isinstance(amount_sat, int), f"{amount_sat!r}"
5✔
2837
        amount_msat = None if not amount_sat else amount_sat * 1000  # amount_sat in [None, 0] implies undefined.
5✔
2838
        message = message or ''
5✔
2839
        address = address or None  # converts "" to None
5✔
2840
        exp_delay = exp_delay or 0
5✔
2841
        timestamp = int(Request._get_cur_time())
5✔
2842
        if address is None:
5✔
2843
            assert self.has_lightning()
5✔
2844
            payment_hash = self.lnworker.create_payment_info(amount_msat=amount_msat, write_to_disk=False)
5✔
2845
        else:
2846
            payment_hash = None
5✔
2847
        outputs = [PartialTxOutput.from_address_and_value(address, amount_sat)] if address else []
5✔
2848
        height = self.adb.get_local_height()
5✔
2849
        req = Request(
5✔
2850
            outputs=outputs,
2851
            message=message,
2852
            time=timestamp,
2853
            amount_msat=amount_msat,
2854
            exp=exp_delay,
2855
            height=height,
2856
            bip70=None,
2857
            payment_hash=payment_hash,
2858
        )
2859
        key = self.add_payment_request(req)
5✔
2860
        return key
5✔
2861

2862
    def add_payment_request(self, req: Request, *, write_to_disk: bool = True):
5✔
2863
        request_id = req.get_id()
5✔
2864
        self._receive_requests[request_id] = req
5✔
2865
        if addr:=req.get_address():
5✔
2866
            self._requests_addr_to_key[addr].add(request_id)
5✔
2867
        if write_to_disk:
5✔
2868
            self.save_db()
5✔
2869
        return request_id
5✔
2870

2871
    def delete_request(self, request_id, *, write_to_disk: bool = True):
5✔
2872
        """ lightning or on-chain """
UNCOV
2873
        req = self.get_request(request_id)
×
UNCOV
2874
        if req is None:
×
2875
            return
×
2876
        self._receive_requests.pop(request_id, None)
×
2877
        if addr:=req.get_address():
×
2878
            self._requests_addr_to_key[addr].discard(request_id)
×
2879
        if req.is_lightning() and self.lnworker:
×
2880
            self.lnworker.delete_payment_info(req.rhash)
×
2881
        if write_to_disk:
×
2882
            self.save_db()
×
2883

2884
    def delete_invoice(self, invoice_id, *, write_to_disk: bool = True):
5✔
2885
        """ lightning or on-chain """
UNCOV
2886
        inv = self._invoices.pop(invoice_id, None)
×
UNCOV
2887
        if inv is None:
×
2888
            return
×
2889
        if inv.is_lightning() and self.lnworker:
×
2890
            self.lnworker.delete_payment_info(inv.rhash)
×
2891
        if write_to_disk:
×
2892
            self.save_db()
×
2893

2894
    def get_sorted_requests(self) -> List[Request]:
5✔
2895
        """ sorted by timestamp """
UNCOV
2896
        out = [self.get_request(x) for x in self._receive_requests.keys()]
×
UNCOV
2897
        out = [x for x in out if x is not None]
×
2898
        out.sort(key=lambda x: x.time)
×
2899
        return out
×
2900

2901
    def get_unpaid_requests(self) -> List[Request]:
5✔
UNCOV
2902
        out = [x for x in self._receive_requests.values() if self.get_invoice_status(x) != PR_PAID]
×
UNCOV
2903
        out.sort(key=lambda x: x.time)
×
2904
        return out
×
2905

2906
    def delete_expired_requests(self):
5✔
UNCOV
2907
        keys = [k for k, v in self._receive_requests.items() if self.get_invoice_status(v) == PR_EXPIRED]
×
UNCOV
2908
        self.delete_requests(keys)
×
2909
        return keys
×
2910

2911
    def delete_requests(self, keys):
5✔
UNCOV
2912
        for key in keys:
×
UNCOV
2913
            self.delete_request(key, write_to_disk=False)
×
2914
        if keys:
×
2915
            self.save_db()
×
2916

2917
    @abstractmethod
5✔
2918
    def get_fingerprint(self) -> str:
5✔
2919
        """Returns a string that can be used to identify this wallet.
2920
        Used e.g. by Labels plugin, and LN channel backups.
2921
        Returns empty string "" for wallets that don't have an ID.
2922
        """
UNCOV
2923
        pass
×
2924

2925
    def can_import_privkey(self):
5✔
UNCOV
2926
        return False
×
2927

2928
    def can_import_address(self):
5✔
UNCOV
2929
        return False
×
2930

2931
    def can_delete_address(self):
5✔
UNCOV
2932
        return False
×
2933

2934
    def has_password(self) -> bool:
5✔
2935
        return self.has_keystore_encryption() or self.has_storage_encryption()
5✔
2936

2937
    def can_have_keystore_encryption(self):
5✔
2938
        return self.keystore and self.keystore.may_have_password()
5✔
2939

2940
    def get_available_storage_encryption_version(self) -> StorageEncryptionVersion:
5✔
2941
        """Returns the type of storage encryption offered to the user.
2942

2943
        A wallet file (storage) is either encrypted with this version
2944
        or is stored in plaintext.
2945
        """
2946
        if isinstance(self.keystore, Hardware_KeyStore):
5✔
UNCOV
2947
            return StorageEncryptionVersion.XPUB_PASSWORD
×
2948
        else:
2949
            return StorageEncryptionVersion.USER_PASSWORD
5✔
2950

2951
    def has_keystore_encryption(self) -> bool:
5✔
2952
        """Returns whether encryption is enabled for the keystore.
2953

2954
        If True, e.g. signing a transaction will require a password.
2955
        """
2956
        if self.can_have_keystore_encryption():
5✔
2957
            return bool(self.db.get('use_encryption', False))
5✔
2958
        return False
5✔
2959

2960
    def has_storage_encryption(self) -> bool:
5✔
2961
        """Returns whether encryption is enabled for the wallet file on disk."""
2962
        return bool(self.storage) and self.storage.is_encrypted()
5✔
2963

2964
    @classmethod
5✔
2965
    def may_have_password(cls):
5✔
UNCOV
2966
        return True
×
2967

2968
    def check_password(self, password: Optional[str]) -> None:
5✔
2969
        """Raises an InvalidPassword exception on invalid password"""
2970
        if not self.has_password():
5✔
2971
            if password is not None:
5✔
2972
                raise InvalidPassword("password given but wallet has no password")
5✔
2973
            return
5✔
2974
        if self.has_keystore_encryption():
5✔
2975
            self.keystore.check_password(password)
5✔
2976
        if self.has_storage_encryption():
5✔
2977
            self.storage.check_password(password)
5✔
2978

2979
    def update_password(self, old_pw, new_pw, *, encrypt_storage: bool = True):
5✔
2980
        if old_pw is None and self.has_password():
5✔
UNCOV
2981
            raise InvalidPassword()
×
2982
        self.check_password(old_pw)
5✔
2983
        if self.storage:
5✔
2984
            if encrypt_storage:
5✔
2985
                enc_version = self.get_available_storage_encryption_version()
5✔
2986
            else:
2987
                enc_version = StorageEncryptionVersion.PLAINTEXT
5✔
2988
            self.storage.set_password(new_pw, enc_version)
5✔
2989
        # make sure next storage.write() saves changes
2990
        self.db.set_modified(True)
5✔
2991

2992
        # note: Encrypting storage with a hw device is currently only
2993
        #       allowed for non-multisig wallets. Further,
2994
        #       Hardware_KeyStore.may_have_password() == False.
2995
        #       If these were not the case,
2996
        #       extra care would need to be taken when encrypting keystores.
2997
        self._update_password_for_keystore(old_pw, new_pw)
5✔
2998
        encrypt_keystore = self.can_have_keystore_encryption()
5✔
2999
        self.db.set_keystore_encryption(bool(new_pw) and encrypt_keystore)
5✔
3000
        # save changes. force full rewrite to rm remnants of old password
3001
        if self.storage and self.storage.file_exists():
5✔
3002
            self.db.write_and_force_consolidation()
5✔
3003
        # if wallet was previously unlocked, update password in memory
3004
        if self.requires_unlock():
5✔
UNCOV
3005
            self.unlock(new_pw)
×
3006

3007
    @abstractmethod
5✔
3008
    def _update_password_for_keystore(self, old_pw: Optional[str], new_pw: Optional[str]) -> None:
5✔
UNCOV
3009
        pass
×
3010

3011
    def sign_message(self, address: str, message: str, password) -> bytes:
5✔
UNCOV
3012
        index = self.get_address_index(address)
×
UNCOV
3013
        script_type = self.get_txin_type(address)
×
3014
        assert script_type != "address"
×
3015
        return self.keystore.sign_message(index, message, password, script_type=script_type)
×
3016

3017
    def decrypt_message(self, pubkey: str, message, password) -> bytes:
5✔
UNCOV
3018
        addr = self.pubkeys_to_address([pubkey])
×
UNCOV
3019
        index = self.get_address_index(addr)
×
3020
        return self.keystore.decrypt_message(index, message, password)
×
3021

3022
    @abstractmethod
5✔
3023
    def pubkeys_to_address(self, pubkeys: Sequence[str]) -> Optional[str]:
5✔
UNCOV
3024
        pass
×
3025

3026
    def price_at_timestamp(self, txid, price_func):
5✔
3027
        """Returns fiat price of bitcoin at the time tx got confirmed."""
3028
        timestamp = self.adb.get_tx_height(txid).timestamp
5✔
3029
        return price_func(timestamp if timestamp else time.time())
5✔
3030

3031
    def average_price(self, txid, price_func, ccy) -> Decimal:
5✔
3032
        """ Average acquisition price of the inputs of a transaction """
UNCOV
3033
        input_value = 0
×
UNCOV
3034
        total_price = 0
×
3035
        txi_addresses = self.db.get_txi_addresses(txid)
×
3036
        if not txi_addresses:
×
3037
            return Decimal('NaN')
×
3038
        for addr in txi_addresses:
×
3039
            d = self.db.get_txi_addr(txid, addr)
×
3040
            for ser, v in d:
×
3041
                input_value += v
×
3042
                total_price += self.coin_price(ser.split(':')[0], price_func, ccy, v)
×
3043
        return total_price / (input_value/Decimal(COIN))
×
3044

3045
    def clear_coin_price_cache(self):
5✔
UNCOV
3046
        self._coin_price_cache = {}
×
3047

3048
    def coin_price(self, txid, price_func, ccy, txin_value) -> Decimal:
5✔
3049
        """
3050
        Acquisition price of a coin.
3051
        This assumes that either all inputs are mine, or no input is mine.
3052
        """
UNCOV
3053
        if txin_value is None:
×
UNCOV
3054
            return Decimal('NaN')
×
3055
        cache_key = "{}:{}:{}".format(str(txid), str(ccy), str(txin_value))
×
3056
        result = self._coin_price_cache.get(cache_key, None)
×
3057
        if result is not None:
×
3058
            return result
×
3059
        if self.db.get_txi_addresses(txid):
×
3060
            result = self.average_price(txid, price_func, ccy) * txin_value/Decimal(COIN)
×
3061
            self._coin_price_cache[cache_key] = result
×
3062
            return result
×
3063
        else:
3064
            fiat_value = self.get_fiat_value(txid, ccy)
×
UNCOV
3065
            if fiat_value is not None:
×
3066
                return fiat_value
×
3067
            else:
3068
                p = self.price_at_timestamp(txid, price_func)
×
UNCOV
3069
                return p * txin_value/Decimal(COIN)
×
3070

3071
    def is_billing_address(self, addr):
5✔
3072
        # overridden for TrustedCoin wallets
3073
        return False
5✔
3074

3075
    @abstractmethod
5✔
3076
    def is_watching_only(self) -> bool:
5✔
UNCOV
3077
        pass
×
3078

3079
    def get_keystore(self) -> Optional[KeyStore]:
5✔
UNCOV
3080
        return self.keystore
×
3081

3082
    def get_keystores(self) -> Sequence[KeyStore]:
5✔
3083
        return [self.keystore] if self.keystore else []
5✔
3084

3085
    @abstractmethod
5✔
3086
    def save_keystore(self):
5✔
UNCOV
3087
        pass
×
3088

3089
    @abstractmethod
5✔
3090
    def has_seed(self) -> bool:
5✔
UNCOV
3091
        pass
×
3092

3093
    def get_seed_type(self) -> Optional[str]:
5✔
UNCOV
3094
        return None
×
3095

3096
    @abstractmethod
5✔
3097
    def get_all_known_addresses_beyond_gap_limit(self) -> Set[str]:
5✔
UNCOV
3098
        pass
×
3099

3100
    def create_transaction(
5✔
3101
        self,
3102
        outputs,
3103
        *,
3104
        fee=None,
3105
        feerate=None,
3106
        change_addr=None,
3107
        domain_addr=None,
3108
        domain_coins=None,
3109
        sign=True,
3110
        rbf=True,
3111
        password=None,
3112
        locktime=None,
3113
        tx_version: Optional[int] = None,
3114
        base_tx: Optional[PartialTransaction] = None,
3115
        inputs: Optional[List[PartialTxInput]] = None,
3116
        send_change_to_lightning: Optional[bool] = None,
3117
        nonlocal_only: bool = False,
3118
        BIP69_sort: bool = True,
3119
    ) -> PartialTransaction:
3120
        """Helper function for make_unsigned_transaction."""
3121
        if fee is not None and feerate is not None:
5✔
UNCOV
3122
            raise UserFacingException("Cannot specify both 'fee' and 'feerate' at the same time!")
×
3123
        coins = self.get_spendable_coins(domain_addr, nonlocal_only=nonlocal_only)
5✔
3124
        if domain_coins is not None:
5✔
UNCOV
3125
            coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)]
×
3126
        if feerate is not None:
5✔
3127
            fee_per_kb = 1000 * Decimal(feerate)
5✔
3128
            fee_estimator = partial(SimpleConfig.estimate_fee_for_feerate, fee_per_kb)
5✔
3129
        else:
3130
            fee_estimator = fee
5✔
3131
        tx = self.make_unsigned_transaction(
5✔
3132
            coins=coins,
3133
            inputs=inputs,
3134
            outputs=outputs,
3135
            fee=fee_estimator,
3136
            change_addr=change_addr,
3137
            base_tx=base_tx,
3138
            send_change_to_lightning=send_change_to_lightning,
3139
            rbf=rbf,
3140
            BIP69_sort=BIP69_sort,
3141
        )
3142
        if locktime is not None:
5✔
3143
            tx.locktime = locktime
5✔
3144
        if tx_version is not None:
5✔
3145
            tx.version = tx_version
5✔
3146
        if sign:
5✔
3147
            self.sign_transaction(tx, password)
5✔
3148
        return tx
5✔
3149

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

3154
        Note:
3155
            - legacy sighash does not commit to any input amounts
3156
            - BIP-0143 sighash only commits to the *corresponding* input amount
3157
            - BIP-taproot sighash commits to *all* input amounts
3158
        """
3159
        assert isinstance(tx, PartialTransaction)
5✔
3160
        rl = TxSighashRiskLevel
5✔
3161
        short_message = _("Warning") + ": " + _("The fee could not be verified!")
5✔
3162
        # check that all inputs use SIGHASH_ALL
3163
        if not all(txin.sighash in (None, Sighash.ALL) for txin in tx.inputs()):
5✔
3164
            messages = [(_("Warning") + ": "
5✔
3165
                         + _("Some inputs use non-default sighash flags, which might affect the fee."))]
3166
            return TxSighashDanger(risk_level=rl.FEE_WARNING_NEEDCONFIRM, short_message=short_message, messages=messages)
5✔
3167
        # if we have all full previous txs, we *know* all the input amounts -> fine
3168
        if all([txin.utxo for txin in tx.inputs()]):
5✔
3169
            return TxSighashDanger(risk_level=rl.SAFE)
5✔
3170
        # a single segwit input -> fine
3171
        if len(tx.inputs()) == 1 and tx.inputs()[0].is_segwit() and tx.inputs()[0].witness_utxo:
5✔
3172
            return TxSighashDanger(risk_level=rl.SAFE)
5✔
3173
        # coinjoin or similar
3174
        if any([not self.is_mine(txin.address) for txin in tx.inputs()]):
5✔
UNCOV
3175
            messages = [(_("Warning") + ": "
×
3176
                         + _("The input amounts could not be verified as the previous transactions are missing.\n"
3177
                             "The amount of money being spent CANNOT be verified."))]
UNCOV
3178
            return TxSighashDanger(risk_level=rl.FEE_WARNING_NEEDCONFIRM, short_message=short_message, messages=messages)
×
3179
        # some inputs are legacy
3180
        if any([not txin.is_segwit() for txin in tx.inputs()]):
5✔
3181
            messages = [(_("Warning") + ": "
5✔
3182
                         + _("The fee could not be verified. Signing non-segwit inputs is risky:\n"
3183
                             "if this transaction was maliciously modified before you sign,\n"
3184
                             "you might end up paying a higher mining fee than displayed."))]
3185
            return TxSighashDanger(risk_level=rl.FEE_WARNING_NEEDCONFIRM, short_message=short_message, messages=messages)
5✔
3186
        # all inputs are segwit
3187
        # https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-August/014843.html
UNCOV
3188
        messages = [(_("Warning") + ": "
×
3189
                     + _("If you received this transaction from an untrusted device, "
3190
                         "do not accept to sign it more than once,\n"
3191
                         "otherwise you could end up paying a different fee."))]
UNCOV
3192
        return TxSighashDanger(risk_level=rl.FEE_WARNING_SKIPCONFIRM, short_message=short_message, messages=messages)
×
3193

3194
    def check_sighash(self, tx: 'PartialTransaction') -> TxSighashDanger:
5✔
3195
        """Checks the Sighash for my inputs and considers if the tx is safe to sign."""
3196
        assert isinstance(tx, PartialTransaction)
5✔
3197
        rl = TxSighashRiskLevel
5✔
3198
        hintmap = {
5✔
3199
            0:                    (rl.SAFE,           None),
3200
            Sighash.NONE:         (rl.INSANE_SIGHASH, _('Input {} is marked SIGHASH_NONE.')),
3201
            Sighash.SINGLE:       (rl.WEIRD_SIGHASH,  _('Input {} is marked SIGHASH_SINGLE.')),
3202
            Sighash.ALL:          (rl.SAFE,           None),
3203
            Sighash.ANYONECANPAY: (rl.WEIRD_SIGHASH,  _('Input {} is marked SIGHASH_ANYONECANPAY.')),
3204
        }
3205
        sighash_danger = TxSighashDanger()
5✔
3206
        for txin_idx, txin in enumerate(tx.inputs()):
5✔
3207
            if txin.sighash in (None, Sighash.ALL):
5✔
3208
                continue  # None will get converted to Sighash.ALL, so these values are safe
5✔
3209
            # found interesting sighash flag
3210
            addr = self.adb.get_txin_address(txin)
5✔
3211
            if self.is_mine(addr):
5✔
3212
                sh_base = txin.sighash & (Sighash.ANYONECANPAY ^ 0xff)
5✔
3213
                sh_acp = txin.sighash & Sighash.ANYONECANPAY
5✔
3214
                for sh in [sh_base, sh_acp]:
5✔
3215
                    if msg := hintmap[sh][1]:
5✔
3216
                        risk_level = hintmap[sh][0]
5✔
3217
                        header = _('Fatal') if TxSighashDanger(risk_level=risk_level).needs_reject() else _('Warning')
5✔
3218
                        shd = TxSighashDanger(
5✔
3219
                            risk_level=risk_level,
3220
                            short_message=_('Danger! This transaction uses non-default sighash flags!'),
3221
                            messages=[f"{header}: {msg.format(txin_idx)}"],
3222
                        )
3223
                        sighash_danger = sighash_danger.combine(shd)
5✔
3224
        if sighash_danger.needs_reject():  # no point for further tests
5✔
3225
            return sighash_danger
5✔
3226
        # if we show any fee to the user, check now how reliable that is:
3227
        if self.get_wallet_delta(tx).fee is not None:
5✔
3228
            shd = self._check_risk_of_burning_coins_as_fees(tx)
5✔
3229
            sighash_danger = sighash_danger.combine(shd)
5✔
3230
        return sighash_danger
5✔
3231

3232
    def get_tx_fee_warning(
5✔
3233
            self, *,
3234
            invoice_amt: int,
3235
            tx_size: int,
3236
            fee: int) -> Optional[Tuple[bool, str, str]]:
3237

UNCOV
3238
        assert invoice_amt >= 0, f"{invoice_amt=!r} must be non-negative satoshis"
×
UNCOV
3239
        assert fee >= 0, f"{fee=!r} must be non-negative satoshis"
×
3240
        feerate = Decimal(fee) / tx_size  # sat/byte
×
3241
        fee_ratio = Decimal(fee) / invoice_amt if invoice_amt else 0
×
3242
        long_warning = None
×
3243
        short_warning = None
×
3244
        allow_send = True
×
3245
        if feerate < self.relayfee() / 1000:
×
3246
            long_warning = ' '.join([
×
3247
                _("This transaction requires a higher fee, or it will not be propagated by your current server."),
3248
                _("Try to raise your transaction fee, or use a server with a lower relay fee.")
3249
            ])
UNCOV
3250
            short_warning = _("below relay fee") + "!"
×
UNCOV
3251
            allow_send = False
×
3252
        elif fee_ratio >= FEE_RATIO_HIGH_WARNING:
×
3253
            long_warning = ' '.join([
×
3254
                _("The fee for this transaction seems unusually high."),
3255
                _("({}% of amount)").format(f'{fee_ratio*100:.2f}')
3256
            ])
UNCOV
3257
            short_warning = _("high fee ratio") + "!"
×
UNCOV
3258
        elif feerate > FEERATE_WARNING_HIGH_FEE / 1000:
×
3259
            long_warning = ' '.join([
×
3260
                _("The fee for this transaction seems unusually high."),
3261
                _("(feerate: {})").format(self.config.format_fee_rate(1000 * feerate))
3262
            ])
UNCOV
3263
            short_warning = _("high fee rate") + "!"
×
UNCOV
3264
        if long_warning is None:
×
3265
            return None
×
3266
        else:
3267
            return allow_send, long_warning, short_warning
×
3268

3269
    def get_help_texts_for_receive_request(self, req: Request) -> ReceiveRequestHelp:
5✔
UNCOV
3270
        key = req.get_id()
×
UNCOV
3271
        addr = req.get_address() or ''
×
3272
        amount_sat = req.get_amount_sat() or 0
×
3273
        address_help = ''
×
3274
        URI_help = ''
×
3275
        ln_help = ''
×
3276
        address_is_error = False
×
3277
        URI_is_error = False
×
3278
        ln_is_error = False
×
3279
        ln_swap_suggestion = None
×
3280
        ln_rebalance_suggestion = None
×
3281
        URI = self.get_request_URI(req) or ''
×
3282
        lightning_has_channels = (
×
3283
            self.lnworker and len([chan for chan in self.lnworker.channels.values() if chan.is_open()]) > 0
3284
        )
UNCOV
3285
        lightning_online = self.lnworker and self.lnworker.num_peers() > 0
×
UNCOV
3286
        can_receive_lightning = self.lnworker and amount_sat <= self.lnworker.num_sats_can_receive()
×
3287
        status = self.get_invoice_status(req)
×
3288

3289
        if status == PR_EXPIRED:
×
UNCOV
3290
            address_help = URI_help = ln_help = _('This request has expired')
×
3291

3292
        is_amt_too_small_for_onchain = amount_sat and amount_sat < self.dust_threshold()
×
UNCOV
3293
        if not addr:
×
3294
            address_is_error = True
×
3295
            address_help = _('This request cannot be paid on-chain')
×
3296
            if is_amt_too_small_for_onchain:
×
3297
                address_help = _('Amount too small to be received onchain')
×
3298
        if not URI:
×
3299
            URI_is_error = True
×
3300
            URI_help = _('This request cannot be paid on-chain')
×
3301
            if is_amt_too_small_for_onchain:
×
3302
                URI_help = _('Amount too small to be received onchain')
×
3303
        if not req.is_lightning():
×
3304
            ln_is_error = True
×
3305
            ln_help = _('This request does not have a Lightning invoice.')
×
3306

3307
        if status == PR_UNPAID:
×
UNCOV
3308
            if self.adb.is_used(addr):
×
3309
                address_help = URI_help = (_("This address has already been used. "
×
3310
                                             "For better privacy, do not reuse it for new payments."))
3311
            if req.is_lightning():
×
UNCOV
3312
                if not lightning_has_channels:
×
3313
                    ln_is_error = True
×
3314
                    ln_help = _("You must have an open Lightning channel to receive payments.")
×
3315
                elif not lightning_online:
×
3316
                    ln_is_error = True
×
3317
                    ln_help = _('You must be online to receive Lightning payments.')
×
3318
                elif not can_receive_lightning:
×
3319
                    ln_is_error = True
×
3320
                    ln_rebalance_suggestion = self.lnworker.suggest_rebalance_to_receive(amount_sat)
×
3321
                    ln_swap_suggestion = self.lnworker.suggest_swap_to_receive(amount_sat)
×
3322
                    ln_help = _('You do not have the capacity to receive this amount with Lightning.')
×
3323
                    if bool(ln_rebalance_suggestion):
×
3324
                        ln_help += '\n\n' + _('You may have that capacity if you rebalance your channels.')
×
3325
                    elif bool(ln_swap_suggestion):
×
3326
                        ln_help += '\n\n' + _('You may have that capacity if you swap some of your funds.')
×
3327
                # for URI that has LN part but no onchain part, copy error:
3328
                if not addr and ln_is_error:
×
UNCOV
3329
                    URI_is_error = ln_is_error
×
3330
                    URI_help = ln_help
×
3331
        return ReceiveRequestHelp(
×
3332
            address_help=address_help,
3333
            URI_help=URI_help,
3334
            ln_help=ln_help,
3335
            address_is_error=address_is_error,
3336
            URI_is_error=URI_is_error,
3337
            ln_is_error=ln_is_error,
3338
            ln_rebalance_suggestion=ln_rebalance_suggestion,
3339
            ln_swap_suggestion=ln_swap_suggestion,
3340
        )
3341

3342

3343
    def synchronize(self) -> int:
5✔
3344
        """Returns the number of new addresses we generated."""
3345
        return 0
5✔
3346

3347
    def unlock(self, password):
5✔
UNCOV
3348
        self.logger.info(f'unlocking wallet')
×
UNCOV
3349
        self.check_password(password)
×
3350
        self._password_in_memory = password
×
3351

3352
    def get_unlocked_password(self):
5✔
UNCOV
3353
        return self._password_in_memory
×
3354

3355
    def get_text_not_enough_funds_mentioning_frozen(self) -> str:
5✔
UNCOV
3356
        text = _('Not enough funds')
×
UNCOV
3357
        frozen_str = self.get_frozen_balance_str()
×
3358
        if frozen_str:
×
3359
            text += ' ' + _('({} are frozen)').format(frozen_str)
×
3360
        return text
×
3361

3362
    def get_frozen_balance_str(self) -> Optional[str]:
5✔
UNCOV
3363
        frozen_bal = sum(self.get_frozen_balance())
×
UNCOV
3364
        if not frozen_bal:
×
3365
            return None
×
3366
        return self.config.format_amount_and_units(frozen_bal)
×
3367

3368

3369
class Simple_Wallet(Abstract_Wallet):
5✔
3370
    # wallet with a single keystore
3371

3372
    def is_watching_only(self):
5✔
3373
        return self.keystore.is_watching_only()
5✔
3374

3375
    def _update_password_for_keystore(self, old_pw, new_pw):
5✔
3376
        if self.keystore and self.keystore.may_have_password():
5✔
3377
            self.keystore.update_password(old_pw, new_pw)
5✔
3378
            self.save_keystore()
5✔
3379

3380
    def save_keystore(self):
5✔
3381
        self.db.put('keystore', self.keystore.dump())
5✔
3382

3383
    @abstractmethod
5✔
3384
    def get_public_key(self, address: str) -> Optional[str]:
5✔
UNCOV
3385
        pass
×
3386

3387
    def get_public_keys(self, address: str) -> Sequence[str]:
5✔
UNCOV
3388
        pk = self.get_public_key(address)
×
UNCOV
3389
        return [pk] if pk else []
×
3390

3391

3392
class Imported_Wallet(Simple_Wallet):
5✔
3393
    # wallet made of imported addresses
3394

3395
    wallet_type = 'imported'
5✔
3396
    txin_type = 'address'
5✔
3397

3398
    def __init__(self, db, *, config):
5✔
3399
        Abstract_Wallet.__init__(self, db, config=config)
5✔
3400
        self.use_change = db.get('use_change', False)
5✔
3401

3402
    def is_watching_only(self):
5✔
3403
        return self.keystore is None
5✔
3404

3405
    def can_import_privkey(self):
5✔
3406
        return bool(self.keystore)
5✔
3407

3408
    def load_keystore(self):
5✔
3409
        self.keystore = load_keystore(self.db, 'keystore') if self.db.get('keystore') else None
5✔
3410

3411
    def save_keystore(self):
5✔
3412
        self.db.put('keystore', self.keystore.dump())
5✔
3413

3414
    def can_import_address(self):
5✔
UNCOV
3415
        return self.is_watching_only()
×
3416

3417
    def can_delete_address(self):
5✔
UNCOV
3418
        return True
×
3419

3420
    def has_seed(self):
5✔
3421
        return False
5✔
3422

3423
    def is_deterministic(self):
5✔
UNCOV
3424
        return False
×
3425

3426
    def is_change(self, address):
5✔
3427
        return False
5✔
3428

3429
    def get_all_known_addresses_beyond_gap_limit(self) -> Set[str]:
5✔
UNCOV
3430
        return set()
×
3431

3432
    def get_fingerprint(self):
5✔
UNCOV
3433
        return ''
×
3434

3435
    def get_addresses(self):
5✔
3436
        # note: overridden so that the history can be cleared
3437
        return self.db.get_imported_addresses()
5✔
3438

3439
    def get_receiving_addresses(self, **kwargs):
5✔
3440
        return self.get_addresses()
5✔
3441

3442
    def get_change_addresses(self, **kwargs):
5✔
3443
        return self.get_addresses()
5✔
3444

3445
    def import_addresses(self, addresses: List[str], *,
5✔
3446
                         write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:
3447
        good_addr = []  # type: List[str]
5✔
3448
        bad_addr = []  # type: List[Tuple[str, str]]
5✔
3449
        for address in addresses:
5✔
3450
            if not bitcoin.is_address(address):
5✔
UNCOV
3451
                bad_addr.append((address, _('invalid address')))
×
UNCOV
3452
                continue
×
3453
            if self.db.has_imported_address(address):
5✔
3454
                bad_addr.append((address, _('address already in wallet')))
×
UNCOV
3455
                continue
×
3456
            good_addr.append(address)
5✔
3457
            self.db.add_imported_address(address, {})
5✔
3458
            self.adb.add_address(address)
5✔
3459
        if write_to_disk:
5✔
3460
            self.save_db()
5✔
3461
        return good_addr, bad_addr
5✔
3462

3463
    def import_address(self, address: str) -> str:
5✔
3464
        good_addr, bad_addr = self.import_addresses([address])
5✔
3465
        if good_addr and good_addr[0] == address:
5✔
3466
            return address
5✔
3467
        else:
UNCOV
3468
            raise BitcoinException(str(bad_addr[0][1]))
×
3469

3470
    def delete_address(self, address: str) -> None:
5✔
3471
        if not self.db.has_imported_address(address):
5✔
UNCOV
3472
            return
×
3473
        if len(self.get_addresses()) <= 1:
5✔
3474
            raise UserFacingException(_('Cannot delete last remaining address from wallet'))
5✔
3475
        transactions_to_remove = set()  # only referred to by this address
5✔
3476
        transactions_new = set()  # txs that are not only referred to by address
5✔
3477
        with self.lock:
5✔
3478
            for addr in self.db.get_history():
5✔
3479
                details = self.adb.get_address_history(addr).items()
5✔
3480
                if addr == address:
5✔
3481
                    for tx_hash, height in details:
5✔
3482
                        transactions_to_remove.add(tx_hash)
5✔
3483
                else:
3484
                    for tx_hash, height in details:
5✔
3485
                        transactions_new.add(tx_hash)
5✔
3486
            transactions_to_remove -= transactions_new
5✔
3487
            self.db.remove_addr_history(address)
5✔
3488
            for tx_hash in transactions_to_remove:
5✔
3489
                self.adb._remove_transaction(tx_hash)
5✔
3490
        self.set_label(address, None)
5✔
3491
        if req:= self.get_request_by_addr(address):
5✔
UNCOV
3492
            self.delete_request(req.get_id())
×
3493
        self.set_frozen_state_of_addresses([address], False, write_to_disk=False)
5✔
3494
        pubkey = self.get_public_key(address)
5✔
3495
        self.db.remove_imported_address(address)
5✔
3496
        if pubkey:
5✔
3497
            # delete key iff no other address uses it (e.g. p2pkh and p2wpkh for same key)
3498
            for txin_type in bitcoin.WIF_SCRIPT_TYPES.keys():
5✔
3499
                try:
5✔
3500
                    addr2 = bitcoin.pubkey_to_address(txin_type, pubkey)
5✔
3501
                except NotImplementedError:
5✔
3502
                    pass
5✔
3503
                else:
3504
                    if self.db.has_imported_address(addr2):
5✔
UNCOV
3505
                        break
×
3506
            else:
3507
                self.keystore.delete_imported_key(pubkey)
5✔
3508
                self.save_keystore()
5✔
3509
        self.save_db()
5✔
3510

3511
    def get_change_addresses_for_new_transaction(self, *args, **kwargs) -> List[str]:
5✔
3512
        # for an imported wallet, if all "change addresses" are already used,
3513
        # it is probably better to send change back to the "from address", than to
3514
        # send it to another random used address and link them together, hence
3515
        # we force "allow_reusing_used_change_addrs=False"
3516
        return super().get_change_addresses_for_new_transaction(
5✔
3517
            *args,
3518
            **{**kwargs, "allow_reusing_used_change_addrs": False},
3519
        )
3520

3521
    def calc_unused_change_addresses(self) -> Sequence[str]:
5✔
3522
        with self.lock:
5✔
3523
            unused_addrs = [addr for addr in self.get_change_addresses()
5✔
3524
                            if not self.adb.is_used(addr) and not self.is_address_reserved(addr)]
3525
            return unused_addrs
5✔
3526

3527
    def is_mine(self, address) -> bool:
5✔
3528
        if not address: return False
5✔
3529
        return self.db.has_imported_address(address)
5✔
3530

3531
    def get_address_index(self, address) -> Optional[str]:
5✔
3532
        # returns None if address is not mine
3533
        return self.get_public_key(address)
5✔
3534

3535
    def get_address_path_str(self, address):
5✔
UNCOV
3536
        return None
×
3537

3538
    def get_public_key(self, address) -> Optional[str]:
5✔
3539
        x = self.db.get_imported_address(address)
5✔
3540
        return x.get('pubkey') if x else None
5✔
3541

3542
    def import_private_keys(self, keys: List[str], password: Optional[str], *,
5✔
3543
                            write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:
3544
        good_addr = []  # type: List[str]
5✔
3545
        bad_keys = []  # type: List[Tuple[str, str]]
5✔
3546
        for key in keys:
5✔
3547
            try:
5✔
3548
                txin_type, pubkey = self.keystore.import_privkey(key, password)
5✔
3549
            except Exception as e:
5✔
3550
                bad_keys.append((key, _('invalid private key') + f': {e}'))
5✔
3551
                continue
5✔
3552
            if txin_type not in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
5✔
UNCOV
3553
                bad_keys.append((key, _('not implemented type') + f': {txin_type}'))
×
UNCOV
3554
                continue
×
3555
            addr = bitcoin.pubkey_to_address(txin_type, pubkey)
5✔
3556
            good_addr.append(addr)
5✔
3557
            self.db.add_imported_address(addr, {'type':txin_type, 'pubkey':pubkey})
5✔
3558
            self.adb.add_address(addr)
5✔
3559
        self.save_keystore()
5✔
3560
        if write_to_disk:
5✔
3561
            self.save_db()
5✔
3562
        return good_addr, bad_keys
5✔
3563

3564
    def import_private_key(self, key: str, password: Optional[str]) -> str:
5✔
3565
        good_addr, bad_keys = self.import_private_keys([key], password=password)
5✔
3566
        if good_addr:
5✔
3567
            return good_addr[0]
5✔
3568
        else:
3569
            raise BitcoinException(str(bad_keys[0][1]))
5✔
3570

3571
    def get_txin_type(self, address):
5✔
3572
        return self.db.get_imported_address(address).get('type', 'address')
5✔
3573

3574
    @profiler
5✔
3575
    def try_detecting_internal_addresses_corruption(self):
5✔
3576
        # we check only a random sample, for performance
UNCOV
3577
        addresses_all = self.get_addresses()
×
3578
        # some random *used* addresses (note: we likely have not synced yet)
3579
        addresses_used = [addr for addr in addresses_all if self.adb.is_used(addr)]
×
UNCOV
3580
        sample1 = random.sample(addresses_used, min(len(addresses_used), 10))
×
3581
        # some random *unused* addresses
3582
        addresses_unused = [addr for addr in addresses_all if not self.adb.is_used(addr)]
×
UNCOV
3583
        sample2 = random.sample(addresses_unused, min(len(addresses_unused), 10))
×
3584
        for addr_found in itertools.chain(sample1, sample2):
×
3585
            self.check_address_for_corruption(addr_found)
×
3586

3587
    def check_address_for_corruption(self, addr):
5✔
3588
        if addr and self.is_mine(addr):
5✔
3589
            pubkey = self.get_public_key(addr)
5✔
3590
            if not pubkey:
5✔
UNCOV
3591
                return
×
3592
            txin_type = self.get_txin_type(addr)
5✔
3593
            if txin_type == 'address':
5✔
UNCOV
3594
                return
×
3595
            if addr != bitcoin.pubkey_to_address(txin_type, pubkey):
5✔
3596
                raise InternalAddressCorruption()
×
3597

3598
    def pubkeys_to_address(self, pubkeys):
5✔
UNCOV
3599
        pubkey = pubkeys[0]
×
3600
        # FIXME This is slow.
3601
        #       Ideally we would re-derive the address from the pubkey and the txin_type,
3602
        #       but we don't know the txin_type, and we only have an addr->txin_type map.
3603
        #       so instead a linear search of reverse-lookups is done...
UNCOV
3604
        for addr in self.db.get_imported_addresses():
×
UNCOV
3605
            if self.db.get_imported_address(addr)['pubkey'] == pubkey:
×
3606
                return addr
×
3607
        return None
×
3608

3609
    def decrypt_message(self, pubkey: str, message, password) -> bytes:
5✔
3610
        # this is significantly faster than the implementation in the superclass
3611
        return self.keystore.decrypt_message(pubkey, message, password)
5✔
3612

3613

3614
class Deterministic_Wallet(Abstract_Wallet):
5✔
3615

3616
    def __init__(self, db, *, config):
5✔
3617
        self._ephemeral_addr_to_addr_index = {}  # type: Dict[str, Sequence[int]]
5✔
3618
        Abstract_Wallet.__init__(self, db, config=config)
5✔
3619
        self.gap_limit = db.get('gap_limit', 20)
5✔
3620
        # generate addresses now. note that without libsecp this might block
3621
        # for a few seconds!
3622
        self.synchronize()
5✔
3623

3624
    def _init_lnworker(self):
5✔
3625
        # lightning_privkey2 is not deterministic (legacy wallets, bip39)
3626
        ln_xprv = self.db.get('lightning_xprv') or self.db.get('lightning_privkey2')
5✔
3627
        # lnworker can only be initialized once receiving addresses are available
3628
        # therefore we instantiate lnworker in DeterministicWallet
3629
        self.lnworker = LNWallet(self, ln_xprv) if ln_xprv else None
5✔
3630

3631
    def has_seed(self):
5✔
3632
        return self.keystore.has_seed()
5✔
3633

3634
    def get_addresses(self):
5✔
3635
        # note: overridden so that the history can be cleared.
3636
        # addresses are ordered based on derivation
3637
        out = self.get_receiving_addresses()
5✔
3638
        out += self.get_change_addresses()
5✔
3639
        return out
5✔
3640

3641
    def get_receiving_addresses(self, *, slice_start=None, slice_stop=None):
5✔
3642
        return self.db.get_receiving_addresses(slice_start=slice_start, slice_stop=slice_stop)
5✔
3643

3644
    def get_change_addresses(self, *, slice_start=None, slice_stop=None):
5✔
3645
        return self.db.get_change_addresses(slice_start=slice_start, slice_stop=slice_stop)
5✔
3646

3647
    @profiler
5✔
3648
    def try_detecting_internal_addresses_corruption(self):
5✔
UNCOV
3649
        addresses_all = self.get_addresses()
×
3650
        # first few addresses
3651
        nfirst_few = 10
×
UNCOV
3652
        sample1 = addresses_all[:nfirst_few]
×
3653
        # some random *used* addresses (note: we likely have not synced yet)
3654
        addresses_used = [addr for addr in addresses_all[nfirst_few:] if self.adb.is_used(addr)]
×
UNCOV
3655
        sample2 = random.sample(addresses_used, min(len(addresses_used), 10))
×
3656
        # some random *unused* addresses
3657
        addresses_unused = [addr for addr in addresses_all[nfirst_few:] if not self.adb.is_used(addr)]
×
UNCOV
3658
        sample3 = random.sample(addresses_unused, min(len(addresses_unused), 10))
×
3659
        for addr_found in itertools.chain(sample1, sample2, sample3):
×
3660
            self.check_address_for_corruption(addr_found)
×
3661

3662
    def check_address_for_corruption(self, addr):
5✔
3663
        if addr and self.is_mine(addr):
5✔
3664
            if addr != self.derive_address(*self.get_address_index(addr)):
5✔
UNCOV
3665
                raise InternalAddressCorruption()
×
3666

3667
    def get_seed(self, password):
5✔
3668
        return self.keystore.get_seed(password)
5✔
3669

3670
    def get_seed_type(self) -> Optional[str]:
5✔
UNCOV
3671
        if not self.has_seed():
×
UNCOV
3672
            return None
×
3673
        assert isinstance(self.keystore, keystore.Deterministic_KeyStore), type(self.keystore)
×
3674
        return self.keystore.get_seed_type()
×
3675

3676
    def change_gap_limit(self, value):
5✔
3677
        '''This method is not called in the code, it is kept for console use'''
UNCOV
3678
        value = int(value)
×
UNCOV
3679
        if value >= self.min_acceptable_gap():
×
3680
            self.gap_limit = value
×
3681
            self.db.put('gap_limit', self.gap_limit)
×
3682
            self.save_db()
×
3683
            return True
×
3684
        else:
3685
            return False
×
3686

3687
    def num_unused_trailing_addresses(self, addresses):
5✔
UNCOV
3688
        k = 0
×
UNCOV
3689
        for addr in addresses[::-1]:
×
3690
            if self.db.get_addr_history(addr):
×
3691
                break
×
3692
            k += 1
×
3693
        return k
×
3694

3695
    def min_acceptable_gap(self) -> int:
5✔
3696
        # fixme: this assumes wallet is synchronized
UNCOV
3697
        n = 0
×
UNCOV
3698
        nmax = 0
×
3699
        addresses = self.get_receiving_addresses()
×
3700
        k = self.num_unused_trailing_addresses(addresses)
×
3701
        for addr in addresses[0:-k]:
×
3702
            if self.adb.address_is_old(addr):
×
3703
                n = 0
×
3704
            else:
3705
                n += 1
×
UNCOV
3706
                nmax = max(nmax, n)
×
3707
        return nmax + 1
×
3708

3709
    @abstractmethod
5✔
3710
    def derive_pubkeys(self, c: int, i: int) -> Sequence[str]:
5✔
UNCOV
3711
        pass
×
3712

3713
    def derive_address(self, for_change: int, n: int) -> str:
5✔
3714
        for_change = int(for_change)
5✔
3715
        pubkeys = self.derive_pubkeys(for_change, n)
5✔
3716
        return self.pubkeys_to_address(pubkeys)
5✔
3717

3718
    def export_private_key_for_path(self, path: Union[Sequence[int], str], password: Optional[str]) -> str:
5✔
3719
        if isinstance(path, str):
5✔
3720
            path = convert_bip32_strpath_to_intpath(path)
5✔
3721
        pk, compressed = self.keystore.get_private_key(path, password)
5✔
3722
        txin_type = self.get_txin_type()  # assumes no mixed-scripts in wallet
5✔
3723
        return bitcoin.serialize_privkey(pk, compressed, txin_type)
5✔
3724

3725
    def get_public_keys_with_deriv_info(self, address: str):
5✔
3726
        der_suffix = self.get_address_index(address)
5✔
3727
        der_suffix = [int(x) for x in der_suffix]
5✔
3728
        return {k.derive_pubkey(*der_suffix): (k, der_suffix)
5✔
3729
                for k in self.get_keystores()}
3730

3731
    def _add_txinout_derivation_info(self, txinout, address, *, only_der_suffix):
5✔
3732
        if not self.is_mine(address):
5✔
UNCOV
3733
            return
×
3734
        pubkey_deriv_info = self.get_public_keys_with_deriv_info(address)
5✔
3735
        for pubkey in pubkey_deriv_info:
5✔
3736
            ks, der_suffix = pubkey_deriv_info[pubkey]
5✔
3737
            fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix,
5✔
3738
                                                                                   only_der_suffix=only_der_suffix)
3739
            txinout.bip32_paths[pubkey] = (fp_bytes, der_full)
5✔
3740

3741
    def create_new_address(self, for_change: bool = False):
5✔
3742
        assert type(for_change) is bool
5✔
3743
        with self.lock:
5✔
3744
            n = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses()
5✔
3745
            address = self.derive_address(int(for_change), n)
5✔
3746
            self.db.add_change_address(address) if for_change else self.db.add_receiving_address(address)
5✔
3747
            self.adb.add_address(address)
5✔
3748
            if for_change:
5✔
3749
                # note: if it's actually "old", it will get filtered later
3750
                self._not_old_change_addresses.append(address)
5✔
3751
            return address
5✔
3752

3753
    def synchronize_sequence(self, for_change: bool) -> int:
5✔
3754
        count = 0  # num new addresses we generated
5✔
3755
        limit = self.gap_limit_for_change if for_change else self.gap_limit
5✔
3756
        while True:
5✔
3757
            num_addr = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses()
5✔
3758
            if num_addr < limit:
5✔
3759
                count += 1
5✔
3760
                self.create_new_address(for_change)
5✔
3761
                continue
5✔
3762
            if for_change:
5✔
3763
                last_few_addresses = self.get_change_addresses(slice_start=-limit)
5✔
3764
            else:
3765
                last_few_addresses = self.get_receiving_addresses(slice_start=-limit)
5✔
3766
            if any(map(self.adb.address_is_old, last_few_addresses)):
5✔
3767
                count += 1
5✔
3768
                self.create_new_address(for_change)
5✔
3769
            else:
3770
                break
5✔
3771
        return count
5✔
3772

3773
    def synchronize(self):
5✔
3774
        count = 0
5✔
3775
        with self.lock:
5✔
3776
            count += self.synchronize_sequence(False)
5✔
3777
            count += self.synchronize_sequence(True)
5✔
3778
        return count
5✔
3779

3780
    def get_all_known_addresses_beyond_gap_limit(self):
5✔
3781
        # note that we don't stop at first large gap
UNCOV
3782
        found = set()
×
3783

3784
        def process_addresses(addrs, gap_limit):
×
UNCOV
3785
            rolling_num_unused = 0
×
3786
            for addr in addrs:
×
3787
                if self.db.get_addr_history(addr):
×
3788
                    rolling_num_unused = 0
×
3789
                else:
3790
                    if rolling_num_unused >= gap_limit:
×
UNCOV
3791
                        found.add(addr)
×
3792
                    rolling_num_unused += 1
×
3793

3794
        process_addresses(self.get_receiving_addresses(), self.gap_limit)
×
UNCOV
3795
        process_addresses(self.get_change_addresses(), self.gap_limit_for_change)
×
3796
        return found
×
3797

3798
    def get_address_index(self, address) -> Optional[Sequence[int]]:
5✔
3799
        return self.db.get_address_index(address) or self._ephemeral_addr_to_addr_index.get(address)
5✔
3800

3801
    def get_address_path_str(self, address):
5✔
UNCOV
3802
        intpath = self.get_address_index(address)
×
UNCOV
3803
        if intpath is None:
×
3804
            return None
×
3805
        return convert_bip32_intpath_to_strpath(intpath)
×
3806

3807
    def _learn_derivation_path_for_address_from_txinout(self, txinout, address):
5✔
3808
        for ks in self.get_keystores():
5✔
3809
            pubkey, der_suffix = ks.find_my_pubkey_in_txinout(txinout, only_der_suffix=True)
5✔
3810
            if der_suffix is not None:
5✔
3811
                # note: we already know the pubkey belongs to the keystore,
3812
                #       but the script template might be different
3813
                if len(der_suffix) != 2: continue
5✔
3814
                try:
5✔
3815
                    my_address = self.derive_address(*der_suffix)
5✔
UNCOV
3816
                except CannotDerivePubkey:
×
UNCOV
3817
                    my_address = None
×
3818
                if my_address == address:
5✔
3819
                    self._ephemeral_addr_to_addr_index[address] = list(der_suffix)
5✔
3820
                    return True
5✔
3821
        return False
5✔
3822

3823
    def get_master_public_keys(self):
5✔
UNCOV
3824
        return [self.get_master_public_key()]
×
3825

3826
    def get_fingerprint(self):
5✔
3827
        return self.get_master_public_key()
5✔
3828

3829
    def get_txin_type(self, address=None):
5✔
3830
        return self.txin_type
5✔
3831

3832

3833
class Standard_Wallet(Simple_Wallet, Deterministic_Wallet):
5✔
3834
    """ Deterministic Wallet with a single pubkey per address """
3835
    wallet_type = 'standard'
5✔
3836

3837
    def __init__(self, db, *, config):
5✔
3838
        Deterministic_Wallet.__init__(self, db, config=config)
5✔
3839

3840
    def get_public_key(self, address):
5✔
UNCOV
3841
        sequence = self.get_address_index(address)
×
UNCOV
3842
        pubkeys = self.derive_pubkeys(*sequence)
×
3843
        return pubkeys[0]
×
3844

3845
    def load_keystore(self):
5✔
3846
        self.keystore = load_keystore(self.db, 'keystore')  # type: KeyStoreWithMPK
5✔
3847
        try:
5✔
3848
            xtype = bip32.xpub_type(self.keystore.xpub)
5✔
3849
        except Exception:
5✔
3850
            xtype = 'standard'
5✔
3851
        self.txin_type = 'p2pkh' if xtype == 'standard' else xtype
5✔
3852

3853
    def get_master_public_key(self):
5✔
3854
        return self.keystore.get_master_public_key()
5✔
3855

3856
    def derive_pubkeys(self, c, i):
5✔
3857
        return [self.keystore.derive_pubkey(c, i).hex()]
5✔
3858

3859
    def pubkeys_to_address(self, pubkeys):
5✔
3860
        pubkey = pubkeys[0]
5✔
3861
        return bitcoin.pubkey_to_address(self.txin_type, pubkey)
5✔
3862

3863
    def has_support_for_slip_19_ownership_proofs(self) -> bool:
5✔
UNCOV
3864
        return self.keystore.has_support_for_slip_19_ownership_proofs()
×
3865

3866
    def add_slip_19_ownership_proofs_to_tx(self, tx: PartialTransaction) -> None:
5✔
UNCOV
3867
        tx.add_info_from_wallet(self)
×
UNCOV
3868
        self.keystore.add_slip_19_ownership_proofs_to_tx(tx=tx, password=None)
×
3869

3870

3871
class Multisig_Wallet(Deterministic_Wallet):
5✔
3872
    # generic m of n
3873

3874
    def __init__(self, db, *, config):
5✔
3875
        self.wallet_type = db.get('wallet_type')
5✔
3876
        self.m, self.n = multisig_type(self.wallet_type)
5✔
3877
        Deterministic_Wallet.__init__(self, db, config=config)
5✔
3878
        # sanity checks
3879
        for ks in self.get_keystores():
5✔
3880
            if not isinstance(ks, keystore.Xpub):
5✔
3881
                raise Exception(f"unexpected keystore type={type(ks)} in multisig")
5✔
3882
            if bip32.xpub_type(self.keystore.xpub) != bip32.xpub_type(ks.xpub):
5✔
3883
                raise Exception(f"multisig wallet needs to have homogeneous xpub types")
5✔
3884
        bip32_nodes = set({ks.get_bip32_node_for_xpub() for ks in self.get_keystores()})
5✔
3885
        if len(bip32_nodes) != len(self.get_keystores()):
5✔
3886
            raise Exception(f"duplicate xpubs in multisig")
5✔
3887

3888
    def get_public_keys(self, address):
5✔
UNCOV
3889
        return [pk.hex() for pk in self.get_public_keys_with_deriv_info(address)]
×
3890

3891
    def pubkeys_to_address(self, pubkeys):
5✔
3892
        redeem_script = self.pubkeys_to_scriptcode(pubkeys)
5✔
3893
        return bitcoin.redeem_script_to_address(self.txin_type, redeem_script)
5✔
3894

3895
    def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> bytes:
5✔
3896
        return transaction.multisig_script(sorted(pubkeys), self.m)
5✔
3897

3898
    def derive_pubkeys(self, c, i):
5✔
3899
        return [k.derive_pubkey(c, i).hex() for k in self.get_keystores()]
5✔
3900

3901
    def load_keystore(self):
5✔
3902
        self.keystores = {}
5✔
3903
        for i in range(self.n):
5✔
3904
            name = 'x%d'%(i+1)
5✔
3905
            self.keystores[name] = load_keystore(self.db, name)
5✔
3906
        self.keystore = self.keystores['x1']
5✔
3907
        xtype = bip32.xpub_type(self.keystore.xpub)
5✔
3908
        self.txin_type = 'p2sh' if xtype == 'standard' else xtype
5✔
3909

3910
    def save_keystore(self):
5✔
UNCOV
3911
        for name, k in self.keystores.items():
×
UNCOV
3912
            self.db.put(name, k.dump())
×
3913

3914
    def get_keystore(self):
5✔
UNCOV
3915
        return self.keystores.get('x1')
×
3916

3917
    def get_keystores(self):
5✔
3918
        return [self.keystores[i] for i in sorted(self.keystores.keys())]
5✔
3919

3920
    def can_have_keystore_encryption(self):
5✔
UNCOV
3921
        return any([k.may_have_password() for k in self.get_keystores()])
×
3922

3923
    def _update_password_for_keystore(self, old_pw, new_pw):
5✔
UNCOV
3924
        for name, keystore in self.keystores.items():
×
UNCOV
3925
            if keystore.may_have_password():
×
3926
                keystore.update_password(old_pw, new_pw)
×
3927
                self.db.put(name, keystore.dump())
×
3928

3929
    def check_password(self, password):
5✔
UNCOV
3930
        for name, keystore in self.keystores.items():
×
UNCOV
3931
            if keystore.may_have_password():
×
3932
                keystore.check_password(password)
×
3933
        if self.has_storage_encryption():
×
3934
            self.storage.check_password(password)
×
3935

3936
    def get_available_storage_encryption_version(self):
5✔
3937
        # multisig wallets are not offered hw device encryption
UNCOV
3938
        return StorageEncryptionVersion.USER_PASSWORD
×
3939

3940
    def has_seed(self):
5✔
UNCOV
3941
        return self.keystore.has_seed()
×
3942

3943
    def is_watching_only(self):
5✔
3944
        return all([k.is_watching_only() for k in self.get_keystores()])
5✔
3945

3946
    def get_master_public_key(self):
5✔
UNCOV
3947
        return self.keystore.get_master_public_key()
×
3948

3949
    def get_master_public_keys(self):
5✔
UNCOV
3950
        return [k.get_master_public_key() for k in self.get_keystores()]
×
3951

3952
    def get_fingerprint(self):
5✔
UNCOV
3953
        return ''.join(sorted(self.get_master_public_keys()))
×
3954

3955

3956
wallet_types = ['standard', 'multisig', 'imported']
5✔
3957

3958
def register_wallet_type(category):
5✔
3959
    wallet_types.append(category)
5✔
3960

3961
wallet_constructors = {
5✔
3962
    'standard': Standard_Wallet,
3963
    'old': Standard_Wallet,
3964
    'xpub': Standard_Wallet,
3965
    'imported': Imported_Wallet
3966
}
3967

3968
def register_constructor(wallet_type, constructor):
5✔
3969
    wallet_constructors[wallet_type] = constructor
5✔
3970

3971
# former WalletFactory
3972
class Wallet(object):
5✔
3973
    """The main wallet "entry point".
3974
    This class is actually a factory that will return a wallet of the correct
3975
    type when passed a WalletStorage instance."""
3976

3977
    def __new__(self, db: 'WalletDB', *, config: SimpleConfig):
5✔
3978
        wallet_type = db.get('wallet_type')
5✔
3979
        WalletClass = Wallet.wallet_class(wallet_type)
5✔
3980
        wallet = WalletClass(db, config=config)
5✔
3981
        return wallet
5✔
3982

3983
    @staticmethod
5✔
3984
    def wallet_class(wallet_type):
5✔
3985
        if multisig_type(wallet_type):
5✔
3986
            return Multisig_Wallet
5✔
3987
        if wallet_type in wallet_constructors:
5✔
3988
            return wallet_constructors[wallet_type]
5✔
UNCOV
3989
        raise WalletFileException("Unknown wallet type: " + str(wallet_type))
×
3990

3991

3992
def create_new_wallet(*, path, config: SimpleConfig, passphrase=None, password=None,
5✔
3993
                      encrypt_file=True, seed_type=None, gap_limit=None) -> dict:
3994
    """Create a new wallet"""
3995
    storage = WalletStorage(path)
5✔
3996
    if storage.file_exists():
5✔
UNCOV
3997
        raise UserFacingException("Remove the existing wallet first!")
×
3998
    db = WalletDB('', storage=storage, upgrade=True)
5✔
3999

4000
    seed = Mnemonic('en').make_seed(seed_type=seed_type)
5✔
4001
    k = keystore.from_seed(seed, passphrase=passphrase)
5✔
4002
    db.put('keystore', k.dump())
5✔
4003
    db.put('wallet_type', 'standard')
5✔
4004
    if k.can_have_deterministic_lightning_xprv():
5✔
4005
        db.put('lightning_xprv', k.get_lightning_xprv(None))
5✔
4006
    if gap_limit is not None:
5✔
4007
        db.put('gap_limit', gap_limit)
5✔
4008
    wallet = Wallet(db, config=config)
5✔
4009
    wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)
5✔
4010
    wallet.synchronize()
5✔
4011
    msg = "Please keep your seed in a safe place; if you lose it, you will not be able to restore your wallet."
5✔
4012
    wallet.save_db()
5✔
4013
    return {'seed': seed, 'wallet': wallet, 'msg': msg}
5✔
4014

4015

4016
def restore_wallet_from_text(
5✔
4017
    text: str,
4018
    *,
4019
    path: Optional[str],
4020
    config: SimpleConfig,
4021
    passphrase: Optional[str] = None,
4022
    password: Optional[str] = None,
4023
    encrypt_file: Optional[bool] = None,
4024
    gap_limit: Optional[int] = None,
4025
) -> dict:
4026
    """Restore a wallet from text. Text can be a seed phrase, a master
4027
    public key, a master private key, a list of bitcoin addresses
4028
    or bitcoin private keys."""
4029
    if path is None:  # create wallet in-memory
5✔
4030
        storage = None
5✔
4031
    else:
4032
        storage = WalletStorage(path)
5✔
4033
        if storage.file_exists():
5✔
UNCOV
4034
            raise UserFacingException("Remove the existing wallet first!")
×
4035
    if encrypt_file is None:
5✔
4036
        encrypt_file = True
5✔
4037
    db = WalletDB('', storage=storage, upgrade=True)
5✔
4038
    text = text.strip()
5✔
4039
    if keystore.is_address_list(text):
5✔
4040
        wallet = Imported_Wallet(db, config=config)
5✔
4041
        addresses = text.split()
5✔
4042
        good_inputs, bad_inputs = wallet.import_addresses(addresses, write_to_disk=False)
5✔
4043
        # FIXME tell user about bad_inputs
4044
        if not good_inputs:
5✔
UNCOV
4045
            raise UserFacingException("None of the given addresses can be imported")
×
4046
    elif keystore.is_private_key_list(text, allow_spaces_inside_key=False):
5✔
4047
        k = keystore.Imported_KeyStore({})
5✔
4048
        db.put('keystore', k.dump())
5✔
4049
        wallet = Imported_Wallet(db, config=config)
5✔
4050
        keys = keystore.get_private_keys(text, allow_spaces_inside_key=False)
5✔
4051
        good_inputs, bad_inputs = wallet.import_private_keys(keys, None, write_to_disk=False)
5✔
4052
        # FIXME tell user about bad_inputs
4053
        if not good_inputs:
5✔
UNCOV
4054
            raise UserFacingException("None of the given privkeys can be imported")
×
4055
    else:
4056
        if keystore.is_master_key(text):
5✔
4057
            k = keystore.from_master_key(text)
5✔
4058
        elif keystore.is_seed(text):
5✔
4059
            k = keystore.from_seed(text, passphrase=passphrase)
5✔
4060
            if k.can_have_deterministic_lightning_xprv():
5✔
4061
                db.put('lightning_xprv', k.get_lightning_xprv(None))
5✔
4062
        else:
UNCOV
4063
            raise UserFacingException("Seed or key not recognized")
×
4064
        db.put('keystore', k.dump())
5✔
4065
        db.put('wallet_type', 'standard')
5✔
4066
        if gap_limit is not None:
5✔
4067
            db.put('gap_limit', gap_limit)
5✔
4068
        wallet = Wallet(db, config=config)
5✔
4069
    if db.storage:
5✔
4070
        assert not db.storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk"
5✔
4071
    wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)
5✔
4072
    wallet.synchronize()
5✔
4073
    msg = ("This wallet was restored offline. It may contain more addresses than displayed. "
5✔
4074
           "Start a daemon and use load_wallet to sync its history.")
4075
    wallet.save_db()
5✔
4076
    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