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

spesmilo / electrum / 5304010765238272

17 Aug 2023 02:17PM UTC coverage: 59.027% (+0.02%) from 59.008%
5304010765238272

Pull #8493

CirrusCI

ecdsa
storage.append: fail if the file length is not what we expect
Pull Request #8493: partial-writes using jsonpatch

165 of 165 new or added lines in 9 files covered. (100.0%)

18653 of 31601 relevant lines covered (59.03%)

2.95 hits per line

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

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

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

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

50
from aiorpcx import timeout_after, TaskTimeout, ignore_after, run_in_thread
5✔
51

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

90
if TYPE_CHECKING:
5✔
91
    from .network import Network
×
92
    from .exchange_rate import FxThread
×
93

94

95
_logger = get_logger(__name__)
5✔
96

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

104

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

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

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

136

137
async def sweep_preparations(privkeys, network: 'Network', imax=100):
5✔
138

139
    async def find_utxos_for_privkey(txin_type, privkey, compressed):
5✔
140
        pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)
5✔
141
        desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=txin_type)
5✔
142
        await _append_utxos_to_inputs(
5✔
143
            inputs=inputs,
144
            network=network,
145
            script_descriptor=desc,
146
            imax=imax)
147
        keypairs[pubkey] = privkey, compressed
5✔
148

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

168

169
async def sweep(
5✔
170
        privkeys,
171
        *,
172
        network: 'Network',
173
        config: 'SimpleConfig',
174
        to_address: str,
175
        fee: int = None,
176
        imax=100,
177
        locktime=None,
178
        tx_version=None) -> PartialTransaction:
179

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

192
    outputs = [PartialTxOutput(scriptpubkey=bfh(bitcoin.address_to_script(to_address)),
5✔
193
                               value=total - fee)]
194
    if locktime is None:
5✔
195
        locktime = get_locktime_for_new_transaction(network)
×
196

197
    tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime, version=tx_version)
5✔
198
    tx.set_rbf(True)
5✔
199
    tx.sign(keypairs)
5✔
200
    return tx
5✔
201

202

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

227

228
class CannotRBFTx(Exception): pass
5✔
229

230

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

235
class CannotDoubleSpendTx(CannotRBFTx):
5✔
236
    def __str__(self):
5✔
237
        return _('Cannot cancel transaction') + ':\n\n' + Exception.__str__(self)
×
238

239
class CannotCPFP(Exception):
5✔
240
    def __str__(self):
5✔
241
        return _('Cannot create child transaction') + ':\n\n' + Exception.__str__(self)
×
242

243
class InternalAddressCorruption(Exception):
5✔
244
    def __str__(self):
5✔
245
        return _("Wallet file corruption detected. "
×
246
                 "Please restore your wallet from seed, and compare the addresses in both files")
247

248

249
class ReceiveRequestHelp(NamedTuple):
5✔
250
    # help texts (warnings/errors):
251
    address_help: str
5✔
252
    URI_help: str
5✔
253
    ln_help: str
5✔
254
    # whether the texts correspond to an error (or just a warning):
255
    address_is_error: bool
5✔
256
    URI_is_error: bool
5✔
257
    ln_is_error: bool
5✔
258

259
    ln_swap_suggestion: Optional[Any] = None
5✔
260
    ln_rebalance_suggestion: Optional[Any] = None
5✔
261

262
    def can_swap(self) -> bool:
5✔
263
        return bool(self.ln_swap_suggestion)
×
264

265
    def can_rebalance(self) -> bool:
5✔
266
        return bool(self.ln_rebalance_suggestion)
×
267

268

269
class TxWalletDelta(NamedTuple):
5✔
270
    is_relevant: bool  # "related to wallet?"
5✔
271
    is_any_input_ismine: bool
5✔
272
    is_all_input_ismine: bool
5✔
273
    delta: int
5✔
274
    fee: Optional[int]
5✔
275

276
class TxWalletDetails(NamedTuple):
5✔
277
    txid: Optional[str]
5✔
278
    status: str
5✔
279
    label: str
5✔
280
    can_broadcast: bool
5✔
281
    can_bump: bool
5✔
282
    can_cpfp: bool
5✔
283
    can_dscancel: bool  # whether user can double-spend to self
5✔
284
    can_save_as_local: bool
5✔
285
    amount: Optional[int]
5✔
286
    fee: Optional[int]
5✔
287
    tx_mined_status: TxMinedInfo
5✔
288
    mempool_depth_bytes: Optional[int]
5✔
289
    can_remove: bool  # whether user should be allowed to delete tx
5✔
290
    is_lightning_funding_tx: bool
5✔
291
    is_related_to_wallet: bool
5✔
292

293

294
class Abstract_Wallet(ABC, Logger, EventListener):
5✔
295
    """
296
    Wallet classes are created to handle various address generation methods.
297
    Completion states (watching-only, single account, no seed, etc) are handled inside classes.
298
    """
299

300
    LOGGING_SHORTCUT = 'w'
5✔
301
    max_change_outputs = 3
5✔
302
    gap_limit_for_change = 10
5✔
303

304
    txin_type: str
5✔
305
    wallet_type: str
5✔
306
    lnworker: Optional['LNWallet']
5✔
307
    network: Optional['Network']
5✔
308

309
    def __init__(self, db: WalletDB, *, config: SimpleConfig):
5✔
310

311
        if not db.is_ready_to_be_used_by_wallet():
5✔
312
            raise Exception("storage not ready to be used by Abstract_Wallet")
×
313

314
        self.config = config
5✔
315
        assert self.config is not None, "config must not be None"
5✔
316
        self.db = db
5✔
317
        self.storage = db.storage
5✔
318
        # load addresses needs to be called before constructor for sanity checks
319
        db.load_addresses(self.wallet_type)
5✔
320
        self.keystore = None  # type: Optional[KeyStore]  # will be set by load_keystore
5✔
321
        Logger.__init__(self)
5✔
322

323
        self.network = None
5✔
324
        self.adb = AddressSynchronizer(db, config, name=self.diagnostic_name())
5✔
325
        for addr in self.get_addresses():
5✔
326
            self.adb.add_address(addr)
5✔
327
        self.lock = self.adb.lock
5✔
328
        self.transaction_lock = self.adb.transaction_lock
5✔
329
        self._last_full_history = None
5✔
330
        self._tx_parents_cache = {}
5✔
331

332
        self.taskgroup = OldTaskGroup()
5✔
333

334
        # saved fields
335
        self.use_change            = db.get('use_change', True)
5✔
336
        self.multiple_change       = db.get('multiple_change', False)
5✔
337
        self._labels                = db.get_dict('labels')
5✔
338
        self._frozen_addresses      = set(db.get('frozen_addresses', []))
5✔
339
        self._frozen_coins          = db.get_dict('frozen_coins')  # type: Dict[str, bool]
5✔
340
        self.fiat_value            = db.get_dict('fiat_value')
5✔
341
        self._receive_requests      = db.get_dict('payment_requests')  # type: Dict[str, Request]
5✔
342
        self._invoices              = db.get_dict('invoices')  # type: Dict[str, Invoice]
5✔
343
        self._reserved_addresses   = set(db.get('reserved_addresses', []))
5✔
344
        self._num_parents          = db.get_dict('num_parents')
5✔
345

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

348
        self.load_keystore()
5✔
349
        self._init_lnworker()
5✔
350
        self._init_requests_rhash_index()
5✔
351
        self._prepare_onchain_invoice_paid_detection()
5✔
352
        self.calc_unused_change_addresses()
5✔
353
        # save wallet type the first time
354
        if self.db.get('wallet_type') is None:
5✔
355
            self.db.put('wallet_type', self.wallet_type)
5✔
356
        self.contacts = Contacts(self.db)
5✔
357
        self._coin_price_cache = {}
5✔
358

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

363
        self.test_addresses_sanity()
5✔
364
        self.register_callbacks()
5✔
365

366
    def _init_lnworker(self):
5✔
367
        self.lnworker = None
5✔
368

369
    @ignore_exceptions  # don't kill outer taskgroup
5✔
370
    async def main_loop(self):
5✔
371
        self.logger.info("starting taskgroup.")
×
372
        try:
×
373
            async with self.taskgroup as group:
×
374
                await group.spawn(asyncio.Event().wait)  # run forever (until cancel)
×
375
                await group.spawn(self.do_synchronize_loop())
×
376
        except Exception as e:
×
377
            self.logger.exception("taskgroup died.")
×
378
        finally:
379
            util.trigger_callback('wallet_updated', self)
×
380
            self.logger.info("taskgroup stopped.")
×
381

382
    async def do_synchronize_loop(self):
5✔
383
        """Generates new deterministic addresses if needed (gap limit roll-forward),
384
        and sets up_to_date.
385
        """
386
        while True:
387
            # polling.
388
            # TODO if adb had "up_to_date_changed" asyncio.Event(), we could *also* trigger on that.
389
            #      The polling would still be useful as often need to gen new addrs while adb.is_up_to_date() is False
390
            await asyncio.sleep(0.1)
×
391
            # note: we only generate new HD addresses if the existing ones
392
            #       have history that are mined and SPV-verified.
393
            await run_in_thread(self.synchronize)
×
394

395
    def save_db(self):
5✔
396
        if self.db.storage:
5✔
397
            self.db.write()
5✔
398

399
    def save_backup(self, backup_dir):
5✔
400
        new_path = os.path.join(backup_dir, self.basename() + '.backup')
×
401
        new_storage = WalletStorage(new_path)
×
402
        new_storage._encryption_version = self.storage._encryption_version
×
403
        new_storage.pubkey = self.storage.pubkey
×
404

405
        new_db = WalletDB(self.db.dump(), storage=new_storage, manual_upgrades=False)
×
406
        if self.lnworker:
×
407
            channel_backups = new_db.get_dict('imported_channel_backups')
×
408
            for chan_id, chan in self.lnworker.channels.items():
×
409
                channel_backups[chan_id.hex()] = self.lnworker.create_channel_backup(chan_id)
×
410
            new_db.put('channels', None)
×
411
            new_db.put('lightning_privkey2', None)
×
412
        new_db.set_modified(True)
×
413
        new_db.write()
×
414
        return new_path
×
415

416
    def has_lightning(self) -> bool:
5✔
417
        return bool(self.lnworker)
5✔
418

419
    def can_have_lightning(self) -> bool:
5✔
420
        # we want static_remotekey to be a wallet address
421
        return self.txin_type == 'p2wpkh'
×
422

423
    def can_have_deterministic_lightning(self) -> bool:
5✔
424
        if not self.can_have_lightning():
×
425
            return False
×
426
        if not self.keystore:
×
427
            return False
×
428
        return self.keystore.can_have_deterministic_lightning_xprv()
×
429

430
    def init_lightning(self, *, password) -> None:
5✔
431
        assert self.can_have_lightning()
×
432
        assert self.db.get('lightning_xprv') is None
×
433
        assert self.db.get('lightning_privkey2') is None
×
434
        if self.can_have_deterministic_lightning():
×
435
            assert isinstance(self.keystore, keystore.BIP32_KeyStore)
×
436
            ln_xprv = self.keystore.get_lightning_xprv(password)
×
437
            self.db.put('lightning_xprv', ln_xprv)
×
438
        else:
439
            seed = os.urandom(32)
×
440
            node = BIP32Node.from_rootseed(seed, xtype='standard')
×
441
            ln_xprv = node.to_xprv()
×
442
            self.db.put('lightning_privkey2', ln_xprv)
×
443
        self.lnworker = LNWallet(self, ln_xprv)
×
444
        self.save_db()
×
445
        if self.network:
×
446
            self._start_network_lightning()
×
447

448
    async def stop(self):
5✔
449
        """Stop all networking and save DB to disk."""
450
        self.unregister_callbacks()
5✔
451
        try:
5✔
452
            async with ignore_after(5):
5✔
453
                if self.network:
5✔
454
                    if self.lnworker:
×
455
                        await self.lnworker.stop()
×
456
                        self.lnworker = None
×
457
                await self.adb.stop()
5✔
458
                await self.taskgroup.cancel_remaining()
5✔
459
        finally:  # even if we get cancelled
460
            if any([ks.is_requesting_to_be_rewritten_to_wallet_file for ks in self.get_keystores()]):
5✔
461
                self.save_keystore()
5✔
462
            self.save_db()
5✔
463

464
    def is_up_to_date(self) -> bool:
5✔
465
        if self.taskgroup.joined:  # either stop() was called, or the taskgroup died
×
466
            return False
×
467
        return self._up_to_date
×
468

469
    def tx_is_related(self, tx):
5✔
470
        is_mine = any([self.is_mine(out.address) for out in tx.outputs()])
5✔
471
        is_mine |= any([self.is_mine(self.adb.get_txin_address(txin)) for txin in tx.inputs()])
5✔
472
        return is_mine
5✔
473

474
    def clear_tx_parents_cache(self):
5✔
475
        with self.lock, self.transaction_lock:
5✔
476
            self._tx_parents_cache.clear()
5✔
477
            self._num_parents.clear()
5✔
478
            self._last_full_history = None
5✔
479

480
    @event_listener
5✔
481
    async def on_event_adb_set_up_to_date(self, adb):
5✔
482
        if self.adb != adb:
5✔
483
            return
5✔
484
        num_new_addrs = await run_in_thread(self.synchronize)
5✔
485
        up_to_date = self.adb.is_up_to_date() and num_new_addrs == 0
5✔
486
        with self.lock:
5✔
487
            status_changed = self._up_to_date != up_to_date
5✔
488
            self._up_to_date = up_to_date
5✔
489
        if up_to_date:
5✔
490
            self.adb.reset_netrequest_counters()  # sync progress indicator
×
491
            self.save_db()
×
492
        # fire triggers
493
        if status_changed or up_to_date:  # suppress False->False transition, as it is spammy
5✔
494
            util.trigger_callback('wallet_updated', self)
×
495
            util.trigger_callback('status')
×
496
        if status_changed:
5✔
497
            self.logger.info(f'set_up_to_date: {up_to_date}')
×
498

499
    @event_listener
5✔
500
    def on_event_adb_added_tx(self, adb, tx_hash: str, tx: Transaction):
5✔
501
        if self.adb != adb:
5✔
502
            return
5✔
503
        if not self.tx_is_related(tx):
5✔
504
            return
5✔
505
        self.clear_tx_parents_cache()
5✔
506
        if self.lnworker:
5✔
507
            self.lnworker.maybe_add_backup_from_tx(tx)
5✔
508
        self._update_invoices_and_reqs_touched_by_tx(tx_hash)
5✔
509
        util.trigger_callback('new_transaction', self, tx)
5✔
510

511
    @event_listener
5✔
512
    def on_event_adb_removed_tx(self, adb, txid: str, tx: Transaction):
5✔
513
        if self.adb != adb:
5✔
514
            return
5✔
515
        if not self.tx_is_related(tx):
5✔
516
            return
×
517
        self.clear_tx_parents_cache()
5✔
518
        util.trigger_callback('removed_transaction', self, tx)
5✔
519

520
    @event_listener
5✔
521
    def on_event_adb_added_verified_tx(self, adb, tx_hash):
5✔
522
        if adb != self.adb:
5✔
523
            return
5✔
524
        self._update_invoices_and_reqs_touched_by_tx(tx_hash)
5✔
525
        tx_mined_status = self.adb.get_tx_height(tx_hash)
5✔
526
        util.trigger_callback('verified', self, tx_hash, tx_mined_status)
5✔
527

528
    @event_listener
5✔
529
    def on_event_adb_removed_verified_tx(self, adb, tx_hash):
5✔
530
        if adb != self.adb:
×
531
            return
×
532
        self._update_invoices_and_reqs_touched_by_tx(tx_hash)
×
533

534
    def clear_history(self):
5✔
535
        self.adb.clear_history()
×
536
        self.save_db()
×
537

538
    def start_network(self, network: 'Network'):
5✔
539
        assert self.network is None, "already started"
5✔
540
        self.network = network
5✔
541
        if network:
5✔
542
            asyncio.run_coroutine_threadsafe(self.main_loop(), self.network.asyncio_loop)
×
543
            self.adb.start_network(network)
×
544
            if self.lnworker:
×
545
                self._start_network_lightning()
×
546

547
    def _start_network_lightning(self):
5✔
548
        assert self.lnworker
×
549
        assert self.lnworker.network is None, 'lnworker network already initialized'
×
550
        self.lnworker.start_network(self.network)
×
551
        # only start gossiping when we already have channels
552
        if self.db.get('channels'):
×
553
            self.network.start_gossip()
×
554

555
    @abstractmethod
5✔
556
    def load_keystore(self) -> None:
5✔
557
        pass
×
558

559
    def diagnostic_name(self):
5✔
560
        return self.basename()
5✔
561

562
    def __str__(self):
5✔
563
        return self.basename()
×
564

565
    def get_master_public_key(self):
5✔
566
        return None
×
567

568
    def get_master_public_keys(self):
5✔
569
        return []
×
570

571
    def basename(self) -> str:
5✔
572
        return self.storage.basename() if self.storage else 'no_name'
5✔
573

574
    def test_addresses_sanity(self) -> None:
5✔
575
        addrs = self.get_receiving_addresses()
5✔
576
        if len(addrs) > 0:
5✔
577
            addr = str(addrs[0])
5✔
578
            if not bitcoin.is_address(addr):
5✔
579
                neutered_addr = addr[:5] + '..' + addr[-2:]
×
580
                raise WalletFileException(f'The addresses in this wallet are not bitcoin addresses.\n'
×
581
                                          f'e.g. {neutered_addr} (length: {len(addr)})')
582

583
    def check_returned_address_for_corruption(func):
5✔
584
        def wrapper(self, *args, **kwargs):
5✔
585
            addr = func(self, *args, **kwargs)
5✔
586
            self.check_address_for_corruption(addr)
5✔
587
            return addr
5✔
588
        return wrapper
5✔
589

590
    def calc_unused_change_addresses(self) -> Sequence[str]:
5✔
591
        """Returns a list of change addresses to choose from, for usage in e.g. new transactions.
592
        The caller should give priority to earlier ones in the list.
593
        """
594
        with self.lock:
5✔
595
            # We want a list of unused change addresses.
596
            # As a performance optimisation, to avoid checking all addresses every time,
597
            # we maintain a list of "not old" addresses ("old" addresses have deeply confirmed history),
598
            # and only check those.
599
            if not hasattr(self, '_not_old_change_addresses'):
5✔
600
                self._not_old_change_addresses = self.get_change_addresses()
5✔
601
            self._not_old_change_addresses = [addr for addr in self._not_old_change_addresses
5✔
602
                                              if not self.adb.address_is_old(addr)]
603
            unused_addrs = [addr for addr in self._not_old_change_addresses
5✔
604
                            if not self.adb.is_used(addr) and not self.is_address_reserved(addr)]
605
            return unused_addrs
5✔
606

607
    def is_deterministic(self) -> bool:
5✔
608
        return self.keystore.is_deterministic()
×
609

610
    def _set_label(self, key: str, value: Optional[str]) -> None:
5✔
611
        with self.lock:
×
612
            if value is None:
×
613
                self._labels.pop(key, None)
×
614
            else:
615
                self._labels[key] = value
×
616

617
    def set_label(self, name: str, text: str = None) -> bool:
5✔
618
        if not name:
5✔
619
            return False
×
620
        changed = False
5✔
621
        with self.lock:
5✔
622
            old_text = self._labels.get(name)
5✔
623
            if text:
5✔
624
                text = text.replace("\n", " ")
×
625
                if old_text != text:
×
626
                    self._labels[name] = text
×
627
                    changed = True
×
628
            else:
629
                if old_text is not None:
5✔
630
                    self._labels.pop(name)
×
631
                    changed = True
×
632
        if changed:
5✔
633
            run_hook('set_label', self, name, text)
×
634
        return changed
5✔
635

636
    def import_labels(self, path):
5✔
637
        data = read_json_file(path)
×
638
        for key, value in data.items():
×
639
            self.set_label(key, value)
×
640

641
    def export_labels(self, path):
5✔
642
        write_json_file(path, self.get_all_labels())
×
643

644
    def set_fiat_value(self, txid, ccy, text, fx, value_sat):
5✔
645
        if not self.db.get_transaction(txid):
5✔
646
            return
×
647
        # since fx is inserting the thousands separator,
648
        # and not util, also have fx remove it
649
        text = fx.remove_thousands_separator(text)
5✔
650
        def_fiat = self.default_fiat_value(txid, fx, value_sat)
5✔
651
        formatted = fx.ccy_amount_str(def_fiat, add_thousands_sep=False)
5✔
652
        def_fiat_rounded = Decimal(formatted)
5✔
653
        reset = not text
5✔
654
        if not reset:
5✔
655
            try:
5✔
656
                text_dec = Decimal(text)
5✔
657
                text_dec_rounded = Decimal(fx.ccy_amount_str(text_dec, add_thousands_sep=False))
5✔
658
                reset = text_dec_rounded == def_fiat_rounded
5✔
659
            except Exception:
5✔
660
                # garbage. not resetting, but not saving either
661
                return False
5✔
662
        if reset:
5✔
663
            d = self.fiat_value.get(ccy, {})
5✔
664
            if d and txid in d:
5✔
665
                d.pop(txid)
5✔
666
            else:
667
                # avoid saving empty dict
668
                return True
5✔
669
        else:
670
            if ccy not in self.fiat_value:
5✔
671
                self.fiat_value[ccy] = {}
5✔
672
            self.fiat_value[ccy][txid] = text
5✔
673
        return reset
5✔
674

675
    def get_fiat_value(self, txid, ccy):
