• 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

80.67
/electrum/wallet_db.py
1
#!/usr/bin/env python
2
#
3
# Electrum - lightweight Bitcoin client
4
# Copyright (C) 2015 Thomas Voegtlin
5
#
6
# Permission is hereby granted, free of charge, to any person
7
# obtaining a copy of this software and associated documentation files
8
# (the "Software"), to deal in the Software without restriction,
9
# including without limitation the rights to use, copy, modify, merge,
10
# publish, distribute, sublicense, and/or sell copies of the Software,
11
# and to permit persons to whom the Software is furnished to do so,
12
# subject to the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be
15
# included in all copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
21
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
22
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
# SOFTWARE.
25
import os
5✔
26
import ast
5✔
27
import datetime
5✔
28
import json
5✔
29
import copy
5✔
30
import threading
5✔
31
from collections import defaultdict
5✔
32
from typing import Dict, Optional, List, Tuple, Set, Iterable, NamedTuple, Sequence, TYPE_CHECKING, Union
5✔
33
import binascii
5✔
34
import time
5✔
35

36
import attr
5✔
37

38
from . import util, bitcoin
5✔
39
from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh
5✔
40
from .invoices import Invoice, Request
5✔
41
from .keystore import bip44_derivation
5✔
42
from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput
5✔
43
from .logging import Logger
5✔
44

45
from .lnutil import LOCAL, REMOTE, HTLCOwner, ChannelType
5✔
46
from . import json_db
5✔
47
from .json_db import StoredDict, JsonDB, locked, modifier, StoredObject, stored_in, stored_as
5✔
48
from .plugin import run_hook, plugin_loaders
5✔
49
from .version import ELECTRUM_VERSION
5✔
50

51

52

53
# seed_version is now used for the version of the wallet file
54

55
OLD_SEED_VERSION = 4        # electrum versions < 2.0
5✔
56
NEW_SEED_VERSION = 11       # electrum versions >= 2.0
5✔
57
FINAL_SEED_VERSION = 52     # electrum >= 2.7 will set this to prevent
5✔
58
                            # old versions from overwriting new format
59

60

61
@stored_in('tx_fees', tuple)
5✔
62
class TxFeesValue(NamedTuple):
5✔
63
    fee: Optional[int] = None
5✔
64
    is_calculated_by_us: bool = False
5✔
65
    num_inputs: Optional[int] = None
5✔
66

67

68
@stored_as('db_metadata')
5✔
69
@attr.s
5✔
70
class DBMetadata(StoredObject):
5✔
71
    creation_timestamp = attr.ib(default=None, type=int)
5✔
72
    first_electrum_version_used = attr.ib(default=None, type=str)
5✔
73

74
    def to_str(self) -> str:
5✔
75
        ts = self.creation_timestamp
×
76
        ver = self.first_electrum_version_used
×
77
        if ts is None or ver is None:
×
78
            return "unknown"
×
79
        date_str = datetime.date.fromtimestamp(ts).isoformat()
×
80
        return f"using {ver}, on {date_str}"
×
81

82

83
# note: subclassing WalletFileException for some specific cases
84
#       allows the crash reporter to distinguish them and open
85
#       separate tracking issues
86
class WalletFileExceptionVersion51(WalletFileException): pass
5✔
87

88
# register dicts that require value conversions not handled by constructor
89
json_db.register_dict('transactions', lambda x: tx_from_any(x, deserialize=False), None)
5✔
90
json_db.register_dict('prevouts_by_scripthash', lambda x: set(tuple(k) for k in x), None)
5✔
91
json_db.register_dict('data_loss_protect_remote_pcp', lambda x: bytes.fromhex(x), None)
5✔
92
# register dicts that require key conversion
93
for key in [
5✔
94
        'adds', 'locked_in', 'settles', 'fails', 'fee_updates', 'buckets',
95
        'unacked_updates', 'unfulfilled_htlcs', 'fail_htlc_reasons', 'onion_keys']:
96
    json_db.register_dict_key(key, int)
5✔
97
for key in ['log']:
5✔
98
    json_db.register_dict_key(key, lambda x: HTLCOwner(int(x)))
5✔
99
for key in ['locked_in', 'fails', 'settles']:
5✔
100
    json_db.register_parent_key(key, lambda x: HTLCOwner(int(x)))
5✔
101

102

103
class WalletDB(JsonDB):
5✔
104

105
    def __init__(self, data, *, storage=None, manual_upgrades: bool):
5✔
106
        JsonDB.__init__(self, data, storage)
5✔
107
        if not data:
5✔
108
            # create new DB
109
            self.put('seed_version', FINAL_SEED_VERSION)
5✔
110
            self._add_db_creation_metadata()
5✔
111
            self._after_upgrade_tasks()
5✔
112
        self._manual_upgrades = manual_upgrades
5✔
113
        self._called_after_upgrade_tasks = False
5✔
114
        if not self._manual_upgrades and self.requires_split():
5✔
115
            raise WalletFileException("This wallet has multiple accounts and must be split")
×
116
        if not self.requires_upgrade():
5✔
117
            self._after_upgrade_tasks()
5✔
118
        elif not self._manual_upgrades:
5✔
119
            self.upgrade()
5✔
120
        # load plugins that are conditional on wallet type
121
        self.load_plugins()
5✔
122

123
    def load_data(self, s):
5✔
124
        try:
5✔
125
            JsonDB.load_data(self, s)
5✔
126
        except Exception:
5✔
127
            try:
5✔
128
                d = ast.literal_eval(s)
5✔
129
                labels = d.get('labels', {})
5✔
130
            except Exception as e:
×
131
                raise WalletFileException("Cannot read wallet file. (parsing failed)")
×
132
            self.data = {}
5✔
133
            for key, value in d.items():
5✔
134
                try:
5✔
135
                    json.dumps(key)
5✔
136
                    json.dumps(value)
5✔
137
                except Exception:
×
138
                    self.logger.info(f'Failed to convert label to json format: {key}')
×
139
                    continue
×
140
                self.data[key] = value
5✔
141
        if not isinstance(self.data, dict):
5✔
142
            raise WalletFileException("Malformed wallet file (not dict)")
×
143

144
    def requires_split(self):
5✔
145
        d = self.get('accounts', {})
5✔
146
        return len(d) > 1
5✔
147

148
    def get_split_accounts(self):
5✔
149
        result = []
5✔
150
        # backward compatibility with old wallets
151
        d = self.get('accounts', {})
5✔
152
        if len(d) < 2:
5✔
153
            return
×
154
        wallet_type = self.get('wallet_type')
5✔
155
        if wallet_type == 'old':
5✔
156
            assert len(d) == 2
×
157
            data1 = copy.deepcopy(self.data)
×
158
            data1['accounts'] = {'0': d['0']}
×
159
            data1['suffix'] = 'deterministic'
×
160
            data2 = copy.deepcopy(self.data)
×
161
            data2['accounts'] = {'/x': d['/x']}
×
162
            data2['seed'] = None
×
163
            data2['seed_version'] = None
×
164
            data2['master_public_key'] = None
×
165
            data2['wallet_type'] = 'imported'
×
166
            data2['suffix'] = 'imported'
×
167
            result = [data1, data2]
×
168

169
        # note: do not add new hardware types here, this code is for converting legacy wallets
170
        elif wallet_type in ['bip44', 'trezor', 'keepkey', 'ledger', 'btchip']:
5✔
171
            mpk = self.get('master_public_keys')
5✔
172
            for k in d.keys():
5✔
173
                i = int(k)
5✔
174
                x = d[k]
5✔
175
                if x.get("pending"):
5✔
176
                    continue
×
177
                xpub = mpk["x/%d'"%i]
5✔
178
                new_data = copy.deepcopy(self.data)
5✔
179
                # save account, derivation and xpub at index 0
180
                new_data['accounts'] = {'0': x}
5✔
181
                new_data['master_public_keys'] = {"x/0'": xpub}
5✔
182
                new_data['derivation'] = bip44_derivation(k)
5✔
183
                new_data['suffix'] = k
5✔
184
                result.append(new_data)
5✔
185
        else:
186
            raise WalletFileException("This wallet has multiple accounts and must be split")
×
187
        return result
5✔
188

189
    def requires_upgrade(self):
5✔
190
        return self.get_seed_version() < FINAL_SEED_VERSION
5✔
191

192
    @profiler
5✔
193
    def upgrade(self):
5✔
194
        self.logger.info('upgrading wallet format')
5✔
195
        if self._called_after_upgrade_tasks:
5✔
196
            # we need strict ordering between upgrade() and after_upgrade_tasks()
197
            raise Exception("'after_upgrade_tasks' must NOT be called before 'upgrade'")
×
198
        self._convert_imported()
5✔
199
        self._convert_wallet_type()
5✔
200
        self._convert_account()
5✔
201
        self._convert_version_13_b()
5✔
202
        self._convert_version_14()
5✔
203
        self._convert_version_15()
5✔
204
        self._convert_version_16()
5✔
205
        self._convert_version_17()
5✔
206
        self._convert_version_18()
5✔
207
        self._convert_version_19()
5✔
208
        self._convert_version_20()
5✔
209
        self._convert_version_21()
5✔
210
        self._convert_version_22()
5✔
211
        self._convert_version_23()
5✔
212
        self._convert_version_24()
5✔
213
        self._convert_version_25()
5✔
214
        self._convert_version_26()
5✔
215
        self._convert_version_27()
5✔
216
        self._convert_version_28()
5✔
217
        self._convert_version_29()
5✔
218
        self._convert_version_30()
5✔
219
        self._convert_version_31()
5✔
220
        self._convert_version_32()
5✔
221
        self._convert_version_33()
5✔
222
        self._convert_version_34()
5✔
223
        self._convert_version_35()
5✔
224
        self._convert_version_36()
5✔
225
        self._convert_version_37()
5✔
226
        self._convert_version_38()
5✔
227
        self._convert_version_39()
