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

spesmilo / electrum / 5558179037184000

18 Feb 2025 02:59PM UTC coverage: 60.523% (+0.02%) from 60.502%
5558179037184000

push

CirrusCI

ecdsa
simplify history-related commands:
 - reduce number of methods
 - use nametuples instead of dicts
 - only two types: OnchainHistoryItem and LightningHistoryItem
 - channel open/closes are groups
 - move capital gains into separate RPC

34 of 102 new or added lines in 5 files covered. (33.33%)

4 existing lines in 2 files now uncovered.

20266 of 33485 relevant lines covered (60.52%)

3.02 hits per line

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

63.6
/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
×
856
                for chan in self.lnworker.channels.values()]):
857
            return True
×
858
        if any([chan.funding_outpoint.txid == txid
×
859
                for chan in self.lnworker.channel_backups.values()]):
860
            return True
×
861
        return False
×
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 tx.is_rbf_enabled()
5✔
949
                    can_dscancel = (is_any_input_ismine and tx.is_rbf_enabled()
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 tx.is_rbf_enabled()
×
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
            can_bump = False  # 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:
×
NEW
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():
×
NEW
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(self, *, domain=None) -> Dict[str, OnchainHistoryItem]:
5✔
1150
        # call lnworker first, because it adds accounting addresses
NEW
1151
        groups = self.lnworker.get_groups_for_onchain_history() if self.lnworker else {}
×
1152

1153
        if domain is None:
×
1154
            domain = self.get_addresses()
×
1155

NEW
1156
        transactions = OrderedDictWithIndex()
×
1157
        monotonic_timestamp = 0
×
1158
        for hist_item in self.adb.get_history(domain=domain):
×
1159
            monotonic_timestamp = max(monotonic_timestamp, (hist_item.tx_mined_status.timestamp or TX_TIMESTAMP_INF))
×
NEW
1160
            txid = hist_item.txid
×
NEW
1161
            group_id = groups.get(txid)
×
NEW
1162
            label = self.get_label_for_txid(txid)
×
NEW
1163
            tx_item = OnchainHistoryItem(
×
1164
                txid=hist_item.txid,
1165
                amount_sat=hist_item.delta,
1166
                fee_sat=hist_item.fee,
1167
                balance_sat=hist_item.balance,
1168
                tx_mined_status=hist_item.tx_mined_status,
1169
                label=label,
1170
                monotonic_timestamp=monotonic_timestamp,
1171
                group_id=group_id,
1172
            )
NEW
1173
            transactions[hist_item.txid] = tx_item
×
1174

NEW
1175
        return transactions
×
1176

1177
    def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> Invoice:
5✔
1178
        height = self.adb.get_local_height()
5✔
1179
        if pr:
5✔
1180
            return Invoice.from_bip70_payreq(pr, height=height)
×
1181
        amount_msat = 0
5✔
1182
        for x in outputs:
5✔
1183
            if parse_max_spend(x.value):
5✔
1184
                amount_msat = '!'
5✔
1185
                break
5✔
1186
            else:
1187
                assert isinstance(x.value, int), f"{x.value!r}"
×
1188
                amount_msat += x.value * 1000
×
1189
        timestamp = None
5✔
1190
        exp = None
5✔
1191
        if URI:
5✔
1192
            timestamp = URI.get('time')
5✔
1193
            exp = URI.get('exp')
5✔
1194
        timestamp = timestamp or int(Invoice._get_cur_time())
5✔
1195
        exp = exp or 0
5✔
1196
        invoice = Invoice(
5✔
1197
            amount_msat=amount_msat,
1198
            message=message,
1199
            time=timestamp,
1200
            exp=exp,
1201
            outputs=outputs,
1202
            bip70=None,
1203
            height=height,
1204
            lightning_invoice=None,
1205
        )
1206
        return invoice
5✔
1207

1208
    def save_invoice(self, invoice: Invoice, *, write_to_disk: bool = True) -> None:
5✔
1209
        key = invoice.get_id()
×
1210
        if not invoice.is_lightning():
×
1211
            if self.is_onchain_invoice_paid(invoice)[0]:
×
1212
                _logger.info("saving invoice... but it is already paid!")
×
1213
            with self.transaction_lock:
×
1214
                for txout in invoice.get_outputs():
×
1215
                    self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(key)
×
1216
        self._invoices[key] = invoice
×
1217
        if write_to_disk:
×
1218
            self.save_db()
×
1219

1220
    def clear_invoices(self):
5✔
1221
        self._invoices.clear()
×
1222
        self.save_db()
×
1223

1224
    def clear_requests(self):
5✔
1225
        self._receive_requests.clear()
×
1226
        self._requests_addr_to_key.clear()
×
1227
        self.save_db()
×
1228

1229
    def get_invoices(self) -> List[Invoice]:
5✔
1230
        out = list(self._invoices.values())
×
1231
        out.sort(key=lambda x:x.time)
×
1232
        return out
×
1233

1234
    def get_unpaid_invoices(self) -> List[Invoice]:
5✔
1235
        invoices = self.get_invoices()
×
1236
        return [x for x in invoices if self.get_invoice_status(x) != PR_PAID]
×
1237

1238
    def get_invoice(self, invoice_id):
5✔
1239
        return self._invoices.get(invoice_id)
×
1240

1241
    def import_requests(self, path):
5✔
1242
        data = read_json_file(path)
×
1243
        for x in data:
×
1244
            try:
×
1245
                req = Request(**x)
×
1246
            except Exception:
×
1247
                raise FileImportFailed(_("Invalid invoice format"))
×
1248
            self.add_payment_request(req, write_to_disk=False)
×
1249
        self.save_db()
×
1250

1251
    def export_requests(self, path):
5✔
1252
        # note: this does not export preimages for LN bolt11 invoices
1253
        write_json_file(path, list(self._receive_requests.values()))
×
1254

1255
    def import_invoices(self, path):
5✔
1256
        data = read_json_file(path)
×
1257
        for x in data:
×
1258
            try:
×
1259
                invoice = Invoice(**x)
×
1260
            except Exception:
×
1261
                raise FileImportFailed(_("Invalid invoice format"))
×
1262
            self.save_invoice(invoice, write_to_disk=False)
×
1263
        self.save_db()
×
1264

1265
    def export_invoices(self, path):
5✔
1266
        write_json_file(path, list(self._invoices.values()))
×
1267

1268
    def get_relevant_invoices_for_tx(self, tx_hash: Optional[str]) -> Sequence[Invoice]:
5✔
1269
        if not tx_hash:
5✔
1270
            return []
×
1271
        invoice_keys = self._invoices_from_txid_map.get(tx_hash, set())
5✔
1272
        invoices = [self.get_invoice(key) for key in invoice_keys]
5✔
1273
        invoices = [inv for inv in invoices if inv]  # filter out None
5✔
1274
        for inv in invoices:
5✔
1275
            assert isinstance(inv, Invoice), f"unexpected type {type(inv)}"
×
1276
        return invoices
5✔
1277

1278
    def _init_requests_rhash_index(self):
5✔
1279
        # self._requests_addr_to_key may contain addresses that can be reused
1280
        # this is checked in get_request_by_address
1281
        self._requests_addr_to_key = defaultdict(set)  # type: Dict[str, Set[str]]
5✔
1282
        for req in self._receive_requests.values():
5✔
1283
            if addr := req.get_address():
5✔
1284
                self._requests_addr_to_key[addr].add(req.get_id())
5✔
1285

1286
    def _prepare_onchain_invoice_paid_detection(self):
5✔
1287
        self._invoices_from_txid_map = defaultdict(set)  # type: Dict[str, Set[str]]
5✔
1288
        self._invoices_from_scriptpubkey_map = defaultdict(set)  # type: Dict[bytes, Set[str]]
5✔
1289
        self._update_onchain_invoice_paid_detection(self._invoices.keys())
5✔
1290

1291
    def _update_onchain_invoice_paid_detection(self, invoice_keys: Iterable[str]) -> None:
5✔
1292
        for invoice_key in invoice_keys:
5✔
1293
            invoice = self._invoices.get(invoice_key)
5✔
1294
            if not invoice:
5✔
1295
                continue
×
1296
            if invoice.is_lightning() and not invoice.get_address():
5✔
1297
                continue
5✔
1298
            if invoice.is_lightning() and self.lnworker and self.lnworker.get_invoice_status(invoice) == PR_PAID:
5✔
1299
                continue
5✔
1300
            is_paid, conf_needed, relevant_txs = self._is_onchain_invoice_paid(invoice)
5✔
1301
            if is_paid:
5✔
1302
                for txid in relevant_txs:
5✔
1303
                    self._invoices_from_txid_map[txid].add(invoice_key)
5✔
1304
            for txout in invoice.get_outputs():
5✔
1305
                self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key)
5✔
1306
            # update invoice status
1307
            status = self.get_invoice_status(invoice)
5✔
1308
            util.trigger_callback('invoice_status', self, invoice_key, status)
5✔
1309

1310
    def _is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional[int], Sequence[str]]:
5✔
1311
        """Returns whether on-chain invoice/request is satisfied, num confs required txs have,
1312
        and list of relevant TXIDs.
1313
        """
1314
        outputs = invoice.get_outputs()
5✔
1315
        if not outputs:  # e.g. lightning-only
5✔
1316
            return False, None, []
×
1317
        invoice_amounts = defaultdict(int)  # type: Dict[bytes, int]  # scriptpubkey -> value_sats
5✔
1318
        for txo in outputs:  # type: PartialTxOutput
5✔
1319
            invoice_amounts[txo.scriptpubkey] += 1 if parse_max_spend(txo.value) else txo.value
5✔
1320
        relevant_txs = set()
5✔
1321
        is_paid = True
5✔
1322
        conf_needed = None  # type: Optional[int]
5✔
1323
        with self.lock, self.transaction_lock:
5✔
1324
            for invoice_scriptpubkey, invoice_amt in invoice_amounts.items():
5✔
1325
                scripthash = bitcoin.script_to_scripthash(invoice_scriptpubkey)
5✔
1326
                prevouts_and_values = self.db.get_prevouts_by_scripthash(scripthash)
5✔
1327
                confs_and_values = []
5✔
1328
                for prevout, v in prevouts_and_values:
5✔
1329
                    relevant_txs.add(prevout.txid.hex())
