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

spesmilo / electrum / 5735552722403328

16 May 2025 10:28AM UTC coverage: 59.722% (+0.002%) from 59.72%
5735552722403328

Pull #9833

CirrusCI

f321x
make lightning dns seed fetching async
Pull Request #9833: dns: use async dnspython interface

22 of 50 new or added lines in 7 files covered. (44.0%)

1107 existing lines in 11 files now uncovered.

21549 of 36082 relevant lines covered (59.72%)

2.39 hits per line

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

44.25
/electrum/commands.py
1
#!/usr/bin/env python
2
#
3
# Electrum - lightweight Bitcoin client
4
# Copyright (C) 2011 thomasv@gitorious
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 io
4✔
26
import sys
4✔
27
import datetime
4✔
28
import argparse
4✔
29
import json
4✔
30
import ast
4✔
31
import base64
4✔
32
import asyncio
4✔
33
import inspect
4✔
34
from collections import defaultdict
4✔
35
from functools import wraps, partial
4✔
36
from itertools import repeat
4✔
37
from decimal import Decimal, InvalidOperation
4✔
38
from typing import Optional, TYPE_CHECKING, Dict, List
4✔
39
import os
4✔
40
import re
4✔
41

42
import electrum_ecc as ecc
4✔
43

44
from . import util
4✔
45
from .lnmsg import OnionWireSerializer
4✔
46
from .logging import Logger
4✔
47
from .onion_message import create_blinded_path, send_onion_message_to
4✔
48
from .util import (bfh, json_decode, json_normalize, is_hash256_str, is_hex_str, to_bytes,
4✔
49
                   parse_max_spend, to_decimal, UserFacingException, InvalidPassword)
50

51
from . import bitcoin
4✔
52
from .bitcoin import is_address,  hash_160, COIN
4✔
53
from .bip32 import BIP32Node
4✔
54
from .i18n import _
4✔
55
from .transaction import (Transaction, multisig_script, TxOutput, PartialTransaction, PartialTxOutput,
4✔
56
                          tx_from_any, PartialTxInput, TxOutpoint)
57
from . import transaction
4✔
58
from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
4✔
59
from .synchronizer import Notifier
4✔
60
from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text, Deterministic_Wallet, BumpFeeStrategy, Imported_Wallet
4✔
61
from .address_synchronizer import TX_HEIGHT_LOCAL
4✔
62
from .mnemonic import Mnemonic
4✔
63
from .lnutil import SENT, RECEIVED
4✔
64
from .lnutil import LnFeatures
4✔
65
from .lntransport import extract_nodeid
4✔
66
from .lnutil import channel_id_from_funding_tx
4✔
67
from .plugin import run_hook, DeviceMgr, Plugins
4✔
68
from .version import ELECTRUM_VERSION
4✔
69
from .simple_config import SimpleConfig
4✔
70
from .invoices import Invoice
4✔
71
from .fee_policy import FeePolicy
4✔
72
from . import GuiImportError
4✔
73
from . import crypto
4✔
74
from . import constants
4✔
75
from . import descriptor
4✔
76

77
if TYPE_CHECKING:
4✔
78
    from .network import Network
×
79
    from .daemon import Daemon
×
80

81

82
known_commands = {}  # type: Dict[str, Command]
4✔
83

84

85
class NotSynchronizedException(UserFacingException):
4✔
86
    pass
4✔
87

88

89
def satoshis_or_max(amount):
4✔
90
    return satoshis(amount) if not parse_max_spend(amount) else amount
4✔
91

92

93
def satoshis(amount):
4✔
94
    # satoshi conversion must not be performed by the parser
95
    return int(COIN*to_decimal(amount)) if amount is not None else None
4✔
96

97

98
def format_satoshis(x):
4✔
99
    return str(to_decimal(x)/COIN) if x is not None else None
×
100

101

102
class Command:
4✔
103
    def __init__(self, func, name, s):
4✔
104
        self.name = name
4✔
105
        self.requires_network = 'n' in s
4✔
106
        self.requires_wallet = 'w' in s
4✔
107
        self.requires_password = 'p' in s
4✔
108
        self.requires_lightning = 'l' in s
4✔
109
        self.parse_docstring(func.__doc__)
4✔
110
        varnames = func.__code__.co_varnames[1:func.__code__.co_argcount]
4✔
111
        self.defaults = func.__defaults__
4✔
112
        if self.defaults:
4✔
113
            n = len(self.defaults)
4✔
114
            self.params = list(varnames[:-n])
4✔
115
            self.options = list(varnames[-n:])
4✔
116
        else:
117
            self.params = list(varnames)
4✔
118
            self.options = []
4✔
119
            self.defaults = []
4✔
120

121
        # sanity checks
122
        if self.requires_password:
4✔
123
            assert self.requires_wallet
4✔
124
        for varname in ('wallet_path', 'wallet'):
4✔
125
            if varname in varnames:
4✔
126
                assert varname in self.options, f"cmd: {self.name}: {varname} not in options {self.options}"
4✔
127
        assert not ('wallet_path' in varnames and 'wallet' in varnames)
4✔
128
        if self.requires_wallet:
4✔
129
            assert 'wallet' in varnames
4✔
130

131
    def parse_docstring(self, docstring):
4✔
132
        docstring = docstring or ''
4✔
133
        self.description = docstring
4✔
134
        self.arg_descriptions = {}
4✔
135
        self.arg_types = {}
4✔
136
        for x in re.finditer(r'arg:(.*?):(.*?):(.*)$', docstring, flags=re.MULTILINE):
4✔
137
            self.arg_descriptions[x.group(2)] = x.group(3)
4✔
138
            self.arg_types[x.group(2)] = x.group(1)
4✔
139
            self.description = self.description.replace(x.group(), '')
4✔
140

141

142
def command(s):
4✔
143
    def decorator(func):
4✔
144
        if hasattr(func, '__wrapped__'):
4✔
145
            # plugin command function
146
            name = func.plugin_name + '_' + func.__name__
×
147
            known_commands[name] = Command(func.__wrapped__, name, s)
×
148
        else:
149
            # regular command function
150
            name = func.__name__
4✔
151
            known_commands[name] = Command(func, name, s)
4✔
152

153
        @wraps(func)
4✔
154
        async def func_wrapper(*args, **kwargs):
4✔
155
            cmd_runner = args[0]  # type: Commands
4✔
156
            cmd = known_commands[name]  # type: Command
4✔
157
            password = kwargs.get('password')
4✔
158
            daemon = cmd_runner.daemon
4✔
159
            if daemon:
4✔
160
                if 'wallet_path' in cmd.options and kwargs.get('wallet_path') is None:
4✔
161
                    kwargs['wallet_path'] = daemon.config.get_wallet_path()
×
162
                if cmd.requires_wallet and kwargs.get('wallet') is None:
4✔
163
                    kwargs['wallet'] = daemon.config.get_wallet_path()
×
164
                if 'wallet' in cmd.options:
4✔
165
                    wallet = kwargs.get('wallet', None)
4✔
166
                    if isinstance(wallet, str):
4✔
167
                        wallet = daemon.get_wallet(wallet)
4✔
168
                        if wallet is None:
4✔
169
                            raise UserFacingException('wallet not loaded')
×
170
                        kwargs['wallet'] = wallet
4✔
171
                    if cmd.requires_password and password is None and wallet.has_password():
4✔
172
                        password = wallet.get_unlocked_password()
×
173
                        if password:
×
174
                            kwargs['password'] = password
×
175
                        else:
176
                            raise UserFacingException('Password required. Unlock the wallet, or add a --password option to your command')
×
177
            wallet = kwargs.get('wallet')  # type: Optional[Abstract_Wallet]
4✔
178
            if cmd.requires_wallet and not wallet:
4✔
179
                raise UserFacingException('wallet not loaded')
×
180
            if cmd.requires_password and wallet.has_password():
4✔
181
                if password is None:
4✔
182
                    raise UserFacingException('Password required')
×
183
                try:
4✔
184
                    wallet.check_password(password)
4✔
185
                except InvalidPassword as e:
×
186
                    raise UserFacingException(str(e)) from None
×
187
            if cmd.requires_lightning and (not wallet or not wallet.has_lightning()):
4✔
188
                raise UserFacingException('Lightning not enabled in this wallet')
×
189
            return await func(*args, **kwargs)
4✔
190
        return func_wrapper
4✔
191
    return decorator
4✔
192

193

194
class Commands(Logger):
4✔
195

196
    def __init__(self, *, config: 'SimpleConfig',
4✔
197
                 network: 'Network' = None,
198
                 daemon: 'Daemon' = None, callback=None):
199
        Logger.__init__(self)
4✔
200
        self.config = config
4✔
201
        self.daemon = daemon
4✔
202
        self.network = network
4✔
203
        self._callback = callback
4✔
204

205
    def _run(self, method, args, password_getter=None, **kwargs):
4✔
206
        """This wrapper is called from unit tests and the Qt python console."""
207
        cmd = known_commands[method]
×
208
        password = kwargs.get('password', None)
×
209
        wallet = kwargs.get('wallet', None)
×
210
        if (cmd.requires_password and wallet and wallet.has_password()
×
211
                and password is None):
212
            password = password_getter()
×
213
            if password is None:
×
214
                return
×
215

216
        f = getattr(self, method)
×
217
        if cmd.requires_password:
×
218
            kwargs['password'] = password
×
219

220
        if 'wallet' in kwargs:
×
221
            sig = inspect.signature(f)
×
222
            if 'wallet' not in sig.parameters:
×
223
                kwargs.pop('wallet')
×
224

225
        coro = f(*args, **kwargs)
×
226
        fut = asyncio.run_coroutine_threadsafe(coro, util.get_asyncio_loop())
×
227
        result = fut.result()
×
228

229
        if self._callback:
×
230
            self._callback()
×
231
        return result
×
232

233
    @command('')
4✔
234
    async def commands(self):
4✔
235
        """List of commands"""
236
        return ' '.join(sorted(known_commands.keys()))
×
237

238
    @command('n')
4✔
239
    async def getinfo(self):
4✔
240
        """ network info """
241
        net_params = self.network.get_parameters()
×
242
        response = {
×
243
            'network': constants.net.NET_NAME,
244
            'path': self.network.config.path,
245
            'server': net_params.server.host,
246
            'blockchain_height': self.network.get_local_height(),
247
            'server_height': self.network.get_server_height(),
248
            'spv_nodes': len(self.network.get_interfaces()),
249
            'connected': self.network.is_connected(),
250
            'auto_connect': net_params.auto_connect,
251
            'version': ELECTRUM_VERSION,
252
            'default_wallet': self.config.get_wallet_path(),
253
            'fee_estimates': self.network.fee_estimates.get_data()
254
        }
255
        return response
×
256

257
    @command('n')
4✔
258
    async def stop(self):
4✔
259
        """Stop daemon"""
260
        await self.daemon.stop()
×
261
        return "Daemon stopped"
×
262

263
    @command('n')
4✔
264
    async def list_wallets(self):
4✔
265
        """List wallets open in daemon"""
266
        return [
×
267
            {
268
                'path': path,
269
                'synchronized': w.is_up_to_date(),
270
                'unlocked': w.has_password() and (w.get_unlocked_password() is not None),
271
            }
272
            for path, w in self.daemon.get_wallets().items()
273
        ]
274

275
    @command('n')
4✔
276
    async def load_wallet(self, wallet_path=None, password=None):
4✔
277
        """
278
        Load the wallet in memory
279
        """
280
        wallet = self.daemon.load_wallet(wallet_path, password, upgrade=True)
4✔
281
        if wallet is None:
4✔
282
            raise UserFacingException('could not load wallet')
×
283
        run_hook('load_wallet', wallet, None)
4✔
284

285
    @command('n')
4✔
286
    async def close_wallet(self, wallet_path=None):
4✔
287
        """Close wallet"""
288
        return await self.daemon._stop_wallet(wallet_path)
×
289

290
    @command('')
4✔
291
    async def create(self, passphrase=None, password=None, encrypt_file=True, seed_type=None, wallet_path=None):
4✔
292
        """Create a new wallet.
293
        If you want to be prompted for an argument, type '?' or ':' (concealed)
294

295
        arg:str:passphrase:Seed extension
296
        arg:str:seed_type:The type of wallet to create, e.g. 'standard' or 'segwit'
297
        arg:bool:encrypt_file:Whether the file on disk should be encrypted with the provided password
298
        """
299
        d = create_new_wallet(
×
300
            path=wallet_path,
301
            passphrase=passphrase,
302
            password=password,
303
            encrypt_file=encrypt_file,
304
            seed_type=seed_type,
305
            config=self.config)
306
        return {
×
307
            'seed': d['seed'],
308
            'path': d['wallet'].storage.path,
309
            'msg': d['msg'],
310
        }
311

312
    @command('')
4✔
313
    async def restore(self, text, passphrase=None, password=None, encrypt_file=True, wallet_path=None):
4✔
314
        """Restore a wallet from text. Text can be a seed phrase, a master
315
        public key, a master private key, a list of bitcoin addresses
316
        or bitcoin private keys.
317
        If you want to be prompted for an argument, type '?' or ':' (concealed)
318

319
        arg:str:text:seed phrase
320
        arg:str:passphrase:Seed extension
321
        arg:bool:encrypt_file:Whether the file on disk should be encrypted with the provided password
322
        """