5✔
228
        self._convert_version_40()
5✔
229
        self._convert_version_41()
5✔
230
        self._convert_version_42()
5✔
231
        self._convert_version_43()
5✔
232
        self._convert_version_44()
5✔
233
        self._convert_version_45()
5✔
234
        self._convert_version_46()
5✔
235
        self._convert_version_47()
5✔
236
        self._convert_version_48()
5✔
237
        self._convert_version_49()
5✔
238
        self._convert_version_50()
5✔
239
        self._convert_version_51()
5✔
240
        self._convert_version_52()
5✔
241
        self.put('seed_version', FINAL_SEED_VERSION)  # just to be sure
5✔
242

243
        self._after_upgrade_tasks()
5✔
244

245
    def _after_upgrade_tasks(self):
5✔
246
        self._called_after_upgrade_tasks = True
5✔
247
        self._load_transactions()
5✔
248

249
    def _convert_wallet_type(self):
5✔
250
        if not self._is_upgrade_method_needed(0, 13):
5✔
251
            return
5✔
252

253
        wallet_type = self.get('wallet_type')
5✔
254
        if wallet_type == 'btchip': wallet_type = 'ledger'
5✔
255
        if self.get('keystore') or self.get('x1/') or wallet_type=='imported':
5✔
256
            return False
5✔
257
        assert not self.requires_split()
5✔
258
        seed_version = self.get_seed_version()
5✔
259
        seed = self.get('seed')
5✔
260
        xpubs = self.get('master_public_keys')
5✔
261
        xprvs = self.get('master_private_keys', {})
5✔
262
        mpk = self.get('master_public_key')
5✔
263
        keypairs = self.get('keypairs')
5✔
264
        key_type = self.get('key_type')
5✔
265
        if seed_version == OLD_SEED_VERSION or wallet_type == 'old':
5✔
266
            d = {
5✔
267
                'type': 'old',
268
                'seed': seed,
269
                'mpk': mpk,
270
            }
271
            self.put('wallet_type', 'standard')
5✔
272
            self.put('keystore', d)
5✔
273

274
        elif key_type == 'imported':
5✔
275
            d = {
5✔
276
                'type': 'imported',
277
                'keypairs': keypairs,
278
            }
279
            self.put('wallet_type', 'standard')
5✔
280
            self.put('keystore', d)
5✔
281

282
        elif wallet_type in ['xpub', 'standard']:
5✔
283
            xpub = xpubs["x/"]
5✔
284
            xprv = xprvs.get("x/")
5✔
285
            d = {
5✔
286
                'type': 'bip32',
287
                'xpub': xpub,
288
                'xprv': xprv,
289
                'seed': seed,
290
            }
291
            self.put('wallet_type', 'standard')
5✔
292
            self.put('keystore', d)
5✔
293

294
        elif wallet_type in ['bip44']:
5✔
295
            xpub = xpubs["x/0'"]
×
296
            xprv = xprvs.get("x/0'")
×
297
            d = {
×
298
                'type': 'bip32',
299
                'xpub': xpub,
300
                'xprv': xprv,
301
            }
302
            self.put('wallet_type', 'standard')
×
303
            self.put('keystore', d)
×
304

305
        # note: do not add new hardware types here, this code is for converting legacy wallets
306
        elif wallet_type in ['trezor', 'keepkey', 'ledger']:
5✔
307
            xpub = xpubs["x/0'"]
5✔
308
            derivation = self.get('derivation', bip44_derivation(0))
5✔
309
            d = {
5✔
310
                'type': 'hardware',
311
                'hw_type': wallet_type,
312
                'xpub': xpub,
313
                'derivation': derivation,
314
            }
315
            self.put('wallet_type', 'standard')
5✔
316
            self.put('keystore', d)
5✔
317

318
        elif (wallet_type == '2fa') or multisig_type(wallet_type):
5✔
319
            for key in xpubs.keys():
5✔
320
                d = {
5✔
321
                    'type': 'bip32',
322
                    'xpub': xpubs[key],
323
                    'xprv': xprvs.get(key),
324
                }
325
                if key == 'x1/' and seed:
5✔
326
                    d['seed'] = seed
5✔
327
                self.put(key, d)
5✔
328
        else:
329
            raise WalletFileException('Unable to tell wallet type. Is this even a wallet file?')
×
330
        # remove junk
331
        self.put('master_public_key', None)
5✔
332
        self.put('master_public_keys', None)
5✔
333
        self.put('master_private_keys', None)
5✔
334
        self.put('derivation', None)
5✔
335
        self.put('seed', None)
5✔
336
        self.put('keypairs', None)
5✔
337
        self.put('key_type', None)
5✔
338

339
    def _convert_version_13_b(self):
5✔
340
        # version 13 is ambiguous, and has an earlier and a later structure
341
        if not self._is_upgrade_method_needed(0, 13):
5✔
342
            return
5✔
343

344
        if self.get('wallet_type') == 'standard':
5✔
345
            if self.get('keystore').get('type') == 'imported':
5✔
346
                pubkeys = self.get('keystore').get('keypairs').keys()
5✔
347
                d = {'change': []}
5✔
348
                receiving_addresses = []
5✔
349
                for pubkey in pubkeys:
5✔
350
                    addr = bitcoin.pubkey_to_address('p2pkh', pubkey)
5✔
351
                    receiving_addresses.append(addr)
5✔
352
                d['receiving'] = receiving_addresses
5✔
353
                self.put('addresses', d)
5✔
354
                self.put('pubkeys', None)
5✔
355

356
        self.put('seed_version', 13)
5✔
357

358
    def _convert_version_14(self):
5✔
359
        # convert imported wallets for 3.0
360
        if not self._is_upgrade_method_needed(13, 13):
5✔
361
            return
5✔
362

363
        if self.get('wallet_type') =='imported':
5✔
364
            addresses = self.get('addresses')
5✔
365
            if type(addresses) is list:
5✔
366
                addresses = dict([(x, None) for x in addresses])
5✔
367
                self.put('addresses', addresses)
5✔
368
        elif self.get('wallet_type') == 'standard':
5✔
369
            if self.get('keystore').get('type')=='imported':
5✔
370
                addresses = set(self.get('addresses').get('receiving'))
5✔
371
                pubkeys = self.get('keystore').get('keypairs').keys()
5✔
372
                assert len(addresses) == len(pubkeys)
5✔
373
                d = {}
5✔
374
                for pubkey in pubkeys:
5✔
375
                    addr = bitcoin.pubkey_to_address('p2pkh', pubkey)
5✔
376
                    assert addr in addresses
5✔
377
                    d[addr] = {
5✔
378
                        'pubkey': pubkey,
379
                        'redeem_script': None,
380
                        'type': 'p2pkh'
381
                    }
382
                self.put('addresses', d)
5✔
383
                self.put('pubkeys', None)
5✔
384
                self.put('wallet_type', 'imported')
5✔
385
        self.put('seed_version', 14)
5✔
386

387
    def _convert_version_15(self):
5✔
388
        if not self._is_upgrade_method_needed(14, 14):
5✔
389
            return
5✔
390
        if self.get('seed_type') == 'segwit':
5✔
391
            # should not get here; get_seed_version should have caught this
392
            raise Exception('unsupported derivation (development segwit, v14)')
×
393
        self.put('seed_version', 15)
5✔
394

395
    def _convert_version_16(self):
5✔
396
        # fixes issue #3193 for Imported_Wallets with addresses
397
        # also, previous versions allowed importing any garbage as an address
398
        #       which we now try to remove, see pr #3191
399
        if not self._is_upgrade_method_needed(15, 15):
5✔
400
            return
5✔
401

402
        def remove_address(addr):
5✔
403
            def remove_from_dict(dict_name):
×
404
                d = self.get(dict_name, None)
×
405
                if d is not None:
×
406
                    d.pop(addr, None)
×
407
                    self.put(dict_name, d)
×
408

409
            def remove_from_list(list_name):
×
410
                lst = self.get(list_name, None)
×
411
                if lst is not None:
×
412
                    s = set(lst)
×
413
                    s -= {addr}
×
414
                    self.put(list_name, list(s))
×
415

416
            # note: we don't remove 'addr' from self.get('addresses')
417
            remove_from_dict('addr_history')
×
418
            remove_from_dict('labels')
×
419
            remove_from_dict('payment_requests')
×
420
            remove_from_list('frozen_addresses')
×
421

422
        if self.get('wallet_type') == 'imported':
5✔
423
            addresses = self.get('addresses')
5✔
424
            assert isinstance(addresses, dict)
5✔
425
            addresses_new = dict()
5✔
426
            for address, details in addresses.items():
5✔
427
                if not bitcoin.is_address(address):
5✔
428
                    remove_address(address)
×
429
                    continue
×
430
                if details is None:
5✔
431
                    addresses_new[address] = {}
5✔
432
                else:
433
                    addresses_new[address] = details
5✔
434
            self.put('addresses', addresses_new)
5✔
435

436
        self.put('seed_version', 16)
5✔
437

438
    def _convert_version_17(self):
5✔
439
        # delete pruned_txo; construct spent_outpoints
440
        if not self._is_upgrade_method_needed(16, 16):
5✔
441
            return
5✔
442

443
        self.put('pruned_txo', None)
5✔
444

445
        transactions = self.get('transactions', {})  # txid -> raw_tx
5✔
446
        spent_outpoints = defaultdict(dict)
5✔
447
        for txid, raw_tx in transactions.items():
5✔
448
            tx = Transaction(raw_tx)
5✔
449
            for txin in tx.inputs():
5✔
450
                if txin.is_coinbase_input():
5✔
451
                    continue
×
452
                prevout_hash = txin.prevout.txid.hex()
5✔
453
                prevout_n = txin.prevout.out_idx
5✔
454
                spent_outpoints[prevout_hash][str(prevout_n)] = txid
5✔
455
        self.put('spent_outpoints', spent_outpoints)
5✔
456

