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

spesmilo / electrum / 5218769850597376

24 Feb 2025 09:03PM UTC coverage: 60.737% (+0.007%) from 60.73%
5218769850597376

Pull #9580

CirrusCI

accumulator
qml: remove display states in ReceiveDialog, add balance check for enabling Lightning receive option.
Pull Request #9580: reintroduce separate request types for lightning and onchain

4 of 14 new or added lines in 3 files covered. (28.57%)

20679 of 34047 relevant lines covered (60.74%)

3.03 hits per line

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

45.93
/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
5✔
26
import sys
5✔
27
import datetime
5✔
28
import copy
5✔
29
import argparse
5✔
30
import json
5✔
31
import ast
5✔
32
import base64
5✔
33
import operator
5✔
34
import asyncio
5✔
35
import inspect
5✔
36
from collections import defaultdict
5✔
37
from functools import wraps, partial
5✔
38
from itertools import repeat
5✔
39
from decimal import Decimal, InvalidOperation
5✔
40
from typing import Optional, TYPE_CHECKING, Dict, List
5✔
41
import os
5✔
42

43
import electrum_ecc as ecc
5✔
44

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

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

79
if TYPE_CHECKING:
5✔
80
    from .network import Network
×
81
    from .daemon import Daemon
×
82

83

84
known_commands = {}  # type: Dict[str, Command]
5✔
85

86

87
class NotSynchronizedException(UserFacingException):
5✔
88
    pass
5✔
89

90

91
def satoshis_or_max(amount):
5✔
92
    return satoshis(amount) if not parse_max_spend(amount) else amount
5✔
93

94

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

99

100
def format_satoshis(x):
5✔
101
    return str(to_decimal(x)/COIN) if x is not None else None
×
102

103

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

124
        # sanity checks
125
        if self.requires_password:
5✔
126
            assert self.requires_wallet
5✔
127
        for varname in ('wallet_path', 'wallet'):
5✔
128
            if varname in varnames:
5✔
129
                assert varname in self.options
5✔
130
        assert not ('wallet_path' in varnames and 'wallet' in varnames)
5✔
131
        if self.requires_wallet:
5✔
132
            assert 'wallet' in varnames
5✔
133

134

135
def command(s):
5✔
136
    def decorator(func):
5✔
137
        global known_commands
138
        name = func.__name__
5✔
139
        known_commands[name] = Command(func, s)
5✔
140
        @wraps(func)
5✔
141
        async def func_wrapper(*args, **kwargs):
5✔
142
            cmd_runner = args[0]  # type: Commands
5✔
143
            cmd = known_commands[func.__name__]  # type: Command
5✔
144
            password = kwargs.get('password')
5✔
145
            daemon = cmd_runner.daemon
5✔
146
            if daemon:
5✔
147
                if 'wallet_path' in cmd.options and kwargs.get('wallet_path') is None:
5✔
148
                    kwargs['wallet_path'] = daemon.config.get_wallet_path()
×
149
                if cmd.requires_wallet and kwargs.get('wallet') is None:
5✔
150
                    kwargs['wallet'] = daemon.config.get_wallet_path()
×
151
                if 'wallet' in cmd.options:
5✔
152
                    wallet = kwargs.get('wallet', None)
5✔
153
                    if isinstance(wallet, str):
5✔
154
                        wallet = daemon.get_wallet(wallet)
5✔
155
                        if wallet is None:
5✔
156
                            raise UserFacingException('wallet not loaded')
×
157
                        kwargs['wallet'] = wallet
5✔
158
                    if cmd.requires_password and password is None and wallet.has_password():
5✔
159
                        password = wallet.get_unlocked_password()
×
160
                        if password:
×
161
                            kwargs['password'] = password
×
162
                        else:
163
                            raise UserFacingException('Password required. Unlock the wallet, or add a --password option to your command')
×
164
            wallet = kwargs.get('wallet')  # type: Optional[Abstract_Wallet]
5✔
165
            if cmd.requires_wallet and not wallet:
5✔
166
                raise UserFacingException('wallet not loaded')
×
167
            if cmd.requires_password and wallet.has_password():
5✔
168
                if password is None:
5✔
169
                    raise UserFacingException('Password required')
×
170
                try:
5✔
171
                    wallet.check_password(password)
5✔
172
                except InvalidPassword as e:
×
173
                    raise UserFacingException(str(e)) from None
×
174
            if cmd.requires_lightning and (not wallet or not wallet.has_lightning()):
5✔
175
                raise UserFacingException('Lightning not enabled in this wallet')
×
176
            return await func(*args, **kwargs)
5✔
177
        return func_wrapper
5✔
178
    return decorator
5✔
179

180

181
class Commands(Logger):
5✔
182

183
    def __init__(self, *, config: 'SimpleConfig',
5✔
184
                 network: 'Network' = None,
185
                 daemon: 'Daemon' = None, callback=None):
186
        Logger.__init__(self)
5✔
187
        self.config = config
5✔
188
        self.daemon = daemon
5✔
189
        self.network = network
5✔
190
        self._callback = callback
5✔
191

192
    def _run(self, method, args, password_getter=None, **kwargs):
5✔
193
        """This wrapper is called from unit tests and the Qt python console."""
194
        cmd = known_commands[method]
×
195
        password = kwargs.get('password', None)
×
196
        wallet = kwargs.get('wallet', None)
×
197
        if (cmd.requires_password and wallet and wallet.has_password()
×
198
                and password is None):
199
            password = password_getter()
×
200
            if password is None:
×
201
                return
×
202

203
        f = getattr(self, method)
×
204
        if cmd.requires_password:
×
205
            kwargs['password'] = password
×
206

207
        if 'wallet' in kwargs:
×
208
            sig = inspect.signature(f)
×
209
            if 'wallet' not in sig.parameters:
×
210
                kwargs.pop('wallet')
×
211

212
        coro = f(*args, **kwargs)
×
213
        fut = asyncio.run_coroutine_threadsafe(coro, util.get_asyncio_loop())
×
214
        result = fut.result()
×
215

216
        if self._callback:
×
217
            self._callback()
×
218
        return result
×
219

220
    @command('')
5✔
221
    async def commands(self):
5✔
222
        """List of commands"""
223
        return ' '.join(sorted(known_commands.keys()))
×
224

225
    @command('n')
5✔
226
    async def getinfo(self):
5✔
227
        """ network info """
228
        net_params = self.network.get_parameters()
×
229
        response = {
×
230
            'network': constants.net.NET_NAME,
231
            'path': self.network.config.path,
232
            'server': net_params.server.host,
233
            'blockchain_height': self.network.get_local_height(),
234
            'server_height': self.network.get_server_height(),
235
            'spv_nodes': len(self.network.get_interfaces()),
236
            'connected': self.network.is_connected(),
237
            'auto_connect': net_params.auto_connect,
238
            'version': ELECTRUM_VERSION,
239
            'default_wallet': self.config.get_wallet_path(),
240
            'fee_per_kb': self.config.fee_per_kb(),
241
        }
242
        return response
×
243

244
    @command('n')
5✔
245
    async def stop(self):
5✔
246
        """Stop daemon"""
247
        await self.daemon.stop()
×
248
        return "Daemon stopped"
×
249

250
    @command('n')
5✔
251
    async def list_wallets(self):
5✔
252
        """List wallets open in daemon"""
253
        return [
×
254
            {
255
                'path': path,
256
                'synchronized': w.is_up_to_date(),
257
                'unlocked': w.has_password() and (w.get_unlocked_password() is not None),
258
            }
259
            for path, w in self.daemon.get_wallets().items()
260
        ]
261

262
    @command('n')
5✔
263
    async def load_wallet(self, wallet_path=None, password=None):
5✔
264
        """
265
        Load the wallet in memory
266
        """
267
        wallet = self.daemon.load_wallet(wallet_path, password, upgrade=True)
5✔
268
        if wallet is None:
5✔
269
            raise UserFacingException('could not load wallet')
×
270
        run_hook('load_wallet', wallet, None)
5✔
271

272
    @command('n')
5✔
273
    async def close_wallet(self, wallet_path=None):
5✔
274
        """Close wallet"""
275
        return await self.daemon._stop_wallet(wallet_path)
×
276

277
    @command('')
5✔
278
    async def create(self, passphrase=None, password=None, encrypt_file=True, seed_type=None, wallet_path=None):
5✔
279
        """Create a new wallet.
280
        If you want to be prompted for an argument, type '?' or ':' (concealed)
281
        """
282
        d = create_new_wallet(path=wallet_path,
×
283
                              passphrase=passphrase,
284
                              password=password,
285
                              encrypt_file=encrypt_file,
286
                              seed_type=seed_type,
287
                              config=self.config)
288
        return {
×
289
            'seed': d['seed'],
290
            'path': d['wallet'].storage.path,
291
            'msg': d['msg'],
292
        }
293

294
    @command('')
5✔
295
    async def restore(self, text, passphrase=None, password=None, encrypt_file=True, wallet_path=None):
5✔
296
        """Restore a wallet from text. Text can be a seed phrase, a master
297
        public key, a master private key, a list of bitcoin addresses
298
        or bitcoin private keys.
299
        If you want to be prompted for an argument, type '?' or ':' (concealed)
300
        """
301
        # TODO create a separate command that blocks until wallet is synced
302
        d = restore_wallet_from_text(text,
×
303
                                     path=wallet_path,
304
                                     passphrase=passphrase,
305
                                     password=password,
306
                                     encrypt_file=encrypt_file,
307
                                     config=self.config)
308
        return {
×
309
            'path': d['wallet'].storage.path,
310
            'msg': d['msg'],
311
        }
312

313
    @command('wp')
5✔
314
    async def password(self, password=None, new_password=None, encrypt_file=None, wallet: Abstract_Wallet = None):
5✔
315
        """Change wallet password. """
316
        if wallet.storage.is_encrypted_with_hw_device() and new_password:
×
317
            raise UserFacingException("Can't change the password of a wallet encrypted with a hw device.")
×
318
        if encrypt_file is None:
×
319
            if not password and new_password:
×
320
                # currently no password, setting one now: we encrypt by default
321
                encrypt_file = True
×
322
            else:
323
                encrypt_file = wallet.storage.is_encrypted()
×
324
        wallet.update_password(password, new_password, encrypt_storage=encrypt_file)
×
325
        wallet.save_db()
×
326
        return {'password':wallet.has_password()}
×
327

328
    @command('w')
5✔
329
    async def get(self, key, wallet: Abstract_Wallet = None):
5✔
330
        """Return item from wallet storage"""
331
        return wallet.db.get(key)
×
332

333
    @command('')
5✔
334
    async def getconfig(self, key):
5✔
335
        """Return a configuration variable. """
336
        if Plugins.is_plugin_enabler_config_key(key):
×
337
            return self.config.get(key)
×
338
        else:
339
            cv = self.config.cv.from_key(key)
×
340
            return cv.get()
×
341

342
    @classmethod