5✔
676
        fiat_value = self.fiat_value.get(ccy, {}).get(txid)
×
677
        try:
×
678
            return Decimal(fiat_value)
×
679
        except Exception:
×
680
            return
×
681

682
    def is_mine(self, address) -> bool:
5✔
683
        if not address: return False
5✔
684
        return bool(self.get_address_index(address))
5✔
685

686
    def is_change(self, address) -> bool:
5✔
687
        if not self.is_mine(address):
5✔
688
            return False
5✔
689
        return self.get_address_index(address)[0] == 1
5✔
690

691
    @abstractmethod
5✔
692
    def get_addresses(self) -> Sequence[str]:
5✔
693
        pass
×
694

695
    @abstractmethod
5✔
696
    def get_address_index(self, address: str) -> Optional[AddressIndexGeneric]:
5✔
697
        pass
×
698

699
    @abstractmethod
5✔
700
    def get_address_path_str(self, address: str) -> Optional[str]:
5✔
701
        """Returns derivation path str such as "m/0/5" to address,
702
        or None if not applicable.
703
        """
704
        pass
×
705

706
    def get_redeem_script(self, address: str) -> Optional[str]:
5✔
707
        desc = self.get_script_descriptor_for_address(address)
×
708
        if desc is None: return None
×
709
        redeem_script = desc.expand().redeem_script
×
710
        if redeem_script:
×
711
            return redeem_script.hex()
×
712

713
    def get_witness_script(self, address: str) -> Optional[str]:
5✔
714
        desc = self.get_script_descriptor_for_address(address)
×
715
        if desc is None: return None
×
716
        witness_script = desc.expand().witness_script
×
717
        if witness_script:
×
718
            return witness_script.hex()
×
719

720
    @abstractmethod
5✔
721
    def get_txin_type(self, address: str) -> str:
5✔
722
        """Return script type of wallet address."""
723
        pass
×
724

725
    def export_private_key(self, address: str, password: Optional[str]) -> str:
5✔
726
        if self.is_watching_only():
5✔
727
            raise UserFacingException(_("This is a watching-only wallet"))
×
728
        if not is_address(address):
5✔
729
            raise UserFacingException(f"Invalid bitcoin address: {address}")
5✔
730
        if not self.is_mine(address):
5✔
731
            raise UserFacingException(_('Address not in wallet.') + f' {address}')
5✔
732
        index = self.get_address_index(address)
5✔
733
        pk, compressed = self.keystore.get_private_key(index, password)
5✔
734
        txin_type = self.get_txin_type(address)
5✔
735
        serialized_privkey = bitcoin.serialize_privkey(pk, compressed, txin_type)
5✔
736
        return serialized_privkey
5✔
737

738
    def export_private_key_for_path(self, path: Union[Sequence[int], str], password: Optional[str]) -> str:
5✔
739
        raise Exception("this wallet is not deterministic")
×
740

741
    @abstractmethod
5✔
742
    def get_public_keys(self, address: str) -> Sequence[str]:
5✔
743
        pass
×
744

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

749
    def is_lightning_funding_tx(self, txid: Optional[str]) -> bool:
5✔
750
        if not self.lnworker or txid is None:
5✔
751
            return False
5✔
752
        return any([chan.funding_outpoint.txid == txid
×
753
                    for chan in self.lnworker.channels.values()])
754

755
    def get_swap_by_claim_tx(self, tx: Transaction) -> bool:
5✔
756
        return self.lnworker.swap_manager.get_swap_by_claim_tx(tx) if self.lnworker else None
5✔
757

758
    def get_swap_by_funding_tx(self, tx: Transaction) -> bool:
5✔
759
        return bool(self.lnworker.swap_manager.get_swap_by_funding_tx(tx)) if self.lnworker else None
×
760

761
    def get_wallet_delta(self, tx: Transaction) -> TxWalletDelta:
5✔
762
        """Return the effect a transaction has on the wallet.
763
        This method must use self.is_mine, not self.adb.is_mine()
764
        """
765
        is_relevant = False  # "related to wallet?"
5✔
766
        num_input_ismine = 0
5✔
767
        v_in = v_in_mine = v_out = v_out_mine = 0
5✔
768
        with self.lock, self.transaction_lock:
5✔
769
            for txin in tx.inputs():
5✔
770
                addr = self.adb.get_txin_address(txin)
5✔
771
                value = self.adb.get_txin_value(txin, address=addr)
5✔
772
                if self.is_mine(addr):
5✔
773
                    num_input_ismine += 1
5✔
774
                    is_relevant = True
5✔
775
                    assert value is not None
5✔
776
                    v_in_mine += value
5✔
777
                if value is None:
5✔
778
                    v_in = None
×
779
                elif v_in is not None:
5✔
780
                    v_in += value
5✔
781
            for txout in tx.outputs():
5✔
782
                v_out += txout.value
5✔
783
                if self.is_mine(txout.address):
5✔
784
                    v_out_mine += txout.value
5✔
785
                    is_relevant = True
5✔
786
        delta = v_out_mine - v_in_mine
5✔
787
        if v_in is not None:
5✔
788
            fee = v_in - v_out
5✔
789
        else:
790
            fee = None
×
791
        if fee is None and isinstance(tx, PartialTransaction):
5✔
792
            fee = tx.get_fee()
×
793
        return TxWalletDelta(
5✔
794
            is_relevant=is_relevant,
795
            is_any_input_ismine=num_input_ismine > 0,
796
            is_all_input_ismine=num_input_ismine == len(tx.inputs()),
797
            delta=delta,
798
            fee=fee,
799
        )
800

801
    def get_tx_info(self, tx: Transaction) -> TxWalletDetails:
5✔
802
        tx_wallet_delta = self.get_wallet_delta(tx)
5✔
803
        is_relevant = tx_wallet_delta.is_relevant
5✔
804
        is_any_input_ismine = tx_wallet_delta.is_any_input_ismine
5✔
805
        is_swap = bool(self.get_swap_by_claim_tx(tx))
5✔
806
        fee = tx_wallet_delta.fee
5✔
807
        exp_n = None
5✔
808
        can_broadcast = False
5✔
809
        can_bump = False
5✔
810
        can_cpfp = False
5✔
811
        tx_hash = tx.txid()  # note: txid can be None! e.g. when called from GUI tx dialog
5✔
812
        is_lightning_funding_tx = self.is_lightning_funding_tx(tx_hash)
5✔
813
        tx_we_already_have_in_db = self.adb.db.get_transaction(tx_hash)
5✔
814
        can_save_as_local = (is_relevant and tx.txid() is not None
5✔
815
                             and (tx_we_already_have_in_db is None or not tx_we_already_have_in_db.is_complete()))
816
        label = ''
5✔
817
        tx_mined_status = self.adb.get_tx_height(tx_hash)
5✔
818
        can_remove = ((tx_mined_status.height in [TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL])
5✔
819
                      # otherwise 'height' is unreliable (typically LOCAL):
820
                      and is_relevant
821
                      # don't offer during common signing flow, e.g. when watch-only wallet starts creating a tx:
822
                      and bool(tx_we_already_have_in_db))
823
        can_dscancel = False
5✔
824
        if tx.is_complete():
5✔
825
            if tx_we_already_have_in_db:
5✔
826
                label = self.get_label_for_txid(tx_hash)
5✔
827
                if tx_mined_status.height > 0:
5✔
828
                    if tx_mined_status.conf:
×
829
                        status = _("{} confirmations").format(tx_mined_status.conf)
×
830
                    else:
831
                        status = _('Not verified')
×
832
                elif tx_mined_status.height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED):
5✔
833
                    status = _('Unconfirmed')
5✔
834
                    if fee is None:
5✔
835
                        fee = self.adb.get_tx_fee(tx_hash)
×
836
                    if fee and self.network and self.config.has_fee_mempool():
5✔
837
                        size = tx.estimated_size()
×
838
                        fee_per_byte = fee / size
×
839
                        exp_n = self.config.fee_to_depth(fee_per_byte)
×
840
                    can_bump = (is_any_input_ismine or is_swap) and not tx.is_final()
5✔
841
                    can_dscancel = (is_any_input_ismine and not tx.is_final()
5✔
842
                                    and not all([self.is_mine(txout.address) for txout in tx.outputs()]))
843
                    try:
5✔
844
                        self.cpfp(tx, 0)
5✔
845
                        can_cpfp = True
5✔
846
                    except Exception:
5✔
847
                        can_cpfp = False
5✔
848
                else:
849
                    status = _('Local')
×
850
                    if tx_mined_status.height == TX_HEIGHT_FUTURE:
×
851
                        num_blocks_remainining = tx_mined_status.wanted_height - self.adb.get_local_height()
×
852
                        num_blocks_remainining = max(0, num_blocks_remainining)
×
853
                        status = _('Local (future: {})').format(_('in {} blocks').format(num_blocks_remainining))
×
854
                    can_broadcast = self.network is not None
×
855
                    can_bump = (is_any_input_ismine or is_swap) and not tx.is_final()
×
856
            else:
857
                status = _("Signed")
×
858
                can_broadcast = self.network is not None
×
859
        else:
860
            assert isinstance(tx, PartialTransaction)
×
861
            s, r = tx.signature_count()
×
862
            status = _("Unsigned") if s == 0 else _('Partially signed') + ' (%d/%d)'%(s,r)
×
863

864
        if is_relevant:
5✔
865
            if tx_wallet_delta.is_all_input_ismine:
5✔
866
                assert fee is not None
5✔
867
                amount = tx_wallet_delta.delta + fee
5✔
868
            else:
869
                amount = tx_wallet_delta.delta
×
870
        else:
871
            amount = None
×
872

873
        if is_lightning_funding_tx:
5✔
874
            can_bump = False  # would change txid
×
875

876
        return TxWalletDetails(
5✔
877
            txid=tx_hash,
878
            status=status,
879
            label=label,
880
            can_broadcast=can_broadcast,
881
            can_bump=can_bump,
882
            can_cpfp=can_cpfp,
883
            can_dscancel=can_dscancel,
884
            can_save_as_local=can_save_as_local,
885
            amount=amount,
886
            fee=fee,
887
            tx_mined_status=tx_mined_status,
888
            mempool_depth_bytes=exp_n,
889
            can_remove=can_remove,
890
            is_lightning_funding_tx=is_lightning_funding_tx,
891
            is_related_to_wallet=is_relevant,
892
        )
893

894
    def get_num_parents(self, txid: str) -> Optional[int]:
5✔
895
        if not self.is_up_to_date():
×
896
            return
×
897
        if txid not in self._num_parents:
×
898
            self._num_parents[txid] = len(self.get_tx_parents(txid))
×
899
        return self._num_parents[txid]
×
900

901
    def get_tx_parents(self, txid: str) -> Dict[str, Tuple[List[str], List[str]]]:
5✔
902
        """
903
        returns a flat dict:
904
        txid -> list of parent txids
905
        """
906
        with self.lock, self.transaction_lock:
×
907
            if self._last_full_history is None:
×
908
                self._last_full_history = self.get_full_history(None, include_lightning=False)
×
909
                # populate cache in chronological order (confirmed tx only)
910
                # todo: get_full_history should return unconfirmed tx topologically sorted
911
                for _txid, tx_item in self._last_full_history.items():
×
912
                    if tx_item['height'] > 0:
×
913
                        self.get_tx_parents(_txid)
×
914

915
            result = self._tx_parents_cache.get(txid, None)
×
916
            if result is not None:
×
917
                return result
×
918
            result = {}   # type: Dict[str, Tuple[List[str], List[str]]]
×
919
            parents = []  # type: List[str]
×
920
            uncles = []   # type: List[str]
×
921
            tx = self.adb.get_transaction(txid)
×
922
            assert tx, f"cannot find {txid} in db"
×
923
            for i, txin in enumerate(tx.inputs()):
×
924
                _txid = txin.prevout.txid.hex()
×
925
                parents.append(_txid)
×
926
                # detect address reuse
927
                addr = self.adb.get_txin_address(txin)
×
928
                if addr is None:
×
929
                    continue
×
930
                received, sent = self.adb.get_addr_io(addr)
×
931
                if len(sent) > 1:
×
932
                    my_txid, my_height, my_pos = sent[txin.prevout.to_str()]
×
933
                    assert my_txid == txid
×
934
                    for k, v in sent.items():
×
935
                        if k != txin.prevout.to_str():
×
936
                            reuse_txid, reuse_height, reuse_pos = v
×
937
                            if reuse_height <= 0:  # exclude not-yet-mined (we need topological ordering)
×
938
                                continue
×
939
                            if (reuse_height, reuse_pos) < (my_height, my_pos):
×
940
                                uncle_txid, uncle_index = k.split(':')
×
941
                                uncles.append(uncle_txid)
×
942

943
            for _txid in parents + uncles:
×
944
                if _txid in self._last_full_history.keys():
×
945
                    result.update(self.get_tx_parents(_txid))
×
946
            result[txid] = parents, uncles
×
947
            self._tx_parents_cache[txid] = result
×
948
            return result
×
949

950
    def get_balance(self, **kwargs):
5✔
951
        domain = self.get_addresses()
5✔
952
        return self.adb.get_balance(domain, **kwargs)
5✔
953

954
    def get_addr_balance(self, address):
5✔
955
        return self.adb.get_balance([address])
×
956

957
    def get_utxos(
5✔
958
            self,
959
            domain: Optional[Iterable[str]] = None,
960
            **kwargs,
961
    ):
962
        if domain is None:
5✔
963
            domain = self.get_addresses()
5✔
964
        return self.adb.get_utxos(domain=domain, **kwargs)
5✔
965

966
    def get_spendable_coins(
5✔
967
            self,
968
            domain: Optional[Iterable[str]] = None,
969
            *,
970
            nonlocal_only: bool = False,
971
            confirmed_only: bool = False,
972
    ) -> Sequence[PartialTxInput]:
973
        with self._freeze_lock:
5✔
974
            frozen_addresses = self._frozen_addresses.copy()
5✔
975
        utxos = self.get_utxos(
5✔
976
            domain=domain,
977
            excluded_addresses=frozen_addresses,
978
            mature_only=True,
979
            confirmed_funding_only=confirmed_only,
980
            nonlocal_only=nonlocal_only,
981
        )
982
        utxos = [utxo for utxo in utxos if not self.is_frozen_coin(utxo)]
5✔
983
        return utxos
5✔
984

985
    @abstractmethod
5✔
986
    def get_receiving_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence[str]:
5✔
987
        pass
×
988

989
    @abstractmethod
5✔
990
    def get_change_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence[str]:
5✔
991
        pass
×
992

993
    def dummy_address(self):
5✔
994
        # first receiving address
995
        return self.get_receiving_addresses(slice_start=0, slice_stop=1)[0]
×
996

997
    def get_frozen_balance(self):
5✔
998
        with self._freeze_lock:
×
999
            frozen_addresses = self._frozen_addresses.copy()
×
1000
        # note: for coins, use is_frozen_coin instead of _frozen_coins,
1001
        #       as latter only contains *manually* frozen ones
1002
        frozen_coins = {utxo.prevout.to_str() for utxo in self.get_utxos()
×
1003
                        if self.is_frozen_coin(utxo)}
1004
        if not frozen_coins:  # shortcut
×
1005
            return self.adb.get_balance(frozen_addresses)
×
1006
        c1, u1, x1 = self.get_balance()
×
1007
        c2, u2, x2 = self.get_balance(
×
1008
            excluded_addresses=frozen_addresses,
1009
            excluded_coins=frozen_coins,
1010
        )
1011
        return c1-c2, u1-u2, x1-x2
×
1012

1013
    def get_balances_for_piechart(self):
5✔
1014
        # return only positive values
1015
        # todo: add lightning frozen
1016
        c, u, x = self.get_balance()
×
1017
        fc, fu, fx = self.get_frozen_balance()
×
1018
        lightning = self.lnworker.get_balance() if self.has_lightning() else 0
×
1019
        f_lightning = self.lnworker.get_balance(frozen=True) if self.has_lightning() else 0
×
1020
        # subtract frozen funds
1021
        cc = c - fc
×
1022
        uu = u - fu
×
1023
        xx = x - fx
×
1024
        frozen = fc + fu + fx
×
1025
        return cc, uu, xx, frozen, lightning - f_lightning, f_lightning
×
1026

1027
    def balance_at_timestamp(self, domain, target_timestamp):
5✔
1028
        # we assume that get_history returns items ordered by block height
1029
        # we also assume that block timestamps are monotonic (which is false...!)
1030
        h = self.adb.get_history(domain=domain)
×
1031
        balance = 0
×
1032
        for hist_item in h:
×
1033
            balance = hist_item.balance
×
1034
            if hist_item.tx_mined_status.timestamp is None or hist_item.tx_mined_status.timestamp > target_timestamp:
×
1035
                return balance - hist_item.delta
×
1036
        # return last balance
1037
        return balance
×
1038

1039
    def get_onchain_history(self, *, domain=None):
5✔
1040
        if domain is None:
×
1041
            domain = self.get_addresses()
×
1042
        monotonic_timestamp = 0
×
1043
        for hist_item in self.adb.get_history(domain=domain):
×
1044
            monotonic_timestamp = max(monotonic_timestamp, (hist_item.tx_mined_status.timestamp or TX_TIMESTAMP_INF))
×
1045
            d = {
×
1046
                'txid': hist_item.txid,
1047
                'fee_sat': hist_item.fee,
1048
                'height': hist_item.tx_mined_status.height,
1049
                'confirmations': hist_item.tx_mined_status.conf,
1050
                'timestamp': hist_item.tx_mined_status.timestamp,
1051
                'monotonic_timestamp': monotonic_timestamp,
1052
                'incoming': True if hist_item.delta>0 else False,
1053
                'bc_value': Satoshis(hist_item.delta),
1054
                'bc_balance': Satoshis(hist_item.balance),
1055
                'date': timestamp_to_datetime(hist_item.tx_mined_status.timestamp),
1056
                'label': self.get_label_for_txid(hist_item.txid),
1057
                'txpos_in_block': hist_item.tx_mined_status.txpos,
1058
            }
1059
            if wanted_height := hist_item.tx_mined_status.wanted_height:
×
1060
                d['wanted_height'] = wanted_height
×
1061
            yield d
×
1062

1063
    def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> Invoice:
5✔
1064
        height = self.adb.get_local_height()
×
1065
        if pr:
×
1066
            return Invoice.from_bip70_payreq(pr, height=height)
×
1067
        amount_msat = 0
×
1068
        for x in outputs:
×
1069
            if parse_max_spend(x.value):
×
1070
                amount_msat = '!'
×
1071
                break
×
1072
            else:
1073
                assert isinstance(x.value, int), f"{x.value!r}"
×
1074
                amount_msat += x.value * 1000
×
1075
        timestamp = None
×
1076
        exp = None
×
1077
        if URI:
×
1078
            timestamp = URI.get('time')
×
1079
            exp = URI.get('exp')
×
1080
        timestamp = timestamp or int(Invoice._get_cur_time())
×
1081
        exp = exp or 0
×
1082
        invoice = Invoice(
×
1083
            amount_msat=amount_msat,
1084
            message=message,
1085
            time=timestamp,
1086
            exp=exp,
1087
            outputs=outputs,
1088
            bip70=None,
1089
            height=height,
1090
            lightning_invoice=None,
1091
        )
1092
        return invoice
×
1093

1094
    def save_invoice(self, invoice: Invoice, *, write_to_disk: bool = True) -> None:
5✔
1095
        key = invoice.get_id()
×
1096
        if not invoice.is_lightning():
×
1097
            if self.is_onchain_invoice_paid(invoice)[0]:
×
1098
                _logger.info("saving invoice... but it is already paid!")
×
1099
            with self.transaction_lock:
×
1100
                for txout in invoice.get_outputs():
×
1101
                    self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(key)
×
1102
        self._invoices[key] = invoice
×
1103
        if write_to_disk:
×
1104
            self.save_db()
×
1105

1106
    def clear_invoices(self):
5✔
1107
        self._invoices.clear()
×
1108
        self.save_db()
×
1109

1110
    def clear_requests(self):
5✔
1111
        self._receive_requests.clear()
×
1112
        self._requests_addr_to_key.clear()
×
1113
        self.save_db()
×
1114

1115
    def get_invoices(self) -> List[Invoice]:
5✔
1116
        out = list(self._invoices.values())
×
1117
        out.sort(key=lambda x:x.time)
×
1118
        return out
×
1119

1120
    def get_unpaid_invoices(self) -> List[Invoice]:
5✔
1121
        invoices = self.get_invoices()
×
1122
        return [x for x in invoices if self.get_invoice_status(x) != PR_PAID]
×
1123

1124
    def get_invoice(self, invoice_id):
5✔
1125
        return self._invoices.get(invoice_id)
×
1126

1127
    def import_requests(self, path):
5✔
1128
        data = read_json_file(path)
×
1129
        for x in data:
×
1130
            try:
×
1131
                req = Request(**x)
×
1132
            except Exception:
×
1133
                raise FileImportFailed(_("Invalid invoice format"))
×
1134
            self.add_payment_request(req, write_to_disk=False)
×
1135
        self.save_db()
×
1136

1137
    def export_requests(self, path):
5✔
1138
        # note: this does not export preimages for LN bolt11 invoices
1139
        write_json_file(path, list(self._receive_requests.values()))
×
1140

1141
    def import_invoices(self, path):
5✔
1142
        data = read_json_file(path)
×
1143
        for x in data:
×
1144
            try:
×
1145
                invoice = Invoice(**x)
×
1146
            except Exception:
×
1147
                raise FileImportFailed(_("Invalid invoice format"))