457
        self.put('seed_version', 17)
5✔
458

459
    def _convert_version_18(self):
5✔
460
        # delete verified_tx3 as its structure changed
461
        if not self._is_upgrade_method_needed(17, 17):
5✔
462
            return
5✔
463
        self.put('verified_tx3', None)
5✔
464
        self.put('seed_version', 18)
5✔
465

466
    def _convert_version_19(self):
5✔
467
        # delete tx_fees as its structure changed
468
        if not self._is_upgrade_method_needed(18, 18):
5✔
469
            return
×
470
        self.put('tx_fees', None)
5✔
471
        self.put('seed_version', 19)
5✔
472

473
    def _convert_version_20(self):
5✔
474
        # store 'derivation' (prefix) and 'root_fingerprint' in all xpub-based keystores.
475
        # store explicit None values if we cannot retroactively determine them
476
        if not self._is_upgrade_method_needed(19, 19):
5✔
477
            return
×
478

479
        from .bip32 import BIP32Node, convert_bip32_intpath_to_strpath
5✔
480
        # note: This upgrade method reimplements bip32.root_fp_and_der_prefix_from_xkey.
481
        #       This is done deliberately, to avoid introducing that method as a dependency to this upgrade.
482
        for ks_name in ('keystore', *['x{}/'.format(i) for i in range(1, 16)]):
5✔
483
            ks = self.get(ks_name, None)
5✔
484
            if ks is None: continue
5✔
485
            xpub = ks.get('xpub', None)
5✔
486
            if xpub is None: continue
5✔
487
            bip32node = BIP32Node.from_xkey(xpub)
5✔
488
            # derivation prefix
489
            derivation_prefix = ks.get('derivation', None)
5✔
490
            if derivation_prefix is None:
5✔
491
                assert bip32node.depth >= 0, bip32node.depth
5✔
492
                if bip32node.depth == 0:
5✔
493
                    derivation_prefix = 'm'
5✔
494
                elif bip32node.depth == 1:
5✔
495
                    child_number_int = int.from_bytes(bip32node.child_number, 'big')
5✔
496
                    derivation_prefix = convert_bip32_intpath_to_strpath([child_number_int])
5✔
497
                ks['derivation'] = derivation_prefix
5✔
498
            # root fingerprint
499
            root_fingerprint = ks.get('ckcc_xfp', None)
5✔
500
            if root_fingerprint is not None:
5✔
501
                root_fingerprint = root_fingerprint.to_bytes(4, byteorder="little", signed=False).hex().lower()
×
502
            if root_fingerprint is None:
5✔
503
                if bip32node.depth == 0:
5✔
504
                    root_fingerprint = bip32node.calc_fingerprint_of_this_node().hex().lower()
5✔
505
                elif bip32node.depth == 1:
5✔
506
                    root_fingerprint = bip32node.fingerprint.hex()
5✔
507
            ks['root_fingerprint'] = root_fingerprint
5✔
508
            ks.pop('ckcc_xfp', None)
5✔
509
            self.put(ks_name, ks)
5✔
510

511
        self.put('seed_version', 20)
5✔
512

513
    def _convert_version_21(self):
5✔
514
        if not self._is_upgrade_method_needed(20, 20):
5✔
515
            return
×
516
        channels = self.get('channels')
5✔
517
        if channels:
5✔
518
            for channel in channels:
×
519
                channel['state'] = 'OPENING'
×
520
            self.put('channels', channels)
×
521
        self.put('seed_version', 21)
5✔
522

523
    def _convert_version_22(self):
5✔
524
        # construct prevouts_by_scripthash
525
        if not self._is_upgrade_method_needed(21, 21):
5✔
526
            return
×
527

528
        from .bitcoin import script_to_scripthash
5✔
529
        transactions = self.get('transactions', {})  # txid -> raw_tx
5✔
530
        prevouts_by_scripthash = defaultdict(list)
5✔
531
        for txid, raw_tx in transactions.items():
5✔
532
            tx = Transaction(raw_tx)
5✔
533
            for idx, txout in enumerate(tx.outputs()):
5✔
534
                outpoint = f"{txid}:{idx}"
5✔
535
                scripthash = script_to_scripthash(txout.scriptpubkey.hex())
5✔
536
                prevouts_by_scripthash[scripthash].append((outpoint, txout.value))
5✔
537
        self.put('prevouts_by_scripthash', prevouts_by_scripthash)
5✔
538

539
        self.put('seed_version', 22)
5✔
540

541
    def _convert_version_23(self):
5✔
542
        if not self._is_upgrade_method_needed(22, 22):
5✔
543
            return
×
544
        channels = self.get('channels', [])
5✔
545
        LOCAL = 1
5✔
546
        REMOTE = -1
5✔
547
        for c in channels:
5✔
548
            # move revocation store from remote_config
549
            r = c['remote_config'].pop('revocation_store')
×
550
            c['revocation_store'] = r
×
551
            # convert fee updates
552
            log = c.get('log', {})
×
553
            for sub in LOCAL, REMOTE:
×
554
                l = log[str(sub)]['fee_updates']
×
555
                d = {}
×
556
                for i, fu in enumerate(l):
×
557
                    d[str(i)] = {
×
558
                        'rate':fu['rate'],
559
                        'ctn_local':fu['ctns'][str(LOCAL)],
560
                        'ctn_remote':fu['ctns'][str(REMOTE)]
561
                    }
562
                log[str(int(sub))]['fee_updates'] = d
×
563
        self.data['channels'] = channels
5✔
564

565
        self.data['seed_version'] = 23
5✔
566

567
    def _convert_version_24(self):
5✔
568
        if not self._is_upgrade_method_needed(23, 23):
5✔
569
            return
×
570
        channels = self.get('channels', [])
5✔
571
        for c in channels:
5✔
572
            # convert revocation store to dict
573
            r = c['revocation_store']
×
574
            d = {}
×
575
            for i in range(49):
×
576
                v = r['buckets'][i]
×
577
                if v is not None:
×
578
                    d[str(i)] = v
×
579
            r['buckets'] = d
×
580
            c['revocation_store'] = r
×
581
        # convert channels to dict
582
        self.data['channels'] = {x['channel_id']: x for x in channels}
5✔
583
        # convert txi & txo
584
        txi = self.get('txi', {})
5✔
585
        for tx_hash, d in list(txi.items()):
5✔
586
            d2 = {}
5✔
587
            for addr, l in d.items():
5✔
588
                d2[addr] = {}
5✔
589
                for ser, v in l:
5✔
590
                    d2[addr][ser] = v
5✔
591
            txi[tx_hash] = d2
5✔
592
        self.data['txi'] = txi
5✔
593
        txo = self.get('txo', {})
5✔
594
        for tx_hash, d in list(txo.items()):
5✔
595
            d2 = {}
5✔
596
            for addr, l in d.items():
5✔
597
                d2[addr] = {}
5✔
598
                for n, v, cb in l:
5✔
599
                    d2[addr][str(n)] = (v, cb)
5✔
600
            txo[tx_hash] = d2
5✔
601
        self.data['txo'] = txo
5✔
602

603
        self.data['seed_version'] = 24
5✔
604

605
    def _convert_version_25(self):
5✔
606
        from .crypto import sha256
5✔
607
        if not self._is_upgrade_method_needed(24, 24):
5✔
608
            return
×
609
        # add 'type' field to onchain requests
610
        PR_TYPE_ONCHAIN = 0
5✔
611
        requests = self.data.get('payment_requests', {})
5✔
612
        for k, r in list(requests.items()):
5✔
613
            if r.get('address') == k:
5✔
614
                requests[k] = {
5✔
615
                    'address': r['address'],
616
                    'amount': r.get('amount'),
617
                    'exp': r.get('exp'),
618
                    'id': r.get('id'),
619
                    'memo': r.get('memo'),
620
                    'time': r.get('time'),
621
                    'type': PR_TYPE_ONCHAIN,
622
                }
623
        # delete bip70 invoices
624
        # note: this upgrade was changed ~2 years after-the-fact to delete instead of converting
625
        invoices = self.data.get('invoices', {})
5✔
626
        for k, r in list(invoices.items()):
5✔
627
            data = r.get("hex")
5✔
628
            pr_id = sha256(bytes.fromhex(data))[0:16].hex()
5✔
629
            if pr_id != k:
5✔
630
                continue
5✔
631
            del invoices[k]
5✔
632
        self.data['seed_version'] = 25
5✔
633

634
    def _convert_version_26(self):
5✔
635
        if not self._is_upgrade_method_needed(25, 25):
5✔
636
            return
×
637
        channels = self.data.get('channels', {})
5✔
638
        channel_timestamps = self.data.pop('lightning_channel_timestamps', {})
5✔
639
        for channel_id, c in channels.items():
5✔
640
            item = channel_timestamps.get(channel_id)
×
641
            if item:
×
642
                funding_txid, funding_height, funding_timestamp, closing_txid, closing_height, closing_timestamp = item
×
643
                if funding_txid:
×
644
                    c['funding_height'] = funding_txid, funding_height, funding_timestamp
×
645
                if closing_txid:
×
646
                    c['closing_height'] = closing_txid, closing_height, closing_timestamp
×
647
        self.data['seed_version'] = 26
5✔
648

649
    def _convert_version_27(self):
5✔
650
        if not self._is_upgrade_method_needed(26, 26):
5✔
651
            return
×
652
        channels = self.data.get('channels', {})
5✔
653
        for channel_id, c in channels.items():
5✔
654
            c['local_config']['htlc_minimum_msat'] = 1
×
655
        self.data['seed_version'] = 27
5✔
656

657
    def _convert_version_28(self):
5✔
658
        if not self._is_upgrade_method_needed(27, 27):
5✔
659
            return
×
660
        channels = self.data.get('channels', {})
5✔
661
        for channel_id, c in channels.items():
5✔
662
            c['local_config']['channel_seed'] = None
×
663
        self.data['seed_version'] = 28