5✔
343
    def _setconfig_normalize_value(cls, key, value):
5✔
344
        if key not in (SimpleConfig.RPC_USERNAME.key(), SimpleConfig.RPC_PASSWORD.key()):
5✔
345
            value = json_decode(value)
5✔
346
            # call literal_eval for backward compatibility (see #4225)
347
            try:
5✔
348
                value = ast.literal_eval(value)
5✔
349
            except Exception:
5✔
350
                pass
5✔
351
        return value
5✔
352

353
    @command('')
5✔
354
    async def setconfig(self, key, value):
5✔
355
        """Set a configuration variable. 'value' may be a string or a Python expression."""
356
        value = self._setconfig_normalize_value(key, value)
×
357
        if self.daemon and key == SimpleConfig.RPC_USERNAME.key():
×
358
            self.daemon.commands_server.rpc_user = value
×
359
        if self.daemon and key == SimpleConfig.RPC_PASSWORD.key():
×
360
            self.daemon.commands_server.rpc_password = value
×
361
        if Plugins.is_plugin_enabler_config_key(key):
×
362
            self.config.set_key(key, value)
×
363
        else:
364
            cv = self.config.cv.from_key(key)
×
365
            cv.set(value)
×
366

367
    @command('')
5✔
368
    async def listconfig(self):
5✔
369
        """Returns the list of all configuration variables. """
370
        return self.config.list_config_vars()
×
371

372
    @command('')
5✔
373
    async def helpconfig(self, key):
5✔
374
        """Returns help about a configuration variable. """
375
        cv = self.config.cv.from_key(key)
×
376
        short = cv.get_short_desc()
×
377
        long = cv.get_long_desc()
×
378
        if short and long:
×
379
            return short + "\n---\n\n" + long
×
380
        elif short or long:
×
381
            return short or long
×
382
        else:
383
            return f"No description available for '{key}'"
×
384

385
    @command('')
5✔
386
    async def make_seed(self, nbits=None, language=None, seed_type=None):
5✔
387
        """Create a seed"""
388
        from .mnemonic import Mnemonic
×
389
        s = Mnemonic(language).make_seed(seed_type=seed_type, num_bits=nbits)
×
390
        return s
×
391

392
    @command('n')
5✔
393
    async def getaddresshistory(self, address):
5✔
394
        """Return the transaction history of any address. Note: This is a
395
        walletless server query, results are not checked by SPV.
396
        """
397
        sh = bitcoin.address_to_scripthash(address)
×
398
        return await self.network.get_history_for_scripthash(sh)
×
399

400
    @command('wp')
5✔
401
    async def unlock(self, wallet: Abstract_Wallet = None, password=None):
5✔
402
        """Unlock the wallet (store the password in memory)."""
403
        wallet.unlock(password)
×
404

405
    @command('w')
5✔
406
    async def listunspent(self, wallet: Abstract_Wallet = None):
5✔
407
        """List unspent outputs. Returns the list of unspent transaction
408
        outputs in your wallet."""
409
        coins = []
×
410
        for txin in wallet.get_utxos():
×
411
            d = txin.to_json()
×
412
            v = d.pop("value_sats")
×
413
            d["value"] = str(to_decimal(v)/COIN) if v is not None else None
×
414
            coins.append(d)
×
415
        return coins
×
416

417
    @command('n')
5✔
418
    async def getaddressunspent(self, address):
5✔
419
        """Returns the UTXO list of any address. Note: This
420
        is a walletless server query, results are not checked by SPV.
421
        """
422
        sh = bitcoin.address_to_scripthash(address)
×
423
        return await self.network.listunspent_for_scripthash(sh)
×
424

425
    @command('')
5✔
426
    async def serialize(self, jsontx):
5✔
427
        """Create a signed raw transaction from a json tx template.
428

429
        Example value for "jsontx" arg: {
430
            "inputs": [
431
                {"prevout_hash": "9d221a69ca3997cbeaf5624d723e7dc5f829b1023078c177d37bdae95f37c539", "prevout_n": 1,
432
                 "value_sats": 1000000, "privkey": "p2wpkh:cVDXzzQg6RoCTfiKpe8MBvmm5d5cJc6JLuFApsFDKwWa6F5TVHpD"}
433
            ],
434
            "outputs": [
435
                {"address": "tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd", "value_sats": 990000}
436
            ]
437
        }
438
        """
439
        keypairs = {}
5✔
440
        inputs = []  # type: List[PartialTxInput]
5✔
441
        locktime = jsontx.get('locktime', 0)
5✔
442
        for txin_idx, txin_dict in enumerate(jsontx.get('inputs')):
5✔
443
            if txin_dict.get('prevout_hash') is not None and txin_dict.get('prevout_n') is not None:
5✔
444
                prevout = TxOutpoint(txid=bfh(txin_dict['prevout_hash']), out_idx=int(txin_dict['prevout_n']))
5✔
445
            elif txin_dict.get('output'):
×
446
                prevout = TxOutpoint.from_str(txin_dict['output'])
×
447
            else:
448
                raise UserFacingException(f"missing prevout for txin {txin_idx}")
×
449
            txin = PartialTxInput(prevout=prevout)
5✔
450
            try:
5✔
451
                txin._trusted_value_sats = int(txin_dict.get('value') or txin_dict['value_sats'])
5✔
452
            except KeyError:
×
453
                raise UserFacingException(f"missing 'value_sats' field for txin {txin_idx}")
×
454
            nsequence = txin_dict.get('nsequence', None)
5✔
455
            if nsequence is not None:
5✔
456
                txin.nsequence = nsequence
5✔
457
            sec = txin_dict.get('privkey')
5✔
458
            if sec:
5✔
459
                txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)
5✔
460
                pubkey = ecc.ECPrivkey(privkey).get_public_key_bytes(compressed=compressed)
5✔
461
                keypairs[pubkey] = privkey
5✔
462
                desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=txin_type)
5✔
463
                txin.script_descriptor = desc
5✔
464
            inputs.append(txin)
5✔
465

466
        outputs = []  # type: List[PartialTxOutput]
5✔
467
        for txout_idx, txout_dict in enumerate(jsontx.get('outputs')):
5✔
468
            try:
5✔
469
                txout_addr = txout_dict['address']
5✔
470
            except KeyError:
×
471
                raise UserFacingException(f"missing 'address' field for txout {txout_idx}")
×
472
            try:
5✔
473
                txout_val = int(txout_dict.get('value') or txout_dict['value_sats'])
5✔
474
            except KeyError:
×
475
                raise UserFacingException(f"missing 'value_sats' field for txout {txout_idx}")
×
476
            txout = PartialTxOutput.from_address_and_value(txout_addr, txout_val)
5✔
477
            outputs.append(txout)
5✔
478

479
        tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
5✔
480
        tx.sign(keypairs)
5✔
481
        return tx.serialize()
5✔
482

483
    @command('')
5✔
484
    async def signtransaction_with_privkey(self, tx, privkey):
5✔
485
        """Sign a transaction. The provided list of private keys will be used to sign the transaction."""
486
        tx = tx_from_any(tx)
5✔
487

488
        txins_dict = defaultdict(list)
5✔
489
        for txin in tx.inputs():
5✔
490
            txins_dict[txin.address].append(txin)
5✔
491

492
        if not isinstance(privkey, list):
5✔
493
            privkey = [privkey]
5✔
494

495
        for priv in privkey:
5✔
496
            txin_type, priv2, compressed = bitcoin.deserialize_privkey(priv)
5✔
497
            pubkey = ecc.ECPrivkey(priv2).get_public_key_bytes(compressed=compressed)
5✔
498
            desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=txin_type)
5✔
499
            address = desc.expand().address()
5✔
500
            if address in txins_dict.keys():
5✔
501
                for txin in txins_dict[address]:
5✔
502
                    txin.script_descriptor = desc
5✔
503
                tx.sign({pubkey: priv2})
5✔
504

505
        return tx.serialize()
5✔
506

507
    @command('wp')
5✔
508
    async def signtransaction(self, tx, password=None, wallet: Abstract_Wallet = None, iknowwhatimdoing: bool=False):
5✔
509
        """Sign a transaction. The wallet keys will be used to sign the transaction."""
510
        tx = tx_from_any(tx)
5✔
511
        wallet.sign_transaction(tx, password, ignore_warnings=iknowwhatimdoing)
5✔
512
        return tx.serialize()
5✔
513

514
    @command('')
5✔
515
    async def deserialize(self, tx):
5✔
516
        """Deserialize a serialized transaction"""
517
        tx = tx_from_any(tx)
×
518
        return tx.to_json()
×
519

520
    @command('n')
5✔
521
    async def broadcast(self, tx):
5✔
522
        """Broadcast a transaction to the network. """
523
        tx = Transaction(tx)
×
524
        await self.network.broadcast_transaction(tx)
×
525
        return tx.txid()
×
526

527
    @command('')
5✔
528
    async def createmultisig(self, num, pubkeys):
5✔
529
        """Create multisig address"""
530
        assert isinstance(pubkeys, list), (type(num), type(pubkeys))
×
531
        redeem_script = multisig_script(pubkeys, num)
×
532
        address = bitcoin.hash160_to_p2sh(hash_160(redeem_script))
×
533
        return {'address': address, 'redeemScript': redeem_script.hex()}
×
534

535
    @command('w')
5✔
536
    async def freeze(self, address: str, wallet: Abstract_Wallet = None):
5✔
537
        """Freeze address. Freeze the funds at one of your wallet\'s addresses"""
538
        return wallet.set_frozen_state_of_addresses([address], True)
×
539

540
    @command('w')
5✔
541
    async def unfreeze(self, address: str, wallet: Abstract_Wallet = None):
5✔
542
        """Unfreeze address. Unfreeze the funds at one of your wallet\'s address"""
543
        return wallet.set_frozen_state_of_addresses([address], False)
×
544

545
    @command('w')
5✔
546
    async def freeze_utxo(self, coin: str, wallet: Abstract_Wallet = None):
5✔
547
        """Freeze a UTXO so that the wallet will not spend it."""
548
        wallet.set_frozen_state_of_coins([coin], True)
×
549
        return True
×
550

551
    @command('w')
5✔
552
    async def unfreeze_utxo(self, coin: str, wallet: Abstract_Wallet = None):
5✔
553
        """Unfreeze a UTXO so that the wallet might spend it."""
554
        wallet.set_frozen_state_of_coins([coin], False)
×
555
        return True
×
556

557
    @command('wp')
5✔
558
    async def getprivatekeys(self, address, password=None, wallet: Abstract_Wallet = None):
5✔
559
        """Get private keys of addresses. You may pass a single wallet address, or a list of wallet addresses."""
560
        if isinstance(address, str):
5✔
561
            address = address.strip()
5✔
562
        if is_address(address):
5✔
563
            return wallet.export_private_key(address, password)
5✔
564
        domain = address
5✔
565
        return [wallet.export_private_key(address, password) for address in domain]