5✔
1330
                    tx_height = self.adb.get_tx_height(prevout.txid.hex())
5✔
1331
                    if 0 < tx_height.height <= invoice.height:  # exclude txs older than invoice
5✔
1332
                        continue
5✔
1333
                    confs_and_values.append((tx_height.conf or 0, v))
5✔
1334
                # check that there is at least one TXO, and that they pay enough.
1335
                # note: "at least one TXO" check is needed for zero amount invoice (e.g. OP_RETURN)
1336
                vsum = 0
5✔
1337
                for conf, v in reversed(sorted(confs_and_values)):
5✔
1338
                    vsum += v
5✔
1339
                    if vsum >= invoice_amt:
5✔
1340
                        conf_needed = min(conf_needed, conf) if conf_needed is not None else conf
5✔
1341
                        break
5✔
1342
                else:
1343
                    is_paid = False
5✔
1344
        return is_paid, conf_needed, list(relevant_txs)
5✔
1345

1346
    def is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional[int]]:
5✔
1347
        is_paid, conf_needed, relevant_txs = self._is_onchain_invoice_paid(invoice)
5✔
1348
        return is_paid, conf_needed
5✔
1349

1350
    @profiler
5✔
1351
    def get_full_history(self, fx=None, *, onchain_domain=None, include_lightning=True, include_fiat=False) -> dict:
5✔
1352
        """
1353
        includes both onchain and lightning
1354
        includes grouping information
1355
        """
UNCOV
1356
        transactions_tmp = OrderedDictWithIndex()
×
1357
        # add on-chain txns
1358
        onchain_history = self.get_onchain_history(domain=onchain_domain)
×
NEW
1359
        for tx_item in onchain_history.values():
×
NEW
1360
            txid = tx_item.txid
×
NEW
1361
            transactions_tmp[txid] = tx_item.to_dict()
×
NEW
1362
            transactions_tmp[txid]['lightning'] = False
×
1363

1364
        # add lightning_transactions
1365
        lightning_history = self.lnworker.get_lightning_history() if self.lnworker and include_lightning else {}
×
1366
        for tx_item in lightning_history.values():
×
NEW
1367
            key = tx_item.payment_hash or 'ln:' + tx_item.group_id
×
NEW
1368
            transactions_tmp[key] = tx_item.to_dict()
×
NEW
1369
            transactions_tmp[key]['lightning'] = True
×
1370

1371
        # sort on-chain and LN stuff into new dict, by timestamp
1372
        # (we rely on this being a *stable* sort)
1373
        def sort_key(x):
×
1374
            txid, tx_item = x
×
1375
            ts = tx_item.get('monotonic_timestamp') or tx_item.get('timestamp') or float('inf')
×
1376
            height = self.adb.tx_height_to_sort_height(tx_item.get('height'))
×
1377
            return ts, height
×
1378
        # create groups
1379
        transactions = OrderedDictWithIndex()
×
1380
        for k, tx_item in sorted(list(transactions_tmp.items()), key=sort_key):
×
1381
            group_id = tx_item.get('group_id')
×
1382
            if not group_id:
×
1383
                transactions[k] = tx_item
×
1384
            else:
1385
                key = 'group:' + group_id
×
1386
                parent = transactions.get(key)
×
NEW
1387
                group_label = self.get_label_for_group(group_id)
×
1388
                if parent is None:
×
1389
                    parent = {
×
1390
                        'label': group_label,
1391
                        'fiat_value': Fiat(Decimal(0), fx.ccy) if fx else None,
1392
                        'bc_value': Satoshis(0),
1393
                        'ln_value': Satoshis(0),
1394
                        'value': Satoshis(0),
1395
                        'children': [],
1396
                        'timestamp': 0,
1397
                        'date': timestamp_to_datetime(0),
1398
                        'fee_sat': 0,
1399
                        # fixme: there is no guarantee that there will be an onchain tx in the group
1400
                        'height': 0,
1401
                        'confirmations': 0,
1402
                        'txid': '----',
1403
                    }
1404
                    transactions[key] = parent
×
1405
                if 'bc_value' in tx_item:
×
1406
                    parent['bc_value'] += tx_item['bc_value']
×
1407
                if 'ln_value' in tx_item:
×
1408
                    parent['ln_value'] += tx_item['ln_value']
×
1409
                parent['value'] = parent['bc_value'] + parent['ln_value']
×
1410
                if 'fiat_value' in tx_item:
×
1411
                    parent['fiat_value'] += tx_item['fiat_value']
×
1412
                if tx_item.get('txid') == group_id:
×
1413
                    parent['lightning'] = False
×
1414
                    parent['txid'] = tx_item['txid']
×
1415
                    parent['timestamp'] = tx_item['timestamp']
×
1416
                    parent['date'] = timestamp_to_datetime(tx_item['timestamp'])
×
1417
                    parent['height'] = tx_item['height']
×
1418
                    parent['confirmations'] = tx_item['confirmations']
×
1419
                    parent['wanted_height'] = tx_item.get('wanted_height')
×
1420
                parent['children'].append(tx_item)
×
1421

1422
        now = time.time()
×
1423
        for key, item in transactions.items():
×
1424
            children = item.get('children', [])
×
1425
            if len(children) == 1:
×
1426
                transactions[key] = children[0]
×
1427
            # add on-chain and lightning values
1428
            # note: 'value' has msat precision (as LN has msat precision)
1429
            item['value'] = item.get('bc_value', Satoshis(0)) + item.get('ln_value', Satoshis(0))
×
1430
            for child in item.get('children', []):
×
1431
                child['value'] = child.get('bc_value', Satoshis(0)) + child.get('ln_value', Satoshis(0))
×
1432
            if include_fiat:
×
1433
                value = item['value'].value
×
1434
                txid = item.get('txid')
×
1435
                if not item.get('lightning') and txid:
×
1436
                    fiat_fields = self.get_tx_item_fiat(tx_hash=txid, amount_sat=value, fx=fx, tx_fee=item['fee_sat'])
×
1437
                    item.update(fiat_fields)
×
1438
                else:
1439
                    timestamp = item['timestamp'] or now
×
1440
                    fiat_value = value / Decimal(bitcoin.COIN) * fx.timestamp_rate(timestamp)
×
1441
                    item['fiat_value'] = Fiat(fiat_value, fx.ccy)
×
1442
                    item['fiat_default'] = True
×
1443
        return transactions
×
1444

1445
    @profiler
5✔
1446
    def get_onchain_capital_gains(
5✔
1447
            self,
1448
            from_timestamp=None,
1449
            to_timestamp=None,
1450
            fx=None,
1451
            show_addresses=False,
1452
            from_height=None,
1453
            to_height=None):
1454
        # History with capital gains, using utxo pricing
1455
        # FIXME: Lightning capital gains would requires FIFO
1456
        if (from_timestamp is not None or to_timestamp is not None) \
×
1457
                and (from_height is not None or to_height is not None):
1458
            raise UserFacingException('timestamp and block height based filtering cannot be used together')
×
1459

1460
        show_fiat = fx and fx.is_enabled() and fx.has_history()
×
1461
        out = []
×
1462
        income = 0
×
1463
        expenditures = 0
×
1464
        capital_gains = Decimal(0)
×
1465
        fiat_income = Decimal(0)
×
1466
        fiat_expenditures = Decimal(0)
×
1467
        now = time.time()
×
NEW
1468
        for txid, hitem in self.get_onchain_history().items():
×
NEW
1469
            item = hitem.to_dict()
×
NEW
1470
            if item['bc_value'].value == 0:
×
NEW
1471
                continue
×
1472
            timestamp = item['timestamp']
×
1473
            if from_timestamp and (timestamp or now) < from_timestamp:
×
1474
                continue
×
1475
            if to_timestamp and (timestamp or now) >= to_timestamp:
×
1476
                continue
×
1477
            height = item['height']
×
1478
            if from_height is not None and from_height > height > 0:
×
1479
                continue
×
1480
            if to_height is not None and (height >= to_height or height <= 0):
×
1481
                continue
×
1482
            tx_hash = item['txid']
×
1483
            tx = self.db.get_transaction(tx_hash)
×
1484
            tx_fee = item['fee_sat']
×
1485
            item['fee'] = Satoshis(tx_fee) if tx_fee is not None else None
×
1486
            if show_addresses:
×
1487
                item['inputs'] = list(map(lambda x: x.to_json(), tx.inputs()))
×
1488
                item['outputs'] = list(map(lambda x: {'address': x.get_ui_address_str(), 'value': Satoshis(x.value)},
×
1489
                                           tx.outputs()))
1490
            # fixme: use in and out values
1491
            value = item['bc_value'].value
×
1492
            if value < 0:
×
1493
                expenditures += -value
×
1494
            else:
1495
                income += value
×
1496
            # fiat computations
1497
            if show_fiat:
×
1498
                fiat_fields = self.get_tx_item_fiat(tx_hash=tx_hash, amount_sat=value, fx=fx, tx_fee=tx_fee)
×
1499
                fiat_value = fiat_fields['fiat_value'].value
×
1500
                item.update(fiat_fields)
×
1501
                if value < 0:
×
1502
                    capital_gains += fiat_fields['capital_gain'].value
×
1503
                    fiat_expenditures += -fiat_value
×
1504
                else:
1505
                    fiat_income += fiat_value
×
1506
            out.append(item)
×
1507
        # add summary
1508
        if out:
×
1509
            first_item = out[0]
×
1510
            last_item = out[-1]
×
1511
            if from_height or to_height:
×
1512
                start_height = from_height
×
1513
                end_height = to_height
×
1514
            else:
1515
                start_height = first_item['height'] - 1
×
1516
                end_height = last_item['height']
×
1517

1518
            b = first_item['bc_balance'].value
×
1519
            v = first_item['bc_value'].value
×
1520
            start_balance = None if b is None or v is None else b - v
×
1521
            end_balance = last_item['bc_balance'].value
×
1522

1523
            if from_timestamp is not None and to_timestamp is not None:
×
1524
                start_timestamp = from_timestamp
×
1525
                end_timestamp = to_timestamp
×
1526
            else:
1527
                start_timestamp = first_item['timestamp']
×
1528
                end_timestamp = last_item['timestamp']
×
1529