323
        # TODO create a separate command that blocks until wallet is synced
324
        d = restore_wallet_from_text(
×
325
            text,
326
            path=wallet_path,
327
            passphrase=passphrase,
328
            password=password,
329
            encrypt_file=encrypt_file,
330
            config=self.config)
331
        return {
×
332
            'path': d['wallet'].storage.path,
333
            'msg': d['msg'],
334
        }
335

336
    @command('wp')
4✔
337
    async def password(self, password=None, new_password=None, encrypt_file=None, wallet: Abstract_Wallet = None):
4✔
338
        """
339
        Change wallet password.
340

341
        arg:bool:encrypt_file:Whether the file on disk should be encrypted with the provided password (default=true)
342
        arg:str:new_password:New Password
343
        """
344
        if wallet.storage.is_encrypted_with_hw_device() and new_password:
×
345
            raise UserFacingException("Can't change the password of a wallet encrypted with a hw device.")
×
346
        if encrypt_file is None:
×
347
            if not password and new_password:
×
348
                # currently no password, setting one now: we encrypt by default
349
                encrypt_file = True
×
350
            else:
351
                encrypt_file = wallet.storage.is_encrypted()
×
352
        wallet.update_password(password, new_password, encrypt_storage=encrypt_file)
×
353
        wallet.save_db()
×
354
        return {'password':wallet.has_password()}
×
355

356
    @command('w')
4✔
357
    async def get(self, key, wallet: Abstract_Wallet = None):
4✔
358
        """
359
        Return item from wallet storage
360

361
        arg:str:key:storage key
362
        """
363
        return wallet.db.get(key)
×
364

365
    @command('')
4✔
366
    async def getconfig(self, key):
4✔
367
        """Return the current value of a configuration variable.
368

369
        arg:str:key:name of the configuration variable
370
        """
371
        if Plugins.is_plugin_enabler_config_key(key):
×
372
            return self.config.get(key)
×
373
        else:
374
            cv = self.config.cv.from_key(key)
×
375
            return cv.get()
×
376

377
    @classmethod
4✔
378
    def _setconfig_normalize_value(cls, key, value):
4✔
379
        if key not in (SimpleConfig.RPC_USERNAME.key(), SimpleConfig.RPC_PASSWORD.key()):
4✔
380
            value = json_decode(value)
4✔
381
            # call literal_eval for backward compatibility (see #4225)
382
            try:
4✔
383
                value = ast.literal_eval(value)
4✔
384
            except Exception:
4✔
385
                pass
4✔
386
        return value
4✔
387

388
    @command('')
4✔
389
    async def setconfig(self, key, value):
4✔
390
        """
391
        Set a configuration variable.
392

393
        arg:str:key:name of the configuration variable
394
        arg:str:value:value. may be a string or a Python expression.
395
        """
396
        value = self._setconfig_normalize_value(key, value)
×
397
        if self.daemon and key == SimpleConfig.RPC_USERNAME.key():
×
398
            self.daemon.commands_server.rpc_user = value
×
399
        if self.daemon and key == SimpleConfig.RPC_PASSWORD.key():
×
400
            self.daemon.commands_server.rpc_password = value
×
401
        if Plugins.is_plugin_enabler_config_key(key):
×
402
            self.config.set_key(key, value)
×
403
        else:
404
            cv = self.config.cv.from_key(key)
×
405
            cv.set(value)
×
406

407
    @command('')
4✔
408
    async def listconfig(self):
4✔
409
        """Returns the list of all configuration variables. """
410
        return self.config.list_config_vars()
×
411

412
    @command('')
4✔
413
    async def helpconfig(self, key):
4✔
414
        """Returns help about a configuration variable.
415

416
        arg:str:key:name of the configuration variable
417
        """
418
        cv = self.config.cv.from_key(key)
×
419
        short = cv.get_short_desc()
×
420
        long = cv.get_long_desc()
×
421
        if short and long:
×
422
            return short + "\n---\n\n" + long
×
423
        elif short or long:
×
424
            return short or long
×
425
        else:
426
            return f"No description available for '{key}'"
×
427

428
    @command('')
4✔
429
    async def make_seed(self, nbits=None, language=None, seed_type=None):
4✔
430
        """
431
        Create a seed
432

433
        arg:int:nbits:Number of bits of entropy
434
        arg:str:seed_type:The type of seed to create, e.g. 'standard' or 'segwit'
435
        arg:str:language:Default language for wordlist
436
        """
437
        s = Mnemonic(language).make_seed(seed_type=seed_type, num_bits=nbits)
×
438
        return s
×
439

440
    @command('n')
4✔
441
    async def getaddresshistory(self, address):
4✔
442
        """
443
        Return the transaction history of any address. Note: This is a
444
        walletless server query, results are not checked by SPV.
445

446
        arg:str:address:Bitcoin address
447
        """
448
        sh = bitcoin.address_to_scripthash(address)
×
449
        return await self.network.get_history_for_scripthash(sh)
×
450

451
    @command('wp')
4✔
452
    async def unlock(self, wallet: Abstract_Wallet = None, password=None):
4✔
453
        """Unlock the wallet (store the password in memory)."""
454
        wallet.unlock(password)
×
455

456
    @command('w')
4✔
457
    async def listunspent(self, wallet: Abstract_Wallet = None):
4✔
458
        """List unspent outputs. Returns the list of unspent transaction
459
        outputs in your wallet."""
460
        coins = []
×
461
        for txin in wallet.get_utxos():
×
462
            d = txin.to_json()
×
463
            v = d.pop("value_sats")
×
464
            d["value"] = str(to_decimal(v)/COIN) if v is not None else None
×
465
            coins.append(d)
×
466
        return coins
×
467

468
    @command('n')
4✔
469
    async def getaddressunspent(self, address):
4✔
470
        """
471
        Returns the UTXO list of any address. Note: This
472
        is a walletless server query, results are not checked by SPV.
473

474
        arg:str:address:Bitcoin address
475
        """
476
        sh = bitcoin.address_to_scripthash(address)
×
477
        return await self.network.listunspent_for_scripthash(sh)
×
478

479
    @command('')
4✔
480
    async def serialize(self, jsontx):
4✔
481
        """Create a signed raw transaction from a json tx template.
482

483
        Example value for "jsontx" arg: {
484
            "inputs": [
485
                {"prevout_hash": "9d221a69ca3997cbeaf5624d723e7dc5f829b1023078c177d37bdae95f37c539", "prevout_n": 1,
486
                 "value_sats": 1000000, "privkey": "p2wpkh:cVDXzzQg6RoCTfiKpe8MBvmm5d5cJc6JLuFApsFDKwWa6F5TVHpD"}
487
            ],
488
            "outputs": [
489
                {"address": "tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd", "value_sats": 990000}
490
            ]
491
        }
492
        :arg:json:jsontx:Transaction in json
493
        """
494
        keypairs = {}
4✔
495
        inputs = []  # type: List[PartialTxInput]
4✔
496
        locktime = jsontx.get('locktime', 0)
4✔
497
        for txin_idx, txin_dict in enumerate(jsontx.get('inputs')):
4✔
498
            if txin_dict.get('prevout_hash') is not None and txin_dict.get('prevout_n') is not None:
4✔
499
                prevout = TxOutpoint(txid=bfh(txin_dict['prevout_hash']), out_idx=int(txin_dict['prevout_n']))
4✔
500
            elif txin_dict.get('output'):
×
501
                prevout = TxOutpoint.from_str(txin_dict['output'])
×
502
            else:
503
                raise UserFacingException(f"missing prevout for txin {txin_idx}")
×
504
            txin = PartialTxInput(prevout=prevout)
4✔
505
            try:
4✔
506
                txin._trusted_value_sats = int(txin_dict.get('value') or txin_dict['value_sats'])
4✔
507
            except KeyError:
×
508
                raise UserFacingException(f"missing 'value_sats' field for txin {txin_idx}")
×
509
            nsequence = txin_dict.get('nsequence', None)
4✔
510
            if nsequence is not None:
4✔
511
                txin.nsequence = nsequence
4✔
512
            sec = txin_dict.get('privkey')
4✔
513
            if sec:
4✔
514
                txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)
4✔
515
                pubkey = ecc.ECPrivkey(privkey).get_public_key_bytes(compressed=compressed)
4✔
516
                keypairs[pubkey] = privkey
4✔
517
                desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=txin_type)
4✔
518
                txin.script_descriptor = desc
4✔
519
            inputs.append(txin)
4✔
520

521
        outputs = []  # type: List[PartialTxOutput]
4✔
522
        for txout_idx, txout_dict in enumerate(jsontx.get('outputs')):
4✔
523
            try:
4✔
524
                txout_addr = txout_dict['address']
4✔
525
            except KeyError:
×
526
                raise UserFacingException(f"missing 'address' field for txout {txout_idx}")
×
527
            try:
4✔
528
                txout_val = int(txout_dict.get('value') or txout_dict['value_sats'])
4✔
529
            except KeyError:
×
530
                raise UserFacingException(f"missing 'value_sats' field for txout {txout_idx}")
×
531
            txout = PartialTxOutput.from_address_and_value(txout_addr, txout_val)
4✔
532
            outputs.append(txout)
4✔
533

534
        tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
4✔
535
        tx.sign(keypairs)
4✔
536
        return tx.serialize()
4✔
537

538
    @command('')
4✔
539
    async def signtransaction_with_privkey(self, tx, privkey):
4✔
540
        """Sign a transaction. The provided list of private keys will be used to sign the transaction.
541

542
        arg:tx:tx:Transaction to sign
543
        arg:str:privkey:private key
544
        """
545
        tx = tx_from_any(tx)
4✔
546

547
        txins_dict = defaultdict(list)
4✔
548
        for txin in tx.inputs():
4✔
549
            txins_dict[txin.address].append(txin)
4✔
550

551
        if not isinstance(privkey, list):
4✔
552
            privkey = [privkey]
4✔
553

554
        for priv in privkey:
4✔
555
            txin_type, priv2, compressed = bitcoin.deserialize_privkey(priv)
4✔
556
            pubkey = ecc.ECPrivkey(priv2).get_public_key_bytes(compressed=compressed)
4✔
557
            desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=txin_type)
4✔
558
            address = desc.expand().address()
4✔
559
            if address in txins_dict.keys():
4✔
560
                for txin in txins_dict[address]:
4✔
561
                    txin.script_descriptor = desc
4✔
562
                tx.sign({pubkey: priv2})
4✔
563

564
        return tx.serialize()
4✔
565

566
    @command('wp')
4✔
567
    async def signtransaction(self, tx, password=None, wallet: Abstract_Wallet = None, ignore_warnings: bool=False):
4✔
568
        """
569
        Sign a transaction. The wallet keys will be used to sign the transaction.
570

571
        arg:tx:tx:transaction
572
        arg:bool:ignore_warnings:ignore warnings
573
        """
574
        tx = tx_from_any(tx)
4✔
575
        wallet.sign_transaction(tx, password, ignore_warnings=ignore_warnings)
4✔
576
        return tx.serialize()
4✔
577

578
    @command('')
4✔
579
    async def deserialize(self, tx):
4✔
580
        """
581
        Deserialize a transaction
582

583
        arg:str:tx:Serialized transaction
584
        """
585
        tx = tx_from_any(tx)
×
586
        return tx.to_json()
×
587

588
    @command('n')
4✔
589
    async def broadcast(self, tx):
4✔
590
        """
591
        Broadcast a transaction to the network.
592

593
        arg:str:tx:Serialized transaction (must be hexadecimal)
594
        """
595
        tx = Transaction(tx)
×
596
        await self.network.broadcast_transaction(tx)
×
597
        return tx.txid()
×
598

599
    @command('')
4✔
600
    async def createmultisig(self, num, pubkeys):
4✔
601
        """
602
        Create multisig 'n of m' address
603

604
        arg:int:num:Number of cosigners required
605
        arg:json:pubkeys:List of public keys
606
        """
607
        assert isinstance(pubkeys, list), (type(num), type(pubkeys))
×
608
        redeem_script = multisig_script(pubkeys, num)
×
609
        address = bitcoin.hash160_to_p2sh(hash_160(redeem_script))
×
610
        return {'address': address, 'redeemScript': redeem_script.hex()}
×
611

612
    @command('w')
4✔
613
    async def freeze(self, address: str, wallet: Abstract_Wallet = None):
4✔
614
        """
615
        Freeze address. Freeze the funds at one of your wallet\'s addresses
616

617
        arg:str:address:Bitcoin address
618
        """
619
        return wallet.set_frozen_state_of_addresses([address], True)
×
620

621
    @command('w')
4✔
622
    async def unfreeze(self, address: str, wallet: Abstract_Wallet = None):
4✔
623
        """
624
        Unfreeze address. Unfreeze the funds at one of your wallet\'s address
625

626
        arg:str:address:Bitcoin address
627
        """
628
        return wallet.set_frozen_state_of_addresses([address], False)
×
629

630
    @command('w')
4✔
631
    async def freeze_utxo(self, coin: str, wallet: Abstract_Wallet = None):
4✔
632
        """
633
        Freeze a UTXO so that the wallet will not spend it.
634

635
        arg:str:coin:outpoint, in the <txid:index> format
636
        """
637
        wallet.set_frozen_state_of_coins([coin], True)
×
638
        return True
×
639

640
    @command('w')