5✔
566

567
    @command('wp')
5✔
568
    async def getprivatekeyforpath(self, path, password=None, wallet: Abstract_Wallet = None):
5✔
569
        """Get private key corresponding to derivation path (address index).
570
        'path' can be either a str such as "m/0/50", or a list of ints such as [0, 50].
571
        """
572
        return wallet.export_private_key_for_path(path, password)
5✔
573

574
    @command('w')
5✔
575
    async def ismine(self, address, wallet: Abstract_Wallet = None):
5✔
576
        """Check if address is in wallet. Return true if and only address is in wallet"""
577
        return wallet.is_mine(address)
×
578

579
    @command('')
5✔
580
    async def dumpprivkeys(self):
5✔
581
        """Deprecated."""
582
        return "This command is deprecated. Use a pipe instead: 'electrum listaddresses | electrum getprivatekeys - '"
×
583

584
    @command('')
5✔
585
    async def validateaddress(self, address):
5✔
586
        """Check that an address is valid. """
587
        return is_address(address)
×
588

589
    @command('w')
5✔
590
    async def getpubkeys(self, address, wallet: Abstract_Wallet = None):
5✔
591
        """Return the public keys for a wallet address. """
592
        return wallet.get_public_keys(address)
×
593

594
    @command('w')
5✔
595
    async def getbalance(self, wallet: Abstract_Wallet = None):
5✔
596
        """Return the balance of your wallet. """
597
        c, u, x = wallet.get_balance()
×
598
        l = wallet.lnworker.get_balance() if wallet.lnworker else None
×
599
        out = {"confirmed": str(to_decimal(c)/COIN)}
×
600
        if u:
×
601
            out["unconfirmed"] = str(to_decimal(u)/COIN)
×
602
        if x:
×
603
            out["unmatured"] = str(to_decimal(x)/COIN)
×
604
        if l:
×
605
            out["lightning"] = str(to_decimal(l)/COIN)
×
606
        return out
×
607

608
    @command('n')
5✔
609
    async def getaddressbalance(self, address):
5✔
610
        """Return the balance of any address. Note: This is a walletless
611
        server query, results are not checked by SPV.
612
        """
613
        sh = bitcoin.address_to_scripthash(address)
×
614
        out = await self.network.get_balance_for_scripthash(sh)
×
615
        out["confirmed"] =  str(to_decimal(out["confirmed"])/COIN)
×
616
        out["unconfirmed"] =  str(to_decimal(out["unconfirmed"])/COIN)
×
617
        return out
×
618

619
    @command('n')
5✔
620
    async def getmerkle(self, txid, height):
5✔
621
        """Get Merkle branch of a transaction included in a block. Electrum
622
        uses this to verify transactions (Simple Payment Verification)."""
623
        return await self.network.get_merkle_for_transaction(txid, int(height))
×
624

625
    @command('n')
5✔
626
    async def getservers(self):
5✔
627
        """Return the list of known servers (candidates for connecting)."""
628
        return self.network.get_servers()
×
629

630
    @command('')
5✔
631
    async def version(self):
5✔
632
        """Return the version of Electrum."""
633
        return ELECTRUM_VERSION
×
634

635
    @command('')
5✔
636
    async def version_info(self):
5✔
637
        """Return information about dependencies, such as their version and path."""
638
        ret = {
×
639
            "electrum.version": ELECTRUM_VERSION,
640
            "electrum.path": os.path.dirname(os.path.realpath(__file__)),
641
            "python.version": sys.version,
642
            "python.path": sys.executable,
643
        }
644
        # add currently running GUI
645
        if self.daemon and self.daemon.gui_object:
×
646
            ret.update(self.daemon.gui_object.version_info())
×
647
        # always add Qt GUI, so we get info even when running this from CLI
648
        try:
×
649
            from .gui.qt import ElectrumGui as QtElectrumGui
×
650
            ret.update(QtElectrumGui.version_info())
×
651
        except GuiImportError:
×
652
            pass
×
653
        # Add shared libs (.so/.dll), and non-pure-python dependencies.
654
        # Such deps can be installed in various ways - often via the Linux distro's pkg manager,
655
        # instead of using pip, hence it is useful to list them for debugging.
656
        from electrum_ecc import ecc_fast
×
657
        ret.update(ecc_fast.version_info())
×
658
        from . import qrscanner
×
659
        ret.update(qrscanner.version_info())
×
660
        ret.update(DeviceMgr.version_info())
×
661
        ret.update(crypto.version_info())
×
662
        # add some special cases
663
        import aiohttp
×
664
        ret["aiohttp.version"] = aiohttp.__version__
×
665
        import aiorpcx
×
666
        ret["aiorpcx.version"] = aiorpcx._version_str
×
667
        import certifi
×
668
        ret["certifi.version"] = certifi.__version__
×
669
        import dns
×
670
        ret["dnspython.version"] = dns.__version__
×
671

672
        return ret
×
673

674
    @command('w')
5✔
675
    async def getmpk(self, wallet: Abstract_Wallet = None):
5✔
676
        """Get master public key. Return your wallet\'s master public key"""
677
        return wallet.get_master_public_key()
×
678

679
    @command('wp')
5✔
680
    async def getmasterprivate(self, password=None, wallet: Abstract_Wallet = None):
5✔
681
        """Get master private key. Return your wallet\'s master private key"""
682
        return str(wallet.keystore.get_master_private_key(password))
×
683

684
    @command('')
5✔
685
    async def convert_xkey(self, xkey, xtype):
5✔
686
        """Convert xtype of a master key. e.g. xpub -> ypub"""
687
        try:
5✔
688
            node = BIP32Node.from_xkey(xkey)
5✔
689
        except Exception:
×
690
            raise UserFacingException('xkey should be a master public/private key')
×
691
        return node._replace(xtype=xtype).to_xkey()
5✔
692

693
    @command('wp')
5✔
694
    async def getseed(self, password=None, wallet: Abstract_Wallet = None):
5✔
695
        """Get seed phrase. Print the generation seed of your wallet."""
696
        s = wallet.get_seed(password)
5✔
697
        return s
5✔
698

699
    @command('wp')
5✔
700
    async def importprivkey(self, privkey, password=None, wallet: Abstract_Wallet = None):
5✔
701
        """Import a private key or a list of private keys."""
702
        if not wallet.can_import_privkey():
5✔
703
            return "Error: This type of wallet cannot import private keys. Try to create a new wallet with that key."
×
704
        assert isinstance(wallet, Imported_Wallet)
5✔
705
        keys = privkey.split()
5✔
706
        if not keys:
5✔
707
            return "Error: no keys given"
5✔
708
        elif len(keys) == 1:
5✔
709
            try:
5✔
710
                addr = wallet.import_private_key(keys[0], password)
5✔
711
                out = "Keypair imported: " + addr
5✔
712
            except Exception as e:
5✔
713
                out = "Error: " + repr(e)
5✔
714
            return out
5✔
715
        else:
716
            good_inputs, bad_inputs = wallet.import_private_keys(keys, password)
5✔
717
            return {
5✔
718
                "good_keys": len(good_inputs),
719
                "bad_keys": len(bad_inputs),
720
            }
721

722
    def _resolver(self, x, wallet: Abstract_Wallet):
5✔
723
        if x is None:
5✔
724
            return None
5✔
725
        out = wallet.contacts.resolve(x)
5✔
726
        if out.get('type') == 'openalias' and self.nocheck is False and out.get('validated') is False:
5✔
727
            raise UserFacingException(f"cannot verify alias: {x}")
×
728
        return out['address']
5✔
729

730
    @command('n')
5✔
731
    async def sweep(self, privkey, destination, fee=None, nocheck=False, imax=100):
5✔
732
        """Sweep private keys. Returns a transaction that spends UTXOs from
733
        privkey to a destination address. The transaction is not
734
        broadcasted."""
735
        from .wallet import sweep
×
736
        tx_fee = satoshis(fee)
×
737
        privkeys = privkey.split()
×
738
        self.nocheck = nocheck
×
739
        #dest = self._resolver(destination)
740
        tx = await sweep(
×
741
            privkeys,
742
            network=self.network,
743
            config=self.config,
744
            to_address=destination,
745
            fee=tx_fee,
746
            imax=imax,
747
        )
748
        return tx.serialize() if tx else None
×
749

750
    @command('wp')
5✔
751
    async def signmessage(self, address, message, password=None, wallet: Abstract_Wallet = None):
5✔
752
        """Sign a message with a key. Use quotes if your message contains
753
        whitespaces"""
754
        sig = wallet.sign_message(address, message, password)
×
755
        return base64.b64encode(sig).decode('ascii')
×
756

757
    @command('')
5✔
758
    async def verifymessage(self, address, signature, message):
5✔
759
        """Verify a signature."""
760
        sig = base64.b64decode(signature)
×
761
        message = util.to_bytes(message)
×
762
        return bitcoin.verify_usermessage_with_address(address, sig, message)
×
763

764
    @command('wp')
5✔
765
    async def payto(self, destination, amount, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None,
5✔
766
                    nocheck=False, unsigned=False, rbf=True, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None):
767
        """Create a transaction. """
768
        self.nocheck = nocheck
5✔
769
        tx_fee = satoshis(fee)
5✔
770
        domain_addr = from_addr.split(',') if from_addr else None
5✔
771
        domain_coins = from_coins.split(',') if from_coins else None
5✔
772
        change_addr = self._resolver(change_addr, wallet)
5✔
773
        domain_addr = None if domain_addr is None else map(self._resolver, domain_addr, repeat(wallet))
5✔
774
        amount_sat = satoshis_or_max(amount)
5✔
775
        outputs = [PartialTxOutput.from_address_and_value(destination, amount_sat)]
5✔
776
        tx = wallet.create_transaction(
5✔
777
            outputs,
778
            fee=tx_fee,
779
            feerate=feerate,
780
            change_addr=change_addr,
781
            domain_addr=domain_addr,
782
            domain_coins=domain_coins,
783
            sign=not unsigned,
784
            rbf=rbf,
785
            password=password,
786
            locktime=locktime)
787
        result = tx.serialize()
5✔
788
        if addtransaction:
5✔
789
            await self.addtransaction(result, wallet=wallet)
×
790
        return result
5✔
791

792
    @command('wp')
5✔
793
    async def paytomany(self, outputs, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None,
5✔
794
                        nocheck=False, unsigned=False, rbf=True, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None):
795
        """Create a multi-output transaction. """
796
        self.nocheck = nocheck
5✔
797
        tx_fee = satoshis(fee)
5✔
798
        domain_addr = from_addr.split(',') if from_addr else None
5✔
799
        domain_coins = from_coins.split(',') if from_coins else None
5✔
800
        change_addr = self._resolver(change_addr, wallet)
5✔
801
        domain_addr = None if domain_addr is None else map(self._resolver, domain_addr, repeat(wallet))
5✔
802
        final_outputs = []