1530
            start_coins = self.get_utxos(
×
1531
                block_height=start_height,
1532
                confirmed_funding_only=True,
1533
                confirmed_spending_only=True,
1534
                nonlocal_only=True)
1535
            end_coins = self.get_utxos(
×
1536
                block_height=end_height,
1537
                confirmed_funding_only=True,
1538
                confirmed_spending_only=True,
1539
                nonlocal_only=True)
1540

1541
            def summary_point(timestamp, height, balance, coins):
×
1542
                date = timestamp_to_datetime(timestamp)
×
1543
                out = {
×
1544
                    'date': date,
1545
                    'block_height': height,
1546
                    'BTC_balance': Satoshis(balance),
1547
                }
1548
                if show_fiat:
×
1549
                    ap = self.acquisition_price(coins, fx.timestamp_rate, fx.ccy)
×
1550
                    lp = self.liquidation_price(coins, fx.timestamp_rate, timestamp)
×
1551
                    out['acquisition_price'] = Fiat(ap, fx.ccy)
×
1552
                    out['liquidation_price'] = Fiat(lp, fx.ccy)
×
1553
                    out['unrealized_gains'] = Fiat(lp - ap, fx.ccy)
×
1554
                    out['fiat_balance'] = Fiat(fx.historical_value(balance, date), fx.ccy)
×
1555
                    out['BTC_fiat_price'] = Fiat(fx.historical_value(COIN, date), fx.ccy)
×
1556
                return out
×
1557

1558
            summary_start = summary_point(start_timestamp, start_height, start_balance, start_coins)
×
1559
            summary_end = summary_point(end_timestamp, end_height, end_balance, end_coins)
×
1560
            flow = {
×
1561
                'BTC_incoming': Satoshis(income),
1562
                'BTC_outgoing': Satoshis(expenditures)
1563
            }
1564
            if show_fiat:
×
1565
                flow['fiat_currency'] = fx.ccy
×
1566
                flow['fiat_incoming'] = Fiat(fiat_income, fx.ccy)
×
1567
                flow['fiat_outgoing'] = Fiat(fiat_expenditures, fx.ccy)
×
1568
                flow['realized_capital_gains'] = Fiat(capital_gains, fx.ccy)
×
1569
            summary = {
×
1570
                'begin': summary_start,
1571
                'end': summary_end,
1572
                'flow': flow,
1573
            }
1574

1575
        else:
1576
            summary = {}
×
NEW
1577
        return summary
×
1578

1579
    def acquisition_price(self, coins, price_func, ccy):
5✔
1580
        return Decimal(sum(self.coin_price(coin.prevout.txid.hex(), price_func, ccy, self.adb.get_txin_value(coin)) for coin in coins))
×
1581

1582
    def liquidation_price(self, coins, price_func, timestamp):
5✔
1583
        p = price_func(timestamp)
×
1584
        return sum([coin.value_sats() for coin in coins]) * p / Decimal(COIN)
×
1585

1586
    def default_fiat_value(self, tx_hash, fx, value_sat):
5✔
1587
        return value_sat / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate)
5✔
1588

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

1616
    def _get_label(self, key: str) -> str:
5✔
1617
        # key is typically: address / txid / LN-payment-hash-hex
1618
        return self._labels.get(key) or ''
×
1619

1620
    def get_label_for_address(self, addr: str) -> str:
5✔
1621
        label = self._labels.get(addr) or ''
×
1622
        if not label and (request := self.get_request_by_addr(addr)):
×
1623
            label = request.get_message()
×
1624
        return label
×
1625

1626
    def set_default_label(self, key:str, value:str):
5✔
1627
        self._default_labels[key] = value
×
1628

1629
    def get_label_for_outpoint(self, outpoint:str) -> str:
5✔
1630
        return self._labels.get(outpoint) or self._get_default_label_for_outpoint(outpoint)
5✔
1631

1632
    def _get_default_label_for_outpoint(self, outpoint: str) -> str:
5✔
1633
        return self._default_labels.get(outpoint)
5✔
1634

1635
    def get_label_for_group(self, group_id: str) -> str:
5✔
NEW
1636
        return self._default_labels.get('group:' + group_id)
×
1637

1638
    def set_group_label(self, group_id: str, label: str):
5✔
NEW
1639
        self._default_labels['group:' + group_id] = label
×
1640

1641
    def get_label_for_txid(self, tx_hash: str) -> str:
5✔
1642
        return self._labels.get(tx_hash) or self._get_default_label_for_txid(tx_hash)
5✔
1643

1644
    def _get_default_label_for_txid(self, tx_hash: str) -> str:
5✔
1645
        if label := self._default_labels.get(tx_hash):
5✔
1646
            return label
×
1647
        labels = []
5✔
1648
        tx = self.adb.get_transaction(tx_hash)
5✔
1649
        if tx:
5✔
1650
            for i in range(len(tx.outputs())):
5✔
1651
                outpoint = tx_hash + f':{i}'
5✔
1652
                if label := self.get_label_for_outpoint(outpoint):
5✔
1653
                    labels.append(label)
×
1654
            for txin in tx.inputs():
5✔
1655
                outpoint = txin.prevout.to_str()
5✔
1656
                if label := self.get_label_for_outpoint(outpoint):
5✔
1657
                    labels.append(label)
×
1658

1659
        # note: we don't deserialize tx as the history calls us for every tx, and that would be slow
1660
        if not self.db.get_txi_addresses(tx_hash):
5✔
1661
            # no inputs are ismine -> likely incoming payment -> concat labels of output addresses
1662
            for addr in self.db.get_txo_addresses(tx_hash):
×
1663
                label = self.get_label_for_address(addr)
×
1664
                if label:
×
1665
                    labels.append(label)
×
1666
        else:
1667
            # some inputs are ismine -> likely outgoing payment
1668
            for invoice in self.get_relevant_invoices_for_tx(tx_hash):
5✔
1669
                if invoice.message:
×
1670
                    labels.append(invoice.message)
×
1671
        #if not labels and self.lnworker and (label:= self.lnworker.get_label_for_txid(tx_hash)):
1672
        #    labels.append(label)
1673
        return ', '.join(labels)
5✔
1674

1675
    def _get_default_label_for_rhash(self, rhash: str) -> str:
5✔
1676
        req = self.get_request(rhash)
×
1677
        return req.get_message() if req else ''
×
1678

1679
    def get_label_for_rhash(self, rhash: str) -> str:
5✔
1680
        return self._labels.get(rhash) or self._get_default_label_for_rhash(rhash)
×
1681

1682
    def get_all_labels(self) -> Dict[str, str]:
5✔
1683
        with self.lock:
×
1684
            return copy.copy(self._labels)
×
1685

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

1727
    def relayfee(self):
5✔
1728
        return relayfee(self.network)
5✔
1729

1730
    def dust_threshold(self):
5✔
1731
        return dust_threshold(self.network)
5✔
1732

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

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

1809
    def get_single_change_address_for_new_transaction(
5✔
1810
            self, preferred_change_addr=None, *, allow_reusing_used_change_addrs: bool = True,
1811
    ) -> Optional[str]:
1812
        addrs = self.get_change_addresses_for_new_transaction(
5✔
1813
            preferred_change_addr=preferred_change_addr,
1814
            allow_reusing_used_change_addrs=allow_reusing_used_change_addrs,
1815
        )
1816
        if addrs:
5✔
1817
            return addrs[0]
5✔
1818
        return None
×
1819

1820
    @check_returned_address_for_corruption
5✔
1821
    def get_new_sweep_address_for_channel(self) -> str:
5✔
1822
        # Recalc and get unused change addresses
1823
        addrs = self.calc_unused_change_addresses()
×
1824
        if addrs:
×
1825
            selected_addr = addrs[0]
×
1826
        else:
1827
            # if there are none, take one randomly from the last few
1828
            addrs = self.get_change_addresses(slice_start=-self.gap_limit_for_change)
×
1829
            if addrs:
×
1830
                selected_addr = random.choice(addrs)
×
1831
            else:  # fallback for e.g. imported wallets
1832
                selected_addr = self.get_receiving_address()
×
1833
        assert is_address(selected_addr), f"not valid bitcoin address: {selected_addr}"
×
1834
        return selected_addr
×
1835

1836
    def can_pay_onchain(self, outputs, coins=None):
5✔
1837
        fee = partial(self.config.estimate_fee, allow_fallback_to_static_rates=True)  # to avoid NoDynamicFeeEstimates
×
1838
        try:
×
1839
            self.make_unsigned_transaction(
×
1840
                coins=coins,
1841
                outputs=outputs,
1842
                fee=fee)
1843
        except NotEnoughFunds:
×
1844
            return False
×
1845
        return True
×
1846

1847
    @profiler(min_threshold=0.1)
5✔
1848
    def make_unsigned_transaction(
5✔
1849
            self, *,
1850
            coins: Sequence[PartialTxInput],
1851
            outputs: List[PartialTxOutput],
1852
            inputs: Optional[List[PartialTxInput]] = None,
1853
            fee=None,
1854
            change_addr: str = None,
1855
            is_sweep: bool = False,  # used by Wallet_2fa subclass
1856
            rbf: Optional[bool] = True,
1857
            BIP69_sort: Optional[bool] = True,
1858
            base_tx: Optional[PartialTransaction] = None,
1859
            send_change_to_lightning: Optional[bool] = None,
1860
    ) -> PartialTransaction:
1861
        """Can raise NotEnoughFunds or NoDynamicFeeEstimates."""
1862

1863
        if not inputs and not coins:  # any bitcoin tx must have at least 1 input by consensus
5✔
1864
            raise NotEnoughFunds()
5✔
1865
        if any([c.already_has_some_signatures() for c in coins]):
5✔
1866
            raise Exception("Some inputs already contain signatures!")
×
1867
        if inputs is None:
5✔
1868
            inputs = []
5✔
1869
        if inputs:
5✔
1870
            input_set = set(txin.prevout for txin in inputs)
×
1871
            coins = [coin for coin in coins if (coin.prevout not in input_set)]
×
1872
        if base_tx is None and self.config.WALLET_BATCH_RBF:
5✔
1873
            base_tx = self.get_unconfirmed_base_tx_for_batching(outputs, coins)
5✔
1874
        if send_change_to_lightning is None:
5✔
1875
            send_change_to_lightning = self.config.WALLET_SEND_CHANGE_TO_LIGHTNING
5✔
1876

1877
        # prevent side-effect with '!'
1878
        outputs = copy.deepcopy(outputs)
5✔
1879

1880
        # check outputs for "max" amount
1881
        i_max = []
5✔
1882
        i_max_sum = 0
5✔
1883
        for i, o in enumerate(outputs):
5✔
1884
            weight = parse_max_spend(o.value)
5✔
1885
            if weight:
5✔
1886
                i_max_sum += weight
5✔
1887
                i_max.append((weight, i))
5✔
1888

1889
        if fee is None and self.config.fee_per_kb() is None:
5✔
1890
            raise NoDynamicFeeEstimates()
×
1891

1892
        for txin in coins:
5✔
1893
            self.add_input_info(txin)
5✔
1894
            nSequence = 0xffffffff - (2 if rbf else 1)
5✔
1895
            txin.nsequence = nSequence
5✔
1896

1897
        # Fee estimator
1898
        if fee is None:
5✔
1899
            fee_estimator = self.config.estimate_fee
×
1900
        elif isinstance(fee, Number):
5✔
1901
            fee_estimator = lambda size: fee
5✔
1902
        elif callable(fee):
5✔
1903
            fee_estimator = fee
5✔
1904
        else:
1905
            raise Exception(f'Invalid argument fee: {fee}')
×
1906

1907
        # set if we merge with another transaction
1908
        rbf_merge_txid = None
5✔
1909

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

1984
            (x,i) = i_max[-1]
5✔
1985
            outputs[i].value += (amount - distr_amount)
5✔
1986
            tx = PartialTransaction.from_io(list(coins), list(outputs))
5✔
1987

1988
        # Timelock tx to current height.
1989
        tx.locktime = get_locktime_for_new_transaction(self.network)
5✔
1990
        tx.rbf_merge_txid = rbf_merge_txid
5✔
1991
        tx.add_info_from_wallet(self)
5✔
1992
        run_hook('make_unsigned_transaction', self, tx)
5✔
1993
        return tx
5✔
1994

1995
    def is_frozen_address(self, addr: str) -> bool:
5✔
1996
        return addr in self._frozen_addresses
×
1997

1998
    def is_frozen_coin(self, utxo: PartialTxInput) -> bool:
5✔
1999
        prevout_str = utxo.prevout.to_str()
5✔
2000
        frozen = self._frozen_coins.get(prevout_str, None)
5✔
2001
        # note: there are three possible states for 'frozen':
2002
        #       True/False if the user explicitly set it,
2003
        #       None otherwise
2004
        if frozen is None:
5✔
2005
            return self._is_coin_small_and_unconfirmed(utxo)
5✔
2006
        return bool(frozen)
×
2007

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

2041
    def set_frozen_state_of_addresses(
5✔
2042
        self,
2043
        addrs: Iterable[str],
2044
        freeze: bool,
2045
        *,
2046
        write_to_disk: bool = True,
2047
    ) -> bool:
2048
        """Set frozen state of the addresses to FREEZE, True or False"""
2049
        if all(self.is_mine(addr) for addr in addrs):
5✔
2050
            with self._freeze_lock:
5✔
2051
                if freeze:
5✔
2052
                    self._frozen_addresses |= set(addrs)
5✔
2053
                else:
2054
                    self._frozen_addresses -= set(addrs)
5✔
2055
                self.db.put('frozen_addresses', list(self._frozen_addresses))
5✔
2056
            util.trigger_callback('status')
5✔
2057
            if write_to_disk:
5✔
2058
                self.save_db()
5✔
2059
            return True
5✔
2060
        return False
×
2061

2062
    def set_frozen_state_of_coins(
5✔
2063
        self,
2064
        utxos: Iterable[str],
2065
        freeze: bool,
2066
        *,
2067
        write_to_disk: bool = True,
2068
    ) -> None:
2069
        """Set frozen state of the utxos to FREEZE, True or False"""
2070
        # basic sanity check that input is not garbage: (see if raises)
2071
        [TxOutpoint.from_str(utxo) for utxo in utxos]
×
2072
        with self._freeze_lock:
×
2073
            for utxo in utxos:
×
2074
                self._frozen_coins[utxo] = bool(freeze)
×
2075
        util.trigger_callback('status')
×
2076
        if write_to_disk:
×
2077
            self.save_db()
×
2078

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

2083
    def set_reserved_state_of_address(self, addr: str, *, reserved: bool) -> None:
5✔
2084
        if not self.is_mine(addr):
5✔
2085
            # silently ignore non-ismine addresses
2086
            return
×
2087
        with self.lock:
5✔
2088
            has_changed = (addr in self._reserved_addresses) != reserved
5✔
2089
            if reserved:
5✔
2090
                self._reserved_addresses.add(addr)
5✔
2091
            else:
2092
                self._reserved_addresses.discard(addr)
×
2093
            if has_changed:
5✔
2094
                self.db.put('reserved_addresses', list(self._reserved_addresses))
×
2095

2096
    def set_reserved_addresses_for_chan(self, chan: 'AbstractChannel', *, reserved: bool) -> None:
5✔
2097
        for addr in chan.get_wallet_addresses_channel_might_want_reserved():
5✔
2098
            self.set_reserved_state_of_address(addr, reserved=reserved)
5✔
2099

2100
    def can_export(self):
5✔
2101
        return not self.is_watching_only() and hasattr(self.keystore, 'get_private_key')
×
2102

2103
    def get_bumpfee_strategies_for_tx(
5✔
2104
        self,
2105
        *,
2106
        tx: Transaction,
2107
    ) -> Tuple[Sequence[BumpFeeStrategy], int]:
2108
        """Returns tuple(list of available strategies, idx of recommended option among those)."""
2109
        all_strats = BumpFeeStrategy.all()
×
2110
        # are we paying max?
2111
        invoices = self.get_relevant_invoices_for_tx(tx.txid())
×
2112
        if len(invoices) == 1 and len(invoices[0].outputs) == 1:
×
2113
            if invoices[0].outputs[0].value == '!':
×
2114
                return all_strats, all_strats.index(BumpFeeStrategy.DECREASE_PAYMENT)
×
2115
        # do not decrease payment if it is a swap
2116
        if self.get_swaps_by_funding_tx(tx):
×
2117
            return [BumpFeeStrategy.PRESERVE_PAYMENT], 0
×
2118
        # default
2119
        return all_strats, all_strats.index(BumpFeeStrategy.PRESERVE_PAYMENT)
×
2120

2121
    def bump_fee(
5✔
2122
            self,
2123
            *,
2124
            tx: Transaction,
2125
            new_fee_rate: Union[int, float, Decimal],
2126
            coins: Sequence[PartialTxInput] = None,
2127
            strategy: BumpFeeStrategy = BumpFeeStrategy.PRESERVE_PAYMENT,
2128
    ) -> PartialTransaction:
2129
        """Increase the miner fee of 'tx'.
2130
        'new_fee_rate' is the target min rate in sat/vbyte
2131
        'coins' is a list of UTXOs we can choose from as potential new inputs to be added
2132

2133
        note: it is the caller's responsibility to have already called tx.add_info_from_network().
2134
              Without that, all txins must be ismine.
2135
        """
2136
        assert tx
5✔
2137
        if not isinstance(tx, PartialTransaction):
5✔
2138
            tx = PartialTransaction.from_tx(tx)
5✔
2139
        assert isinstance(tx, PartialTransaction)
5✔
2140
        tx.remove_signatures()
5✔
2141
        if not tx.is_rbf_enabled():
5✔
2142
            raise CannotBumpFee(_('Transaction is final'))
×
2143
        new_fee_rate = quantize_feerate(new_fee_rate)  # strip excess precision
5✔
2144
        tx.add_info_from_wallet(self)
5✔
2145
        if tx.is_missing_info_from_network():
5✔
2146
            raise Exception("tx missing info from network")
×
2147
        old_tx_size = tx.estimated_size()
5✔
2148
        old_fee = tx.get_fee()
5✔
2149
        assert old_fee is not None
5✔
2150
        old_fee_rate = old_fee / old_tx_size  # sat/vbyte
5✔
2151
        if new_fee_rate <= old_fee_rate:
5✔
2152
            raise CannotBumpFee(_("The new fee rate needs to be higher than the old fee rate."))
×
2153

2154
        if strategy == BumpFeeStrategy.PRESERVE_PAYMENT:
5✔
2155
            # FIXME: we should try decreasing change first,
2156
            # but it requires updating a bunch of unit tests
2157
            try:
5✔
2158
                tx_new = self._bump_fee_through_coinchooser(
5✔
2159
                    tx=tx,
2160
                    new_fee_rate=new_fee_rate,
2161
                    coins=coins,
2162
                )
2163
            except CannotBumpFee as e:
5✔
2164
                tx_new = self._bump_fee_through_decreasing_change(
5✔
2165
                    tx=tx, new_fee_rate=new_fee_rate)
2166
        elif strategy == BumpFeeStrategy.DECREASE_PAYMENT:
5✔
2167
            tx_new = self._bump_fee_through_decreasing_payment(
5✔
2168
                tx=tx, new_fee_rate=new_fee_rate)
2169
        else:
2170
            raise Exception(f"unknown strategy: {strategy=}")
×
2171

2172
        target_min_fee = new_fee_rate * tx_new.estimated_size()
5✔
2173
        actual_fee = tx_new.get_fee()
5✔
2174
        if actual_fee + 1 < target_min_fee:
5✔
2175
            raise CannotBumpFee(
×
2176
                f"bump_fee fee target was not met. "
2177
                f"got {actual_fee}, expected >={target_min_fee}. "
2178
                f"target rate was {new_fee_rate}")
2179
        tx_new.locktime = get_locktime_for_new_transaction(self.network)
5✔
2180
        tx_new.set_rbf(True)
5✔
2181
        tx_new.add_info_from_wallet(self)
5✔
2182
        return tx_new
5✔
2183

2184
    def _bump_fee_through_coinchooser(
5✔
2185
            self,
2186
            *,
2187
            tx: PartialTransaction,
2188
            new_fee_rate: Union[int, Decimal],
2189
            coins: Sequence[PartialTxInput] = None,
2190
    ) -> PartialTransaction:
2191
        """Increase the miner fee of 'tx'.
2192

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

2222
        if coins is None:
5✔
2223
            coins = self.get_spendable_coins(None)
5✔
2224
        # make sure we don't try to spend output from the tx-to-be-replaced:
2225
        coins = [c for c in coins
5✔
2226
                 if c.prevout.txid.hex() not in self.adb.get_conflicting_transactions(tx, include_self=True)]
2227
        for item in coins:
5✔
2228
            self.add_input_info(item)
5✔
2229
        def fee_estimator(size):
5✔
2230
            return self.config.estimate_fee_for_feerate(fee_per_kb=new_fee_rate*1000, size=size)
5✔
2231
        coin_chooser = coinchooser.get_coin_chooser(self.config)
5✔
2232
        try:
5✔
2233
            return coin_chooser.make_tx(
5✔
2234
                coins=coins,
2235
                inputs=old_inputs,
2236
                outputs=fixed_outputs,
2237
                change_addrs=change_addrs,
2238
                fee_estimator_vb=fee_estimator,
2239
                dust_threshold=self.dust_threshold())
2240
        except NotEnoughFunds as e:
5✔
2241
            raise CannotBumpFee(e)
5✔
2242

2243
    def _bump_fee_through_decreasing_change(
5✔
2244
            self,
2245
            *,
2246
            tx: PartialTransaction,
2247
            new_fee_rate: Union[int, Decimal],
2248
    ) -> PartialTransaction:
2249
        """Increase the miner fee of 'tx'.
2250

2251
        - keeps all inputs
2252
        - no new inputs are added
2253
        - change outputs are decreased or removed
2254
        """
2255
        tx = copy.deepcopy(tx)
5✔
2256
        tx.add_info_from_wallet(self)
5✔
2257
        assert tx.get_fee() is not None
5✔
2258
        inputs = tx.inputs()
5✔
2259
        outputs = tx._outputs  # note: we will mutate this directly
5✔
2260

2261
        # use own outputs
2262
        s = list(filter(lambda o: self.is_mine(o.address), outputs))
5✔
2263
        if not s:
5✔
2264
            raise CannotBumpFee('No suitable output')
×
2265

2266
        # prioritize low value outputs, to get rid of dust
2267
        s = sorted(s, key=lambda o: o.value)
5✔
2268
        for o in s:
5✔
2269
            target_fee = int(math.ceil(tx.estimated_size() * new_fee_rate))
5✔
2270
            delta = target_fee - tx.get_fee()
5✔
2271
            if delta <= 0:
5✔
2272
                break
×
2273
            i = outputs.index(o)
5✔
2274
            if o.value - delta >= self.dust_threshold():
5✔
2275
                new_output_value = o.value - delta
5✔
2276
                assert isinstance(new_output_value, int)
5✔
2277
                outputs[i].value = new_output_value
5✔
2278
                delta = 0
5✔
2279
                break
5✔
2280
            else:
2281
                del outputs[i]
5✔
2282
                # note: we mutated the outputs of tx, which will affect
2283
                #       tx.estimated_size() in the next iteration
2284
        else:
2285
            # recompute delta if there was no next iteration
2286
            target_fee = int(math.ceil(tx.estimated_size() * new_fee_rate))
5✔
2287
            delta = target_fee - tx.get_fee()
5✔
2288

2289
        if delta > 0:
5✔
2290
            raise CannotBumpFee(_('Could not find suitable outputs'))
5✔
2291

2292
        return PartialTransaction.from_io(inputs, outputs)
5✔
2293

2294
    def _bump_fee_through_decreasing_payment(
5✔
2295
            self,
2296
            *,
2297
            tx: PartialTransaction,
2298
            new_fee_rate: Union[int, Decimal],
2299
    ) -> PartialTransaction:
2300
        """
2301
        Increase the miner fee of 'tx' by decreasing amount paid.
2302
        This should be used for transactions that pay "Max".
2303

2304
        - keeps all inputs
2305
        - no new inputs are added
2306
        - Each non-ismine output is decreased proportionally to their byte-size.
2307
        """
2308
        tx = copy.deepcopy(tx)
5✔
2309
        tx.add_info_from_wallet(self)
5✔
2310
        assert tx.get_fee() is not None
5✔
2311
        inputs = tx.inputs()
5✔
2312
        outputs = tx.outputs()
5✔
2313

2314
        # select non-ismine outputs
2315
        s = [(idx, out) for (idx, out) in enumerate(outputs)
5✔
2316
             if not self.is_mine(out.address)]
2317
        s = [(idx, out) for (idx, out) in s if self._is_rbf_allowed_to_touch_tx_output(out)]
5✔
2318
        if not s:
5✔
2319
            raise CannotBumpFee("Cannot find payment output")
×
2320

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

2351
        outputs = [out for (idx, out) in enumerate(outputs) if idx not in del_out_idxs]
5✔
2352
        return PartialTransaction.from_io(inputs, outputs)
5✔
2353

2354
    def _is_rbf_allowed_to_touch_tx_output(self, txout: TxOutput) -> bool:
5✔
2355
        # 2fa fee outputs if present, should not be removed or have their value decreased
2356
        if self.is_billing_address(txout.address):
5✔
2357
            return False
×
2358
        # submarine swap funding outputs must not be decreased
2359
        if self.lnworker and self.lnworker.swap_manager.is_lockup_address_for_a_swap(txout.address):
5✔
2360
            return False
×
2361
        return True
5✔
2362

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

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

2397
        note: it is the caller's responsibility to have already called tx.add_info_from_network().
2398
              Without that, all txins must be ismine.
2399
        """
2400
        assert tx
5✔
2401
        if not isinstance(tx, PartialTransaction):
5✔
2402
            tx = PartialTransaction.from_tx(tx)
5✔
2403
        assert isinstance(tx, PartialTransaction)
5✔
2404
        tx.remove_signatures()
5✔
2405

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

2444
    def _add_txinout_derivation_info(self, txinout: Union[PartialTxInput, PartialTxOutput],
5✔
2445
                                     address: str, *, only_der_suffix: bool) -> None:
2446
        pass  # implemented by subclasses
5✔
2447

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

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

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

2512
    def has_support_for_slip_19_ownership_proofs(self) -> bool:
5✔
2513
        return False
×
2514

2515
    def add_slip_19_ownership_proofs_to_tx(self, tx: PartialTransaction) -> None:
5✔
2516
        raise NotImplementedError()
×
2517

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

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

2572
    def add_output_info(self, txout: PartialTxOutput, *, only_der_suffix: bool = False) -> None:
5✔
2573
        address = txout.address
5✔
2574
        if not self.is_mine(address):
5✔
2575
            is_mine = self._learn_derivation_path_for_address_from_txinout(txout, address)
5✔
2576
            if not is_mine:
5✔
2577
                return
5✔
2578
        txout.script_descriptor = self.get_script_descriptor_for_address(address)
5✔
2579
        txout.is_mine = True
5✔
2580
        txout.is_change = self.is_change(address)
5✔
2581
        self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix)
5✔
2582

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

2592
        # check if signing is dangerous
2593
        sh_danger = self.check_sighash(tx)
5✔
2594
        if sh_danger.needs_reject():
5✔
2595
            raise TransactionDangerousException('Not signing transaction:\n' + sh_danger.get_long_message())
5✔
2596
        if sh_danger.needs_confirm() and not ignore_warnings:
5✔
2597
            raise TransactionPotentiallyDangerousException('Not signing transaction:\n' + sh_danger.get_long_message())
5✔
2598

2599
        # sign with make_witness
2600
        for i, txin in enumerate(tx.inputs()):
5✔
2601
            if hasattr(txin, 'make_witness'):
5✔
2602
                self.logger.info(f'sign_transaction: adding witness using make_witness')
×
2603
                privkey = txin.privkey
×
2604
                sig = tx.sign_txin(i, privkey)
×
2605
                txin.script_sig = b''
×
2606
                txin.witness = txin.make_witness(sig)
×
2607
                assert txin.is_complete()
×
2608

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

2629
    def try_detecting_internal_addresses_corruption(self) -> None:
5✔
2630
        pass
×
2631

2632
    def check_address_for_corruption(self, addr: str) -> None:
5✔
2633
        pass
×
2634

2635
    def get_unused_addresses(self) -> Sequence[str]:
5✔
2636
        domain = self.get_receiving_addresses()
5✔
2637
        return [addr for addr in domain if not self.adb.is_used(addr) and not self.get_request_by_addr(addr)]
5✔
2638

2639
    @check_returned_address_for_corruption
5✔
2640
    def get_unused_address(self) -> Optional[str]:
5✔
2641
        """Get an unused receiving address, if there is one.
2642
        Note: there might NOT be one available!
2643
        """
2644
        addrs = self.get_unused_addresses()
5✔
2645
        if addrs:
5✔
2646
            return addrs[0]
5✔
2647

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

2666
    def create_new_address(self, for_change: bool = False):
5✔
2667
        raise UserFacingException("this wallet cannot generate new addresses")
×
2668

2669
    def import_address(self, address: str) -> str:
5✔
2670
        raise UserFacingException("this wallet cannot import addresses")
×
2671