5✔
664

665
    def _convert_version_29(self):
5✔
666
        if not self._is_upgrade_method_needed(28, 28):
5✔
667
            return
×
668
        PR_TYPE_ONCHAIN = 0
5✔
669
        requests = self.data.get('payment_requests', {})
5✔
670
        invoices = self.data.get('invoices', {})
5✔
671
        for d in [invoices, requests]:
5✔
672
            for key, r in list(d.items()):
5✔
673
                _type = r.get('type', 0)
5✔
674
                item = {
5✔
675
                    'type': _type,
676
                    'message': r.get('message') or r.get('memo', ''),
677
                    'amount': r.get('amount'),
678
                    'exp': r.get('exp') or 0,
679
                    'time': r.get('time', 0),
680
                }
681
                if _type == PR_TYPE_ONCHAIN:
5✔
682
                    address = r.pop('address', None)
5✔
683
                    if address:
5✔
684
                        outputs = [(0, address, r.get('amount'))]
5✔
685
                    else:
686
                        outputs = r.get('outputs')
5✔
687
                    item.update({
5✔
688
                        'outputs': outputs,
689
                        'id': r.get('id'),
690
                        'bip70': r.get('bip70'),
691
                        'requestor': r.get('requestor'),
692
                    })
693
                else:
694
                    item.update({
×
695
                        'rhash': r['rhash'],
696
                        'invoice': r['invoice'],
697
                    })
698
                d[key] = item
5✔
699
        self.data['seed_version'] = 29
5✔
700

701
    def _convert_version_30(self):
5✔
702
        if not self._is_upgrade_method_needed(29, 29):
5✔
703
            return
×
704
        PR_TYPE_ONCHAIN = 0
5✔
705
        PR_TYPE_LN = 2
5✔
706
        requests = self.data.get('payment_requests', {})
5✔
707
        invoices = self.data.get('invoices', {})
5✔
708
        for d in [invoices, requests]:
5✔
709
            for key, item in list(d.items()):
5✔
710
                _type = item['type']
5✔
711
                if _type == PR_TYPE_ONCHAIN:
5✔
712
                    item['amount_sat'] = item.pop('amount')
5✔
713
                elif _type == PR_TYPE_LN:
×
714
                    amount_sat = item.pop('amount')
×
715
                    item['amount_msat'] = 1000 * amount_sat if amount_sat is not None else None
×
716
                    item.pop('exp')
×
717
                    item.pop('message')
×
718
                    item.pop('rhash')
×
719
                    item.pop('time')
×
720
                else:
721
                    raise Exception(f"unknown invoice type: {_type}")
×
722
        self.data['seed_version'] = 30
5✔
723

724
    def _convert_version_31(self):
5✔
725
        if not self._is_upgrade_method_needed(30, 30):
5✔
726
            return
×
727
        PR_TYPE_ONCHAIN = 0
5✔
728
        requests = self.data.get('payment_requests', {})
5✔
729
        invoices = self.data.get('invoices', {})
5✔
730
        for d in [invoices, requests]:
5✔
731
            for key, item in list(d.items()):
5✔
732
                if item['type'] == PR_TYPE_ONCHAIN:
5✔
733
                    item['amount_sat'] = item['amount_sat'] or 0
5✔
734
                    item['exp'] = item['exp'] or 0
5✔
735
                    item['time'] = item['time'] or 0
5✔
736
        self.data['seed_version'] = 31
5✔
737

738
    def _convert_version_32(self):
5✔
739
        if not self._is_upgrade_method_needed(31, 31):
5✔
740
            return
×
741
        PR_TYPE_ONCHAIN = 0
5✔
742
        invoices_old = self.data.get('invoices', {})
5✔
743
        invoices_new = {k: item for k, item in invoices_old.items()
5✔
744
                        if not (item['type'] == PR_TYPE_ONCHAIN and item['outputs'] is None)}
745
        self.data['invoices'] = invoices_new
5✔
746
        self.data['seed_version'] = 32
5✔
747

748
    def _convert_version_33(self):
5✔
749
        if not self._is_upgrade_method_needed(32, 32):
5✔
750
            return
×
751
        PR_TYPE_ONCHAIN = 0
5✔
752
        requests = self.data.get('payment_requests', {})
5✔
753
        invoices = self.data.get('invoices', {})
5✔
754
        for d in [invoices, requests]:
5✔
755
            for key, item in list(d.items()):
5✔
756
                if item['type'] == PR_TYPE_ONCHAIN:
5✔
757
                    item['height'] = item.get('height') or 0
5✔
758
        self.data['seed_version'] = 33
5✔
759

760
    def _convert_version_34(self):
5✔
761
        if not self._is_upgrade_method_needed(33, 33):
5✔
762
            return
×
763
        channels = self.data.get('channels', {})
5✔
764
        for key, item in channels.items():
5✔
765
            item['local_config']['upfront_shutdown_script'] = \
×
766
                item['local_config'].get('upfront_shutdown_script') or ""
767
            item['remote_config']['upfront_shutdown_script'] = \
×
768
                item['remote_config'].get('upfront_shutdown_script') or ""
769
        self.data['seed_version'] = 34
5✔
770

771
    def _convert_version_35(self):
5✔
772
        # same as 32, but for payment_requests
773
        if not self._is_upgrade_method_needed(34, 34):
5✔
774
            return
×
775
        PR_TYPE_ONCHAIN = 0
5✔
776
        requests_old = self.data.get('payment_requests', {})
5✔
777
        requests_new = {k: item for k, item in requests_old.items()
5✔
778
                        if not (item['type'] == PR_TYPE_ONCHAIN and item['outputs'] is None)}
779
        self.data['payment_requests'] = requests_new
5✔
780
        self.data['seed_version'] = 35
5✔
781

782
    def _convert_version_36(self):
5✔
783
        if not self._is_upgrade_method_needed(35, 35):
5✔
784
            return
×
785
        old_frozen_coins = self.data.get('frozen_coins', [])
5✔
786
        new_frozen_coins = {coin: True for coin in old_frozen_coins}
5✔
787
        self.data['frozen_coins'] = new_frozen_coins
5✔
788
        self.data['seed_version'] = 36
5✔
789

790
    def _convert_version_37(self):
5✔
791
        if not self._is_upgrade_method_needed(36, 36):
5✔
792
            return
×
793
        payments = self.data.get('lightning_payments', {})
5✔
794
        for k, v in list(payments.items()):
5✔
795
            amount_sat, direction, status = v
×
796
            amount_msat = amount_sat * 1000 if amount_sat is not None else None
×
797
            payments[k] = amount_msat, direction, status
×
798
        self.data['lightning_payments'] = payments
5✔
799
        self.data['seed_version'] = 37
5✔
800

801
    def _convert_version_38(self):
5✔
802
        if not self._is_upgrade_method_needed(37, 37):
5✔
803
            return
×
804
        PR_TYPE_ONCHAIN = 0
5✔
805
        PR_TYPE_LN = 2
5✔
806
        from .bitcoin import TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN
5✔
807
        max_sats = TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN
5✔
808
        requests = self.data.get('payment_requests', {})
5✔
809
        invoices = self.data.get('invoices', {})
5✔
810
        for d in [invoices, requests]:
5✔
811
            for key, item in list(d.items()):
5✔
812
                if item['type'] == PR_TYPE_ONCHAIN:
5✔
813
                    amount_sat = item['amount_sat']
5✔
814
                    if amount_sat == '!':
5✔
815
                        continue
×
816
                    if not (isinstance(amount_sat, int) and 0 <= amount_sat <= max_sats):
5✔
817
                        del d[key]
×
818
                elif item['type'] == PR_TYPE_LN:
×
819
                    amount_msat = item['amount_msat']
×
820
                    if not amount_msat:
×
821
                        continue
×
822
                    if not (isinstance(amount_msat, int) and 0 <= amount_msat <= max_sats * 1000):
×
823
                        del d[key]
×
824
        self.data['seed_version'] = 38
5✔
825

826
    def _convert_version_39(self):
5✔
827
        # this upgrade prevents initialization of lightning_privkey2 after lightning_xprv has been set
828
        if not self._is_upgrade_method_needed(38, 38):
5✔
829
            return
×
830
        self.data['imported_channel_backups'] = self.data.pop('channel_backups', {})
5✔
831
        self.data['seed_version'] = 39
5✔
832

833
    def _convert_version_40(self):
5✔
834
        # put 'seed_type' into keystores
835
        if not self._is_upgrade_method_needed(39, 39):
5✔
836
            return
×
837
        for ks_name in ('keystore', *['x{}/'.format(i) for i in range(1, 16)]):
5✔
838
            ks = self.data.get(ks_name, None)
5✔
839
            if ks is None: continue
5✔
840
            seed = ks.get('seed')
5✔
841
            if not seed: continue
5✔
842
            seed_type = None
5✔
843
            xpub = ks.get('xpub') or None
5✔
844
            if xpub:
5✔
845
                assert isinstance(xpub, str)
5✔
846
                if xpub[0:4] in ('xpub', 'tpub'):
5✔
847
                    seed_type = 'standard'
5✔
848
                elif xpub[0:4] in ('zpub', 'Zpub', 'vpub', 'Vpub'):
×
849
                    seed_type = 'segwit'
×
850
            elif ks.get('type') == 'old':
5✔
851
                seed_type = 'old'
5✔
852
            if seed_type is not None:
5✔
853
                ks['seed_type'] = seed_type
5✔
854
        self.data['seed_version'] = 40
5✔
855

856
    def _convert_version_41(self):
5✔
857
        # this is a repeat of upgrade 39, to fix wallet backup files (see #7339)
858
        if not self._is_upgrade_method_needed(40, 40):
5✔
859
            return
×
860
        imported_channel_backups = self.data.pop('channel_backups', {})
5✔
861
        imported_channel_backups.update(self.data.get('imported_channel_backups', {}))