5✔
803
        for address, amount in outputs:
5✔
804
            address = self._resolver(address, wallet)
5✔
805
            amount_sat = satoshis_or_max(amount)
5✔
806
            final_outputs.append(PartialTxOutput.from_address_and_value(address, amount_sat))
5✔
807
        tx = wallet.create_transaction(
5✔
808
            final_outputs,
809
            fee=tx_fee,
810
            feerate=feerate,
811
            change_addr=change_addr,
812
            domain_addr=domain_addr,
813
            domain_coins=domain_coins,
814
            sign=not unsigned,
815
            rbf=rbf,
816
            password=password,
817
            locktime=locktime)
818
        result = tx.serialize()
5✔
819
        if addtransaction:
5✔
820
            await self.addtransaction(result, wallet=wallet)
×
821
        return result
5✔
822

823
    def get_year_timestamps(self, year:int):
5✔
824
        kwargs = {}
×
825
        if year:
×
826
            import time
×
827
            start_date = datetime.datetime(year, 1, 1)
×
828
            end_date = datetime.datetime(year+1, 1, 1)
×
829
            kwargs['from_timestamp'] = time.mktime(start_date.timetuple())
×
830
            kwargs['to_timestamp'] = time.mktime(end_date.timetuple())
×
831
        return kwargs
×
832

833
    @command('w')
5✔
834
    async def onchain_capital_gains(self, year=None, wallet: Abstract_Wallet = None):
5✔
835
        """
836
        Capital gains, using utxo pricing.
837
        This cannot be used with lightning.
838
        """
839
        kwargs = self.get_year_timestamps(year)
×
840
        from .exchange_rate import FxThread
×
841
        fx = self.daemon.fx if self.daemon else FxThread(config=self.config)
×
842
        return json_normalize(wallet.get_onchain_capital_gains(fx, **kwargs))
×
843

844
    @command('wp')
5✔
845
    async def bumpfee(self, tx, new_fee_rate, from_coins=None, decrease_payment=False, password=None, unsigned=False, wallet: Abstract_Wallet = None):
5✔
846
        """Bump the fee for an unconfirmed transaction.
847
        'tx' can be either a raw hex tx or a txid. If txid, the corresponding tx must already be part of the wallet history.
848
        """
849
        if is_hash256_str(tx):  # txid
5✔
850
            tx = wallet.db.get_transaction(tx)
5✔
851
            if tx is None:
5✔
852
                raise UserFacingException("Transaction not in wallet.")
5✔
853
        else:  # raw tx
854
            try:
5✔
855
                tx = Transaction(tx)
5✔
856
                tx.deserialize()
5✔
857
            except transaction.SerializationError as e:
×
858
                raise UserFacingException(f"Failed to deserialize transaction: {e}") from e
×
859
        domain_coins = from_coins.split(',') if from_coins else None
5✔
860
        coins = wallet.get_spendable_coins(None)
5✔
861
        if domain_coins is not None:
5✔
862
            coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)]
5✔
863
        tx.add_info_from_wallet(wallet)
5✔
864
        await tx.add_info_from_network(self.network)
5✔
865
        new_tx = wallet.bump_fee(
5✔
866
            tx=tx,
867
            coins=coins,
868
            strategy=BumpFeeStrategy.DECREASE_PAYMENT if decrease_payment else BumpFeeStrategy.PRESERVE_PAYMENT,
869
            new_fee_rate=new_fee_rate)
870
        if not unsigned:
5✔
871
            wallet.sign_transaction(new_tx, password)
5✔
872
        return new_tx.serialize()
5✔
873

874
    @command('w')
5✔
875
    async def onchain_history(self, show_fiat=False, year=None, show_addresses=False, wallet: Abstract_Wallet = None):
5✔
876
        """Wallet onchain history. Returns the transaction history of your wallet."""
877
        kwargs = self.get_year_timestamps(year)
×
878
        onchain_history = wallet.get_onchain_history(**kwargs)
×
879
        out = [x.to_dict() for x in onchain_history.values()]
×
880
        if show_fiat:
×
881
            from .exchange_rate import FxThread
×
882
            fx = self.daemon.fx if self.daemon else FxThread(config=self.config)
×
883
        else:
884
            fx = None
×
885
        for item in out:
×
886
            if show_addresses:
×
887
                tx = wallet.db.get_transaction(item['txid'])
×
888
                item['inputs'] = list(map(lambda x: x.to_json(), tx.inputs()))
×
889
                item['outputs'] = list(map(lambda x: {'address': x.get_ui_address_str(), 'value_sat': x.value},
×
890
                                           tx.outputs()))
891
            if fx:
×
892
                fiat_fields = wallet.get_tx_item_fiat(tx_hash=item['txid'], amount_sat=item['amount_sat'], fx=fx, tx_fee=item['fee_sat'])
×
893
                item.update(fiat_fields)
×
894
        return json_normalize(out)
×
895

896
    @command('wl')
5✔
897
    async def lightning_history(self, wallet: Abstract_Wallet = None):
5✔
898
        """ lightning history. """
899
        lightning_history = wallet.lnworker.get_lightning_history() if wallet.lnworker else {}
×
900
        sorted_hist= sorted(lightning_history.values(), key=lambda x: x.timestamp)
×
901
        return json_normalize([x.to_dict() for x in sorted_hist])
×
902

903
    @command('w')
5✔
904
    async def setlabel(self, key, label, wallet: Abstract_Wallet = None):
5✔
905
        """Assign a label to an item. Item may be a bitcoin address or a
906
        transaction ID"""
907
        wallet.set_label(key, label)
×
908

909
    @command('w')
5✔
910
    async def listcontacts(self, wallet: Abstract_Wallet = None):
5✔
911
        """Show your list of contacts"""
912
        return wallet.contacts
×
913

914
    @command('w')
5✔
915
    async def getalias(self, key, wallet: Abstract_Wallet = None):
5✔
916
        """Retrieve alias. Lookup in your list of contacts, and for an OpenAlias DNS record."""
917
        return wallet.contacts.resolve(key)
×
918

919
    @command('w')
5✔
920
    async def searchcontacts(self, query, wallet: Abstract_Wallet = None):
5✔
921
        """Search through contacts, return matching entries. """
922
        results = {}
×
923
        for key, value in wallet.contacts.items():
×
924
            if query.lower() in key.lower():
×
925
                results[key] = value
×
926
        return results
×
927

928
    @command('w')
5✔
929
    async def listaddresses(self, receiving=False, change=False, labels=False, frozen=False, unused=False, funded=False, balance=False, wallet: Abstract_Wallet = None):
5✔
930
        """List wallet addresses. Returns the list of all addresses in your wallet. Use optional arguments to filter the results."""
931
        out = []
×
932
        for addr in wallet.get_addresses():
×
933
            if frozen and not wallet.is_frozen_address(addr):
×
934
                continue
×
935
            if receiving and wallet.is_change(addr):
×
936
                continue
×
937
            if change and not wallet.is_change(addr):
×
938
                continue
×
939
            if unused and wallet.adb.is_used(addr):
×
940
                continue
×
941
            if funded and wallet.adb.is_empty(addr):
×
942
                continue
×
943
            item = addr
×
944
            if labels or balance:
×
945
                item = (item,)
×
946
            if balance:
×
947
                item += (format_satoshis(sum(wallet.get_addr_balance(addr))),)
×
948
            if labels:
×
949
                item += (repr(wallet.get_label_for_address(addr)),)
×
950
            out.append(item)
×
951
        return out
×
952

953
    @command('n')
5✔
954
    async def gettransaction(self, txid, wallet: Abstract_Wallet = None):
5✔
955
        """Retrieve a transaction. """
956
        tx = None
×
957
        if wallet:
×
958
            tx = wallet.db.get_transaction(txid)
×
959
        if tx is None:
×
960
            raw = await self.network.get_transaction(txid)
×
961
            if raw:
×
962
                tx = Transaction(raw)
×
963
            else:
964
                raise UserFacingException("Unknown transaction")
×
965
        if tx.txid() != txid:
×
966
            raise UserFacingException("Mismatching txid")
×
967
        return tx.serialize()
×
968

969
    @command('')
5✔
970
    async def encrypt(self, pubkey, message) -> str:
5✔
971
        """Encrypt a message with a public key. Use quotes if the message contains whitespaces."""
972
        if not is_hex_str(pubkey):
5✔
973
            raise UserFacingException(f"pubkey must be a hex string instead of {repr(pubkey)}")
×
974
        try:
5✔
975
            message = to_bytes(message)
5✔
976
        except TypeError:
×
977
            raise UserFacingException(f"message must be a string-like object instead of {repr(message)}")
×
978
        public_key = ecc.ECPubkey(bfh(pubkey))
5✔
979
        encrypted = crypto.ecies_encrypt_message(public_key, message)
5✔
980
        return encrypted.decode('utf-8')
5✔
981

982
    @command('wp')
5✔
983
    async def decrypt(self, pubkey, encrypted, password=None, wallet: Abstract_Wallet = None) -> str:
5✔
984
        """Decrypt a message encrypted with a public key."""
985
        if not is_hex_str(pubkey):
5✔
986
            raise UserFacingException(f"pubkey must be a hex string instead of {repr(pubkey)}")
×
987
        if not isinstance(encrypted, (str, bytes, bytearray)):
5✔
988
            raise UserFacingException(f"encrypted must be a string-like object instead of {repr(encrypted)}")
×
989
        decrypted = wallet.decrypt_message(pubkey, encrypted, password)
5✔
990
        return decrypted.decode('utf-8')
5✔
991

992
    @command('w')
5✔
993
    async def get_request(self, request_id, wallet: Abstract_Wallet = None):
5✔
994
        """Returns a payment request"""
995
        r = wallet.get_request(request_id)
×
996
        if not r:
×
997
            raise UserFacingException("Request not found")
×
998
        return wallet.export_request(r)
×
999

1000
    @command('w')
5✔
1001
    async def get_invoice(self, invoice_id, wallet: Abstract_Wallet = None):
5✔
1002
        """Returns an invoice (request for outgoing payment)"""
1003
        r = wallet.get_invoice(invoice_id)
×
1004
        if not r:
×
1005
            raise UserFacingException("Request not found")
×
1006
        return wallet.export_invoice(r)
×
1007

1008
    #@command('w')
1009
    #async def ackrequest(self, serialized):
1010
    #    """<Not implemented>"""
1011
    #    pass
1012

1013
    def _filter_invoices(self, _list, wallet, pending, expired, paid):
5✔
1014
        if pending:
×
1015
            f = PR_UNPAID
×
1016
        elif expired:
×
1017
            f = PR_EXPIRED
×
1018
        elif paid:
×
1019
            f = PR_PAID
×
1020
        else:
1021
            f = None
×
1022
        if f is not None:
×
1023
            _list = [x for x in _list if f == wallet.get_invoice_status(x)]
×
1024
        return _list
×
1025

1026
    @command('w')