4✔
641
    async def unfreeze_utxo(self, coin: str, wallet: Abstract_Wallet = None):
4✔
642
        """Unfreeze a UTXO so that the wallet might spend it.
643

644
        arg:str:coin:outpoint
645
        """
646
        wallet.set_frozen_state_of_coins([coin], False)
×
647
        return True
×
648

649
    @command('wp')
4✔
650
    async def getprivatekeys(self, address, password=None, wallet: Abstract_Wallet = None):
4✔
651
        """
652
        Get private keys of addresses. You may pass a single wallet address, or a list of wallet addresses.
653

654
        arg:str:address:Bitcoin address
655
        """
656
        if isinstance(address, str):
4✔
657
            address = address.strip()
4✔
658
        if is_address(address):
4✔
659
            return wallet.export_private_key(address, password)
4✔
660
        domain = address
4✔
661
        return [wallet.export_private_key(address, password) for address in domain]
4✔
662

663
    @command('wp')
4✔
664
    async def getprivatekeyforpath(self, path, password=None, wallet: Abstract_Wallet = None):
4✔
665
        """Get private key corresponding to derivation path (address index).
666

667
        arg:str:path:Derivation path. Can be either a str such as "m/0/50", or a list of ints such as [0, 50].
668
        """
669
        return wallet.export_private_key_for_path(path, password)
4✔
670

671
    @command('w')
4✔
672
    async def ismine(self, address, wallet: Abstract_Wallet = None):
4✔
673
        """
674
        Check if address is in wallet. Return true if and only address is in wallet
675

676
        arg:str:address:Bitcoin address
677
        """
678
        return wallet.is_mine(address)
×
679

680
    @command('')
4✔
681
    async def dumpprivkeys(self):
4✔
682
        """Deprecated."""
683
        return "This command is deprecated. Use a pipe instead: 'electrum listaddresses | electrum getprivatekeys - '"
×
684

685
    @command('')
4✔
686
    async def validateaddress(self, address):
4✔
687
        """Check that an address is valid.
688

689
        arg:str:address:Bitcoin address
690
        """
691
        return is_address(address)
×
692

693
    @command('w')
4✔
694
    async def getpubkeys(self, address, wallet: Abstract_Wallet = None):
4✔
695
        """
696
        Return the public keys for a wallet address.
697

698
        arg:str:address:Bitcoin address
699
        """
700
        return wallet.get_public_keys(address)
×
701

702
    @command('w')
4✔
703
    async def getbalance(self, wallet: Abstract_Wallet = None):
4✔
704
        """Return the balance of your wallet. """
705
        c, u, x = wallet.get_balance()
×
706
        l = wallet.lnworker.get_balance() if wallet.lnworker else None
×
707
        out = {"confirmed": str(to_decimal(c)/COIN)}
×
708
        if u:
×
709
            out["unconfirmed"] = str(to_decimal(u)/COIN)
×
710
        if x:
×
711
            out["unmatured"] = str(to_decimal(x)/COIN)
×
712
        if l:
×
713
            out["lightning"] = str(to_decimal(l)/COIN)
×
714
        return out
×
715

716
    @command('n')
4✔
717
    async def getaddressbalance(self, address):
4✔
718
        """
719
        Return the balance of any address. Note: This is a walletless
720
        server query, results are not checked by SPV.
721

722
        arg:str:address:Bitcoin address
723
        """
724
        sh = bitcoin.address_to_scripthash(address)
×
725
        out = await self.network.get_balance_for_scripthash(sh)
×
726
        out["confirmed"] =  str(to_decimal(out["confirmed"])/COIN)
×
727
        out["unconfirmed"] =  str(to_decimal(out["unconfirmed"])/COIN)
×
728
        return out
×
729

730
    @command('n')
4✔
731
    async def getmerkle(self, txid, height):
4✔
732
        """Get Merkle branch of a transaction included in a block. Electrum
733
        uses this to verify transactions (Simple Payment Verification).
734

735
        arg:txid:txid:Transaction ID
736
        arg:int:height:Block height
737
        """
738
        return await self.network.get_merkle_for_transaction(txid, int(height))
×
739

740
    @command('n')
4✔
741
    async def getservers(self):
4✔
742
        """Return the list of known servers (candidates for connecting)."""
743
        return self.network.get_servers()
×
744

745
    @command('')
4✔
746
    async def version(self):
4✔
747
        """Return the version of Electrum."""
748
        return ELECTRUM_VERSION
×
749

750
    @command('')
4✔
751
    async def version_info(self):
4✔
752
        """Return information about dependencies, such as their version and path."""
753
        ret = {
×
754
            "electrum.version": ELECTRUM_VERSION,
755
            "electrum.path": os.path.dirname(os.path.realpath(__file__)),
756
            "python.version": sys.version,
757
            "python.path": sys.executable,
758
        }
759
        # add currently running GUI
760
        if self.daemon and self.daemon.gui_object:
×
761
            ret.update(self.daemon.gui_object.version_info())
×
762
        # always add Qt GUI, so we get info even when running this from CLI
763
        try:
×
764
            from .gui.qt import ElectrumGui as QtElectrumGui
×
765
            ret.update(QtElectrumGui.version_info())
×
766
        except GuiImportError:
×
767
            pass
×
768
        # Add shared libs (.so/.dll), and non-pure-python dependencies.
769
        # Such deps can be installed in various ways - often via the Linux distro's pkg manager,
770
        # instead of using pip, hence it is useful to list them for debugging.
771
        from electrum_ecc import ecc_fast
×
772
        ret.update(ecc_fast.version_info())
×
773
        from . import qrscanner
×
774
        ret.update(qrscanner.version_info())
×
775
        ret.update(DeviceMgr.version_info())
×
776
        ret.update(crypto.version_info())
×
777
        # add some special cases
778
        import aiohttp
×
779
        ret["aiohttp.version"] = aiohttp.__version__
×
780
        import aiorpcx
×
781
        ret["aiorpcx.version"] = aiorpcx._version_str
×
782
        import certifi
×
783
        ret["certifi.version"] = certifi.__version__
×
784
        import dns
×
785
        ret["dnspython.version"] = dns.__version__
×
786

787
        return ret
×
788

789
    @command('w')
4✔
790
    async def getmpk(self, wallet: Abstract_Wallet = None):
4✔
791
        """Get master public key. Return your wallet\'s master public key"""
792
        return wallet.get_master_public_key()
×
793

794
    @command('wp')
4✔
795
    async def getmasterprivate(self, password=None, wallet: Abstract_Wallet = None):
4✔
796
        """Get master private key. Return your wallet\'s master private key"""
797
        return str(wallet.keystore.get_master_private_key(password))
×
798

799
    @command('')
4✔
800
    async def convert_xkey(self, xkey, xtype):
4✔
801
        """Convert xtype of a master key. e.g. xpub -> ypub
802

803
        arg:str:xkey:the key
804
        arg:str:xtype:the type, eg 'xpub'
805
        """
806
        try:
4✔
807
            node = BIP32Node.from_xkey(xkey)
4✔
808
        except Exception:
×
809
            raise UserFacingException('xkey should be a master public/private key')
×
810
        return node._replace(xtype=xtype).to_xkey()
4✔
811

812
    @command('wp')
4✔
813
    async def getseed(self, password=None, wallet: Abstract_Wallet = None):
4✔
814
        """Get seed phrase. Print the generation seed of your wallet."""
815
        s = wallet.get_seed(password)
4✔
816
        return s
4✔
817

818
    @command('wp')
4✔
819
    async def importprivkey(self, privkey, password=None, wallet: Abstract_Wallet = None):
4✔
820
        """Import a private key or a list of private keys.
821

822
        arg:str:privkey:Private key. Type \'?\' to get a prompt.
823
        """
824
        if not wallet.can_import_privkey():
4✔
825
            return "Error: This type of wallet cannot import private keys. Try to create a new wallet with that key."
×
826
        assert isinstance(wallet, Imported_Wallet)
4✔
827
        keys = privkey.split()
4✔
828
        if not keys:
4✔
829
            return "Error: no keys given"
4✔
830
        elif len(keys) == 1:
4✔
831
            try:
4✔
832
                addr = wallet.import_private_key(keys[0], password)
4✔
833
                out = "Keypair imported: " + addr
4✔
834
            except Exception as e:
4✔
835
                out = "Error: " + repr(e)
4✔
836
            return out
4✔
837
        else:
838
            good_inputs, bad_inputs = wallet.import_private_keys(keys, password)
4✔
839
            return {
4✔
840
                "good_keys": len(good_inputs),
841
                "bad_keys": len(bad_inputs),
842
            }
843

844
    async def _resolver(self, x, wallet: Abstract_Wallet):
4✔
845
        if x is None:
4✔
846
            return None
4✔
847
        out = await wallet.contacts.resolve(x)
4✔
848
        if out.get('type') == 'openalias' and self.nocheck is False and out.get('validated') is False:
4✔
849
            raise UserFacingException(f"cannot verify alias: {x}")
×
850
        return out['address']
4✔
851

852
    @command('n')
4✔
853
    async def sweep(self, privkey, destination, fee=None, feerate=None, nocheck=False, imax=100):
4✔
854
        """
855
        Sweep private keys. Returns a transaction that spends UTXOs from
856
        privkey to a destination address. The transaction will not be broadcast.
857

858
        arg:str:privkey:Private key. Type \'?\' to get a prompt.
859
        arg:str:destination:Bitcoin address, contact or alias
860
        arg:str:fee:Transaction fee (absolute, in BTC)
861
        arg:str:feerate:Transaction fee rate (in sat/vbyte)
862
        arg:int:imax:Maximum number of inputs
863
        arg:bool:nocheck:Do not verify aliases
864
        """
865
        from .wallet import sweep
×
866
        fee_policy = self._get_fee_policy(fee, feerate)
×
867
        privkeys = privkey.split()
×
868
        self.nocheck = nocheck
×
869
        #dest = self._resolver(destination)
870
        tx = await sweep(
×
871
            privkeys,
872
            network=self.network,
873
            to_address=destination,
874
            fee_policy=fee_policy,
875
            imax=imax,
876
        )
877
        return tx.serialize() if tx else None
×
878

879
    @command('wp')
4✔
880
    async def signmessage(self, address, message, password=None, wallet: Abstract_Wallet = None):
4✔
881
        """Sign a message with a key. Use quotes if your message contains
882
        whitespaces
883

884
        arg:str:address:Bitcoin address
885
        arg:str:message:Clear text message. Use quotes if it contains spaces.
886
        """
887
        sig = wallet.sign_message(address, message, password)
×
888
        return base64.b64encode(sig).decode('ascii')
×
889

890
    @command('')
4✔
891
    async def verifymessage(self, address, signature, message):
4✔
892
        """Verify a signature.
893

894
        arg:str:address:Bitcoin address
895
        arg:str:message:Clear text message. Use quotes if it contains spaces.
896
        arg:str:signature:The signature
897
        """
898
        sig = base64.b64decode(signature)
×
899
        message = util.to_bytes(message)
×
900
        return bitcoin.verify_usermessage_with_address(address, sig, message)
×
901

902
    def _get_fee_policy(self, fee, feerate):
4✔
903
        if fee is not None and feerate is not None:
4✔
904
            raise Exception('Cannot set both fee and feerate')
×
905
        if fee is not None:
4✔
906
            fee_sats = satoshis(fee)
4✔
907
            fee_policy = FeePolicy(f'fixed:{fee_sats}')
4✔
908
        elif feerate is not None:
4✔
909
            feerate_per_byte = 1000 * feerate
4✔
910
            fee_policy = FeePolicy(f'feerate:{feerate_per_byte}')
4✔
911
        else:
912
            fee_policy = FeePolicy(self.config.FEE_POLICY)
×
913
        return fee_policy
4✔
914

915
    @command('wp')
4✔
916
    async def payto(self, destination, amount, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None,
4✔
917
                    nocheck=False, unsigned=False, rbf=True, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None):
918
        """Create an on-chain transaction.
919

920
        arg:str:destination:Bitcoin address, contact or alias
921
        arg:decimal_or_max:amount:Amount to be sent (in BTC). Type '!' to send the maximum available.
922
        arg:decimal:fee:Transaction fee (absolute, in BTC)
923
        arg:float:feerate:Transaction fee rate (in sat/vbyte)
924
        arg:str:from_addr:Source address (must be a wallet address; use sweep to spend from non-wallet address)
925
        arg:str:change_addr:Change address. Default is a spare address, or the source address if it's not in the wallet
926
        arg:bool:rbf:Whether to signal opt-in Replace-By-Fee in the transaction (true/false)
927
        arg:bool:addtransaction:Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet
928
        arg:int:locktime:Set locktime block number
929
        arg:bool:unsigned:Do not sign transaction
930
        arg:bool:nocheck:Do not verify aliases
931
        arg:json:from_coins:Source coins (must be in wallet; use sweep to spend from non-wallet address)
932
        """
933
        return await self.paytomany(
4✔
934
            outputs=[(destination, amount),],
935
            fee=fee,
936
            feerate=feerate,
937
            from_addr=from_addr,
938
            from_coins=from_coins,
939
            change_addr=change_addr,
940
            nocheck=nocheck,
941
            unsigned=unsigned,
942
            rbf=rbf,
943
            password=password,
944
            locktime=locktime,
945
            addtransaction=addtransaction,
946
            wallet=wallet,
947
        )
948

949
    @command('wp')
