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

spesmilo / electrum / 6542897949966336

11 Dec 2024 03:12PM UTC coverage: 60.418% (+0.05%) from 60.37%
6542897949966336

push

CirrusCI

ecdsa
Refactor lnsweep:
 - PartialTxInput has an optional make_witness method
 - SweepInfo has a txin enriched with make_witness
 - SweepInfo has an optional txout, for 1st stage HTLCs
 - sweep transactions are created in lnwatcher

The purpose of this change is to allow combining several
inputs in the same sweep transaction.

29 of 128 new or added lines in 5 files covered. (22.66%)

4 existing lines in 2 files now uncovered.

20124 of 33308 relevant lines covered (60.42%)

3.62 hits per line

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

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

24
# Wallet classes:
25
#   - Imported_Wallet: imported addresses or single keys, 0 or 1 keystore
26
#   - Standard_Wallet: one HD keystore, P2PKH-like scripts
27
#   - Multisig_Wallet: several HD keystores, M-of-N OP_CHECKMULTISIG scripts
28

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

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

53
from .i18n import _
6✔
54
from .bip32 import BIP32Node, convert_bip32_intpath_to_strpath, convert_bip32_strpath_to_intpath
6✔
55
from .crypto import sha256
6✔
56
from . import util
6✔
57
from .util import (NotEnoughFunds, UserCancelled, profiler, OldTaskGroup, ignore_exceptions,
6✔
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
6✔
63
from .bitcoin import COIN, TYPE_ADDRESS
6✔
64
from .bitcoin import is_address, address_to_script, is_minikey, relayfee, dust_threshold
6✔
65
from .bitcoin import DummyAddress, DummyAddressUsedInTxException
6✔
66
from .crypto import sha256d
6✔
67
from . import keystore
6✔
68
from .keystore import (load_keystore, Hardware_KeyStore, KeyStore, KeyStoreWithMPK,
6✔
69
                       AddressIndexGeneric, CannotDerivePubkey)
70
from .util import multisig_type, parse_max_spend
6✔
71
from .storage import StorageEncryptionVersion, WalletStorage
6✔
72
from .wallet_db import WalletDB
6✔
73
from . import transaction, bitcoin, coinchooser, paymentrequest, bip32
6✔
74
from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput,
6✔
75
                          PartialTransaction, PartialTxInput, PartialTxOutput, TxOutpoint, Sighash)
76
from .plugin import run_hook
6✔
77
from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
6✔
78
                                   TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE, TX_TIMESTAMP_INF)
79
from .invoices import BaseInvoice, Invoice, Request
6✔
80
from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_UNCONFIRMED, PR_INFLIGHT
6✔
81
from .contacts import Contacts
6✔
82
from .interface import NetworkException
6✔
83
from .mnemonic import Mnemonic
6✔
84
from .logging import get_logger, Logger
6✔
85
from .lnworker import LNWallet
6✔
86
from .paymentrequest import PaymentRequest
6✔
87
from .util import read_json_file, write_json_file, UserFacingException, FileImportFailed
6✔
88
from .util import EventListener, event_listener
6✔
89
from . import descriptor
6✔
90
from .descriptor import Descriptor
6✔
91

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

98

99
_logger = get_logger(__name__)
6✔
100

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

108

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

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

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

140

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

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

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

174

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

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

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

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

208

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

233

234
class CannotRBFTx(Exception): pass
6✔
235

236

237
class CannotBumpFee(CannotRBFTx):
6✔
238
    def __str__(self):
6✔
239
        return _('Cannot bump fee') + ':\n\n' + Exception.__str__(self)
×
240

241

242
class CannotDoubleSpendTx(CannotRBFTx):
6✔
243
    def __str__(self):
6✔
244
        return _('Cannot cancel transaction') + ':\n\n' + Exception.__str__(self)
×
245

246

247
class CannotCPFP(Exception):
6✔
248
    def __str__(self):
6✔
249
        return _('Cannot create child transaction') + ':\n\n' + Exception.__str__(self)
×
250

251

252
class InternalAddressCorruption(Exception):
6✔
253
    def __str__(self):
6✔
254
        return _("Wallet file corruption detected. "
×
255
                 "Please restore your wallet from seed, and compare the addresses in both files")
256

257

258
class TransactionPotentiallyDangerousException(Exception): pass
6✔
259

260

261
class TransactionDangerousException(TransactionPotentiallyDangerousException): pass
6✔
262

263

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

272

273
class TxSighashDanger:
6✔
274

275
    def __init__(
6✔
276
        self,
277
        *,
278
        risk_level: TxSighashRiskLevel = TxSighashRiskLevel.SAFE,
279
        short_message: str = None,
280
        messages: List[str] = None,
281
    ):
282
        self.risk_level = risk_level
6✔
283
        self.short_message = short_message
6✔
284
        self._messages = messages or []
6✔
285

286
    def needs_confirm(self) -> bool:
6✔
287
        """If True, the user should be prompted for explicit confirmation before signing."""
288
        return self.risk_level >= TxSighashRiskLevel.FEE_WARNING_NEEDCONFIRM
6✔
289

290
    def needs_reject(self) -> bool:
6✔
291
        """If True, the transaction should be rejected, i.e. abort signing."""
292
        return self.risk_level >= TxSighashRiskLevel.INSANE_SIGHASH
6✔
293

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

304
    def combine(*args: 'TxSighashDanger') -> 'TxSighashDanger':
6✔
305
        max_danger = max(args, key=lambda sighash_danger: sighash_danger.risk_level)  # type: TxSighashDanger
6✔
306
        messages = [msg for sighash_danger in args for msg in sighash_danger._messages]
6✔
307
        return TxSighashDanger(
6✔
308
            risk_level=max_danger.risk_level,
309
            short_message=max_danger.short_message,
310
            messages=messages,
311
        )
312

313
    def __repr__(self):
6✔
314
        return (f"<{self.__class__.__name__} risk_level={self.risk_level} "
×
315
                f"short_message={self.short_message!r} _messages={self._messages!r}>")
316

317

318
class BumpFeeStrategy(enum.Enum):
6✔
319
    PRESERVE_PAYMENT = enum.auto()
6✔
320
    DECREASE_PAYMENT = enum.auto()
6✔
321

322
    @classmethod
6✔
323
    def all(cls) -> Sequence['BumpFeeStrategy']:
6✔
324
        return list(BumpFeeStrategy.__members__.values())
×
325

326
    def text(self) -> str:
6✔
327
        if self == self.PRESERVE_PAYMENT:
×
328
            return _('Preserve payment')
×
329
        elif self == self.DECREASE_PAYMENT:
×
330
            return _('Decrease payment')
×
331
        else:
332
            raise Exception(f"unknown strategy: {self=}")
×
333

334

335
class ReceiveRequestHelp(NamedTuple):
6✔
336
    # help texts (warnings/errors):
337
    address_help: str
6✔
338
    URI_help: str
6✔
339
    ln_help: str
6✔
340
    # whether the texts correspond to an error (or just a warning):
341
    address_is_error: bool
6✔
342
    URI_is_error: bool
6✔
343
    ln_is_error: bool
6✔
344

345
    ln_swap_suggestion: Optional[Any] = None
6✔
346
    ln_rebalance_suggestion: Optional[Any] = None
6✔
347

348
    def can_swap(self) -> bool:
6✔
349
        return bool(self.ln_swap_suggestion)
×
350

351
    def can_rebalance(self) -> bool:
6✔
352
        return bool(self.ln_rebalance_suggestion)
×
353

354

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

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

379

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

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

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

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

406
        self.network = None
6✔
407
        self.adb = AddressSynchronizer(db, config, name=self.diagnostic_name())
6✔
408
        for addr in self.get_addresses():
6✔
409
            self.adb.add_address(addr)
6✔
410
        self.lock = self.adb.lock
6✔
411
        self.transaction_lock = self.adb.transaction_lock
6✔
412
        self._last_full_history = None
6✔
413
        self._tx_parents_cache = {}
6✔
414

415
        self.taskgroup = OldTaskGroup()
6✔
416

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

429
        self._freeze_lock = threading.RLock()  # for mutating/iterating frozen_{addresses,coins}
6✔
430

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

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

446
        self.test_addresses_sanity()
6✔
447
        if self.storage and self.has_storage_encryption():
6✔
448
            if (se := self.storage.get_encryption_version()) != (ae := self.get_available_storage_encryption_version()):
6✔
449
                raise WalletFileException(f"unexpected storage encryption type. found: {se!r}. allowed: {ae!r}")
×
450

451
        self.register_callbacks()
6✔
452

453
    def _init_lnworker(self):
6✔
454
        self.lnworker = None
6✔
455

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

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

481
    def save_db(self):
6✔
482
        if self.db.storage:
6✔
483
            self.db.write()
6✔
484

485
    def save_backup(self, backup_dir):
6✔
486
        new_path = os.path.join(backup_dir, self.basename() + '.backup')
×
487
        new_storage = WalletStorage(new_path)
×
488
        new_storage._encryption_version = self.storage._encryption_version
×
489
        new_storage.pubkey = self.storage.pubkey
×
490

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

502
    def has_lightning(self) -> bool:
6✔
503
        return bool(self.lnworker)
6✔
504

505
    def can_have_lightning(self) -> bool:
6✔
506
        # we want static_remotekey to be a wallet address
507
        return self.txin_type == 'p2wpkh'
×
508

509
    def can_have_deterministic_lightning(self) -> bool:
6✔
510
        if not self.can_have_lightning():
×
511
            return False
×
512
        if not self.keystore:
×
513
            return False
×
514
        return self.keystore.can_have_deterministic_lightning_xprv()
×
515

516
    def init_lightning(self, *, password) -> None:
6✔
517
        assert self.can_have_lightning()
×
518
        assert self.db.get('lightning_xprv') is None
×
519
        assert self.db.get('lightning_privkey2') is None
×
520
        if self.can_have_deterministic_lightning():
×
521
            assert isinstance(self.keystore, keystore.BIP32_KeyStore)
×
522
            ln_xprv = self.keystore.get_lightning_xprv(password)
×
523
            self.db.put('lightning_xprv', ln_xprv)
×
524
        else:
525
            seed = os.urandom(32)
×
526
            node = BIP32Node.from_rootseed(seed, xtype='standard')
×
527
            ln_xprv = node.to_xprv()
×
528
            self.db.put('lightning_privkey2', ln_xprv)
×
529
        self.lnworker = LNWallet(self, ln_xprv)
×
530
        self.save_db()
×
531
        if self.network:
×
532
            self._start_network_lightning()
×
533

534
    async def stop(self):
6✔
535
        """Stop all networking and save DB to disk."""
536
        self.unregister_callbacks()
6✔
537
        try:
6✔
538
            async with ignore_after(5):
6✔
539
                if self.network:
6✔
540
                    if self.lnworker:
×
541
                        await self.lnworker.stop()
×
542
                        self.lnworker = None
×
543
                await self.adb.stop()
6✔
544
                await self.taskgroup.cancel_remaining()
6✔
545
        finally:  # even if we get cancelled
546
            if any([ks.is_requesting_to_be_rewritten_to_wallet_file for ks in self.get_keystores()]):
6✔
547
                self.save_keystore()
6✔
548
            self.save_db()
6✔
549

550
    def is_up_to_date(self) -> bool:
6✔
551
        if self.taskgroup.joined:  # either stop() was called, or the taskgroup died
×
552
            return False
×
553
        return self._up_to_date
×
554

555
    def tx_is_related(self, tx):
6✔
556
        is_mine = any([self.is_mine(out.address) for out in tx.outputs()])
6✔
557
        is_mine |= any([self.is_mine(self.adb.get_txin_address(txin)) for txin in tx.inputs()])
6✔
558
        return is_mine
6✔
559

560
    def clear_tx_parents_cache(self):
6✔
561
        with self.lock, self.transaction_lock:
6✔
562
            self._tx_parents_cache.clear()
6✔
563
            self._num_parents.clear()
6✔
564
            self._last_full_history = None
6✔
565

566
    @event_listener
6✔
567
    async def on_event_adb_set_up_to_date(self, adb):
6✔
568
        if self.adb != adb:
6✔
569
            return
6✔
570
        num_new_addrs = await run_in_thread(self.synchronize)
6✔
571
        up_to_date = self.adb.is_up_to_date() and num_new_addrs == 0
6✔
572
        with self.lock:
6✔
573
            status_changed = self._up_to_date != up_to_date
6✔
574
            self._up_to_date = up_to_date
6✔
575
        if up_to_date:
6✔
576
            self.adb.reset_netrequest_counters()  # sync progress indicator
×
577
            self.save_db()
×
578
        # fire triggers
579
        if status_changed or up_to_date:  # suppress False->False transition, as it is spammy
6✔
580
            util.trigger_callback('wallet_updated', self)
×
581
            util.trigger_callback('status')
×
582
        if status_changed:
6✔
583
            self.logger.info(f'set_up_to_date: {up_to_date}')
×
584

585
    @event_listener
6✔
586
    def on_event_adb_added_tx(self, adb, tx_hash: str, tx: Transaction):
6✔
587
        if self.adb != adb:
6✔
588
            return
6✔
589
        if not self.tx_is_related(tx):
6✔
590
            return
6✔
591
        self.clear_tx_parents_cache()
6✔
592
        if self.lnworker:
6✔
593
            self.lnworker.maybe_add_backup_from_tx(tx)
6✔
594
        self._update_invoices_and_reqs_touched_by_tx(tx_hash)
6✔
595
        util.trigger_callback('new_transaction', self, tx)
6✔
596

597
    @event_listener
6✔
598
    def on_event_adb_removed_tx(self, adb, txid: str, tx: Transaction):
6✔
599
        if self.adb != adb:
6✔
600
            return
6✔
601
        if not self.tx_is_related(tx):
6✔
602
            return
×
603
        self.clear_tx_parents_cache()
6✔
604
        util.trigger_callback('removed_transaction', self, tx)
6✔
605

606
    @event_listener
6✔
607
    def on_event_adb_added_verified_tx(self, adb, tx_hash):
6✔
608
        if adb != self.adb:
6✔
609
            return
6✔
610
        self._update_invoices_and_reqs_touched_by_tx(tx_hash)
6✔
611
        tx_mined_status = self.adb.get_tx_height(tx_hash)
6✔
612
        util.trigger_callback('verified', self, tx_hash, tx_mined_status)
6✔
613

614
    @event_listener
6✔
615
    def on_event_adb_removed_verified_tx(self, adb, tx_hash):
6✔
616
        if adb != self.adb:
×
617
            return
×
618
        self._update_invoices_and_reqs_touched_by_tx(tx_hash)
×
619

620
    def clear_history(self):
6✔
621
        self.adb.clear_history()
×
622
        self.save_db()
×
623

624
    def start_network(self, network: 'Network'):
6✔
625
        assert self.network is None, "already started"
6✔
626
        self.network = network
6✔
627
        if network:
6✔
628
            asyncio.run_coroutine_threadsafe(self.main_loop(), self.network.asyncio_loop)
×
629
            self.adb.start_network(network)
×
630
            if self.lnworker:
×
631
                self._start_network_lightning()
×
632

633
    def _start_network_lightning(self):
6✔
634
        assert self.lnworker
×
635
        assert self.lnworker.network is None, 'lnworker network already initialized'
×
636
        self.lnworker.start_network(self.network)
×
637
        # only start gossiping when we already have channels
638
        if self.db.get('channels'):
×
639
            self.network.start_gossip()
×
640

641
    @abstractmethod
6✔
642
    def load_keystore(self) -> None:
6✔
643
        pass
×
644

645
    def diagnostic_name(self):
6✔
646
        return self.basename()
6✔
647

648
    def __str__(self):
6✔
649
        return self.basename()
×
650

651
    def get_master_public_key(self):
6✔
652
        return None
×
653

654
    def get_master_public_keys(self):
6✔
655
        return []
×
656

657
    def basename(self) -> str:
6✔
658
        return self.storage.basename() if self.storage else 'no_name'
6✔
659

660
    def test_addresses_sanity(self) -> None:
6✔
661
        addrs = self.get_receiving_addresses()
6✔
662
        if len(addrs) > 0:
6✔
663
            addr = str(addrs[0])
6✔
664
            if not bitcoin.is_address(addr):
6✔
665
                neutered_addr = addr[:5] + '..' + addr[-2:]
×
666
                raise WalletFileException(f'The addresses in this wallet are not bitcoin addresses.\n'
×
667
                                          f'e.g. {neutered_addr} (length: {len(addr)})')
668

669
    def check_returned_address_for_corruption(func):
6✔
670
        def wrapper(self, *args, **kwargs):
6✔
671
            addr = func(self, *args, **kwargs)
6✔
672
            self.check_address_for_corruption(addr)
6✔
673
            return addr
6✔
674
        return wrapper
6✔
675

676
    def calc_unused_change_addresses(self) -> Sequence[str]:
6✔
677
        """Returns a list of change addresses to choose from, for usage in e.g. new transactions.
678
        The caller should give priority to earlier ones in the list.
679
        """
680
        with self.lock:
6✔
681
            # We want a list of unused change addresses.
682
            # As a performance optimisation, to avoid checking all addresses every time,
683
            # we maintain a list of "not old" addresses ("old" addresses have deeply confirmed history),
684
            # and only check those.
685
            if not hasattr(self, '_not_old_change_addresses'):
6✔
686
                self._not_old_change_addresses = self.get_change_addresses()
6✔
687
            self._not_old_change_addresses = [addr for addr in self._not_old_change_addresses
6✔
688
                                              if not self.adb.address_is_old(addr)]
689
            unused_addrs = [addr for addr in self._not_old_change_addresses
6✔
690
                            if not self.adb.is_used(addr) and not self.is_address_reserved(addr)]
691
            return unused_addrs
6✔
692

693
    def is_deterministic(self) -> bool:
6✔
694
        return self.keystore.is_deterministic()
×
695

696
    def _set_label(self, key: str, value: Optional[str]) -> None:
6✔
697
        with self.lock:
×
698
            if value is None:
×
699
                self._labels.pop(key, None)
×
700
            else:
701
                self._labels[key] = value
×
702

703
    def set_label(self, name: str, text: str = None) -> bool:
6✔
704
        if not name:
6✔
705
            return False
×
706
        changed = False
6✔
707
        with self.lock:
6✔
708
            old_text = self._labels.get(name)
6✔
709
            if text:
6✔
710
                text = text.replace("\n", " ")
×
711
                if old_text != text:
×
712
                    self._labels[name] = text
×
713
                    changed = True
×
714
            else:
715
                if old_text is not None:
6✔
716
                    self._labels.pop(name)
×
717
                    changed = True
×
718
        if changed:
6✔
719
            run_hook('set_label', self, name, text)
×
720
        return changed
6✔
721

722
    def import_labels(self, path):
6✔
723
        data = read_json_file(path)
×
724
        for key, value in data.items():
×
725
            self.set_label(key, value)
×
726

727
    def export_labels(self, path):
6✔
728
        write_json_file(path, self.get_all_labels())
×
729

730
    def set_fiat_value(self, txid, ccy, text, fx, value_sat):
6✔
731
        if not self.db.get_transaction(txid):
6✔
732
            return
×
733
        # since fx is inserting the thousands separator,
734
        # and not util, also have fx remove it
735
        text = fx.remove_thousands_separator(text)
6✔
736
        def_fiat = self.default_fiat_value(txid, fx, value_sat)
6✔
737
        formatted = fx.ccy_amount_str(def_fiat, add_thousands_sep=False)
6✔
738
        def_fiat_rounded = Decimal(formatted)
6✔
739
        reset = not text
6✔
740
        if not reset:
6✔
741
            try:
6✔
742
                text_dec = Decimal(text)
6✔
743
                text_dec_rounded = Decimal(fx.ccy_amount_str(text_dec, add_thousands_sep=False))
6✔
744
                reset = text_dec_rounded == def_fiat_rounded
6✔
745
            except Exception:
6✔
746
                # garbage. not resetting, but not saving either
747
                return False
6✔
748
        if reset:
6✔
749
            d = self.fiat_value.get(ccy, {})
6✔
750
            if d and txid in d:
6✔
751
                d.pop(txid)
6✔
752
            else:
753
                # avoid saving empty dict
754
                return True
6✔
755
        else:
756
            if ccy not in self.fiat_value:
6✔
757
                self.fiat_value[ccy] = {}
6✔
758
            self.fiat_value[ccy][txid] = text
6✔
759
        return reset
6✔
760

761
    def get_fiat_value(self, txid, ccy):
6✔
762
        fiat_value = self.fiat_value.get(ccy, {}).get(txid)
×
763
        try:
×
764
            return Decimal(fiat_value)
×
765
        except Exception:
×
766
            return
×
767

768
    def is_mine(self, address) -> bool:
6✔
769
        if not address: return False
6✔
770
        return bool(self.get_address_index(address))
6✔
771

772
    def is_change(self, address) -> bool:
6✔
773
        if not self.is_mine(address):
6✔
774
            return False
6✔
775
        return self.get_address_index(address)[0] == 1
6✔
776

777
    @abstractmethod
6✔
778
    def get_addresses(self) -> Sequence[str]:
6✔
779
        pass
×
780

781
    @abstractmethod
6✔
782
    def get_address_index(self, address: str) -> Optional[AddressIndexGeneric]:
6✔
783
        pass
×
784

785
    @abstractmethod
6✔
786
    def get_address_path_str(self, address: str) -> Optional[str]:
6✔
787
        """Returns derivation path str such as "m/0/5" to address,
788
        or None if not applicable.
789
        """
790
        pass
×
791

792
    def get_redeem_script(self, address: str) -> Optional[str]:
6✔
793
        desc = self.get_script_descriptor_for_address(address)
×
794
        if desc is None: return None
×
795
        redeem_script = desc.expand().redeem_script
×
796
        if redeem_script:
×
797
            return redeem_script.hex()
×
798

799
    def get_witness_script(self, address: str) -> Optional[str]:
6✔
800
        desc = self.get_script_descriptor_for_address(address)
×
801
        if desc is None: return None
×
802
        witness_script = desc.expand().witness_script
×
803
        if witness_script:
×
804
            return witness_script.hex()
×
805

806
    @abstractmethod
6✔
807
    def get_txin_type(self, address: str) -> str:
6✔
808
        """Return script type of wallet address."""
809
        pass
×
810

811
    def export_private_key(self, address: str, password: Optional[str]) -> str:
6✔
812
        if self.is_watching_only():
6✔
813
            raise UserFacingException(_("This is a watching-only wallet"))
×
814
        if not is_address(address):
6✔
815
            raise UserFacingException(_('Invalid bitcoin address: {}').format(address))
6✔
816
        if not self.is_mine(address):
6✔
817
            raise UserFacingException(_('Address not in wallet: {}').format(address))
6✔
818
        index = self.get_address_index(address)
6✔
819
        pk, compressed = self.keystore.get_private_key(index, password)
6✔
820
        txin_type = self.get_txin_type(address)
6✔
821
        serialized_privkey = bitcoin.serialize_privkey(pk, compressed, txin_type)
6✔
822
        return serialized_privkey
6✔
823

824
    def export_private_key_for_path(self, path: Union[Sequence[int], str], password: Optional[str]) -> str:
6✔
825
        raise UserFacingException("this wallet is not deterministic")
×
826

827
    @abstractmethod
6✔
828
    def get_public_keys(self, address: str) -> Sequence[str]:
6✔
829
        pass
×
830

831
    def get_public_keys_with_deriv_info(self, address: str) -> Dict[bytes, Tuple[KeyStoreWithMPK, Sequence[int]]]:
6✔
832
        """Returns a map: pubkey -> (keystore, derivation_suffix)"""
833
        return {}
×
834

835
    def is_lightning_funding_tx(self, txid: Optional[str]) -> bool:
6✔
836
        if not self.lnworker or txid is None:
6✔
837
            return False
6✔
838
        if any([chan.funding_outpoint.txid == txid
×
839
                for chan in self.lnworker.channels.values()]):
840
            return True
×
841
        if any([chan.funding_outpoint.txid == txid
×
842
                for chan in self.lnworker.channel_backups.values()]):
843
            return True
×
844
        return False
×
845

846
    def get_swap_by_claim_tx(self, tx: Transaction) -> Optional['SwapData']:
6✔
847
        return self.lnworker.swap_manager.get_swap_by_claim_tx(tx) if self.lnworker else None
6✔
848

849
    def get_swaps_by_funding_tx(self, tx: Transaction) -> Iterable['SwapData']:
6✔
850
        return self.lnworker.swap_manager.get_swaps_by_funding_tx(tx) if self.lnworker else []
×
851

852
    def get_wallet_delta(self, tx: Transaction) -> TxWalletDelta:
6✔
853
        """Return the effect a transaction has on the wallet.
854
        This method must use self.is_mine, not self.adb.is_mine()
855
        """
856
        is_relevant = False  # "related to wallet?"
6✔
857
        num_input_ismine = 0
6✔
858
        v_in = v_in_mine = v_out = v_out_mine = 0
6✔
859
        with self.lock, self.transaction_lock:
6✔
860
            for txin in tx.inputs():
6✔
861
                addr = self.adb.get_txin_address(txin)
6✔
862
                value = self.adb.get_txin_value(txin, address=addr)
6✔
863
                if self.is_mine(addr):
6✔
864
                    num_input_ismine += 1
6✔
865
                    is_relevant = True
6✔
866
                    assert value is not None
6✔
867
                    v_in_mine += value
6✔
868
                if value is None:
6✔
869
                    v_in = None
×
870
                elif v_in is not None:
6✔
871
                    v_in += value
6✔
872
            for txout in tx.outputs():
6✔
873
                v_out += txout.value
6✔
874
                if self.is_mine(txout.address):
6✔
875
                    v_out_mine += txout.value
6✔
876
                    is_relevant = True
6✔
877
        delta = v_out_mine - v_in_mine
6✔
878
        if v_in is not None:
6✔
879
            fee = v_in - v_out
6✔
880
        else:
881
            fee = None
×
882
        if fee is None and isinstance(tx, PartialTransaction):
6✔
883
            fee = tx.get_fee()
×
884
        return TxWalletDelta(
6✔
885
            is_relevant=is_relevant,
886
            is_any_input_ismine=num_input_ismine > 0,
887
            is_all_input_ismine=num_input_ismine == len(tx.inputs()),
888
            delta=delta,
889
            fee=fee,
890
        )
891

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

955
        if is_relevant:
6✔
956
            if tx_wallet_delta.is_all_input_ismine:
6✔
957
                assert fee is not None
6✔
958
                amount = tx_wallet_delta.delta + fee
6✔
959
            else:
960
                amount = tx_wallet_delta.delta
×
961
        else:
962
            amount = None
×
963

964
        if is_lightning_funding_tx:
6✔
965
            can_bump = False  # would change txid
×
966

967
        return TxWalletDetails(
6✔
968
            txid=tx_hash,
969
            status=status,
970
            label=label,
971
            can_broadcast=can_broadcast,
972
            can_bump=can_bump,
973
            can_cpfp=can_cpfp,
974
            can_dscancel=can_dscancel,
975
            can_save_as_local=can_save_as_local,
976
            amount=amount,
977
            fee=fee,
978
            tx_mined_status=tx_mined_status,
979
            mempool_depth_bytes=exp_n,
980
            can_remove=can_remove,
981
            is_lightning_funding_tx=is_lightning_funding_tx,
982
            is_related_to_wallet=is_relevant,
983
        )
984

985
    def get_num_parents(self, txid: str) -> Optional[int]:
6✔
986
        if not self.is_up_to_date():
×
987
            return
×
988
        if txid not in self._num_parents:
×
989
            self._num_parents[txid] = len(self.get_tx_parents(txid))
×
990
        return self._num_parents[txid]
×
991

992
    def get_tx_parents(self, txid: str) -> Dict[str, Tuple[List[str], List[str]]]:
6✔
993
        """
994
        returns a flat dict:
995
        txid -> list of parent txids
996
        """
997
        with self.lock, self.transaction_lock:
×
998
            if self._last_full_history is None:
×
999
                self._last_full_history = self.get_full_history(None, include_lightning=False)
×
1000
                # populate cache in chronological order (confirmed tx only)
1001
                # todo: get_full_history should return unconfirmed tx topologically sorted
1002
                for _txid, tx_item in self._last_full_history.items():
×
1003
                    if tx_item['height'] > 0:
×
1004
                        self.get_tx_parents(_txid)
×
1005

1006
            result = self._tx_parents_cache.get(txid, None)
×
1007
            if result is not None:
×
1008
                return result
×
1009
            result = {}   # type: Dict[str, Tuple[List[str], List[str]]]
×
1010
            parents = []  # type: List[str]
×
1011
            uncles = []   # type: List[str]
×
1012
            tx = self.adb.get_transaction(txid)
×
1013
            assert tx, f"cannot find {txid} in db"
×
1014
            for i, txin in enumerate(tx.inputs()):
×
1015
                _txid = txin.prevout.txid.hex()
×
1016
                parents.append(_txid)
×
1017
                # detect address reuse
1018
                addr = self.adb.get_txin_address(txin)
×
1019
                if addr is None:
×
1020
                    continue
×
1021
                received, sent = self.adb.get_addr_io(addr)
×
1022
                if len(sent) > 1:
×
1023
                    my_txid, my_height, my_pos = sent[txin.prevout.to_str()]
×
1024
                    assert my_txid == txid
×
1025
                    for k, v in sent.items():
×
1026
                        if k != txin.prevout.to_str():
×
1027
                            reuse_txid, reuse_height, reuse_pos = v
×
1028
                            if reuse_height <= 0:  # exclude not-yet-mined (we need topological ordering)
×
1029
                                continue
×
1030
                            if (reuse_height, reuse_pos) < (my_height, my_pos):
×
1031
                                uncle_txid, uncle_index = k.split(':')
×
1032
                                uncles.append(uncle_txid)
×
1033

1034
            for _txid in parents + uncles:
×
1035
                if _txid in self._last_full_history.keys():
×
1036
                    result.update(self.get_tx_parents(_txid))
×
1037
            result[txid] = parents, uncles
×
1038
            self._tx_parents_cache[txid] = result
×
1039
            return result
×
1040

1041
    def get_balance(self, **kwargs):
6✔
1042
        domain = self.get_addresses()
6✔
1043
        return self.adb.get_balance(domain, **kwargs)
6✔
1044

1045
    def get_addr_balance(self, address):
6✔
1046
        return self.adb.get_balance([address])
×
1047

1048
    def get_utxos(
6✔
1049
            self,
1050
            domain: Optional[Iterable[str]] = None,
1051
            **kwargs,
1052
    ):
1053
        if domain is None:
6✔
1054
            domain = self.get_addresses()
6✔
1055
        return self.adb.get_utxos(domain=domain, **kwargs)
6✔
1056

1057
    def get_spendable_coins(
6✔
1058
            self,
1059
            domain: Optional[Iterable[str]] = None,
1060
            *,
1061
            nonlocal_only: bool = False,
1062
            confirmed_only: bool = None,
1063
    ) -> Sequence[PartialTxInput]:
1064
        with self._freeze_lock:
6✔
1065
            frozen_addresses = self._frozen_addresses.copy()
6✔
1066
        if confirmed_only is None:
6✔
1067
            confirmed_only = self.config.WALLET_SPEND_CONFIRMED_ONLY
6✔
1068
        utxos = self.get_utxos(
6✔
1069
            domain=domain,
1070
            excluded_addresses=frozen_addresses,
1071
            mature_only=True,
1072
            confirmed_funding_only=confirmed_only,
1073
            nonlocal_only=nonlocal_only,
1074
        )
1075
        utxos = [utxo for utxo in utxos if not self.is_frozen_coin(utxo)]
6✔
1076
        return utxos
6✔
1077

1078
    @abstractmethod
6✔
1079
    def get_receiving_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence[str]:
6✔
1080
        pass
×
1081

1082
    @abstractmethod
6✔
1083
    def get_change_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence[str]:
6✔
1084
        pass
×
1085

1086
    def dummy_address(self):
6✔
1087
        # first receiving address
1088
        return self.get_receiving_addresses(slice_start=0, slice_stop=1)[0]
×
1089

1090
    def get_frozen_balance(self):
6✔
1091
        with self._freeze_lock:
×
1092
            frozen_addresses = self._frozen_addresses.copy()
×
1093
        # note: for coins, use is_frozen_coin instead of _frozen_coins,
1094
        #       as latter only contains *manually* frozen ones
1095
        frozen_coins = {utxo.prevout.to_str() for utxo in self.get_utxos()
×
1096
                        if self.is_frozen_coin(utxo)}
1097
        if not frozen_coins:  # shortcut
×
1098
            return self.adb.get_balance(frozen_addresses)
×
1099
        c1, u1, x1 = self.get_balance()
×
1100
        c2, u2, x2 = self.get_balance(
×
1101
            excluded_addresses=frozen_addresses,
1102
            excluded_coins=frozen_coins,
1103
        )
1104
        return c1-c2, u1-u2, x1-x2
×
1105

1106
    def get_balances_for_piechart(self):
6✔
1107
        # return only positive values
1108
        # todo: add lightning frozen
1109
        c, u, x = self.get_balance()
×
1110
        fc, fu, fx = self.get_frozen_balance()
×
1111
        lightning = self.lnworker.get_balance() if self.has_lightning() else 0
×
1112
        f_lightning = self.lnworker.get_balance(frozen=True) if self.has_lightning() else 0
×
1113
        # subtract frozen funds
1114
        cc = c - fc
×
1115
        uu = u - fu
×
1116
        xx = x - fx
×
1117
        frozen = fc + fu + fx
×
1118
        return cc, uu, xx, frozen, lightning - f_lightning, f_lightning
×
1119

1120
    def balance_at_timestamp(self, domain, target_timestamp):
6✔
1121
        # we assume that get_history returns items ordered by block height
1122
        # we also assume that block timestamps are monotonic (which is false...!)
1123
        h = self.adb.get_history(domain=domain)
×
1124
        balance = 0
×
1125
        for hist_item in h:
×
1126
            balance = hist_item.balance
×
1127
            if hist_item.tx_mined_status.timestamp is None or hist_item.tx_mined_status.timestamp > target_timestamp:
×
1128
                return balance - hist_item.delta
×
1129
        # return last balance
1130
        return balance
×
1131

1132
    def get_onchain_history(self, *, domain=None):
6✔
1133
        if domain is None:
×
1134
            domain = self.get_addresses()
×
1135
        monotonic_timestamp = 0
×
1136
        for hist_item in self.adb.get_history(domain=domain):
×
1137
            monotonic_timestamp = max(monotonic_timestamp, (hist_item.tx_mined_status.timestamp or TX_TIMESTAMP_INF))
×
1138
            d = {
×
1139
                'txid': hist_item.txid,
1140
                'fee_sat': hist_item.fee,
1141
                'height': hist_item.tx_mined_status.height,
1142
                'confirmations': hist_item.tx_mined_status.conf,
1143
                'timestamp': hist_item.tx_mined_status.timestamp,
1144
                'monotonic_timestamp': monotonic_timestamp,
1145
                'incoming': True if hist_item.delta>0 else False,
1146
                'bc_value': Satoshis(hist_item.delta),
1147
                'bc_balance': Satoshis(hist_item.balance),
1148
                'date': timestamp_to_datetime(hist_item.tx_mined_status.timestamp),
1149
                'label': self.get_label_for_txid(hist_item.txid),
1150
                'txpos_in_block': hist_item.tx_mined_status.txpos,
1151
            }
1152
            if wanted_height := hist_item.tx_mined_status.wanted_height:
×
1153
                d['wanted_height'] = wanted_height
×
1154
            yield d
×
1155

1156
    def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> Invoice:
6✔
1157
        height = self.adb.get_local_height()
6✔
1158
        if pr:
6✔
1159
            return Invoice.from_bip70_payreq(pr, height=height)
×
1160
        amount_msat = 0
6✔
1161
        for x in outputs:
6✔
1162
            if parse_max_spend(x.value):
6✔
1163
                amount_msat = '!'
6✔
1164
                break
6✔
1165
            else:
1166
                assert isinstance(x.value, int), f"{x.value!r}"
×
1167
                amount_msat += x.value * 1000
×
1168
        timestamp = None
6✔
1169
        exp = None
6✔
1170
        if URI:
6✔
1171
            timestamp = URI.get('time')
6✔
1172
            exp = URI.get('exp')
6✔
1173
        timestamp = timestamp or int(Invoice._get_cur_time())
6✔
1174
        exp = exp or 0
6✔
1175
        invoice = Invoice(
6✔
1176
            amount_msat=amount_msat,
1177
            message=message,
1178
            time=timestamp,
1179
            exp=exp,
1180
            outputs=outputs,
1181
            bip70=None,
1182
            height=height,
1183
            lightning_invoice=None,
1184
        )
1185
        return invoice
6✔
1186

1187
    def save_invoice(self, invoice: Invoice, *, write_to_disk: bool = True) -> None:
6✔
1188
        key = invoice.get_id()
×
1189
        if not invoice.is_lightning():
×
1190
            if self.is_onchain_invoice_paid(invoice)[0]:
×
1191
                _logger.info("saving invoice... but it is already paid!")
×
1192
            with self.transaction_lock:
×
1193
                for txout in invoice.get_outputs():
×
1194
                    self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(key)
×
1195
        self._invoices[key] = invoice
×
1196
        if write_to_disk:
×
1197
            self.save_db()
×
1198

1199
    def clear_invoices(self):
6✔
1200
        self._invoices.clear()
×
1201
        self.save_db()
×
1202

1203
    def clear_requests(self):
6✔
1204
        self._receive_requests.clear()
×
1205
        self._requests_addr_to_key.clear()
×
1206
        self.save_db()
×
1207

1208
    def get_invoices(self) -> List[Invoice]:
6✔
1209
        out = list(self._invoices.values())
×
1210
        out.sort(key=lambda x:x.time)
×
1211
        return out
×
1212

1213
    def get_unpaid_invoices(self) -> List[Invoice]:
6✔
1214
        invoices = self.get_invoices()
×
1215
        return [x for x in invoices if self.get_invoice_status(x) != PR_PAID]
×
1216

1217
    def get_invoice(self, invoice_id):
6✔
1218
        return self._invoices.get(invoice_id)
×
1219

1220
    def import_requests(self, path):
6✔
1221
        data = read_json_file(path)
×
1222
        for x in data:
×
1223
            try:
×
1224
                req = Request(**x)
×
1225
            except Exception:
×
1226
                raise FileImportFailed(_("Invalid invoice format"))
×
1227
            self.add_payment_request(req, write_to_disk=False)
×
1228
        self.save_db()
×
1229

1230
    def export_requests(self, path):
6✔
1231
        # note: this does not export preimages for LN bolt11 invoices
1232
        write_json_file(path, list(self._receive_requests.values()))
×
1233

1234
    def import_invoices(self, path):
6✔
1235
        data = read_json_file(path)
×
1236
        for x in data:
×
1237
            try:
×
1238
                invoice = Invoice(**x)
×
1239
            except Exception:
×
1240
                raise FileImportFailed(_("Invalid invoice format"))
×
1241
            self.save_invoice(invoice, write_to_disk=False)
×
1242
        self.save_db()
×
1243

1244
    def export_invoices(self, path):
6✔
1245
        write_json_file(path, list(self._invoices.values()))
×
1246

1247
    def get_relevant_invoices_for_tx(self, tx_hash: Optional[str]) -> Sequence[Invoice]:
6✔
1248
        if not tx_hash:
6✔
1249
            return []
×
1250
        invoice_keys = self._invoices_from_txid_map.get(tx_hash, set())
6✔
1251
        invoices = [self.get_invoice(key) for key in invoice_keys]
6✔
1252
        invoices = [inv for inv in invoices if inv]  # filter out None
6✔
1253
        for inv in invoices:
6✔
1254
            assert isinstance(inv, Invoice), f"unexpected type {type(inv)}"
×
1255
        return invoices
6✔
1256

1257
    def _init_requests_rhash_index(self):
6✔
1258
        # self._requests_addr_to_key may contain addresses that can be reused
1259
        # this is checked in get_request_by_address
1260
        self._requests_addr_to_key = defaultdict(set)  # type: Dict[str, Set[str]]
6✔
1261
        for req in self._receive_requests.values():
6✔
1262
            if addr := req.get_address():
6✔
1263
                self._requests_addr_to_key[addr].add(req.get_id())
6✔
1264

1265
    def _prepare_onchain_invoice_paid_detection(self):
6✔
1266
        self._invoices_from_txid_map = defaultdict(set)  # type: Dict[str, Set[str]]
6✔
1267
        self._invoices_from_scriptpubkey_map = defaultdict(set)  # type: Dict[bytes, Set[str]]
6✔
1268
        self._update_onchain_invoice_paid_detection(self._invoices.keys())
6✔
1269

1270
    def _update_onchain_invoice_paid_detection(self, invoice_keys: Iterable[str]) -> None:
6✔
1271
        for invoice_key in invoice_keys:
6✔
1272
            invoice = self._invoices.get(invoice_key)
6✔
1273
            if not invoice:
6✔
1274
                continue
×
1275
            if invoice.is_lightning() and not invoice.get_address():
6✔
1276
                continue
6✔
1277
            if invoice.is_lightning() and self.lnworker and self.lnworker.get_invoice_status(invoice) == PR_PAID:
6✔
1278
                continue
6✔
1279
            is_paid, conf_needed, relevant_txs = self._is_onchain_invoice_paid(invoice)
6✔
1280
            if is_paid:
6✔
1281
                for txid in relevant_txs:
6✔
1282
                    self._invoices_from_txid_map[txid].add(invoice_key)
6✔
1283
            for txout in invoice.get_outputs():
6✔
1284
                self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key)
6✔
1285
            # update invoice status
1286
            status = self.get_invoice_status(invoice)
6✔
1287
            util.trigger_callback('invoice_status', self, invoice_key, status)
6✔
1288

1289
    def _is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional[int], Sequence[str]]:
6✔
1290
        """Returns whether on-chain invoice/request is satisfied, num confs required txs have,
1291
        and list of relevant TXIDs.
1292
        """
1293
        outputs = invoice.get_outputs()
6✔
1294
        if not outputs:  # e.g. lightning-only
6✔
1295
            return False, None, []
×
1296
        invoice_amounts = defaultdict(int)  # type: Dict[bytes, int]  # scriptpubkey -> value_sats
6✔
1297
        for txo in outputs:  # type: PartialTxOutput
6✔
1298
            invoice_amounts[txo.scriptpubkey] += 1 if parse_max_spend(txo.value) else txo.value
6✔
1299
        relevant_txs = set()
6✔
1300
        is_paid = True
6✔
1301
        conf_needed = None  # type: Optional[int]
6✔
1302
        with self.lock, self.transaction_lock:
6✔
1303
            for invoice_scriptpubkey, invoice_amt in invoice_amounts.items():
6✔
1304
                scripthash = bitcoin.script_to_scripthash(invoice_scriptpubkey)
6✔
1305
                prevouts_and_values = self.db.get_prevouts_by_scripthash(scripthash)
6✔
1306
                confs_and_values = []
6✔
1307
                for prevout, v in prevouts_and_values:
6✔
1308
                    relevant_txs.add(prevout.txid.hex())
6✔
1309
                    tx_height = self.adb.get_tx_height(prevout.txid.hex())
6✔
1310
                    if 0 < tx_height.height <= invoice.height:  # exclude txs older than invoice
6✔
1311
                        continue
5✔
1312
                    confs_and_values.append((tx_height.conf or 0, v))
6✔
1313
                # check that there is at least one TXO, and that they pay enough.
1314
                # note: "at least one TXO" check is needed for zero amount invoice (e.g. OP_RETURN)
1315
                vsum = 0
6✔
1316
                for conf, v in reversed(sorted(confs_and_values)):
6✔
1317
                    vsum += v
6✔
1318
                    if vsum >= invoice_amt:
6✔
1319
                        conf_needed = min(conf_needed, conf) if conf_needed is not None else conf
6✔
1320
                        break
6✔
1321
                else:
1322
                    is_paid = False
6✔
1323
        return is_paid, conf_needed, list(relevant_txs)
6✔
1324

1325
    def is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional[int]]:
6✔
1326
        is_paid, conf_needed, relevant_txs = self._is_onchain_invoice_paid(invoice)
6✔
1327
        return is_paid, conf_needed
6✔
1328

1329
    @profiler
6✔
1330
    def get_full_history(self, fx=None, *, onchain_domain=None, include_lightning=True, include_fiat=False):
6✔
1331
        transactions_tmp = OrderedDictWithIndex()
×
1332
        # add on-chain txns
1333
        onchain_history = self.get_onchain_history(domain=onchain_domain)
×
1334
        for tx_item in onchain_history:
×
1335
            txid = tx_item['txid']
×
1336
            transactions_tmp[txid] = tx_item
×
1337
        # add lnworker onchain transactions to transactions_tmp
1338
        # add group_id to tx that are in a group
1339
        lnworker_history = self.lnworker.get_onchain_history() if self.lnworker and include_lightning else {}
×
1340
        for txid, item in lnworker_history.items():
×
1341
            if txid in transactions_tmp:
×
1342
                tx_item = transactions_tmp[txid]
×
1343
                tx_item['group_id'] = item.get('group_id')  # for swaps
×
1344
                tx_item['label'] = item['label']
×
1345
                tx_item['type'] = item['type']
×
1346
                ln_value = Decimal(item['amount_msat']) / 1000   # for channel open/close tx
×
1347
                tx_item['ln_value'] = Satoshis(ln_value)
×
1348
                if channel_id := item.get('channel_id'):
×
1349
                    tx_item['channel_id'] = channel_id
×
1350
            else:
1351
                if item['type'] == 'swap':
×
1352
                    # swap items do not have all the fields. We can skip skip them
1353
                    # because they will eventually be in onchain_history
1354
                    # TODO: use attr.s objects instead of dicts
1355
                    continue
×
1356
                transactions_tmp[txid] = item
×
1357
                ln_value = Decimal(item['amount_msat']) / 1000   # for channel open/close tx
×
1358
                item['ln_value'] = Satoshis(ln_value)
×
1359
        # add lightning_transactions
1360
        lightning_history = self.lnworker.get_lightning_history() if self.lnworker and include_lightning else {}
×
1361
        for tx_item in lightning_history.values():
×
1362
            txid = tx_item.get('txid')
×
1363
            ln_value = Decimal(tx_item['amount_msat']) / 1000
×
1364
            tx_item['lightning'] = True
×
1365
            tx_item['ln_value'] = Satoshis(ln_value)
×
1366
            key = tx_item.get('txid') or tx_item['payment_hash']
×
1367
            transactions_tmp[key] = tx_item
×
1368
        # sort on-chain and LN stuff into new dict, by timestamp
1369
        # (we rely on this being a *stable* sort)
1370
        def sort_key(x):
×
1371
            txid, tx_item = x
×
1372
            ts = tx_item.get('monotonic_timestamp') or tx_item.get('timestamp') or float('inf')
×
1373
            height = self.adb.tx_height_to_sort_height(tx_item.get('height'))
×
1374
            return ts, height
×
1375
        # create groups
1376
        transactions = OrderedDictWithIndex()
×
1377
        for k, tx_item in sorted(list(transactions_tmp.items()), key=sort_key):
×
1378
            group_id = tx_item.get('group_id')
×
1379
            if not group_id:
×
1380
                transactions[k] = tx_item
×
1381
            else:
1382
                key = 'group:' + group_id
×
1383
                parent = transactions.get(key)
×
1384
                label = self.get_label_for_txid(group_id)
×
1385
                if parent is None:
×
1386
                    parent = {
×
1387
                        'label': label,
1388
                        'fiat_value': Fiat(Decimal(0), fx.ccy) if fx else None,
1389
                        'bc_value': Satoshis(0),
1390
                        'ln_value': Satoshis(0),
1391
                        'value': Satoshis(0),
1392
                        'children': [],
1393
                        'timestamp': 0,
1394
                        'date': timestamp_to_datetime(0),
1395
                        'fee_sat': 0,
1396
                    }
1397
                    transactions[key] = parent
×
1398
                if 'bc_value' in tx_item:
×
1399
                    parent['bc_value'] += tx_item['bc_value']
×
1400
                if 'ln_value' in tx_item:
×
1401
                    parent['ln_value'] += tx_item['ln_value']
×
1402
                parent['value'] = parent['bc_value'] + parent['ln_value']
×
1403
                if 'fiat_value' in tx_item:
×
1404
                    parent['fiat_value'] += tx_item['fiat_value']
×
1405
                if tx_item.get('txid') == group_id:
×
1406
                    parent['lightning'] = False
×
1407
                    parent['txid'] = tx_item['txid']
×
1408
                    parent['timestamp'] = tx_item['timestamp']
×
1409
                    parent['date'] = timestamp_to_datetime(tx_item['timestamp'])
×
1410
                    parent['height'] = tx_item['height']
×
1411
                    parent['confirmations'] = tx_item['confirmations']
×
1412
                parent['children'].append(tx_item)
×
1413

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

1437
    @profiler
6✔
1438
    def get_detailed_history(
6✔
1439
            self,
1440
            from_timestamp=None,
1441
            to_timestamp=None,
1442
            fx=None,
1443
            show_addresses=False,
1444
            from_height=None,
1445
            to_height=None):
1446
        # History with capital gains, using utxo pricing
1447
        # FIXME: Lightning capital gains would requires FIFO
1448
        if (from_timestamp is not None or to_timestamp is not None) \
×
1449
                and (from_height is not None or to_height is not None):
1450
            raise UserFacingException('timestamp and block height based filtering cannot be used together')
×
1451

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

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

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

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

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

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

1564
        else:
1565
            summary = {}
×
1566
        return {
×
1567
            'transactions': out,
1568
            'summary': summary
1569
        }
1570

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

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

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

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

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

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

1618
    def get_label_for_txid(self, tx_hash: str) -> str:
6✔
1619
        return self._labels.get(tx_hash) or self._get_default_label_for_txid(tx_hash)
6✔
1620

1621
    def _get_default_label_for_txid(self, tx_hash: str) -> str:
6✔
1622
        labels = []
6✔
1623
        # note: we don't deserialize tx as the history calls us for every tx, and that would be slow
1624
        if not self.db.get_txi_addresses(tx_hash):
6✔
1625
            # no inputs are ismine -> likely incoming payment -> concat labels of output addresses
1626
            for addr in self.db.get_txo_addresses(tx_hash):
×
1627
                label = self.get_label_for_address(addr)
×
1628
                if label:
×
1629
                    labels.append(label)
×
1630
        else:
1631
            # some inputs are ismine -> likely outgoing payment
1632
            for invoice in self.get_relevant_invoices_for_tx(tx_hash):
6✔
1633
                if invoice.message:
×
1634
                    labels.append(invoice.message)
×
1635
        if not labels and self.lnworker and (label:= self.lnworker.get_label_for_txid(tx_hash)):
6✔
1636
            labels.append(label)
×
1637
        return ', '.join(labels)
6✔
1638

1639
    def _get_default_label_for_rhash(self, rhash: str) -> str:
6✔
1640
        req = self.get_request(rhash)
×
1641
        return req.get_message() if req else ''
×
1642

1643
    def get_label_for_rhash(self, rhash: str) -> str:
6✔
1644
        return self._labels.get(rhash) or self._get_default_label_for_rhash(rhash)
×
1645

1646
    def get_all_labels(self) -> Dict[str, str]:
6✔
1647
        with self.lock:
×
1648
            return copy.copy(self._labels)
×
1649

1650
    def get_tx_status(self, tx_hash: str, tx_mined_info: TxMinedInfo):
6✔
1651
        extra = []
6✔
1652
        height = tx_mined_info.height
6✔
1653
        conf = tx_mined_info.conf
6✔
1654
        timestamp = tx_mined_info.timestamp
6✔
1655
        if height == TX_HEIGHT_FUTURE:
6✔
1656
            num_blocks_remainining = tx_mined_info.wanted_height - self.adb.get_local_height()
×
1657
            num_blocks_remainining = max(0, num_blocks_remainining)
×
1658
            return 2, _('in {} blocks').format(num_blocks_remainining)
×
1659
        if conf == 0:
6✔
1660
            tx = self.db.get_transaction(tx_hash)
6✔
1661
            if not tx:
6✔
1662
                return 2, _("unknown")
×
1663
            if not tx.is_complete():
6✔
1664
                tx.add_info_from_wallet(self)  # needed for estimated_size(), for txin size calc
6✔
1665
            fee = self.adb.get_tx_fee(tx_hash)
6✔
1666
            if fee is not None:
6✔
1667
                size = tx.estimated_size()
6✔
1668
                fee_per_byte = fee / size
6✔
1669
                extra.append(format_fee_satoshis(fee_per_byte) + f" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VB}")
6✔
1670
            if fee is not None and height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED) \
6✔
1671
               and self.config.has_fee_mempool():
1672
                exp_n = self.config.fee_to_depth(fee_per_byte)
×
1673
                if exp_n is not None:
×
1674
                    extra.append(self.config.get_depth_mb_str(exp_n))
×
1675
            if height == TX_HEIGHT_LOCAL:
6✔
1676
                status = 3
6✔
1677
            elif height == TX_HEIGHT_UNCONF_PARENT:
×
1678
                status = 1
×
1679
            elif height == TX_HEIGHT_UNCONFIRMED:
×
1680
                status = 0
×
1681
            else:
1682
                status = 2  # not SPV verified
×
1683
        else:
1684
            status = 3 + min(conf, 6)
×
1685
        time_str = format_time(timestamp) if timestamp else _("unknown")
6✔
1686
        status_str = TX_STATUS[status] if status < 4 else time_str
6✔
1687
        if extra:
6✔
1688
            status_str += ' [%s]'%(', '.join(extra))
6✔
1689
        return status, status_str
6✔
1690

1691
    def relayfee(self):
6✔
1692
        return relayfee(self.network)
6✔
1693

1694
    def dust_threshold(self):
6✔
1695
        return dust_threshold(self.network)
6✔
1696

1697
    def get_unconfirmed_base_tx_for_batching(self, outputs, coins) -> Optional[Transaction]:
6✔
1698
        candidate = None
6✔
1699
        domain = self.get_addresses()
6✔
1700
        for hist_item in self.adb.get_history(domain):
6✔
1701
            # tx should not be mined yet
1702
            if hist_item.tx_mined_status.conf > 0: continue
6✔
1703
            # conservative future proofing of code: only allow known unconfirmed types
1704
            if hist_item.tx_mined_status.height not in (TX_HEIGHT_UNCONFIRMED,
6✔
1705
                                                        TX_HEIGHT_UNCONF_PARENT,
1706
                                                        TX_HEIGHT_LOCAL):
1707
                continue
×
1708
            # tx should be "outgoing" from wallet
1709
            if hist_item.delta >= 0:
6✔
1710
                continue
6✔
1711
            tx = self.db.get_transaction(hist_item.txid)
6✔
1712
            if not tx:
6✔
1713
                continue
×
1714
            # is_mine outputs should not be spent yet
1715
            # to avoid cancelling our own dependent transactions
1716
            txid = tx.txid()
6✔
1717
            if any([self.is_mine(o.address) and self.db.get_spent_outpoint(txid, output_idx)
6✔
1718
                    for output_idx, o in enumerate(tx.outputs())]):
1719
                continue
×
1720
            # all inputs should be is_mine
1721
            if not all([self.is_mine(self.adb.get_txin_address(txin)) for txin in tx.inputs()]):
6✔
1722
                continue
×
1723
            # do not mutate LN funding txs, as that would change their txid
1724
            if self.is_lightning_funding_tx(txid):
6✔
1725
                continue
×
1726
            # tx must have opted-in for RBF (even if local, for consistency)
1727
            if not tx.is_rbf_enabled():
6✔
1728
                continue
×
1729
            # reject merge if we need to spend outputs from the base tx
1730
            remaining_amount = sum(c.value_sats() for c in coins if c.prevout.txid.hex() != tx.txid())
6✔
1731
            change_amount = sum(o.value for o in tx.outputs() if self.is_change(o.address))
6✔
1732
            output_amount = sum(o.value for o in outputs)
6✔
1733
            if output_amount > remaining_amount + change_amount:
6✔
1734
                continue
6✔
1735
            # prefer txns already in mempool (vs local)
1736
            if hist_item.tx_mined_status.height == TX_HEIGHT_LOCAL:
6✔
1737
                candidate = tx
×
1738
                continue
×
1739
            return tx
6✔
1740
        return candidate
6✔
1741

1742
    def get_change_addresses_for_new_transaction(
6✔
1743
            self, preferred_change_addr=None, *, allow_reusing_used_change_addrs: bool = True,
1744
    ) -> List[str]:
1745
        change_addrs = []
6✔
1746
        if preferred_change_addr:
6✔
1747
            if isinstance(preferred_change_addr, (list, tuple)):
6✔
1748
                change_addrs = list(preferred_change_addr)
6✔
1749
            else:
1750
                change_addrs = [preferred_change_addr]
×
1751
        elif self.use_change:
6✔
1752
            # Recalc and get unused change addresses
1753
            addrs = self.calc_unused_change_addresses()
6✔
1754
            # New change addresses are created only after a few
1755
            # confirmations.
1756
            if addrs:
6✔
1757
                # if there are any unused, select all
1758
                change_addrs = addrs
6✔
1759
            else:
1760
                # if there are none, take one randomly from the last few
1761
                if not allow_reusing_used_change_addrs:
6✔
1762
                    return []
6✔
1763
                addrs = self.get_change_addresses(slice_start=-self.gap_limit_for_change)
×
1764
                change_addrs = [random.choice(addrs)] if addrs else []
×
1765
        for addr in change_addrs:
6✔
1766
            assert is_address(addr), f"not valid bitcoin address: {addr}"
6✔
1767
            # note that change addresses are not necessarily ismine
1768
            # in which case this is a no-op
1769
            self.check_address_for_corruption(addr)
6✔
1770
        max_change = self.max_change_outputs if self.multiple_change else 1
6✔
1771
        return change_addrs[:max_change]
6✔
1772

1773
    def get_single_change_address_for_new_transaction(
6✔
1774
            self, preferred_change_addr=None, *, allow_reusing_used_change_addrs: bool = True,
1775
    ) -> Optional[str]:
1776
        addrs = self.get_change_addresses_for_new_transaction(
6✔
1777
            preferred_change_addr=preferred_change_addr,
1778
            allow_reusing_used_change_addrs=allow_reusing_used_change_addrs,
1779
        )
1780
        if addrs:
6✔
1781
            return addrs[0]
6✔
1782
        return None
×
1783

1784
    @check_returned_address_for_corruption
6✔
1785
    def get_new_sweep_address_for_channel(self) -> str:
6✔
1786
        # Recalc and get unused change addresses
1787
        addrs = self.calc_unused_change_addresses()
×
1788
        if addrs:
×
1789
            selected_addr = addrs[0]
×
1790
        else:
1791
            # if there are none, take one randomly from the last few
1792
            addrs = self.get_change_addresses(slice_start=-self.gap_limit_for_change)
×
1793
            if addrs:
×
1794
                selected_addr = random.choice(addrs)
×
1795
            else:  # fallback for e.g. imported wallets
1796
                selected_addr = self.get_receiving_address()
×
1797
        assert is_address(selected_addr), f"not valid bitcoin address: {selected_addr}"
×
1798
        return selected_addr
×
1799

1800
    def can_pay_onchain(self, outputs, coins=None):
6✔
1801
        fee = partial(self.config.estimate_fee, allow_fallback_to_static_rates=True)  # to avoid NoDynamicFeeEstimates
×
1802
        try:
×
1803
            self.make_unsigned_transaction(
×
1804
                coins=coins,
1805
                outputs=outputs,
1806
                fee=fee)
1807
        except NotEnoughFunds:
×
1808
            return False
×
1809
        return True
×
1810

1811
    @profiler(min_threshold=0.1)
6✔
1812
    def make_unsigned_transaction(
6✔
1813
            self, *,
1814
            coins: Sequence[PartialTxInput],
1815
            outputs: List[PartialTxOutput],
1816
            inputs: Optional[List[PartialTxInput]] = None,
1817
            fee=None,
1818
            change_addr: str = None,
1819
            is_sweep: bool = False,  # used by Wallet_2fa subclass
1820
            rbf: Optional[bool] = True,
1821
            BIP69_sort: Optional[bool] = True,
1822
            base_tx: Optional[PartialTransaction] = None,
1823
            send_change_to_lightning: Optional[bool] = None,
1824
    ) -> PartialTransaction:
1825
        """Can raise NotEnoughFunds or NoDynamicFeeEstimates."""
1826

1827
        if not coins:  # any bitcoin tx must have at least 1 input by consensus
6✔
1828
            raise NotEnoughFunds()
6✔
1829
        if any([c.already_has_some_signatures() for c in coins]):
6✔
1830
            raise Exception("Some inputs already contain signatures!")
×
1831
        if inputs is None:
6✔
1832
            inputs = []
6✔
1833
        if base_tx is None and self.config.WALLET_BATCH_RBF:
6✔
1834
            base_tx = self.get_unconfirmed_base_tx_for_batching(outputs, coins)
6✔
1835
        if send_change_to_lightning is None:
6✔
1836
            send_change_to_lightning = self.config.WALLET_SEND_CHANGE_TO_LIGHTNING
6✔
1837

1838
        # prevent side-effect with '!'
1839
        outputs = copy.deepcopy(outputs)
6✔
1840

1841
        # check outputs for "max" amount
1842
        i_max = []
6✔
1843
        i_max_sum = 0
6✔
1844
        for i, o in enumerate(outputs):
6✔
1845
            weight = parse_max_spend(o.value)
6✔
1846
            if weight:
6✔
1847
                i_max_sum += weight
6✔
1848
                i_max.append((weight, i))
6✔
1849

1850
        if fee is None and self.config.fee_per_kb() is None:
6✔
1851
            raise NoDynamicFeeEstimates()
×
1852

1853
        for txin in coins:
6✔
1854
            self.add_input_info(txin)
6✔
1855
            nSequence = 0xffffffff - (2 if rbf else 1)
6✔
1856
            txin.nsequence = nSequence
6✔
1857

1858
        # Fee estimator
1859
        if fee is None:
6✔
1860
            fee_estimator = self.config.estimate_fee
×
1861
        elif isinstance(fee, Number):
6✔
1862
            fee_estimator = lambda size: fee
6✔
1863
        elif callable(fee):
6✔
1864
            fee_estimator = fee
6✔
1865
        else:
1866
            raise Exception(f'Invalid argument fee: {fee}')
×
1867

1868
        # set if we merge with another transaction
1869
        rbf_merge_txid = None
6✔
1870

1871
        if len(i_max) == 0:
6✔
1872
            # Let the coin chooser select the coins to spend
1873
            coin_chooser = coinchooser.get_coin_chooser(self.config)
6✔
1874
            # If there is an unconfirmed RBF tx, merge with it
1875
            if base_tx:
6✔
1876
                # make sure we don't try to spend change from the tx-to-be-replaced:
1877
                coins = [c for c in coins if c.prevout.txid.hex() != base_tx.txid()]
6✔
1878
                is_local = self.adb.get_tx_height(base_tx.txid()).height == TX_HEIGHT_LOCAL
6✔
1879
                base_tx = PartialTransaction.from_tx(base_tx)
6✔
1880
                base_tx.add_info_from_wallet(self)
6✔
1881
                base_tx_fee = base_tx.get_fee()
6✔
1882
                base_feerate = Decimal(base_tx_fee)/base_tx.estimated_size()
6✔
1883
                relayfeerate = Decimal(self.relayfee()) / 1000
6✔
1884
                original_fee_estimator = fee_estimator
6✔
1885
                def fee_estimator(size: Union[int, float, Decimal]) -> int:
6✔
1886
                    size = Decimal(size)
6✔
1887
                    lower_bound_relayfee = int(base_tx_fee + round(size * relayfeerate)) if not is_local else 0
6✔
1888
                    lower_bound_feerate = int(base_feerate * size) + 1
6✔
1889
                    lower_bound = max(lower_bound_feerate, lower_bound_relayfee)
6✔
1890
                    return max(lower_bound, original_fee_estimator(size))
6✔
1891
                txi = base_tx.inputs() + list(inputs)
6✔
1892
                txo = list(filter(lambda o: not self.is_change(o.address), base_tx.outputs())) + list(outputs)
6✔
1893
                old_change_addrs = [o.address for o in base_tx.outputs() if self.is_change(o.address)]
6✔
1894
                rbf_merge_txid = base_tx.txid()
6✔
1895
            else:
1896
                txi = list(inputs)
6✔
1897
                txo = list(outputs)
6✔
1898
                old_change_addrs = []
6✔
1899
            # change address. if empty, coin_chooser will set it
1900
            change_addrs = self.get_change_addresses_for_new_transaction(change_addr or old_change_addrs)
6✔
1901
            if self.config.WALLET_MERGE_DUPLICATE_OUTPUTS:
6✔
1902
                txo = transaction.merge_duplicate_tx_outputs(txo)
6✔
1903
            tx = coin_chooser.make_tx(
6✔
1904
                coins=coins,
1905
                inputs=txi,
1906
                outputs=txo,
1907
                change_addrs=change_addrs,
1908
                fee_estimator_vb=fee_estimator,
1909
                dust_threshold=self.dust_threshold(),
1910
                BIP69_sort=BIP69_sort)
1911
            if self.lnworker and send_change_to_lightning:
6✔
1912
                change = tx.get_change_outputs()
×
1913
                # do not use multiple change addresses
1914
                if len(change) == 1:
×
1915
                    amount = change[0].value
×
1916
                    if amount <= self.lnworker.num_sats_can_receive():
×
1917
                        tx.replace_output_address(change[0].address, DummyAddress.SWAP)
×
1918
        else:
1919
            # "spend max" branch
1920
            # note: This *will* spend inputs with negative effective value (if there are any).
1921
            #       Given as the user is spending "max", and so might be abandoning the wallet,
1922
            #       try to include all UTXOs, otherwise leftover might remain in the UTXO set
1923
            #       forever. see #5433
1924
            # note: Actually, it might be the case that not all UTXOs from the wallet are
1925
            #       being spent if the user manually selected UTXOs.
1926
            sendable = sum(map(lambda c: c.value_sats(), coins))
6✔
1927
            for (_,i) in i_max:
6✔
1928
                outputs[i].value = 0
6✔
1929
            tx = PartialTransaction.from_io(list(coins), list(outputs))
6✔
1930
            fee = fee_estimator(tx.estimated_size())
6✔
1931
            amount = sendable - tx.output_value() - fee
6✔
1932
            if amount < 0:
6✔
1933
                raise NotEnoughFunds()
×
1934
            distr_amount = 0
6✔
1935
            for (weight, i) in i_max:
6✔
1936
                val = int((amount/i_max_sum) * weight)
6✔
1937
                outputs[i].value = val
6✔
1938
                distr_amount += val
6✔
1939

1940
            (x,i) = i_max[-1]
6✔
1941
            outputs[i].value += (amount - distr_amount)
6✔
1942
            tx = PartialTransaction.from_io(list(coins), list(outputs))
6✔
1943

1944
        # Timelock tx to current height.
1945
        tx.locktime = get_locktime_for_new_transaction(self.network)
6✔
1946
        tx.rbf_merge_txid = rbf_merge_txid
6✔
1947
        tx.add_info_from_wallet(self)
6✔
1948
        run_hook('make_unsigned_transaction', self, tx)
6✔
1949
        return tx
6✔
1950

1951
    def is_frozen_address(self, addr: str) -> bool:
6✔
1952
        return addr in self._frozen_addresses
×
1953

1954
    def is_frozen_coin(self, utxo: PartialTxInput) -> bool:
6✔
1955
        prevout_str = utxo.prevout.to_str()
6✔
1956
        frozen = self._frozen_coins.get(prevout_str, None)
6✔
1957
        # note: there are three possible states for 'frozen':
1958
        #       True/False if the user explicitly set it,
1959
        #       None otherwise
1960
        if frozen is None:
6✔
1961
            return self._is_coin_small_and_unconfirmed(utxo)
6✔
1962
        return bool(frozen)
×
1963

1964
    def _is_coin_small_and_unconfirmed(self, utxo: PartialTxInput) -> bool:
6✔
1965
        """If true, the coin should not be spent.
1966
        The idea here is that an attacker might send us a UTXO in a
1967
        large low-fee unconfirmed tx that will ~never confirm. If we
1968
        spend it as part of a tx ourselves, that too will not confirm
1969
        (unless we use a high fee, but that might not be worth it for
1970
        a small value UTXO).
1971
        In particular, this test triggers for large "dusting transactions"
1972
        that are used for advertising purposes by some entities.
1973
        see #6960
1974
        """
1975
        # confirmed UTXOs are fine; check this first for performance:
1976
        block_height = utxo.block_height
6✔
1977
        assert block_height is not None
6✔
1978
        if block_height > 0:
6✔
1979
            return False
×
1980
        # exempt large value UTXOs
1981
        value_sats = utxo.value_sats()
6✔
1982
        assert value_sats is not None
6✔
1983
        threshold = self.config.WALLET_UNCONF_UTXO_FREEZE_THRESHOLD_SAT
6✔
1984
        if value_sats >= threshold:
6✔
1985
            return False
6✔
1986
        # if funding tx has any is_mine input, then UTXO is fine
1987
        funding_tx = self.db.get_transaction(utxo.prevout.txid.hex())
6✔
1988
        if funding_tx is None:
6✔
1989
            # we should typically have the funding tx available;
1990
            # might not have it e.g. while not up_to_date
1991
            return True
×
1992
        if any(self.is_mine(self.adb.get_txin_address(txin))
6✔
1993
               for txin in funding_tx.inputs()):
1994
            return False
6✔
1995
        return True
×
1996

1997
    def set_frozen_state_of_addresses(
6✔
1998
        self,
1999
        addrs: Iterable[str],
2000
        freeze: bool,
2001
        *,
2002
        write_to_disk: bool = True,
2003
    ) -> bool:
2004
        """Set frozen state of the addresses to FREEZE, True or False"""
2005
        if all(self.is_mine(addr) for addr in addrs):
6✔
2006
            with self._freeze_lock:
6✔
2007
                if freeze:
6✔
2008
                    self._frozen_addresses |= set(addrs)
6✔
2009
                else:
2010
                    self._frozen_addresses -= set(addrs)
6✔
2011
                self.db.put('frozen_addresses', list(self._frozen_addresses))
6✔
2012
            util.trigger_callback('status')
6✔
2013
            if write_to_disk:
6✔
2014
                self.save_db()
6✔
2015
            return True
6✔
2016
        return False
×
2017

2018
    def set_frozen_state_of_coins(
6✔
2019
        self,
2020
        utxos: Iterable[str],
2021
        freeze: bool,
2022
        *,
2023
        write_to_disk: bool = True,
2024
    ) -> None:
2025
        """Set frozen state of the utxos to FREEZE, True or False"""
2026
        # basic sanity check that input is not garbage: (see if raises)
2027
        [TxOutpoint.from_str(utxo) for utxo in utxos]
×
2028
        with self._freeze_lock:
×
2029
            for utxo in utxos:
×
2030
                self._frozen_coins[utxo] = bool(freeze)
×
2031
        util.trigger_callback('status')
×
2032
        if write_to_disk:
×
2033
            self.save_db()
×
2034

2035
    def is_address_reserved(self, addr: str) -> bool:
6✔
2036
        # note: atm 'reserved' status is only taken into consideration for 'change addresses'
2037
        return addr in self._reserved_addresses
6✔
2038

2039
    def set_reserved_state_of_address(self, addr: str, *, reserved: bool) -> None:
6✔
2040
        if not self.is_mine(addr):
6✔
2041
            # silently ignore non-ismine addresses
2042
            return
×
2043
        with self.lock:
6✔
2044
            has_changed = (addr in self._reserved_addresses) != reserved
6✔
2045
            if reserved:
6✔
2046
                self._reserved_addresses.add(addr)
6✔
2047
            else:
2048
                self._reserved_addresses.discard(addr)
×
2049
            if has_changed:
6✔
2050
                self.db.put('reserved_addresses', list(self._reserved_addresses))
×
2051

2052
    def set_reserved_addresses_for_chan(self, chan: 'AbstractChannel', *, reserved: bool) -> None:
6✔
2053
        for addr in chan.get_wallet_addresses_channel_might_want_reserved():
6✔
2054
            self.set_reserved_state_of_address(addr, reserved=reserved)
6✔
2055

2056
    def can_export(self):
6✔
2057
        return not self.is_watching_only() and hasattr(self.keystore, 'get_private_key')
×
2058

2059
    def get_bumpfee_strategies_for_tx(
6✔
2060
        self,
2061
        *,
2062
        tx: Transaction,
2063
    ) -> Tuple[Sequence[BumpFeeStrategy], int]:
2064
        """Returns tuple(list of available strategies, idx of recommended option among those)."""
2065
        all_strats = BumpFeeStrategy.all()
×
2066
        # are we paying max?
2067
        invoices = self.get_relevant_invoices_for_tx(tx.txid())
×
2068
        if len(invoices) == 1 and len(invoices[0].outputs) == 1:
×
2069
            if invoices[0].outputs[0].value == '!':
×
2070
                return all_strats, all_strats.index(BumpFeeStrategy.DECREASE_PAYMENT)
×
2071
        # do not decrease payment if it is a swap
2072
        if self.get_swaps_by_funding_tx(tx):
×
2073
            return [BumpFeeStrategy.PRESERVE_PAYMENT], 0
×
2074
        # default
2075
        return all_strats, all_strats.index(BumpFeeStrategy.PRESERVE_PAYMENT)
×
2076

2077
    def bump_fee(
6✔
2078
            self,
2079
            *,
2080
            tx: Transaction,
2081
            new_fee_rate: Union[int, float, Decimal],
2082
            coins: Sequence[PartialTxInput] = None,
2083
            strategy: BumpFeeStrategy = BumpFeeStrategy.PRESERVE_PAYMENT,
2084
    ) -> PartialTransaction:
2085
        """Increase the miner fee of 'tx'.
2086
        'new_fee_rate' is the target min rate in sat/vbyte
2087
        'coins' is a list of UTXOs we can choose from as potential new inputs to be added
2088

2089
        note: it is the caller's responsibility to have already called tx.add_info_from_network().
2090
              Without that, all txins must be ismine.
2091
        """
2092
        assert tx
6✔
2093
        if not isinstance(tx, PartialTransaction):
6✔
2094
            tx = PartialTransaction.from_tx(tx)
6✔
2095
        assert isinstance(tx, PartialTransaction)
6✔
2096
        tx.remove_signatures()
6✔
2097
        if not tx.is_rbf_enabled():
6✔
2098
            raise CannotBumpFee(_('Transaction is final'))
×
2099
        new_fee_rate = quantize_feerate(new_fee_rate)  # strip excess precision
6✔
2100
        tx.add_info_from_wallet(self)
6✔
2101
        if tx.is_missing_info_from_network():
6✔
2102
            raise Exception("tx missing info from network")
×
2103
        old_tx_size = tx.estimated_size()
6✔
2104
        old_fee = tx.get_fee()
6✔
2105
        assert old_fee is not None
6✔
2106
        old_fee_rate = old_fee / old_tx_size  # sat/vbyte
6✔
2107
        if new_fee_rate <= old_fee_rate:
6✔
2108
            raise CannotBumpFee(_("The new fee rate needs to be higher than the old fee rate."))
×
2109

2110
        if strategy == BumpFeeStrategy.PRESERVE_PAYMENT:
6✔
2111
            # FIXME: we should try decreasing change first,
2112
            # but it requires updating a bunch of unit tests
2113
            try:
6✔
2114
                tx_new = self._bump_fee_through_coinchooser(
6✔
2115
                    tx=tx,
2116
                    new_fee_rate=new_fee_rate,
2117
                    coins=coins,
2118
                )
2119
            except CannotBumpFee as e:
6✔
2120
                tx_new = self._bump_fee_through_decreasing_change(
6✔
2121
                    tx=tx, new_fee_rate=new_fee_rate)
2122
        elif strategy == BumpFeeStrategy.DECREASE_PAYMENT:
6✔
2123
            tx_new = self._bump_fee_through_decreasing_payment(
6✔
2124
                tx=tx, new_fee_rate=new_fee_rate)
2125
        else:
2126
            raise Exception(f"unknown strategy: {strategy=}")
×
2127

2128
        target_min_fee = new_fee_rate * tx_new.estimated_size()
6✔
2129
        actual_fee = tx_new.get_fee()
6✔
2130
        if actual_fee + 1 < target_min_fee:
6✔
2131
            raise CannotBumpFee(
×
2132
                f"bump_fee fee target was not met. "
2133
                f"got {actual_fee}, expected >={target_min_fee}. "
2134
                f"target rate was {new_fee_rate}")
2135
        tx_new.locktime = get_locktime_for_new_transaction(self.network)
6✔
2136
        tx_new.set_rbf(True)
6✔
2137
        tx_new.add_info_from_wallet(self)
6✔
2138
        return tx_new
6✔
2139

2140
    def _bump_fee_through_coinchooser(
6✔
2141
            self,
2142
            *,
2143
            tx: PartialTransaction,
2144
            new_fee_rate: Union[int, Decimal],
2145
            coins: Sequence[PartialTxInput] = None,
2146
    ) -> PartialTransaction:
2147
        """Increase the miner fee of 'tx'.
2148

2149
        - keeps all inputs
2150
        - keeps all not is_mine outputs,
2151
        - allows adding new inputs
2152
        """
2153
        tx = copy.deepcopy(tx)
6✔
2154
        tx.add_info_from_wallet(self)
6✔
2155
        assert tx.get_fee() is not None
6✔
2156
        old_inputs = list(tx.inputs())
6✔
2157
        old_outputs = list(tx.outputs())
6✔
2158
        # change address
2159
        old_change_addrs = [o.address for o in old_outputs if self.is_change(o.address)]
6✔
2160
        change_addrs = self.get_change_addresses_for_new_transaction(old_change_addrs)
6✔
2161
        # which outputs to keep?
2162
        if old_change_addrs:
6✔
2163
            fixed_outputs = list(filter(lambda o: not self.is_change(o.address), old_outputs))
6✔
2164
        else:
2165
            if all(self.is_mine(o.address) for o in old_outputs):
6✔
2166
                # all outputs are is_mine and none of them are change.
2167
                # we bail out as it's unclear what the user would want!
2168
                # the coinchooser bump fee method is probably not a good idea in this case
2169
                raise CannotBumpFee(_('All outputs are non-change is_mine'))
6✔
2170
            old_not_is_mine = list(filter(lambda o: not self.is_mine(o.address), old_outputs))
6✔
2171
            if old_not_is_mine:
6✔
2172
                fixed_outputs = old_not_is_mine
6✔
2173
            else:
2174
                fixed_outputs = old_outputs
×
2175
        if not fixed_outputs:
6✔
2176
            raise CannotBumpFee(_('Could not figure out which outputs to keep'))
6✔
2177

2178
        if coins is None:
6✔
2179
            coins = self.get_spendable_coins(None)
6✔
2180
        # make sure we don't try to spend output from the tx-to-be-replaced:
2181
        coins = [c for c in coins
6✔
2182
                 if c.prevout.txid.hex() not in self.adb.get_conflicting_transactions(tx, include_self=True)]
2183
        for item in coins:
6✔
2184
            self.add_input_info(item)
6✔
2185
        def fee_estimator(size):
6✔
2186
            return self.config.estimate_fee_for_feerate(fee_per_kb=new_fee_rate*1000, size=size)
6✔
2187
        coin_chooser = coinchooser.get_coin_chooser(self.config)
6✔
2188
        try:
6✔
2189
            return coin_chooser.make_tx(
6✔
2190
                coins=coins,
2191
                inputs=old_inputs,
2192
                outputs=fixed_outputs,
2193
                change_addrs=change_addrs,
2194
                fee_estimator_vb=fee_estimator,
2195
                dust_threshold=self.dust_threshold())
2196
        except NotEnoughFunds as e:
6✔
2197
            raise CannotBumpFee(e)
6✔
2198

2199
    def _bump_fee_through_decreasing_change(
6✔
2200
            self,
2201
            *,
2202
            tx: PartialTransaction,
2203
            new_fee_rate: Union[int, Decimal],
2204
    ) -> PartialTransaction:
2205
        """Increase the miner fee of 'tx'.
2206

2207
        - keeps all inputs
2208
        - no new inputs are added
2209
        - change outputs are decreased or removed
2210
        """
2211
        tx = copy.deepcopy(tx)
6✔
2212
        tx.add_info_from_wallet(self)
6✔
2213
        assert tx.get_fee() is not None
6✔
2214
        inputs = tx.inputs()
6✔
2215
        outputs = tx._outputs  # note: we will mutate this directly
6✔
2216

2217
        # use own outputs
2218
        s = list(filter(lambda o: self.is_mine(o.address), outputs))
6✔
2219
        if not s:
6✔
2220
            raise CannotBumpFee('No suitable output')
×
2221

2222
        # prioritize low value outputs, to get rid of dust
2223
        s = sorted(s, key=lambda o: o.value)
6✔
2224
        for o in s:
6✔
2225
            target_fee = int(math.ceil(tx.estimated_size() * new_fee_rate))
6✔
2226
            delta = target_fee - tx.get_fee()
6✔
2227
            if delta <= 0:
6✔
2228
                break
×
2229
            i = outputs.index(o)
6✔
2230
            if o.value - delta >= self.dust_threshold():
6✔
2231
                new_output_value = o.value - delta
6✔
2232
                assert isinstance(new_output_value, int)
6✔
2233
                outputs[i].value = new_output_value
6✔
2234
                delta = 0
6✔
2235
                break
6✔
2236
            else:
2237
                del outputs[i]
6✔
2238
                # note: we mutated the outputs of tx, which will affect
2239
                #       tx.estimated_size() in the next iteration
2240
        else:
2241
            # recompute delta if there was no next iteration
2242
            target_fee = int(math.ceil(tx.estimated_size() * new_fee_rate))
6✔
2243
            delta = target_fee - tx.get_fee()
6✔
2244

2245
        if delta > 0:
6✔
2246
            raise CannotBumpFee(_('Could not find suitable outputs'))
6✔
2247

2248
        return PartialTransaction.from_io(inputs, outputs)
6✔
2249

2250
    def _bump_fee_through_decreasing_payment(
6✔
2251
            self,
2252
            *,
2253
            tx: PartialTransaction,
2254
            new_fee_rate: Union[int, Decimal],
2255
    ) -> PartialTransaction:
2256
        """
2257
        Increase the miner fee of 'tx' by decreasing amount paid.
2258
        This should be used for transactions that pay "Max".
2259

2260
        - keeps all inputs
2261
        - no new inputs are added
2262
        - Each non-ismine output is decreased proportionally to their byte-size.
2263
        """
2264
        tx = copy.deepcopy(tx)
6✔
2265
        tx.add_info_from_wallet(self)
6✔
2266
        assert tx.get_fee() is not None
6✔
2267
        inputs = tx.inputs()
6✔
2268
        outputs = tx.outputs()
6✔
2269

2270
        # select non-ismine outputs
2271
        s = [(idx, out) for (idx, out) in enumerate(outputs)
6✔
2272
             if not self.is_mine(out.address)]
2273
        s = [(idx, out) for (idx, out) in s if self._is_rbf_allowed_to_touch_tx_output(out)]
6✔
2274
        if not s:
6✔
2275
            raise CannotBumpFee("Cannot find payment output")
×
2276

2277
        del_out_idxs = set()
6✔
2278
        tx_size = tx.estimated_size()
6✔
2279
        cur_fee = tx.get_fee()
6✔
2280
        # Main loop. Each iteration decreases value of all selected outputs.
2281
        # The number of iterations is bounded by len(s) as only the final iteration
2282
        # can *not remove* any output.
2283
        for __ in range(len(s) + 1):
6✔
2284
            target_fee = int(math.ceil(tx_size * new_fee_rate))
6✔
2285
            delta_total = target_fee - cur_fee
6✔
2286
            if delta_total <= 0:
6✔
2287
                break
6✔
2288
            out_size_total = sum(Transaction.estimated_output_size_for_script(out.scriptpubkey)
6✔
2289
                                 for (idx, out) in s if idx not in del_out_idxs)
2290
            if out_size_total == 0:  # no outputs left to decrease
6✔
2291
                raise CannotBumpFee(_('Could not find suitable outputs'))
6✔
2292
            for idx, out in s:
6✔
2293
                out_size = Transaction.estimated_output_size_for_script(out.scriptpubkey)
6✔
2294
                delta = int(math.ceil(delta_total * out_size / out_size_total))
6✔
2295
                if out.value - delta >= self.dust_threshold():
6✔
2296
                    new_output_value = out.value - delta
6✔
2297
                    assert isinstance(new_output_value, int)
6✔
2298
                    outputs[idx].value = new_output_value
6✔
2299
                    cur_fee += delta
6✔
2300
                else:  # remove output
2301
                    tx_size -= out_size
6✔
2302
                    cur_fee += out.value
6✔
2303
                    del_out_idxs.add(idx)
6✔
2304
        if delta_total > 0:
6✔
2305
            raise CannotBumpFee(_('Could not find suitable outputs'))
×
2306

2307
        outputs = [out for (idx, out) in enumerate(outputs) if idx not in del_out_idxs]
6✔
2308
        return PartialTransaction.from_io(inputs, outputs)
6✔
2309

2310
    def _is_rbf_allowed_to_touch_tx_output(self, txout: TxOutput) -> bool:
6✔
2311
        # 2fa fee outputs if present, should not be removed or have their value decreased
2312
        if self.is_billing_address(txout.address):
6✔
2313
            return False
×
2314
        # submarine swap funding outputs must not be decreased
2315
        if self.lnworker and self.lnworker.swap_manager.is_lockup_address_for_a_swap(txout.address):
6✔
2316
            return False
×
2317
        return True
6✔
2318

2319
    def cpfp(self, tx: Transaction, fee: int) -> Optional[PartialTransaction]:
6✔
2320
        assert tx
6✔
2321
        txid = tx.txid()
6✔
2322
        for i, o in enumerate(tx.outputs()):
6✔
2323
            address, value = o.address, o.value
6✔
2324
            if self.is_mine(address):
6✔
2325
                break
6✔
2326
        else:
2327
            raise CannotCPFP(_("Could not find suitable output"))
6✔
2328
        coins = self.adb.get_addr_utxo(address)
6✔
2329
        item = coins.get(TxOutpoint.from_str(txid+':%d'%i))
6✔
2330
        if not item:
6✔
2331
            raise CannotCPFP(_("Could not find coins for output"))
×
2332
        inputs = [item]
6✔
2333
        out_address = (self.get_single_change_address_for_new_transaction(allow_reusing_used_change_addrs=False)
6✔
2334
                       or self.get_unused_address()
2335
                       or address)
2336
        output_value = value - fee
6✔
2337
        if output_value < self.dust_threshold():
6✔
2338
            raise CannotCPFP(_("The output value remaining after fee is too low."))
×
2339
        outputs = [PartialTxOutput.from_address_and_value(out_address, output_value)]
6✔
2340
        locktime = get_locktime_for_new_transaction(self.network)
6✔
2341
        tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
6✔
2342
        tx_new.set_rbf(True)
6✔
2343
        tx_new.add_info_from_wallet(self)
6✔
2344
        return tx_new
6✔
2345

2346
    def dscancel(
6✔
2347
            self, *, tx: Transaction, new_fee_rate: Union[int, float, Decimal]
2348
    ) -> PartialTransaction:
2349
        """Double-Spend-Cancel: cancel an unconfirmed tx by double-spending
2350
        its inputs, paying ourselves.
2351
        'new_fee_rate' is the target min rate in sat/vbyte
2352

2353
        note: it is the caller's responsibility to have already called tx.add_info_from_network().
2354
              Without that, all txins must be ismine.
2355
        """
2356
        assert tx
6✔
2357
        if not isinstance(tx, PartialTransaction):
6✔
2358
            tx = PartialTransaction.from_tx(tx)
6✔
2359
        assert isinstance(tx, PartialTransaction)
6✔
2360
        tx.remove_signatures()
6✔
2361

2362
        if not tx.is_rbf_enabled():
6✔
2363
            raise CannotDoubleSpendTx(_('Transaction is final'))
×
2364
        new_fee_rate = quantize_feerate(new_fee_rate)  # strip excess precision
6✔
2365
        tx.add_info_from_wallet(self)
6✔
2366
        if tx.is_missing_info_from_network():
6✔
2367
            raise Exception("tx missing info from network")
×
2368
        old_tx_size = tx.estimated_size()
6✔
2369
        old_fee = tx.get_fee()
6✔
2370
        assert old_fee is not None
6✔
2371
        old_fee_rate = old_fee / old_tx_size  # sat/vbyte
6✔
2372
        if new_fee_rate <= old_fee_rate:
6✔
2373
            raise CannotDoubleSpendTx(_("The new fee rate needs to be higher than the old fee rate."))
×
2374
        # grab all ismine inputs
2375
        inputs = [txin for txin in tx.inputs()
6✔
2376
                  if self.is_mine(self.adb.get_txin_address(txin))]
2377
        value = sum([txin.value_sats() for txin in inputs])
6✔
2378
        # figure out output address
2379
        old_change_addrs = [o.address for o in tx.outputs() if self.is_mine(o.address)]
6✔
2380
        out_address = (self.get_single_change_address_for_new_transaction(old_change_addrs)
6✔
2381
                       or self.get_receiving_address())
2382
        locktime = get_locktime_for_new_transaction(self.network)
6✔
2383
        outputs = [PartialTxOutput.from_address_and_value(out_address, value)]
6✔
2384
        tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
6✔
2385
        new_tx_size = tx_new.estimated_size()
6✔
2386
        new_fee = max(
6✔
2387
            new_fee_rate * new_tx_size,
2388
            old_fee + self.relayfee() * new_tx_size / Decimal(1000),  # BIP-125 rules 3 and 4
2389
        )
2390
        new_fee = int(math.ceil(new_fee))
6✔
2391
        output_value = value - new_fee
6✔
2392
        if output_value < self.dust_threshold():
6✔
2393
            raise CannotDoubleSpendTx(_("The output value remaining after fee is too low."))
×
2394
        outputs = [PartialTxOutput.from_address_and_value(out_address, value - new_fee)]
6✔
2395
        tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
6✔
2396
        tx_new.set_rbf(True)
6✔
2397
        tx_new.add_info_from_wallet(self)
6✔
2398
        return tx_new
6✔
2399

2400
    def _add_txinout_derivation_info(self, txinout: Union[PartialTxInput, PartialTxOutput],
6✔
2401
                                     address: str, *, only_der_suffix: bool) -> None:
2402
        pass  # implemented by subclasses
6✔
2403

2404
    def _add_input_utxo_info(
6✔
2405
            self,
2406
            txin: PartialTxInput,
2407
            *,
2408
            address: str = None,
2409
    ) -> None:
2410
        # - We prefer to include UTXO (full tx), even for segwit inputs (see #6198).
2411
        # - For witness v0 inputs, we include *both* UTXO and WITNESS_UTXO. UTXO is a strict superset,
2412
        #   so this is redundant, but it is (implied to be) "expected" from bip-0174 (see #8039).
2413
        #   Regardless, this might improve compatibility with some other software.
2414
        # - For witness v1, witness_utxo will be enough though (bip-0341 sighash fixes known prior issues).
2415
        # - We cannot include UTXO if the prev tx is not signed yet (chain of unsigned txs).
2416
        address = address or txin.address
6✔
2417
        # add witness_utxo
2418
        if txin.witness_utxo is None and txin.is_segwit() and address:
6✔
2419
            received, spent = self.adb.get_addr_io(address)
6✔
2420
            item = received.get(txin.prevout.to_str())
6✔
2421
            if item:
6✔
2422
                txin_value = item[2]
6✔
2423
                txin.witness_utxo = TxOutput.from_address_and_value(address, txin_value)
6✔
2424
        # add utxo
2425
        if txin.utxo is None:
6✔
2426
            txin.utxo = self.db.get_transaction(txin.prevout.txid.hex())
6✔
2427
        # Maybe remove witness_utxo. witness_utxo should not be present for non-segwit inputs.
2428
        # If it is present, it might be because another electrum instance added it when sharing the psbt via QR code.
2429
        # If we have the full utxo available, we can remove it without loss of information.
2430
        if txin.witness_utxo and not txin.is_segwit() and txin.utxo:
6✔
2431
            txin.witness_utxo = None
6✔
2432

2433
    def _learn_derivation_path_for_address_from_txinout(self, txinout: Union[PartialTxInput, PartialTxOutput],
6✔
2434
                                                        address: str) -> bool:
2435
        """Tries to learn the derivation path for an address (potentially beyond gap limit)
2436
        using data available in given txin/txout.
2437
        Returns whether the address was found to be is_mine.
2438
        """
2439
        return False  # implemented by subclasses
6✔
2440

2441
    def add_input_info(
6✔
2442
            self,
2443
            txin: TxInput,
2444
            *,
2445
            only_der_suffix: bool = False,
2446
    ) -> None:
2447
        """Populates the txin, using info the wallet already has.
2448
        That is, network requests are *not* done to fetch missing prev txs!
2449
        For that, use txin.add_info_from_network.
2450
        """
2451
        # note: we add input utxos regardless of is_mine
2452
        if txin.utxo is None:
6✔
2453
            txin.utxo = self.db.get_transaction(txin.prevout.txid.hex())
6✔
2454
        if not isinstance(txin, PartialTxInput):
6✔
2455
            return
6✔
2456
        address = self.adb.get_txin_address(txin)
6✔
2457
        self._add_input_utxo_info(txin, address=address)
6✔
2458
        is_mine = self.is_mine(address)
6✔
2459
        if not is_mine:
6✔
2460
            is_mine = self._learn_derivation_path_for_address_from_txinout(txin, address)
6✔
2461
        if not is_mine:
6✔
2462
            if self.lnworker:
6✔
2463
                self.lnworker.swap_manager.add_txin_info(txin)
×
2464
            return
6✔
2465
        txin.script_descriptor = self.get_script_descriptor_for_address(address)
6✔
2466
        txin.is_mine = True
6✔
2467
        self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix)
6✔
2468
        txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height
6✔
2469

2470
    def has_support_for_slip_19_ownership_proofs(self) -> bool:
6✔
2471
        return False
×
2472

2473
    def add_slip_19_ownership_proofs_to_tx(self, tx: PartialTransaction) -> None:
6✔
2474
        raise NotImplementedError()
×
2475

2476
    def get_script_descriptor_for_address(self, address: str) -> Optional[Descriptor]:
6✔
2477
        if not self.is_mine(address):
6✔
2478
            return None
×
2479
        script_type = self.get_txin_type(address)
6✔
2480
        if script_type in ('address', 'unknown'):
6✔
2481
            return None
6✔
2482
        addr_index = self.get_address_index(address)
6✔
2483
        if addr_index is None:
6✔
2484
            return None
×
2485
        pubkeys = [ks.get_pubkey_provider(addr_index) for ks in self.get_keystores()]
6✔
2486
        if not pubkeys:
6✔
2487
            return None
×
2488
        if script_type == 'p2pk':
6✔
2489
            return descriptor.PKDescriptor(pubkey=pubkeys[0])
×
2490
        elif script_type == 'p2pkh':
6✔
2491
            return descriptor.PKHDescriptor(pubkey=pubkeys[0])
6✔
2492
        elif script_type == 'p2wpkh':
6✔
2493
            return descriptor.WPKHDescriptor(pubkey=pubkeys[0])
6✔
2494
        elif script_type == 'p2wpkh-p2sh':
6✔
2495
            wpkh = descriptor.WPKHDescriptor(pubkey=pubkeys[0])
6✔
2496
            return descriptor.SHDescriptor(subdescriptor=wpkh)
6✔
2497
        elif script_type == 'p2sh':
6✔
2498
            multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)
6✔
2499
            return descriptor.SHDescriptor(subdescriptor=multi)
6✔
2500
        elif script_type == 'p2wsh':
6✔
2501
            multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)
6✔
2502
            return descriptor.WSHDescriptor(subdescriptor=multi)
6✔
2503
        elif script_type == 'p2wsh-p2sh':
6✔
2504
            multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)
6✔
2505
            wsh = descriptor.WSHDescriptor(subdescriptor=multi)
6✔
2506
            return descriptor.SHDescriptor(subdescriptor=wsh)
6✔
2507
        else:
2508
            raise NotImplementedError(f"unexpected {script_type=}")
×
2509

2510
    def can_sign(self, tx: Transaction) -> bool:
6✔
2511
        if not isinstance(tx, PartialTransaction):
6✔
2512
            return False
×
2513
        if tx.is_complete():
6✔
2514
            return False
×
2515
        # add info to inputs if we can; otherwise we might return a false negative:
2516
        tx.add_info_from_wallet(self)
6✔
2517
        for txin in tx.inputs():
6✔
2518
            # note: is_mine check needed to avoid false positives.
2519
            #       just because keystore could sign, txin does not necessarily belong to wallet.
2520
            #       Example: we have p2pkh-like addresses and txin is a multisig that involves our pubkey.
2521
            if not self.is_mine(txin.address):
6✔
2522
                continue
6✔
2523
            for k in self.get_keystores():
×
2524
                if k.can_sign_txin(txin):
×
2525
                    return True
×
2526
        if self.get_swap_by_claim_tx(tx):
6✔
2527
            return True
×
2528
        return False
6✔
2529

2530
    def add_output_info(self, txout: PartialTxOutput, *, only_der_suffix: bool = False) -> None:
6✔
2531
        address = txout.address
6✔
2532
        if not self.is_mine(address):
6✔
2533
            is_mine = self._learn_derivation_path_for_address_from_txinout(txout, address)
6✔
2534
            if not is_mine:
6✔
2535
                return
6✔
2536
        txout.script_descriptor = self.get_script_descriptor_for_address(address)
6✔
2537
        txout.is_mine = True
6✔
2538
        txout.is_change = self.is_change(address)
6✔
2539
        self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix)
6✔
2540

2541
    def sign_transaction(self, tx: Transaction, password, *, ignore_warnings: bool = False) -> Optional[PartialTransaction]:
6✔
2542
        """ returns tx if successful else None """
2543
        if self.is_watching_only():
6✔
2544
            return
6✔
2545
        if not isinstance(tx, PartialTransaction):
6✔
2546
            return
×
2547
        if any(DummyAddress.is_dummy_address(txout.address) for txout in tx.outputs()):
6✔
2548
            raise DummyAddressUsedInTxException("tried to sign tx with dummy address!")
6✔
2549
        # note: swap signing does not require the password
2550
        swap = self.get_swap_by_claim_tx(tx)
6✔
2551
        if swap:
6✔
2552
            self.lnworker.swap_manager.sign_tx(tx, swap)
×
2553
            return tx
×
2554

2555
        # check if signing is dangerous
2556
        sh_danger = self.check_sighash(tx)
6✔
2557
        if sh_danger.needs_reject():
6✔
2558
            raise TransactionDangerousException('Not signing transaction:\n' + sh_danger.get_long_message())
6✔
2559
        if sh_danger.needs_confirm() and not ignore_warnings:
6✔
2560
            raise TransactionPotentiallyDangerousException('Not signing transaction:\n' + sh_danger.get_long_message())
6✔
2561

2562
        # sign with make_witness
2563
        for i, txin in enumerate(tx.inputs()):
6✔
2564
            if hasattr(txin, 'make_witness'):
6✔
NEW
2565
                self.logger.info(f'sign_transaction: adding witness using make_witness')
×
NEW
2566
                privkey = txin.privkey
×
NEW
2567
                sig = tx.sign_txin(i, privkey)
×
NEW
2568
                txin.witness = txin.make_witness(sig)
×
NEW
2569
                assert txin.is_complete()
×
2570

2571
        # add info to a temporary tx copy; including xpubs
2572
        # and full derivation paths as hw keystores might want them
2573
        tmp_tx = copy.deepcopy(tx)
6✔
2574
        tmp_tx.add_info_from_wallet(self, include_xpubs=True)
6✔
2575
        # sign. start with ready keystores.
2576
        # note: ks.ready_to_sign() side-effect: we trigger pairings with potential hw devices.
2577
        #       We only do this once, before the loop, however we could rescan after each iteration,
2578
        #       to see if the user connected/disconnected devices in the meantime.
2579
        for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True):
6✔
2580
            try:
6✔
2581
                if k.can_sign(tmp_tx):
6✔
2582
                    k.sign_transaction(tmp_tx, password)
6✔
2583
            except UserCancelled:
×
2584
                continue
×
2585
        # remove sensitive info; then copy back details from temporary tx
2586
        tmp_tx.remove_xpubs_and_bip32_paths()
6✔
2587
        tx.combine_with_other_psbt(tmp_tx)
6✔
2588
        tx.add_info_from_wallet(self, include_xpubs=False)
6✔
2589
        return tx
6✔
2590

2591
    def try_detecting_internal_addresses_corruption(self) -> None:
6✔
2592
        pass
×
2593

2594
    def check_address_for_corruption(self, addr: str) -> None:
6✔
2595
        pass
×
2596

2597
    def get_unused_addresses(self) -> Sequence[str]:
6✔
2598
        domain = self.get_receiving_addresses()
6✔
2599
        return [addr for addr in domain if not self.adb.is_used(addr) and not self.get_request_by_addr(addr)]
6✔
2600

2601
    @check_returned_address_for_corruption
6✔
2602
    def get_unused_address(self) -> Optional[str]:
6✔
2603
        """Get an unused receiving address, if there is one.
2604
        Note: there might NOT be one available!
2605
        """
2606
        addrs = self.get_unused_addresses()
6✔
2607
        if addrs:
6✔
2608
            return addrs[0]
6✔
2609

2610
    @check_returned_address_for_corruption
6✔
2611
    def get_receiving_address(self) -> str:
6✔
2612
        """Get a receiving address. Guaranteed to always return an address."""
2613
        unused_addr = self.get_unused_address()
6✔
2614
        if unused_addr:
6✔
2615
            return unused_addr
6✔
2616
        domain = self.get_receiving_addresses()
×
2617
        if not domain:
×
2618
            raise Exception("no receiving addresses in wallet?!")
×
2619
        choice = domain[0]
×
2620
        for addr in domain:
×
2621
            if not self.adb.is_used(addr):
×
2622
                if self.get_request_by_addr(addr) is None:
×
2623
                    return addr
×
2624
                else:
2625
                    choice = addr
×
2626
        return choice
×
2627

2628
    def create_new_address(self, for_change: bool = False):
6✔
2629
        raise UserFacingException("this wallet cannot generate new addresses")
×
2630

2631
    def import_address(self, address: str) -> str:
6✔
2632
        raise UserFacingException("this wallet cannot import addresses")
×
2633

2634
    def import_addresses(self, addresses: List[str], *,
6✔
2635
                         write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:
2636
        raise UserFacingException("this wallet cannot import addresses")
×
2637

2638
    def delete_address(self, address: str) -> None:
6✔
2639
        raise UserFacingException("this wallet cannot delete addresses")
×
2640

2641
    def get_request_URI(self, req: Request) -> Optional[str]:
6✔
2642
        lightning_invoice = None
×
2643
        if self.config.WALLET_BIP21_LIGHTNING:
×
2644
            lightning_invoice = self.get_bolt11_invoice(req)
×
2645
        return req.get_bip21_URI(lightning_invoice=lightning_invoice)
×
2646

2647
    def check_expired_status(self, r: BaseInvoice, status):
6✔
2648
        #if r.is_lightning() and r.exp == 0:
2649
        #    status = PR_EXPIRED  # for BOLT-11 invoices, exp==0 means 0 seconds
2650
        if status == PR_UNPAID and r.has_expired():
6✔
2651
            status = PR_EXPIRED
6✔
2652
        return status
6✔
2653

2654
    def get_invoice_status(self, invoice: BaseInvoice):
6✔
2655
        """Returns status of (incoming) request or (outgoing) invoice."""
2656
        # lightning invoices can be paid onchain
2657
        if invoice.is_lightning() and self.lnworker:
6✔
2658
            status = self.lnworker.get_invoice_status(invoice)
6✔
2659
            if status != PR_UNPAID:
6✔
2660
                return self.check_expired_status(invoice, status)
6✔
2661
        paid, conf = self.is_onchain_invoice_paid(invoice)
6✔
2662
        if not paid:
6✔
2663
            if isinstance(invoice, Invoice):
6✔
2664
                if status:=invoice.get_broadcasting_status():
6✔
2665
                    return status
×
2666
            status = PR_UNPAID
6✔
2667
        elif conf == 0:
6✔
2668
            status = PR_UNCONFIRMED
6✔
2669
        else:
2670
            assert conf >= 1, conf
6✔
2671
            status = PR_PAID
6✔
2672
        return self.check_expired_status(invoice, status)
6✔
2673

2674
    def get_request_by_addr(self, addr: str) -> Optional[Request]:
6✔
2675
        """Returns a relevant request for address, from an on-chain PoV.
2676
        (One that has been paid on-chain or is pending)
2677

2678
        Called in get_label_for_address and update_invoices_and_reqs_touched_by_tx
2679
        Returns None if the address can be reused (i.e. was paid by lightning or has expired)
2680
        """
2681
        keys = self._requests_addr_to_key.get(addr) or []
6✔
2682
        reqs = [self._receive_requests.get(key) for key in keys]
6✔
2683
        reqs = [req for req in reqs if req]  # filter None
6✔
2684
        if not reqs:
6✔
2685
            return
6✔
2686
        # filter out expired
2687
        reqs = [req for req in reqs if self.get_invoice_status(req) != PR_EXPIRED]
6✔
2688
        # filter out paid-with-lightning
2689
        if self.lnworker:
6✔
2690
            reqs = [req for req in reqs
6✔
2691
                    if not req.is_lightning() or self.lnworker.get_invoice_status(req) == PR_UNPAID]
2692
        if not reqs:
6✔
2693
            return None
6✔
2694
        # note: There typically should not be more than one relevant request for an address.
2695
        #       If there's multiple, return the one created last (see #8113). Consider:
2696
        #       - there is an old expired req1, and a newer unpaid req2, reusing the same addr (and same amount),
2697
        #       - now req2 gets paid. however, get_invoice_status will say both req1 and req2 are PAID. (see #8061)
2698
        #       - as a workaround, we return the request with the larger creation time.
2699
        reqs.sort(key=lambda req: req.get_time())
6✔
2700
        return reqs[-1]
6✔
2701

2702
    def get_request(self, request_id: str) -> Optional[Request]:
6✔
2703
        return self._receive_requests.get(request_id)
6✔
2704

2705
    def get_formatted_request(self, request_id):
6✔
2706
        x = self.get_request(request_id)
×
2707
        if x:
×
2708
            return self.export_request(x)
×
2709

2710
    def export_request(self, x: Request) -> Dict[str, Any]:
6✔
2711
        key = x.get_id()
×
2712
        status = self.get_invoice_status(x)
×
2713
        d = x.as_dict(status)
×
2714
        d['request_id'] = d.pop('id')
×
2715
        if x.is_lightning():
×
2716
            d['rhash'] = x.rhash
×
2717
            d['lightning_invoice'] = self.get_bolt11_invoice(x)
×
2718
            if self.lnworker and status == PR_UNPAID:
×
2719
                d['can_receive'] = self.lnworker.can_receive_invoice(x)
×
2720
        if address := x.get_address():
×
2721
            d['address'] = address
×
2722
            d['URI'] = self.get_request_URI(x)
×
2723
            # if request was paid onchain, add relevant fields
2724
            # note: addr is reused when getting paid on LN! so we check for that.
2725
            _, conf, tx_hashes = self._is_onchain_invoice_paid(x)
×
2726
            if not x.is_lightning() or not self.lnworker or self.lnworker.get_invoice_status(x) != PR_PAID:
×
2727
                if conf is not None:
×
2728
                    d['confirmations'] = conf
×
2729
                d['tx_hashes'] = tx_hashes
×
2730
        run_hook('wallet_export_request', d, key)
×
2731
        return d
×
2732

2733
    def export_invoice(self, x: Invoice) -> Dict[str, Any]:
6✔
2734
        key = x.get_id()
×
2735
        status = self.get_invoice_status(x)
×
2736
        d = x.as_dict(status)
×
2737
        d['invoice_id'] = d.pop('id')
×
2738
        if x.is_lightning():
×
2739
            d['lightning_invoice'] = x.lightning_invoice
×
2740
            if self.lnworker and status == PR_UNPAID:
×
2741
                d['can_pay'] = self.lnworker.can_pay_invoice(x)
×
2742
        else:
2743
            amount_sat = x.get_amount_sat()
×
2744
            assert isinstance(amount_sat, (int, str, type(None)))
×
2745
            d['outputs'] = [y.to_legacy_tuple() for y in x.get_outputs()]
×
2746
            if x.bip70:
×
2747
                d['bip70'] = x.bip70
×
2748
        return d
×
2749

2750
    def get_invoices_and_requests_touched_by_tx(self, tx):
6✔
2751
        request_keys = set()
6✔
2752
        invoice_keys = set()
6✔
2753
        with self.lock, self.transaction_lock:
6✔
2754
            for txo in tx.outputs():
6✔
2755
                addr = txo.address
6✔
2756
                if request:=self.get_request_by_addr(addr):
6✔
2757
                    request_keys.add(request.get_id())
6✔
2758
                for invoice_key in self._invoices_from_scriptpubkey_map.get(txo.scriptpubkey, set()):
6✔
2759
                    invoice_keys.add(invoice_key)
×
2760
        return request_keys, invoice_keys
6✔
2761

2762
    def _update_invoices_and_reqs_touched_by_tx(self, tx_hash: str) -> None:
6✔
2763
        # FIXME in some cases if tx2 replaces unconfirmed tx1 in the mempool, we are not called.
2764
        #       For a given receive request, if tx1 touches it but tx2 does not, then
2765
        #       we were called when tx1 was added, but we will not get called when tx2 replaces tx1.
2766
        tx = self.db.get_transaction(tx_hash)
6✔
2767
        if tx is None:
6✔
2768
            return
×
2769
        request_keys, invoice_keys = self.get_invoices_and_requests_touched_by_tx(tx)
6✔
2770
        for key in request_keys:
6✔
2771
            request = self.get_request(key)
6✔
2772
            if not request:
6✔
2773
                continue
×
2774
            status = self.get_invoice_status(request)
6✔
2775
            util.trigger_callback('request_status', self, request.get_id(), status)
6✔
2776
        self._update_onchain_invoice_paid_detection(invoice_keys)
6✔
2777

2778
    def set_broadcasting(self, tx: Transaction, *, broadcasting_status: Optional[int]):
6✔
2779
        request_keys, invoice_keys = self.get_invoices_and_requests_touched_by_tx(tx)
×
2780
        for key in invoice_keys:
×
2781
            invoice = self._invoices.get(key)
×
2782
            if not invoice:
×
2783
                continue
×
2784
            invoice._broadcasting_status = broadcasting_status
×
2785
            status = self.get_invoice_status(invoice)
×
2786
            util.trigger_callback('invoice_status', self, key, status)
×
2787

2788
    def get_bolt11_invoice(self, req: Request) -> str:
6✔
2789
        if not self.lnworker:
×
2790
            return ''
×
2791
        if (payment_hash := req.payment_hash) is None:  # e.g. req might have been generated before enabling LN
×
2792
            return ''
×
2793
        amount_msat = req.get_amount_msat() or None
×
2794
        assert (amount_msat is None or amount_msat > 0), amount_msat
×
2795
        lnaddr, invoice = self.lnworker.get_bolt11_invoice(
×
2796
            payment_hash=payment_hash,
2797
            amount_msat=amount_msat,
2798
            message=req.message,
2799
            expiry=req.exp,
2800
            fallback_address=req.get_address() if self.config.WALLET_BOLT11_FALLBACK else None)
2801
        return invoice
×
2802

2803
    def create_request(self, amount_sat: Optional[int], message: Optional[str], exp_delay: Optional[int], address: Optional[str]):
6✔
2804
        # for receiving
2805
        amount_sat = amount_sat or 0
6✔
2806
        assert isinstance(amount_sat, int), f"{amount_sat!r}"
6✔
2807
        amount_msat = None if not amount_sat else amount_sat * 1000  # amount_sat in [None, 0] implies undefined.
6✔
2808
        message = message or ''
6✔
2809
        address = address or None  # converts "" to None
6✔
2810
        exp_delay = exp_delay or 0
6✔
2811
        timestamp = int(Request._get_cur_time())
6✔
2812
        payment_hash = None  # type: Optional[bytes]
6✔
2813
        if self.has_lightning():
6✔
2814
            payment_hash = self.lnworker.create_payment_info(amount_msat=amount_msat, write_to_disk=False)
6✔
2815
        outputs = [PartialTxOutput.from_address_and_value(address, amount_sat)] if address else []
6✔
2816
        height = self.adb.get_local_height()
6✔
2817
        req = Request(
6✔
2818
            outputs=outputs,
2819
            message=message,
2820
            time=timestamp,
2821
            amount_msat=amount_msat,
2822
            exp=exp_delay,
2823
            height=height,
2824
            bip70=None,
2825
            payment_hash=payment_hash,
2826
        )
2827
        key = self.add_payment_request(req)
6✔
2828
        return key
6✔
2829

2830
    def add_payment_request(self, req: Request, *, write_to_disk: bool = True):
6✔
2831
        request_id = req.get_id()
6✔
2832
        self._receive_requests[request_id] = req
6✔
2833
        if addr:=req.get_address():
6✔
2834
            self._requests_addr_to_key[addr].add(request_id)
6✔
2835
        if write_to_disk:
6✔
2836
            self.save_db()
6✔
2837
        return request_id
6✔
2838

2839
    def delete_request(self, request_id, *, write_to_disk: bool = True):
6✔
2840
        """ lightning or on-chain """
2841
        req = self.get_request(request_id)
×
2842
        if req is None:
×
2843
            return
×
2844
        self._receive_requests.pop(request_id, None)
×
2845
        if addr:=req.get_address():
×
2846
            self._requests_addr_to_key[addr].discard(request_id)
×
2847
        if req.is_lightning() and self.lnworker:
×
2848
            self.lnworker.delete_payment_info(req.rhash)
×
2849
        if write_to_disk:
×
2850
            self.save_db()
×
2851

2852
    def delete_invoice(self, invoice_id, *, write_to_disk: bool = True):
6✔
2853
        """ lightning or on-chain """
2854
        inv = self._invoices.pop(invoice_id, None)
×
2855
        if inv is None:
×
2856
            return
×
2857
        if inv.is_lightning() and self.lnworker:
×
2858
            self.lnworker.delete_payment_info(inv.rhash)
×
2859
        if write_to_disk:
×
2860
            self.save_db()
×
2861

2862
    def get_sorted_requests(self) -> List[Request]:
6✔
2863
        """ sorted by timestamp """
2864
        out = [self.get_request(x) for x in self._receive_requests.keys()]
×
2865
        out = [x for x in out if x is not None]
×
2866
        out.sort(key=lambda x: x.time)
×
2867
        return out
×
2868

2869
    def get_unpaid_requests(self) -> List[Request]:
6✔
2870
        out = [x for x in self._receive_requests.values() if self.get_invoice_status(x) != PR_PAID]
×
2871
        out.sort(key=lambda x: x.time)
×
2872
        return out
×
2873

2874
    def delete_expired_requests(self):
6✔
2875
        keys = [k for k, v in self._receive_requests.items() if self.get_invoice_status(v) == PR_EXPIRED]
×
2876
        self.delete_requests(keys)
×
2877
        return keys
×
2878

2879
    def delete_requests(self, keys):
6✔
2880
        for key in keys:
×
2881
            self.delete_request(key, write_to_disk=False)
×
2882
        if keys:
×
2883
            self.save_db()
×
2884

2885
    @abstractmethod
6✔
2886
    def get_fingerprint(self) -> str:
6✔
2887
        """Returns a string that can be used to identify this wallet.
2888
        Used e.g. by Labels plugin, and LN channel backups.
2889
        Returns empty string "" for wallets that don't have an ID.
2890
        """
2891
        pass
×
2892

2893
    def can_import_privkey(self):
6✔
2894
        return False
×
2895

2896
    def can_import_address(self):
6✔
2897
        return False
×
2898

2899
    def can_delete_address(self):
6✔
2900
        return False
×
2901

2902
    def has_password(self) -> bool:
6✔
2903
        return self.has_keystore_encryption() or self.has_storage_encryption()
6✔
2904

2905
    def can_have_keystore_encryption(self):
6✔
2906
        return self.keystore and self.keystore.may_have_password()
6✔
2907

2908
    def get_available_storage_encryption_version(self) -> StorageEncryptionVersion:
6✔
2909
        """Returns the type of storage encryption offered to the user.
2910

2911
        A wallet file (storage) is either encrypted with this version
2912
        or is stored in plaintext.
2913
        """
2914
        if isinstance(self.keystore, Hardware_KeyStore):
6✔
2915
            return StorageEncryptionVersion.XPUB_PASSWORD
×
2916
        else:
2917
            return StorageEncryptionVersion.USER_PASSWORD
6✔
2918

2919
    def has_keystore_encryption(self) -> bool:
6✔
2920
        """Returns whether encryption is enabled for the keystore.
2921

2922
        If True, e.g. signing a transaction will require a password.
2923
        """
2924
        if self.can_have_keystore_encryption():
6✔
2925
            return bool(self.db.get('use_encryption', False))
6✔
2926
        return False
6✔
2927

2928
    def has_storage_encryption(self) -> bool:
6✔
2929
        """Returns whether encryption is enabled for the wallet file on disk."""
2930
        return bool(self.storage) and self.storage.is_encrypted()
6✔
2931

2932
    @classmethod
6✔
2933
    def may_have_password(cls):
6✔
2934
        return True
×
2935

2936
    def check_password(self, password: Optional[str]) -> None:
6✔
2937
        """Raises an InvalidPassword exception on invalid password"""
2938
        if not self.has_password():
6✔
2939
            if password is not None:
6✔
2940
                raise InvalidPassword("password given but wallet has no password")
6✔
2941
            return
6✔
2942
        if self.has_keystore_encryption():
6✔
2943
            self.keystore.check_password(password)
6✔
2944
        if self.has_storage_encryption():
6✔
2945
            self.storage.check_password(password)
6✔
2946

2947
    def update_password(self, old_pw, new_pw, *, encrypt_storage: bool = True):
6✔
2948
        if old_pw is None and self.has_password():
6✔
2949
            raise InvalidPassword()
×
2950
        self.check_password(old_pw)
6✔
2951
        if self.storage:
6✔
2952
            if encrypt_storage:
6✔
2953
                enc_version = self.get_available_storage_encryption_version()
6✔
2954
            else:
2955
                enc_version = StorageEncryptionVersion.PLAINTEXT
6✔
2956
            self.storage.set_password(new_pw, enc_version)
6✔
2957
        # make sure next storage.write() saves changes
2958
        self.db.set_modified(True)
6✔
2959

2960
        # note: Encrypting storage with a hw device is currently only
2961
        #       allowed for non-multisig wallets. Further,
2962
        #       Hardware_KeyStore.may_have_password() == False.
2963
        #       If these were not the case,
2964
        #       extra care would need to be taken when encrypting keystores.
2965
        self._update_password_for_keystore(old_pw, new_pw)
6✔
2966
        encrypt_keystore = self.can_have_keystore_encryption()
6✔
2967
        self.db.set_keystore_encryption(bool(new_pw) and encrypt_keystore)
6✔
2968
        # save changes. force full rewrite to rm remnants of old password
2969
        if self.storage and self.storage.file_exists():
6✔
2970
            self.db.write_and_force_consolidation()
6✔
2971
        # if wallet was previously unlocked, update password in memory
2972
        if self._password_in_memory is not None:
6✔
2973
            self._password_in_memory = new_pw
×
2974

2975
    @abstractmethod
6✔
2976
    def _update_password_for_keystore(self, old_pw: Optional[str], new_pw: Optional[str]) -> None:
6✔
2977
        pass
×
2978

2979
    def sign_message(self, address: str, message: str, password) -> bytes:
6✔
2980
        index = self.get_address_index(address)
×
2981
        script_type = self.get_txin_type(address)
×
2982
        assert script_type != "address"
×
2983
        return self.keystore.sign_message(index, message, password, script_type=script_type)
×
2984

2985
    def decrypt_message(self, pubkey: str, message, password) -> bytes:
6✔
2986
        addr = self.pubkeys_to_address([pubkey])
×
2987
        index = self.get_address_index(addr)
×
2988
        return self.keystore.decrypt_message(index, message, password)
×
2989

2990
    @abstractmethod
6✔
2991
    def pubkeys_to_address(self, pubkeys: Sequence[str]) -> Optional[str]:
6✔
2992
        pass
×
2993

2994
    def price_at_timestamp(self, txid, price_func):
6✔
2995
        """Returns fiat price of bitcoin at the time tx got confirmed."""
2996
        timestamp = self.adb.get_tx_height(txid).timestamp
6✔
2997
        return price_func(timestamp if timestamp else time.time())
6✔
2998

2999
    def average_price(self, txid, price_func, ccy) -> Decimal:
6✔
3000
        """ Average acquisition price of the inputs of a transaction """
3001
        input_value = 0
×
3002
        total_price = 0
×
3003
        txi_addresses = self.db.get_txi_addresses(txid)
×
3004
        if not txi_addresses:
×
3005
            return Decimal('NaN')
×
3006
        for addr in txi_addresses:
×
3007
            d = self.db.get_txi_addr(txid, addr)
×
3008
            for ser, v in d:
×
3009
                input_value += v
×
3010
                total_price += self.coin_price(ser.split(':')[0], price_func, ccy, v)
×
3011
        return total_price / (input_value/Decimal(COIN))
×
3012

3013
    def clear_coin_price_cache(self):
6✔
3014
        self._coin_price_cache = {}
×
3015

3016
    def coin_price(self, txid, price_func, ccy, txin_value) -> Decimal:
6✔
3017
        """
3018
        Acquisition price of a coin.
3019
        This assumes that either all inputs are mine, or no input is mine.
3020
        """
3021
        if txin_value is None:
×
3022
            return Decimal('NaN')
×
3023
        cache_key = "{}:{}:{}".format(str(txid), str(ccy), str(txin_value))
×
3024
        result = self._coin_price_cache.get(cache_key, None)
×
3025
        if result is not None:
×
3026
            return result
×
3027
        if self.db.get_txi_addresses(txid):
×
3028
            result = self.average_price(txid, price_func, ccy) * txin_value/Decimal(COIN)
×
3029
            self._coin_price_cache[cache_key] = result
×
3030
            return result
×
3031
        else:
3032
            fiat_value = self.get_fiat_value(txid, ccy)
×
3033
            if fiat_value is not None:
×
3034
                return fiat_value
×
3035
            else:
3036
                p = self.price_at_timestamp(txid, price_func)
×
3037
                return p * txin_value/Decimal(COIN)
×
3038

3039
    def is_billing_address(self, addr):
6✔
3040
        # overridden for TrustedCoin wallets
3041
        return False
6✔
3042

3043
    @abstractmethod
6✔
3044
    def is_watching_only(self) -> bool:
6✔
3045
        pass
×
3046

3047
    def get_keystore(self) -> Optional[KeyStore]:
6✔
3048
        return self.keystore
×
3049

3050
    def get_keystores(self) -> Sequence[KeyStore]:
6✔
3051
        return [self.keystore] if self.keystore else []
6✔
3052

3053
    @abstractmethod
6✔
3054
    def save_keystore(self):
6✔
3055
        pass
×
3056

3057
    @abstractmethod
6✔
3058
    def has_seed(self) -> bool:
6✔
3059
        pass
×
3060

3061
    def get_seed_type(self) -> Optional[str]:
6✔
3062
        return None
×
3063

3064
    @abstractmethod
6✔
3065
    def get_all_known_addresses_beyond_gap_limit(self) -> Set[str]:
6✔
3066
        pass
×
3067

3068
    def create_transaction(
6✔
3069
        self,
3070
        outputs,
3071
        *,
3072
        fee=None,
3073
        feerate=None,
3074
        change_addr=None,
3075
        domain_addr=None,
3076
        domain_coins=None,
3077
        sign=True,
3078
        rbf=True,
3079
        password=None,
3080
        locktime=None,
3081
        tx_version: Optional[int] = None,
3082
        base_tx: Optional[PartialTransaction] = None,
3083
        inputs: Optional[List[PartialTxInput]] = None,
3084
        send_change_to_lightning: Optional[bool] = None,
3085
        nonlocal_only: bool = False,
3086
        BIP69_sort: bool = True,
3087
    ) -> PartialTransaction:
3088
        """Helper function for make_unsigned_transaction."""
3089
        if fee is not None and feerate is not None:
6✔
3090
            raise UserFacingException("Cannot specify both 'fee' and 'feerate' at the same time!")
×
3091
        coins = self.get_spendable_coins(domain_addr, nonlocal_only=nonlocal_only)
6✔
3092
        if domain_coins is not None:
6✔
3093
            coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)]
×
3094
        if feerate is not None:
6✔
3095
            fee_per_kb = 1000 * Decimal(feerate)
6✔
3096
            fee_estimator = partial(SimpleConfig.estimate_fee_for_feerate, fee_per_kb)
6✔
3097
        else:
3098
            fee_estimator = fee
6✔
3099
        tx = self.make_unsigned_transaction(
6✔
3100
            coins=coins,
3101
            inputs=inputs,
3102
            outputs=outputs,
3103
            fee=fee_estimator,
3104
            change_addr=change_addr,
3105
            base_tx=base_tx,
3106
            send_change_to_lightning=send_change_to_lightning,
3107
            rbf=rbf,
3108
            BIP69_sort=BIP69_sort,
3109
        )
3110
        if locktime is not None:
6✔
3111
            tx.locktime = locktime
6✔
3112
        if tx_version is not None:
6✔
3113
            tx.version = tx_version
6✔
3114
        if sign:
6✔
3115
            self.sign_transaction(tx, password)
6✔
3116
        return tx
6✔
3117

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

3122
        Note:
3123
            - legacy sighash does not commit to any input amounts
3124
            - BIP-0143 sighash only commits to the *corresponding* input amount
3125
            - BIP-taproot sighash commits to *all* input amounts
3126
        """
3127
        assert isinstance(tx, PartialTransaction)
6✔
3128
        rl = TxSighashRiskLevel
6✔
3129
        short_message = _("Warning") + ": " + _("The fee could not be verified!")
6✔
3130
        # check that all inputs use SIGHASH_ALL
3131
        if not all(txin.sighash in (None, Sighash.ALL) for txin in tx.inputs()):
6✔
3132
            messages = [(_("Warning") + ": "
6✔
3133
                         + _("Some inputs use non-default sighash flags, which might affect the fee."))]
3134
            return TxSighashDanger(risk_level=rl.FEE_WARNING_NEEDCONFIRM, short_message=short_message, messages=messages)
6✔
3135
        # if we have all full previous txs, we *know* all the input amounts -> fine
3136
        if all([txin.utxo for txin in tx.inputs()]):
6✔
3137
            return TxSighashDanger(risk_level=rl.SAFE)
6✔
3138
        # a single segwit input -> fine
3139
        if len(tx.inputs()) == 1 and tx.inputs()[0].is_segwit() and tx.inputs()[0].witness_utxo:
6✔
3140
            return TxSighashDanger(risk_level=rl.SAFE)
6✔
3141
        # coinjoin or similar
3142
        if any([not self.is_mine(txin.address) for txin in tx.inputs()]):
6✔
3143
            messages = [(_("Warning") + ": "
×
3144
                         + _("The input amounts could not be verified as the previous transactions are missing.\n"
3145
                             "The amount of money being spent CANNOT be verified."))]
3146
            return TxSighashDanger(risk_level=rl.FEE_WARNING_NEEDCONFIRM, short_message=short_message, messages=messages)
×
3147
        # some inputs are legacy
3148
        if any([not txin.is_segwit() for txin in tx.inputs()]):
6✔
3149
            messages = [(_("Warning") + ": "
6✔
3150
                         + _("The fee could not be verified. Signing non-segwit inputs is risky:\n"
3151
                             "if this transaction was maliciously modified before you sign,\n"
3152
                             "you might end up paying a higher mining fee than displayed."))]
3153
            return TxSighashDanger(risk_level=rl.FEE_WARNING_NEEDCONFIRM, short_message=short_message, messages=messages)
6✔
3154
        # all inputs are segwit
3155
        # https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-August/014843.html
3156
        messages = [(_("Warning") + ": "
×
3157
                     + _("If you received this transaction from an untrusted device, "
3158
                         "do not accept to sign it more than once,\n"
3159
                         "otherwise you could end up paying a different fee."))]
3160
        return TxSighashDanger(risk_level=rl.FEE_WARNING_SKIPCONFIRM, short_message=short_message, messages=messages)
×
3161

3162
    def check_sighash(self, tx: 'PartialTransaction') -> TxSighashDanger:
6✔
3163
        """Checks the Sighash for my inputs and considers if the tx is safe to sign."""
3164
        assert isinstance(tx, PartialTransaction)
6✔
3165
        rl = TxSighashRiskLevel
6✔
3166
        hintmap = {
6✔
3167
            0:                    (rl.SAFE,           None),
3168
            Sighash.NONE:         (rl.INSANE_SIGHASH, _('Input {} is marked SIGHASH_NONE.')),
3169
            Sighash.SINGLE:       (rl.WEIRD_SIGHASH,  _('Input {} is marked SIGHASH_SINGLE.')),
3170
            Sighash.ALL:          (rl.SAFE,           None),
3171
            Sighash.ANYONECANPAY: (rl.WEIRD_SIGHASH,  _('Input {} is marked SIGHASH_ANYONECANPAY.')),
3172
        }
3173
        sighash_danger = TxSighashDanger()
6✔
3174
        for txin_idx, txin in enumerate(tx.inputs()):
6✔
3175
            if txin.sighash in (None, Sighash.ALL):
6✔
3176
                continue  # None will get converted to Sighash.ALL, so these values are safe
6✔
3177
            # found interesting sighash flag
3178
            addr = self.adb.get_txin_address(txin)
6✔
3179
            if self.is_mine(addr):
6✔
3180
                sh_base = txin.sighash & (Sighash.ANYONECANPAY ^ 0xff)
6✔
3181
                sh_acp = txin.sighash & Sighash.ANYONECANPAY
6✔
3182
                for sh in [sh_base, sh_acp]:
6✔
3183
                    if msg := hintmap[sh][1]:
6✔
3184
                        risk_level = hintmap[sh][0]
6✔
3185
                        header = _('Fatal') if TxSighashDanger(risk_level=risk_level).needs_reject() else _('Warning')
6✔
3186
                        shd = TxSighashDanger(
6✔
3187
                            risk_level=risk_level,
3188
                            short_message=_('Danger! This transaction uses non-default sighash flags!'),
3189
                            messages=[f"{header}: {msg.format(txin_idx)}"],
3190
                        )
3191
                        sighash_danger = sighash_danger.combine(shd)
6✔
3192
        if sighash_danger.needs_reject():  # no point for further tests
6✔
3193
            return sighash_danger
6✔
3194
        # if we show any fee to the user, check now how reliable that is:
3195
        if self.get_wallet_delta(tx).fee is not None:
6✔
3196
            shd = self._check_risk_of_burning_coins_as_fees(tx)
6✔
3197
            sighash_danger = sighash_danger.combine(shd)
6✔
3198
        return sighash_danger
6✔
3199

3200
    def get_tx_fee_warning(
6✔
3201
            self, *,
3202
            invoice_amt: int,
3203
            tx_size: int,
3204
            fee: int) -> Optional[Tuple[bool, str, str]]:
3205

3206
        assert invoice_amt >= 0, f"{invoice_amt=!r} must be non-negative satoshis"
×
3207
        assert fee >= 0, f"{fee=!r} must be non-negative satoshis"
×
3208
        feerate = Decimal(fee) / tx_size  # sat/byte
×
3209
        fee_ratio = Decimal(fee) / invoice_amt if invoice_amt else 0
×
3210
        long_warning = None
×
3211
        short_warning = None
×
3212
        allow_send = True
×
3213
        if feerate < self.relayfee() / 1000:
×
3214
            long_warning = ' '.join([
×
3215
                _("This transaction requires a higher fee, or it will not be propagated by your current server."),
3216
                _("Try to raise your transaction fee, or use a server with a lower relay fee.")
3217
            ])
3218
            short_warning = _("below relay fee") + "!"
×
3219
            allow_send = False
×
3220
        elif fee_ratio >= FEE_RATIO_HIGH_WARNING:
×
3221
            long_warning = ' '.join([
×
3222
                _("The fee for this transaction seems unusually high."),
3223
                _("({}% of amount)").format(f'{fee_ratio*100:.2f}')
3224
            ])
3225
            short_warning = _("high fee ratio") + "!"
×
3226
        elif feerate > FEERATE_WARNING_HIGH_FEE / 1000:
×
3227
            long_warning = ' '.join([
×
3228
                _("The fee for this transaction seems unusually high."),
3229
                _("(feerate: {})").format(self.config.format_fee_rate(1000 * feerate))
3230
            ])
3231
            short_warning = _("high fee rate") + "!"
×
3232
        if long_warning is None:
×
3233
            return None
×
3234
        else:
3235
            return allow_send, long_warning, short_warning
×
3236

3237
    def get_help_texts_for_receive_request(self, req: Request) -> ReceiveRequestHelp:
6✔
3238
        key = req.get_id()
×
3239
        addr = req.get_address() or ''
×
3240
        amount_sat = req.get_amount_sat() or 0
×
3241
        address_help = ''
×
3242
        URI_help = ''
×
3243
        ln_help = ''
×
3244
        address_is_error = False
×
3245
        URI_is_error = False
×
3246
        ln_is_error = False
×
3247
        ln_swap_suggestion = None
×
3248
        ln_rebalance_suggestion = None
×
3249
        URI = self.get_request_URI(req) or ''
×
3250
        lightning_online = self.lnworker and self.lnworker.num_peers() > 0
×
3251
        can_receive_lightning = self.lnworker and amount_sat <= self.lnworker.num_sats_can_receive()
×
3252
        status = self.get_invoice_status(req)
×
3253

3254
        if status == PR_EXPIRED:
×
3255
            address_help = URI_help = ln_help = _('This request has expired')
×
3256

3257
        is_amt_too_small_for_onchain = amount_sat and amount_sat < self.dust_threshold()
×
3258
        if not addr:
×
3259
            address_is_error = True
×
3260
            address_help = _('This request cannot be paid on-chain')
×
3261
            if is_amt_too_small_for_onchain:
×
3262
                address_help = _('Amount too small to be received onchain')
×
3263
        if not URI:
×
3264
            URI_is_error = True
×
3265
            URI_help = _('This request cannot be paid on-chain')
×
3266
            if is_amt_too_small_for_onchain:
×
3267
                URI_help = _('Amount too small to be received onchain')
×
3268
        if not req.is_lightning():
×
3269
            ln_is_error = True
×
3270
            ln_help = _('This request does not have a Lightning invoice.')
×
3271

3272
        if status == PR_UNPAID:
×
3273
            if self.adb.is_used(addr):
×
3274
                address_help = URI_help = (_("This address has already been used. "
×
3275
                                             "For better privacy, do not reuse it for new payments."))
3276
            if req.is_lightning():
×
3277
                if not lightning_online:
×
3278
                    ln_is_error = True
×
3279
                    ln_help = _('You must be online to receive Lightning payments.')
×
3280
                elif not can_receive_lightning:
×
3281
                    ln_is_error = True
×
3282
                    ln_rebalance_suggestion = self.lnworker.suggest_rebalance_to_receive(amount_sat)
×
3283
                    ln_swap_suggestion = self.lnworker.suggest_swap_to_receive(amount_sat)
×
3284
                    ln_help = _('You do not have the capacity to receive this amount with Lightning.')
×
3285
                    if bool(ln_rebalance_suggestion):
×
3286
                        ln_help += '\n\n' + _('You may have that capacity if you rebalance your channels.')
×
3287
                    elif bool(ln_swap_suggestion):
×
3288
                        ln_help += '\n\n' + _('You may have that capacity if you swap some of your funds.')
×
3289
        return ReceiveRequestHelp(
×
3290
            address_help=address_help,
3291
            URI_help=URI_help,
3292
            ln_help=ln_help,
3293
            address_is_error=address_is_error,
3294
            URI_is_error=URI_is_error,
3295
            ln_is_error=ln_is_error,
3296
            ln_rebalance_suggestion=ln_rebalance_suggestion,
3297
            ln_swap_suggestion=ln_swap_suggestion,
3298
        )
3299

3300

3301
    def synchronize(self) -> int:
6✔
3302
        """Returns the number of new addresses we generated."""
3303
        return 0
6✔
3304

3305
    def unlock(self, password):
6✔
3306
        self.logger.info(f'unlocking wallet')
×
3307
        self.check_password(password)
×
3308
        self._password_in_memory = password
×
3309

3310
    def get_unlocked_password(self):
6✔
3311
        return self._password_in_memory
×
3312

3313

3314
class Simple_Wallet(Abstract_Wallet):
6✔
3315
    # wallet with a single keystore
3316

3317
    def is_watching_only(self):
6✔
3318
        return self.keystore.is_watching_only()
6✔
3319

3320
    def _update_password_for_keystore(self, old_pw, new_pw):
6✔
3321
        if self.keystore and self.keystore.may_have_password():
6✔
3322
            self.keystore.update_password(old_pw, new_pw)
6✔
3323
            self.save_keystore()
6✔
3324

3325
    def save_keystore(self):
6✔
3326
        self.db.put('keystore', self.keystore.dump())
6✔
3327

3328
    @abstractmethod
6✔
3329
    def get_public_key(self, address: str) -> Optional[str]:
6✔
3330
        pass
×
3331

3332
    def get_public_keys(self, address: str) -> Sequence[str]:
6✔
3333
        pk = self.get_public_key(address)
×
3334
        return [pk] if pk else []
×
3335

3336

3337
class Imported_Wallet(Simple_Wallet):
6✔
3338
    # wallet made of imported addresses
3339

3340
    wallet_type = 'imported'
6✔
3341
    txin_type = 'address'
6✔
3342

3343
    def __init__(self, db, *, config):
6✔
3344
        Abstract_Wallet.__init__(self, db, config=config)
6✔
3345
        self.use_change = db.get('use_change', False)
6✔
3346

3347
    def is_watching_only(self):
6✔
3348
        return self.keystore is None
6✔
3349

3350
    def can_import_privkey(self):
6✔
3351
        return bool(self.keystore)
6✔
3352

3353
    def load_keystore(self):
6✔
3354
        self.keystore = load_keystore(self.db, 'keystore') if self.db.get('keystore') else None
6✔
3355

3356
    def save_keystore(self):
6✔
3357
        self.db.put('keystore', self.keystore.dump())
6✔
3358

3359
    def can_import_address(self):
6✔
3360
        return self.is_watching_only()
×
3361

3362
    def can_delete_address(self):
6✔
3363
        return True
×
3364

3365
    def has_seed(self):
6✔
3366
        return False
6✔
3367

3368
    def is_deterministic(self):
6✔
3369
        return False
×
3370

3371
    def is_change(self, address):
6✔
3372
        return False
6✔
3373

3374
    def get_all_known_addresses_beyond_gap_limit(self) -> Set[str]:
6✔
3375
        return set()
×
3376

3377
    def get_fingerprint(self):
6✔
3378
        return ''
×
3379

3380
    def get_addresses(self):
6✔
3381
        # note: overridden so that the history can be cleared
3382
        return self.db.get_imported_addresses()
6✔
3383

3384
    def get_receiving_addresses(self, **kwargs):
6✔
3385
        return self.get_addresses()
6✔
3386

3387
    def get_change_addresses(self, **kwargs):
6✔
3388
        return self.get_addresses()
6✔
3389

3390
    def import_addresses(self, addresses: List[str], *,
6✔
3391
                         write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:
3392
        good_addr = []  # type: List[str]
6✔
3393
        bad_addr = []  # type: List[Tuple[str, str]]
6✔
3394
        for address in addresses:
6✔
3395
            if not bitcoin.is_address(address):
6✔
3396
                bad_addr.append((address, _('invalid address')))
×
3397
                continue
×
3398
            if self.db.has_imported_address(address):
6✔
3399
                bad_addr.append((address, _('address already in wallet')))
×
3400
                continue
×
3401
            good_addr.append(address)
6✔
3402
            self.db.add_imported_address(address, {})
6✔
3403
            self.adb.add_address(address)
6✔
3404
        if write_to_disk:
6✔
3405
            self.save_db()
6✔
3406
        return good_addr, bad_addr
6✔
3407

3408
    def import_address(self, address: str) -> str:
6✔
3409
        good_addr, bad_addr = self.import_addresses([address])
6✔
3410
        if good_addr and good_addr[0] == address:
6✔
3411
            return address
6✔
3412
        else:
3413
            raise BitcoinException(str(bad_addr[0][1]))
×
3414

3415
    def delete_address(self, address: str) -> None:
6✔
3416
        if not self.db.has_imported_address(address):
6✔
3417
            return
×
3418
        if len(self.get_addresses()) <= 1:
6✔
3419
            raise UserFacingException(_('Cannot delete last remaining address from wallet'))
6✔
3420
        transactions_to_remove = set()  # only referred to by this address
6✔
3421
        transactions_new = set()  # txs that are not only referred to by address
6✔
3422
        with self.lock:
6✔
3423
            for addr in self.db.get_history():
6✔
3424
                details = self.adb.get_address_history(addr).items()
6✔
3425
                if addr == address:
6✔
3426
                    for tx_hash, height in details:
6✔
3427
                        transactions_to_remove.add(tx_hash)
6✔
3428
                else:
3429
                    for tx_hash, height in details:
6✔
3430
                        transactions_new.add(tx_hash)
6✔
3431
            transactions_to_remove -= transactions_new
6✔
3432
            self.db.remove_addr_history(address)
6✔
3433
            for tx_hash in transactions_to_remove:
6✔
3434
                self.adb._remove_transaction(tx_hash)
6✔
3435
        self.set_label(address, None)
6✔
3436
        if req:= self.get_request_by_addr(address):
6✔
3437
            self.delete_request(req.get_id())
×
3438
        self.set_frozen_state_of_addresses([address], False, write_to_disk=False)
6✔
3439
        pubkey = self.get_public_key(address)
6✔
3440
        self.db.remove_imported_address(address)
6✔
3441
        if pubkey:
6✔
3442
            # delete key iff no other address uses it (e.g. p2pkh and p2wpkh for same key)
3443
            for txin_type in bitcoin.WIF_SCRIPT_TYPES.keys():
6✔
3444
                try:
6✔
3445
                    addr2 = bitcoin.pubkey_to_address(txin_type, pubkey)
6✔
3446
                except NotImplementedError:
6✔
3447
                    pass
6✔
3448
                else:
3449
                    if self.db.has_imported_address(addr2):
6✔
3450
                        break
×
3451
            else:
3452
                self.keystore.delete_imported_key(pubkey)
6✔
3453
                self.save_keystore()
6✔
3454
        self.save_db()
6✔
3455

3456
    def get_change_addresses_for_new_transaction(self, *args, **kwargs) -> List[str]:
6✔
3457
        # for an imported wallet, if all "change addresses" are already used,
3458
        # it is probably better to send change back to the "from address", than to
3459
        # send it to another random used address and link them together, hence
3460
        # we force "allow_reusing_used_change_addrs=False"
3461
        return super().get_change_addresses_for_new_transaction(
6✔
3462
            *args,
3463
            **{**kwargs, "allow_reusing_used_change_addrs": False},
3464
        )
3465

3466
    def calc_unused_change_addresses(self) -> Sequence[str]:
6✔
3467
        with self.lock:
6✔
3468
            unused_addrs = [addr for addr in self.get_change_addresses()
6✔
3469
                            if not self.adb.is_used(addr) and not self.is_address_reserved(addr)]
3470
            return unused_addrs
6✔
3471

3472
    def is_mine(self, address) -> bool:
6✔
3473
        if not address: return False
6✔
3474
        return self.db.has_imported_address(address)
6✔
3475

3476
    def get_address_index(self, address) -> Optional[str]:
6✔
3477
        # returns None if address is not mine
3478
        return self.get_public_key(address)
6✔
3479

3480
    def get_address_path_str(self, address):
6✔
3481
        return None
×
3482

3483
    def get_public_key(self, address) -> Optional[str]:
6✔
3484
        x = self.db.get_imported_address(address)
6✔
3485
        return x.get('pubkey') if x else None
6✔
3486

3487
    def import_private_keys(self, keys: List[str], password: Optional[str], *,
6✔
3488
                            write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:
3489
        good_addr = []  # type: List[str]
6✔
3490
        bad_keys = []  # type: List[Tuple[str, str]]
6✔
3491
        for key in keys:
6✔
3492
            try:
6✔
3493
                txin_type, pubkey = self.keystore.import_privkey(key, password)
6✔
3494
            except Exception as e:
6✔
3495
                bad_keys.append((key, _('invalid private key') + f': {e}'))
6✔
3496
                continue
6✔
3497
            if txin_type not in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
6✔
3498
                bad_keys.append((key, _('not implemented type') + f': {txin_type}'))
×
3499
                continue
×
3500
            addr = bitcoin.pubkey_to_address(txin_type, pubkey)
6✔
3501
            good_addr.append(addr)
6✔
3502
            self.db.add_imported_address(addr, {'type':txin_type, 'pubkey':pubkey})
6✔
3503
            self.adb.add_address(addr)
6✔
3504
        self.save_keystore()
6✔
3505
        if write_to_disk:
6✔
3506
            self.save_db()
6✔
3507
        return good_addr, bad_keys
6✔
3508

3509
    def import_private_key(self, key: str, password: Optional[str]) -> str:
6✔
3510
        good_addr, bad_keys = self.import_private_keys([key], password=password)
6✔
3511
        if good_addr:
6✔
3512
            return good_addr[0]
6✔
3513
        else:
3514
            raise BitcoinException(str(bad_keys[0][1]))
6✔
3515

3516
    def get_txin_type(self, address):
6✔
3517
        return self.db.get_imported_address(address).get('type', 'address')
6✔
3518

3519
    @profiler
6✔
3520
    def try_detecting_internal_addresses_corruption(self):
6✔
3521
        # we check only a random sample, for performance
3522
        addresses_all = self.get_addresses()
×
3523
        # some random *used* addresses (note: we likely have not synced yet)
3524
        addresses_used = [addr for addr in addresses_all if self.adb.is_used(addr)]
×
3525
        sample1 = random.sample(addresses_used, min(len(addresses_used), 10))
×
3526
        # some random *unused* addresses
3527
        addresses_unused = [addr for addr in addresses_all if not self.adb.is_used(addr)]
×
3528
        sample2 = random.sample(addresses_unused, min(len(addresses_unused), 10))
×
3529
        for addr_found in itertools.chain(sample1, sample2):
×
3530
            self.check_address_for_corruption(addr_found)
×
3531

3532
    def check_address_for_corruption(self, addr):
6✔
3533
        if addr and self.is_mine(addr):
6✔
3534
            pubkey = self.get_public_key(addr)
6✔
3535
            if not pubkey:
6✔
3536
                return
×
3537
            txin_type = self.get_txin_type(addr)
6✔
3538
            if txin_type == 'address':
6✔
3539
                return
×
3540
            if addr != bitcoin.pubkey_to_address(txin_type, pubkey):
6✔
3541
                raise InternalAddressCorruption()
×
3542

3543
    def pubkeys_to_address(self, pubkeys):
6✔
3544
        pubkey = pubkeys[0]
×
3545
        # FIXME This is slow.
3546
        #       Ideally we would re-derive the address from the pubkey and the txin_type,
3547
        #       but we don't know the txin_type, and we only have an addr->txin_type map.
3548
        #       so instead a linear search of reverse-lookups is done...
3549
        for addr in self.db.get_imported_addresses():
×
3550
            if self.db.get_imported_address(addr)['pubkey'] == pubkey:
×
3551
                return addr
×
3552
        return None
×
3553

3554
    def decrypt_message(self, pubkey: str, message, password) -> bytes:
6✔
3555
        # this is significantly faster than the implementation in the superclass
3556
        return self.keystore.decrypt_message(pubkey, message, password)
6✔
3557

3558

3559
class Deterministic_Wallet(Abstract_Wallet):
6✔
3560

3561
    def __init__(self, db, *, config):
6✔
3562
        self._ephemeral_addr_to_addr_index = {}  # type: Dict[str, Sequence[int]]
6✔
3563
        Abstract_Wallet.__init__(self, db, config=config)
6✔
3564
        self.gap_limit = db.get('gap_limit', 20)
6✔
3565
        # generate addresses now. note that without libsecp this might block
3566
        # for a few seconds!
3567
        self.synchronize()
6✔
3568

3569
    def _init_lnworker(self):
6✔
3570
        # lightning_privkey2 is not deterministic (legacy wallets, bip39)
3571
        ln_xprv = self.db.get('lightning_xprv') or self.db.get('lightning_privkey2')
6✔
3572
        # lnworker can only be initialized once receiving addresses are available
3573
        # therefore we instantiate lnworker in DeterministicWallet
3574
        self.lnworker = LNWallet(self, ln_xprv) if ln_xprv else None
6✔
3575

3576
    def has_seed(self):
6✔
3577
        return self.keystore.has_seed()
6✔
3578

3579
    def get_addresses(self):
6✔
3580
        # note: overridden so that the history can be cleared.
3581
        # addresses are ordered based on derivation
3582
        out = self.get_receiving_addresses()
6✔
3583
        out += self.get_change_addresses()
6✔
3584
        return out
6✔
3585

3586
    def get_receiving_addresses(self, *, slice_start=None, slice_stop=None):
6✔
3587
        return self.db.get_receiving_addresses(slice_start=slice_start, slice_stop=slice_stop)
6✔
3588

3589
    def get_change_addresses(self, *, slice_start=None, slice_stop=None):
6✔
3590
        return self.db.get_change_addresses(slice_start=slice_start, slice_stop=slice_stop)
6✔
3591

3592
    @profiler
6✔
3593
    def try_detecting_internal_addresses_corruption(self):
6✔
3594
        addresses_all = self.get_addresses()
×
3595
        # first few addresses
3596
        nfirst_few = 10
×
3597
        sample1 = addresses_all[:nfirst_few]
×
3598
        # some random *used* addresses (note: we likely have not synced yet)
3599
        addresses_used = [addr for addr in addresses_all[nfirst_few:] if self.adb.is_used(addr)]
×
3600
        sample2 = random.sample(addresses_used, min(len(addresses_used), 10))
×
3601
        # some random *unused* addresses
3602
        addresses_unused = [addr for addr in addresses_all[nfirst_few:] if not self.adb.is_used(addr)]
×
3603
        sample3 = random.sample(addresses_unused, min(len(addresses_unused), 10))
×
3604
        for addr_found in itertools.chain(sample1, sample2, sample3):
×
3605
            self.check_address_for_corruption(addr_found)
×
3606

3607
    def check_address_for_corruption(self, addr):
6✔
3608
        if addr and self.is_mine(addr):
6✔
3609
            if addr != self.derive_address(*self.get_address_index(addr)):
6✔
3610
                raise InternalAddressCorruption()
×
3611

3612
    def get_seed(self, password):
6✔
3613
        return self.keystore.get_seed(password)
6✔
3614

3615
    def get_seed_type(self) -> Optional[str]:
6✔
3616
        if not self.has_seed():
×
3617
            return None
×
3618
        assert isinstance(self.keystore, keystore.Deterministic_KeyStore), type(self.keystore)
×
3619
        return self.keystore.get_seed_type()
×
3620

3621
    def change_gap_limit(self, value):
6✔
3622
        '''This method is not called in the code, it is kept for console use'''
3623
        value = int(value)
×
3624
        if value >= self.min_acceptable_gap():
×
3625
            self.gap_limit = value
×
3626
            self.db.put('gap_limit', self.gap_limit)
×
3627
            self.save_db()
×
3628
            return True
×
3629
        else:
3630
            return False
×
3631

3632
    def num_unused_trailing_addresses(self, addresses):
6✔
3633
        k = 0
×
3634
        for addr in addresses[::-1]:
×
3635
            if self.db.get_addr_history(addr):
×
3636
                break
×
3637
            k += 1
×
3638
        return k
×
3639

3640
    def min_acceptable_gap(self) -> int:
6✔
3641
        # fixme: this assumes wallet is synchronized
3642
        n = 0
×
3643
        nmax = 0
×
3644
        addresses = self.get_receiving_addresses()
×
3645
        k = self.num_unused_trailing_addresses(addresses)
×
3646
        for addr in addresses[0:-k]:
×
3647
            if self.adb.address_is_old(addr):
×
3648
                n = 0
×
3649
            else:
3650
                n += 1
×
3651
                nmax = max(nmax, n)
×
3652
        return nmax + 1
×
3653

3654
    @abstractmethod
6✔
3655
    def derive_pubkeys(self, c: int, i: int) -> Sequence[str]:
6✔
3656
        pass
×
3657

3658
    def derive_address(self, for_change: int, n: int) -> str:
6✔
3659
        for_change = int(for_change)
6✔
3660
        pubkeys = self.derive_pubkeys(for_change, n)
6✔
3661
        return self.pubkeys_to_address(pubkeys)
6✔
3662

3663
    def export_private_key_for_path(self, path: Union[Sequence[int], str], password: Optional[str]) -> str:
6✔
3664
        if isinstance(path, str):
6✔
3665
            path = convert_bip32_strpath_to_intpath(path)
6✔
3666
        pk, compressed = self.keystore.get_private_key(path, password)
6✔
3667
        txin_type = self.get_txin_type()  # assumes no mixed-scripts in wallet
6✔
3668
        return bitcoin.serialize_privkey(pk, compressed, txin_type)
6✔
3669

3670
    def get_public_keys_with_deriv_info(self, address: str):
6✔
3671
        der_suffix = self.get_address_index(address)
6✔
3672
        der_suffix = [int(x) for x in der_suffix]
6✔
3673
        return {k.derive_pubkey(*der_suffix): (k, der_suffix)
6✔
3674
                for k in self.get_keystores()}
3675

3676
    def _add_txinout_derivation_info(self, txinout, address, *, only_der_suffix):
6✔
3677
        if not self.is_mine(address):
6✔
3678
            return
×
3679
        pubkey_deriv_info = self.get_public_keys_with_deriv_info(address)
6✔
3680
        for pubkey in pubkey_deriv_info:
6✔
3681
            ks, der_suffix = pubkey_deriv_info[pubkey]
6✔
3682
            fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix,
6✔
3683
                                                                                   only_der_suffix=only_der_suffix)
3684
            txinout.bip32_paths[pubkey] = (fp_bytes, der_full)
6✔
3685

3686
    def create_new_address(self, for_change: bool = False):
6✔
3687
        assert type(for_change) is bool
6✔
3688
        with self.lock:
6✔
3689
            n = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses()
6✔
3690
            address = self.derive_address(int(for_change), n)
6✔
3691
            self.db.add_change_address(address) if for_change else self.db.add_receiving_address(address)
6✔
3692
            self.adb.add_address(address)
6✔
3693
            if for_change:
6✔
3694
                # note: if it's actually "old", it will get filtered later
3695
                self._not_old_change_addresses.append(address)
6✔
3696
            return address
6✔
3697

3698
    def synchronize_sequence(self, for_change: bool) -> int:
6✔
3699
        count = 0  # num new addresses we generated
6✔
3700
        limit = self.gap_limit_for_change if for_change else self.gap_limit
6✔
3701
        while True:
5✔
3702
            num_addr = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses()
6✔
3703
            if num_addr < limit:
6✔
3704
                count += 1
6✔
3705
                self.create_new_address(for_change)
6✔
3706
                continue
6✔
3707
            if for_change:
6✔
3708
                last_few_addresses = self.get_change_addresses(slice_start=-limit)
6✔
3709
            else:
3710
                last_few_addresses = self.get_receiving_addresses(slice_start=-limit)
6✔
3711
            if any(map(self.adb.address_is_old, last_few_addresses)):
6✔
3712
                count += 1
6✔
3713
                self.create_new_address(for_change)
6✔
3714
            else:
3715
                break
5✔
3716
        return count
6✔
3717

3718
    def synchronize(self):
6✔
3719
        count = 0
6✔
3720
        with self.lock:
6✔
3721
            count += self.synchronize_sequence(False)
6✔
3722
            count += self.synchronize_sequence(True)
6✔
3723
        return count
6✔
3724

3725
    def get_all_known_addresses_beyond_gap_limit(self):
6✔
3726
        # note that we don't stop at first large gap
3727
        found = set()
×
3728

3729
        def process_addresses(addrs, gap_limit):
×
3730
            rolling_num_unused = 0
×
3731
            for addr in addrs:
×
3732
                if self.db.get_addr_history(addr):
×
3733
                    rolling_num_unused = 0
×
3734
                else:
3735
                    if rolling_num_unused >= gap_limit:
×
3736
                        found.add(addr)
×
3737
                    rolling_num_unused += 1
×
3738

3739
        process_addresses(self.get_receiving_addresses(), self.gap_limit)
×
3740
        process_addresses(self.get_change_addresses(), self.gap_limit_for_change)
×
3741
        return found
×
3742

3743
    def get_address_index(self, address) -> Optional[Sequence[int]]:
6✔
3744
        return self.db.get_address_index(address) or self._ephemeral_addr_to_addr_index.get(address)
6✔
3745

3746
    def get_address_path_str(self, address):
6✔
3747
        intpath = self.get_address_index(address)
×
3748
        if intpath is None:
×
3749
            return None
×
3750
        return convert_bip32_intpath_to_strpath(intpath)
×
3751

3752
    def _learn_derivation_path_for_address_from_txinout(self, txinout, address):
6✔
3753
        for ks in self.get_keystores():
6✔
3754
            pubkey, der_suffix = ks.find_my_pubkey_in_txinout(txinout, only_der_suffix=True)
6✔
3755
            if der_suffix is not None:
6✔
3756
                # note: we already know the pubkey belongs to the keystore,
3757
                #       but the script template might be different
3758
                if len(der_suffix) != 2: continue
6✔
3759
                try:
6✔
3760
                    my_address = self.derive_address(*der_suffix)
6✔
3761
                except CannotDerivePubkey:
×
3762
                    my_address = None
×
3763
                if my_address == address:
6✔
3764
                    self._ephemeral_addr_to_addr_index[address] = list(der_suffix)
6✔
3765
                    return True
6✔
3766
        return False
6✔
3767

3768
    def get_master_public_keys(self):
6✔
3769
        return [self.get_master_public_key()]
×
3770

3771
    def get_fingerprint(self):
6✔
3772
        return self.get_master_public_key()
6✔
3773

3774
    def get_txin_type(self, address=None):
6✔
3775
        return self.txin_type
6✔
3776

3777

3778
class Standard_Wallet(Simple_Wallet, Deterministic_Wallet):
6✔
3779
    """ Deterministic Wallet with a single pubkey per address """
3780
    wallet_type = 'standard'
6✔
3781

3782
    def __init__(self, db, *, config):
6✔
3783
        Deterministic_Wallet.__init__(self, db, config=config)
6✔
3784

3785
    def get_public_key(self, address):
6✔
3786
        sequence = self.get_address_index(address)
×
3787
        pubkeys = self.derive_pubkeys(*sequence)
×
3788
        return pubkeys[0]
×
3789

3790
    def load_keystore(self):
6✔
3791
        self.keystore = load_keystore(self.db, 'keystore')  # type: KeyStoreWithMPK
6✔
3792
        try:
6✔
3793
            xtype = bip32.xpub_type(self.keystore.xpub)
6✔
3794
        except Exception:
6✔
3795
            xtype = 'standard'
6✔
3796
        self.txin_type = 'p2pkh' if xtype == 'standard' else xtype
6✔
3797

3798
    def get_master_public_key(self):
6✔
3799
        return self.keystore.get_master_public_key()
6✔
3800

3801
    def derive_pubkeys(self, c, i):
6✔
3802
        return [self.keystore.derive_pubkey(c, i).hex()]
6✔
3803

3804
    def pubkeys_to_address(self, pubkeys):
6✔
3805
        pubkey = pubkeys[0]
6✔
3806
        return bitcoin.pubkey_to_address(self.txin_type, pubkey)
6✔
3807

3808
    def has_support_for_slip_19_ownership_proofs(self) -> bool:
6✔
3809
        return self.keystore.has_support_for_slip_19_ownership_proofs()
×
3810

3811
    def add_slip_19_ownership_proofs_to_tx(self, tx: PartialTransaction) -> None:
6✔
3812
        tx.add_info_from_wallet(self)
×
3813
        self.keystore.add_slip_19_ownership_proofs_to_tx(tx=tx, password=None)
×
3814

3815

3816
class Multisig_Wallet(Deterministic_Wallet):
6✔
3817
    # generic m of n
3818

3819
    def __init__(self, db, *, config):
6✔
3820
        self.wallet_type = db.get('wallet_type')
6✔
3821
        self.m, self.n = multisig_type(self.wallet_type)
6✔
3822
        Deterministic_Wallet.__init__(self, db, config=config)
6✔
3823
        # sanity checks
3824
        for ks in self.get_keystores():
6✔
3825
            if not isinstance(ks, keystore.Xpub):
6✔
3826
                raise Exception(f"unexpected keystore type={type(ks)} in multisig")
6✔
3827
            if bip32.xpub_type(self.keystore.xpub) != bip32.xpub_type(ks.xpub):
6✔
3828
                raise Exception(f"multisig wallet needs to have homogeneous xpub types")
6✔
3829
        bip32_nodes = set({ks.get_bip32_node_for_xpub() for ks in self.get_keystores()})
6✔
3830
        if len(bip32_nodes) != len(self.get_keystores()):
6✔
3831
            raise Exception(f"duplicate xpubs in multisig")
6✔
3832

3833
    def get_public_keys(self, address):
6✔
3834
        return [pk.hex() for pk in self.get_public_keys_with_deriv_info(address)]
×
3835

3836
    def pubkeys_to_address(self, pubkeys):
6✔
3837
        redeem_script = self.pubkeys_to_scriptcode(pubkeys)
6✔
3838
        return bitcoin.redeem_script_to_address(self.txin_type, redeem_script)
6✔
3839

3840
    def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> bytes:
6✔
3841
        return transaction.multisig_script(sorted(pubkeys), self.m)
6✔
3842

3843
    def derive_pubkeys(self, c, i):
6✔
3844
        return [k.derive_pubkey(c, i).hex() for k in self.get_keystores()]
6✔
3845

3846
    def load_keystore(self):
6✔
3847
        self.keystores = {}
6✔
3848
        for i in range(self.n):
6✔
3849
            name = 'x%d'%(i+1)
6✔
3850
            self.keystores[name] = load_keystore(self.db, name)
6✔
3851
        self.keystore = self.keystores['x1']
6✔
3852
        xtype = bip32.xpub_type(self.keystore.xpub)
6✔
3853
        self.txin_type = 'p2sh' if xtype == 'standard' else xtype
6✔
3854

3855
    def save_keystore(self):
6✔
3856
        for name, k in self.keystores.items():
×
3857
            self.db.put(name, k.dump())
×
3858

3859
    def get_keystore(self):
6✔
3860
        return self.keystores.get('x1')
×
3861

3862
    def get_keystores(self):
6✔
3863
        return [self.keystores[i] for i in sorted(self.keystores.keys())]
6✔
3864

3865
    def can_have_keystore_encryption(self):
6✔
3866
        return any([k.may_have_password() for k in self.get_keystores()])
×
3867

3868
    def _update_password_for_keystore(self, old_pw, new_pw):
6✔
3869
        for name, keystore in self.keystores.items():
×
3870
            if keystore.may_have_password():
×
3871
                keystore.update_password(old_pw, new_pw)
×
3872
                self.db.put(name, keystore.dump())
×
3873

3874
    def check_password(self, password):
6✔
3875
        for name, keystore in self.keystores.items():
×
3876
            if keystore.may_have_password():
×
3877
                keystore.check_password(password)
×
3878
        if self.has_storage_encryption():
×
3879
            self.storage.check_password(password)
×
3880

3881
    def get_available_storage_encryption_version(self):
6✔
3882
        # multisig wallets are not offered hw device encryption
3883
        return StorageEncryptionVersion.USER_PASSWORD
×
3884

3885
    def has_seed(self):
6✔
3886
        return self.keystore.has_seed()
×
3887

3888
    def is_watching_only(self):
6✔
3889
        return all([k.is_watching_only() for k in self.get_keystores()])
6✔
3890

3891
    def get_master_public_key(self):
6✔
3892
        return self.keystore.get_master_public_key()
×
3893

3894
    def get_master_public_keys(self):
6✔
3895
        return [k.get_master_public_key() for k in self.get_keystores()]
×
3896

3897
    def get_fingerprint(self):
6✔
3898
        return ''.join(sorted(self.get_master_public_keys()))
×
3899

3900

3901
wallet_types = ['standard', 'multisig', 'imported']
6✔
3902

3903
def register_wallet_type(category):
6✔
3904
    wallet_types.append(category)
6✔
3905

3906
wallet_constructors = {
6✔
3907
    'standard': Standard_Wallet,
3908
    'old': Standard_Wallet,
3909
    'xpub': Standard_Wallet,
3910
    'imported': Imported_Wallet
3911
}
3912

3913
def register_constructor(wallet_type, constructor):
6✔
3914
    wallet_constructors[wallet_type] = constructor
6✔
3915

3916
# former WalletFactory
3917
class Wallet(object):
6✔
3918
    """The main wallet "entry point".
3919
    This class is actually a factory that will return a wallet of the correct
3920
    type when passed a WalletStorage instance."""
3921

3922
    def __new__(self, db: 'WalletDB', *, config: SimpleConfig):
6✔
3923
        wallet_type = db.get('wallet_type')
6✔
3924
        WalletClass = Wallet.wallet_class(wallet_type)
6✔
3925
        wallet = WalletClass(db, config=config)
6✔
3926
        return wallet
6✔
3927

3928
    @staticmethod
6✔
3929
    def wallet_class(wallet_type):
6✔
3930
        if multisig_type(wallet_type):
6✔
3931
            return Multisig_Wallet
6✔
3932
        if wallet_type in wallet_constructors:
6✔
3933
            return wallet_constructors[wallet_type]
6✔
3934
        raise WalletFileException("Unknown wallet type: " + str(wallet_type))
×
3935

3936

3937
def create_new_wallet(*, path, config: SimpleConfig, passphrase=None, password=None,
6✔
3938
                      encrypt_file=True, seed_type=None, gap_limit=None) -> dict:
3939
    """Create a new wallet"""
3940
    storage = WalletStorage(path)
6✔
3941
    if storage.file_exists():
6✔
3942
        raise UserFacingException("Remove the existing wallet first!")
×
3943
    db = WalletDB('', storage=storage, upgrade=True)
6✔
3944

3945
    seed = Mnemonic('en').make_seed(seed_type=seed_type)
6✔
3946
    k = keystore.from_seed(seed, passphrase=passphrase)
6✔
3947
    db.put('keystore', k.dump())
6✔
3948
    db.put('wallet_type', 'standard')
6✔
3949
    if k.can_have_deterministic_lightning_xprv():
6✔
3950
        db.put('lightning_xprv', k.get_lightning_xprv(None))
6✔
3951
    if gap_limit is not None:
6✔
3952
        db.put('gap_limit', gap_limit)
6✔
3953
    wallet = Wallet(db, config=config)
6✔
3954
    wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)
6✔
3955
    wallet.synchronize()
6✔
3956
    msg = "Please keep your seed in a safe place; if you lose it, you will not be able to restore your wallet."
6✔
3957
    wallet.save_db()
6✔
3958
    return {'seed': seed, 'wallet': wallet, 'msg': msg}
6✔
3959

3960

3961
def restore_wallet_from_text(
6✔
3962
    text: str,
3963
    *,
3964
    path: Optional[str],
3965
    config: SimpleConfig,
3966
    passphrase: Optional[str] = None,
3967
    password: Optional[str] = None,
3968
    encrypt_file: Optional[bool] = None,
3969
    gap_limit: Optional[int] = None,
3970
) -> dict:
3971
    """Restore a wallet from text. Text can be a seed phrase, a master
3972
    public key, a master private key, a list of bitcoin addresses
3973
    or bitcoin private keys."""
3974
    if path is None:  # create wallet in-memory
6✔
3975
        storage = None
6✔
3976
    else:
3977
        storage = WalletStorage(path)
6✔
3978
        if storage.file_exists():
6✔
3979
            raise UserFacingException("Remove the existing wallet first!")
×
3980
    if encrypt_file is None:
6✔
3981
        encrypt_file = True
6✔
3982
    db = WalletDB('', storage=storage, upgrade=True)
6✔
3983
    text = text.strip()
6✔
3984
    if keystore.is_address_list(text):
6✔
3985
        wallet = Imported_Wallet(db, config=config)
6✔
3986
        addresses = text.split()
6✔
3987
        good_inputs, bad_inputs = wallet.import_addresses(addresses, write_to_disk=False)
6✔
3988
        # FIXME tell user about bad_inputs
3989
        if not good_inputs:
6✔
3990
            raise UserFacingException("None of the given addresses can be imported")
×
3991
    elif keystore.is_private_key_list(text, allow_spaces_inside_key=False):
6✔
3992
        k = keystore.Imported_KeyStore({})
6✔
3993
        db.put('keystore', k.dump())
6✔
3994
        wallet = Imported_Wallet(db, config=config)
6✔
3995
        keys = keystore.get_private_keys(text, allow_spaces_inside_key=False)
6✔
3996
        good_inputs, bad_inputs = wallet.import_private_keys(keys, None, write_to_disk=False)
6✔
3997
        # FIXME tell user about bad_inputs
3998
        if not good_inputs:
6✔
3999
            raise UserFacingException("None of the given privkeys can be imported")
×
4000
    else:
4001
        if keystore.is_master_key(text):
6✔
4002
            k = keystore.from_master_key(text)
6✔
4003
        elif keystore.is_seed(text):
6✔
4004
            k = keystore.from_seed(text, passphrase=passphrase)
6✔
4005
            if k.can_have_deterministic_lightning_xprv():
6✔
4006
                db.put('lightning_xprv', k.get_lightning_xprv(None))
6✔
4007
        else:
4008
            raise UserFacingException("Seed or key not recognized")
×
4009
        db.put('keystore', k.dump())
6✔
4010
        db.put('wallet_type', 'standard')
6✔
4011
        if gap_limit is not None:
6✔
4012
            db.put('gap_limit', gap_limit)
6✔
4013
        wallet = Wallet(db, config=config)
6✔
4014
    if db.storage:
6✔
4015
        assert not db.storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk"
6✔
4016
    wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)
6✔
4017
    wallet.synchronize()
6✔
4018
    msg = ("This wallet was restored offline. It may contain more addresses than displayed. "
6✔
4019
           "Start a daemon and use load_wallet to sync its history.")
4020
    wallet.save_db()
6✔
4021
    return {'wallet': wallet, 'msg': msg}
6✔
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