5✔
1027
    async def list_requests(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None):
5✔
1028
        """Returns the list of incoming payment requests saved in the wallet."""
1029
        l = wallet.get_sorted_requests()
×
1030
        l = self._filter_invoices(l, wallet, pending, expired, paid)
×
1031
        return [wallet.export_request(x) for x in l]
×
1032

1033
    @command('w')
5✔
1034
    async def list_invoices(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None):
5✔
1035
        """Returns the list of invoices (requests for outgoing payments) saved in the wallet."""
1036
        l = wallet.get_invoices()
×
1037
        l = self._filter_invoices(l, wallet, pending, expired, paid)
×
1038
        return [wallet.export_invoice(x) for x in l]
×
1039

1040
    @command('w')
5✔
1041
    async def createnewaddress(self, wallet: Abstract_Wallet = None):
5✔
1042
        """Create a new receiving address, beyond the gap limit of the wallet"""
1043
        return wallet.create_new_address(False)
×
1044

1045
    @command('w')
5✔
1046
    async def changegaplimit(self, new_limit, iknowwhatimdoing=False, wallet: Abstract_Wallet = None):
5✔
1047
        """Change the gap limit of the wallet."""
1048
        if not iknowwhatimdoing:
×
1049
            raise UserFacingException(
×
1050
                "WARNING: Are you SURE you want to change the gap limit?\n"
1051
                "It makes recovering your wallet from seed difficult!\n"
1052
                "Please do your research and make sure you understand the implications.\n"
1053
                "Typically only merchants and power users might want to do this.\n"
1054
                "To proceed, try again, with the --iknowwhatimdoing option.")
1055
        if not isinstance(wallet, Deterministic_Wallet):
×
1056
            raise UserFacingException("This wallet is not deterministic.")
×
1057
        return wallet.change_gap_limit(new_limit)
×
1058

1059
    @command('wn')
5✔
1060
    async def getminacceptablegap(self, wallet: Abstract_Wallet = None):
5✔
1061
        """Returns the minimum value for gap limit that would be sufficient to discover all
1062
        known addresses in the wallet.
1063
        """
1064
        if not isinstance(wallet, Deterministic_Wallet):
×
1065
            raise UserFacingException("This wallet is not deterministic.")
×
1066
        if not wallet.is_up_to_date():
×
1067
            raise NotSynchronizedException("Wallet not fully synchronized.")
×
1068
        return wallet.min_acceptable_gap()
×
1069

1070
    @command('w')
5✔
1071
    async def getunusedaddress(self, wallet: Abstract_Wallet = None):
5✔
1072
        """Returns the first unused address of the wallet, or None if all addresses are used.
1073
        An address is considered as used if it has received a transaction, or if it is used in a payment request."""
1074
        return wallet.get_unused_address()
×
1075

1076
    @command('w')
5✔
1077
    async def add_request(self, amount, memo='', expiry=3600, lightning=False, force=False, wallet: Abstract_Wallet = None):
5✔
1078
        """Create a payment request, using the first unused address of the wallet.
1079
        The address will be considered as used after this operation.
1080
        If no payment is received, the address will be considered as unused if the payment request is deleted from the wallet."""
1081
        amount = satoshis(amount)
×
NEW
1082
        if not lightning:
×
NEW
1083
            addr = wallet.get_unused_address()
×
NEW
1084
            if addr is None:
×
NEW
1085
                if force:
×
NEW
1086
                    addr = wallet.create_new_address(False)
×
1087
                else:
NEW
1088
                    return False
×
1089
        else:
NEW
1090
            addr = None
×
1091
        expiry = int(expiry) if expiry else None
×
1092
        key = wallet.create_request(amount, memo, expiry, addr)
×
1093
        req = wallet.get_request(key)
×
1094
        return wallet.export_request(req)
×
1095

1096
    @command('w')
5✔
1097
    async def addtransaction(self, tx, wallet: Abstract_Wallet = None):
5✔
1098
        """ Add a transaction to the wallet history """
1099
        tx = Transaction(tx)
×
1100
        if not wallet.adb.add_transaction(tx):
×
1101
            return False
×
1102
        wallet.save_db()
×
1103
        return tx.txid()
×
1104

1105
    @command('w')
5✔
1106
    async def delete_request(self, request_id, wallet: Abstract_Wallet = None):
5✔
1107
        """Remove an incoming payment request"""
1108
        return wallet.delete_request(request_id)
×
1109

1110
    @command('w')
5✔
1111
    async def delete_invoice(self, invoice_id, wallet: Abstract_Wallet = None):
5✔
1112
        """Remove an outgoing payment invoice"""
1113
        return wallet.delete_invoice(invoice_id)
×
1114

1115
    @command('w')
5✔
1116
    async def clear_requests(self, wallet: Abstract_Wallet = None):
5✔
1117
        """Remove all payment requests"""
1118
        wallet.clear_requests()
×
1119
        return True
×
1120

1121
    @command('w')
5✔
1122
    async def clear_invoices(self, wallet: Abstract_Wallet = None):
5✔
1123
        """Remove all invoices"""
1124
        wallet.clear_invoices()
×
1125
        return True
×
1126

1127
    @command('n')
5✔
1128
    async def notify(self, address: str, URL: Optional[str]):
5✔
1129
        """Watch an address. Every time the address changes, a http POST is sent to the URL.
1130
        Call with an empty URL to stop watching an address.
1131
        """
1132
        if not hasattr(self, "_notifier"):
×
1133
            self._notifier = Notifier(self.network)
×
1134
        if URL:
×
1135
            await self._notifier.start_watching_addr(address, URL)
×
1136
        else:
1137
            await self._notifier.stop_watching_addr(address)
×
1138
        return True
×
1139

1140
    @command('wn')
5✔
1141
    async def is_synchronized(self, wallet: Abstract_Wallet = None):
5✔
1142
        """ return wallet synchronization status """
1143
        return wallet.is_up_to_date()
×
1144

1145
    @command('')
5✔
1146
    async def getfeerate(self):
5✔
1147
        """Return current fee rate settings and current estimate (in sat/kvByte).
1148
        """
1149
        method, value, feerate, tooltip = self.config.getfeerate()
×
1150
        return {
×
1151
            'method': method,
1152
            'value': value,
1153
            'sat/kvB': feerate,
1154
            'tooltip': tooltip,
1155
        }
1156

1157
    @command('')
5✔
1158
    async def setfeerate(self, method, value):
5✔
1159
        """Set fee rate estimation method and value"""
1160
        self.config.setfeerate(method, value)
×
1161

1162
    @command('w')
5✔
1163
    async def removelocaltx(self, txid, wallet: Abstract_Wallet = None):
5✔
1164
        """Remove a 'local' transaction from the wallet, and its dependent
1165
        transactions.
1166
        """
1167
        if not is_hash256_str(txid):
×
1168
            raise UserFacingException(f"{repr(txid)} is not a txid")
×
1169
        height = wallet.adb.get_tx_height(txid).height
×
1170
        if height != TX_HEIGHT_LOCAL:
×
1171
            raise UserFacingException(
×
1172
                f'Only local transactions can be removed. '
1173
                f'This tx has height: {height} != {TX_HEIGHT_LOCAL}')
1174
        wallet.adb.remove_transaction(txid)
×
1175
        wallet.save_db()
×
1176

1177
    @command('wn')
5✔
1178
    async def get_tx_status(self, txid, wallet: Abstract_Wallet = None):
5✔
1179
        """Returns some information regarding the tx. For now, only confirmations.
1180
        The transaction must be related to the wallet.
1181
        """
1182
        if not is_hash256_str(txid):
×
1183
            raise UserFacingException(f"{repr(txid)} is not a txid")
×
1184
        if not wallet.db.get_transaction(txid):
×
1185
            raise UserFacingException("Transaction not in wallet.")
×
1186
        return {
×
1187
            "confirmations": wallet.adb.get_tx_height(txid).conf,
1188
        }
1189

1190
    @command('')
5✔
1191
    async def help(self):
5✔
1192
        # for the python console
1193
        return sorted(known_commands.keys())
×
1194

1195
    # lightning network commands
1196
    @command('wnl')
5✔
1197
    async def add_peer(self, connection_string, timeout=20, gossip=False, wallet: Abstract_Wallet = None):
5✔
1198
        lnworker = self.network.lngossip if gossip else wallet.lnworker
×
1199
        await lnworker.add_peer(connection_string)
×
1200
        return True
×
1201

1202
    @command('wnl')
5✔
1203
    async def list_peers(self, gossip=False, wallet: Abstract_Wallet = None):
5✔
1204
        lnworker = self.network.lngossip if gossip else wallet.lnworker
×
1205
        return [{
×
1206
            'node_id':p.pubkey.hex(),
1207
            'address':p.transport.name(),
1208
            'initialized':p.is_initialized(),
1209
            'features': str(LnFeatures(p.features)),
1210
            'channels': [c.funding_outpoint.to_str() for c in p.channels.values()],
1211
        } for p in lnworker.peers.values()]
1212

1213
    @command('wpnl')
5✔
1214
    async def open_channel(self, connection_string, amount, push_amount=0, public=False, zeroconf=False, password=None, wallet: Abstract_Wallet = None):
5✔
1215
        funding_sat = satoshis(amount)
×
1216
        push_sat = satoshis(push_amount)
×
1217
        peer = await wallet.lnworker.add_peer(connection_string)
×
1218
        chan, funding_tx = await wallet.lnworker.open_channel_with_peer(
×
1219
            peer, funding_sat,
1220
            push_sat=push_sat,
1221
            public=public,
1222
            zeroconf=zeroconf,
1223
            password=password)
1224
        return chan.funding_outpoint.to_str()
×
1225

1226
    @command('')
5✔
1227
    async def decode_invoice(self, invoice: str):
5✔
1228
        invoice = Invoice.from_bech32(invoice)
×
1229
        return invoice.to_debug_json()
×
1230

1231
    @command('wnpl')
5✔
1232
    async def lnpay(self, invoice, timeout=120, password=None, wallet: Abstract_Wallet = None):
5✔
1233
        lnworker = wallet.lnworker
×
1234
        lnaddr = lnworker._check_invoice(invoice)
×
1235
        payment_hash = lnaddr.paymenthash
×
1236
        wallet.save_invoice(Invoice.from_bech32(invoice))
×
1237
        success, log = await lnworker.pay_invoice(invoice)
×
1238
        return {
×
1239
            'payment_hash': payment_hash.hex(),
1240
            'success': success,
1241
            'preimage': lnworker.get_preimage(payment_hash).hex() if success else None,
1242
            'log': [x.formatted_tuple() for x in log]
1243
        }
1244

1245
    @command('wl')
5✔
1246
    async def nodeid(self, wallet: Abstract_Wallet = None):
5✔
1247
        listen_addr = self.config.LIGHTNING_LISTEN
×
1248
        return wallet.lnworker.node_keypair.pubkey.hex() + (('@' + listen_addr) if listen_addr else '')
×
1249

1250
    @command('wl')
5✔
1251
    async def list_channels(self, wallet: Abstract_Wallet = None):
5✔
1252
        # FIXME: we need to be online to display capacity of backups
1253
        from .lnutil import LOCAL, REMOTE, format_short_channel_id
×
1254
        channels = list(wallet.lnworker.channels.items())
×
1255
        backups = list(wallet.lnworker.channel_backups.items())
×
1256
        return [
×
1257
            {
1258
                'type': 'CHANNEL',
1259
                'short_channel_id': format_short_channel_id(chan.short_channel_id) if chan.short_channel_id else None,
1260
                'channel_id': chan.channel_id.hex(),
1261
                'channel_point': chan.funding_outpoint.to_str(),
1262
                'state': chan.get_state().name,
1263
                'peer_state': chan.peer_state.name,
1264
                'remote_pubkey': chan.node_id.hex(),
1265
                'local_balance': chan.balance(LOCAL)//1000,
1266
                'remote_balance': chan.balance(REMOTE)//1000,
1267
                'local_ctn': chan.get_latest_ctn(LOCAL),
1268
                'remote_ctn': chan.get_latest_ctn(REMOTE),
1269
                'local_reserve': chan.config[REMOTE].reserve_sat, # their config has our reserve
1270
                'remote_reserve': chan.config[LOCAL].reserve_sat,
1271
                'local_unsettled_sent': chan.balance_tied_up_in_htlcs_by_direction(LOCAL, direction=SENT) // 1000,
1272
                'remote_unsettled_sent': chan.balance_tied_up_in_htlcs_by_direction(REMOTE, direction=SENT) // 1000,
1273
            } for channel_id, chan in channels
1274
        ] + [
1275
            {
1276
                'type': 'BACKUP',
1277
                'short_channel_id': format_short_channel_id(chan.short_channel_id) if chan.short_channel_id else None,
1278
                'channel_id': chan.channel_id.hex(),
1279
                'channel_point': chan.funding_outpoint.to_str(),
1280
                'state': chan.get_state().name,
1281
            } for channel_id, chan in backups
1282
        ]
1283

1284
    @command('wnl')
5✔
1285
    async def enable_htlc_settle(self, b: bool, wallet: Abstract_Wallet = None):
5✔
1286
        wallet.lnworker.enable_htlc_settle = b
×
1287

1288
    @command('n')
5✔
1289
    async def clear_ln_blacklist(self):
5✔
1290
        if self.network.path_finder:
×
1291
            self.network.path_finder.clear_blacklist()
×
1292

1293
    @command('n')
5✔
1294
    async def reset_liquidity_hints(self):
5✔
1295
        if self.network.path_finder:
×
1296
            self.network.path_finder.liquidity_hints.reset_liquidity_hints()
×
1297
            self.network.path_finder.clear_blacklist()
×
1298

1299
    @command('wnpl')
5✔
1300
    async def close_channel(self, channel_point, force=False, password=None, wallet: Abstract_Wallet = None):
5✔
1301
        txid, index = channel_point.split(':')
×
1302
        chan_id, _ = channel_id_from_funding_tx(txid, int(index))
×
1303
        if chan_id not in wallet.lnworker.channels:
×
1304
            raise UserFacingException(f'Unknown channel {channel_point}')
×
1305
        coro = wallet.lnworker.force_close_channel(chan_id) if force else wallet.lnworker.close_channel(chan_id)
×
1306
        return await coro
×
1307

1308
    @command('wnpl')
5✔
1309
    async def request_force_close(self, channel_point, connection_string=None, password=None, wallet: Abstract_Wallet = None):
5✔
1310
        """
1311
        Requests the remote to force close a channel.
1312
        If a connection string is passed, can be used without having state or any backup for the channel.
1313
        Assumes that channel was originally opened with the same local peer (node_keypair).
1314
        """
1315
        txid, index = channel_point.split(':')
×
1316
        chan_id, _ = channel_id_from_funding_tx(txid, int(index))
×
1317
        if chan_id not in wallet.lnworker.channels and chan_id not in wallet.lnworker.channel_backups:
×
1318
            raise UserFacingException(f'Unknown channel {channel_point}')
×
1319
        await wallet.lnworker.request_force_close(chan_id, connect_str=connection_string)
×
1320

1321
    @command('wpl')
5✔
1322
    async def export_channel_backup(self, channel_point, password=None, wallet: Abstract_Wallet = None):
5✔
1323
        txid, index = channel_point.split(':')
×
1324
        chan_id, _ = channel_id_from_funding_tx(txid, int(index))
×
1325
        if chan_id not in wallet.lnworker.channels:
×
1326
            raise UserFacingException(f'Unknown channel {channel_point}')
×
1327
        return wallet.lnworker.export_channel_backup(chan_id)
×
1328

1329
    @command('wl')
5✔
1330
    async def import_channel_backup(self, encrypted, wallet: Abstract_Wallet = None):
5✔
1331
        return wallet.lnworker.import_channel_backup(encrypted)
×
1332

1333
    @command('wnpl')
5✔
1334
    async def get_channel_ctx(self, channel_point, password=None, iknowwhatimdoing=False, wallet: Abstract_Wallet = None):
5✔
1335
        """ return the current commitment transaction of a channel """
1336
        if not iknowwhatimdoing:
×
1337
            raise UserFacingException(
×
1338
                "WARNING: this command is potentially unsafe.\n"
1339
                "To proceed, try again, with the --iknowwhatimdoing option.")
1340
        txid, index = channel_point.split(':')
×
1341
        chan_id, _ = channel_id_from_funding_tx(txid, int(index))
×
1342
        if chan_id not in wallet.lnworker.channels:
×
1343
            raise UserFacingException(f'Unknown channel {channel_point}')
×
1344
        chan = wallet.lnworker.channels[chan_id]
×
1345
        tx = chan.force_close_tx()
×
1346
        return tx.serialize()
×
1347

1348
    @command('wnl')
5✔
1349
    async def get_watchtower_ctn(self, channel_point, wallet: Abstract_Wallet = None):
5✔
1350
        """ return the local watchtower's ctn of channel. used in regtests """
1351
        return wallet.lnworker.get_watchtower_ctn(channel_point)
×
1352

1353
    @command('wnpl')
5✔
1354
    async def rebalance_channels(self, from_scid, dest_scid, amount, password=None, wallet: Abstract_Wallet = None):
5✔
1355
        """
1356
        Rebalance channels.
1357
        If trampoline is used, channels must be with different trampolines.
1358
        """
1359
        from .lnutil import ShortChannelID
×
1360
        from_scid = ShortChannelID.from_str(from_scid)
×
1361
        dest_scid = ShortChannelID.from_str(dest_scid)
×
1362
        from_channel = wallet.lnworker.get_channel_by_short_id(from_scid)
×
1363
        dest_channel = wallet.lnworker.get_channel_by_short_id(dest_scid)
×
1364
        amount_sat = satoshis(amount)
×
1365
        success, log = await wallet.lnworker.rebalance_channels(
×
1366
            from_channel,
1367
            dest_channel,
1368
            amount_msat=amount_sat * 1000,
1369
        )
1370
        return {
×
1371
            'success': success,
1372
            'log': [x.formatted_tuple() for x in log]
1373
        }
1374

1375
    @command('wnpl')
5✔
1376
    async def normal_swap(self, onchain_amount, lightning_amount, password=None, wallet: Abstract_Wallet = None):
5✔
1377
        """
1378
        Normal submarine swap: send on-chain BTC, receive on Lightning
1379
        """
1380
        sm = wallet.lnworker.swap_manager
×
1381
        with sm.create_transport() as transport:
×
1382
            await sm.is_initialized.wait()
×
1383
            if lightning_amount == 'dryrun':
×
1384
                onchain_amount_sat = satoshis(onchain_amount)
×
1385
                lightning_amount_sat = sm.get_recv_amount(onchain_amount_sat, is_reverse=False)
×
1386
                txid = None
×
1387
            elif onchain_amount == 'dryrun':
×
1388
                lightning_amount_sat = satoshis(lightning_amount)
×
1389
                onchain_amount_sat = sm.get_send_amount(lightning_amount_sat, is_reverse=False)
×
1390
                txid = None
×
1391
            else:
1392
                lightning_amount_sat = satoshis(lightning_amount)
×
1393
                onchain_amount_sat = satoshis(onchain_amount)
×
1394
                txid = await wallet.lnworker.swap_manager.normal_swap(
×
1395
                    transport,
1396
                    lightning_amount_sat=lightning_amount_sat,
1397
                    expected_onchain_amount_sat=onchain_amount_sat,
1398
                    password=password,
1399
                )
1400

1401
        return {
×
1402
            'txid': txid,
1403
            'lightning_amount': format_satoshis(lightning_amount_sat),
1404
            'onchain_amount': format_satoshis(onchain_amount_sat),
1405
        }
1406

1407
    @command('wnpl')
5✔
1408
    async def reverse_swap(self, lightning_amount, onchain_amount, password=None, wallet: Abstract_Wallet = None):
5✔
1409
        """Reverse submarine swap: send on Lightning, receive on-chain
1410
        """
1411
        sm = wallet.lnworker.swap_manager
×
1412
        with sm.create_transport() as transport:
×
1413
            await sm.is_initialized.wait()
×
1414
            if onchain_amount == 'dryrun':
×
1415
                lightning_amount_sat = satoshis(lightning_amount)
×
1416
                onchain_amount_sat = sm.get_recv_amount(lightning_amount_sat, is_reverse=True)
×
1417
                funding_txid = None
×
1418
            elif lightning_amount == 'dryrun':
×
1419
                onchain_amount_sat = satoshis(onchain_amount)
×
1420
                lightning_amount_sat = sm.get_send_amount(onchain_amount_sat, is_reverse=True)
×
1421
                funding_txid = None
×
1422
            else:
1423
                lightning_amount_sat = satoshis(lightning_amount)
×
1424
                claim_fee = sm.get_swap_tx_fee()
×
1425
                onchain_amount_sat = satoshis(onchain_amount) + claim_fee
×
1426
                funding_txid = await wallet.lnworker.swap_manager.reverse_swap(
×
1427
                    transport,
1428
                    lightning_amount_sat=lightning_amount_sat,
1429
                    expected_onchain_amount_sat=onchain_amount_sat,
1430
                )
1431
        return {
×
1432
            'funding_txid': funding_txid,
1433
            'lightning_amount': format_satoshis(lightning_amount_sat),
1434
            'onchain_amount': format_satoshis(onchain_amount_sat),
1435
        }
1436

1437
    @command('n')
5✔
1438
    async def convert_currency(self, from_amount=1, from_ccy = '', to_ccy = ''):
5✔
1439
        """Converts the given amount of currency to another using the
1440
        configured exchange rate source.
1441
        """
1442
        if not self.daemon.fx.is_enabled():
×
1443
            raise UserFacingException("FX is disabled. To enable, run: 'electrum setconfig use_exchange_rate true'")
×
1444
        # Currency codes are uppercase
1445
        from_ccy = from_ccy.upper()
×
1446
        to_ccy = to_ccy.upper()
×
1447
        # Default currencies
1448
        if from_ccy == '':
×
1449
            from_ccy = "BTC" if to_ccy != "BTC" else self.daemon.fx.ccy
×
1450
        if to_ccy == '':
×
1451
            to_ccy = "BTC" if from_ccy != "BTC" else self.daemon.fx.ccy
×
1452
        # Get current rates
1453
        rate_from = self.daemon.fx.exchange.get_cached_spot_quote(from_ccy)
×
1454
        rate_to = self.daemon.fx.exchange.get_cached_spot_quote(to_ccy)
×
1455
        # Test if currencies exist
1456
        if rate_from.is_nan():
×
1457
            raise UserFacingException(f'Currency to convert from ({from_ccy}) is unknown or rate is unavailable')
×
1458
        if rate_to.is_nan():
×
1459
            raise UserFacingException(f'Currency to convert to ({to_ccy}) is unknown or rate is unavailable')
×
1460
        # Conversion
1461
        try:
×
1462
            from_amount = to_decimal(from_amount)
×
1463
            to_amount = from_amount / rate_from * rate_to
×
1464
        except InvalidOperation:
×
1465
            raise Exception("from_amount is not a number")
×
1466
        return {
×
1467
            "from_amount": self.daemon.fx.ccy_amount_str(from_amount, add_thousands_sep=False, ccy=from_ccy),
1468
            "to_amount": self.daemon.fx.ccy_amount_str(to_amount, add_thousands_sep=False, ccy=to_ccy),
1469
            "from_ccy": from_ccy,
1470
            "to_ccy": to_ccy,
1471
            "source": self.daemon.fx.exchange.name(),
1472
        }
1473

1474
    @command('wnl')
5✔
1475
    async def send_onion_message(self, node_id_or_blinded_path_hex: str, message: str, wallet: Abstract_Wallet = None):
5✔
1476
        """
1477
        Send an onion message with onionmsg_tlv.message payload to node_id.
1478
        """
1479
        assert wallet
×
1480
        assert wallet.lnworker
×
1481
        assert node_id_or_blinded_path_hex
×
1482
        assert message
×
1483

1484
        node_id_or_blinded_path = bfh(node_id_or_blinded_path_hex)
×
1485
        assert len(node_id_or_blinded_path) >= 33
×
1486

1487
        destination_payload = {
×
1488
            'message': {'text': message.encode('utf-8')}
1489
        }
1490

1491
        try:
×
1492
            send_onion_message_to(wallet.lnworker, node_id_or_blinded_path, destination_payload)
×
1493
            return {'success': True}
×
1494
        except Exception as e:
×
1495
            msg = str(e)
×
1496

1497
        return {
×
1498
            'success': False,
1499
            'msg': msg
1500
        }
1501

1502
    @command('wnl')
5✔
1503
    async def get_blinded_path_via(self, node_id: str, dummy_hops: int = 0, wallet: Abstract_Wallet = None):
5✔
1504
        """
1505
        Create a blinded path with node_id as introduction point. Introduction point must be direct peer of me.
1506
        """
1507
        # TODO: allow introduction_point to not be a direct peer and construct a route
1508
        assert wallet
×
1509
        assert node_id
×
1510

1511
        pubkey = bfh(node_id)
×
1512
        assert len(pubkey) == 33, 'invalid node_id'
×
1513

1514
        peer = wallet.lnworker.peers[pubkey]
×
1515
        assert peer, 'node_id not a peer'
×
1516

1517
        path = [pubkey, wallet.lnworker.node_keypair.pubkey]
×
1518
        session_key = os.urandom(32)
×
1519
        blinded_path = create_blinded_path(session_key, path=path, final_recipient_data={}, dummy_hops=dummy_hops)
×
1520

1521
        with io.BytesIO() as blinded_path_fd:
×
1522
            OnionWireSerializer.write_field(
×
1523
                fd=blinded_path_fd,
1524
                field_type='blinded_path',
1525
                count=1,
1526
                value=blinded_path)
1527
            encoded_blinded_path = blinded_path_fd.getvalue()
×
1528

1529
        return encoded_blinded_path.hex()
×
1530

1531

1532
def eval_bool(x: str) -> bool:
5✔
1533
    if x == 'false': return False
5✔
1534
    if x == 'true': return True
5✔
1535
    try:
5✔
1536
        return bool(ast.literal_eval(x))
5✔
1537
    except Exception:
×
1538
        return bool(x)
×
1539

1540

1541
param_descriptions = {
5✔
1542
    'privkey': 'Private key. Type \'?\' to get a prompt.',
1543
    'destination': 'Bitcoin address, contact or alias',
1544
    'address': 'Bitcoin address',
1545
    'seed': 'Seed phrase',
1546
    'txid': 'Transaction ID',
1547
    'pos': 'Position',
1548
    'height': 'Block height',
1549
    'tx': 'Serialized transaction (hexadecimal)',
1550
    'key': 'Variable name',
1551
    'pubkey': 'Public key',
1552
    'message': 'Clear text message. Use quotes if it contains spaces.',
1553
    'encrypted': 'Encrypted message',
1554
    'amount': 'Amount to be sent (in BTC). Type \'!\' to send the maximum available.',
1555
    'outputs': 'list of ["address", amount]',
1556
    'redeem_script': 'redeem script (hexadecimal)',
1557
    'lightning_amount': "Amount sent or received in a submarine swap. Set it to 'dryrun' to receive a value",
1558
    'onchain_amount': "Amount sent or received in a submarine swap. Set it to 'dryrun' to receive a value",
1559
    'node_id': "Node pubkey in hex format"
1560
}
1561

1562
command_options = {
5✔
1563
    'password':    ("-W", "Password. Use '--password :' if you want a prompt."),
1564
    'new_password':(None, "New Password"),
1565
    'encrypt_file':(None, "Whether the file on disk should be encrypted with the provided password"),
1566
    'receiving':   (None, "Show only receiving addresses"),
1567
    'change':      (None, "Show only change addresses"),
1568
    'frozen':      (None, "Show only frozen addresses"),
1569
    'unused':      (None, "Show only unused addresses"),
1570
    'funded':      (None, "Show only funded addresses"),
1571
    'balance':     ("-b", "Show the balances of listed addresses"),
1572
    'labels':      ("-l", "Show the labels of listed addresses"),
1573
    'nocheck':     (None, "Do not verify aliases"),
1574
    'imax':        (None, "Maximum number of inputs"),
1575
    'fee':         ("-f", "Transaction fee (absolute, in BTC)"),
1576
    'feerate':     (None, f"Transaction fee rate (in {util.UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE})"),
1577
    'from_addr':   ("-F", "Source address (must be a wallet address; use sweep to spend from non-wallet address)."),
1578
    'from_coins':  (None, "Source coins (must be in wallet; use sweep to spend from non-wallet address)."),
1579
    'change_addr': ("-c", "Change address. Default is a spare address, or the source address if it's not in the wallet"),
1580
    'nbits':       (None, "Number of bits of entropy"),
1581
    'seed_type':   (None, "The type of seed to create, e.g. 'standard' or 'segwit'"),
1582
    'language':    ("-L", "Default language for wordlist"),
1583
    'passphrase':  (None, "Seed extension"),
1584
    'privkey':     (None, "Private key. Set to '?' to get a prompt."),
1585
    'unsigned':    ("-u", "Do not sign transaction"),
1586
    'rbf':         (None, "Whether to signal opt-in Replace-By-Fee in the transaction (true/false)"),
1587
    'decrease_payment': (None, "Whether payment amount will be decreased (true/false)"),
1588
    'locktime':    (None, "Set locktime block number"),
1589
    'addtransaction': (None,'Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet'),
1590
    'domain':      ("-D", "List of addresses"),
1591
    'memo':        ("-m", "Description of the request"),
1592
    'amount':      (None, "Requested amount (in btc)"),
1593
    'lightning':   (None, "Create lightning request"),
1594
    'expiry':      (None, "Time in seconds"),
1595
    'timeout':     (None, "Timeout in seconds"),
1596
    'force':       (None, "Create new address beyond gap limit, if no more addresses are available."),
1597
    'pending':     (None, "Show only pending requests."),
1598
    'push_amount': (None, 'Push initial amount (in BTC)'),
1599
    'zeroconf':    (None, 'request zeroconf channel'),
1600
    'expired':     (None, "Show only expired requests."),
1601
    'paid':        (None, "Show only paid requests."),
1602
    'show_addresses': (None, "Show input and output addresses"),
1603
    'show_fiat':   (None, "Show fiat value of transactions"),
1604
    'show_fees':   (None, "Show miner fees paid by transactions"),
1605
    'year':        (None, "Show history for a given year"),
1606
    'from_height': (None, "Only show transactions that confirmed after given block height"),
1607
    'to_height':   (None, "Only show transactions that confirmed before given block height"),
1608
    'iknowwhatimdoing': (None, "Acknowledge that I understand the full implications of what I am about to do"),
1609
    'gossip':      (None, "Apply command to gossip node instead of wallet"),
1610
    'connection_string':      (None, "Lightning network node ID or network address"),
1611
    'new_fee_rate': (None, f"The Updated/Increased Transaction fee rate (in {util.UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE})"),
1612
    'from_amount': (None, "Amount to convert (default: 1)"),
1613
    'from_ccy':    (None, "Currency to convert from"),
1614
    'to_ccy':      (None, "Currency to convert to"),
1615
    'public':      (None, 'Channel will be announced'),
1616
    'dummy_hops':  (None, 'Number of dummy hops to add'),
1617
}
1618

1619

1620
# don't use floats because of rounding errors
1621
from .transaction import convert_raw_tx_to_hex
5✔
1622
json_loads = lambda x: json.loads(x, parse_float=lambda x: str(to_decimal(x)))
5✔
1623
arg_types = {
5✔
1624
    'num': int,
1625
    'nbits': int,
1626
    'imax': int,
1627
    'year': int,
1628
    'from_height': int,
1629
    'to_height': int,
1630
    'tx': convert_raw_tx_to_hex,
1631
    'pubkeys': json_loads,
1632
    'jsontx': json_loads,
1633
    'inputs': json_loads,
1634
    'outputs': json_loads,
1635
    'fee': lambda x: str(to_decimal(x)) if x is not None else None,
1636
    'amount': lambda x: str(to_decimal(x)) if not parse_max_spend(x) else x,
1637
    'locktime': int,
1638
    'addtransaction': eval_bool,
1639
    'encrypt_file': eval_bool,
1640
    'rbf': eval_bool,
1641
    'timeout': float,
1642
    'dummy_hops': int,
1643
}
1644

1645
config_variables = {
5✔
1646
    'addrequest': {
1647
        'ssl_privkey': 'Path to your SSL private key, needed to sign the request.',
1648
        'ssl_chain': 'Chain of SSL certificates, needed for signed requests. Put your certificate at the top and the root CA at the end',
1649
        'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"',
1650
    },
1651
    'listrequests':{
1652
        'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"',
1653
    }
1654
}
1655

1656

1657
def set_default_subparser(self, name, args=None):
5✔
1658
    """see http://stackoverflow.com/questions/5176691/argparse-how-to-specify-a-default-subcommand"""
1659
    subparser_found = False
×
1660
    for arg in sys.argv[1:]:
×
1661
        if arg in ['-h', '--help', '--version']:  # global help/version if no subparser
×
1662
            break
×
1663
    else:
1664
        for x in self._subparsers._actions:
×
1665
            if not isinstance(x, argparse._SubParsersAction):
×
1666
                continue
×
1667
            for sp_name in x._name_parser_map.keys():
×
1668
                if sp_name in sys.argv[1:]:
×
1669
                    subparser_found = True
×
1670
        if not subparser_found:
×
1671
            # insert default in first position, this implies no
1672
            # global options without a sub_parsers specified
1673
            if args is None:
×
1674
                sys.argv.insert(1, name)
×
1675
            else:
1676
                args.insert(0, name)
×
1677

1678

1679
argparse.ArgumentParser.set_default_subparser = set_default_subparser
5✔
1680

1681

1682
# workaround https://bugs.python.org/issue23058
1683
# see https://github.com/nickstenning/honcho/pull/121
1684

1685
def subparser_call(self, parser, namespace, values, option_string=None):
5✔
1686
    from argparse import ArgumentError, SUPPRESS, _UNRECOGNIZED_ARGS_ATTR
×
1687
    parser_name = values[0]
×
1688
    arg_strings = values[1:]
×
1689
    # set the parser name if requested
1690
    if self.dest is not SUPPRESS:
×
1691
        setattr(namespace, self.dest, parser_name)
×
1692
    # select the parser
1693
    try:
×
1694
        parser = self._name_parser_map[parser_name]
×
1695
    except KeyError:
×
1696
        tup = parser_name, ', '.join(self._name_parser_map)
×
1697
        msg = _('unknown parser {!r} (choices: {})').format(*tup)
×
1698
        raise ArgumentError(self, msg)
×
1699
    # parse all the remaining options into the namespace
1700
    # store any unrecognized options on the object, so that the top
1701
    # level parser can decide what to do with them
1702
    namespace, arg_strings = parser.parse_known_args(arg_strings, namespace)
×
1703
    if arg_strings:
×
1704
        vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, [])
×
1705
        getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings)
×
1706

1707

1708
argparse._SubParsersAction.__call__ = subparser_call
5✔
1709

1710

1711
def add_network_options(parser):
5✔
1712
    parser.add_argument("-f", "--serverfingerprint", dest=SimpleConfig.NETWORK_SERVERFINGERPRINT.key(), default=None,
×
1713
                        help="only allow connecting to servers with a matching SSL certificate SHA256 fingerprint. " +
1714
                             "To calculate this yourself: '$ openssl x509 -noout -fingerprint -sha256 -inform pem -in mycertfile.crt'. Enter as 64 hex chars.")
1715
    parser.add_argument("-1", "--oneserver", action="store_true", dest=SimpleConfig.NETWORK_ONESERVER.key(), default=None,
×
1716
                        help="connect to one server only")
1717
    parser.add_argument("-s", "--server", dest=SimpleConfig.NETWORK_SERVER.key(), default=None,
×
1718
                        help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)")
1719
    parser.add_argument("-p", "--proxy", dest=SimpleConfig.NETWORK_PROXY.key(), default=None,
×
1720
                        help="set proxy [type:]host:port (or 'none' to disable proxy), where type is socks4 or socks5")
1721
    parser.add_argument("--proxyuser", dest=SimpleConfig.NETWORK_PROXY_USER.key(), default=None,
×
1722
                        help="set proxy username")
1723
    parser.add_argument("--proxypassword", dest=SimpleConfig.NETWORK_PROXY_PASSWORD.key(), default=None,
×
1724
                        help="set proxy password")
1725
    parser.add_argument("--noonion", action="store_true", dest=SimpleConfig.NETWORK_NOONION.key(), default=None,
×
1726
                        help="do not try to connect to onion servers")
1727
    parser.add_argument("--skipmerklecheck", action="store_true", dest=SimpleConfig.NETWORK_SKIPMERKLECHECK.key(), default=None,
×
1728
                        help="Tolerate invalid merkle proofs from server")
1729

1730

1731
def add_global_options(parser):
5✔
1732
    group = parser.add_argument_group('global options')
×
1733
    group.add_argument("-v", dest="verbosity", help="Set verbosity (log levels)", default='')
×
1734
    group.add_argument("-V", dest="verbosity_shortcuts", help="Set verbosity (shortcut-filter list)", default='')
×
1735
    group.add_argument("-D", "--dir", dest="electrum_path", help="electrum directory")
×
1736
    group.add_argument("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum_data' directory")
×
1737
    group.add_argument("--testnet", action="store_true", dest="testnet", default=False, help="Use Testnet")
×
1738
    group.add_argument("--testnet4", action="store_true", dest="testnet4", default=False, help="Use Testnet4")
×
1739
    group.add_argument("--regtest", action="store_true", dest="regtest", default=False, help="Use Regtest")
×
1740
    group.add_argument("--simnet", action="store_true", dest="simnet", default=False, help="Use Simnet")
×
1741
    group.add_argument("--signet", action="store_true", dest="signet", default=False, help="Use Signet")
×
1742
    group.add_argument("-o", "--offline", action="store_true", dest=SimpleConfig.NETWORK_OFFLINE.key(), default=None, help="Run offline")
×
1743
    group.add_argument("--rpcuser", dest=SimpleConfig.RPC_USERNAME.key(), default=argparse.SUPPRESS, help="RPC user")
×
1744
    group.add_argument("--rpcpassword", dest=SimpleConfig.RPC_PASSWORD.key(), default=argparse.SUPPRESS, help="RPC password")
×
1745

1746

1747
def add_wallet_option(parser):
5✔
1748
    parser.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path")
×
1749
    parser.add_argument("--forgetconfig", action="store_true", dest=SimpleConfig.CONFIG_FORGET_CHANGES.key(), default=False, help="Forget config on exit")
×
1750

1751

1752
def get_parser():
5✔
1753
    # create main parser
1754
    parser = argparse.ArgumentParser(
×
1755
        epilog="Run 'electrum help <command>' to see the help for a command")
1756
    parser.add_argument("--version", dest="cmd", action='store_const', const='version', help="Return the version of Electrum.")
×
1757
    add_global_options(parser)
×
1758
    add_wallet_option(parser)
×
1759
    subparsers = parser.add_subparsers(dest='cmd', metavar='<command>')
×
1760
    # gui
1761
    parser_gui = subparsers.add_parser('gui', description="Run Electrum's Graphical User Interface.", help="Run GUI (default)")
×
1762
    parser_gui.add_argument("url", nargs='?', default=None, help="bitcoin URI (or bip70 file)")
×
1763
    parser_gui.add_argument("-g", "--gui", dest=SimpleConfig.GUI_NAME.key(), help="select graphical user interface", choices=['qt', 'text', 'stdio', 'qml'])
×
1764
    parser_gui.add_argument("-m", action="store_true", dest=SimpleConfig.GUI_QT_HIDE_ON_STARTUP.key(), default=False, help="hide GUI on startup")
×
1765
    parser_gui.add_argument("-L", "--lang", dest=SimpleConfig.LOCALIZATION_LANGUAGE.key(), default=None, help="default language used in GUI")
×
1766
    parser_gui.add_argument("--daemon", action="store_true", dest="daemon", default=False, help="keep daemon running after GUI is closed")
×
1767
    parser_gui.add_argument("--nosegwit", action="store_true", dest=SimpleConfig.WIZARD_DONT_CREATE_SEGWIT.key(), default=False, help="Do not create segwit wallets")
×
1768
    add_wallet_option(parser_gui)
×
1769
    add_network_options(parser_gui)
×
1770
    add_global_options(parser_gui)
×
1771
    # daemon
1772
    parser_daemon = subparsers.add_parser('daemon', help="Run Daemon")
×
1773
    parser_daemon.add_argument("-d", "--detached", action="store_true", dest="detach", default=False, help="run daemon in detached mode")
×
1774
    # FIXME: all these options are rpc-server-side. The CLI client-side cannot use e.g. --rpcport,
1775
    #        instead it reads it from the daemon lockfile.
1776
    parser_daemon.add_argument("--rpchost", dest=SimpleConfig.RPC_HOST.key(), default=argparse.SUPPRESS, help="RPC host")
×
1777
    parser_daemon.add_argument("--rpcport", dest=SimpleConfig.RPC_PORT.key(), type=int, default=argparse.SUPPRESS, help="RPC port")
×
1778
    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'])
×
1779
    parser_daemon.add_argument("--rpcsockpath", dest=SimpleConfig.RPC_SOCKET_FILEPATH.key(), help="where to place RPC file socket")
×
1780
    add_network_options(parser_daemon)
×
1781
    add_global_options(parser_daemon)
×
1782
    # commands
1783
    for cmdname in sorted(known_commands.keys()):
×
1784
        cmd = known_commands[cmdname]
×
1785
        p = subparsers.add_parser(cmdname, help=cmd.help, description=cmd.description)
×
1786
        for optname, default in zip(cmd.options, cmd.defaults):
×
1787
            if optname in ['wallet_path', 'wallet']:
×
1788
                add_wallet_option(p)
×
1789
                continue
×
1790
            a, help = command_options[optname]
×
1791
            b = '--' + optname
×
1792
            action = "store_true" if default is False else 'store'
×
1793
            args = (a, b) if a else (b,)
×
1794
            if action == 'store':
×
1795
                _type = arg_types.get(optname, str)
×
1796
                p.add_argument(*args, dest=optname, action=action, default=default, help=help, type=_type)
×
1797
            else:
1798
                p.add_argument(*args, dest=optname, action=action, default=default, help=help)
×
1799
        add_global_options(p)
×
1800

1801
        for param in cmd.params:
×
1802
            if param in ['wallet_path', 'wallet']:
×
1803
                continue
×
1804
            h = param_descriptions.get(param, '')
×
1805
            _type = arg_types.get(param, str)
×
1806
            p.add_argument(param, help=h, type=_type)
×
1807

1808
        cvh = config_variables.get(cmdname)
×
1809
        if cvh:
×
1810
            group = p.add_argument_group('configuration variables', '(set with setconfig/getconfig)')
×
1811
            for k, v in cvh.items():
×
1812
                group.add_argument(k, nargs='?', help=v)
×
1813

1814
    # 'gui' is the default command
1815
    parser.set_default_subparser('gui')
×
1816
    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

© 2026 Coveralls, Inc