4✔
950
    async def paytomany(self, outputs, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None,
4✔
951
                        nocheck=False, unsigned=False, rbf=True, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None):
952
        """Create a multi-output transaction.
953

954
        arg:json:outputs:json list of ["address", "amount in BTC"]
955
        arg:bool:rbf:Whether to signal opt-in Replace-By-Fee in the transaction (true/false)
956
        arg:str:fee:Transaction fee (absolute, in BTC)
957
        arg:str:feerate:Transaction fee rate (in sat/vbyte)
958
        arg:str:from_addr:Source address (must be a wallet address; use sweep to spend from non-wallet address)
959
        arg:str:change_addr:Change address. Default is a spare address, or the source address if it's not in the wallet
960
        arg:bool:addtransaction:Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet
961
        arg:int:locktime:Set locktime block number
962
        arg:bool:unsigned:Do not sign transaction
963
        arg:bool:nocheck:Do not verify aliases
964
        arg:json:from_coins:Source coins (must be in wallet; use sweep to spend from non-wallet address)
965
        """
966
        self.nocheck = nocheck
4✔
967
        fee_policy = self._get_fee_policy(fee, feerate)
4✔
968
        domain_addr = from_addr.split(',') if from_addr else None
4✔
969
        domain_coins = from_coins.split(',') if from_coins else None
4✔
970
        change_addr = await self._resolver(change_addr, wallet)
4✔
971
        if domain_addr is not None:
4✔
NEW
972
            resolvers = [self._resolver(addr, wallet) for addr in domain_addr]
×
NEW
973
            domain_addr = await asyncio.gather(*resolvers)
×
974
        final_outputs = []
4✔
975
        for address, amount in outputs:
4✔
976
            address = await self._resolver(address, wallet)
4✔
977
            amount_sat = satoshis_or_max(amount)
4✔
978
            final_outputs.append(PartialTxOutput.from_address_and_value(address, amount_sat))
4✔
979
        coins = wallet.get_spendable_coins(domain_addr)
4✔
980
        if domain_coins is not None:
4✔
981
            coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)]
×
982
        tx = wallet.make_unsigned_transaction(
4✔
983
            outputs=final_outputs,
984
            fee_policy=fee_policy,
985
            change_addr=change_addr,
986
            coins=coins,
987
            rbf=rbf,
988
            locktime=locktime,
989
        )
990
        if not unsigned:
4✔
991
            wallet.sign_transaction(tx, password)
4✔
992
        result = tx.serialize()
4✔
993
        if addtransaction:
4✔
994
            await self.addtransaction(result, wallet=wallet)
×
995
        return result
4✔
996

997
    def get_year_timestamps(self, year:int):
4✔
998
        kwargs = {}
×
999
        if year:
×
1000
            import time
×
1001
            start_date = datetime.datetime(year, 1, 1)
×
1002
            end_date = datetime.datetime(year+1, 1, 1)
×
1003
            kwargs['from_timestamp'] = time.mktime(start_date.timetuple())
×
1004
            kwargs['to_timestamp'] = time.mktime(end_date.timetuple())
×
1005
        return kwargs
×
1006

1007
    @command('w')
4✔
1008
    async def onchain_capital_gains(self, year=None, wallet: Abstract_Wallet = None):
4✔
1009
        """
1010
        Capital gains, using utxo pricing.
1011
        This cannot be used with lightning.
1012

1013
        arg:int:year:Show cap gains for a given year
1014
        """
1015
        kwargs = self.get_year_timestamps(year)
×
1016
        from .exchange_rate import FxThread
×
1017
        fx = self.daemon.fx if self.daemon else FxThread(config=self.config)
×
1018
        return json_normalize(wallet.get_onchain_capital_gains(fx, **kwargs))
×
1019

1020
    @command('wp')
4✔
1021
    async def bumpfee(self, tx, new_fee_rate, from_coins=None, decrease_payment=False, password=None, unsigned=False, wallet: Abstract_Wallet = None):
4✔
1022
        """
1023
        Bump the fee for an unconfirmed transaction.
1024
        'tx' can be either a raw hex tx or a txid. If txid, the corresponding tx must already be part of the wallet history.
1025

1026
        arg:str:tx:Serialized transaction (hexadecimal)
1027
        arg:str:new_fee_rate: The Updated/Increased Transaction fee rate (in sats/vbyte)
1028
        arg:bool:decrease_payment:Whether payment amount will be decreased (true/false)
1029
        arg:bool:unsigned:Do not sign transaction
1030
        arg:json:from_coins:Coins that may be used to inncrease the fee (must be in wallet)
1031
        """
1032
        if is_hash256_str(tx):  # txid
4✔
1033
            tx = wallet.db.get_transaction(tx)
4✔
1034
            if tx is None:
4✔
1035
                raise UserFacingException("Transaction not in wallet.")
4✔
1036
        else:  # raw tx
1037
            try:
4✔
1038
                tx = Transaction(tx)
4✔
1039
                tx.deserialize()
4✔
1040
            except transaction.SerializationError as e:
×
1041
                raise UserFacingException(f"Failed to deserialize transaction: {e}") from e
×
1042
        domain_coins = from_coins.split(',') if from_coins else None
4✔
1043
        coins = wallet.get_spendable_coins(None)
4✔
1044
        if domain_coins is not None:
4✔
1045
            coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)]
4✔
1046
        tx.add_info_from_wallet(wallet)
4✔
1047
        await tx.add_info_from_network(self.network)
4✔
1048
        new_tx = wallet.bump_fee(
4✔
1049
            tx=tx,
1050
            coins=coins,
1051
            strategy=BumpFeeStrategy.DECREASE_PAYMENT if decrease_payment else BumpFeeStrategy.PRESERVE_PAYMENT,
1052
            new_fee_rate=new_fee_rate)
1053
        if not unsigned:
4✔
1054
            wallet.sign_transaction(new_tx, password)
4✔
1055
        return new_tx.serialize()
4✔
1056

1057
    @command('w')
4✔
1058
    async def onchain_history(self, show_fiat=False, year=None, show_addresses=False, wallet: Abstract_Wallet = None):
4✔
1059
        """Wallet onchain history. Returns the transaction history of your wallet.
1060

1061
        arg:bool:show_addresses:Show input and output addresses
1062
        arg:bool:show_fiat:Show fiat value of transactions
1063
        arg:bool:show_fees:Show miner fees paid by transactions
1064
        arg:int:year:Show history for a given year
1065
        """
1066
        # trigger lnwatcher callbacks for their side effects: setting labels and accounting_addresses
1067
        await wallet.lnworker.lnwatcher.trigger_callbacks(requires_synchronizer=False)
×
1068

1069
        #'from_height': (None, "Only show transactions that confirmed after given block height"),
1070
        #'to_height':   (None, "Only show transactions that confirmed before given block height"),
1071
        kwargs = self.get_year_timestamps(year)
×
1072
        onchain_history = wallet.get_onchain_history(**kwargs)
×
1073
        out = [x.to_dict() for x in onchain_history.values()]
×
1074
        if show_fiat:
×
1075
            from .exchange_rate import FxThread
×
1076
            fx = self.daemon.fx if self.daemon else FxThread(config=self.config)
×
1077
        else:
1078
            fx = None
×
1079
        for item in out:
×
1080
            if show_addresses:
×
1081
                tx = wallet.db.get_transaction(item['txid'])
×
1082
                item['inputs'] = list(map(lambda x: x.to_json(), tx.inputs()))
×
1083
                item['outputs'] = list(map(lambda x: {'address': x.get_ui_address_str(), 'value_sat': x.value},
×
1084
                                           tx.outputs()))
1085
            if fx:
×
1086
                fiat_fields = wallet.get_tx_item_fiat(tx_hash=item['txid'], amount_sat=item['amount_sat'], fx=fx, tx_fee=item['fee_sat'])
×
1087
                item.update(fiat_fields)
×
1088
        return json_normalize(out)
×
1089

1090
    @command('wl')
4✔
1091
    async def lightning_history(self, wallet: Abstract_Wallet = None):
4✔
1092
        """ lightning history. """
1093
        lightning_history = wallet.lnworker.get_lightning_history() if wallet.lnworker else {}
×
1094
        sorted_hist= sorted(lightning_history.values(), key=lambda x: x.timestamp)
×
1095
        return json_normalize([x.to_dict() for x in sorted_hist])
×
1096

1097
    @command('w')
4✔
1098
    async def setlabel(self, key, label, wallet: Abstract_Wallet = None):
4✔
1099
        """
1100
        Assign a label to an item. Item may be a bitcoin address or a
1101
        transaction ID
1102

1103
        arg:str:key:Key
1104
        arg:str:label:Label
1105
        """
1106
        wallet.set_label(key, label)
×
1107

1108
    @command('w')
4✔
1109
    async def listcontacts(self, wallet: Abstract_Wallet = None):
4✔
1110
        """Show your list of contacts"""
1111
        return wallet.contacts
×
1112

1113
    @command('w')
4✔
1114
    async def getalias(self, key, wallet: Abstract_Wallet = None):
4✔
1115
        """
1116
        Retrieve alias. Lookup in your list of contacts, and for an OpenAlias DNS record.
1117

1118
        arg:str:key:the alias to be retrieved
1119
        """
NEW
1120
        return await wallet.contacts.resolve(key)
×
1121

1122
    @command('w')
4✔
1123
    async def searchcontacts(self, query, wallet: Abstract_Wallet = None):
4✔
1124
        """
1125
        Search through your wallet contacts, return matching entries.
1126

1127
        arg:str:query:Search query
1128
        """
1129
        results = {}
×
1130
        for key, value in wallet.contacts.items():
×
1131
            if query.lower() in key.lower():
×
1132
                results[key] = value
×
1133
        return results
×
1134

1135
    @command('w')
4✔
1136
    async def listaddresses(self, receiving=False, change=False, labels=False, frozen=False, unused=False, funded=False, balance=False, wallet: Abstract_Wallet = None):
4✔
1137
        """List wallet addresses. Returns the list of all addresses in your wallet. Use optional arguments to filter the results.
1138

1139
        arg:bool:receiving:Show only receiving addresses
1140
        arg:bool:change:Show only change addresses
1141
        arg:bool:frozen:Show only frozen addresses
1142
        arg:bool:unused:Show only unused addresses
1143
        arg:bool:funded:Show only funded addresses
1144
        arg:bool:balance:Show the balances of listed addresses
1145
        arg:bool:labels:Show the labels of listed addresses
1146
        """
1147
        out = []
×
1148
        for addr in wallet.get_addresses():
×
1149
            if frozen and not wallet.is_frozen_address(addr):
×
1150
                continue
×
1151
            if receiving and wallet.is_change(addr):
×
1152
                continue
×
1153
            if change and not wallet.is_change(addr):
×
1154
                continue
×
1155
            if unused and wallet.adb.is_used(addr):
×
1156
                continue
×
1157
            if funded and wallet.adb.is_empty(addr):
×
1158
                continue
×
1159
            item = addr
×
1160
            if labels or balance:
×
1161
                item = (item,)
×
1162
            if balance:
×
1163
                item += (util.format_satoshis(sum(wallet.get_addr_balance(addr))),)
×
1164
            if labels:
×
1165
                item += (repr(wallet.get_label_for_address(addr)),)
×
1166
            out.append(item)
×
1167
        return out
×
1168

1169
    @command('n')
4✔
1170
    async def gettransaction(self, txid, wallet: Abstract_Wallet = None):
4✔
1171
        """Retrieve a transaction.
1172

1173
        arg:txid:txid:Transaction ID
1174
        """
1175
        tx = None
×
1176
        if wallet:
×
1177
            tx = wallet.db.get_transaction(txid)
×
1178
        if tx is None:
×
1179
            raw = await self.network.get_transaction(txid)
×
1180
            if raw:
×
1181
                tx = Transaction(raw)
×
1182
            else:
1183
                raise UserFacingException("Unknown transaction")
×
1184
        if tx.txid() != txid:
×
1185
            raise UserFacingException("Mismatching txid")
×
1186
        return tx.serialize()
×
1187

1188
    @command('')
4✔
1189
    async def encrypt(self, pubkey, message) -> str:
4✔
1190
        """
1191
        Encrypt a message with a public key. Use quotes if the message contains whitespaces.
1192

1193
        arg:str:pubkey:Public key
1194
        arg:str:message:Clear text message. Use quotes if it contains spaces.
1195
        """
1196
        if not is_hex_str(pubkey):
4✔
1197
            raise UserFacingException(f"pubkey must be a hex string instead of {repr(pubkey)}")
×
1198
        try:
4✔
1199
            message = to_bytes(message)
4✔
1200
        except TypeError:
×
1201
            raise UserFacingException(f"message must be a string-like object instead of {repr(message)}")
×
1202
        public_key = ecc.ECPubkey(bfh(pubkey))
4✔
1203
        encrypted = crypto.ecies_encrypt_message(public_key, message)
4✔
1204
        return encrypted.decode('utf-8')
4✔
1205

1206
    @command('wp')
4✔
1207
    async def decrypt(self, pubkey, encrypted, password=None, wallet: Abstract_Wallet = None) -> str:
4✔
1208
        """Decrypt a message encrypted with a public key.
1209

1210
        arg:str:encrypted:Encrypted message
1211
        arg:str:pubkey:Public key of one of your wallet addresses
1212
        """