×
1148
            self.save_invoice(invoice, write_to_disk=False)
×
1149
        self.save_db()
×
1150

1151
    def export_invoices(self, path):
5✔
1152
        write_json_file(path, list(self._invoices.values()))
×
1153

1154
    def get_relevant_invoices_for_tx(self, tx_hash) -> Sequence[Invoice]:
5✔
1155
        invoice_keys = self._invoices_from_txid_map.get(tx_hash, set())
5✔
1156
        invoices = [self.get_invoice(key) for key in invoice_keys]
5✔
1157
        invoices = [inv for inv in invoices if inv]  # filter out None
5✔
1158
        for inv in invoices:
5✔
1159
            assert isinstance(inv, Invoice), f"unexpected type {type(inv)}"
×
1160
        return invoices
5✔
1161

1162
    def _init_requests_rhash_index(self):
5✔
1163
        # self._requests_addr_to_key may contain addresses that can be reused
1164
        # this is checked in get_request_by_address
1165
        self._requests_addr_to_key = defaultdict(set)  # type: Dict[str, Set[str]]
5✔
1166
        for req in self._receive_requests.values():
5✔
1167
            if addr := req.get_address():
5✔
1168
                self._requests_addr_to_key[addr].add(req.get_id())
5✔
1169

1170
    def _prepare_onchain_invoice_paid_detection(self):
5✔
1171
        self._invoices_from_txid_map = defaultdict(set)  # type: Dict[str, Set[str]]
5✔
1172
        self._invoices_from_scriptpubkey_map = defaultdict(set)  # type: Dict[bytes, Set[str]]
5✔
1173
        self._update_onchain_invoice_paid_detection(self._invoices.keys())
5✔
1174

1175
    def _update_onchain_invoice_paid_detection(self, invoice_keys: Iterable[str]) -> None:
5✔
1176
        for invoice_key in invoice_keys:
5✔
1177
            invoice = self._invoices.get(invoice_key)
×
1178
            if not invoice:
×
1179
                continue
×
1180
            if invoice.is_lightning() and not invoice.get_address():
×
1181
                continue
×
1182
            if invoice.is_lightning() and self.lnworker and self.lnworker.get_invoice_status(invoice) == PR_PAID:
×
1183
                continue
×
1184
            is_paid, conf_needed, relevant_txs = self._is_onchain_invoice_paid(invoice)
×
1185
            if is_paid:
×
1186
                for txid in relevant_txs:
×
1187
                    self._invoices_from_txid_map[txid].add(invoice_key)
×
1188
            for txout in invoice.get_outputs():
×
1189
                self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key)
×
1190
            # update invoice status
1191
            status = self.get_invoice_status(invoice)
×
1192
            util.trigger_callback('invoice_status', self, invoice_key, status)
×
1193

1194
    def _is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional[int], Sequence[str]]:
5✔
1195
        """Returns whether on-chain invoice/request is satisfied, num confs required txs have,
1196
        and list of relevant TXIDs.
1197
        """
1198
        outputs = invoice.get_outputs()
5✔
1199
        if not outputs:  # e.g. lightning-only
5✔
1200
            return False, None, []
×
1201
        invoice_amounts = defaultdict(int)  # type: Dict[bytes, int]  # scriptpubkey -> value_sats
5✔
1202
        for txo in outputs:  # type: PartialTxOutput
5✔
1203
            invoice_amounts[txo.scriptpubkey] += 1 if parse_max_spend(txo.value) else txo.value
5✔
1204
        relevant_txs = set()
5✔
1205
        is_paid = True
5✔
1206
        conf_needed = None  # type: Optional[int]
5✔
1207
        with self.lock, self.transaction_lock:
5✔
1208
            for invoice_scriptpubkey, invoice_amt in invoice_amounts.items():
5✔
1209
                scripthash = bitcoin.script_to_scripthash(invoice_scriptpubkey.hex())
5✔
1210
                prevouts_and_values = self.db.get_prevouts_by_scripthash(scripthash)
5✔
1211
                confs_and_values = []
5✔
1212
                for prevout, v in prevouts_and_values:
5✔
1213
                    relevant_txs.add(prevout.txid.hex())
5✔
1214
                    tx_height = self.adb.get_tx_height(prevout.txid.hex())
5✔
1215
                    if 0 < tx_height.height <= invoice.height:  # exclude txs older than invoice
5✔
1216
                        continue
3✔
1217
                    confs_and_values.append((tx_height.conf or 0, v))
5✔
1218
                # check that there is at least one TXO, and that they pay enough.
1219
                # note: "at least one TXO" check is needed for zero amount invoice (e.g. OP_RETURN)
1220
                vsum = 0
5✔
1221
                for conf, v in reversed(sorted(confs_and_values)):
5✔
1222
                    vsum += v
5✔
1223
                    if vsum >= invoice_amt:
5✔
1224
                        conf_needed = min(conf_needed, conf) if conf_needed is not None else conf
5✔
1225
                        break
5✔
1226
                else:
1227
                    is_paid = False
5✔
1228
        return is_paid, conf_needed, list(relevant_txs)
5✔
1229

1230
    def is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional[int]]:
5✔
1231
        is_paid, conf_needed, relevant_txs = self._is_onchain_invoice_paid(invoice)
5✔
1232
        return is_paid, conf_needed
5✔
1233

1234
    @profiler
5✔
1235
    def get_full_history(self, fx=None, *, onchain_domain=None, include_lightning=True, include_fiat=False):
5✔
1236
        transactions_tmp = OrderedDictWithIndex()
×
1237
        # add on-chain txns
1238
        onchain_history = self.get_onchain_history(domain=onchain_domain)
×
1239
        for tx_item in onchain_history:
×
1240
            txid = tx_item['txid']
×
1241
            transactions_tmp[txid] = tx_item
×
1242
        # add lnworker onchain transactions
1243
        lnworker_history = self.lnworker.get_onchain_history() if self.lnworker and include_lightning else {}
×
1244
        for txid, item in lnworker_history.items():
×
1245
            if txid in transactions_tmp:
×
1246
                tx_item = transactions_tmp[txid]
×
1247
                tx_item['group_id'] = item.get('group_id')  # for swaps
×
1248
                tx_item['label'] = item['label']
×
1249
                tx_item['type'] = item['type']
×
1250
                ln_value = Decimal(item['amount_msat']) / 1000   # for channel open/close tx
×
1251
                tx_item['ln_value'] = Satoshis(ln_value)
×
1252
                if channel_id := item.get('channel_id'):
×
1253
                    tx_item['channel_id'] = channel_id
×
1254
            else:
1255
                if item['type'] == 'swap':
×
1256
                    # swap items do not have all the fields. We can skip skip them
1257
                    # because they will eventually be in onchain_history
1258
                    # TODO: use attr.s objects instead of dicts
1259
                    continue
×
1260
                transactions_tmp[txid] = item
×
1261
                ln_value = Decimal(item['amount_msat']) / 1000   # for channel open/close tx
×
1262
                item['ln_value'] = Satoshis(ln_value)
×
1263
        # add lightning_transactions
1264
        lightning_history = self.lnworker.get_lightning_history() if self.lnworker and include_lightning else {}
×
1265
        for tx_item in lightning_history.values():
×
1266
            txid = tx_item.get('txid')
×
1267
            ln_value = Decimal(tx_item['amount_msat']) / 1000
×
1268
            tx_item['lightning'] = True
×
1269
            tx_item['ln_value'] = Satoshis(ln_value)
×
1270
            key = tx_item.get('txid') or tx_item['payment_hash']
×
1271
            transactions_tmp[key] = tx_item
×
1272
        # sort on-chain and LN stuff into new dict, by timestamp
1273
        # (we rely on this being a *stable* sort)
1274
        def sort_key(x):
×
1275
            txid, tx_item = x
×
1276
            ts = tx_item.get('monotonic_timestamp') or tx_item.get('timestamp') or float('inf')
×
1277
            height = self.adb.tx_height_to_sort_height(tx_item.get('height'))
×
1278
            return ts, height
×
1279
        transactions = OrderedDictWithIndex()
×
1280
        for k, v in sorted(list(transactions_tmp.items()), key=sort_key):
×
1281
            transactions[k] = v
×
1282
        now = time.time()
×
1283
        balance = 0
×
1284
        for item in transactions.values():
×
1285
            # add on-chain and lightning values
1286
            value = Decimal(0)
×
1287
            if item.get('bc_value'):
×
1288
                value += item['bc_value'].value
×
1289
            if item.get('ln_value'):
×
1290
                value += item.get('ln_value').value
×
1291
            # note: 'value' and 'balance' has msat precision (as LN has msat precision)
1292
            item['value'] = Satoshis(value)
×
1293
            balance += value
×
1294
            item['balance'] = Satoshis(balance)
×
1295
            if include_fiat:
×
1296
                txid = item.get('txid')
×
1297
                if not item.get('lightning') and txid:
×
1298
                    fiat_fields = self.get_tx_item_fiat(tx_hash=txid, amount_sat=value, fx=fx, tx_fee=item['fee_sat'])
×
1299
                    item.update(fiat_fields)
×
1300
                else:
1301
                    timestamp = item['timestamp'] or now
×
1302
                    fiat_value = value / Decimal(bitcoin.COIN) * fx.timestamp_rate(timestamp)
×
1303
                    item['fiat_value'] = Fiat(fiat_value, fx.ccy)
×
1304
                    item['fiat_default'] = True
×
1305
        return transactions
×
1306

1307
    @profiler
5✔
1308
    def get_detailed_history(
5✔
1309
            self,
1310
            from_timestamp=None,
1311
            to_timestamp=None,
1312
            fx=None,
1313
            show_addresses=False,
1314
            from_height=None,
1315
            to_height=None):
1316
        # History with capital gains, using utxo pricing
1317
        # FIXME: Lightning capital gains would requires FIFO
1318
        if (from_timestamp is not None or to_timestamp is not None) \
×
1319
                and (from_height is not None or to_height is not None):
1320
            raise Exception('timestamp and block height based filtering cannot be used together')
×
1321

1322
        show_fiat = fx and fx.is_enabled() and fx.has_history()
×
1323
        out = []
×
1324
        income = 0
×
1325
        expenditures = 0
×
1326
        capital_gains = Decimal(0)
×
1327
        fiat_income = Decimal(0)
×
1328
        fiat_expenditures = Decimal(0)
×
1329
        now = time.time()
×
1330
        for item in self.get_onchain_history():
×
1331
            timestamp = item['timestamp']
×
1332
            if from_timestamp and (timestamp or now) < from_timestamp:
×
1333
                continue
×
1334
            if to_timestamp and (timestamp or now) >= to_timestamp:
×
1335
                continue
×
1336
            height = item['height']
×
1337
            if from_height is not None and from_height > height > 0:
×
1338
                continue
×
1339
            if to_height is not None and (height >= to_height or height <= 0):
×
1340
                continue
×
1341
            tx_hash = item['txid']
×
1342
            tx = self.db.get_transaction(tx_hash)
×
1343
            tx_fee = item['fee_sat']
×
1344
            item['fee'] = Satoshis(tx_fee) if tx_fee is not None else None
×
1345
            if show_addresses:
×
1346
                item['inputs'] = list(map(lambda x: x.to_json(), tx.inputs()))
×
1347
                item['outputs'] = list(map(lambda x: {'address': x.get_ui_address_str(), 'value': Satoshis(x.value)},
×
1348
                                           tx.outputs()))
1349
            # fixme: use in and out values
1350
            value = item['bc_value'].value
×
1351
            if value < 0:
×
1352
                expenditures += -value
×
1353
            else:
1354
                income += value
×
1355
            # fiat computations
1356
            if show_fiat:
×
1357
                fiat_fields = self.get_tx_item_fiat(tx_hash=tx_hash, amount_sat=value, fx=fx, tx_fee=tx_fee)
×
1358
                fiat_value = fiat_fields['fiat_value'].value
×
1359
                item.update(fiat_fields)
×
1360
                if value < 0:
×
1361
                    capital_gains += fiat_fields['capital_gain'].value
×
1362
                    fiat_expenditures += -fiat_value
×
1363
                else:
1364
                    fiat_income += fiat_value
×
1365
            out.append(item)
×
1366
        # add summary
1367
        if out:
×
1368
            first_item = out[0]
×
1369
            last_item = out[-1]
×
1370
            if from_height or to_height:
×
1371
                start_height = from_height
×
1372
                end_height = to_height
×
1373
            else:
1374
                start_height = first_item['height'] - 1
×
1375
                end_height = last_item['height']
×
1376

1377
            b = first_item['bc_balance'].value
×
1378
            v = first_item['bc_value'].value
×
1379
            start_balance = None if b is None or v is None else b - v
×
1380
            end_balance = last_item['bc_balance'].value
×
1381

1382
            if from_timestamp is not None and to_timestamp is not None:
×
1383
                start_timestamp = from_timestamp
×
1384
                end_timestamp = to_timestamp
×
1385
            else:
1386
                start_timestamp = first_item['timestamp']
×
1387
                end_timestamp = last_item['timestamp']
×
1388

1389
            start_coins = self.get_utxos(
×
1390
                block_height=start_height,
1391
                confirmed_funding_only=True,
1392
                confirmed_spending_only=True,
1393
                nonlocal_only=True)
1394
            end_coins = self.get_utxos(
×
1395
                block_height=end_height,
1396
                confirmed_funding_only=True,
1397
                confirmed_spending_only=True,
1398
                nonlocal_only=True)
1399

1400
            def summary_point(timestamp, height, balance, coins):
×
1401
                date = timestamp_to_datetime(timestamp)
×
1402
                out = {
×
1403
                    'date': date,
1404
                    'block_height': height,
1405
                    'BTC_balance': Satoshis(balance),
1406
                }
1407
                if show_fiat:
×
1408
                    ap = self.acquisition_price(coins, fx.timestamp_rate, fx.ccy)
×
1409
                    lp = self.liquidation_price(coins, fx.timestamp_rate, timestamp)
×
1410
                    out['acquisition_price'] = Fiat(ap, fx.ccy)
×
1411
                    out['liquidation_price'] = Fiat(lp, fx.ccy)
×
1412
                    out['unrealized_gains'] = Fiat(lp - ap, fx.ccy)
×
1413
                    out['fiat_balance'] = Fiat(fx.historical_value(balance, date), fx.ccy)
×
1414
                    out['BTC_fiat_price'] = Fiat(fx.historical_value(COIN, date), fx.ccy)
×
1415
                return out
×
1416

1417
            summary_start = summary_point(start_timestamp, start_height, start_balance, start_coins)
×
1418
            summary_end = summary_point(end_timestamp, end_height, end_balance, end_coins)
×
1419
            flow = {
×
1420
                'BTC_incoming': Satoshis(income),
1421
                'BTC_outgoing': Satoshis(expenditures)
1422
            }
1423
            if show_fiat:
×
1424
                flow['fiat_currency'] = fx.ccy
×
1425
                flow['fiat_incoming'] = Fiat(fiat_income, fx.ccy)
×
1426
                flow['fiat_outgoing'] = Fiat(fiat_expenditures, fx.ccy)
×
1427
                flow['realized_capital_gains'] = Fiat(capital_gains, fx.ccy)
×
1428
            summary = {
×
1429
                'begin': summary_start,
1430
                'end': summary_end,
1431
                'flow': flow,
1432
            }
1433

1434
        else:
1435
            summary = {}
×
1436
        return {
×
1437
            'transactions': out,
1438
            'summary': summary
1439
        }
1440

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

1444
    def liquidation_price(self, coins, price_func, timestamp):
5✔
1445
        p = price_func(timestamp)
×
1446
        return sum([coin.value_sats() for coin in coins]) * p / Decimal(COIN)
×
1447

1448
    def default_fiat_value(self, tx_hash, fx, value_sat):
5✔
1449
        return value_sat / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate)
5✔
1450

1451
    def get_tx_item_fiat(
5✔
1452
            self,
1453
            *,
1454
            tx_hash: str,
1455
            amount_sat: int,
1456
            fx: 'FxThread',
1457
            tx_fee: Optional[int],
1458
    ) -> Dict[str, Any]:
1459
        item = {}
×
1460
        fiat_value = self.get_fiat_value(tx_hash, fx.ccy)
×
1461
        fiat_default = fiat_value is None
×
1462
        fiat_rate = self.price_at_timestamp(tx_hash, fx.timestamp_rate)
×
1463
        fiat_value = fiat_value if fiat_value is not None else self.default_fiat_value(tx_hash, fx, amount_sat)
×
1464
        fiat_fee = tx_fee / Decimal(COIN) * fiat_rate if tx_fee is not None else None
×
1465
        item['fiat_currency'] = fx.ccy
×
1466
        item['fiat_rate'] = Fiat(fiat_rate, fx.ccy)
×
1467
        item['fiat_value'] = Fiat(fiat_value, fx.ccy)
×
1468
        item['fiat_fee'] = Fiat(fiat_fee, fx.ccy) if fiat_fee is not None else None
×
1469
        item['fiat_default'] = fiat_default
×
1470
        if amount_sat < 0:
×
1471
            acquisition_price = - amount_sat / Decimal(COIN) * self.average_price(tx_hash, fx.timestamp_rate, fx.ccy)
×
1472
            liquidation_price = - fiat_value
×
1473
            item['acquisition_price'] = Fiat(acquisition_price, fx.ccy)
×
1474
            cg = liquidation_price - acquisition_price
×
1475
            item['capital_gain'] = Fiat(cg, fx.ccy)
×
1476
        return item
×
1477

1478
    def _get_label(self, key: str) -> str:
5✔
1479
        # key is typically: address / txid / LN-payment-hash-hex
1480
        return self._labels.get(key) or ''
×
1481

1482
    def get_label_for_address(self, addr: str) -> str:
5✔
1483
        label = self._labels.get(addr) or ''
×
1484
        if not label and (request := self.get_request_by_addr(addr)):
×
1485
            label = request.get_message()
×
1486
        return label
×
1487

1488
    def get_label_for_txid(self, tx_hash: str) -> str:
5✔
1489
        return self._labels.get(tx_hash) or self._get_default_label_for_txid(tx_hash)
5✔
1490

1491
    def _get_default_label_for_txid(self, tx_hash: str) -> str:
5✔
1492
        if self.lnworker and (label:= self.lnworker.get_label_for_txid(tx_hash)):
5✔
1493
            return label
×
1494
        # note: we don't deserialize tx as the history calls us for every tx, and that would be slow
1495
        if not self.db.get_txi_addresses(tx_hash):
5✔
1496
            # no inputs are ismine -> likely incoming payment -> concat labels of output addresses
1497
            labels = []
×
1498
            for addr in self.db.get_txo_addresses(tx_hash):
×
1499
                label = self.get_label_for_address(addr)
×
1500
                if label:
×
1501
                    labels.append(label)
×
1502
            return ', '.join(labels)
×
1503
        else:
1504
            # some inputs are ismine -> likely outgoing payment
1505
            labels = []
5✔
1506
            for invoice in self.get_relevant_invoices_for_tx(tx_hash):
5✔
1507
                if invoice.message:
×
1508
                    labels.append(invoice.message)
×
1509
            return ', '.join(labels)
5✔
1510

1511
    def _get_default_label_for_rhash(self, rhash: str) -> str:
5✔
1512
        req = self.get_request(rhash)
×
1513
        return req.get_message() if req else ''
×
1514

1515
    def get_label_for_rhash(self, rhash: str) -> str:
5✔
1516
        return self._labels.get(rhash) or self._get_default_label_for_rhash(rhash)
×
1517

1518
    def get_all_labels(self) -> Dict[str, str]:
5✔
1519
        with self.lock:
×
1520
            return copy.copy(self._labels)
×
1521

1522
    def get_tx_status(self, tx_hash, tx_mined_info: TxMinedInfo):
5✔
1523
        extra = []
×
1524
        height = tx_mined_info.height
×
1525
        conf = tx_mined_info.conf
×
1526
        timestamp = tx_mined_info.timestamp
×
1527
        if height == TX_HEIGHT_FUTURE:
×
1528
            num_blocks_remainining = tx_mined_info.wanted_height - self.adb.get_local_height()
×
1529
            num_blocks_remainining = max(0, num_blocks_remainining)
×
1530
            return 2, _('in {} blocks').format(num_blocks_remainining)
×
1531
        if conf == 0:
×
1532
            tx = self.db.get_transaction(tx_hash)
×
1533
            if not tx:
×
1534
                return 2, _("unknown")
×
1535
            is_final = tx and tx.is_final()
×
1536
            fee = self.adb.get_tx_fee(tx_hash)
×
1537
            if fee is not None:
×
1538
                size = tx.estimated_size()
×
1539
                fee_per_byte = fee / size
×
1540
                extra.append(format_fee_satoshis(fee_per_byte) + ' sat/b')
×
1541
            if fee is not None and height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED) \
×
1542
               and self.config.has_fee_mempool():
1543
                exp_n = self.config.fee_to_depth(fee_per_byte)
×
1544
                if exp_n is not None:
×
1545
                    extra.append(self.config.get_depth_mb_str(exp_n))
×
1546
            if height == TX_HEIGHT_LOCAL:
×
1547
                status = 3
×
1548
            elif height == TX_HEIGHT_UNCONF_PARENT:
×
1549
                status = 1
×
1550
            elif height == TX_HEIGHT_UNCONFIRMED:
×
1551
                status = 0
×
1552
            else:
1553
                status = 2  # not SPV verified
×
1554
        else:
1555
            status = 3 + min(conf, 6)
×
1556
        time_str = format_time(timestamp) if timestamp else _("unknown")