5✔
862
        self.data['imported_channel_backups'] = imported_channel_backups
5✔
863
        self.data['seed_version'] = 41
5✔
864

865
    def _convert_version_42(self):
5✔
866
        # in OnchainInvoice['outputs'], convert values from None to 0
867
        if not self._is_upgrade_method_needed(41, 41):
5✔
868
            return
×
869
        PR_TYPE_ONCHAIN = 0
5✔
870
        requests = self.data.get('payment_requests', {})
5✔
871
        invoices = self.data.get('invoices', {})
5✔
872
        for d in [invoices, requests]:
5✔
873
            for key, item in list(d.items()):
5✔
874
                if item['type'] == PR_TYPE_ONCHAIN:
5✔
875
                    item['outputs'] = [(_type, addr, (val or 0))
5✔
876
                                       for _type, addr, val in item['outputs']]
877
        self.data['seed_version'] = 42
5✔
878

879
    def _convert_version_43(self):
5✔
880
        if not self._is_upgrade_method_needed(42, 42):
5✔
881
            return
×
882
        channels = self.data.pop('channels', {})
5✔
883
        for k, c in channels.items():
5✔
884
            log = c['log']
×
885
            c['fail_htlc_reasons'] = log.pop('fail_htlc_reasons', {})
×
886
            c['unfulfilled_htlcs'] = log.pop('unfulfilled_htlcs', {})
×
887
            log["1"]['unacked_updates'] = log.pop('unacked_local_updates2', {})
×
888
        self.data['channels'] = channels
5✔
889
        self.data['seed_version'] = 43
5✔
890

891
    def _convert_version_44(self):
5✔
892
        if not self._is_upgrade_method_needed(43, 43):
5✔
893
            return
×
894
        channels = self.data.get('channels', {})
5✔
895
        for key, item in channels.items():
5✔
896
            if bool(item.get('static_remotekey_enabled')):
×
897
                channel_type = ChannelType.OPTION_STATIC_REMOTEKEY
×
898
            else:
899
                channel_type = ChannelType(0)
×
900
            item.pop('static_remotekey_enabled', None)
×
901
            item['channel_type'] = channel_type
×
902
        self.data['seed_version'] = 44
5✔
903

904
    def _convert_version_45(self):
5✔
905
        from .lnaddr import lndecode
5✔
906
        if not self._is_upgrade_method_needed(44, 44):
5✔
907
            return
×
908
        swaps = self.data.get('submarine_swaps', {})
5✔
909
        for key, item in swaps.items():
5✔
910
            item['receive_address'] = None
×
911
        # note: we set height to zero
912
        # the new key for all requests is a wallet address, not done here
913
        for name in ['invoices', 'payment_requests']:
5✔
914
            invoices = self.data.get(name, {})
5✔
915
            for key, item in invoices.items():
5✔
916
                is_lightning = item['type'] == 2
5✔
917
                lightning_invoice = item['invoice'] if is_lightning else None
5✔
918
                outputs = item['outputs'] if not is_lightning else None
5✔
919
                bip70 = item['bip70'] if not is_lightning else None
5✔
920
                if is_lightning:
5✔
921
                    lnaddr = lndecode(item['invoice'])
×
922
                    amount_msat = lnaddr.get_amount_msat()
×
923
                    timestamp = lnaddr.date
×
924
                    exp_delay = lnaddr.get_expiry()
×
925
                    message = lnaddr.get_description()
×
926
                    height = 0
×
927
                else:
928
                    amount_sat = item['amount_sat']
5✔
929
                    amount_msat = amount_sat * 1000 if amount_sat not in [None, '!'] else amount_sat
5✔
930
                    message = item['message']
5✔
931
                    timestamp = item['time']
5✔
932
                    exp_delay = item['exp']
5✔
933
                    height = item['height']
5✔
934

935
                invoices[key] = {
5✔
936
                    'amount_msat':amount_msat,
937
                    'message':message,
938
                    'time':timestamp,
939
                    'exp':exp_delay,
940
                    'height':height,
941
                    'outputs':outputs,
942
                    'bip70':bip70,
943
                    'lightning_invoice':lightning_invoice,
944
                }
945
        self.data['seed_version'] = 45
5✔
946

947
    def _convert_invoices_keys(self, invoices):
5✔
948
        # recalc keys of outgoing on-chain invoices
949
        from .crypto import sha256d
5✔
950
        def get_id_from_onchain_outputs(raw_outputs, timestamp):
5✔
951
            outputs = [PartialTxOutput.from_legacy_tuple(*output) for output in raw_outputs]
5✔
952
            outputs_str = "\n".join(f"{txout.scriptpubkey.hex()}, {txout.value}" for txout in outputs)
5✔
953
            return sha256d(outputs_str + "%d" % timestamp).hex()[0:10]
5✔
954
        for key, item in list(invoices.items()):
5✔
955
            is_lightning = item['lightning_invoice'] is not None
5✔
956
            if is_lightning:
5✔
957
                continue
×
958
            outputs_raw = item['outputs']
5✔
959
            assert outputs_raw, outputs_raw
5✔
960
            timestamp = item['time']
5✔
961
            newkey = get_id_from_onchain_outputs(outputs_raw, timestamp)
5✔
962
            if newkey != key:
5✔
963
                invoices[newkey] = item
5✔
964
                del invoices[key]
5✔
965

966
    def _convert_version_46(self):
5✔
967
        if not self._is_upgrade_method_needed(45, 45):
5✔
968
            return
×
969
        invoices = self.data.get('invoices', {})
5✔
970
        self._convert_invoices_keys(invoices)
5✔
971
        self.data['seed_version'] = 46
5✔
972

973
    def _convert_version_47(self):
5✔
974
        from .lnaddr import lndecode
5✔
975
        if not self._is_upgrade_method_needed(46, 46):
5✔
976
            return
×
977
        # recalc keys of requests
978
        requests = self.data.get('payment_requests', {})
5✔
979
        for key, item in list(requests.items()):
5✔
980
            lnaddr = item.get('lightning_invoice')
5✔
981
            if lnaddr:
5✔
982
                lnaddr = lndecode(lnaddr)
×
983
                rhash = lnaddr.paymenthash.hex()
×
984
                if key != rhash:
×
985
                    requests[rhash] = item
×
986
                    del requests[key]
×
987
        self.data['seed_version'] = 47
5✔
988

989
    def _convert_version_48(self):
5✔
990
        # fix possible corruption of invoice amounts, see #7774
991
        if not self._is_upgrade_method_needed(47, 47):
5✔
992
            return
×
993
        invoices = self.data.get('invoices', {})
5✔
994
        for key, item in list(invoices.items()):
5✔
995
            if item['amount_msat'] == 1000 * "!":
×
996
                item['amount_msat'] = "!"
×
997
        self.data['seed_version'] = 48
5✔
998

999
    def _convert_version_49(self):
5✔
1000
        if not self._is_upgrade_method_needed(48, 48):
5✔
1001
            return
×
1002
        channels = self.data.get('channels', {})
5✔
1003
        legacy_chans = [chan_dict for chan_dict in channels.values()
5✔
1004
                        if chan_dict['channel_type'] == ChannelType.OPTION_LEGACY_CHANNEL]
1005
        if legacy_chans:
5✔
1006
            raise WalletFileException(
×
1007
                f"This wallet contains {len(legacy_chans)} lightning channels of type 'LEGACY'. "
1008
                f"These channels were created using unreleased development versions of Electrum "
1009
                f"before the first lightning-capable release of 4.0, and are not supported anymore. "
1010
                f"Please use Electrum 4.3.0 to open this wallet, close the channels, "
1011
                f"and delete them from the wallet."
1012
            )
1013
        self.data['seed_version'] = 49
5✔
1014

1015
    def _convert_version_50(self):
5✔
1016
        if not self._is_upgrade_method_needed(49, 49):
5✔
1017
            return
×
1018
        requests = self.data.get('payment_requests', {})
5✔
1019
        self._convert_invoices_keys(requests)
5✔
1020
        self.data['seed_version'] = 50
5✔
1021

1022
    def _convert_version_51(self):
5✔
1023
        from .lnaddr import lndecode
5✔
1024
        if not self._is_upgrade_method_needed(50, 50):
5✔
1025
            return
×
1026
        requests = self.data.get('payment_requests', {})
5✔
1027
        for key, item in list(requests.items()):
5✔
1028
            lightning_invoice = item.pop('lightning_invoice')
5✔
1029
            if lightning_invoice is None:
5✔
1030
                payment_hash = None
5✔
1031
            else:
1032
                lnaddr = lndecode(lightning_invoice)
×
1033
                payment_hash = lnaddr.paymenthash.hex()
×
1034
            item['payment_hash'] = payment_hash
5✔
1035
        self.data['seed_version'] = 51
5✔
1036

1037
    def _detect_insane_version_51(self) -> int:
5✔
1038
        """Returns 0 if file okay,
1039
        error code 1: multisig wallet has old_mpk
1040
        error code 2: multisig wallet has mixed Ypub/Zpub
1041
        """
1042
        assert self.get('seed_version') == 51
5✔
1043
        xpub_type = None
5✔
1044
        for ks_name in ['x{}/'.format(i) for i in range(1, 16)]:  # having any such field <=> multisig wallet
5✔
1045
            ks = self.data.get(ks_name, None)
5✔
1046
            if ks is None: continue
5✔
1047
            ks_type = ks.get('type')
5✔
1048
            if ks_type == "old":
5✔
1049
                return 1  # error
×
1050
            assert ks_type in ("bip32", "hardware"), f"unexpected {ks_type=}"
5✔
1051
            xpub = ks.get('xpub') or None
5✔
1052
            assert xpub is not None
5✔
1053
            assert isinstance(xpub, str)
5✔
1054
            if xpub_type is None:  # first iter
5✔
1055
                xpub_type = xpub[0:4]
5✔
1056
            if xpub[0:4] != xpub_type:
5✔
1057
                return 2  # error