1213
        if not is_hex_str(pubkey):
4✔
1214
            raise UserFacingException(f"pubkey must be a hex string instead of {repr(pubkey)}")
×
1215
        if not isinstance(encrypted, (str, bytes, bytearray)):
4✔
1216
            raise UserFacingException(f"encrypted must be a string-like object instead of {repr(encrypted)}")
×
1217
        decrypted = wallet.decrypt_message(pubkey, encrypted, password)
4✔
1218
        return decrypted.decode('utf-8')
4✔
1219

1220
    @command('w')
4✔
1221
    async def get_request(self, request_id, wallet: Abstract_Wallet = None):
4✔
1222
        """Returns a payment request
1223

1224
        arg:str:request_id:The request ID, as seen in list_requests or add_request
1225
        """
1226
        r = wallet.get_request(request_id)
×
1227
        if not r:
×
1228
            raise UserFacingException("Request not found")
×
1229
        return wallet.export_request(r)
×
1230

1231
    @command('w')
4✔
1232
    async def get_invoice(self, invoice_id, wallet: Abstract_Wallet = None):
4✔
1233
        """
1234
        Returns an invoice (request for outgoing payment)
1235

1236
        arg:str:invoice_id:The invoice ID, as seen in list_invoices
1237
        """
1238
        r = wallet.get_invoice(invoice_id)
×
1239
        if not r:
×
1240
            raise UserFacingException("Request not found")
×
1241
        return wallet.export_invoice(r)
×
1242

1243
    #@command('w')
1244
    #async def ackrequest(self, serialized):
1245
    #    """<Not implemented>"""
1246
    #    pass
1247

1248
    def _filter_invoices(self, _list, wallet, pending, expired, paid):
4✔
1249
        if pending:
×
1250
            f = PR_UNPAID
×
1251
        elif expired:
×
1252
            f = PR_EXPIRED
×
1253
        elif paid:
×
1254
            f = PR_PAID
×
1255
        else:
1256
            f = None
×
1257
        if f is not None:
×
1258
            _list = [x for x in _list if f == wallet.get_invoice_status(x)]
×
1259
        return _list
×
1260

1261
    @command('w')
4✔
1262
    async def list_requests(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None):
4✔
1263
        """
1264
        Returns the list of incoming payment requests saved in the wallet.
1265
        arg:bool:paid:Show only paid requests
1266
        arg:bool:pending:Show only pending requests
1267
        arg:bool:expired:Show only expired requests
1268
        """
1269
        l = wallet.get_sorted_requests()
×
1270
        l = self._filter_invoices(l, wallet, pending, expired, paid)
×
1271
        return [wallet.export_request(x) for x in l]
×
1272

1273
    @command('w')
4✔
1274
    async def list_invoices(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None):
4✔
1275
        """
1276
        Returns the list of invoices (requests for outgoing payments) saved in the wallet.
1277
        arg:bool:paid:Show only paid invoices
1278
        arg:bool:pending:Show only pending invoices
1279
        arg:bool:expired:Show only expired invoices
1280
        """
1281
        l = wallet.get_invoices()
×
1282
        l = self._filter_invoices(l, wallet, pending, expired, paid)
×
1283
        return [wallet.export_invoice(x) for x in l]
×
1284

1285
    @command('w')
4✔
1286
    async def createnewaddress(self, wallet: Abstract_Wallet = None):
4✔
1287
        """Create a new receiving address, beyond the gap limit of the wallet"""
1288
        return wallet.create_new_address(False)
×
1289

1290
    @command('w')
4✔
1291
    async def changegaplimit(self, new_limit, iknowwhatimdoing=False, wallet: Abstract_Wallet = None):
4✔
1292
        """
1293
        Change the gap limit of the wallet.
1294

1295
        arg:int:new_limit:new gap limit
1296
        arg:bool:iknowwhatimdoing:Acknowledge that I understand the full implications of what I am about to do
1297
        """
1298
        if not iknowwhatimdoing:
×
1299
            raise UserFacingException(
×
1300
                "WARNING: Are you SURE you want to change the gap limit?\n"
1301
                "It makes recovering your wallet from seed difficult!\n"
1302
                "Please do your research and make sure you understand the implications.\n"
1303
                "Typically only merchants and power users might want to do this.\n"
1304
                "To proceed, try again, with the --iknowwhatimdoing option.")
1305
        if not isinstance(wallet, Deterministic_Wallet):
×
1306
            raise UserFacingException("This wallet is not deterministic.")
×
1307
        return wallet.change_gap_limit(new_limit)
×
1308

1309
    @command('wn')
4✔
1310
    async def getminacceptablegap(self, wallet: Abstract_Wallet = None):
4✔
1311
        """Returns the minimum value for gap limit that would be sufficient to discover all
1312
        known addresses in the wallet.
1313
        """
1314
        if not isinstance(wallet, Deterministic_Wallet):
×
1315
            raise UserFacingException("This wallet is not deterministic.")
×
1316
        if not wallet.is_up_to_date():
×
1317
            raise NotSynchronizedException("Wallet not fully synchronized.")
×
1318
        return wallet.min_acceptable_gap()
×
1319

1320
    @command('w')
4✔
1321
    async def getunusedaddress(self, wallet: Abstract_Wallet = None):
4✔
1322
        """Returns the first unused address of the wallet, or None if all addresses are used.
1323
        An address is considered as used if it has received a transaction, or if it is used in a payment request."""
1324
        return wallet.get_unused_address()
×
1325

1326
    @command('w')
4✔
1327
    async def add_request(self, amount, memo='', expiry=3600, lightning=False, force=False, wallet: Abstract_Wallet = None):
4✔
1328
        """Create a payment request, using the first unused address of the wallet.
1329

1330
        The address will be considered as used after this operation.
1331
        If no payment is received, the address will be considered as unused if the payment request is deleted from the wallet.
1332

1333
        arg:decimal:amount:Requested amount (in btc)
1334
        arg:str:memo:Description of the request
1335
        arg:bool:force:Create new address beyond gap limit, if no more addresses are available.
1336
        arg:bool:lightning:Create lightning request.
1337
        arg:int:expiry:Time in seconds.
1338
        """
1339
        amount = satoshis(amount)
×
1340
        if not lightning:
×
1341
            addr = wallet.get_unused_address()
×
1342
            if addr is None:
×
1343
                if force:
×
1344
                    addr = wallet.create_new_address(False)
×
1345
                else:
1346
                    return False
×
1347
        else:
1348
            addr = None
×
1349
        expiry = int(expiry) if expiry else None
×
1350
        key = wallet.create_request(amount, memo, expiry, addr)
×
1351
        req = wallet.get_request(key)
×
1352
        return wallet.export_request(req)
×
1353

1354
    @command('w')
4✔
1355
    async def addtransaction(self, tx, wallet: Abstract_Wallet = None):
4✔
1356
        """
1357
        Add a transaction to the wallet history, without broadcasting it.
1358

1359
        arg:tx:tx:Transaction, in hexadecimal format.
1360
        """
1361
        tx = Transaction(tx)
×
1362
        if not wallet.adb.add_transaction(tx):
×
1363
            return False
×
1364
        wallet.save_db()
×
1365
        return tx.txid()
×
1366

1367
    @command('w')
4✔
1368
    async def delete_request(self, request_id, wallet: Abstract_Wallet = None):
4✔
1369
        """Remove an incoming payment request
1370

1371
        arg:str:request_id:The request ID, as returned in list_invoices
1372
        """
1373
        return wallet.delete_request(request_id)
×
1374

1375
    @command('w')
4✔
1376
    async def delete_invoice(self, invoice_id, wallet: Abstract_Wallet = None):
4✔
1377
        """Remove an outgoing payment invoice
1378

1379
        arg:str:invoice_id:The invoice ID, as returned in list_invoices
1380
        """
1381
        return wallet.delete_invoice(invoice_id)
×
1382

1383
    @command('w')
4✔
1384
    async def clear_requests(self, wallet: Abstract_Wallet = None):
4✔
1385
        """Remove all payment requests"""
1386
        wallet.clear_requests()
×
1387
        return True
×
1388

1389
    @command('w')
4✔
1390
    async def clear_invoices(self, wallet: Abstract_Wallet = None):
4✔
1391
        """Remove all invoices"""
1392
        wallet.clear_invoices()
×
1393
        return True
×
1394

1395
    @command('n')
4✔
1396
    async def notify(self, address: str, URL: Optional[str]):
4✔
1397
        """
1398
        Watch an address. Every time the address changes, a http POST is sent to the URL.
1399
        Call with an empty URL to stop watching an address.
1400

1401
        arg:str:address:Bitcoin address
1402
        arg:str:URL:The callback URL
1403
        """
1404
        if not hasattr(self, "_notifier"):
×
1405
            self._notifier = Notifier(self.network)
×
1406
        if URL:
×
1407
            await self._notifier.start_watching_addr(address, URL)
×
1408
        else:
1409
            await self._notifier.stop_watching_addr(address)
×
1410
        return True
×
1411

1412
    @command('wn')
4✔
1413
    async def is_synchronized(self, wallet: Abstract_Wallet = None):
4✔
1414
        """ return wallet synchronization status """
1415
        return wallet.is_up_to_date()
×
1416

1417
    @command('n')
4✔
1418
    async def getfeerate(self):
4✔
1419
        """
1420
        Return current fee estimate given network conditions (in sat/kvByte).
1421
        To change the fee policy, use 'getconfig/setconfig fee_policy'
1422
        """
1423
        fee_policy = FeePolicy(self.config.FEE_POLICY)
×
UNCOV
1424
        description = fee_policy.get_target_text()
×
UNCOV
1425
        feerate = fee_policy.fee_per_kb(self.network)
×
UNCOV
1426
        tooltip = fee_policy.get_estimate_text(self.network)
×
UNCOV
1427
        return {
×
1428
            'policy': fee_policy.get_descriptor(),
1429
            'description': description,
1430
            'sat/kvB': feerate,
1431
            'tooltip': tooltip,
1432
        }
1433

1434
    @command('w')
4✔
1435
    async def removelocaltx(self, txid, wallet: Abstract_Wallet = None):
4✔
1436
        """Remove a 'local' transaction from the wallet, and its dependent
1437
        transactions.
1438

1439
        arg:txid:txid:Transaction ID
1440
        """
UNCOV
1441
        height = wallet.adb.get_tx_height(txid).height
×
UNCOV
1442
        if height != TX_HEIGHT_LOCAL:
×
UNCOV
1443
            raise UserFacingException(
×
1444
                f'Only local transactions can be removed. '
1445
                f'This tx has height: {height} != {TX_HEIGHT_LOCAL}')
UNCOV
1446
        wallet.adb.remove_transaction(txid)
×
UNCOV
1447
        wallet.save_db()
×
1448

1449
    @command('wn')
4✔
1450
    async def get_tx_status(self, txid, wallet: Abstract_Wallet = None):
4✔
1451
        """Returns some information regarding the tx. For now, only confirmations.
1452
        The transaction must be related to the wallet.
1453

1454
        arg:txid:txid:Transaction ID
1455
        """
UNCOV
1456
        if not wallet.db.get_transaction(txid):
×
UNCOV
1457
            raise UserFacingException("Transaction not in wallet.")
×
UNCOV
1458
        return {
×
1459
            "confirmations": wallet.adb.get_tx_height(txid).conf,
1460
        }
1461

1462
    @command('')
4✔
1463
    async def help(self):
4✔
1464
        # for the python console
1465
        return sorted(known_commands.keys())
×
1466

1467
    # lightning network commands
1468
    @command('wnl')
4✔
1469
    async def add_peer(self, connection_string, timeout=20, gossip=False, wallet: Abstract_Wallet = None):
4✔
1470
        """
1471
        Connect to a lightning node
1472

1473
        arg:str:connection_string:Lightning network node ID or network address
1474
        arg:bool:gossip:Apply command to your gossip node instead of wallet node
1475
        arg:int:timeout:Timeout in seconds (default=20)
1476
        """
UNCOV
1477
        lnworker = self.network.lngossip if gossip else wallet.lnworker
×
UNCOV
1478
        await lnworker.add_peer(connection_string)
×
UNCOV
1479
        return True
×
1480

1481
    @command('wnl')
4✔
1482
    async def gossip_info(self, wallet: Abstract_Wallet = None):
4✔
1483
        """Display statistics about lightninig gossip"""
UNCOV
1484
        lngossip = self.network.lngossip
×
1485
        channel_db = lngossip.channel_db
×
1486
        forwarded = dict([(key.hex(), p._num_gossip_messages_forwarded) for key, p in wallet.lnworker.peers.items()]),
×
1487
        out = {
×
1488
            'received': {
1489
                'channel_announcements': lngossip._num_chan_ann,
1490
                'channel_updates': lngossip._num_chan_upd,
1491
                'channel_updates_good': lngossip._num_chan_upd_good,
1492
                'node_announcements': lngossip._num_node_ann,
1493
            },
1494
            'database': {
1495
                'nodes': channel_db.num_nodes,
1496
                'channels': channel_db.num_channels,
1497
                'channel_policies': channel_db.num_policies,
1498
            },
1499
            'forwarded': forwarded,
1500
        }
UNCOV
1501
        return out
×
1502

1503
    @command('wnl')
4✔
1504
    async def list_peers(self, gossip=False, wallet: Abstract_Wallet = None):