×
1557
        status_str = TX_STATUS[status] if status < 4 else time_str
×
1558
        if extra:
×
1559
            status_str += ' [%s]'%(', '.join(extra))
×
1560
        return status, status_str
×
1561

1562
    def relayfee(self):
5✔
1563
        return relayfee(self.network)
5✔
1564

1565
    def dust_threshold(self):
5✔
1566
        return dust_threshold(self.network)
5✔
1567

1568
    def get_unconfirmed_base_tx_for_batching(self, outputs, coins) -> Optional[Transaction]:
5✔
1569
        candidate = None
5✔
1570
        domain = self.get_addresses()
5✔
1571
        for hist_item in self.adb.get_history(domain):
5✔
1572
            # tx should not be mined yet
1573
            if hist_item.tx_mined_status.conf > 0: continue
5✔
1574
            # conservative future proofing of code: only allow known unconfirmed types
1575
            if hist_item.tx_mined_status.height not in (TX_HEIGHT_UNCONFIRMED,
5✔
1576
                                                        TX_HEIGHT_UNCONF_PARENT,
1577
                                                        TX_HEIGHT_LOCAL):
1578
                continue
×
1579
            # tx should be "outgoing" from wallet
1580
            if hist_item.delta >= 0:
5✔
1581
                continue
5✔
1582
            tx = self.db.get_transaction(hist_item.txid)
5✔
1583
            if not tx:
5✔
1584
                continue
×
1585
            # is_mine outputs should not be spent yet
1586
            # to avoid cancelling our own dependent transactions
1587
            txid = tx.txid()
5✔
1588
            if any([self.is_mine(o.address) and self.db.get_spent_outpoint(txid, output_idx)
5✔
1589
                    for output_idx, o in enumerate(tx.outputs())]):
1590
                continue
×
1591
            # all inputs should be is_mine
1592
            if not all([self.is_mine(self.adb.get_txin_address(txin)) for txin in tx.inputs()]):
5✔
1593
                continue
×
1594
            # do not mutate LN funding txs, as that would change their txid
1595
            if self.is_lightning_funding_tx(txid):
5✔
1596
                continue
×
1597
            # tx must have opted-in for RBF (even if local, for consistency)
1598
            if tx.is_final():
5✔
1599
                continue
×
1600
            # reject merge if we need to spend outputs from the base tx
1601
            remaining_amount = sum(c.value_sats() for c in coins if c.prevout.txid.hex() != tx.txid())
5✔
1602
            change_amount = sum(o.value for o in tx.outputs() if self.is_change(o.address))
5✔
1603
            output_amount = sum(o.value for o in outputs)
5✔
1604
            if output_amount > remaining_amount + change_amount:
5✔
1605
                continue
5✔
1606
            # prefer txns already in mempool (vs local)
1607
            if hist_item.tx_mined_status.height == TX_HEIGHT_LOCAL:
5✔
1608
                candidate = tx
×
1609
                continue
×
1610
            return tx
5✔
1611
        return candidate
5✔
1612

1613
    def get_change_addresses_for_new_transaction(
5✔
1614
            self, preferred_change_addr=None, *, allow_reusing_used_change_addrs: bool = True,
1615
    ) -> List[str]:
1616
        change_addrs = []
5✔
1617
        if preferred_change_addr:
5✔
1618
            if isinstance(preferred_change_addr, (list, tuple)):
5✔
1619
                change_addrs = list(preferred_change_addr)
5✔
1620
            else:
1621
                change_addrs = [preferred_change_addr]
×
1622
        elif self.use_change:
5✔
1623
            # Recalc and get unused change addresses
1624
            addrs = self.calc_unused_change_addresses()
5✔
1625
            # New change addresses are created only after a few
1626
            # confirmations.
1627
            if addrs:
5✔
1628
                # if there are any unused, select all
1629
                change_addrs = addrs
5✔
1630
            else:
1631
                # if there are none, take one randomly from the last few
1632
                if not allow_reusing_used_change_addrs:
5✔
1633
                    return []
5✔
1634
                addrs = self.get_change_addresses(slice_start=-self.gap_limit_for_change)
×
1635
                change_addrs = [random.choice(addrs)] if addrs else []
×
1636
        for addr in change_addrs:
5✔
1637
            assert is_address(addr), f"not valid bitcoin address: {addr}"
5✔
1638
            # note that change addresses are not necessarily ismine
1639
            # in which case this is a no-op
1640
            self.check_address_for_corruption(addr)
5✔
1641
        max_change = self.max_change_outputs if self.multiple_change else 1
5✔
1642
        return change_addrs[:max_change]
5✔
1643

1644
    def get_single_change_address_for_new_transaction(
5✔
1645
            self, preferred_change_addr=None, *, allow_reusing_used_change_addrs: bool = True,
1646
    ) -> Optional[str]:
1647
        addrs = self.get_change_addresses_for_new_transaction(
5✔
1648
            preferred_change_addr=preferred_change_addr,
1649
            allow_reusing_used_change_addrs=allow_reusing_used_change_addrs,
1650
        )
1651
        if addrs:
5✔
1652
            return addrs[0]
5✔
1653
        return None
×
1654

1655
    @check_returned_address_for_corruption
5✔
1656
    def get_new_sweep_address_for_channel(self) -> str:
5✔
1657
        # Recalc and get unused change addresses
1658
        addrs = self.calc_unused_change_addresses()
×
1659
        if addrs:
×
1660
            selected_addr = addrs[0]
×
1661
        else:
1662
            # if there are none, take one randomly from the last few
1663
            addrs = self.get_change_addresses(slice_start=-self.gap_limit_for_change)
×
1664
            if addrs:
×
1665
                selected_addr = random.choice(addrs)
×
1666
            else:  # fallback for e.g. imported wallets
1667
                selected_addr = self.get_receiving_address()
×
1668
        assert is_address(selected_addr), f"not valid bitcoin address: {selected_addr}"
×
1669
        return selected_addr
×
1670

1671
    def can_pay_onchain(self, outputs, coins=None):
5✔
1672
        fee = partial(self.config.estimate_fee, allow_fallback_to_static_rates=True)  # to avoid NoDynamicFeeEstimates
×
1673
        try:
×
1674
            self.make_unsigned_transaction(
×
1675
                coins=coins,
1676
                outputs=outputs,
1677
                fee=fee)
1678
        except NotEnoughFunds:
×
1679
            return False
×
1680
        return True
×
1681

1682
    @profiler(min_threshold=0.1)
5✔
1683
    def make_unsigned_transaction(
5✔
1684
            self, *,
1685
            coins: Sequence[PartialTxInput],
1686
            outputs: List[PartialTxOutput],
1687
            fee=None,
1688
            change_addr: str = None,
1689
            is_sweep=False,
1690
            rbf=True) -> PartialTransaction:
1691
        """Can raise NotEnoughFunds or NoDynamicFeeEstimates."""
1692

1693
        if not coins:  # any bitcoin tx must have at least 1 input by consensus
5✔
1694
            raise NotEnoughFunds()
5✔
1695
        if any([c.already_has_some_signatures() for c in coins]):
5✔
1696
            raise Exception("Some inputs already contain signatures!")
×
1697

1698
        # prevent side-effect with '!'
1699
        outputs = copy.deepcopy(outputs)
5✔
1700

1701
        # check outputs
1702
        i_max = []
5✔
1703
        i_max_sum = 0
5✔
1704
        for i, o in enumerate(outputs):
5✔
1705
            weight = parse_max_spend(o.value)
5✔
1706
            if weight:
5✔
1707
                i_max_sum += weight
5✔
1708
                i_max.append((weight, i))
5✔
1709

1710
        if fee is None and self.config.fee_per_kb() is None:
5✔
1711
            raise NoDynamicFeeEstimates()
×
1712

1713
        for item in coins:
5✔
1714
            self.add_input_info(item)
5✔
1715

1716
        # Fee estimator
1717
        if fee is None:
5✔
1718
            fee_estimator = self.config.estimate_fee
×
1719
        elif isinstance(fee, Number):
5✔
1720
            fee_estimator = lambda size: fee
5✔
1721
        elif callable(fee):
5✔
1722
            fee_estimator = fee
5✔
1723
        else:
1724
            raise Exception(f'Invalid argument fee: {fee}')
×
1725

1726
        # set if we merge with another transaction
1727
        rbf_merge_txid = None
5✔
1728

1729
        if len(i_max) == 0:
5✔
1730
            # Let the coin chooser select the coins to spend
1731
            coin_chooser = coinchooser.get_coin_chooser(self.config)
5✔
1732
            # If there is an unconfirmed RBF tx, merge with it
1733
            base_tx = self.get_unconfirmed_base_tx_for_batching(outputs, coins) if self.config.WALLET_BATCH_RBF else None
5✔
1734
            if base_tx:
5✔
1735
                # make sure we don't try to spend change from the tx-to-be-replaced:
1736
                coins = [c for c in coins if c.prevout.txid.hex() != base_tx.txid()]
5✔
1737
                is_local = self.adb.get_tx_height(base_tx.txid()).height == TX_HEIGHT_LOCAL
5✔
1738
                base_tx = PartialTransaction.from_tx(base_tx)
5✔
1739
                base_tx.add_info_from_wallet(self)
5✔
1740
                base_tx_fee = base_tx.get_fee()
5✔
1741
                relayfeerate = Decimal(self.relayfee()) / 1000
5✔
1742
                original_fee_estimator = fee_estimator
5✔
1743
                def fee_estimator(size: Union[int, float, Decimal]) -> int:
5✔
1744
                    size = Decimal(size)
5✔
1745
                    lower_bound = base_tx_fee + round(size * relayfeerate)
5✔
1746
                    lower_bound = lower_bound if not is_local else 0
5✔
1747
                    return int(max(lower_bound, original_fee_estimator(size)))
5✔
1748
                txi = base_tx.inputs()
5✔
1749
                txo = list(filter(lambda o: not self.is_change(o.address), base_tx.outputs()))
5✔
1750
                old_change_addrs = [o.address for o in base_tx.outputs() if self.is_change(o.address)]
5✔
1751
                rbf_merge_txid = base_tx.txid()
5✔
1752
            else:
1753
                txi = []
5✔
1754
                txo = []
5✔
1755
                old_change_addrs = []
5✔
1756
            # change address. if empty, coin_chooser will set it
1757
            change_addrs = self.get_change_addresses_for_new_transaction(change_addr or old_change_addrs)
5✔
1758
            tx = coin_chooser.make_tx(
5✔
1759
                coins=coins,
1760
                inputs=txi,
1761
                outputs=list(outputs) + txo,
1762
                change_addrs=change_addrs,
1763
                fee_estimator_vb=fee_estimator,
1764
                dust_threshold=self.dust_threshold())
1765
        else:
1766
            # "spend max" branch
1767
            # note: This *will* spend inputs with negative effective value (if there are any).
1768
            #       Given as the user is spending "max", and so might be abandoning the wallet,
1769
            #       try to include all UTXOs, otherwise leftover might remain in the UTXO set
1770
            #       forever. see #5433
1771
            # note: Actually, it might be the case that not all UTXOs from the wallet are
1772
            #       being spent if the user manually selected UTXOs.
1773
            sendable = sum(map(lambda c: c.value_sats(), coins))
5✔
1774
            for (_,i) in i_max:
5✔
1775
                outputs[i].value = 0
5✔
1776
            tx = PartialTransaction.from_io(list(coins), list(outputs))
5✔
1777
            fee = fee_estimator(tx.estimated_size())
5✔
1778
            amount = sendable - tx.output_value() - fee
5✔
1779
            if amount < 0:
5✔
1780
                raise NotEnoughFunds()
×
1781
            distr_amount = 0
5✔
1782
            for (weight, i) in i_max:
5✔
1783
                val = int((amount/i_max_sum) * weight)
5✔
1784
                outputs[i].value = val
5✔
1785
                distr_amount += val
5✔
1786

1787
            (x,i) = i_max[-1]
5✔
1788
            outputs[i].value += (amount - distr_amount)
5✔
1789
            tx = PartialTransaction.from_io(list(coins), list(outputs))
5✔
1790

1791
        # Timelock tx to current height.
1792
        tx.locktime = get_locktime_for_new_transaction(self.network)
5✔
1793
        tx.rbf_merge_txid = rbf_merge_txid
5✔
1794
        tx.set_rbf(rbf)
5✔
1795
        tx.add_info_from_wallet(self)
5✔
1796
        run_hook('make_unsigned_transaction', self, tx)
5✔
1797
        return tx
5✔
1798

1799
    def mktx(self, *,
5✔
1800
             outputs: List[PartialTxOutput],
1801
             password=None, fee=None, change_addr=None,
1802
             domain=None, rbf=True, nonlocal_only=False,
1803
             tx_version=None, sign=True) -> PartialTransaction:
1804
        coins = self.get_spendable_coins(domain, nonlocal_only=nonlocal_only)
5✔
1805
        tx = self.make_unsigned_transaction(
5✔
1806
            coins=coins,
1807
            outputs=outputs,
1808
            fee=fee,
1809
            change_addr=change_addr,
1810
            rbf=rbf)
1811
        if tx_version is not None:
5✔
1812
            tx.version = tx_version
5✔
1813
        if sign:
5✔
1814
            self.sign_transaction(tx, password)
5✔
1815
        return tx
5✔
1816

1817
    def is_frozen_address(self, addr: str) -> bool:
5✔
1818
        return addr in self._frozen_addresses
×
1819

1820
    def is_frozen_coin(self, utxo: PartialTxInput) -> bool:
5✔
1821
        prevout_str = utxo.prevout.to_str()
5✔
1822
        frozen = self._frozen_coins.get(prevout_str, None)
5✔
1823
        # note: there are three possible states for 'frozen':
1824
        #       True/False if the user explicitly set it,
1825
        #       None otherwise
1826
        if frozen is None:
5✔
1827
            return self._is_coin_small_and_unconfirmed(utxo)
5✔
1828
        return bool(frozen)
×
1829

1830
    def _is_coin_small_and_unconfirmed(self, utxo: PartialTxInput) -> bool:
5✔
1831
        """If true, the coin should not be spent.
1832
        The idea here is that an attacker might send us a UTXO in a
1833
        large low-fee unconfirmed tx that will ~never confirm. If we
1834
        spend it as part of a tx ourselves, that too will not confirm
1835
        (unless we use a high fee, but that might not be worth it for
1836
        a small value UTXO).
1837
        In particular, this test triggers for large "dusting transactions"
1838
        that are used for advertising purposes by some entities.
1839
        see #6960
1840
        """
1841
        # confirmed UTXOs are fine; check this first for performance:
1842
        block_height = utxo.block_height
5✔
1843
        assert block_height is not None
5✔
1844
        if block_height > 0:
5✔
1845
            return False
×
1846
        # exempt large value UTXOs
1847
        value_sats = utxo.value_sats()
5✔
1848
        assert value_sats is not None
5✔
1849
        threshold = self.config.WALLET_UNCONF_UTXO_FREEZE_THRESHOLD_SAT
5✔
1850
        if value_sats >= threshold:
5✔
1851
            return False
5✔
1852
        # if funding tx has any is_mine input, then UTXO is fine
1853
        funding_tx = self.db.get_transaction(utxo.prevout.txid.hex())
5✔
1854
        if funding_tx is None:
5✔
1855
            # we should typically have the funding tx available;
1856
            # might not have it e.g. while not up_to_date
1857
            return True
×
1858
        if any(self.is_mine(self.adb.get_txin_address(txin))
5✔
1859
               for txin in funding_tx.inputs()):
1860
            return False
5✔
1861
        return True
×
1862

1863
    def set_frozen_state_of_addresses(
5✔
1864
        self,
1865
        addrs: Iterable[str],
1866
        freeze: bool,
1867
        *,
1868
        write_to_disk: bool = True,
1869
    ) -> bool:
1870
        """Set frozen state of the addresses to FREEZE, True or False"""
1871
        if all(self.is_mine(addr) for addr in addrs):
5✔
1872
            with self._freeze_lock:
5✔
1873
                if freeze:
5✔
1874
                    self._frozen_addresses |= set(addrs)
5✔
1875
                else:
1876
                    self._frozen_addresses -= set(addrs)
5✔
1877
                self.db.put('frozen_addresses', list(self._frozen_addresses))
5✔
1878
            util.trigger_callback('status')
5✔
1879
            if write_to_disk:
5✔
1880
                self.save_db()
5✔
1881
            return True
5✔
1882
        return False
×
1883

1884
    def set_frozen_state_of_coins(
5✔
1885
        self,
1886
        utxos: Iterable[str],
1887
        freeze: bool,
1888
        *,
1889
        write_to_disk: bool = True,
1890
    ) -> None:
1891
        """Set frozen state of the utxos to FREEZE, True or False"""
1892
        # basic sanity check that input is not garbage: (see if raises)
1893
        [TxOutpoint.from_str(utxo) for utxo in utxos]
×
1894
        with self._freeze_lock:
×
1895
            for utxo in utxos:
×
1896
                self._frozen_coins[utxo] = bool(freeze)
×
1897
        util.trigger_callback('status')
×
1898
        if write_to_disk:
×
1899
            self.save_db()
×
1900

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

1905
    def set_reserved_state_of_address(self, addr: str, *, reserved: bool) -> None:
5✔
1906
        if not self.is_mine(addr):
×
1907
            return
×
1908
        with self.lock:
×
1909
            if reserved:
×
1910
                self._reserved_addresses.add(addr)
×
1911
            else:
1912
                self._reserved_addresses.discard(addr)
×
1913
            self.db.put('reserved_addresses', list(self._reserved_addresses))
×
1914

1915
    def can_export(self):
5✔
1916
        return not self.is_watching_only() and hasattr(self.keystore, 'get_private_key')
×
1917

1918
    def bump_fee(
5✔
1919
            self,
1920
            *,
1921
            tx: Transaction,
1922
            txid: str = None,
1923
            new_fee_rate: Union[int, float, Decimal],
1924
            coins: Sequence[PartialTxInput] = None,
1925
            decrease_payment=False,
1926
    ) -> PartialTransaction:
1927
        """Increase the miner fee of 'tx'.
1928
        'new_fee_rate' is the target min rate in sat/vbyte
1929
        'coins' is a list of UTXOs we can choose from as potential new inputs to be added
1930

1931
        note: it is the caller's responsibility to have already called tx.add_info_from_network().
1932
              Without that, all txins must be ismine.
1933
        """
1934
        txid = txid or tx.txid()
5✔
1935
        assert txid
5✔
1936
        assert tx.txid() in (None, txid)
5✔
1937
        if not isinstance(tx, PartialTransaction):
5✔
1938
            tx = PartialTransaction.from_tx(tx)
5✔
1939
        assert isinstance(tx, PartialTransaction)
5✔
1940
        tx.remove_signatures()
5✔
1941
        if tx.is_final():
5✔
1942
            raise CannotBumpFee(_('Transaction is final'))
×
1943
        new_fee_rate = quantize_feerate(new_fee_rate)  # strip excess precision
5✔
1944
        tx.add_info_from_wallet(self)
5✔
1945
        if tx.is_missing_info_from_network():
5✔
1946
            raise Exception("tx missing info from network")
×
1947
        old_tx_size = tx.estimated_size()
5✔
1948
        old_fee = tx.get_fee()
5✔
1949
        assert old_fee is not None
5✔
1950
        old_fee_rate = old_fee / old_tx_size  # sat/vbyte
5✔
1951
        if new_fee_rate <= old_fee_rate:
5✔
1952
            raise CannotBumpFee(_("The new fee rate needs to be higher than the old fee rate."))
×
1953

1954
        if not decrease_payment:
5✔
1955
            # FIXME: we should try decreasing change first,
1956
            # but it requires updating a bunch of unit tests
1957
            try:
5✔
1958
                tx_new = self._bump_fee_through_coinchooser(
5✔
1959
                    tx=tx,
1960
                    txid=txid,
1961
                    new_fee_rate=new_fee_rate,
1962
                    coins=coins,
1963
                )
1964
            except CannotBumpFee as e:
5✔
1965
                tx_new = self._bump_fee_through_decreasing_change(
5✔
1966
                    tx=tx, new_fee_rate=new_fee_rate)
1967
        else:
1968
            tx_new = self._bump_fee_through_decreasing_payment(
5✔
1969
                tx=tx, new_fee_rate=new_fee_rate)
1970

1971
        target_min_fee = new_fee_rate * tx_new.estimated_size()
5✔
1972
        actual_fee = tx_new.get_fee()
5✔
1973
        if actual_fee + 1 < target_min_fee:
5✔
1974
            raise CannotBumpFee(
×
1975
                f"bump_fee fee target was not met. "
1976
                f"got {actual_fee}, expected >={target_min_fee}. "
1977
                f"target rate was {new_fee_rate}")
1978
        tx_new.locktime = get_locktime_for_new_transaction(self.network)
5✔
1979
        tx_new.set_rbf(True)
5✔
1980
        tx_new.add_info_from_wallet(self)
5✔
1981
        return tx_new
5✔
1982

1983
    def _bump_fee_through_coinchooser(
5✔
1984
            self,
1985
            *,
1986
            tx: PartialTransaction,
1987
            txid: str,
1988
            new_fee_rate: Union[int, Decimal],
1989
            coins: Sequence[PartialTxInput] = None,
1990
    ) -> PartialTransaction:
1991
        """Increase the miner fee of 'tx'.
1992

1993
        - keeps all inputs
1994
        - keeps all not is_mine outputs,
1995
        - allows adding new inputs
1996
        """
1997
        assert txid
5✔
1998
        tx = copy.deepcopy(tx)