2672
    def import_addresses(self, addresses: List[str], *,
5✔
2673
                         write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:
2674
        raise UserFacingException("this wallet cannot import addresses")
×
2675

2676
    def delete_address(self, address: str) -> None:
5✔
2677
        raise UserFacingException("this wallet cannot delete addresses")
×
2678

2679
    def get_request_URI(self, req: Request) -> Optional[str]:
5✔
2680
        lightning_invoice = None
×
2681
        if self.config.WALLET_BIP21_LIGHTNING:
×
2682
            lightning_invoice = self.get_bolt11_invoice(req)
×
2683
        return req.get_bip21_URI(lightning_invoice=lightning_invoice)
×
2684

2685
    def check_expired_status(self, r: BaseInvoice, status):
5✔
2686
        #if r.is_lightning() and r.exp == 0:
2687
        #    status = PR_EXPIRED  # for BOLT-11 invoices, exp==0 means 0 seconds
2688
        if status == PR_UNPAID and r.has_expired():
5✔
2689
            status = PR_EXPIRED
5✔
2690
        return status
5✔
2691

2692
    def get_invoice_status(self, invoice: BaseInvoice):
5✔
2693
        """Returns status of (incoming) request or (outgoing) invoice."""
2694
        # lightning invoices can be paid onchain
2695
        if invoice.is_lightning() and self.lnworker:
5✔
2696
            status = self.lnworker.get_invoice_status(invoice)
5✔
2697
            if status != PR_UNPAID:
5✔
2698
                return self.check_expired_status(invoice, status)
5✔
2699
        paid, conf = self.is_onchain_invoice_paid(invoice)
5✔
2700
        if not paid:
5✔
2701
            if isinstance(invoice, Invoice):
5✔
2702
                if status:=invoice.get_broadcasting_status():
5✔
2703
                    return status
×
2704
            status = PR_UNPAID
5✔
2705
        elif conf == 0:
5✔
2706
            status = PR_UNCONFIRMED
5✔
2707
        else:
2708
            assert conf >= 1, conf
5✔
2709
            status = PR_PAID
5✔
2710
        return self.check_expired_status(invoice, status)
5✔
2711

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

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

2740
    def get_request(self, request_id: str) -> Optional[Request]:
5✔
2741
        return self._receive_requests.get(request_id)
5✔
2742

2743
    def get_formatted_request(self, request_id):
5✔
2744
        x = self.get_request(request_id)
×
2745
        if x:
×
2746
            return self.export_request(x)
×
2747

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

2771
    def export_invoice(self, x: Invoice) -> Dict[str, Any]:
5✔
2772
        key = x.get_id()
×
2773
        status = self.get_invoice_status(x)
×
2774
        d = x.as_dict(status)
×
2775
        d['invoice_id'] = d.pop('id')
×
2776
        if x.is_lightning():
×
2777
            d['lightning_invoice'] = x.lightning_invoice
×
2778
            if self.lnworker and status == PR_UNPAID:
×
2779
                d['can_pay'] = self.lnworker.can_pay_invoice(x)
×
2780
        else:
2781
            amount_sat = x.get_amount_sat()
×
2782
            assert isinstance(amount_sat, (int, str, type(None)))
×
2783
            d['outputs'] = [y.to_legacy_tuple() for y in x.get_outputs()]
×
2784
            if x.bip70:
×
2785
                d['bip70'] = x.bip70
×
2786
        return d
×
2787

2788
    def get_invoices_and_requests_touched_by_tx(self, tx):
5✔
2789
        request_keys = set()
5✔
2790
        invoice_keys = set()
5✔
2791
        with self.lock, self.transaction_lock:
5✔
2792
            for txo in tx.outputs():
5✔
2793
                addr = txo.address
5✔
2794
                if request:=self.get_request_by_addr(addr):
5✔
2795
                    request_keys.add(request.get_id())
5✔
2796
                for invoice_key in self._invoices_from_scriptpubkey_map.get(txo.scriptpubkey, set()):
5✔
2797
                    invoice_keys.add(invoice_key)
×
2798
        return request_keys, invoice_keys
5✔
2799

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

2816
    def set_broadcasting(self, tx: Transaction, *, broadcasting_status: Optional[int]):
5✔
2817
        request_keys, invoice_keys = self.get_invoices_and_requests_touched_by_tx(tx)
×
2818
        for key in invoice_keys:
×
2819
            invoice = self._invoices.get(key)
×
2820
            if not invoice:
×
2821
                continue
×
2822
            invoice._broadcasting_status = broadcasting_status
×
2823
            status = self.get_invoice_status(invoice)
×
2824
            util.trigger_callback('invoice_status', self, key, status)
×
2825

2826
    def get_bolt11_invoice(self, req: Request) -> str:
5✔
2827
        if not self.lnworker:
×
2828
            return ''
×
2829
        if (payment_hash := req.payment_hash) is None:  # e.g. req might have been generated before enabling LN
×
2830
            return ''
×
2831
        amount_msat = req.get_amount_msat() or None
×
2832
        assert (amount_msat is None or amount_msat > 0), amount_msat
×
2833
        lnaddr, invoice = self.lnworker.get_bolt11_invoice(
×
2834
            payment_hash=payment_hash,
2835
            amount_msat=amount_msat,
2836
            message=req.message,
2837
            expiry=req.exp,
2838
            fallback_address=req.get_address() if self.config.WALLET_BOLT11_FALLBACK else None)
2839
        return invoice
×
2840

2841
    def create_request(self, amount_sat: Optional[int], message: Optional[str], exp_delay: Optional[int], address: Optional[str]):
5✔
2842
        # for receiving
2843
        amount_sat = amount_sat or 0
5✔
2844
        assert isinstance(amount_sat, int), f"{amount_sat!r}"
5✔
2845
        amount_msat = None if not amount_sat else amount_sat * 1000  # amount_sat in [None, 0] implies undefined.
5✔
2846
        message = message or ''
5✔
2847
        address = address or None  # converts "" to None
5✔
2848
        exp_delay = exp_delay or 0
5✔
2849
        timestamp = int(Request._get_cur_time())
5✔
2850
        payment_hash = None  # type: Optional[bytes]
5✔
2851
        if self.has_lightning():
5✔
2852
            payment_hash = self.lnworker.create_payment_info(amount_msat=amount_msat, write_to_disk=False)
5✔
2853
        outputs = [PartialTxOutput.from_address_and_value(address, amount_sat)] if address else []
5✔
2854
        height = self.adb.get_local_height()
5✔
2855
        req = Request(
5✔
2856
            outputs=outputs,
2857
            message=message,
2858
            time=timestamp,
2859
            amount_msat=amount_msat,
2860
            exp=exp_delay,
2861
            height=height,
2862
            bip70=None,
2863
            payment_hash=payment_hash,
2864
        )
2865
        key = self.add_payment_request(req)
5✔
2866
        return key
5✔
2867

2868
    def add_payment_request(self, req: Request, *, write_to_disk: bool = True):
5✔
2869
        request_id = req.get_id()
5✔
2870
        self._receive_requests[request_id] = req
5✔
2871
        if addr:=req.get_address():
5✔
2872
            self._requests_addr_to_key[addr].add(request_id)
5✔
2873
        if write_to_disk:
5✔
2874
            self.save_db()
5✔
2875
        return request_id
5✔
2876

2877
    def delete_request(self, request_id, *, write_to_disk: bool = True):
5✔
2878
        """ lightning or on-chain """
2879
        req = self.get_request(request_id)
×
2880
        if req is None:
×
2881
            return
×
2882
        self._receive_requests.pop(request_id, None)
×
2883
        if addr:=req.get_address():
×
2884
            self._requests_addr_to_key[addr].discard(request_id)
×
2885
        if req.is_lightning() and self.lnworker:
×
2886
            self.lnworker.delete_payment_info(req.rhash)
×
2887
        if write_to_disk:
×
2888
            self.save_db()
×
2889

2890
    def delete_invoice(self, invoice_id, *, write_to_disk: bool = True):
5✔
2891
        """ lightning or on-chain """
2892
        inv = self._invoices.pop(invoice_id, None)
×
2893
        if inv is None:
×
2894
            return
×
2895
        if inv.is_lightning() and self.lnworker:
×
2896
            self.lnworker.delete_payment_info(inv.rhash)
×
2897
        if write_to_disk:
×
2898
            self.save_db()
×
2899

2900
    def get_sorted_requests(self) -> List[Request]:
5✔
2901
        """ sorted by timestamp """
2902
        out = [self.get_request(x) for x in self._receive_requests.keys()]
×
2903
        out = [x for x in out if x is not None]
×
2904
        out.sort(key=lambda x: x.time)
×
2905
        return out
×
2906

2907
    def get_unpaid_requests(self) -> List[Request]:
5✔
2908
        out = [x for x in self._receive_requests.values() if self.get_invoice_status(x) != PR_PAID]
×
2909
        out.sort(key=lambda x: x.time)
×
2910
        return out
×
2911

2912
    def delete_expired_requests(self):
5✔
2913
        keys = [k for k, v in self._receive_requests.items() if self.get_invoice_status(v) == PR_EXPIRED]
×
2914
        self.delete_requests(keys)
×
2915
        return keys
×
2916

2917
    def delete_requests(self, keys):
5✔
2918
        for key in keys:
×
2919
            self.delete_request(key, write_to_disk=False)
×
2920
        if keys:
×
2921
            self.save_db()
×
2922

2923
    @abstractmethod
5✔
2924
    def get_fingerprint(self) -> str:
5✔
2925
        """Returns a string that can be used to identify this wallet.
2926
        Used e.g. by Labels plugin, and LN channel backups.
2927
        Returns empty string "" for wallets that don't have an ID.
2928
        """
2929
        pass
×
2930

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

2934
    def can_import_address(self):
5✔
2935
        return False
×
2936

2937
    def can_delete_address(self):
5✔
2938
        return False
×
2939

2940
    def has_password(self) -> bool:
5✔
2941
        return self.has_keystore_encryption() or self.has_storage_encryption()
5✔
2942

2943
    def can_have_keystore_encryption(self):
5✔
2944
        return self.keystore and self.keystore.may_have_password()
5✔
2945

2946
    def get_available_storage_encryption_version(self) -> StorageEncryptionVersion:
5✔
2947
        """Returns the type of storage encryption offered to the user.
2948

2949
        A wallet file (storage) is either encrypted with this version
2950
        or is stored in plaintext.
2951
        """
2952
        if isinstance(self.keystore, Hardware_KeyStore):
5✔
2953
            return StorageEncryptionVersion.XPUB_PASSWORD
×
2954
        else:
2955
            return StorageEncryptionVersion.USER_PASSWORD
5✔
2956

2957
    def has_keystore_encryption(self) -> bool:
5✔
2958
        """Returns whether encryption is enabled for the keystore.
2959

2960
        If True, e.g. signing a transaction will require a password.
2961
        """
2962
        if self.can_have_keystore_encryption():
5✔
2963
            return bool(self.db.get('use_encryption', False))
5✔
2964
        return False
5✔
2965

2966
    def has_storage_encryption(self) -> bool:
5✔
2967
        """Returns whether encryption is enabled for the wallet file on disk."""
2968
        return bool(self.storage) and self.storage.is_encrypted()
5✔
2969

2970
    @classmethod
5✔
2971
    def may_have_password(cls):
5✔
2972
        return True
×
2973

2974
    def check_password(self, password: Optional[str]) -> None:
5✔
2975
        """Raises an InvalidPassword exception on invalid password"""
2976
        if not self.has_password():
5✔
2977
            if password is not None:
5✔
2978
                raise InvalidPassword("password given but wallet has no password")
5✔
2979
            return
5✔
2980
        if self.has_keystore_encryption():
5✔
2981
            self.keystore.check_password(password)
5✔
2982
        if self.has_storage_encryption():
5✔
2983
            self.storage.check_password(password)
5✔
2984

2985
    def update_password(self, old_pw, new_pw, *, encrypt_storage: bool = True):
5✔
2986
        if old_pw is None and self.has_password():
5✔
2987
            raise InvalidPassword()
×
2988
        self.check_password(old_pw)
5✔
2989
        if self.storage:
5✔
2990
            if encrypt_storage:
5✔
2991
                enc_version = self.get_available_storage_encryption_version()
5✔
2992
            else:
2993
                enc_version = StorageEncryptionVersion.PLAINTEXT
5✔
2994
            self.storage.set_password(new_pw, enc_version)
5✔
2995
        # make sure next storage.write() saves changes
2996
        self.db.set_modified(True)
5✔
2997

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

3013
    @abstractmethod
5✔
3014
    def _update_password_for_keystore(self, old_pw: Optional[str], new_pw: Optional[str]) -> None:
5✔
3015
        pass
×
3016

3017
    def sign_message(self, address: str, message: str, password) -> bytes:
5✔
3018
        index = self.get_address_index(address)
×
3019
        script_type = self.get_txin_type(address)
×
3020
        assert script_type != "address"
×
3021
        return self.keystore.sign_message(index, message, password, script_type=script_type)
×
3022

3023
    def decrypt_message(self, pubkey: str, message, password) -> bytes:
5✔
3024
        addr = self.pubkeys_to_address([pubkey])
×
3025
        index = self.get_address_index(addr)
×
3026
        return self.keystore.decrypt_message(index, message, password)
×
3027

3028
    @abstractmethod
5✔
3029
    def pubkeys_to_address(self, pubkeys: Sequence[str]) -> Optional[str]:
5✔
3030
        pass
×
3031

3032
    def price_at_timestamp(self, txid, price_func):
5✔
3033
        """Returns fiat price of bitcoin at the time tx got confirmed."""
3034
        timestamp = self.adb.get_tx_height(txid).timestamp
5✔
3035
        return price_func(timestamp if timestamp else time.time())
5✔
3036

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

3051
    def clear_coin_price_cache(self):
5✔
3052
        self._coin_price_cache = {}
×
3053

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

3077
    def is_billing_address(self, addr):
5✔
3078
        # overridden for TrustedCoin wallets
3079
        return False
5✔
3080

3081
    @abstractmethod
5✔
3082
    def is_watching_only(self) -> bool:
5✔
3083
        pass
×
3084

3085
    def get_keystore(self) -> Optional[KeyStore]:
5✔
3086
        return self.keystore
×
3087

3088
    def get_keystores(self) -> Sequence[KeyStore]:
5✔
3089
        return [self.keystore] if self.keystore else []
5✔
3090

3091
    @abstractmethod
5✔
3092
    def save_keystore(self):
5✔
3093
        pass
×
3094

3095
    @abstractmethod
5✔
3096
    def has_seed(self) -> bool:
5✔
3097
        pass
×
3098

3099
    def get_seed_type(self) -> Optional[str]:
5✔
3100
        return None
×
3101

3102
    @abstractmethod
5✔
3103
    def get_all_known_addresses_beyond_gap_limit(self) -> Set[str]:
5✔
3104
        pass
×
3105

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

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

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

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

3238
    def get_tx_fee_warning(
5✔
3239
            self, *,
3240
            invoice_amt: int,
3241
            tx_size: int,
3242
            fee: int) -> Optional[Tuple[bool, str, str]]:
3243

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

3275
    def get_help_texts_for_receive_request(self, req: Request) -> ReceiveRequestHelp:
5✔
3276
        key = req.get_id()
×
3277
        addr = req.get_address() or ''
×
3278
        amount_sat = req.get_amount_sat() or 0
×
3279
        address_help = ''
×
3280
        URI_help = ''
×
3281
        ln_help = ''
×
3282
        address_is_error = False
×
3283
        URI_is_error = False
×
3284
        ln_is_error = False
×
3285
        ln_swap_suggestion = None
×
3286
        ln_rebalance_suggestion = None
×
3287
        URI = self.get_request_URI(req) or ''
×
3288
        lightning_online = self.lnworker and self.lnworker.num_peers() > 0
×
3289
        can_receive_lightning = self.lnworker and amount_sat <= self.lnworker.num_sats_can_receive()
×
3290
        status = self.get_invoice_status(req)
×
3291

3292
        if status == PR_EXPIRED:
×
3293
            address_help = URI_help = ln_help = _('This request has expired')
×
3294

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

3310
        if status == PR_UNPAID:
×
3311
            if self.adb.is_used(addr):
×
3312
                address_help = URI_help = (_("This address has already been used. "
×
3313
                                             "For better privacy, do not reuse it for new payments."))
3314
            if req.is_lightning():
×
3315
                if 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
        return ReceiveRequestHelp(
×
3328
            address_help=address_help,
3329
            URI_help=URI_help,
3330
            ln_help=ln_help,
3331
            address_is_error=address_is_error,
3332
            URI_is_error=URI_is_error,
3333
            ln_is_error=ln_is_error,
3334
            ln_rebalance_suggestion=ln_rebalance_suggestion,
3335
            ln_swap_suggestion=ln_swap_suggestion,
3336
        )
3337

3338

3339
    def synchronize(self) -> int:
5✔
3340
        """Returns the number of new addresses we generated."""
3341
        return 0
5✔
3342

3343
    def unlock(self, password):
5✔
3344
        self.logger.info(f'unlocking wallet')
×
3345
        self.check_password(password)
×
3346
        self._password_in_memory = password
×
3347

3348
    def get_unlocked_password(self):
5✔
3349
        return self._password_in_memory
×
3350

3351
    def get_text_not_enough_funds_mentioning_frozen(self) -> str:
5✔
3352
        text = _('Not enough funds')
×
3353
        frozen_str = self.get_frozen_balance_str()
×
3354
        if frozen_str:
×
3355
            text += ' ' + _('({} are frozen)').format(frozen_str)
×
3356
        return text
×
3357

3358
    def get_frozen_balance_str(self) -> Optional[str]:
5✔
3359
        frozen_bal = sum(self.get_frozen_balance())
×
3360
        if not frozen_bal:
×
3361
            return None
×
3362
        return self.config.format_amount_and_units(frozen_bal)
×
3363

3364

3365
class Simple_Wallet(Abstract_Wallet):
5✔
3366
    # wallet with a single keystore
3367

3368
    def is_watching_only(self):
5✔
3369
        return self.keystore.is_watching_only()
5✔
3370

3371
    def _update_password_for_keystore(self, old_pw, new_pw):
5✔
3372
        if self.keystore and self.keystore.may_have_password():
5✔
3373
            self.keystore.update_password(old_pw, new_pw)
5✔
3374
            self.save_keystore()
5✔
3375

3376
    def save_keystore(self):
5✔
3377
        self.db.put('keystore', self.keystore.dump())
5✔
3378

3379
    @abstractmethod
5✔
3380
    def get_public_key(self, address: str) -> Optional[str]:
5✔
3381
        pass
×
3382

3383
    def get_public_keys(self, address: str) -> Sequence[str]:
5✔
3384
        pk = self.get_public_key(address)
×
3385
        return [pk] if pk else []
×
3386

3387

3388
class Imported_Wallet(Simple_Wallet):
5✔
3389
    # wallet made of imported addresses
3390

3391
    wallet_type = 'imported'
5✔
3392
    txin_type = 'address'
5✔
3393

3394
    def __init__(self, db, *, config):
5✔
3395
        Abstract_Wallet.__init__(self, db, config=config)
5✔
3396
        self.use_change = db.get('use_change', False)
5✔
3397

3398
    def is_watching_only(self):
5✔
3399
        return self.keystore is None
5✔
3400

3401
    def can_import_privkey(self):
5✔
3402
        return bool(self.keystore)
5✔
3403

3404
    def load_keystore(self):
5✔
3405
        self.keystore = load_keystore(self.db, 'keystore') if self.db.get('keystore') else None
5✔
3406

3407
    def save_keystore(self):
5✔
3408
        self.db.put('keystore', self.keystore.dump())
5✔
3409

3410
    def can_import_address(self):
5✔
3411
        return self.is_watching_only()
×
3412

3413
    def can_delete_address(self):
5✔
3414
        return True
×
3415

3416
    def has_seed(self):
5✔
3417
        return False
5✔
3418

3419
    def is_deterministic(self):
5✔
3420
        return False
×
3421

3422
    def is_change(self, address):
5✔
3423
        return False
5✔
3424

3425
    def get_all_known_addresses_beyond_gap_limit(self) -> Set[str]:
5✔
3426
        return set()
×
3427

3428
    def get_fingerprint(self):
5✔
3429
        return ''
×
3430

3431
    def get_addresses(self):
5✔
3432
        # note: overridden so that the history can be cleared
3433
        return self.db.get_imported_addresses()
5✔
3434

3435
    def get_receiving_addresses(self, **kwargs):
5✔
3436
        return self.get_addresses()
5✔
3437

3438
    def get_change_addresses(self, **kwargs):
5✔
3439
        return self.get_addresses()
5✔
3440

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

3459
    def import_address(self, address: str) -> str:
5✔
3460
        good_addr, bad_addr = self.import_addresses([address])
5✔
3461
        if good_addr and good_addr[0] == address:
5✔
3462
            return address
5✔
3463
        else:
3464
            raise BitcoinException(str(bad_addr[0][1]))
×
3465

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

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

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

3523
    def is_mine(self, address) -> bool:
5✔
3524
        if not address: return False
5✔
3525
        return self.db.has_imported_address(address)
5✔
3526

3527
    def get_address_index(self, address) -> Optional[str]:
5✔
3528
        # returns None if address is not mine
3529
        return self.get_public_key(address)
5✔
3530

3531
    def get_address_path_str(self, address):
5✔
3532
        return None
×
3533

3534
    def get_public_key(self, address) -> Optional[str]:
5✔
3535
        x = self.db.get_imported_address(address)
5✔
3536
        return x.get('pubkey') if x else None
5✔
3537

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

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

3567
    def get_txin_type(self, address):
5✔
3568
        return self.db.get_imported_address(address).get('type', 'address')
5✔
3569

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

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

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

3605
    def decrypt_message(self, pubkey: str, message, password) -> bytes:
5✔
3606
        # this is significantly faster than the implementation in the superclass
3607
        return self.keystore.decrypt_message(pubkey, message, password)
5✔
3608

3609

3610
class Deterministic_Wallet(Abstract_Wallet):
5✔
3611

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

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

3627
    def has_seed(self):
5✔
3628
        return self.keystore.has_seed()
5✔
3629

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

3637
    def get_receiving_addresses(self, *, slice_start=None, slice_stop=None):
5✔
3638
        return self.db.get_receiving_addresses(slice_start=slice_start, slice_stop=slice_stop)
5✔
3639

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

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

3658
    def check_address_for_corruption(self, addr):
5✔
3659
        if addr and self.is_mine(addr):
5✔
3660
            if addr != self.derive_address(*self.get_address_index(addr)):
5✔
3661
                raise InternalAddressCorruption()
×
3662

3663
    def get_seed(self, password):
5✔
3664
        return self.keystore.get_seed(password)
5✔
3665

3666
    def get_seed_type(self) -> Optional[str]:
5✔
3667
        if not self.has_seed():
×
3668
            return None
×
3669
        assert isinstance(self.keystore, keystore.Deterministic_KeyStore), type(self.keystore)
×
3670
        return self.keystore.get_seed_type()
×
3671

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

3683
    def num_unused_trailing_addresses(self, addresses):
5✔
3684
        k = 0
×
3685
        for addr in addresses[::-1]:
×
3686
            if self.db.get_addr_history(addr):
×
3687
                break
×
3688
            k += 1
×
3689
        return k
×
3690

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

3705
    @abstractmethod
5✔
3706
    def derive_pubkeys(self, c: int, i: int) -> Sequence[str]:
5✔
3707
        pass
×
3708

3709
    def derive_address(self, for_change: int, n: int) -> str:
5✔
3710
        for_change = int(for_change)
5✔
3711
        pubkeys = self.derive_pubkeys(for_change, n)
5✔
3712
        return self.pubkeys_to_address(pubkeys)
5✔
3713

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

3721
    def get_public_keys_with_deriv_info(self, address: str):
5✔
3722
        der_suffix = self.get_address_index(address)
5✔
3723
        der_suffix = [int(x) for x in der_suffix]
5✔
3724
        return {k.derive_pubkey(*der_suffix): (k, der_suffix)
5✔
3725
                for k in self.get_keystores()}
3726

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

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

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

3769
    def synchronize(self):
5✔
3770
        count = 0
5✔
3771
        with self.lock:
5✔
3772
            count += self.synchronize_sequence(False)
5✔
3773
            count += self.synchronize_sequence(True)
5✔
3774
        return count
5✔
3775

3776
    def get_all_known_addresses_beyond_gap_limit(self):
5✔
3777
        # note that we don't stop at first large gap
3778
        found = set()
×
3779

3780
        def process_addresses(addrs, gap_limit):
×
3781
            rolling_num_unused = 0
×
3782
            for addr in addrs:
×
3783
                if self.db.get_addr_history(addr):
×
3784
                    rolling_num_unused = 0
×
3785
                else:
3786
                    if rolling_num_unused >= gap_limit:
×
3787
                        found.add(addr)
×
3788
                    rolling_num_unused += 1
×
3789

3790
        process_addresses(self.get_receiving_addresses(), self.gap_limit)
×
3791
        process_addresses(self.get_change_addresses(), self.gap_limit_for_change)
×
3792
        return found
×
3793

3794
    def get_address_index(self, address) -> Optional[Sequence[int]]:
5✔
3795
        return self.db.get_address_index(address) or self._ephemeral_addr_to_addr_index.get(address)
5✔
3796

3797
    def get_address_path_str(self, address):
5✔
3798
        intpath = self.get_address_index(address)
×
3799
        if intpath is None:
×
3800
            return None
×
3801
        return convert_bip32_intpath_to_strpath(intpath)
×
3802

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

3819
    def get_master_public_keys(self):
5✔
3820
        return [self.get_master_public_key()]
×
3821

3822
    def get_fingerprint(self):
5✔
3823
        return self.get_master_public_key()
5✔
3824

3825
    def get_txin_type(self, address=None):
5✔
3826
        return self.txin_type
5✔
3827

3828

3829
class Standard_Wallet(Simple_Wallet, Deterministic_Wallet):
5✔
3830
    """ Deterministic Wallet with a single pubkey per address """
3831
    wallet_type = 'standard'
5✔
3832

3833
    def __init__(self, db, *, config):
5✔
3834
        Deterministic_Wallet.__init__(self, db, config=config)
5✔
3835

3836
    def get_public_key(self, address):
5✔
3837
        sequence = self.get_address_index(address)
×
3838
        pubkeys = self.derive_pubkeys(*sequence)
×
3839
        return pubkeys[0]
×
3840

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

3849
    def get_master_public_key(self):
5✔
3850
        return self.keystore.get_master_public_key()
5✔
3851

3852
    def derive_pubkeys(self, c, i):
5✔
3853
        return [self.keystore.derive_pubkey(c, i).hex()]
5✔
3854

3855
    def pubkeys_to_address(self, pubkeys):
5✔
3856
        pubkey = pubkeys[0]
5✔
3857
        return bitcoin.pubkey_to_address(self.txin_type, pubkey)
5✔
3858

3859
    def has_support_for_slip_19_ownership_proofs(self) -> bool:
5✔
3860
        return self.keystore.has_support_for_slip_19_ownership_proofs()
×
3861

3862
    def add_slip_19_ownership_proofs_to_tx(self, tx: PartialTransaction) -> None:
5✔
3863
        tx.add_info_from_wallet(self)
×
3864
        self.keystore.add_slip_19_ownership_proofs_to_tx(tx=tx, password=None)
×
3865

3866

3867
class Multisig_Wallet(Deterministic_Wallet):
5✔
3868
    # generic m of n
3869

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

3884
    def get_public_keys(self, address):
5✔
3885
        return [pk.hex() for pk in self.get_public_keys_with_deriv_info(address)]
×
3886

3887
    def pubkeys_to_address(self, pubkeys):
5✔
3888
        redeem_script = self.pubkeys_to_scriptcode(pubkeys)
5✔
3889
        return bitcoin.redeem_script_to_address(self.txin_type, redeem_script)
5✔
3890

3891
    def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> bytes:
5✔
3892
        return transaction.multisig_script(sorted(pubkeys), self.m)
5✔
3893

3894
    def derive_pubkeys(self, c, i):
5✔
3895
        return [k.derive_pubkey(c, i).hex() for k in self.get_keystores()]
5✔
3896

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

3906
    def save_keystore(self):
5✔
3907
        for name, k in self.keystores.items():
×
3908
            self.db.put(name, k.dump())
×
3909

3910
    def get_keystore(self):
5✔
3911
        return self.keystores.get('x1')
×
3912

3913
    def get_keystores(self):
5✔
3914
        return [self.keystores[i] for i in sorted(self.keystores.keys())]
5✔
3915

3916
    def can_have_keystore_encryption(self):
5✔
3917
        return any([k.may_have_password() for k in self.get_keystores()])
×
3918

3919
    def _update_password_for_keystore(self, old_pw, new_pw):
5✔
3920
        for name, keystore in self.keystores.items():
×
3921
            if keystore.may_have_password():
×
3922
                keystore.update_password(old_pw, new_pw)
×
3923
                self.db.put(name, keystore.dump())
×
3924

3925
    def check_password(self, password):
5✔
3926
        for name, keystore in self.keystores.items():
×
3927
            if keystore.may_have_password():
×
3928
                keystore.check_password(password)
×
3929
        if self.has_storage_encryption():
×
3930
            self.storage.check_password(password)
×
3931

3932
    def get_available_storage_encryption_version(self):
5✔
3933
        # multisig wallets are not offered hw device encryption
3934
        return StorageEncryptionVersion.USER_PASSWORD
×
3935

3936
    def has_seed(self):
5✔
3937
        return self.keystore.has_seed()
×
3938

3939
    def is_watching_only(self):
5✔
3940
        return all([k.is_watching_only() for k in self.get_keystores()])
5✔
3941

3942
    def get_master_public_key(self):
5✔
3943
        return self.keystore.get_master_public_key()
×
3944

3945
    def get_master_public_keys(self):
5✔
3946
        return [k.get_master_public_key() for k in self.get_keystores()]
×
3947

3948
    def get_fingerprint(self):
5✔
3949
        return ''.join(sorted(self.get_master_public_keys()))
×
3950

3951

3952
wallet_types = ['standard', 'multisig', 'imported']
5✔
3953

3954
def register_wallet_type(category):
5✔
3955
    wallet_types.append(category)
5✔
3956

3957
wallet_constructors = {
5✔
3958
    'standard': Standard_Wallet,
3959
    'old': Standard_Wallet,
3960
    'xpub': Standard_Wallet,
3961
    'imported': Imported_Wallet
3962
}
3963

3964
def register_constructor(wallet_type, constructor):
5✔
3965
    wallet_constructors[wallet_type] = constructor
5✔
3966

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

3973
    def __new__(self, db: 'WalletDB', *, config: SimpleConfig):
5✔
3974
        wallet_type = db.get('wallet_type')
5✔
3975
        WalletClass = Wallet.wallet_class(wallet_type)
5✔
3976
        wallet = WalletClass(db, config=config)
5✔
3977
        return wallet
5✔
3978

3979
    @staticmethod
5✔
3980
    def wallet_class(wallet_type):
5✔
3981
        if multisig_type(wallet_type):
5✔
3982
            return Multisig_Wallet
5✔
3983
        if wallet_type in wallet_constructors:
5✔
3984
            return wallet_constructors[wallet_type]
5✔
3985
        raise WalletFileException("Unknown wallet type: " + str(wallet_type))
×
3986

3987

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

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

4011

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

© 2025 Coveralls, Inc