4✔
1505
        """
1506
        List lightning peers of your node
1507

1508
        arg:bool:gossip:Apply command to your gossip node instead of wallet node
1509
        """
UNCOV
1510
        lnworker = self.network.lngossip if gossip else wallet.lnworker
×
UNCOV
1511
        return [{
×
1512
            'node_id':p.pubkey.hex(),
1513
            'address':p.transport.name(),
1514
            'initialized':p.is_initialized(),
1515
            'features': str(LnFeatures(p.features)),
1516
            'channels': [c.funding_outpoint.to_str() for c in p.channels.values()],
1517
        } for p in lnworker.peers.values()]
1518

1519
    @command('wpnl')
4✔
1520
    async def open_channel(self, connection_string, amount, push_amount=0, public=False, zeroconf=False, password=None, wallet: Abstract_Wallet = None):
4✔
1521
        """
1522
        Open a lightning channel with a peer
1523

1524
        arg:str:connection_string:Lightning network node ID or network address
1525
        arg:decimal_or_max:amount:funding amount (in BTC)
1526
        arg:decimal:push_amount:Push initial amount (in BTC)
1527
        arg:bool:public:The channel will be announced
1528
        arg:bool:zeroconf:request zeroconf channel
1529
        """
UNCOV
1530
        if not wallet.can_have_lightning():
×
UNCOV
1531
            raise UserFacingException("This wallet cannot create new channels")
×
UNCOV
1532
        funding_sat = satoshis(amount)
×
UNCOV
1533
        push_sat = satoshis(push_amount)
×
UNCOV
1534
        peer = await wallet.lnworker.add_peer(connection_string)
×
UNCOV
1535
        chan, funding_tx = await wallet.lnworker.open_channel_with_peer(
×
1536
            peer, funding_sat,
1537
            push_sat=push_sat,
1538
            public=public,
1539
            zeroconf=zeroconf,
1540
            password=password)
1541
        return chan.funding_outpoint.to_str()
×
1542

1543
    @command('')
4✔
1544
    async def decode_invoice(self, invoice: str):
4✔
1545
        """
1546
        Decode a lightning invoice
1547

1548
        arg:str:invoice:Lightning invoice (bolt 11)
1549
        """
UNCOV
1550
        invoice = Invoice.from_bech32(invoice)
×
UNCOV
1551
        return invoice.to_debug_json()
×
1552

1553
    @command('wnpl')
4✔
1554
    async def lnpay(self, invoice, timeout=120, password=None, wallet: Abstract_Wallet = None):
4✔
1555
        """
1556
        Pay a lightning invoice
1557

1558
        arg:str:invoice:Lightning invoice (bolt 11)
1559
        arg:int:timeout:Timeout in seconds (default=20)
1560
        """
UNCOV
1561
        lnworker = wallet.lnworker
×
UNCOV
1562
        lnaddr = lnworker._check_bolt11_invoice(invoice)
×
UNCOV
1563
        payment_hash = lnaddr.paymenthash
×
UNCOV
1564
        invoice_obj = Invoice.from_bech32(invoice)
×
UNCOV
1565
        wallet.save_invoice(invoice_obj)
×
UNCOV
1566
        success, log = await lnworker.pay_invoice(invoice_obj)
×
UNCOV
1567
        return {
×
1568
            'payment_hash': payment_hash.hex(),
1569
            'success': success,
1570
            'preimage': lnworker.get_preimage(payment_hash).hex() if success else None,
1571
            'log': [x.formatted_tuple() for x in log]
1572
        }
1573

1574
    @command('wl')
4✔
1575
    async def nodeid(self, wallet: Abstract_Wallet = None):
4✔
UNCOV
1576
        listen_addr = self.config.LIGHTNING_LISTEN
×
UNCOV
1577
        return wallet.lnworker.node_keypair.pubkey.hex() + (('@' + listen_addr) if listen_addr else '')
×
1578

1579
    @command('wl')
4✔
1580
    async def list_channels(self, wallet: Abstract_Wallet = None):
4✔
1581
        # FIXME: we need to be online to display capacity of backups
UNCOV
1582
        from .lnutil import LOCAL, REMOTE, format_short_channel_id
×
UNCOV
1583
        channels = list(wallet.lnworker.channels.items())
×
1584
        backups = list(wallet.lnworker.channel_backups.items())
×
1585
        return [
×
1586
            {
1587
                'type': 'CHANNEL',
1588
                'short_channel_id': format_short_channel_id(chan.short_channel_id) if chan.short_channel_id else None,
1589
                'channel_id': chan.channel_id.hex(),
1590
                'channel_point': chan.funding_outpoint.to_str(),
1591
                'state': chan.get_state().name,
1592
                'peer_state': chan.peer_state.name,
1593
                'remote_pubkey': chan.node_id.hex(),
1594
                'local_balance': chan.balance(LOCAL)//1000,
1595
                'remote_balance': chan.balance(REMOTE)//1000,
1596
                'local_ctn': chan.get_latest_ctn(LOCAL),
1597
                'remote_ctn': chan.get_latest_ctn(REMOTE),
1598
                'local_reserve': chan.config[REMOTE].reserve_sat, # their config has our reserve
1599
                'remote_reserve': chan.config[LOCAL].reserve_sat,
1600
                'local_unsettled_sent': chan.balance_tied_up_in_htlcs_by_direction(LOCAL, direction=SENT) // 1000,
1601
                'remote_unsettled_sent': chan.balance_tied_up_in_htlcs_by_direction(REMOTE, direction=SENT) // 1000,
1602
            } for channel_id, chan in channels
1603
        ] + [
1604
            {
1605
                'type': 'BACKUP',
1606
                'short_channel_id': format_short_channel_id(chan.short_channel_id) if chan.short_channel_id else None,
1607
                'channel_id': chan.channel_id.hex(),
1608
                'channel_point': chan.funding_outpoint.to_str(),
1609
                'state': chan.get_state().name,
1610
            } for channel_id, chan in backups
1611
        ]
1612

1613
    @command('wnl')
4✔
1614
    async def enable_htlc_settle(self, b: bool, wallet: Abstract_Wallet = None):
4✔
1615
        """
1616
        command used in regtests
1617

1618
        arg:bool:b:boolean
1619
        """
UNCOV
1620
        wallet.lnworker.enable_htlc_settle = b
×
1621

1622
    @command('n')
4✔
1623
    async def clear_ln_blacklist(self):
4✔
UNCOV
1624
        if self.network.path_finder:
×
UNCOV
1625
            self.network.path_finder.clear_blacklist()
×
1626

1627
    @command('n')
4✔
1628
    async def reset_liquidity_hints(self):
4✔
UNCOV
1629
        if self.network.path_finder:
×
UNCOV
1630
            self.network.path_finder.liquidity_hints.reset_liquidity_hints()
×
UNCOV
1631
            self.network.path_finder.clear_blacklist()
×
1632

1633
    @command('wnpl')
4✔
1634
    async def close_channel(self, channel_point, force=False, password=None, wallet: Abstract_Wallet = None):
4✔
1635
        """
1636
        Close a lightning channel
1637

1638
        arg:str:channel_point:channel point
1639
        arg:bool:force:Force closes (broadcast local commitment transaction)
1640
        """
UNCOV
1641
        txid, index = channel_point.split(':')
×
UNCOV
1642
        chan_id, _ = channel_id_from_funding_tx(txid, int(index))
×
UNCOV
1643
        if chan_id not in wallet.lnworker.channels:
×
UNCOV
1644
            raise UserFacingException(f'Unknown channel {channel_point}')
×
UNCOV
1645
        coro = wallet.lnworker.force_close_channel(chan_id) if force else wallet.lnworker.close_channel(chan_id)
×
UNCOV
1646
        return await coro
×
1647

1648
    @command('wnpl')
4✔
1649
    async def request_force_close(self, channel_point, connection_string=None, password=None, wallet: Abstract_Wallet = None):
4✔
1650
        """
1651
        Requests the remote to force close a channel.
1652
        If a connection string is passed, can be used without having state or any backup for the channel.
1653
        Assumes that channel was originally opened with the same local peer (node_keypair).
1654

1655
        arg:str:connection_string:Lightning network node ID or network address
1656
        arg:str:channel_point:channel point
1657
        """
UNCOV
1658
        txid, index = channel_point.split(':')
×
UNCOV
1659
        chan_id, _ = channel_id_from_funding_tx(txid, int(index))
×
UNCOV
1660
        if chan_id not in wallet.lnworker.channels and chan_id not in wallet.lnworker.channel_backups:
×
UNCOV
1661
            raise UserFacingException(f'Unknown channel {channel_point}')
×
UNCOV
1662
        await wallet.lnworker.request_force_close(chan_id, connect_str=connection_string)
×
1663

1664
    @command('wpl')
4✔
1665
    async def export_channel_backup(self, channel_point, password=None, wallet: Abstract_Wallet = None):
4✔
1666
        """
1667
        Returns an encrypted channel backup
1668

1669
        arg:str:channel_point:Channel outpoint
1670
        """
1671
        txid, index = channel_point.split(':')
×
UNCOV
1672
        chan_id, _ = channel_id_from_funding_tx(txid, int(index))
×
UNCOV
1673
        if chan_id not in wallet.lnworker.channels:
×
UNCOV
1674
            raise UserFacingException(f'Unknown channel {channel_point}')
×
UNCOV
1675
        return wallet.lnworker.export_channel_backup(chan_id)
×
1676

1677
    @command('wl')
4✔
1678
    async def import_channel_backup(self, encrypted, wallet: Abstract_Wallet = None):
4✔
1679
        """
1680
        arg:str:encrypted:Encrypted channel backup
1681
        """
1682
        return wallet.lnworker.import_channel_backup(encrypted)
×
1683

1684
    @command('wnpl')
4✔
1685
    async def get_channel_ctx(self, channel_point, password=None, iknowwhatimdoing=False, wallet: Abstract_Wallet = None):
4✔
1686
        """
1687
        return the current commitment transaction of a channel
1688

1689
        arg:str:channel_point:Channel outpoint
1690
        arg:bool:iknowwhatimdoing:Acknowledge that I understand the full implications of what I am about to do
1691
        """
UNCOV
1692
        if not iknowwhatimdoing:
×
UNCOV
1693
            raise UserFacingException(
×
1694
                "WARNING: this command is potentially unsafe.\n"
1695
                "To proceed, try again, with the --iknowwhatimdoing option.")
UNCOV
1696
        txid, index = channel_point.split(':')
×
UNCOV
1697
        chan_id, _ = channel_id_from_funding_tx(txid, int(index))
×
UNCOV
1698
        if chan_id not in wallet.lnworker.channels:
×
UNCOV
1699
            raise UserFacingException(f'Unknown channel {channel_point}')
×
UNCOV
1700
        chan = wallet.lnworker.channels[chan_id]
×
1701
        tx = chan.force_close_tx()
×
1702
        return tx.serialize()
×
1703

1704
    @command('wnl')
4✔
1705
    async def get_watchtower_ctn(self, channel_point, wallet: Abstract_Wallet = None):
4✔
1706
        """
1707
        Return the local watchtower's ctn of channel. used in regtests
1708

1709
        arg:str:channel_point:Channel outpoint (txid:index)
1710
        """
1711
        return wallet.lnworker.get_watchtower_ctn(channel_point)
×
1712

1713
    @command('wnpl')
4✔
1714
    async def rebalance_channels(self, from_scid, dest_scid, amount, password=None, wallet: Abstract_Wallet = None):
4✔
1715
        """
1716
        Rebalance channels.
1717
        If trampoline is used, channels must be with different trampolines.
1718

1719
        arg:str:from_scid:Short channel ID
1720
        arg:str:dest_scid:Short channel ID
1721
        arg:decimal:amount:Amount (in BTC)
1722

1723
        """
UNCOV
1724
        from .lnutil import ShortChannelID
×
UNCOV
1725
        from_scid = ShortChannelID.from_str(from_scid)
×
UNCOV
1726
        dest_scid = ShortChannelID.from_str(dest_scid)
×
UNCOV
1727
        from_channel = wallet.lnworker.get_channel_by_short_id(from_scid)
×
UNCOV
1728
        dest_channel = wallet.lnworker.get_channel_by_short_id(dest_scid)
×
UNCOV
1729
        amount_sat = satoshis(amount)
×
UNCOV
1730
        success, log = await wallet.lnworker.rebalance_channels(
×
1731
            from_channel,
1732
            dest_channel,
1733
            amount_msat=amount_sat * 1000,
1734
        )
1735
        return {
×
1736
            'success': success,
1737
            'log': [x.formatted_tuple() for x in log]
1738
        }
1739

1740
    @command('wnpl')
4✔
1741
    async def normal_swap(self, onchain_amount, lightning_amount, password=None, wallet: Abstract_Wallet = None):
4✔
1742
        """
1743
        Normal submarine swap: send on-chain BTC, receive on Lightning
1744

1745
        arg:decimal_or_dryrun:lightning_amount:Amount to be received, in BTC. Set it to 'dryrun' to receive a value
1746
        arg:decimal_or_dryrun:onchain_amount:Amount to be sent, in BTC. Set it to 'dryrun' to receive a value
1747
        """
UNCOV
1748
        sm = wallet.lnworker.swap_manager
×
UNCOV
1749
        with sm.create_transport() as transport:
×
UNCOV
1750
            await sm.is_initialized.wait()
×
UNCOV
1751
            if lightning_amount == 'dryrun':
×
UNCOV
1752
                onchain_amount_sat = satoshis(onchain_amount)
×
UNCOV
1753
                lightning_amount_sat = sm.get_recv_amount(onchain_amount_sat, is_reverse=False)
×
UNCOV
1754
                txid = None
×
UNCOV
1755
            elif onchain_amount == 'dryrun':
×
UNCOV
1756
                lightning_amount_sat = satoshis(lightning_amount)
×
1757
                onchain_amount_sat = sm.get_send_amount(lightning_amount_sat, is_reverse=False)
×
1758
                txid = None
×
1759
            else:
1760
                lightning_amount_sat = satoshis(lightning_amount)
×
1761
                onchain_amount_sat = satoshis(onchain_amount)
×
1762
                txid = await wallet.lnworker.swap_manager.normal_swap(
×
1763
                    transport,
1764
                    lightning_amount_sat=lightning_amount_sat,
1765
                    expected_onchain_amount_sat=onchain_amount_sat,
1766
                    password=password,
1767
                )
1768

1769
        return {
×
1770
            'txid': txid,
1771
            'lightning_amount': format_satoshis(lightning_amount_sat),
1772
            'onchain_amount': format_satoshis(onchain_amount_sat),
1773
        }
1774

1775
    @command('wnpl')
4✔
1776
    async def reverse_swap(self, lightning_amount, onchain_amount, password=None, wallet: Abstract_Wallet = None):
4✔
1777
        """
1778
        Reverse submarine swap: send on Lightning, receive on-chain
1779

1780
        arg:decimal_or_dryrun:lightning_amount:Amount to be sent, in BTC. Set it to 'dryrun' to receive a value
1781
        arg:decimal_or_dryrun:onchain_amount:Amount to be received, in BTC. Set it to 'dryrun' to receive a value
1782
        """
UNCOV
1783
        sm = wallet.lnworker.swap_manager
×
UNCOV
1784
        with sm.create_transport() as transport:
×
UNCOV
1785
            await sm.is_initialized.wait()
×
UNCOV
1786
            if onchain_amount == 'dryrun':
×
UNCOV
1787
                lightning_amount_sat = satoshis(lightning_amount)
×
UNCOV
1788
                onchain_amount_sat = sm.get_recv_amount(lightning_amount_sat, is_reverse=True)
×
UNCOV
1789
                funding_txid = None
×
UNCOV
1790
            elif lightning_amount == 'dryrun':
×
UNCOV
1791
                onchain_amount_sat = satoshis(onchain_amount)
×
1792
                lightning_amount_sat = sm.get_send_amount(onchain_amount_sat, is_reverse=True)
×
1793
                funding_txid = None
×
1794
            else:
1795
                lightning_amount_sat = satoshis(lightning_amount)
×
1796
                claim_fee = sm.get_swap_tx_fee()
×
1797
                onchain_amount_sat = satoshis(onchain_amount) + claim_fee
×
1798
                funding_txid = await wallet.lnworker.swap_manager.reverse_swap(
×
1799
                    transport,
1800
                    lightning_amount_sat=lightning_amount_sat,
1801
                    expected_onchain_amount_sat=onchain_amount_sat,
1802
                )
UNCOV
1803
        return {
×
1804
            'funding_txid': funding_txid,
1805
            'lightning_amount': format_satoshis(lightning_amount_sat),
1806
            'onchain_amount': format_satoshis(onchain_amount_sat),
1807
        }
1808

1809
    @command('n')
4✔
1810
    async def convert_currency(self, from_amount=1, from_ccy='', to_ccy=''):
4✔
1811
        """
1812
        Converts the given amount of currency to another using the
1813
        configured exchange rate source.
1814

1815
        arg:decimal:from_amount:Amount to convert (default=1)
1816
        arg:decimal:from_ccy:Currency to convert from
1817
        arg:decimal:to_ccy:Currency to convert to
1818
        """
UNCOV
1819
        if not self.daemon.fx.is_enabled():
×
UNCOV
1820
            raise UserFacingException("FX is disabled. To enable, run: 'electrum setconfig use_exchange_rate true'")
×
1821
        # Currency codes are uppercase
UNCOV
1822
        from_ccy = from_ccy.upper()
×
UNCOV
1823
        to_ccy = to_ccy.upper()
×
1824
        # Default currencies
UNCOV
1825
        if from_ccy == '':
×
UNCOV
1826
            from_ccy = "BTC" if to_ccy != "BTC" else self.daemon.fx.ccy
×
UNCOV
1827
        if to_ccy == '':
×
1828
            to_ccy = "BTC" if from_ccy != "BTC" else self.daemon.fx.ccy
×
1829
        # Get current rates
UNCOV
1830
        rate_from = self.daemon.fx.exchange.get_cached_spot_quote(from_ccy)
×
1831
        rate_to = self.daemon.fx.exchange.get_cached_spot_quote(to_ccy)
×
1832
        # Test if currencies exist
UNCOV
1833
        if rate_from.is_nan():
×
1834
            raise UserFacingException(f'Currency to convert from ({from_ccy}) is unknown or rate is unavailable')
×
1835
        if rate_to.is_nan():
×
1836
            raise UserFacingException(f'Currency to convert to ({to_ccy}) is unknown or rate is unavailable')
×
1837
        # Conversion
UNCOV
1838
        try:
×
1839
            from_amount = to_decimal(from_amount)
×
1840
            to_amount = from_amount / rate_from * rate_to
×
UNCOV
1841
        except InvalidOperation:
×
1842
            raise Exception("from_amount is not a number")
×
1843
        return {
×
1844
            "from_amount": self.daemon.fx.ccy_amount_str(from_amount, add_thousands_sep=False, ccy=from_ccy),
1845
            "to_amount": self.daemon.fx.ccy_amount_str(to_amount, add_thousands_sep=False, ccy=to_ccy),
1846
            "from_ccy": from_ccy,
1847
            "to_ccy": to_ccy,
1848
            "source": self.daemon.fx.exchange.name(),
1849
        }
1850

1851
    @command('wnl')
4✔
1852
    async def send_onion_message(self, node_id_or_blinded_path_hex: str, message: str, wallet: Abstract_Wallet = None):
4✔
1853
        """
1854
        Send an onion message with onionmsg_tlv.message payload to node_id.
1855

1856
        arg:str:node_id_or_blinded_path_hex:node id or blinded path
1857
        arg:str:message:Message to send
1858
        """
UNCOV
1859
        assert wallet
×
UNCOV
1860
        assert wallet.lnworker
×
UNCOV
1861
        assert node_id_or_blinded_path_hex
×
UNCOV
1862
        assert message
×
1863

UNCOV
1864
        node_id_or_blinded_path = bfh(node_id_or_blinded_path_hex)
×
UNCOV
1865
        assert len(node_id_or_blinded_path) >= 33
×
1866

UNCOV
1867
        destination_payload = {
×
1868
            'message': {'text': message.encode('utf-8')}
1869
        }
1870

1871
        try:
×
UNCOV
1872
            send_onion_message_to(wallet.lnworker, node_id_or_blinded_path, destination_payload)
×
1873
            return {'success': True}
×
1874
        except Exception as e:
×
UNCOV
1875
            msg = str(e)
×
1876

UNCOV
1877
        return {
×
1878
            'success': False,
1879
            'msg': msg
1880
        }
1881

1882
    @command('wnl')
4✔
1883
    async def get_blinded_path_via(self, node_id: str, dummy_hops: int = 0, wallet: Abstract_Wallet = None):
4✔
1884
        """
1885
        Create a blinded path with node_id as introduction point. Introduction point must be direct peer of me.
1886

1887
        arg:str:node_id:Node pubkey in hex format
1888
        arg:int:dummy_hops:Number of dummy hops to add
1889
        """
1890
        # TODO: allow introduction_point to not be a direct peer and construct a route
UNCOV
1891
        assert wallet
×
UNCOV
1892
        assert node_id
×
1893

UNCOV
1894
        pubkey = bfh(node_id)
×
UNCOV
1895
        assert len(pubkey) == 33, 'invalid node_id'
×
1896

UNCOV
1897
        peer = wallet.lnworker.peers[pubkey]
×
UNCOV
1898
        assert peer, 'node_id not a peer'
×
1899

1900
        path = [pubkey, wallet.lnworker.node_keypair.pubkey]
×
1901
        session_key = os.urandom(32)
×
UNCOV
1902
        blinded_path = create_blinded_path(session_key, path=path, final_recipient_data={}, dummy_hops=dummy_hops)
×
1903

1904
        with io.BytesIO() as blinded_path_fd:
×
UNCOV
1905
            OnionWireSerializer.write_field(
×
1906
                fd=blinded_path_fd,
1907
                field_type='blinded_path',
1908
                count=1,
1909
                value=blinded_path)
1910
            encoded_blinded_path = blinded_path_fd.getvalue()
×
1911

UNCOV
1912
        return encoded_blinded_path.hex()
×
1913

1914
def plugin_command(s, plugin_name):
4✔
1915
    """Decorator to register a cli command inside a plugin. To be used within a commands.py file
1916
    in the plugins root."""
UNCOV
1917
    def decorator(func):
×
UNCOV
1918
        assert len(plugin_name) > 0, "Plugin name must not be empty"
×
1919
        func.plugin_name = plugin_name
×
UNCOV
1920
        name = plugin_name + '_' + func.__name__
×
1921
        if name in known_commands or hasattr(Commands, name):
×
UNCOV
1922
            raise Exception(f"Command name {name} already exists. Plugin commands should not overwrite other commands.")
×
UNCOV
1923
        assert asyncio.iscoroutinefunction(func), f"Plugin commands must be a coroutine: {name}"
×
UNCOV
1924
        @command(s)
×
UNCOV
1925
        @wraps(func)
×
1926
        async def func_wrapper(*args, **kwargs):
×
1927
            cmd_runner = args[0]  # type: Commands
×
1928
            daemon = cmd_runner.daemon
×
1929
            kwargs['plugin'] = daemon._plugins.get_plugin(plugin_name)
×
1930
            return await func(*args, **kwargs)
×
1931
        setattr(Commands, name, func_wrapper)
×
1932
        return func_wrapper
×
1933
    return decorator
×
1934

1935

1936
def eval_bool(x: str) -> bool:
4✔
1937
    if x == 'false':
4✔
1938
        return False
4✔
1939
    if x == 'true':
4✔
1940
        return True
4✔
1941
    # assume python, raise if malformed
1942
    return bool(ast.literal_eval(x))
4✔
1943

1944

1945

1946
# don't use floats because of rounding errors
1947
from .transaction import convert_raw_tx_to_hex
4✔
1948
json_loads = lambda x: json.loads(x, parse_float=lambda x: str(to_decimal(x)))
4✔
1949
def check_txid(txid):
4✔
UNCOV
1950
    if not is_hash256_str(txid):
×
UNCOV
1951
        raise UserFacingException(f"{repr(txid)} is not a txid")
×
UNCOV
1952
    return txid
×
1953

1954
arg_types = {
4✔
1955
    'int': int,
1956
    'bool': eval_bool,
1957
    'str': str,
1958
    'txid': check_txid,
1959
    'tx': convert_raw_tx_to_hex,
1960
    'json': json_loads,
1961
    'decimal': lambda x: str(to_decimal(x)),
1962
    'decimal_or_dryrun': lambda x: str(to_decimal(x)) if x != 'dryrun' else x,
1963
    'decimal_or_max': lambda x: str(to_decimal(x)) if not parse_max_spend(x) else x,
1964
}
1965

1966
config_variables = {
4✔
1967
    'addrequest': {
1968
        'ssl_privkey': 'Path to your SSL private key, needed to sign the request.',
1969
        'ssl_chain': 'Chain of SSL certificates, needed for signed requests. Put your certificate at the top and the root CA at the end',
1970
        'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"',
1971
    },
1972
    'listrequests':{
1973
        'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"',
1974
    }
1975
}
1976

1977

1978
def set_default_subparser(self, name, args=None):
4✔
1979
    """see http://stackoverflow.com/questions/5176691/argparse-how-to-specify-a-default-subcommand"""
UNCOV
1980
    subparser_found = False
×
UNCOV
1981
    for arg in sys.argv[1:]:
×
UNCOV
1982
        if arg in ['-h', '--help', '--version']:  # global help/version if no subparser
×
UNCOV
1983
            break
×
1984
    else:
UNCOV
1985
        for x in self._subparsers._actions:
×
UNCOV
1986
            if not isinstance(x, argparse._SubParsersAction):
×
UNCOV
1987
                continue
×
UNCOV
1988
            for sp_name in x._name_parser_map.keys():
×
1989
                if sp_name in sys.argv[1:]:
×
1990
                    subparser_found = True
×
1991
        if not subparser_found:
×
1992
            # insert default in first position, this implies no
1993
            # global options without a sub_parsers specified
1994
            if args is None:
×
1995
                sys.argv.insert(1, name)
×
1996
            else:
1997
                args.insert(0, name)
×
1998

1999

2000
argparse.ArgumentParser.set_default_subparser = set_default_subparser
4✔
2001

2002

2003
# workaround https://bugs.python.org/issue23058
2004
# see https://github.com/nickstenning/honcho/pull/121
2005