5✔
1999
        tx.add_info_from_wallet(self)
5✔
2000
        assert tx.get_fee() is not None
5✔
2001
        old_inputs = list(tx.inputs())
5✔
2002
        old_outputs = list(tx.outputs())
5✔
2003
        # change address
2004
        old_change_addrs = [o.address for o in old_outputs if self.is_change(o.address)]
5✔
2005
        change_addrs = self.get_change_addresses_for_new_transaction(old_change_addrs)
5✔
2006
        # which outputs to keep?
2007
        if old_change_addrs:
5✔
2008
            fixed_outputs = list(filter(lambda o: not self.is_change(o.address), old_outputs))
5✔
2009
        else:
2010
            if all(self.is_mine(o.address) for o in old_outputs):
5✔
2011
                # all outputs are is_mine and none of them are change.
2012
                # we bail out as it's unclear what the user would want!
2013
                # the coinchooser bump fee method is probably not a good idea in this case
2014
                raise CannotBumpFee(_('All outputs are non-change is_mine'))
5✔
2015
            old_not_is_mine = list(filter(lambda o: not self.is_mine(o.address), old_outputs))
5✔
2016
            if old_not_is_mine:
5✔
2017
                fixed_outputs = old_not_is_mine
5✔
2018
            else:
2019
                fixed_outputs = old_outputs
×
2020
        if not fixed_outputs:
5✔
2021
            raise CannotBumpFee(_('Could not figure out which outputs to keep'))
5✔
2022

2023
        if coins is None:
5✔
2024
            coins = self.get_spendable_coins(None)
5✔
2025
        # make sure we don't try to spend output from the tx-to-be-replaced:
2026
        coins = [c for c in coins if c.prevout.txid.hex() != txid]
5✔
2027
        for item in coins:
5✔
2028
            self.add_input_info(item)
5✔
2029
        def fee_estimator(size):
5✔
2030
            return self.config.estimate_fee_for_feerate(fee_per_kb=new_fee_rate*1000, size=size)
5✔
2031
        coin_chooser = coinchooser.get_coin_chooser(self.config)
5✔
2032
        try:
5✔
2033
            return coin_chooser.make_tx(
5✔
2034
                coins=coins,
2035
                inputs=old_inputs,
2036
                outputs=fixed_outputs,
2037
                change_addrs=change_addrs,
2038
                fee_estimator_vb=fee_estimator,
2039
                dust_threshold=self.dust_threshold())
2040
        except NotEnoughFunds as e:
5✔
2041
            raise CannotBumpFee(e)
5✔
2042

2043
    def _bump_fee_through_decreasing_change(
5✔
2044
            self,
2045
            *,
2046
            tx: PartialTransaction,
2047
            new_fee_rate: Union[int, Decimal],
2048
    ) -> PartialTransaction:
2049
        """Increase the miner fee of 'tx'.
2050

2051
        - keeps all inputs
2052
        - no new inputs are added
2053
        - change outputs are decreased or removed
2054
        """
2055
        tx = copy.deepcopy(tx)
5✔
2056
        tx.add_info_from_wallet(self)
5✔
2057
        assert tx.get_fee() is not None
5✔
2058
        inputs = tx.inputs()
5✔
2059
        outputs = tx._outputs  # note: we will mutate this directly
5✔
2060

2061
        # use own outputs
2062
        s = list(filter(lambda o: self.is_mine(o.address), outputs))
5✔
2063
        if not s:
5✔
2064
            raise CannotBumpFee('No suitable output')
×
2065

2066
        # prioritize low value outputs, to get rid of dust
2067
        s = sorted(s, key=lambda o: o.value)
5✔
2068
        for o in s:
5✔
2069
            target_fee = int(math.ceil(tx.estimated_size() * new_fee_rate))
5✔
2070
            delta = target_fee - tx.get_fee()
5✔
2071
            if delta <= 0:
5✔
2072
                break
×
2073
            i = outputs.index(o)
5✔
2074
            if o.value - delta >= self.dust_threshold():
5✔
2075
                new_output_value = o.value - delta
5✔
2076
                assert isinstance(new_output_value, int)
5✔
2077
                outputs[i].value = new_output_value
5✔
2078
                delta = 0
5✔
2079
                break
5✔
2080
            else:
2081
                del outputs[i]
5✔
2082
                # note: we mutated the outputs of tx, which will affect
2083
                #       tx.estimated_size() in the next iteration
2084
        else:
2085
            # recompute delta if there was no next iteration
2086
            target_fee = int(math.ceil(tx.estimated_size() * new_fee_rate))
5✔
2087
            delta = target_fee - tx.get_fee()
5✔
2088

2089
        if delta > 0:
5✔
2090
            raise CannotBumpFee(_('Could not find suitable outputs'))
5✔
2091

2092
        return PartialTransaction.from_io(inputs, outputs)
5✔
2093

2094
    def _bump_fee_through_decreasing_payment(
5✔
2095
            self,
2096
            *,
2097
            tx: PartialTransaction,
2098
            new_fee_rate: Union[int, Decimal],
2099
    ) -> PartialTransaction:
2100
        """
2101
        Increase the miner fee of 'tx' by decreasing amount paid.
2102
        This should be used for transactions that pay "Max".
2103

2104
        - keeps all inputs
2105
        - no new inputs are added
2106
        - Each non-ismine output is decreased proportionally to their byte-size.
2107
        """
2108
        tx = copy.deepcopy(tx)
5✔
2109
        tx.add_info_from_wallet(self)
5✔
2110
        assert tx.get_fee() is not None
5✔
2111
        inputs = tx.inputs()
5✔
2112
        outputs = tx.outputs()
5✔
2113

2114
        # select non-ismine outputs
2115
        s = [(idx, out) for (idx, out) in enumerate(outputs)
5✔
2116
             if not self.is_mine(out.address)]
2117
        s = [(idx, out) for (idx, out) in s if self._is_rbf_allowed_to_touch_tx_output(out)]
5✔
2118
        if not s:
5✔
2119
            raise CannotBumpFee("Cannot find payment output")
×
2120

2121
        del_out_idxs = set()
5✔
2122
        tx_size = tx.estimated_size()
5✔
2123
        cur_fee = tx.get_fee()
5✔
2124
        # Main loop. Each iteration decreases value of all selected outputs.
2125
        # The number of iterations is bounded by len(s) as only the final iteration
2126
        # can *not remove* any output.
2127
        for __ in range(len(s) + 1):
5✔
2128
            target_fee = int(math.ceil(tx_size * new_fee_rate))
5✔
2129
            delta_total = target_fee - cur_fee
5✔
2130
            if delta_total <= 0:
5✔
2131
                break
5✔
2132
            out_size_total = sum(Transaction.estimated_output_size_for_script(out.scriptpubkey.hex())
5✔
2133
                                 for (idx, out) in s if idx not in del_out_idxs)
2134
            if out_size_total == 0:  # no outputs left to decrease
5✔
2135
                raise CannotBumpFee(_('Could not find suitable outputs'))
5✔
2136
            for idx, out in s:
5✔
2137
                out_size = Transaction.estimated_output_size_for_script(out.scriptpubkey.hex())
5✔
2138
                delta = int(math.ceil(delta_total * out_size / out_size_total))
5✔
2139
                if out.value - delta >= self.dust_threshold():
5✔
2140
                    new_output_value = out.value - delta
5✔
2141
                    assert isinstance(new_output_value, int)
5✔
2142
                    outputs[idx].value = new_output_value
5✔
2143
                    cur_fee += delta
5✔
2144
                else:  # remove output
2145
                    tx_size -= out_size
5✔
2146
                    cur_fee += out.value
5✔
2147
                    del_out_idxs.add(idx)
5✔
2148
        if delta_total > 0:
5✔
2149
            raise CannotBumpFee(_('Could not find suitable outputs'))
×
2150

2151
        outputs = [out for (idx, out) in enumerate(outputs) if idx not in del_out_idxs]
5✔
2152
        return PartialTransaction.from_io(inputs, outputs)
5✔
2153

2154
    def _is_rbf_allowed_to_touch_tx_output(self, txout: TxOutput) -> bool:
5✔
2155
        # 2fa fee outputs if present, should not be removed or have their value decreased
2156
        if self.is_billing_address(txout.address):
5✔
2157
            return False
×
2158
        # submarine swap funding outputs must not be decreased
2159
        if self.lnworker and self.lnworker.swap_manager.is_lockup_address_for_a_swap(txout.address):
5✔
2160
            return False
×
2161
        return True
5✔
2162

2163
    def cpfp(self, tx: Transaction, fee: int) -> Optional[PartialTransaction]:
5✔
2164
        txid = tx.txid()
5✔
2165
        for i, o in enumerate(tx.outputs()):
5✔
2166
            address, value = o.address, o.value
5✔
2167
            if self.is_mine(address):
5✔
2168
                break
5✔
2169
        else:
2170
            raise CannotCPFP(_("Could not find suitable output"))
5✔
2171
        coins = self.adb.get_addr_utxo(address)
5✔
2172
        item = coins.get(TxOutpoint.from_str(txid+':%d'%i))
5✔
2173
        if not item:
5✔
2174
            raise CannotCPFP(_("Could not find coins for output"))
×
2175
        inputs = [item]
5✔
2176
        out_address = (self.get_single_change_address_for_new_transaction(allow_reusing_used_change_addrs=False)
5✔
2177
                       or self.get_unused_address()
2178
                       or address)
2179
        output_value = value - fee
5✔
2180
        if output_value < self.dust_threshold():
5✔
2181
            raise CannotCPFP(_("The output value remaining after fee is too low."))
×
2182
        outputs = [PartialTxOutput.from_address_and_value(out_address, output_value)]
5✔
2183
        locktime = get_locktime_for_new_transaction(self.network)
5✔
2184
        tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
5✔
2185
        tx_new.set_rbf(True)
5✔
2186
        tx_new.add_info_from_wallet(self)
5✔
2187
        return tx_new
5✔
2188

2189
    def dscancel(
5✔
2190
            self, *, tx: Transaction, new_fee_rate: Union[int, float, Decimal]
2191
    ) -> PartialTransaction:
2192
        """Double-Spend-Cancel: cancel an unconfirmed tx by double-spending
2193
        its inputs, paying ourselves.
2194
        'new_fee_rate' is the target min rate in sat/vbyte
2195

2196
        note: it is the caller's responsibility to have already called tx.add_info_from_network().
2197
              Without that, all txins must be ismine.
2198
        """
2199
        if not isinstance(tx, PartialTransaction):
5✔
2200
            tx = PartialTransaction.from_tx(tx)
5✔
2201
        assert isinstance(tx, PartialTransaction)
5✔
2202
        tx.remove_signatures()
5✔
2203

2204
        if tx.is_final():
5✔
2205
            raise CannotDoubleSpendTx(_('Transaction is final'))
×
2206
        new_fee_rate = quantize_feerate(new_fee_rate)  # strip excess precision
5✔
2207
        tx.add_info_from_wallet(self)
5✔
2208
        if tx.is_missing_info_from_network():
5✔
2209
            raise Exception("tx missing info from network")
×
2210
        old_tx_size = tx.estimated_size()
5✔
2211
        old_fee = tx.get_fee()
5✔
2212
        assert old_fee is not None
5✔
2213
        old_fee_rate = old_fee / old_tx_size  # sat/vbyte
5✔
2214
        if new_fee_rate <= old_fee_rate:
5✔
2215
            raise CannotDoubleSpendTx(_("The new fee rate needs to be higher than the old fee rate."))
×
2216
        # grab all ismine inputs
2217
        inputs = [txin for txin in tx.inputs()
5✔
2218
                  if self.is_mine(self.adb.get_txin_address(txin))]
2219
        value = sum([txin.value_sats() for txin in inputs])
5✔
2220
        # figure out output address
2221
        old_change_addrs = [o.address for o in tx.outputs() if self.is_mine(o.address)]
5✔
2222
        out_address = (self.get_single_change_address_for_new_transaction(old_change_addrs)
5✔
2223
                       or self.get_receiving_address())
2224
        locktime = get_locktime_for_new_transaction(self.network)
5✔
2225
        outputs = [PartialTxOutput.from_address_and_value(out_address, value)]
5✔
2226
        tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
5✔
2227
        new_tx_size = tx_new.estimated_size()
5✔
2228
        new_fee = max(
5✔
2229
            new_fee_rate * new_tx_size,
2230
            old_fee + self.relayfee() * new_tx_size / Decimal(1000),  # BIP-125 rules 3 and 4
2231
        )
2232
        new_fee = int(math.ceil(new_fee))
5✔
2233
        output_value = value - new_fee
5✔
2234
        if output_value < self.dust_threshold():
5✔
2235
            raise CannotDoubleSpendTx(_("The output value remaining after fee is too low."))
×
2236
        outputs = [PartialTxOutput.from_address_and_value(out_address, value - new_fee)]
5✔
2237
        tx_new = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
5✔
2238
        tx_new.set_rbf(True)
5✔
2239
        tx_new.add_info_from_wallet(self)
5✔
2240
        return tx_new
5✔
2241

2242
    def _add_txinout_derivation_info(self, txinout: Union[PartialTxInput, PartialTxOutput],
5✔
2243
                                     address: str, *, only_der_suffix: bool) -> None:
2244
        pass  # implemented by subclasses
5✔
2245

2246
    def _add_input_utxo_info(
5✔
2247
            self,
2248
            txin: PartialTxInput,
2249
            *,
2250
            address: str = None,
2251
    ) -> None:
2252
        # - We prefer to include UTXO (full tx), even for segwit inputs (see #6198).
2253
        # - For witness v0 inputs, we include *both* UTXO and WITNESS_UTXO. UTXO is a strict superset,
2254
        #   so this is redundant, but it is (implied to be) "expected" from bip-0174 (see #8039).
2255
        #   Regardless, this might improve compatibility with some other software.
2256
        # - For witness v1, witness_utxo will be enough though (bip-0341 sighash fixes known prior issues).
2257
        # - We cannot include UTXO if the prev tx is not signed yet (chain of unsigned txs).
2258
        address = address or txin.address
5✔
2259
        # add witness_utxo
2260
        if txin.witness_utxo is None and txin.is_segwit() and address:
5✔
2261
            received, spent = self.adb.get_addr_io(address)
5✔
2262
            item = received.get(txin.prevout.to_str())
5✔
2263
            if item:
5✔
2264
                txin_value = item[2]
5✔
2265
                txin.witness_utxo = TxOutput.from_address_and_value(address, txin_value)
5✔
2266
        # add utxo
2267
        if txin.utxo is None:
5✔
2268
            txin.utxo = self.db.get_transaction(txin.prevout.txid.hex())
5✔
2269
        # Maybe remove witness_utxo. witness_utxo should not be present for non-segwit inputs.
2270
        # If it is present, it might be because another electrum instance added it when sharing the psbt via QR code.
2271
        # If we have the full utxo available, we can remove it without loss of information.
2272
        if txin.witness_utxo and not txin.is_segwit() and txin.utxo:
5✔
2273
            txin.witness_utxo = None
5✔
2274

2275
    def _learn_derivation_path_for_address_from_txinout(self, txinout: Union[PartialTxInput, PartialTxOutput],
5✔
2276
                                                        address: str) -> bool:
2277
        """Tries to learn the derivation path for an address (potentially beyond gap limit)
2278
        using data available in given txin/txout.
2279
        Returns whether the address was found to be is_mine.
2280
        """
2281
        return False  # implemented by subclasses
5✔
2282

2283
    def add_input_info(
5✔
2284
            self,
2285
            txin: TxInput,
2286
            *,
2287
            only_der_suffix: bool = False,
2288
    ) -> None:
2289
        """Populates the txin, using info the wallet already has.
2290
        That is, network requests are *not* done to fetch missing prev txs!
2291
        For that, use txin.add_info_from_network.
2292
        """
2293
        # note: we add input utxos regardless of is_mine
2294
        if txin.utxo is None:
5✔
2295
            txin.utxo = self.db.get_transaction(txin.prevout.txid.hex())
5✔
2296
        if not isinstance(txin, PartialTxInput):
5✔
2297
            return
5✔
2298
        address = self.adb.get_txin_address(txin)
5✔
2299
        self._add_input_utxo_info(txin, address=address)
5✔
2300
        is_mine = self.is_mine(address)
5✔
2301
        if not is_mine:
5✔
2302
            is_mine = self._learn_derivation_path_for_address_from_txinout(txin, address)
5✔
2303
        if not is_mine:
5✔
2304
            if self.lnworker:
5✔
2305
                self.lnworker.swap_manager.add_txin_info(txin)
×
2306
            return
5✔
2307
        txin.script_descriptor = self.get_script_descriptor_for_address(address)
5✔
2308
        self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix)
5✔
2309
        txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height
5✔
2310

2311
    def get_script_descriptor_for_address(self, address: str) -> Optional[Descriptor]:
5✔
2312
        if not self.is_mine(address):
5✔
2313
            return None
×
2314
        script_type = self.get_txin_type(address)
5✔
2315
        if script_type in ('address', 'unknown'):
5✔
2316
            return None
5✔
2317
        addr_index = self.get_address_index(address)
5✔
2318
        if addr_index is None:
5✔
2319
            return None
×
2320
        pubkeys = [ks.get_pubkey_provider(addr_index) for ks in self.get_keystores()]
5✔
2321
        if not pubkeys:
5✔
2322
            return None
×
2323
        if script_type == 'p2pk':
5✔
2324
            return descriptor.PKDescriptor(pubkey=pubkeys[0])
×
2325
        elif script_type == 'p2pkh':
5✔
2326
            return descriptor.PKHDescriptor(pubkey=pubkeys[0])
5✔
2327
        elif script_type == 'p2wpkh':
5✔
2328
            return descriptor.WPKHDescriptor(pubkey=pubkeys[0])
5✔
2329
        elif script_type == 'p2wpkh-p2sh':
5✔
2330
            wpkh = descriptor.WPKHDescriptor(pubkey=pubkeys[0])
5✔
2331
            return descriptor.SHDescriptor(subdescriptor=wpkh)
5✔
2332
        elif script_type == 'p2sh':
5✔
2333
            multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)
5✔
2334
            return descriptor.SHDescriptor(subdescriptor=multi)
5✔
2335
        elif script_type == 'p2wsh':
5✔
2336
            multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)
5✔
2337
            return descriptor.WSHDescriptor(subdescriptor=multi)
5✔
2338
        elif script_type == 'p2wsh-p2sh':
5✔
2339
            multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)
5✔
2340
            wsh = descriptor.WSHDescriptor(subdescriptor=multi)
5✔
2341
            return descriptor.SHDescriptor(subdescriptor=wsh)
5✔
2342
        else:
2343
            raise NotImplementedError(f"unexpected {script_type=}")
×
2344

2345
    def can_sign(self, tx: Transaction) -> bool:
5✔
2346
        if not isinstance(tx, PartialTransaction):
5✔
2347
            return False
×
2348
        if tx.is_complete():
5✔
2349
            return False
×
2350
        # add info to inputs if we can; otherwise we might return a false negative:
2351
        tx.add_info_from_wallet(self)
5✔
2352
        for txin in tx.inputs():
5✔
2353
            # note: is_mine check needed to avoid false positives.
2354
            #       just because keystore could sign, txin does not necessarily belong to wallet.
2355
            #       Example: we have p2pkh-like addresses and txin is a multisig that involves our pubkey.
2356
            if not self.is_mine(txin.address):
5✔
2357
                continue
5✔
2358
            for k in self.get_keystores():
×
2359
                if k.can_sign_txin(txin):
×
2360
                    return True
×
2361
        if self.get_swap_by_claim_tx(tx):
5✔
2362
            return True
×
2363
        return False
5✔
2364

2365
    def add_output_info(self, txout: PartialTxOutput, *, only_der_suffix: bool = False) -> None:
5✔
2366
        address = txout.address
5✔
2367
        if not self.is_mine(address):
5✔
2368
            is_mine = self._learn_derivation_path_for_address_from_txinout(txout, address)
5✔
2369
            if not is_mine:
5✔
2370
                return
5✔
2371
        txout.script_descriptor = self.get_script_descriptor_for_address(address)
5✔
2372
        txout.is_mine = True
5✔
2373
        txout.is_change = self.is_change(address)
5✔
2374
        self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix)
5✔
2375

2376
    def sign_transaction(self, tx: Transaction, password) -> Optional[PartialTransaction]:
5✔
2377
        """ returns tx if successful else None """
2378
        if self.is_watching_only():
5✔
2379
            return
5✔
2380
        if not isinstance(tx, PartialTransaction):
5✔
2381
            return
×
2382
        # note: swap signing does not require the password
2383
        swap = self.get_swap_by_claim_tx(tx)
5✔
2384
        if swap:
5✔
2385
            self.lnworker.swap_manager.sign_tx(tx, swap)
×
2386
            return tx
×
2387
        # add info to a temporary tx copy; including xpubs
2388
        # and full derivation paths as hw keystores might want them
2389
        tmp_tx = copy.deepcopy(tx)
5✔
2390
        tmp_tx.add_info_from_wallet(self, include_xpubs=True)
5✔
2391
        # sign. start with ready keystores.
2392
        # note: ks.ready_to_sign() side-effect: we trigger pairings with potential hw devices.
2393
        #       We only do this once, before the loop, however we could rescan after each iteration,
2394
        #       to see if the user connected/disconnected devices in the meantime.
2395
        for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True):
5✔
2396
            try:
5✔
2397
                if k.can_sign(tmp_tx):
5✔
2398
                    k.sign_transaction(tmp_tx, password)
5✔
2399
            except UserCancelled:
×
2400
                continue
×
2401
        # remove sensitive info; then copy back details from temporary tx
2402
        tmp_tx.remove_xpubs_and_bip32_paths()
5✔
2403
        tx.combine_with_other_psbt(tmp_tx)
5✔
2404
        tx.add_info_from_wallet(self, include_xpubs=False)
5✔
2405
        return tx
5✔
2406

2407
    def try_detecting_internal_addresses_corruption(self) -> None:
5✔
2408
        pass
×
2409

2410
    def check_address_for_corruption(self, addr: str) -> None:
5✔
2411
        pass
×
2412

2413
    def get_unused_addresses(self) -> Sequence[str]:
5✔
2414
        domain = self.get_receiving_addresses()
5✔
2415
        return [addr for addr in domain if not self.adb.is_used(addr) and not self.get_request_by_addr(addr)]
5✔
2416

2417
    @check_returned_address_for_corruption
5✔
2418
    def get_unused_address(self) -> Optional[str]:
5✔
2419
        """Get an unused receiving address, if there is one.
2420
        Note: there might NOT be one available!
2421
        """
2422
        addrs = self.get_unused_addresses()
5✔
2423
        if addrs:
5✔
2424
            return addrs[0]
5✔
2425

2426
    @check_returned_address_for_corruption
5✔
2427
    def get_receiving_address(self) -> str:
5✔
2428
        """Get a receiving address. Guaranteed to always return an address."""
2429
        unused_addr = self.get_unused_address()
5✔
2430
        if unused_addr:
5✔
2431
            return unused_addr
5✔
2432
        domain = self.get_receiving_addresses()
×
2433
        if not domain:
×
2434
            raise Exception("no receiving addresses in wallet?!")
×
2435
        choice = domain[0]
×
2436
        for addr in domain:
×
2437
            if not self.adb.is_used(addr):
×
2438
                if self.get_request_by_addr(addr) is None:
×
2439
                    return addr
×
2440
                else:
2441
                    choice = addr
×
2442
        return choice
×
2443

2444
    def create_new_address(self, for_change: bool = False):
5✔
2445
        raise Exception("this wallet cannot generate new addresses")
×
2446

2447
    def import_address(self, address: str) -> str:
5✔
2448
        raise Exception("this wallet cannot import addresses")
×
2449