×
1058
        # looks okay
1059
        return 0
5✔
1060

1061
    def _convert_version_52(self):
5✔
1062
        if not self._is_upgrade_method_needed(51, 51):
5✔
1063
            return
×
1064
        if (error_code := self._detect_insane_version_51()) != 0:
5✔
1065
            # should not get here; get_seed_version should have caught this
1066
            raise Exception(f'unsupported wallet file: version_51 with error {error_code}')
×
1067
        self.data['seed_version'] = 52
5✔
1068

1069
    def _convert_imported(self):
5✔
1070
        if not self._is_upgrade_method_needed(0, 13):
5✔
1071
            return
5✔
1072

1073
        # '/x' is the internal ID for imported accounts
1074
        d = self.get('accounts', {}).get('/x', {}).get('imported',{})
5✔
1075
        if not d:
5✔
1076
            return False
5✔
1077
        addresses = []
5✔
1078
        keypairs = {}
5✔
1079
        for addr, v in d.items():
5✔
1080
            pubkey, privkey = v
5✔
1081
            if privkey:
5✔
1082
                keypairs[pubkey] = privkey
5✔
1083
            else:
1084
                addresses.append(addr)
5✔
1085
        if addresses and keypairs:
5✔
1086
            raise WalletFileException('mixed addresses and privkeys')
×
1087
        elif addresses:
5✔
1088
            self.put('addresses', addresses)
5✔
1089
            self.put('accounts', None)
5✔
1090
        elif keypairs:
5✔
1091
            self.put('wallet_type', 'standard')
5✔
1092
            self.put('key_type', 'imported')
5✔
1093
            self.put('keypairs', keypairs)
5✔
1094
            self.put('accounts', None)
5✔
1095
        else:
1096
            raise WalletFileException('no addresses or privkeys')
×
1097

1098
    def _convert_account(self):
5✔
1099
        if not self._is_upgrade_method_needed(0, 13):
5✔
1100
            return
5✔
1101
        self.put('accounts', None)
5✔
1102

1103
    def _is_upgrade_method_needed(self, min_version, max_version):
5✔
1104
        assert min_version <= max_version
5✔
1105
        cur_version = self.get_seed_version()
5✔
1106
        if cur_version > max_version:
5✔
1107
            return False
5✔
1108
        elif cur_version < min_version:
5✔
1109
            raise WalletFileException(
×
1110
                'storage upgrade: unexpected version {} (should be {}-{})'
1111
                .format(cur_version, min_version, max_version))
1112
        else:
1113
            return True
5✔
1114

1115
    @locked
5✔
1116
    def get_seed_version(self):
5✔
1117
        seed_version = self.get('seed_version')
5✔
1118
        if not seed_version:
5✔
1119
            seed_version = OLD_SEED_VERSION if len(self.get('master_public_key','')) == 128 else NEW_SEED_VERSION
5✔
1120
        if seed_version > FINAL_SEED_VERSION:
5✔
1121
            raise WalletFileException('This version of Electrum is too old to open this wallet.\n'
×
1122
                                      '(highest supported storage version: {}, version of this file: {})'
1123
                                      .format(FINAL_SEED_VERSION, seed_version))
1124
        if seed_version == 14 and self.get('seed_type') == 'segwit':
5✔
1125
            self._raise_unsupported_version(seed_version)
×
1126
        if seed_version == 51 and self._detect_insane_version_51():
5✔
1127
            self._raise_unsupported_version(seed_version)
×
1128
        if seed_version >= 12:
5✔
1129
            return seed_version
5✔
1130
        if seed_version not in [OLD_SEED_VERSION, NEW_SEED_VERSION]:
5✔
1131
            self._raise_unsupported_version(seed_version)
×
1132
        return seed_version
5✔
1133

1134
    def _raise_unsupported_version(self, seed_version):
5✔
1135
        msg = f"Your wallet has an unsupported seed version: {seed_version}."
×
1136
        if seed_version in [5, 7, 8, 9, 10, 14]:
×
1137
            msg += "\n\nTo open this wallet, try 'git checkout seed_v%d'"%seed_version
×
1138
        if seed_version == 6:
×
1139
            # version 1.9.8 created v6 wallets when an incorrect seed was entered in the restore dialog
1140
            msg += '\n\nThis file was created because of a bug in version 1.9.8.'
×
1141
            if self.get('master_public_keys') is None and self.get('master_private_keys') is None and self.get('imported_keys') is None:
×
1142
                # pbkdf2 (at that time an additional dependency) was not included with the binaries, and wallet creation aborted.
1143
                msg += "\nIt does not contain any keys, and can safely be removed."
×
1144
            else:
1145
                # creation was complete if electrum was run from source
1146
                msg += "\nPlease open this file with Electrum 1.9.8, and move your coins to a new wallet."
×
1147
        if seed_version == 51:
×
1148
            error_code = self._detect_insane_version_51()
×
1149
            assert error_code != 0
×
1150
            msg += f" ({error_code=})"
×
1151
            if error_code == 1:
×
1152
                msg += "\nThis is a multisig wallet containing an old_mpk (pre-bip32 master public key)."
×
1153
                msg += "\nPlease contact us to help recover it by opening an issue on GitHub."
×
1154
            elif error_code == 2:
×
1155
                msg += ("\nThis is a multisig wallet containing mixed xpub/Ypub/Zpub."
×
1156
                        "\nThe script type is determined by the type of the first keystore."
1157
                        "\nTo recover, you should re-create the wallet with matching type "
1158
                        "(converted if needed) master keys."
1159
                        "\nOr you can contact us to help recover it by opening an issue on GitHub.")
1160
            else:
1161
                raise Exception(f"unexpected {error_code=}")
×
1162
            raise WalletFileExceptionVersion51(msg, should_report_crash=True)
×
1163
        # generic exception
1164
        raise WalletFileException(msg)
×
1165

1166
    def _add_db_creation_metadata(self):
5✔
1167
        # store this for debugging purposes
1168
        v = DBMetadata(
5✔
1169
            creation_timestamp=int(time.time()),
1170
            first_electrum_version_used=ELECTRUM_VERSION,
1171
        )
1172
        assert self.get("db_metadata", None) is None
5✔
1173
        self.put("db_metadata", v)
5✔
1174

1175
    def get_db_metadata(self) -> Optional[DBMetadata]:
5✔
1176
        # field only present for wallet files created with ver 4.4.0 or later
1177
        return self.get("db_metadata")
×
1178

1179
    @locked
5✔
1180
    def get_txi_addresses(self, tx_hash: str) -> List[str]:
5✔
1181
        """Returns list of is_mine addresses that appear as inputs in tx."""
1182
        assert isinstance(tx_hash, str)
5✔
1183
        return list(self.txi.get(tx_hash, {}).keys())
5✔
1184

1185
    @locked
5✔
1186
    def get_txo_addresses(self, tx_hash: str) -> List[str]:
5✔
1187
        """Returns list of is_mine addresses that appear as outputs in tx."""
1188
        assert isinstance(tx_hash, str)
5✔
1189
        return list(self.txo.get(tx_hash, {}).keys())
5✔
1190

1191
    @locked
5✔
1192
    def get_txi_addr(self, tx_hash: str, address: str) -> Iterable[Tuple[str, int]]:
5✔
1193
        """Returns an iterable of (prev_outpoint, value)."""
1194
        assert isinstance(tx_hash, str)
5✔
1195
        assert isinstance(address, str)
5✔
1196
        d = self.txi.get(tx_hash, {}).get(address, {})
5✔
1197
        return list(d.items())
5✔
1198

1199
    @locked
5✔
1200
    def get_txo_addr(self, tx_hash: str, address: str) -> Dict[int, Tuple[int, bool]]:
5✔
1201
        """Returns a dict: output_index -> (value, is_coinbase)."""
1202
        assert isinstance(tx_hash, str)
5✔
1203
        assert isinstance(address, str)
5✔
1204
        d = self.txo.get(tx_hash, {}).get(address, {})
5✔
1205
        return {int(n): (v, cb) for (n, (v, cb)) in d.items()}
5✔
1206

1207
    @modifier
5✔
1208
    def add_txi_addr(self, tx_hash: str, addr: str, ser: str, v: int) -> None:
5✔
1209
        assert isinstance(tx_hash, str)
5✔
1210
        assert isinstance(addr, str)
5✔
1211
        assert isinstance(ser, str)
5✔
1212
        assert isinstance(v, int)
5✔
1213
        if tx_hash not in self.txi:
5✔
1214
            self.txi[tx_hash] = {}
5✔
1215
        d = self.txi[tx_hash]
5✔
1216
        if addr not in d:
5✔
1217
            d[addr] = {}
5✔
1218
        d[addr][ser] = v
5✔
1219

1220
    @modifier
5✔
1221
    def add_txo_addr(self, tx_hash: str, addr: str, n: Union[int, str], v: int, is_coinbase: bool) -> None:
5✔
1222
        n = str(n)
5✔
1223
        assert isinstance(tx_hash, str)
5✔
1224
        assert isinstance(addr, str)
5✔
1225
        assert isinstance(n, str)
5✔
1226
        assert isinstance(v, int)
5✔
1227
        assert isinstance(is_coinbase, bool)
5✔
1228
        if tx_hash not in self.txo:
5✔
1229
            self.txo[tx_hash] = {}
5✔
1230
        d = self.txo[tx_hash]
5✔
1231
        if addr not in d:
5✔
1232
            d[addr] = {}
5✔
1233
        d[addr][n] = (v, is_coinbase)
5✔
1234

1235
    @locked
5✔
1236
    def list_txi(self) -> Sequence[str]:
5✔
1237
        return list(self.txi.keys())
5✔
1238

1239
    @locked
5✔
1240
    def list_txo(self) -> Sequence[str]:
5✔
1241
        return list(self.txo.keys())
5✔
1242

1243
    @modifier
5✔
1244
    def remove_txi(self, tx_hash: str) -> None:
5✔
1245
        assert isinstance(tx_hash, str)
5✔
1246
        self.txi.pop(tx_hash, None)
5✔
1247

1248
    @modifier
5✔
1249
    def remove_txo(self, tx_hash: str) -> None:
5✔
1250
        assert isinstance(tx_hash, str)
5✔
1251
        self.txo.pop(tx_hash, None)
5✔
1252

1253
    @locked
5✔
1254
    def list_spent_outpoints(self) -> Sequence[Tuple[str, str]]:
5✔
1255
        return [(h, n)
×
1256
                for h in self.spent_outpoints.keys()
1257
                for n in self.get_spent_outpoints(h)
1258
        ]
1259

1260
    @locked
5✔
1261
    def get_spent_outpoints(self, prevout_hash: str) -> Sequence[str]:
5✔
1262
        assert isinstance(prevout_hash, str)
5✔
1263
        return list(self.spent_outpoints.get(prevout_hash, {}).keys())
5✔
1264

1265
    @locked
5✔
1266
    def get_spent_outpoint(self, prevout_hash: str, prevout_n: Union[int, str]) -> Optional[str]:
5✔
1267
        assert isinstance(prevout_hash, str)
5✔
1268
        prevout_n = str(prevout_n)
5✔
1269
        return self.spent_outpoints.get(prevout_hash, {}).get(prevout_n)
5✔
1270

1271
    @modifier
5✔
1272
    def remove_spent_outpoint(self, prevout_hash: str, prevout_n: Union[int, str]) -> None:
5✔
1273
        assert isinstance(prevout_hash, str)
5✔
1274
        prevout_n = str(prevout_n)
5✔
1275
        self.spent_outpoints[prevout_hash].pop(prevout_n, None)
5✔
1276
        if not self.spent_outpoints[prevout_hash]:
5✔
1277
            self.spent_outpoints.pop(prevout_hash)
5✔
1278

1279
    @modifier
5✔
1280
    def set_spent_outpoint(self, prevout_hash: str, prevout_n: Union[int, str], tx_hash: str) -> None:
5✔
1281
        assert isinstance(prevout_hash, str)
5✔
1282
        assert isinstance(tx_hash, str)
5✔
1283
        prevout_n = str(prevout_n)
5✔
1284
        if prevout_hash not in self.spent_outpoints:
5✔
1285
            self.spent_outpoints[prevout_hash] = {}
5✔
1286
        self.spent_outpoints[prevout_hash][prevout_n] = tx_hash
5✔
1287

1288
    @modifier
5✔
1289
    def add_prevout_by_scripthash(self, scripthash: str, *, prevout: TxOutpoint, value: int) -> None:
5✔
1290
        assert isinstance(scripthash, str)
5✔
1291
        assert isinstance(prevout, TxOutpoint)
5✔
1292
        assert isinstance(value, int)
5✔
1293
        if scripthash not in self._prevouts_by_scripthash:
5✔
1294
            self._prevouts_by_scripthash[scripthash] = set()
5✔
1295
        self._prevouts_by_scripthash[scripthash].add((prevout.to_str(), value))
5✔
1296

1297
    @modifier
5✔
1298
    def remove_prevout_by_scripthash(self, scripthash: str, *, prevout: TxOutpoint, value: int) -> None:
5✔
1299
        assert isinstance(scripthash, str)
5✔
1300
        assert isinstance(prevout, TxOutpoint)
5✔
1301
        assert isinstance(value, int)
5✔
1302
        self._prevouts_by_scripthash[scripthash].discard((prevout.to_str(), value))
5✔
1303
        if not self._prevouts_by_scripthash[scripthash]:
5✔
1304
            self._prevouts_by_scripthash.pop(scripthash)
5✔
1305

1306
    @locked
5✔
1307
    def get_prevouts_by_scripthash(self, scripthash: str) -> Set[Tuple[TxOutpoint, int]]:
5✔
1308
        assert isinstance(scripthash, str)
5✔
1309
        prevouts_and_values = self._prevouts_by_scripthash.get(scripthash, set())
5✔
1310
        return {(TxOutpoint.from_str(prevout), value) for prevout, value in prevouts_and_values}
5✔
1311

1312
    @modifier
5✔
1313
    def add_transaction(self, tx_hash: str, tx: Transaction) -> None:
5✔
1314
        assert isinstance(tx_hash, str)
5✔
1315
        assert isinstance(tx, Transaction), tx
5✔
1316
        # note that tx might be a PartialTransaction
1317
        # serialize and de-serialize tx now. this might e.g. convert a complete PartialTx to a Tx
1318
        tx = tx_from_any(str(tx))
5✔
1319
        if not tx_hash:
5✔
1320
            raise Exception("trying to add tx to db without txid")
×
1321
        if tx_hash != tx.txid():
5✔
1322
            raise Exception(f"trying to add tx to db with inconsistent txid: {tx_hash} != {tx.txid()}")
×
1323
        # don't allow overwriting complete tx with partial tx
1324
        tx_we_already_have = self.transactions.get(tx_hash, None)
5✔
1325
        if tx_we_already_have is None or isinstance(tx_we_already_have, PartialTransaction):
5✔
1326
            self.transactions[tx_hash] = tx
5✔
1327

1328
    @modifier
5✔
1329
    def remove_transaction(self, tx_hash: str) -> Optional[Transaction]:
5✔
1330
        assert isinstance(tx_hash, str)
5✔
1331
        return self.transactions.pop(tx_hash, None)
5✔
1332

1333
    @locked
5✔
1334
    def get_transaction(self, tx_hash: Optional[str]) -> Optional[Transaction]:
5✔
1335
        if tx_hash is None:
5✔
1336
            return None
×
1337
        assert isinstance(tx_hash, str)
5✔
1338
        return self.transactions.get(tx_hash)
5✔
1339

1340
    @locked
5✔
1341
    def list_transactions(self) -> Sequence[str]:
5✔
1342
        return list(self.transactions.keys())
×
1343

1344
    @locked
5✔
1345
    def get_history(self) -> Sequence[str]:
5✔
1346
        return list(self.history.keys())
5✔
1347

1348
    def is_addr_in_history(self, addr: str) -> bool:
5✔
1349
        # does not mean history is non-empty!
1350
        assert isinstance(addr, str)
5✔
1351
        return addr in self.history
5✔
1352

1353
    @locked
5✔
1354
    def get_addr_history(self, addr: str) -> Sequence[Tuple[str, int]]:
5✔
1355
        assert isinstance(addr, str)
5✔
1356
        return self.history.get(addr, [])
5✔
1357

1358
    @modifier
5✔
1359
    def set_addr_history(self, addr: str, hist) -> None:
5✔
1360
        assert isinstance(addr, str)
5✔
1361
        self.history[addr] = hist
5✔
1362

1363
    @modifier
5✔
1364
    def remove_addr_history(self, addr: str) -> None:
5✔
1365
        assert isinstance(addr, str)
5✔
1366
        self.history.pop(addr, None)
5✔
1367

1368
    @locked
5✔
1369
    def list_verified_tx(self) -> Sequence[str]:
5✔
1370
        return list(self.verified_tx.keys())
×
1371

1372
    @locked
5✔
1373
    def get_verified_tx(self, txid: str) -> Optional[TxMinedInfo]:
5✔
1374
        assert isinstance(txid, str)
5✔
1375
        if txid not in self.verified_tx:
5✔
1376
            return None
5✔
1377
        height, timestamp, txpos, header_hash = self.verified_tx[txid]
5✔
1378
        return TxMinedInfo(height=height,
5✔
1379
                           conf=None,
1380
                           timestamp=timestamp,
1381
                           txpos=txpos,
1382
                           header_hash=header_hash)
1383

1384
    @modifier
5✔
1385
    def add_verified_tx(self, txid: str, info: TxMinedInfo):
5✔
1386
        assert isinstance(txid, str)
5✔
1387
        assert isinstance(info, TxMinedInfo)
5✔
1388
        self.verified_tx[txid] = (info.height, info.timestamp, info.txpos, info.header_hash)
5✔
1389

1390
    @modifier
5✔
1391
    def remove_verified_tx(self, txid: str):
5✔
1392
        assert isinstance(txid, str)
5✔
1393
        self.verified_tx.pop(txid, None)
5✔
1394

1395
    def is_in_verified_tx(self, txid: str) -> bool:
5✔
1396
        assert isinstance(txid, str)
5✔
1397
        return txid in self.verified_tx
5✔
1398

1399
    @modifier
5✔
1400
    def add_tx_fee_from_server(self, txid: str, fee_sat: Optional[int]) -> None:
5✔
1401
        assert isinstance(txid, str)
×
1402
        # note: when called with (fee_sat is None), rm currently saved value
1403
        if txid not in self.tx_fees:
×
1404
            self.tx_fees[txid] = TxFeesValue()
×
1405
        tx_fees_value = self.tx_fees[txid]
×
1406
        if tx_fees_value.is_calculated_by_us:
×
1407
            return
×
1408
        self.tx_fees[txid] = tx_fees_value._replace(fee=fee_sat, is_calculated_by_us=False)
×
1409

1410
    @modifier
5✔
1411
    def add_tx_fee_we_calculated(self, txid: str, fee_sat: Optional[int]) -> None:
5✔
1412
        assert isinstance(txid, str)
5✔
1413
        if fee_sat is None:
5✔
1414
            return
×
1415
        assert isinstance(fee_sat, int)
5✔
1416
        if txid not in self.tx_fees:
5✔
1417
            self.tx_fees[txid] = TxFeesValue()
×
1418
        self.tx_fees[txid] = self.tx_fees[txid]._replace(fee=fee_sat, is_calculated_by_us=True)
5✔
1419

1420
    @locked
5✔
1421
    def get_tx_fee(self, txid: str, *, trust_server: bool = False) -> Optional[int]:
5✔
1422
        assert isinstance(txid, str)
5✔
1423
        """Returns tx_fee."""
3✔
1424
        tx_fees_value = self.tx_fees.get(txid)
5✔
1425
        if tx_fees_value is None:
5✔
1426
            return None
×
1427
        if not trust_server and not tx_fees_value.is_calculated_by_us:
5✔
1428
            return None
5✔
1429
        return tx_fees_value.fee
5✔
1430

1431
    @modifier
5✔
1432
    def add_num_inputs_to_tx(self, txid: str, num_inputs: int) -> None:
5✔
1433
        assert isinstance(txid, str)
5✔
1434
        assert isinstance(num_inputs, int)
5✔
1435
        if txid not in self.tx_fees:
5✔
1436
            self.tx_fees[txid] = TxFeesValue()
5✔
1437
        self.tx_fees[txid] = self.tx_fees[txid]._replace(num_inputs=num_inputs)
5✔
1438

1439
    @locked
5✔
1440
    def get_num_all_inputs_of_tx(self, txid: str) -> Optional[int]:
5✔
1441
        assert isinstance(txid, str)
5✔
1442
        tx_fees_value = self.tx_fees.get(txid)
5✔
1443
        if tx_fees_value is None:
5✔
1444
            return None
×
1445
        return tx_fees_value.num_inputs
5✔
1446

1447
    @locked
5✔
1448
    def get_num_ismine_inputs_of_tx(self, txid: str) -> int:
5✔
1449
        assert isinstance(txid, str)
5✔
1450
        txins = self.txi.get(txid, {})
5✔
1451
        return sum([len(tupls) for addr, tupls in txins.items()])
5✔
1452

1453
    @modifier
5✔
1454
    def remove_tx_fee(self, txid: str) -> None:
5✔
1455
        assert isinstance(txid, str)
5✔
1456
        self.tx_fees.pop(txid, None)
5✔
1457

1458
    @locked
5✔
1459
    def num_change_addresses(self) -> int:
5✔
1460
        return len(self.change_addresses)
5✔
1461

1462
    @locked
5✔
1463
    def num_receiving_addresses(self) -> int:
5✔
1464
        return len(self.receiving_addresses)
5✔
1465

1466
    @locked
5✔
1467
    def get_change_addresses(self, *, slice_start=None, slice_stop=None) -> List[str]:
5✔
1468
        # note: slicing makes a shallow copy
1469
        return self.change_addresses[slice_start:slice_stop]
5✔
1470

1471
    @locked
5✔
1472
    def get_receiving_addresses(self, *, slice_start=None, slice_stop=None) -> List[str]:
5✔
1473
        # note: slicing makes a shallow copy
1474
        return self.receiving_addresses[slice_start:slice_stop]
5✔
1475

1476
    @modifier
5✔
1477
    def add_change_address(self, addr: str) -> None:
5✔
1478
        assert isinstance(addr, str)
5✔
1479
        self._addr_to_addr_index[addr] = (1, len(self.change_addresses))
5✔
1480
        self.change_addresses.append(addr)
5✔
1481

1482
    @modifier
5✔
1483
    def add_receiving_address(self, addr: str) -> None:
5✔
1484
        assert isinstance(addr, str)
5✔
1485
        self._addr_to_addr_index[addr] = (0, len(self.receiving_addresses))
5✔
1486
        self.receiving_addresses.append(addr)
5✔
1487

1488
    @locked
5✔
1489
    def get_address_index(self, address: str) -> Optional[Sequence[int]]:
5✔
1490
        assert isinstance(address, str)
5✔
1491
        return self._addr_to_addr_index.get(address)
5✔
1492

1493
    @modifier
5✔
1494
    def add_imported_address(self, addr: str, d: dict) -> None:
5✔
1495
        assert isinstance(addr, str)
5✔
1496
        self.imported_addresses[addr] = d
5✔
1497

1498
    @modifier
5✔
1499
    def remove_imported_address(self, addr: str) -> None:
5✔
1500
        assert isinstance(addr, str)
5✔
1501
        self.imported_addresses.pop(addr)
5✔
1502

1503
    @locked
5✔
1504
    def has_imported_address(self, addr: str) -> bool:
5✔
1505
        assert isinstance(addr, str)
5✔
1506
        return addr in self.imported_addresses
5✔
1507

1508
    @locked
5✔
1509
    def get_imported_addresses(self) -> Sequence[str]:
5✔
1510
        return list(sorted(self.imported_addresses.keys()))
5✔
1511

1512
    @locked
5✔
1513
    def get_imported_address(self, addr: str) -> Optional[dict]:
5✔
1514
        assert isinstance(addr, str)
5✔
1515
        return self.imported_addresses.get(addr)
5✔
1516

1517
    def load_addresses(self, wallet_type):
5✔
1518
        """ called from Abstract_Wallet.__init__ """
1519
        if wallet_type == 'imported':
5✔
1520
            self.imported_addresses = self.get_dict('addresses')  # type: Dict[str, dict]
5✔
1521
        else:
1522
            self.get_dict('addresses')
5✔
1523
            for name in ['receiving', 'change']:
5✔
1524
                if name not in self.data['addresses']:
5✔
1525
                    self.data['addresses'][name] = []
5✔
1526
            self.change_addresses = self.data['addresses']['change']
5✔
1527
            self.receiving_addresses = self.data['addresses']['receiving']
5✔
1528
            self._addr_to_addr_index = {}  # type: Dict[str, Sequence[int]]  # key: address, value: (is_change, index)
5✔
1529
            for i, addr in enumerate(self.receiving_addresses):
5✔
1530
                self._addr_to_addr_index[addr] = (0, i)
5✔
1531
            for i, addr in enumerate(self.change_addresses):
5✔
1532
                self._addr_to_addr_index[addr] = (1, i)
5✔
1533

1534
    @profiler
5✔
1535
    def _load_transactions(self):
5✔
1536
        self.data = StoredDict(self.data, self, [])
5✔
1537
        # references in self.data
1538
        # TODO make all these private
1539
        # txid -> address -> prev_outpoint -> value
1540
        self.txi = self.get_dict('txi')                          # type: Dict[str, Dict[str, Dict[str, int]]]
5✔
1541
        # txid -> address -> output_index -> (value, is_coinbase)
1542
        self.txo = self.get_dict('txo')                          # type: Dict[str, Dict[str, Dict[str, Tuple[int, bool]]]]
5✔
1543
        self.transactions = self.get_dict('transactions')        # type: Dict[str, Transaction]
5✔
1544
        self.spent_outpoints = self.get_dict('spent_outpoints')  # txid -> output_index -> next_txid
5✔
1545
        self.history = self.get_dict('addr_history')             # address -> list of (txid, height)
5✔
1546
        self.verified_tx = self.get_dict('verified_tx3')         # txid -> (height, timestamp, txpos, header_hash)
5✔
1547
        self.tx_fees = self.get_dict('tx_fees')                  # type: Dict[str, TxFeesValue]
5✔
1548
        # scripthash -> set of (outpoint, value)
1549
        self._prevouts_by_scripthash = self.get_dict('prevouts_by_scripthash')  # type: Dict[str, Set[Tuple[str, int]]]
5✔
1550
        # remove unreferenced tx
1551
        for tx_hash in list(self.transactions.keys()):
5✔
1552
            if not self.get_txi_addresses(tx_hash) and not self.get_txo_addresses(tx_hash):
5✔
1553
                self.logger.info(f"removing unreferenced tx: {tx_hash}")
5✔
1554
                self.transactions.pop(tx_hash)
5✔
1555
        # remove unreferenced outpoints
1556
        for prevout_hash in self.spent_outpoints.keys():
5✔
1557
            d = self.spent_outpoints[prevout_hash]
5✔
1558
            for prevout_n, spending_txid in list(d.items()):
5✔
1559
                if spending_txid not in self.transactions:
5✔
1560
                    self.logger.info("removing unreferenced spent outpoint")
5✔
1561
                    d.pop(prevout_n)
5✔
1562

1563
    @modifier
5✔
1564
    def clear_history(self):
5✔
1565
        self.txi.clear()
×
1566
        self.txo.clear()
×
1567
        self.spent_outpoints.clear()
×
1568
        self.transactions.clear()
×
1569
        self.history.clear()
×
1570
        self.verified_tx.clear()
×
1571
        self.tx_fees.clear()
×
1572
        self._prevouts_by_scripthash.clear()
×
1573

1574
    def _should_convert_to_stored_dict(self, key) -> bool:
5✔
1575
        if key == 'keystore':
5✔
1576
            return False
5✔
1577
        multisig_keystore_names = [('x%d/' % i) for i in range(1, 16)]
5✔
1578
        if key in multisig_keystore_names:
5✔
1579
            return False
5✔
1580
        return True
5✔
1581

1582
    def is_ready_to_be_used_by_wallet(self):
5✔
1583
        return not self.requires_upgrade() and self._called_after_upgrade_tasks
5✔
1584

1585
    def split_accounts(self, root_path):
5✔
1586
        from .storage import WalletStorage
×
1587
        out = []
×
1588
        result = self.get_split_accounts()
×
1589
        for data in result:
×
1590
            path = root_path + '.' + data['suffix']
×
1591
            storage = WalletStorage(path)
×
1592
            db = WalletDB(json.dumps(data), storage=storage, manual_upgrades=False)
×
1593
            db._called_after_upgrade_tasks = False
×
1594
            db.upgrade()
×
1595
            db.write()
×
1596
            out.append(path)
×
1597
        return out
×
1598

1599
    def get_action(self):
5✔
1600
        action = run_hook('get_action', self)
5✔
1601
        return action
5✔
1602

1603
    def load_plugins(self):
5✔
1604
        wallet_type = self.get('wallet_type')
5✔
1605
        if wallet_type in plugin_loaders:
5✔
1606
            plugin_loaders[wallet_type]()
×
1607

1608
    def set_keystore_encryption(self, enable):
5✔
1609
        self.put('use_encryption', enable)
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