2006
def subparser_call(self, parser, namespace, values, option_string=None):
4✔
UNCOV
2007
    from argparse import ArgumentError, SUPPRESS, _UNRECOGNIZED_ARGS_ATTR
×
UNCOV
2008
    parser_name = values[0]
×
UNCOV
2009
    arg_strings = values[1:]
×
2010
    # set the parser name if requested
UNCOV
2011
    if self.dest is not SUPPRESS:
×
UNCOV
2012
        setattr(namespace, self.dest, parser_name)
×
2013
    # select the parser
UNCOV
2014
    try:
×
UNCOV
2015
        parser = self._name_parser_map[parser_name]
×
2016
    except KeyError:
×
2017
        tup = parser_name, ', '.join(self._name_parser_map)
×
2018
        msg = _('unknown parser {!r} (choices: {})').format(*tup)
×
UNCOV
2019
        raise ArgumentError(self, msg)
×
2020
    # parse all the remaining options into the namespace
2021
    # store any unrecognized options on the object, so that the top
2022
    # level parser can decide what to do with them
2023
    namespace, arg_strings = parser.parse_known_args(arg_strings, namespace)
×
2024
    if arg_strings:
×
2025
        vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, [])
×
2026
        getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings)
×
2027

2028

2029
argparse._SubParsersAction.__call__ = subparser_call
4✔
2030

2031

2032
def add_network_options(parser):
4✔
2033
    group = parser.add_argument_group('network options')
×
2034
    group.add_argument(
×
2035
        "-f", "--serverfingerprint", dest=SimpleConfig.NETWORK_SERVERFINGERPRINT.key(), default=None,
2036
        help="only allow connecting to servers with a matching SSL certificate SHA256 fingerprint. " +
2037
        "To calculate this yourself: '$ openssl x509 -noout -fingerprint -sha256 -inform pem -in mycertfile.crt'. Enter as 64 hex chars.")
UNCOV
2038
    group.add_argument(
×
2039
        "-1", "--oneserver", action="store_true", dest=SimpleConfig.NETWORK_ONESERVER.key(), default=None,
2040
        help="connect to one server only")
UNCOV
2041
    group.add_argument(
×
2042
        "-s", "--server", dest=SimpleConfig.NETWORK_SERVER.key(), default=None,
2043
        help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)")
UNCOV
2044
    group.add_argument(
×
2045
        "-p", "--proxy", dest=SimpleConfig.NETWORK_PROXY.key(), default=None,
2046
        help="set proxy [type:]host:port (or 'none' to disable proxy), where type is socks4 or socks5")
2047
    group.add_argument(
×
2048
        "--proxyuser", dest=SimpleConfig.NETWORK_PROXY_USER.key(), default=None,
2049
        help="set proxy username")
2050
    group.add_argument(
×
2051
        "--proxypassword", dest=SimpleConfig.NETWORK_PROXY_PASSWORD.key(), default=None,
2052
        help="set proxy password")
2053
    group.add_argument(
×
2054
        "--noonion", action="store_true", dest=SimpleConfig.NETWORK_NOONION.key(), default=None,
2055
        help="do not try to connect to onion servers")
2056
    group.add_argument(
×
2057
        "--skipmerklecheck", action="store_true", dest=SimpleConfig.NETWORK_SKIPMERKLECHECK.key(), default=None,
2058
        help="Tolerate invalid merkle proofs from Electrum server")
2059

2060

2061
def add_global_options(parser, suppress=False):
4✔
2062
    group = parser.add_argument_group('global options')
×
UNCOV
2063
    group.add_argument(
×
2064
        "-v", dest="verbosity", default='',
2065
        help=argparse.SUPPRESS if suppress else "Set verbosity (log levels)")
UNCOV
2066
    group.add_argument(
×
2067
        "-V", dest="verbosity_shortcuts", default='',
2068
        help=argparse.SUPPRESS if suppress else "Set verbosity (shortcut-filter list)")
UNCOV
2069
    group.add_argument(
×
2070
        "-D", "--dir", dest="electrum_path",
2071
        help=argparse.SUPPRESS if suppress else "electrum directory")
2072
    group.add_argument(
×
2073
        "-w", "--wallet", dest="wallet_path",
2074
        help=argparse.SUPPRESS if suppress else "wallet path")
2075
    group.add_argument(
×
2076
        "-P", "--portable", action="store_true", dest="portable", default=False,
2077
        help=argparse.SUPPRESS if suppress else "Use local 'electrum_data' directory")
2078
    group.add_argument(
×
2079
        "--testnet", action="store_true", dest="testnet", default=False,
2080
        help=argparse.SUPPRESS if suppress else "Use Testnet")
2081
    group.add_argument(
×
2082
        "--testnet4", action="store_true", dest="testnet4", default=False,
2083
        help=argparse.SUPPRESS if suppress else "Use Testnet4")
2084
    group.add_argument(
×
2085
        "--regtest", action="store_true", dest="regtest", default=False,
2086
        help=argparse.SUPPRESS if suppress else "Use Regtest")
2087
    group.add_argument(
×
2088
        "--simnet", action="store_true", dest="simnet", default=False,
2089
        help=argparse.SUPPRESS if suppress else "Use Simnet")
2090
    group.add_argument(
×
2091
        "--signet", action="store_true", dest="signet", default=False,
2092
        help=argparse.SUPPRESS if suppress else "Use Signet")
2093
    group.add_argument(
×
2094
        "-o", "--offline", action="store_true", dest=SimpleConfig.NETWORK_OFFLINE.key(), default=None,
2095
        help=argparse.SUPPRESS if suppress else "Run offline")
2096
    group.add_argument(
×
2097
        "--rpcuser", dest=SimpleConfig.RPC_USERNAME.key(), default=argparse.SUPPRESS,
2098
        help=argparse.SUPPRESS if suppress else "RPC user")
2099
    group.add_argument(
×
2100
        "--rpcpassword", dest=SimpleConfig.RPC_PASSWORD.key(), default=argparse.SUPPRESS,
2101
        help=argparse.SUPPRESS if suppress else "RPC password")
2102
    group.add_argument(
×
2103
        "--forgetconfig", action="store_true", dest=SimpleConfig.CONFIG_FORGET_CHANGES.key(), default=False,
2104
        help=argparse.SUPPRESS if suppress else "Forget config on exit")
2105

2106

2107

2108
def get_simple_parser():
4✔
2109
    """ simple parser that figures out the path of the config file and ignore unknown args """
UNCOV
2110
    from optparse import OptionParser, BadOptionError, AmbiguousOptionError
×
2111
    class PassThroughOptionParser(OptionParser):
×
2112
        # see https://stackoverflow.com/questions/1885161/how-can-i-get-optparses-optionparser-to-ignore-invalid-options
UNCOV
2113
        def _process_args(self, largs, rargs, values):
×
UNCOV
2114
            while rargs:
×
UNCOV
2115
                try:
×
UNCOV
2116
                    OptionParser._process_args(self,largs,rargs,values)
×
UNCOV
2117
                except (BadOptionError,AmbiguousOptionError) as e:
×
UNCOV
2118
                    largs.append(e.opt_str)
×
2119
    parser = PassThroughOptionParser()
×
2120
    parser.add_option("-D", "--dir", dest="electrum_path", help="electrum directory")
×
UNCOV
2121
    parser.add_option("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum_data' directory")
×
2122
    parser.add_option("--testnet", action="store_true", dest="testnet", default=False, help="Use Testnet")
×
2123
    parser.add_option("--testnet4", action="store_true", dest="testnet4", default=False, help="Use Testnet4")
×
2124
    parser.add_option("--regtest", action="store_true", dest="regtest", default=False, help="Use Regtest")
×
2125
    parser.add_option("--simnet", action="store_true", dest="simnet", default=False, help="Use Simnet")
×
2126
    parser.add_option("--signet", action="store_true", dest="signet", default=False, help="Use Signet")
×
2127
    return parser
×
2128

2129

2130
def get_parser():
4✔
2131
    # create main parser
2132
    parser = argparse.ArgumentParser(
×
2133
        epilog="Run 'electrum help <command>' to see the help for a command")
2134
    parser.add_argument("--version", dest="cmd", action='store_const', const='version', help="Return the version of Electrum.")
×
2135
    add_global_options(parser)
×
2136
    subparsers = parser.add_subparsers(dest='cmd', metavar='<command>')
×
2137
    # gui
UNCOV
2138
    parser_gui = subparsers.add_parser('gui', description="Run Electrum's Graphical User Interface.", help="Run GUI (default)")
×
UNCOV
2139
    parser_gui.add_argument("url", nargs='?', default=None, help="bitcoin URI (or bip70 file)")
×
UNCOV
2140
    parser_gui.add_argument("-g", "--gui", dest=SimpleConfig.GUI_NAME.key(), help="select graphical user interface", choices=['qt', 'text', 'stdio', 'qml'])
×
2141
    parser_gui.add_argument("-m", action="store_true", dest=SimpleConfig.GUI_QT_HIDE_ON_STARTUP.key(), default=False, help="hide GUI on startup")
×
UNCOV
2142
    parser_gui.add_argument("-L", "--lang", dest=SimpleConfig.LOCALIZATION_LANGUAGE.key(), default=None, help="default language used in GUI")
×
2143
    parser_gui.add_argument("--daemon", action="store_true", dest="daemon", default=False, help="keep daemon running after GUI is closed")
×
2144
    parser_gui.add_argument("--nosegwit", action="store_true", dest=SimpleConfig.WIZARD_DONT_CREATE_SEGWIT.key(), default=False, help="Do not create segwit wallets")
×
2145
    add_network_options(parser_gui)
×
UNCOV
2146
    add_global_options(parser_gui)
×
2147
    # daemon
2148
    parser_daemon = subparsers.add_parser('daemon', help="Run Daemon")
×
2149
    parser_daemon.add_argument("-d", "--detached", action="store_true", dest="detach", default=False, help="run daemon in detached mode")
×
2150
    # FIXME: all these options are rpc-server-side. The CLI client-side cannot use e.g. --rpcport,
2151
    #        instead it reads it from the daemon lockfile.
2152
    parser_daemon.add_argument("--rpchost", dest=SimpleConfig.RPC_HOST.key(), default=argparse.SUPPRESS, help="RPC host")
×
2153
    parser_daemon.add_argument("--rpcport", dest=SimpleConfig.RPC_PORT.key(), type=int, default=argparse.SUPPRESS, help="RPC port")
×
2154
    parser_daemon.add_argument("--rpcsock", dest=SimpleConfig.RPC_SOCKET_TYPE.key(), default=None, help="what socket type to which to bind RPC daemon", choices=['unix', 'tcp', 'auto'])
×
2155
    parser_daemon.add_argument("--rpcsockpath", dest=SimpleConfig.RPC_SOCKET_FILEPATH.key(), help="where to place RPC file socket")
×
UNCOV
2156
    add_network_options(parser_daemon)
×
2157
    add_global_options(parser_daemon)
×
2158
    # commands
UNCOV
2159
    for cmdname in sorted(known_commands.keys()):
×
UNCOV
2160
        cmd = known_commands[cmdname]
×
2161
        p = subparsers.add_parser(
×
2162
            cmdname, description=cmd.description,
2163
            epilog="Run 'electrum -h to see the list of global options",
2164
        )
2165
        for optname, default in zip(cmd.options, cmd.defaults):
×
2166
            if optname in ['wallet_path', 'wallet', 'plugin']:
×
UNCOV
2167
                continue
×
2168
            if optname == 'password':
×
2169
                p.add_argument("-W", "--password", dest='password', help="Wallet password. Use '--password :' if you want a prompt.")
×
2170
                continue
×
UNCOV
2171
            help = cmd.arg_descriptions.get(optname)
×
UNCOV
2172
            if not help:
×
UNCOV
2173
                print(f'undocumented argument {cmdname}::{optname}')
×
2174
            action = "store_true" if default is False else 'store'
×
2175
            if action == 'store':
×
2176
                type_descriptor = cmd.arg_types.get(optname)
×
2177
                _type = arg_types.get(type_descriptor, str)
×
2178
                p.add_argument('--' + optname, dest=optname, action=action, default=default, help=help, type=_type)
×
2179
            else:
2180
                p.add_argument('--' + optname, dest=optname, action=action, default=default, help=help)
×
2181
        add_global_options(p, suppress=True)
×
2182

2183
        for param in cmd.params:
×
2184
            if param in ['wallet_path', 'wallet']:
×
2185
                continue
×
2186
            help = cmd.arg_descriptions.get(param)
×
2187
            if not help:
×
UNCOV
2188
                print(f'undocumented argument {cmdname}::{param}')
×
2189
            type_descriptor = cmd.arg_types.get(param)
×
2190
            _type = arg_types.get(type_descriptor)
×
UNCOV
2191
            if help is not None and _type is None:
×
2192
                print(f'unknown type \'{_type}\' for {cmdname}::{param}')
×
2193
            p.add_argument(param, help=help, type=_type)
×
2194

2195
        cvh = config_variables.get(cmdname)
×
2196
        if cvh:
×
2197
            group = p.add_argument_group('configuration variables', '(set with setconfig/getconfig)')
×
2198
            for k, v in cvh.items():
×
2199
                group.add_argument(k, nargs='?', help=v)
×
2200

2201
    # 'gui' is the default command
2202
    # note: set_default_subparser modifies sys.argv
UNCOV
2203
    parser.set_default_subparser('gui')
×
2204
    return parser
×
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