2450
    def import_addresses(self, addresses: List[str], *,
5✔
2451
                         write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:
2452
        raise Exception("this wallet cannot import addresses")
×
2453

2454
    def delete_address(self, address: str) -> None:
5✔
2455
        raise Exception("this wallet cannot delete addresses")
×
2456

2457
    def get_request_URI(self, req: Request) -> Optional[str]:
5✔
2458
        lightning_invoice = None
×
2459
        if self.config.WALLET_BIP21_LIGHTNING:
×
2460
            lightning_invoice = self.get_bolt11_invoice(req)
×
2461
        return req.get_bip21_URI(lightning_invoice=lightning_invoice)
×
2462

2463
    def check_expired_status(self, r: BaseInvoice, status):
5✔
2464
        #if r.is_lightning() and r.exp == 0:
2465
        #    status = PR_EXPIRED  # for BOLT-11 invoices, exp==0 means 0 seconds
2466
        if status == PR_UNPAID and r.has_expired():
5✔
2467
            status = PR_EXPIRED
5✔
2468
        return status
5✔
2469

2470
    def get_invoice_status(self, invoice: BaseInvoice):
5✔
2471
        """Returns status of (incoming) request or (outgoing) invoice."""
2472
        # lightning invoices can be paid onchain
2473
        if invoice.is_lightning() and self.lnworker:
5✔
2474
            status = self.lnworker.get_invoice_status(invoice)
5✔
2475
            if status != PR_UNPAID:
5✔
2476
                return self.check_expired_status(invoice, status)
5✔
2477
        paid, conf = self.is_onchain_invoice_paid(invoice)
5✔
2478
        if not paid:
5✔
2479
            if isinstance(invoice, Invoice):
5✔
2480
                if status:=invoice.get_broadcasting_status():
×
2481
                    return status
×
2482
            status = PR_UNPAID
5✔
2483
        elif conf == 0:
5✔
2484
            status = PR_UNCONFIRMED
5✔
2485
        else:
2486
            assert conf >= 1, conf
5✔
2487
            status = PR_PAID
5✔
2488
        return self.check_expired_status(invoice, status)
5✔
2489

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

2494
        Called in get_label_for_address and update_invoices_and_reqs_touched_by_tx
2495
        Returns None if the address can be reused (i.e. was paid by lightning or has expired)
2496
        """
2497
        keys = self._requests_addr_to_key.get(addr) or []
5✔
2498
        reqs = [self._receive_requests.get(key) for key in keys]
5✔
2499
        reqs = [req for req in reqs if req]  # filter None
5✔
2500
        if not reqs:
5✔
2501
            return
5✔
2502
        # filter out expired
2503
        reqs = [req for req in reqs if self.get_invoice_status(req) != PR_EXPIRED]
5✔
2504
        # filter out paid-with-lightning
2505
        if self.lnworker:
5✔
2506
            reqs = [req for req in reqs
5✔
2507
                    if not req.is_lightning() or self.lnworker.get_invoice_status(req) == PR_UNPAID]
2508
        if not reqs:
5✔
2509
            return None
5✔
2510
        # note: There typically should not be more than one relevant request for an address.
2511
        #       If there's multiple, return the one created last (see #8113). Consider:
2512
        #       - there is an old expired req1, and a newer unpaid req2, reusing the same addr (and same amount),
2513
        #       - now req2 gets paid. however, get_invoice_status will say both req1 and req2 are PAID. (see #8061)
2514
        #       - as a workaround, we return the request with the larger creation time.
2515
        reqs.sort(key=lambda req: req.get_time())
5✔
2516
        return reqs[-1]
5✔
2517

2518
    def get_request(self, request_id: str) -> Optional[Request]:
5✔
2519
        return self._receive_requests.get(request_id)
5✔
2520

2521
    def get_formatted_request(self, request_id):
5✔
2522
        x = self.get_request(request_id)
×
2523
        if x:
×
2524
            return self.export_request(x)
×
2525

2526
    def export_request(self, x: Request) -> Dict[str, Any]:
5✔
2527
        key = x.get_id()
×
2528
        status = self.get_invoice_status(x)
×
2529
        d = x.as_dict(status)
×
2530
        d['request_id'] = d.pop('id')
×
2531
        if x.is_lightning():
×
2532
            d['rhash'] = x.rhash
×
2533
            d['lightning_invoice'] = self.get_bolt11_invoice(x)
×
2534
            if self.lnworker and status == PR_UNPAID:
×
2535
                d['can_receive'] = self.lnworker.can_receive_invoice(x)
×
2536
        if address := x.get_address():
×
2537
            d['address'] = address
×
2538
            d['URI'] = self.get_request_URI(x)
×
2539
            # if request was paid onchain, add relevant fields
2540
            # note: addr is reused when getting paid on LN! so we check for that.
2541
            _, conf, tx_hashes = self._is_onchain_invoice_paid(x)
×
2542
            if not x.is_lightning() or not self.lnworker or self.lnworker.get_invoice_status(x) != PR_PAID:
×
2543
                if conf is not None:
×
2544
                    d['confirmations'] = conf
×
2545
                d['tx_hashes'] = tx_hashes
×
2546
        run_hook('wallet_export_request', d, key)
×
2547
        return d
×
2548

2549
    def export_invoice(self, x: Invoice) -> Dict[str, Any]:
5✔
2550
        key = x.get_id()
×
2551
        status = self.get_invoice_status(x)
×
2552
        d = x.as_dict(status)
×
2553
        d['invoice_id'] = d.pop('id')
×
2554
        if x.is_lightning():
×
2555
            d['lightning_invoice'] = x.lightning_invoice
×
2556
            if self.lnworker and status == PR_UNPAID:
×
2557
                d['can_pay'] = self.lnworker.can_pay_invoice(x)
×
2558
        else:
2559
            amount_sat = x.get_amount_sat()
×
2560
            assert isinstance(amount_sat, (int, str, type(None)))
×
2561
            d['outputs'] = [y.to_legacy_tuple() for y in x.get_outputs()]
×
2562
            if x.bip70:
×
2563
                d['bip70'] = x.bip70
×
2564
        return d
×
2565

2566
    def get_invoices_and_requests_touched_by_tx(self, tx):
5✔
2567
        request_keys = set()
5✔
2568
        invoice_keys = set()
5✔
2569
        with self.lock, self.transaction_lock:
5✔
2570
            for txo in tx.outputs():
5✔
2571
                addr = txo.address
5✔
2572
                if request:=self.get_request_by_addr(addr):
5✔
2573
                    request_keys.add(request.get_id())
5✔
2574
                for invoice_key in self._invoices_from_scriptpubkey_map.get(txo.scriptpubkey, set()):
5✔
2575
                    invoice_keys.add(invoice_key)
×
2576
        return request_keys, invoice_keys
5✔
2577

2578
    def _update_invoices_and_reqs_touched_by_tx(self, tx_hash: str) -> None:
5✔
2579
        # FIXME in some cases if tx2 replaces unconfirmed tx1 in the mempool, we are not called.
2580
        #       For a given receive request, if tx1 touches it but tx2 does not, then
2581
        #       we were called when tx1 was added, but we will not get called when tx2 replaces tx1.
2582
        tx = self.db.get_transaction(tx_hash)
5✔
2583
        if tx is None:
5✔
2584
            return
×
2585
        request_keys, invoice_keys = self.get_invoices_and_requests_touched_by_tx(tx)
5✔
2586
        for key in request_keys:
5✔
2587
            request = self.get_request(key)
5✔
2588
            if not request:
5✔
2589
                continue
×
2590
            status = self.get_invoice_status(request)
5✔
2591
            util.trigger_callback('request_status', self, request.get_id(), status)
5✔
2592
        self._update_onchain_invoice_paid_detection(invoice_keys)
5✔
2593

2594
    def set_broadcasting(self, tx: Transaction, *, broadcasting_status: Optional[int]):
5✔
2595
        request_keys, invoice_keys = self.get_invoices_and_requests_touched_by_tx(tx)
×
2596
        for key in invoice_keys:
×
2597
            invoice = self._invoices.get(key)
×
2598
            if not invoice:
×
2599
                continue
×
2600
            invoice._broadcasting_status = broadcasting_status
×
2601
            status = self.get_invoice_status(invoice)
×
2602
            util.trigger_callback('invoice_status', self, key, status)
×
2603

2604
    def get_bolt11_invoice(self, req: Request) -> str:
5✔
2605
        if not self.lnworker:
×
2606
            return ''
×
2607
        if (payment_hash := req.payment_hash) is None:  # e.g. req might have been generated before enabling LN
×
2608
            return ''
×
2609
        amount_msat = req.get_amount_msat() or None
×
2610
        assert (amount_msat is None or amount_msat > 0), amount_msat
×
2611
        lnaddr, invoice = self.lnworker.get_bolt11_invoice(
×
2612
            payment_hash=payment_hash,
2613
            amount_msat=amount_msat,
2614
            message=req.message,
2615
            expiry=req.exp,
2616
            fallback_address=req.get_address() if self.config.WALLET_BOLT11_FALLBACK else None)
2617
        return invoice
×
2618

2619
    def create_request(self, amount_sat: int, message: str, exp_delay: int, address: Optional[str]):
5✔
2620
        # for receiving
2621
        amount_sat = amount_sat or 0
5✔
2622
        assert isinstance(amount_sat, int), f"{amount_sat!r}"
5✔
2623
        message = message or ''
5✔
2624
        address = address or None  # converts "" to None
5✔
2625
        exp_delay = exp_delay or 0
5✔
2626
        timestamp = int(Request._get_cur_time())
5✔
2627
        payment_hash = None  # type: Optional[bytes]
5✔
2628
        if self.has_lightning():
5✔
2629
            payment_hash = self.lnworker.create_payment_info(amount_msat=amount_sat * 1000, write_to_disk=False)
5✔
2630
        outputs = [ PartialTxOutput.from_address_and_value(address, amount_sat)] if address else []
5✔
2631
        height = self.adb.get_local_height()
5✔
2632
        req = Request(
5✔
2633
            outputs=outputs,
2634
            message=message,
2635
            time=timestamp,
2636
            amount_msat=amount_sat*1000,
2637
            exp=exp_delay,
2638
            height=height,
2639
            bip70=None,
2640
            payment_hash=payment_hash,
2641
        )
2642
        key = self.add_payment_request(req)
5✔
2643
        return key
5✔
2644

2645
    def add_payment_request(self, req: Request, *, write_to_disk: bool = True):
5✔
2646
        request_id = req.get_id()
5✔
2647
        self._receive_requests[request_id] = req
5✔
2648
        if addr:=req.get_address():
5✔
2649
            self._requests_addr_to_key[addr].add(request_id)
5✔
2650
        if write_to_disk:
5✔
2651
            self.save_db()
5✔
2652
        return request_id
5✔
2653

2654
    def delete_request(self, request_id, *, write_to_disk: bool = True):
5✔
2655
        """ lightning or on-chain """
2656
        req = self.get_request(request_id)
×
2657
        if req is None:
×
2658
            return
×
2659
        self._receive_requests.pop(request_id, None)
×
2660
        if addr:=req.get_address():
×
2661
            self._requests_addr_to_key[addr].discard(request_id)
×
2662
        if req.is_lightning() and self.lnworker:
×
2663
            self.lnworker.delete_payment_info(req.rhash)
×
2664
        if write_to_disk:
×
2665
            self.save_db()
×
2666

2667
    def delete_invoice(self, invoice_id, *, write_to_disk: bool = True):
5✔
2668
        """ lightning or on-chain """
2669
        inv = self._invoices.pop(invoice_id, None)
×
2670
        if inv is None:
×
2671
            return
×
2672
        if inv.is_lightning() and self.lnworker:
×
2673
            self.lnworker.delete_payment_info(inv.rhash)
×
2674
        if write_to_disk:
×
2675
            self.save_db()
×
2676

2677
    def get_sorted_requests(self) -> List[Request]:
5✔
2678
        """ sorted by timestamp """
2679
        out = [self.get_request(x) for x in self._receive_requests.keys()]
×
2680
        out = [x for x in out if x is not None]
×
2681
        out.sort(key=lambda x: x.time)
×
2682
        return out
×
2683

2684
    def get_unpaid_requests(self) -> List[Request]:
5✔
2685
        out = [x for x in self._receive_requests.values() if self.get_invoice_status(x) != PR_PAID]
×
2686
        out.sort(key=lambda x: x.time)
×
2687
        return out
×
2688

2689
    def delete_expired_requests(self):
5✔
2690
        keys = [k for k, v in self._receive_requests.items() if self.get_invoice_status(v) == PR_EXPIRED]
×
2691
        self.delete_requests(keys)
×
2692
        return keys
×
2693

2694
    def delete_requests(self, keys):
5✔
2695
        for key in keys:
×
2696
            self.delete_request(key, write_to_disk=False)
×
2697
        if keys:
×
2698
            self.save_db()
×
2699

2700
    @abstractmethod
5✔
2701
    def get_fingerprint(self) -> str:
5✔
2702
        """Returns a string that can be used to identify this wallet.
2703
        Used e.g. by Labels plugin, and LN channel backups.
2704
        Returns empty string "" for wallets that don't have an ID.
2705
        """
2706
        pass
×
2707

2708
    def can_import_privkey(self):
5✔
2709
        return False
×
2710

2711
    def can_import_address(self):
5✔
2712
        return False
×
2713

2714
    def can_delete_address(self):
5✔
2715
        return False
×
2716

2717
    def has_password(self) -> bool:
5✔
2718
        return self.has_keystore_encryption() or self.has_storage_encryption()
5✔
2719

2720
    def can_have_keystore_encryption(self):
5✔
2721
        return self.keystore and self.keystore.may_have_password()
5✔
2722

2723
    def get_available_storage_encryption_version(self) -> StorageEncryptionVersion:
5✔
2724
        """Returns the type of storage encryption offered to the user.
2725

2726
        A wallet file (storage) is either encrypted with this version
2727
        or is stored in plaintext.
2728
        """
2729
        if isinstance(self.keystore, Hardware_KeyStore):
5✔
2730
            return StorageEncryptionVersion.XPUB_PASSWORD
×
2731
        else:
2732
            return StorageEncryptionVersion.USER_PASSWORD
5✔
2733

2734
    def has_keystore_encryption(self) -> bool:
5✔
2735
        """Returns whether encryption is enabled for the keystore.
2736

2737
        If True, e.g. signing a transaction will require a password.
2738
        """
2739
        if self.can_have_keystore_encryption():
5✔
2740
            return bool(self.db.get('use_encryption', False))
5✔
2741
        return False
5✔
2742

2743
    def has_storage_encryption(self) -> bool:
5✔
2744
        """Returns whether encryption is enabled for the wallet file on disk."""
2745
        return bool(self.storage) and self.storage.is_encrypted()
5✔
2746

2747
    @classmethod
5✔
2748
    def may_have_password(cls):
5✔
2749
        return True
×
2750

2751
    def check_password(self, password: Optional[str]) -> None:
5✔
2752
        """Raises an InvalidPassword exception on invalid password"""
2753
        if not self.has_password():
5✔
2754
            if password is not None:
5✔
2755
                raise InvalidPassword("password given but wallet has no password")
5✔
2756
            return
5✔
2757
        if self.has_keystore_encryption():
5✔
2758
            self.keystore.check_password(password)
5✔
2759
        if self.has_storage_encryption():
5✔
2760
            self.storage.check_password(password)
5✔
2761

2762
    def update_password(self, old_pw, new_pw, *, encrypt_storage: bool = True):
5✔
2763
        if old_pw is None and self.has_password():
5✔
2764
            raise InvalidPassword()
×
2765
        self.check_password(old_pw)
5✔
2766
        if self.storage:
5✔
2767
            if encrypt_storage:
5✔
2768
                enc_version = self.get_available_storage_encryption_version()
5✔
2769
            else:
2770
                enc_version = StorageEncryptionVersion.PLAINTEXT
5✔
2771
            self.storage.set_password(new_pw, enc_version)
5✔
2772
        # make sure next storage.write() saves changes
2773
        self.db.set_modified(True)
5✔
2774

2775
        # note: Encrypting storage with a hw device is currently only
2776
        #       allowed for non-multisig wallets. Further,
2777
        #       Hardware_KeyStore.may_have_password() == False.
2778
        #       If these were not the case,
2779
        #       extra care would need to be taken when encrypting keystores.
2780
        self._update_password_for_keystore(old_pw, new_pw)
5✔
2781
        encrypt_keystore = self.can_have_keystore_encryption()
5✔
2782
        self.db.set_keystore_encryption(bool(new_pw) and encrypt_keystore)
5✔
2783
        ## save changes
2784
        if self.storage and self.storage.file_exists():
5✔
2785
            self.db._write()
5✔
2786

2787
    @abstractmethod
5✔
2788
    def _update_password_for_keystore(self, old_pw: Optional[str], new_pw: Optional[str]) -> None:
5✔
2789
        pass
×
2790

2791
    def sign_message(self, address: str, message: str, password) -> bytes:
5✔
2792
        index = self.get_address_index(address)
×
2793
        script_type = self.get_txin_type(address)
×
2794
        assert script_type != "address"
×
2795
        return self.keystore.sign_message(index, message, password, script_type=script_type)
×
2796

2797
    def decrypt_message(self, pubkey: str, message, password) -> bytes:
5✔
2798
        addr = self.pubkeys_to_address([pubkey])
×
2799
        index = self.get_address_index(addr)
×
2800
        return self.keystore.decrypt_message(index, message, password)
×
2801

2802
    @abstractmethod
5✔
2803
    def pubkeys_to_address(self, pubkeys: Sequence[str]) -> Optional[str]:
5✔
2804
        pass
×
2805

2806
    def price_at_timestamp(self, txid, price_func):
5✔
2807
        """Returns fiat price of bitcoin at the time tx got confirmed."""
2808
        timestamp = self.adb.get_tx_height(txid).timestamp
5✔
2809
        return price_func(timestamp if timestamp else time.time())
5✔
2810

2811
    def average_price(self, txid, price_func, ccy) -> Decimal:
5✔
2812
        """ Average acquisition price of the inputs of a transaction """
2813
        input_value = 0
×
2814
        total_price = 0
×
2815
        txi_addresses = self.db.get_txi_addresses(txid)
×
2816
        if not txi_addresses:
×
2817
            return Decimal('NaN')
×
2818
        for addr in txi_addresses:
×
2819
            d = self.db.get_txi_addr(txid, addr)
×
2820
            for ser, v in d:
×
2821
                input_value += v
×
2822
                total_price += self.coin_price(ser.split(':')[0], price_func, ccy, v)
×
2823
        return total_price / (input_value/Decimal(COIN))
×
2824

2825
    def clear_coin_price_cache(self):
5✔
2826
        self._coin_price_cache = {}
×
2827

2828
    def coin_price(self, txid, price_func, ccy, txin_value) -> Decimal:
5✔
2829
        """
2830
        Acquisition price of a coin.
2831
        This assumes that either all inputs are mine, or no input is mine.
2832
        """
2833
        if txin_value is None:
×
2834
            return Decimal('NaN')
×
2835
        cache_key = "{}:{}:{}".format(str(txid), str(ccy), str(txin_value))
×
2836
        result = self._coin_price_cache.get(cache_key, None)
×
2837
        if result is not None:
×
2838
            return result
×
2839
        if self.db.get_txi_addresses(txid):
×
2840
            result = self.average_price(txid, price_func, ccy) * txin_value/Decimal(COIN)
×
2841
            self._coin_price_cache[cache_key] = result
×
2842
            return result
×
2843
        else:
2844
            fiat_value = self.get_fiat_value(txid, ccy)
×
2845
            if fiat_value is not None:
×
2846
                return fiat_value
×
2847
            else:
2848
                p = self.price_at_timestamp(txid, price_func)
×
2849
                return p * txin_value/Decimal(COIN)
×
2850

2851
    def is_billing_address(self, addr):
5✔
2852
        # overridden for TrustedCoin wallets
2853
        return False
5✔
2854

2855
    @abstractmethod
5✔
2856
    def is_watching_only(self) -> bool:
5✔
2857
        pass
×
2858

2859
    def get_keystore(self) -> Optional[KeyStore]:
5✔
2860
        return self.keystore
×
2861

2862
    def get_keystores(self) -> Sequence[KeyStore]:
5✔
2863
        return [self.keystore] if self.keystore else []
5✔
2864

2865
    @abstractmethod
5✔
2866
    def save_keystore(self):
5✔
2867
        pass
×
2868

2869
    @abstractmethod
5✔
2870
    def has_seed(self) -> bool:
5✔
2871
        pass
×
2872

2873
    @abstractmethod
5✔
2874
    def get_all_known_addresses_beyond_gap_limit(self) -> Set[str]:
5✔
2875
        pass
×
2876

2877
    def create_transaction(self, outputs, *, fee=None, feerate=None, change_addr=None, domain_addr=None, domain_coins=None,
5✔
2878
                           unsigned=False, rbf=True, password=None, locktime=None):
2879
        if fee is not None and feerate is not None:
5✔
2880
            raise Exception("Cannot specify both 'fee' and 'feerate' at the same time!")
×
2881
        coins = self.get_spendable_coins(domain_addr)
5✔
2882
        if domain_coins is not None:
5✔
2883
            coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)]
×
2884
        if feerate is not None:
5✔
2885
            fee_per_kb = 1000 * Decimal(feerate)
5✔
2886
            fee_estimator = partial(SimpleConfig.estimate_fee_for_feerate, fee_per_kb)
5✔
2887
        else:
2888
            fee_estimator = fee
5✔
2889
        tx = self.make_unsigned_transaction(
5✔
2890
            coins=coins,
2891
            outputs=outputs,
2892
            fee=fee_estimator,
2893
            change_addr=change_addr)
2894
        if locktime is not None:
5✔
2895
            tx.locktime = locktime
5✔
2896
        tx.set_rbf(rbf)
5✔
2897
        if not unsigned:
5✔
2898
            self.sign_transaction(tx, password)
5✔
2899
        return tx
5✔
2900

2901
    def get_warning_for_risk_of_burning_coins_as_fees(self, tx: 'PartialTransaction') -> Optional[str]:
5✔
2902
        """Returns a warning message if there is risk of burning coins as fees if we sign.
2903
        Note that if not all inputs are ismine, e.g. coinjoin, the risk is not just about fees.
2904

2905
        Note:
2906
            - legacy sighash does not commit to any input amounts
2907
            - BIP-0143 sighash only commits to the *corresponding* input amount
2908
            - BIP-taproot sighash commits to *all* input amounts
2909
        """
2910
        assert isinstance(tx, PartialTransaction)
×
2911
        # if we have all full previous txs, we *know* all the input amounts -> fine
2912
        if all([txin.utxo for txin in tx.inputs()]):
×
2913
            return None
×
2914
        # a single segwit input -> fine
2915
        if len(tx.inputs()) == 1 and tx.inputs()[0].is_segwit() and tx.inputs()[0].witness_utxo:
×
2916
            return None
×
2917
        # coinjoin or similar
2918
        if any([not self.is_mine(txin.address) for txin in tx.inputs()]):
×
2919
            return (_("Warning") + ": "
×
2920
                    + _("The input amounts could not be verified as the previous transactions are missing.\n"
2921
                        "The amount of money being spent CANNOT be verified."))
2922
        # some inputs are legacy
2923
        if any([not txin.is_segwit() for txin in tx.inputs()]):
×
2924
            return (_("Warning") + ": "
×
2925
                    + _("The fee could not be verified. Signing non-segwit inputs is risky:\n"
2926
                        "if this transaction was maliciously modified before you sign,\n"
2927
                        "you might end up paying a higher mining fee than displayed."))
2928
        # all inputs are segwit
2929
        # https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-August/014843.html
2930
        return (_("Warning") + ": "
×
2931
                + _("If you received this transaction from an untrusted device, "
2932
                    "do not accept to sign it more than once,\n"
2933
                    "otherwise you could end up paying a different fee."))
2934

2935
    def get_tx_fee_warning(
5✔
2936
            self, *,
2937
            invoice_amt: int,
2938
            tx_size: int,
2939
            fee: int) -> Optional[Tuple[bool, str, str]]:
2940

2941
        feerate = Decimal(fee) / tx_size  # sat/byte
×
2942
        fee_ratio = Decimal(fee) / invoice_amt if invoice_amt else 0
×
2943
        long_warning = None
×
2944
        short_warning = None
×
2945
        allow_send = True
×
2946
        if feerate < self.relayfee() / 1000:
×
2947
            long_warning = (
×
2948
                    _("This transaction requires a higher fee, or it will not be propagated by your current server.") + " "
2949
                    + _("Try to raise your transaction fee, or use a server with a lower relay fee."))
2950
            short_warning = _("below relay fee") + "!"
×
2951
            allow_send = False
×
2952
        elif fee_ratio >= FEE_RATIO_HIGH_WARNING:
×
2953
            long_warning = (
×
2954
                    _("The fee for this transaction seems unusually high.")
2955
                    + f' ({fee_ratio*100:.2f}% of amount)')
2956
            short_warning = _("high fee ratio") + "!"
×
2957
        elif feerate > FEERATE_WARNING_HIGH_FEE / 1000:
×
2958
            long_warning = (
×
2959
                    _("The fee for this transaction seems unusually high.")
2960
                    + f' (feerate: {feerate:.2f} sat/byte)')
2961
            short_warning = _("high fee rate") + "!"
×
2962
        if long_warning is None:
×
2963
            return None
×
2964
        else:
2965
            return allow_send, long_warning, short_warning
×
2966

2967
    def get_help_texts_for_receive_request(self, req: Request) -> ReceiveRequestHelp:
5✔
2968
        key = req.get_id()
×
2969
        addr = req.get_address() or ''
×
2970
        amount_sat = req.get_amount_sat() or 0
×
2971
        address_help = ''
×
2972
        URI_help = ''
×
2973
        ln_help = ''
×
2974
        address_is_error = False
×
2975
        URI_is_error = False
×
2976
        ln_is_error = False
×
2977
        ln_swap_suggestion = None
×
2978
        ln_rebalance_suggestion = None
×
2979
        URI = self.get_request_URI(req) or ''
×
2980
        lightning_online = self.lnworker and self.lnworker.num_peers() > 0
×
2981
        can_receive_lightning = self.lnworker and amount_sat <= self.lnworker.num_sats_can_receive()
×
2982
        status = self.get_invoice_status(req)
×
2983

2984
        if status == PR_EXPIRED:
×
2985
            address_help = URI_help = ln_help = _('This request has expired')
×
2986

2987
        is_amt_too_small_for_onchain = amount_sat and amount_sat < self.dust_threshold()
×
2988
        if not addr:
×
2989
            address_is_error = True
×
2990
            address_help = _('This request cannot be paid on-chain')
×
2991
            if is_amt_too_small_for_onchain:
×
2992
                address_help = _('Amount too small to be received onchain')
×
2993
        if not URI:
×
2994
            URI_is_error = True
×
2995
            URI_help = _('This request cannot be paid on-chain')
×
2996
            if is_amt_too_small_for_onchain:
×
2997
                URI_help = _('Amount too small to be received onchain')
×
2998
        if not req.is_lightning():
×
2999
            ln_is_error = True
×
3000
            ln_help = _('This request does not have a Lightning invoice.')
×
3001

3002
        if status == PR_UNPAID:
×
3003
            if self.adb.is_used(addr):
×
3004
                address_help = URI_help = (_("This address has already been used. "
×
3005
                                             "For better privacy, do not reuse it for new payments."))
3006
            if req.is_lightning():
×
3007
                if not lightning_online:
×
3008
                    ln_is_error = True
×
3009
                    ln_help = _('You must be online to receive Lightning payments.')
×
3010
                elif not can_receive_lightning:
×
3011
                    ln_is_error = True
×
3012
                    ln_rebalance_suggestion = self.lnworker.suggest_rebalance_to_receive(amount_sat)
×
3013
                    ln_swap_suggestion = self.lnworker.suggest_swap_to_receive(amount_sat)
×
3014
                    ln_help = _('You do not have the capacity to receive this amount with Lightning.')
×
3015
                    if bool(ln_rebalance_suggestion):
×
3016
                        ln_help += '\n\n' + _('You may have that capacity if you rebalance your channels.')
×
3017
                    elif bool(ln_swap_suggestion):
×
3018
                        ln_help += '\n\n' + _('You may have that capacity if you swap some of your funds.')
×
3019
        return ReceiveRequestHelp(
×
3020
            address_help=address_help,
3021
            URI_help=URI_help,
3022
            ln_help=ln_help,
3023
            address_is_error=address_is_error,
3024
            URI_is_error=URI_is_error,
3025
            ln_is_error=ln_is_error,
3026
            ln_rebalance_suggestion=ln_rebalance_suggestion,
3027
            ln_swap_suggestion=ln_swap_suggestion,
3028
        )
3029

3030

3031
    def synchronize(self) -> int:
5✔
3032
        """Returns the number of new addresses we generated."""
3033
        return 0
5✔
3034

3035
class Simple_Wallet(Abstract_Wallet):
5✔
3036
    # wallet with a single keystore
3037

3038
    def is_watching_only(self):
5✔
3039
        return self.keystore.is_watching_only()
5✔
3040

3041
    def _update_password_for_keystore(self, old_pw, new_pw):
5✔
3042
        if self.keystore and self.keystore.may_have_password():
5✔
3043
            self.keystore.update_password(old_pw, new_pw)
5✔
3044
            self.save_keystore()
5✔
3045

3046
    def save_keystore(self):
5✔
3047
        self.db.put('keystore', self.keystore.dump())
5✔
3048

3049
    @abstractmethod
5✔
3050
    def get_public_key(self, address: str) -> Optional[str]:
5✔
3051
        pass
×
3052

3053
    def get_public_keys(self, address: str) -> Sequence[str]:
5✔
3054
        return [self.get_public_key(address)]
×
3055

3056

3057
class Imported_Wallet(Simple_Wallet):
5✔
3058
    # wallet made of imported addresses
3059

3060
    wallet_type = 'imported'
5✔
3061
    txin_type = 'address'
5✔
3062

3063
    def __init__(self, db, *, config):
5✔
3064
        Abstract_Wallet.__init__(self, db, config=config)
5✔
3065
        self.use_change = db.get('use_change', False)
5✔
3066

3067
    def is_watching_only(self):
5✔
3068
        return self.keystore is None
5✔
3069

3070
    def can_import_privkey(self):
5✔
3071
        return bool(self.keystore)
×
3072

3073
    def load_keystore(self):
5✔
3074
        self.keystore = load_keystore(self.db, 'keystore') if self.db.get('keystore') else None
5✔
3075

3076
    def save_keystore(self):
5✔
3077
        self.db.put('keystore', self.keystore.dump())
5✔
3078

3079
    def can_import_address(self):
5✔
3080
        return self.is_watching_only()
×
3081

3082
    def can_delete_address(self):
5✔
3083
        return True
×
3084

3085
    def has_seed(self):
5✔
3086
        return False
5✔
3087

3088
    def is_deterministic(self):
5✔
3089
        return False
×
3090

3091
    def is_change(self, address):
5✔
3092
        return False
5✔
3093

3094
    def get_all_known_addresses_beyond_gap_limit(self) -> Set[str]:
5✔
3095
        return set()
×
3096

3097
    def get_fingerprint(self):
5✔
3098
        return ''
×
3099

3100
    def get_addresses(self):
5✔
3101
        # note: overridden so that the history can be cleared
3102
        return self.db.get_imported_addresses()
5✔
3103

3104
    def get_receiving_addresses(self, **kwargs):
5✔
3105
        return self.get_addresses()
5✔
3106

3107
    def get_change_addresses(self, **kwargs):
5✔
3108
        return self.get_addresses()
5✔
3109

3110
    def import_addresses(self, addresses: List[str], *,
5✔
3111
                         write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:
3112
        good_addr = []  # type: List[str]
5✔
3113
        bad_addr = []  # type: List[Tuple[str, str]]
5✔
3114
        for address in addresses:
5✔
3115
            if not bitcoin.is_address(address):
5✔
3116
                bad_addr.append((address, _('invalid address')))
×
3117
                continue
×
3118
            if self.db.has_imported_address(address):
5✔
3119
                bad_addr.append((address, _('address already in wallet')))
×
3120
                continue
×
3121
            good_addr.append(address)
5✔
3122
            self.db.add_imported_address(address, {})
5✔
3123
            self.adb.add_address(address)
5✔
3124
        if write_to_disk:
5✔
3125
            self.save_db()
5✔
3126
        return good_addr, bad_addr
5✔
3127

3128
    def import_address(self, address: str) -> str:
5✔
3129
        good_addr, bad_addr = self.import_addresses([address])
5✔
3130
        if good_addr and good_addr[0] == address:
5✔
3131
            return address
5✔
3132
        else:
3133
            raise BitcoinException(str(bad_addr[0][1]))
×
3134

3135
    def delete_address(self, address: str) -> None:
5✔
3136
        if not self.db.has_imported_address(address):
5✔
3137
            return
×
3138
        if len(self.get_addresses()) <= 1:
5✔
3139
            raise UserFacingException("cannot delete last remaining address from wallet")
5✔
3140
        transactions_to_remove = set()  # only referred to by this address
5✔
3141
        transactions_new = set()  # txs that are not only referred to by address
5✔
3142
        with self.lock:
5✔
3143
            for addr in self.db.get_history():
5✔
3144
                details = self.adb.get_address_history(addr).items()
5✔
3145
                if addr == address:
5✔
3146
                    for tx_hash, height in details:
5✔
3147
                        transactions_to_remove.add(tx_hash)
5✔
3148
                else:
3149
                    for tx_hash, height in details:
5✔
3150
                        transactions_new.add(tx_hash)
5✔
3151
            transactions_to_remove -= transactions_new
5✔
3152
            self.db.remove_addr_history(address)
5✔
3153
            for tx_hash in transactions_to_remove:
5✔
3154
                self.adb._remove_transaction(tx_hash)
5✔
3155
        self.set_label(address, None)
5✔
3156
        if req:= self.get_request_by_addr(address):
5✔
3157
            self.delete_request(req.get_id())
×
3158
        self.set_frozen_state_of_addresses([address], False, write_to_disk=False)
5✔
3159
        pubkey = self.get_public_key(address)
5✔
3160
        self.db.remove_imported_address(address)
5✔
3161
        if pubkey:
5✔
3162
            # delete key iff no other address uses it (e.g. p2pkh and p2wpkh for same key)
3163
            for txin_type in bitcoin.WIF_SCRIPT_TYPES.keys():
5✔
3164
                try:
5✔
3165
                    addr2 = bitcoin.pubkey_to_address(txin_type, pubkey)
5✔
3166
                except NotImplementedError:
5✔
3167
                    pass
5✔
3168
                else:
3169
                    if self.db.has_imported_address(addr2):
5✔
3170
                        break
×
3171
            else:
3172
                self.keystore.delete_imported_key(pubkey)
5✔
3173
                self.save_keystore()
5✔
3174
        self.save_db()
5✔
3175

3176
    def get_change_addresses_for_new_transaction(self, *args, **kwargs) -> List[str]:
5✔
3177
        # for an imported wallet, if all "change addresses" are already used,
3178
        # it is probably better to send change back to the "from address", than to
3179
        # send it to another random used address and link them together, hence
3180
        # we force "allow_reusing_used_change_addrs=False"
3181
        return super().get_change_addresses_for_new_transaction(
5✔
3182
            *args,
3183
            **{**kwargs, "allow_reusing_used_change_addrs": False},
3184
        )
3185

3186
    def calc_unused_change_addresses(self) -> Sequence[str]:
5✔
3187
        with self.lock:
5✔
3188
            unused_addrs = [addr for addr in self.get_change_addresses()
5✔
3189
                            if not self.adb.is_used(addr) and not self.is_address_reserved(addr)]
3190
            return unused_addrs
5✔
3191

3192
    def is_mine(self, address) -> bool:
5✔
3193
        if not address: return False
5✔
3194
        return self.db.has_imported_address(address)
5✔
3195

3196
    def get_address_index(self, address) -> Optional[str]:
5✔
3197
        # returns None if address is not mine
3198
        return self.get_public_key(address)
5✔
3199

3200
    def get_address_path_str(self, address):
5✔
3201
        return None
×
3202

3203
    def get_public_key(self, address) -> Optional[str]:
5✔
3204
        x = self.db.get_imported_address(address)
5✔
3205
        return x.get('pubkey') if x else None
5✔
3206

3207
    def import_private_keys(self, keys: List[str], password: Optional[str], *,
5✔
3208
                            write_to_disk=True) -> Tuple[List[str], List[Tuple[str, str]]]:
3209
        good_addr = []  # type: List[str]
5✔
3210
        bad_keys = []  # type: List[Tuple[str, str]]
5✔
3211
        for key in keys:
5✔
3212
            try:
5✔
3213
                txin_type, pubkey = self.keystore.import_privkey(key, password)
5✔
3214
            except Exception as e:
×
3215
                bad_keys.append((key, _('invalid private key') + f': {e}'))
×
3216
                continue
×
3217
            if txin_type not in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
5✔
3218
                bad_keys.append((key, _('not implemented type') + f': {txin_type}'))
×
3219
                continue
×
3220
            addr = bitcoin.pubkey_to_address(txin_type, pubkey)
5✔
3221
            good_addr.append(addr)
5✔
3222
            self.db.add_imported_address(addr, {'type':txin_type, 'pubkey':pubkey})
5✔
3223
            self.adb.add_address(addr)
5✔
3224
        self.save_keystore()
5✔
3225
        if write_to_disk:
5✔
3226
            self.save_db()
5✔
3227
        return good_addr, bad_keys
5✔
3228

3229
    def import_private_key(self, key: str, password: Optional[str]) -> str:
5✔
3230
        good_addr, bad_keys = self.import_private_keys([key], password=password)
5✔
3231
        if good_addr:
5✔
3232
            return good_addr[0]
5✔
3233
        else:
3234
            raise BitcoinException(str(bad_keys[0][1]))
×
3235

3236
    def get_txin_type(self, address):
5✔
3237
        return self.db.get_imported_address(address).get('type', 'address')
5✔
3238

3239
    @profiler
5✔
3240
    def try_detecting_internal_addresses_corruption(self):
5✔
3241
        # we check only a random sample, for performance
3242
        addresses_all = self.get_addresses()
×
3243
        # some random *used* addresses (note: we likely have not synced yet)
3244
        addresses_used = [addr for addr in addresses_all if self.adb.is_used(addr)]
×
3245
        sample1 = random.sample(addresses_used, min(len(addresses_used), 10))
×
3246
        # some random *unused* addresses
3247
        addresses_unused = [addr for addr in addresses_all if not self.adb.is_used(addr)]
×
3248
        sample2 = random.sample(addresses_unused, min(len(addresses_unused), 10))
×
3249
        for addr_found in itertools.chain(sample1, sample2):
×
3250
            self.check_address_for_corruption(addr_found)
×
3251

3252
    def check_address_for_corruption(self, addr):
5✔
3253
        if addr and self.is_mine(addr):
5✔
3254
            pubkey = self.get_public_key(addr)
5✔
3255
            if not pubkey:
5✔
3256
                return
×
3257
            txin_type = self.get_txin_type(addr)
5✔
3258
            if txin_type == 'address':
5✔
3259
                return
×
3260
            if addr != bitcoin.pubkey_to_address(txin_type, pubkey):
5✔
3261
                raise InternalAddressCorruption()
×
3262

3263
    def pubkeys_to_address(self, pubkeys):
5✔
3264
        pubkey = pubkeys[0]
×
3265
        # FIXME This is slow.
3266
        #       Ideally we would re-derive the address from the pubkey and the txin_type,
3267
        #       but we don't know the txin_type, and we only have an addr->txin_type map.
3268
        #       so instead a linear search of reverse-lookups is done...
3269
        for addr in self.db.get_imported_addresses():
×
3270
            if self.db.get_imported_address(addr)['pubkey'] == pubkey:
×
3271
                return addr
×
3272
        return None
×
3273

3274
    def decrypt_message(self, pubkey: str, message, password) -> bytes:
5✔
3275
        # this is significantly faster than the implementation in the superclass
3276
        return self.keystore.decrypt_message(pubkey, message, password)
5✔
3277

3278

3279
class Deterministic_Wallet(Abstract_Wallet):
5✔
3280

3281
    def __init__(self, db, *, config):
5✔
3282
        self._ephemeral_addr_to_addr_index = {}  # type: Dict[str, Sequence[int]]
5✔
3283
        Abstract_Wallet.__init__(self, db, config=config)
5✔
3284
        self.gap_limit = db.get('gap_limit', 20)
5✔
3285
        # generate addresses now. note that without libsecp this might block
3286
        # for a few seconds!
3287
        self.synchronize()
5✔
3288

3289
    def _init_lnworker(self):
5✔
3290
        # lightning_privkey2 is not deterministic (legacy wallets, bip39)
3291
        ln_xprv = self.db.get('lightning_xprv') or self.db.get('lightning_privkey2')
5✔
3292
        # lnworker can only be initialized once receiving addresses are available
3293
        # therefore we instantiate lnworker in DeterministicWallet
3294
        self.lnworker = LNWallet(self, ln_xprv) if ln_xprv else None
5✔
3295

3296
    def has_seed(self):
5✔
3297
        return self.keystore.has_seed()
5✔
3298

3299
    def get_addresses(self):
5✔
3300
        # note: overridden so that the history can be cleared.
3301
        # addresses are ordered based on derivation
3302
        out = self.get_receiving_addresses()
5✔
3303
        out += self.get_change_addresses()
5✔
3304
        return out
5✔
3305

3306
    def get_receiving_addresses(self, *, slice_start=None, slice_stop=None):
5✔
3307
        return self.db.get_receiving_addresses(slice_start=slice_start, slice_stop=slice_stop)
5✔
3308

3309
    def get_change_addresses(self, *, slice_start=None, slice_stop=None):
5✔
3310
        return self.db.get_change_addresses(slice_start=slice_start, slice_stop=slice_stop)
5✔
3311

3312
    @profiler
5✔
3313
    def try_detecting_internal_addresses_corruption(self):
5✔
3314
        addresses_all = self.get_addresses()
×
3315
        # first few addresses
3316
        nfirst_few = 10
×
3317
        sample1 = addresses_all[:nfirst_few]
×
3318
        # some random *used* addresses (note: we likely have not synced yet)
3319
        addresses_used = [addr for addr in addresses_all[nfirst_few:] if self.adb.is_used(addr)]
×
3320
        sample2 = random.sample(addresses_used, min(len(addresses_used), 10))
×
3321
        # some random *unused* addresses
3322
        addresses_unused = [addr for addr in addresses_all[nfirst_few:] if not self.adb.is_used(addr)]
×
3323
        sample3 = random.sample(addresses_unused, min(len(addresses_unused), 10))
×
3324
        for addr_found in itertools.chain(sample1, sample2, sample3):
×
3325
            self.check_address_for_corruption(addr_found)
×
3326

3327
    def check_address_for_corruption(self, addr):
5✔
3328
        if addr and self.is_mine(addr):
5✔
3329
            if addr != self.derive_address(*self.get_address_index(addr)):
5✔
3330
                raise InternalAddressCorruption()
×
3331

3332
    def get_seed(self, password):
5✔
3333
        return self.keystore.get_seed(password)
5✔
3334

3335
    def change_gap_limit(self, value):
5✔
3336
        '''This method is not called in the code, it is kept for console use'''
3337
        value = int(value)
×
3338
        if value >= self.min_acceptable_gap():
×
3339
            self.gap_limit = value
×
3340
            self.db.put('gap_limit', self.gap_limit)
×
3341
            self.save_db()
×
3342
            return True
×
3343
        else:
3344
            return False
×
3345

3346
    def num_unused_trailing_addresses(self, addresses):
5✔
3347
        k = 0
×
3348
        for addr in addresses[::-1]:
×
3349
            if self.db.get_addr_history(addr):
×
3350
                break
×
3351
            k += 1
×
3352
        return k
×
3353

3354
    def min_acceptable_gap(self) -> int:
5✔
3355
        # fixme: this assumes wallet is synchronized
3356
        n = 0
×
3357
        nmax = 0
×
3358
        addresses = self.get_receiving_addresses()
×
3359
        k = self.num_unused_trailing_addresses(addresses)
×
3360
        for addr in addresses[0:-k]:
×
3361
            if self.adb.address_is_old(addr):
×
3362
                n = 0
×
3363
            else:
3364
                n += 1
×
3365
                nmax = max(nmax, n)
×
3366
        return nmax + 1
×
3367

3368
    @abstractmethod
5✔
3369
    def derive_pubkeys(self, c: int, i: int) -> Sequence[str]:
5✔
3370
        pass
×
3371

3372
    def derive_address(self, for_change: int, n: int) -> str:
5✔
3373
        for_change = int(for_change)
5✔
3374
        pubkeys = self.derive_pubkeys(for_change, n)
5✔
3375
        return self.pubkeys_to_address(pubkeys)
5✔
3376

3377
    def export_private_key_for_path(self, path: Union[Sequence[int], str], password: Optional[str]) -> str:
5✔
3378
        if isinstance(path, str):
5✔
3379
            path = convert_bip32_strpath_to_intpath(path)
5✔
3380
        pk, compressed = self.keystore.get_private_key(path, password)
5✔
3381
        txin_type = self.get_txin_type()  # assumes no mixed-scripts in wallet
5✔
3382
        return bitcoin.serialize_privkey(pk, compressed, txin_type)
5✔
3383

3384
    def get_public_keys_with_deriv_info(self, address: str):
5✔
3385
        der_suffix = self.get_address_index(address)
5✔
3386
        der_suffix = [int(x) for x in der_suffix]
5✔
3387
        return {k.derive_pubkey(*der_suffix): (k, der_suffix)
5✔
3388
                for k in self.get_keystores()}
3389

3390
    def _add_txinout_derivation_info(self, txinout, address, *, only_der_suffix):
5✔
3391
        if not self.is_mine(address):
5✔
3392
            return
×
3393
        pubkey_deriv_info = self.get_public_keys_with_deriv_info(address)
5✔
3394
        for pubkey in pubkey_deriv_info:
5✔
3395
            ks, der_suffix = pubkey_deriv_info[pubkey]
5✔
3396
            fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix,
5✔
3397
                                                                                   only_der_suffix=only_der_suffix)
3398
            txinout.bip32_paths[pubkey] = (fp_bytes, der_full)
5✔
3399

3400
    def create_new_address(self, for_change: bool = False):
5✔
3401
        assert type(for_change) is bool
5✔
3402
        with self.lock:
5✔
3403
            n = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses()
5✔
3404
            address = self.derive_address(int(for_change), n)
5✔
3405
            self.db.add_change_address(address) if for_change else self.db.add_receiving_address(address)
5✔
3406
            self.adb.add_address(address)
5✔
3407
            if for_change:
5✔
3408
                # note: if it's actually "old", it will get filtered later
3409
                self._not_old_change_addresses.append(address)
5✔
3410
            return address
5✔
3411

3412
    def synchronize_sequence(self, for_change: bool) -> int:
5✔
3413
        count = 0  # num new addresses we generated
5✔
3414
        limit = self.gap_limit_for_change if for_change else self.gap_limit
5✔
3415
        while True:
3✔
3416
            num_addr = self.db.num_change_addresses() if for_change else self.db.num_receiving_addresses()
5✔
3417
            if num_addr < limit:
5✔
3418
                count += 1
5✔
3419
                self.create_new_address(for_change)
5✔
3420
                continue
5✔
3421
            if for_change:
5✔
3422
                last_few_addresses = self.get_change_addresses(slice_start=-limit)
5✔
3423
            else:
3424
                last_few_addresses = self.get_receiving_addresses(slice_start=-limit)
5✔
3425
            if any(map(self.adb.address_is_old, last_few_addresses)):
5✔
3426
                count += 1
5✔
3427
                self.create_new_address(for_change)
5✔
3428
            else:
3429
                break
3✔
3430
        return count
5✔
3431

3432
    def synchronize(self):
5✔
3433
        count = 0
5✔
3434
        with self.lock:
5✔
3435
            count += self.synchronize_sequence(False)
5✔
3436
            count += self.synchronize_sequence(True)
5✔
3437
        return count
5✔
3438

3439
    def get_all_known_addresses_beyond_gap_limit(self):
5✔
3440
        # note that we don't stop at first large gap
3441
        found = set()
×
3442

3443
        def process_addresses(addrs, gap_limit):
×
3444
            rolling_num_unused = 0
×
3445
            for addr in addrs:
×
3446
                if self.db.get_addr_history(addr):
×
3447
                    rolling_num_unused = 0
×
3448
                else:
3449
                    if rolling_num_unused >= gap_limit:
×
3450
                        found.add(addr)
×
3451
                    rolling_num_unused += 1
×
3452

3453
        process_addresses(self.get_receiving_addresses(), self.gap_limit)
×
3454
        process_addresses(self.get_change_addresses(), self.gap_limit_for_change)
×
3455
        return found
×
3456

3457
    def get_address_index(self, address) -> Optional[Sequence[int]]:
5✔
3458
        return self.db.get_address_index(address) or self._ephemeral_addr_to_addr_index.get(address)
5✔
3459

3460
    def get_address_path_str(self, address):
5✔
3461
        intpath = self.get_address_index(address)
×
3462
        if intpath is None:
×
3463
            return None
×
3464
        return convert_bip32_intpath_to_strpath(intpath)
×
3465

3466
    def _learn_derivation_path_for_address_from_txinout(self, txinout, address):
5✔
3467
        for ks in self.get_keystores():
5✔
3468
            pubkey, der_suffix = ks.find_my_pubkey_in_txinout(txinout, only_der_suffix=True)
5✔
3469
            if der_suffix is not None:
5✔
3470
                # note: we already know the pubkey belongs to the keystore,
3471
                #       but the script template might be different
3472
                if len(der_suffix) != 2: continue
5✔
3473
                try:
5✔
3474
                    my_address = self.derive_address(*der_suffix)
5✔
3475
                except CannotDerivePubkey:
×
3476
                    my_address = None
×
3477
                if my_address == address:
5✔
3478
                    self._ephemeral_addr_to_addr_index[address] = list(der_suffix)
5✔
3479
                    return True
5✔
3480
        return False
5✔
3481

3482
    def get_master_public_keys(self):
5✔
3483
        return [self.get_master_public_key()]
×
3484

3485
    def get_fingerprint(self):
5✔
3486
        return self.get_master_public_key()
5✔
3487

3488
    def get_txin_type(self, address=None):
5✔
3489
        return self.txin_type
5✔
3490

3491

3492
class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet):
5✔
3493

3494
    """ Deterministic Wallet with a single pubkey per address """
3495

3496
    def __init__(self, db, *, config):
5✔
3497
        Deterministic_Wallet.__init__(self, db, config=config)
5✔
3498

3499
    def get_public_key(self, address):
5✔
3500
        sequence = self.get_address_index(address)
×
3501
        pubkeys = self.derive_pubkeys(*sequence)
×
3502
        return pubkeys[0]
×
3503

3504
    def load_keystore(self):
5✔
3505
        self.keystore = load_keystore(self.db, 'keystore')  # type: KeyStoreWithMPK
5✔
3506
        try:
5✔
3507
            xtype = bip32.xpub_type(self.keystore.xpub)
5✔
3508
        except Exception:
5✔
3509
            xtype = 'standard'
5✔
3510
        self.txin_type = 'p2pkh' if xtype == 'standard' else xtype
5✔
3511

3512
    def get_master_public_key(self):
5✔
3513
        return self.keystore.get_master_public_key()
5✔
3514

3515
    def derive_pubkeys(self, c, i):
5✔
3516
        return [self.keystore.derive_pubkey(c, i).hex()]
5✔
3517

3518

3519

3520

3521

3522

3523
class Standard_Wallet(Simple_Deterministic_Wallet):
5✔
3524
    wallet_type = 'standard'
5✔
3525

3526
    def pubkeys_to_address(self, pubkeys):
5✔
3527
        pubkey = pubkeys[0]
5✔
3528
        return bitcoin.pubkey_to_address(self.txin_type, pubkey)
5✔
3529

3530

3531
class Multisig_Wallet(Deterministic_Wallet):
5✔
3532
    # generic m of n
3533

3534
    def __init__(self, db, *, config):
5✔
3535
        self.wallet_type = db.get('wallet_type')
5✔
3536
        self.m, self.n = multisig_type(self.wallet_type)
5✔
3537
        Deterministic_Wallet.__init__(self, db, config=config)
5✔
3538
        # sanity checks
3539
        for ks in self.get_keystores():
5✔
3540
            if not isinstance(ks, keystore.Xpub):
5✔
3541
                raise Exception(f"unexpected keystore type={type(ks)} in multisig")
×
3542
            if bip32.xpub_type(self.keystore.xpub) != bip32.xpub_type(ks.xpub):
5✔
3543
                raise Exception(f"multisig wallet needs to have homogeneous xpub types")
×
3544

3545
    def get_public_keys(self, address):
5✔
3546
        return [pk.hex() for pk in self.get_public_keys_with_deriv_info(address)]
×
3547

3548
    def pubkeys_to_address(self, pubkeys):
5✔
3549
        redeem_script = self.pubkeys_to_scriptcode(pubkeys)
5✔
3550
        return bitcoin.redeem_script_to_address(self.txin_type, redeem_script)
5✔
3551

3552
    def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> str:
5✔
3553
        return transaction.multisig_script(sorted(pubkeys), self.m)
5✔
3554

3555
    def derive_pubkeys(self, c, i):
5✔
3556
        return [k.derive_pubkey(c, i).hex() for k in self.get_keystores()]
5✔
3557

3558
    def load_keystore(self):
5✔
3559
        self.keystores = {}
5✔
3560
        for i in range(self.n):
5✔
3561
            name = 'x%d/'%(i+1)
5✔
3562
            self.keystores[name] = load_keystore(self.db, name)
5✔
3563
        self.keystore = self.keystores['x1/']
5✔
3564
        xtype = bip32.xpub_type(self.keystore.xpub)
5✔
3565
        self.txin_type = 'p2sh' if xtype == 'standard' else xtype
5✔
3566

3567
    def save_keystore(self):
5✔
3568
        for name, k in self.keystores.items():
×
3569
            self.db.put(name, k.dump())
×
3570

3571
    def get_keystore(self):
5✔
3572
        return self.keystores.get('x1/')
×
3573

3574
    def get_keystores(self):
5✔
3575
        return [self.keystores[i] for i in sorted(self.keystores.keys())]
5✔
3576

3577
    def can_have_keystore_encryption(self):
5✔
3578
        return any([k.may_have_password() for k in self.get_keystores()])
×
3579

3580
    def _update_password_for_keystore(self, old_pw, new_pw):
5✔
3581
        for name, keystore in self.keystores.items():
×
3582
            if keystore.may_have_password():
×
3583
                keystore.update_password(old_pw, new_pw)
×
3584
                self.db.put(name, keystore.dump())
×
3585

3586
    def check_password(self, password):
5✔
3587
        for name, keystore in self.keystores.items():
×
3588
            if keystore.may_have_password():
×
3589
                keystore.check_password(password)
×
3590
        if self.has_storage_encryption():
×
3591
            self.storage.check_password(password)
×
3592

3593
    def get_available_storage_encryption_version(self):
5✔
3594
        # multisig wallets are not offered hw device encryption
3595
        return StorageEncryptionVersion.USER_PASSWORD
×
3596

3597
    def has_seed(self):
5✔
3598
        return self.keystore.has_seed()
×
3599

3600
    def is_watching_only(self):
5✔
3601
        return all([k.is_watching_only() for k in self.get_keystores()])
5✔
3602

3603
    def get_master_public_key(self):
5✔
3604
        return self.keystore.get_master_public_key()
×
3605

3606
    def get_master_public_keys(self):
5✔
3607
        return [k.get_master_public_key() for k in self.get_keystores()]
×
3608

3609
    def get_fingerprint(self):
5✔
3610
        return ''.join(sorted(self.get_master_public_keys()))
×
3611

3612

3613
wallet_types = ['standard', 'multisig', 'imported']
5✔
3614

3615
def register_wallet_type(category):
5✔
3616
    wallet_types.append(category)
5✔
3617

3618
wallet_constructors = {
5✔
3619
    'standard': Standard_Wallet,
3620
    'old': Standard_Wallet,
3621
    'xpub': Standard_Wallet,
3622
    'imported': Imported_Wallet
3623
}
3624

3625
def register_constructor(wallet_type, constructor):
5✔
3626
    wallet_constructors[wallet_type] = constructor
×
3627

3628
# former WalletFactory
3629
class Wallet(object):
5✔
3630
    """The main wallet "entry point".
3631
    This class is actually a factory that will return a wallet of the correct
3632
    type when passed a WalletStorage instance."""
3633

3634
    def __new__(self, db: 'WalletDB', *, config: SimpleConfig):
5✔
3635
        wallet_type = db.get('wallet_type')
5✔
3636
        WalletClass = Wallet.wallet_class(wallet_type)
5✔
3637
        wallet = WalletClass(db, config=config)
5✔
3638
        return wallet
5✔
3639

3640
    @staticmethod
5✔
3641
    def wallet_class(wallet_type):
5✔
3642
        if multisig_type(wallet_type):
5✔
3643
            return Multisig_Wallet
5✔
3644
        if wallet_type in wallet_constructors:
5✔
3645
            return wallet_constructors[wallet_type]
5✔
3646
        raise WalletFileException("Unknown wallet type: " + str(wallet_type))
×
3647

3648

3649
def create_new_wallet(*, path, config: SimpleConfig, passphrase=None, password=None,
5✔
3650
                      encrypt_file=True, seed_type=None, gap_limit=None) -> dict:
3651
    """Create a new wallet"""
3652
    storage = WalletStorage(path)
5✔
3653
    if storage.file_exists():
5✔
3654
        raise Exception("Remove the existing wallet first!")
×
3655
    db = WalletDB('', storage=storage, manual_upgrades=False)
5✔
3656

3657
    seed = Mnemonic('en').make_seed(seed_type=seed_type)
5✔
3658
    k = keystore.from_seed(seed, passphrase)
5✔
3659
    db.put('keystore', k.dump())
5✔
3660
    db.put('wallet_type', 'standard')
5✔
3661
    if k.can_have_deterministic_lightning_xprv():
5✔
3662
        db.put('lightning_xprv', k.get_lightning_xprv(None))
5✔
3663
    if gap_limit is not None:
5✔
3664
        db.put('gap_limit', gap_limit)
5✔
3665
    wallet = Wallet(db, config=config)
5✔
3666
    wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)
5✔
3667
    wallet.synchronize()
5✔
3668
    msg = "Please keep your seed in a safe place; if you lose it, you will not be able to restore your wallet."
5✔
3669
    wallet.save_db()
5✔
3670
    return {'seed': seed, 'wallet': wallet, 'msg': msg}
5✔
3671

3672

3673
def restore_wallet_from_text(
5✔
3674
    text: str,
3675
    *,
3676
    path: Optional[str],
3677
    config: SimpleConfig,
3678
    passphrase: Optional[str] = None,
3679
    password: Optional[str] = None,
3680
    encrypt_file: Optional[bool] = None,
3681
    gap_limit: Optional[int] = None,
3682
) -> dict:
3683
    """Restore a wallet from text. Text can be a seed phrase, a master
3684
    public key, a master private key, a list of bitcoin addresses
3685
    or bitcoin private keys."""
3686
    if path is None:  # create wallet in-memory
5✔
3687
        storage = None
5✔
3688
    else:
3689
        storage = WalletStorage(path)
5✔
3690
        if storage.file_exists():
5✔
3691
            raise Exception("Remove the existing wallet first!")
×
3692
    if encrypt_file is None:
5✔
3693
        encrypt_file = True
5✔
3694
    db = WalletDB('', storage=storage, manual_upgrades=False)
5✔
3695
    text = text.strip()
5✔
3696
    if keystore.is_address_list(text):
5✔
3697
        wallet = Imported_Wallet(db, config=config)
5✔
3698
        addresses = text.split()
5✔
3699
        good_inputs, bad_inputs = wallet.import_addresses(addresses, write_to_disk=False)
5✔
3700
        # FIXME tell user about bad_inputs
3701
        if not good_inputs:
5✔
3702
            raise Exception("None of the given addresses can be imported")
×
3703
    elif keystore.is_private_key_list(text, allow_spaces_inside_key=False):
5✔
3704
        k = keystore.Imported_KeyStore({})
5✔
3705
        db.put('keystore', k.dump())
5✔
3706
        wallet = Imported_Wallet(db, config=config)
5✔
3707
        keys = keystore.get_private_keys(text, allow_spaces_inside_key=False)
5✔
3708
        good_inputs, bad_inputs = wallet.import_private_keys(keys, None, write_to_disk=False)
5✔
3709
        # FIXME tell user about bad_inputs
3710
        if not good_inputs:
5✔
3711
            raise Exception("None of the given privkeys can be imported")
×
3712
    else:
3713
        if keystore.is_master_key(text):
5✔
3714
            k = keystore.from_master_key(text)
5✔
3715
        elif keystore.is_seed(text):
5✔
3716
            k = keystore.from_seed(text, passphrase)
5✔
3717
            if k.can_have_deterministic_lightning_xprv():
5✔
3718
                db.put('lightning_xprv', k.get_lightning_xprv(None))
5✔
3719
        else:
3720
            raise Exception("Seed or key not recognized")
×
3721
        db.put('keystore', k.dump())
5✔
3722
        db.put('wallet_type', 'standard')
5✔
3723
        if gap_limit is not None:
5✔
3724
            db.put('gap_limit', gap_limit)
5✔
3725
        wallet = Wallet(db, config=config)
5✔
3726
    if db.storage:
5✔
3727
        assert not db.storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk"
5✔
3728
    wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)
5✔
3729
    wallet.synchronize()
5✔
3730
    msg = ("This wallet was restored offline. It may contain more addresses than displayed. "
5✔
3731
           "Start a daemon and use load_wallet to sync its history.")
3732
    wallet.save_db()
5✔
3733
    return {'wallet': wallet, 'msg': msg}
5✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc