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

spesmilo / electrum / 6601523842514944

26 Jun 2025 02:22PM UTC coverage: 59.829% (-0.001%) from 59.83%
6601523842514944

Pull #9983

CirrusCI

f321x
android: build BarcodeScannerView from src

Adds a script `make_barcode_scanner.sh` which builds the
`BarcodeScannerView` library and its dependencies, `zxing-cpp` and
`CameraView` from source. Builds `zxing-cpp` architecture dependent
reducing the final apk size.
Pull Request #9983: android: replace qr code scanning library

21943 of 36676 relevant lines covered (59.83%)

2.99 hits per line

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

47.21
/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 time
5✔
29
import argparse
5✔
30
import json
5✔
31
import ast
5✔
32
import binascii
5✔
33
import base64
5✔
34
import asyncio
5✔
35
import inspect
5✔
36
from collections import defaultdict
5✔
37
from functools import wraps
5✔
38
from decimal import Decimal, InvalidOperation
5✔
39
from typing import Optional, TYPE_CHECKING, Dict, List
5✔
40
import os
5✔
41
import re
5✔
42

43
import electrum_ecc as ecc
5✔
44

45
from . import util
5✔
46
from .lnmsg import OnionWireSerializer
5✔
47
from .logging import Logger
5✔
48
from .onion_message import create_blinded_path, send_onion_message_to
5✔
49
from .util import (
5✔
50
    bfh, json_decode, json_normalize, is_hash256_str, is_hex_str, to_bytes, parse_max_spend, to_decimal,
51
    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 (
5✔
58
    Transaction, multisig_script, PartialTransaction, PartialTxOutput, tx_from_any, PartialTxInput, TxOutpoint,
59
    convert_raw_tx_to_hex
60
)
61
from . import transaction
5✔
62
from .invoices import Invoice, PR_PAID, PR_UNPAID, PR_EXPIRED
5✔
63
from .synchronizer import Notifier
5✔
64
from .wallet import (
5✔
65
    Abstract_Wallet, create_new_wallet, restore_wallet_from_text, Deterministic_Wallet, BumpFeeStrategy,
66
    Imported_Wallet
67
)
68
from .address_synchronizer import TX_HEIGHT_LOCAL
5✔
69
from .mnemonic import Mnemonic
5✔
70
from .lnutil import channel_id_from_funding_tx, LnFeatures, SENT, MIN_FINAL_CLTV_DELTA_FOR_INVOICE
5✔
71
from .plugin import run_hook, DeviceMgr, Plugins
5✔
72
from .version import ELECTRUM_VERSION
5✔
73
from .simple_config import SimpleConfig
5✔
74
from .fee_policy import FeePolicy
5✔
75
from . import GuiImportError
5✔
76
from . import crypto
5✔
77
from . import constants
5✔
78
from . import descriptor
5✔
79

80
if TYPE_CHECKING:
5✔
81
    from .network import Network
×
82
    from .daemon import Daemon
×
83
    from electrum.lnworker import PaymentInfo
×
84

85

86
known_commands = {}  # type: Dict[str, Command]
5✔
87

88

89
class NotSynchronizedException(UserFacingException):
5✔
90
    pass
5✔
91

92

93
def satoshis_or_max(amount):
5✔
94
    return satoshis(amount) if not parse_max_spend(amount) else amount
5✔
95

96

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

101

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

105

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

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

135
    def parse_docstring(self, docstring):
5✔
136
        docstring = docstring or ''
5✔
137
        docstring = docstring.strip()
5✔
138
        self.description = docstring
5✔
139
        self.arg_descriptions = {}
5✔
140
        self.arg_types = {}
5✔
141
        for x in re.finditer(r'arg:(.*?):(.*?):(.*)$', docstring, flags=re.MULTILINE):
5✔
142
            self.arg_descriptions[x.group(2)] = x.group(3)
5✔
143
            self.arg_types[x.group(2)] = x.group(1)
5✔
144
            self.description = self.description.replace(x.group(), '')
5✔
145
        self.short_description = self.description.split('.')[0]
5✔
146

147

148
def command(s):
5✔
149
    def decorator(func):
5✔
150
        if hasattr(func, '__wrapped__'):
5✔
151
            # plugin command function
152
            name = func.plugin_name + '_' + func.__name__
×
153
            known_commands[name] = Command(func.__wrapped__, name, s)
×
154
        else:
155
            # regular command function
156
            name = func.__name__
5✔
157
            known_commands[name] = Command(func, name, s)
5✔
158

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

198

199
class Commands(Logger):
5✔
200

201
    def __init__(self, *, config: 'SimpleConfig',
5✔
202
                 network: 'Network' = None,
203
                 daemon: 'Daemon' = None, callback=None):
204
        Logger.__init__(self)
5✔
205
        self.config = config
5✔
206
        self.daemon = daemon
5✔
207
        self.network = network
5✔
208
        self._callback = callback
5✔
209

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

221
        f = getattr(self, method)
×
222
        if cmd.requires_password:
×
223
            kwargs['password'] = password
×
224

225
        if 'wallet' in kwargs:
×
226
            sig = inspect.signature(f)
×
227
            if 'wallet' not in sig.parameters:
×
228
                kwargs.pop('wallet')
×
229

230
        coro = f(*args, **kwargs)
×
231
        fut = asyncio.run_coroutine_threadsafe(coro, util.get_asyncio_loop())
×
232
        result = fut.result()
×
233

234
        if self._callback:
×
235
            self._callback()
×
236
        return result
×
237

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

388
    def _setconfig(self, key, value):
5✔
389
        value = self._setconfig_normalize_value(key, value)
×
390
        if self.daemon and key == SimpleConfig.RPC_USERNAME.key():
×
391
            self.daemon.commands_server.rpc_user = value
×
392
        if self.daemon and key == SimpleConfig.RPC_PASSWORD.key():
×
393
            self.daemon.commands_server.rpc_password = value
×
394
        if Plugins.is_plugin_enabler_config_key(key):
×
395
            self.config.set_key(key, value)
×
396
        else:
397
            cv = self.config.cv.from_key(key)
×
398
            cv.set(value)
×
399

400
    @command('')
5✔
401
    async def setconfig(self, key, value):
5✔
402
        """
403
        Set a configuration variable.
404

405
        arg:str:key:name of the configuration variable
406
        arg:str:value:value. may be a string or a Python expression.
407
        """
408
        self._setconfig(key, value)
×
409

410
    @command('')
5✔
411
    async def unsetconfig(self, key):
5✔
412
        """
413
        Clear a configuration variable.
414
        The variable will be reset to its default value.
415

416
        arg:str:key:name of the configuration variable
417
        """
418
        self._setconfig(key, None)
×
419

420
    @command('')
5✔
421
    async def listconfig(self):
5✔
422
        """Returns the list of all configuration variables. """
423
        return self.config.list_config_vars()
×
424

425
    @command('')
5✔
426
    async def helpconfig(self, key):
5✔
427
        """Returns help about a configuration variable.
428

429
        arg:str:key:name of the configuration variable
430
        """
431
        cv = self.config.cv.from_key(key)
×
432
        short = cv.get_short_desc()
×
433
        long = cv.get_long_desc()
×
434
        if short and long:
×
435
            return short + "\n---\n\n" + long
×
436
        elif short or long:
×
437
            return short or long
×
438
        else:
439
            return f"No description available for '{key}'"
×
440

441
    @command('')
5✔
442
    async def make_seed(self, nbits=None, language=None, seed_type=None):
5✔
443
        """
444
        Create a seed
445

446
        arg:int:nbits:Number of bits of entropy
447
        arg:str:seed_type:The type of seed to create, e.g. 'standard' or 'segwit'
448
        arg:str:language:Default language for wordlist
449
        """
450
        s = Mnemonic(language).make_seed(seed_type=seed_type, num_bits=nbits)
×
451
        return s
×
452

453
    @command('n')
5✔
454
    async def getaddresshistory(self, address):
5✔
455
        """
456
        Return the transaction history of any address. Note: This is a
457
        walletless server query, results are not checked by SPV.
458

459
        arg:str:address:Bitcoin address
460
        """
461
        sh = bitcoin.address_to_scripthash(address)
×
462
        return await self.network.get_history_for_scripthash(sh)
×
463

464
    @command('wp')
5✔
465
    async def unlock(self, wallet: Abstract_Wallet = None, password=None):
5✔
466
        """Unlock the wallet (store the password in memory)."""
467
        wallet.unlock(password)
×
468

469
    @command('w')
5✔
470
    async def listunspent(self, wallet: Abstract_Wallet = None):
5✔
471
        """List unspent outputs. Returns the list of unspent transaction
472
        outputs in your wallet."""
473
        coins = []
×
474
        for txin in wallet.get_utxos():
×
475
            d = txin.to_json()
×
476
            v = d.pop("value_sats")
×
477
            d["value"] = str(to_decimal(v)/COIN) if v is not None else None
×
478
            coins.append(d)
×
479
        return coins
×
480

481
    @command('n')
5✔
482
    async def getaddressunspent(self, address):
5✔
483
        """
484
        Returns the UTXO list of any address. Note: This
485
        is a walletless server query, results are not checked by SPV.
486

487
        arg:str:address:Bitcoin address
488
        """
489
        sh = bitcoin.address_to_scripthash(address)
×
490
        return await self.network.listunspent_for_scripthash(sh)
×
491

492
    @command('')
5✔
493
    async def serialize(self, jsontx):
5✔
494
        """Create a signed raw transaction from a json tx template.
495

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

534
        outputs = []  # type: List[PartialTxOutput]
5✔
535
        for txout_idx, txout_dict in enumerate(jsontx.get('outputs')):
5✔
536
            try:
5✔
537
                txout_addr = txout_dict['address']
5✔
538
            except KeyError:
×
539
                raise UserFacingException(f"missing 'address' field for txout {txout_idx}")
×
540
            try:
5✔
541
                txout_val = int(txout_dict.get('value') or txout_dict['value_sats'])
5✔
542
            except KeyError:
×
543
                raise UserFacingException(f"missing 'value_sats' field for txout {txout_idx}")
×
544
            txout = PartialTxOutput.from_address_and_value(txout_addr, txout_val)
5✔
545
            outputs.append(txout)
5✔
546

547
        tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
5✔
548
        tx.sign(keypairs)
5✔
549
        return tx.serialize()
5✔
550

551
    @command('')
5✔
552
    async def signtransaction_with_privkey(self, tx, privkey):
5✔
553
        """Sign a transaction with private keys passed as parameter.
554

555
        arg:tx:tx:Transaction to sign
556
        arg:str:privkey:private key or list of private keys
557
        """
558
        tx = tx_from_any(tx)
5✔
559

560
        txins_dict = defaultdict(list)
5✔
561
        for txin in tx.inputs():
5✔
562
            txins_dict[txin.address].append(txin)
5✔
563

564
        if not isinstance(privkey, list):
5✔
565
            privkey = [privkey]
5✔
566

567
        for priv in privkey:
5✔
568
            txin_type, priv2, compressed = bitcoin.deserialize_privkey(priv)
5✔
569
            pubkey = ecc.ECPrivkey(priv2).get_public_key_bytes(compressed=compressed)
5✔
570
            desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=txin_type)
5✔
571
            address = desc.expand().address()
5✔
572
            if address in txins_dict.keys():
5✔
573
                for txin in txins_dict[address]:
5✔
574
                    txin.script_descriptor = desc
5✔
575
                tx.sign({pubkey: priv2})
5✔
576

577
        return tx.serialize()
5✔
578

579
    @command('wp')
5✔
580
    async def signtransaction(self, tx, password=None, wallet: Abstract_Wallet = None, ignore_warnings: bool=False):
5✔
581
        """
582
        Sign a transaction with the current wallet.
583

584
        arg:tx:tx:transaction
585
        arg:bool:ignore_warnings:ignore warnings
586
        """
587
        tx = tx_from_any(tx)
5✔
588
        wallet.sign_transaction(tx, password, ignore_warnings=ignore_warnings)
5✔
589
        return tx.serialize()
5✔
590

591
    @command('')
5✔
592
    async def deserialize(self, tx):
5✔
593
        """
594
        Deserialize a transaction
595

596
        arg:str:tx:Serialized transaction
597
        """
598
        tx = tx_from_any(tx)
×
599
        return tx.to_json()
×
600

601
    @command('n')
5✔
602
    async def broadcast(self, tx):
5✔
603
        """
604
        Broadcast a transaction to the network.
605

606
        arg:str:tx:Serialized transaction (must be hexadecimal)
607
        """
608
        tx = Transaction(tx)
×
609
        await self.network.broadcast_transaction(tx)
×
610
        return tx.txid()
×
611

612
    @command('')
5✔
613
    async def createmultisig(self, num, pubkeys):
5✔
614
        """
615
        Create multisig 'n of m' address
616

617
        arg:int:num:Number of cosigners required
618
        arg:json:pubkeys:List of public keys
619
        """
620
        assert isinstance(pubkeys, list), (type(num), type(pubkeys))
×
621
        redeem_script = multisig_script(pubkeys, num)
×
622
        address = bitcoin.hash160_to_p2sh(hash_160(redeem_script))
×
623
        return {'address': address, 'redeemScript': redeem_script.hex()}
×
624

625
    @command('w')
5✔
626
    async def freeze(self, address: str, wallet: Abstract_Wallet = None):
5✔
627
        """
628
        Freeze address. Freeze the funds at one of your wallet\'s addresses
629

630
        arg:str:address:Bitcoin address
631
        """
632
        return wallet.set_frozen_state_of_addresses([address], True)
×
633

634
    @command('w')
5✔
635
    async def unfreeze(self, address: str, wallet: Abstract_Wallet = None):
5✔
636
        """
637
        Unfreeze address. Unfreeze the funds at one of your wallet\'s address
638

639
        arg:str:address:Bitcoin address
640
        """
641
        return wallet.set_frozen_state_of_addresses([address], False)
×
642

643
    @command('w')
5✔
644
    async def freeze_utxo(self, coin: str, wallet: Abstract_Wallet = None):
5✔
645
        """
646
        Freeze a UTXO so that the wallet will not spend it.
647

648
        arg:str:coin:outpoint, in the <txid:index> format
649
        """
650
        wallet.set_frozen_state_of_coins([coin], True)
×
651
        return True
×
652

653
    @command('w')
5✔
654
    async def unfreeze_utxo(self, coin: str, wallet: Abstract_Wallet = None):
5✔
655
        """Unfreeze a UTXO so that the wallet might spend it.
656

657
        arg:str:coin:outpoint
658
        """
659
        wallet.set_frozen_state_of_coins([coin], False)
×
660
        return True
×
661

662
    @command('wp')
5✔
663
    async def getprivatekeys(self, address, password=None, wallet: Abstract_Wallet = None):
5✔
664
        """
665
        Get private keys of addresses. You may pass a single wallet address, or a list of wallet addresses.
666

667
        arg:str:address:Bitcoin address
668
        """
669
        if isinstance(address, str):
5✔
670
            address = address.strip()
5✔
671
        if is_address(address):
5✔
672
            return wallet.export_private_key(address, password)
5✔
673
        domain = address
5✔
674
        return [wallet.export_private_key(address, password) for address in domain]
5✔
675

676
    @command('wp')
5✔
677
    async def getprivatekeyforpath(self, path, password=None, wallet: Abstract_Wallet = None):
5✔
678
        """Get private key corresponding to derivation path (address index).
679

680
        arg:str:path:Derivation path. Can be either a str such as "m/0/50", or a list of ints such as [0, 50].
681
        """
682
        return wallet.export_private_key_for_path(path, password)
5✔
683

684
    @command('w')
5✔
685
    async def ismine(self, address, wallet: Abstract_Wallet = None):
5✔
686
        """
687
        Check if address is in wallet. Return true if and only address is in wallet
688

689
        arg:str:address:Bitcoin address
690
        """
691
        return wallet.is_mine(address)
×
692

693
    @command('')
5✔
694
    async def dumpprivkeys(self):
5✔
695
        """Deprecated."""
696
        return "This command is deprecated. Use a pipe instead: 'electrum listaddresses | electrum getprivatekeys - '"
×
697

698
    @command('')
5✔
699
    async def validateaddress(self, address):
5✔
700
        """Check that an address is valid.
701

702
        arg:str:address:Bitcoin address
703
        """
704
        return is_address(address)
×
705

706
    @command('w')
5✔
707
    async def getpubkeys(self, address, wallet: Abstract_Wallet = None):
5✔
708
        """
709
        Return the public keys for a wallet address.
710

711
        arg:str:address:Bitcoin address
712
        """
713
        return wallet.get_public_keys(address)
×
714

715
    @command('w')
5✔
716
    async def getbalance(self, wallet: Abstract_Wallet = None):
5✔
717
        """Return the balance of your wallet. """
718
        c, u, x = wallet.get_balance()
×
719
        l = wallet.lnworker.get_balance() if wallet.lnworker else None
×
720
        out = {"confirmed": str(to_decimal(c)/COIN)}
×
721
        if u:
×
722
            out["unconfirmed"] = str(to_decimal(u)/COIN)
×
723
        if x:
×
724
            out["unmatured"] = str(to_decimal(x)/COIN)
×
725
        if l:
×
726
            out["lightning"] = str(to_decimal(l)/COIN)
×
727
        return out
×
728

729
    @command('n')
5✔
730
    async def getaddressbalance(self, address):
5✔
731
        """
732
        Return the balance of any address. Note: This is a walletless
733
        server query, results are not checked by SPV.
734

735
        arg:str:address:Bitcoin address
736
        """
737
        sh = bitcoin.address_to_scripthash(address)
×
738
        out = await self.network.get_balance_for_scripthash(sh)
×
739
        out["confirmed"] =  str(to_decimal(out["confirmed"])/COIN)
×
740
        out["unconfirmed"] =  str(to_decimal(out["unconfirmed"])/COIN)
×
741
        return out
×
742

743
    @command('n')
5✔
744
    async def getmerkle(self, txid, height):
5✔
745
        """Get Merkle branch of a transaction included in a block. Electrum
746
        uses this to verify transactions (Simple Payment Verification).
747

748
        arg:txid:txid:Transaction ID
749
        arg:int:height:Block height
750
        """
751
        return await self.network.get_merkle_for_transaction(txid, int(height))
×
752

753
    @command('n')
5✔
754
    async def getservers(self):
5✔
755
        """Return the list of known servers (candidates for connecting)."""
756
        return self.network.get_servers()
×
757

758
    @command('')
5✔
759
    async def version(self):
5✔
760
        """Return the version of Electrum."""
761
        return ELECTRUM_VERSION
×
762

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

800
        return ret
×
801

802
    @command('w')
5✔
803
    async def getmpk(self, wallet: Abstract_Wallet = None):
5✔
804
        """Get master public key. Return your wallet\'s master public key"""
805
        return wallet.get_master_public_key()
×
806

807
    @command('wp')
5✔
808
    async def getmasterprivate(self, password=None, wallet: Abstract_Wallet = None):
5✔
809
        """Get master private key. Return your wallet\'s master private key"""
810
        return str(wallet.keystore.get_master_private_key(password))
×
811

812
    @command('')
5✔
813
    async def convert_xkey(self, xkey, xtype):
5✔
814
        """Convert xtype of a master key. e.g. xpub -> ypub
815

816
        arg:str:xkey:the key
817
        arg:str:xtype:the type, eg 'xpub'
818
        """
819
        try:
5✔
820
            node = BIP32Node.from_xkey(xkey)
5✔
821
        except Exception:
×
822
            raise UserFacingException('xkey should be a master public/private key')
×
823
        return node._replace(xtype=xtype).to_xkey()
5✔
824

825
    @command('wp')
5✔
826
    async def getseed(self, password=None, wallet: Abstract_Wallet = None):
5✔
827
        """Get seed phrase. Print the generation seed of your wallet."""
828
        s = wallet.get_seed(password)
5✔
829
        return s
5✔
830

831
    @command('wp')
5✔
832
    async def importprivkey(self, privkey, password=None, wallet: Abstract_Wallet = None):
5✔
833
        """Import a private key or a list of private keys.
834

835
        arg:str:privkey:Private key. Type \'?\' to get a prompt.
836
        """
837
        if not wallet.can_import_privkey():
5✔
838
            return "Error: This type of wallet cannot import private keys. Try to create a new wallet with that key."
×
839
        assert isinstance(wallet, Imported_Wallet)
5✔
840
        keys = privkey.split()
5✔
841
        if not keys:
5✔
842
            return "Error: no keys given"
5✔
843
        elif len(keys) == 1:
5✔
844
            try:
5✔
845
                addr = wallet.import_private_key(keys[0], password)
5✔
846
                out = "Keypair imported: " + addr
5✔
847
            except Exception as e:
5✔
848
                out = "Error: " + repr(e)
5✔
849
            return out
5✔
850
        else:
851
            good_inputs, bad_inputs = wallet.import_private_keys(keys, password)
5✔
852
            return {
5✔
853
                "good_keys": len(good_inputs),
854
                "bad_keys": len(bad_inputs),
855
            }
856

857
    async def _resolver(self, x, wallet: Abstract_Wallet):
5✔
858
        if x is None:
5✔
859
            return None
5✔
860
        out = await wallet.contacts.resolve(x)
5✔
861
        if out.get('type') == 'openalias' and self.nocheck is False and out.get('validated') is False:
5✔
862
            raise UserFacingException(f"cannot verify alias: {x}")
×
863
        return out['address']
5✔
864

865
    @command('n')
5✔
866
    async def sweep(self, privkey, destination, fee=None, feerate=None, nocheck=False, imax=100):
5✔
867
        """
868
        Sweep private keys. Returns a transaction that spends UTXOs from
869
        privkey to a destination address. The transaction will not be broadcast.
870

871
        arg:str:privkey:Private key. Type \'?\' to get a prompt.
872
        arg:str:destination:Bitcoin address, contact or alias
873
        arg:str:fee:Transaction fee (absolute, in BTC)
874
        arg:str:feerate:Transaction fee rate (in sat/vbyte)
875
        arg:int:imax:Maximum number of inputs
876
        arg:bool:nocheck:Do not verify aliases
877
        """
878
        from .wallet import sweep
×
879
        fee_policy = self._get_fee_policy(fee, feerate)
×
880
        privkeys = privkey.split()
×
881
        self.nocheck = nocheck
×
882
        #dest = self._resolver(destination)
883
        tx = await sweep(
×
884
            privkeys,
885
            network=self.network,
886
            to_address=destination,
887
            fee_policy=fee_policy,
888
            imax=imax,
889
        )
890
        return tx.serialize() if tx else None
×
891

892
    @command('wp')
5✔
893
    async def signmessage(self, address, message, password=None, wallet: Abstract_Wallet = None):
5✔
894
        """Sign a message with a key. Use quotes if your message contains
895
        whitespaces
896

897
        arg:str:address:Bitcoin address
898
        arg:str:message:Clear text message. Use quotes if it contains spaces.
899
        """
900
        sig = wallet.sign_message(address, message, password)
×
901
        return base64.b64encode(sig).decode('ascii')
×
902

903
    @command('')
5✔
904
    async def verifymessage(self, address, signature, message):
5✔
905
        """Verify a signature.
906

907
        arg:str:address:Bitcoin address
908
        arg:str:message:Clear text message. Use quotes if it contains spaces.
909
        arg:str:signature:The signature, base64-encoded.
910
        """
911
        try:
5✔
912
            sig = base64.b64decode(signature, validate=True)
5✔
913
        except binascii.Error:
5✔
914
            return False
5✔
915
        message = util.to_bytes(message)
5✔
916
        return bitcoin.verify_usermessage_with_address(address, sig, message)
5✔
917

918
    def _get_fee_policy(self, fee, feerate):
5✔
919
        if fee is not None and feerate is not None:
5✔
920
            raise Exception('Cannot set both fee and feerate')
×
921
        if fee is not None:
5✔
922
            fee_sats = satoshis(fee)
5✔
923
            fee_policy = FeePolicy(f'fixed:{fee_sats}')
5✔
924
        elif feerate is not None:
5✔
925
            feerate_per_byte = 1000 * feerate
5✔
926
            fee_policy = FeePolicy(f'feerate:{feerate_per_byte}')
5✔
927
        else:
928
            fee_policy = FeePolicy(self.config.FEE_POLICY)
×
929
        return fee_policy
5✔
930

931
    @command('wp')
5✔
932
    async def payto(self, destination, amount, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None,
5✔
933
                    nocheck=False, unsigned=False, rbf=True, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None):
934
        """Create an on-chain transaction.
935

936
        arg:str:destination:Bitcoin address, contact or alias
937
        arg:decimal_or_max:amount:Amount to be sent (in BTC). Type '!' to send the maximum available.
938
        arg:decimal:fee:Transaction fee (absolute, in BTC)
939
        arg:float:feerate:Transaction fee rate (in sat/vbyte)
940
        arg:str:from_addr:Source address (must be a wallet address; use sweep to spend from non-wallet address)
941
        arg:str:change_addr:Change address. Default is a spare address, or the source address if it's not in the wallet
942
        arg:bool:rbf:Whether to signal opt-in Replace-By-Fee in the transaction (true/false)
943
        arg:bool:addtransaction:Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet
944
        arg:int:locktime:Set locktime block number
945
        arg:bool:unsigned:Do not sign transaction
946
        arg:bool:nocheck:Do not verify aliases
947
        arg:json:from_coins:Source coins (must be in wallet; use sweep to spend from non-wallet address)
948
        """
949
        return await self.paytomany(
5✔
950
            outputs=[(destination, amount),],
951
            fee=fee,
952
            feerate=feerate,
953
            from_addr=from_addr,
954
            from_coins=from_coins,
955
            change_addr=change_addr,
956
            nocheck=nocheck,
957
            unsigned=unsigned,
958
            rbf=rbf,
959
            password=password,
960
            locktime=locktime,
961
            addtransaction=addtransaction,
962
            wallet=wallet,
963
        )
964

965
    @command('wp')
5✔
966
    async def paytomany(self, outputs, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None,
5✔
967
                        nocheck=False, unsigned=False, rbf=True, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None):
968
        """Create a multi-output transaction.
969

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

1013
    def get_year_timestamps(self, year:int):
5✔
1014
        kwargs = {}
×
1015
        if year:
×
1016
            start_date = datetime.datetime(year, 1, 1)
×
1017
            end_date = datetime.datetime(year+1, 1, 1)
×
1018
            kwargs['from_timestamp'] = time.mktime(start_date.timetuple())
×
1019
            kwargs['to_timestamp'] = time.mktime(end_date.timetuple())
×
1020
        return kwargs
×
1021

1022
    @command('w')
5✔
1023
    async def onchain_capital_gains(self, year=None, wallet: Abstract_Wallet = None):
5✔
1024
        """
1025
        Capital gains, using utxo pricing.
1026
        This cannot be used with lightning.
1027

1028
        arg:int:year:Show cap gains for a given year
1029
        """
1030
        kwargs = self.get_year_timestamps(year)
×
1031
        from .exchange_rate import FxThread
×
1032
        fx = self.daemon.fx if self.daemon else FxThread(config=self.config)
×
1033
        return json_normalize(wallet.get_onchain_capital_gains(fx, **kwargs))
×
1034

1035
    @command('wp')
5✔
1036
    async def bumpfee(self, tx, new_fee_rate, from_coins=None, decrease_payment=False, password=None, unsigned=False, wallet: Abstract_Wallet = None):
5✔
1037
        """
1038
        Bump the fee for an unconfirmed transaction.
1039
        'tx' can be either a raw hex tx or a txid. If txid, the corresponding tx must already be part of the wallet history.
1040

1041
        arg:str:tx:Serialized transaction (hexadecimal)
1042
        arg:str:new_fee_rate: The Updated/Increased Transaction fee rate (in sats/vbyte)
1043
        arg:bool:decrease_payment:Whether payment amount will be decreased (true/false)
1044
        arg:bool:unsigned:Do not sign transaction
1045
        arg:json:from_coins:Coins that may be used to inncrease the fee (must be in wallet)
1046
        """
1047
        if is_hash256_str(tx):  # txid
5✔
1048
            tx = wallet.db.get_transaction(tx)
5✔
1049
            if tx is None:
5✔
1050
                raise UserFacingException("Transaction not in wallet.")
5✔
1051
        else:  # raw tx
1052
            try:
5✔
1053
                tx = Transaction(tx)
5✔
1054
                tx.deserialize()
5✔
1055
            except transaction.SerializationError as e:
×
1056
                raise UserFacingException(f"Failed to deserialize transaction: {e}") from e
×
1057
        domain_coins = from_coins.split(',') if from_coins else None
5✔
1058
        coins = wallet.get_spendable_coins(None)
5✔
1059
        if domain_coins is not None:
5✔
1060
            coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)]
5✔
1061
        tx.add_info_from_wallet(wallet)
5✔
1062
        await tx.add_info_from_network(self.network)
5✔
1063
        new_tx = wallet.bump_fee(
5✔
1064
            tx=tx,
1065
            coins=coins,
1066
            strategy=BumpFeeStrategy.DECREASE_PAYMENT if decrease_payment else BumpFeeStrategy.PRESERVE_PAYMENT,
1067
            new_fee_rate=new_fee_rate)
1068
        if not unsigned:
5✔
1069
            wallet.sign_transaction(new_tx, password)
5✔
1070
        return new_tx.serialize()
5✔
1071

1072
    @command('w')
5✔
1073
    async def onchain_history(self, show_fiat=False, year=None, show_addresses=False, wallet: Abstract_Wallet = None):
5✔
1074
        """Wallet onchain history. Returns the transaction history of your wallet.
1075

1076
        arg:bool:show_addresses:Show input and output addresses
1077
        arg:bool:show_fiat:Show fiat value of transactions
1078
        arg:bool:show_fees:Show miner fees paid by transactions
1079
        arg:int:year:Show history for a given year
1080
        """
1081
        # trigger lnwatcher callbacks for their side effects: setting labels and accounting_addresses
1082
        if not self.network and wallet.lnworker:
×
1083
            await wallet.lnworker.lnwatcher.trigger_callbacks(requires_synchronizer=False)
×
1084

1085
        #'from_height': (None, "Only show transactions that confirmed after given block height"),
1086
        #'to_height':   (None, "Only show transactions that confirmed before given block height"),
1087
        kwargs = self.get_year_timestamps(year)
×
1088
        onchain_history = wallet.get_onchain_history(**kwargs)
×
1089
        out = [x.to_dict() for x in onchain_history.values()]
×
1090
        if show_fiat:
×
1091
            from .exchange_rate import FxThread
×
1092
            fx = self.daemon.fx if self.daemon else FxThread(config=self.config)
×
1093
        else:
1094
            fx = None
×
1095
        for item in out:
×
1096
            if show_addresses:
×
1097
                tx = wallet.db.get_transaction(item['txid'])
×
1098
                item['inputs'] = list(map(lambda x: x.to_json(), tx.inputs()))
×
1099
                item['outputs'] = list(map(lambda x: {'address': x.get_ui_address_str(), 'value_sat': x.value},
×
1100
                                           tx.outputs()))
1101
            if fx:
×
1102
                fiat_fields = wallet.get_tx_item_fiat(tx_hash=item['txid'], amount_sat=item['amount_sat'], fx=fx, tx_fee=item['fee_sat'])
×
1103
                item.update(fiat_fields)
×
1104
        return json_normalize(out)
×
1105

1106
    @command('wl')
5✔
1107
    async def lightning_history(self, wallet: Abstract_Wallet = None):
5✔
1108
        """ lightning history. """
1109
        lightning_history = wallet.lnworker.get_lightning_history() if wallet.lnworker else {}
×
1110
        sorted_hist= sorted(lightning_history.values(), key=lambda x: x.timestamp)
×
1111
        return json_normalize([x.to_dict() for x in sorted_hist])
×
1112

1113
    @command('w')
5✔
1114
    async def setlabel(self, key, label, wallet: Abstract_Wallet = None):
5✔
1115
        """
1116
        Assign a label to an item. Item may be a bitcoin address or a
1117
        transaction ID
1118

1119
        arg:str:key:Key
1120
        arg:str:label:Label
1121
        """
1122
        wallet.set_label(key, label)
×
1123

1124
    @command('w')
5✔
1125
    async def listcontacts(self, wallet: Abstract_Wallet = None):
5✔
1126
        """Show your list of contacts"""
1127
        return wallet.contacts
×
1128

1129
    @command('w')
5✔
1130
    async def getopenalias(self, key, wallet: Abstract_Wallet = None):
5✔
1131
        """
1132
        Retrieve alias. Lookup in your list of contacts, and for an OpenAlias DNS record.
1133

1134
        arg:str:key:the alias to be retrieved
1135
        """
1136
        return await wallet.contacts.resolve(key)
×
1137

1138
    @command('w')
5✔
1139
    async def searchcontacts(self, query, wallet: Abstract_Wallet = None):
5✔
1140
        """
1141
        Search through your wallet contacts, return matching entries.
1142

1143
        arg:str:query:Search query
1144
        """
1145
        results = {}
×
1146
        for key, value in wallet.contacts.items():
×
1147
            if query.lower() in key.lower():
×
1148
                results[key] = value
×
1149
        return results
×
1150

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

1155
        arg:bool:receiving:Show only receiving addresses
1156
        arg:bool:change:Show only change addresses
1157
        arg:bool:frozen:Show only frozen addresses
1158
        arg:bool:unused:Show only unused addresses
1159
        arg:bool:funded:Show only funded addresses
1160
        arg:bool:balance:Show the balances of listed addresses
1161
        arg:bool:labels:Show the labels of listed addresses
1162
        """
1163
        out = []
×
1164
        for addr in wallet.get_addresses():
×
1165
            if frozen and not wallet.is_frozen_address(addr):
×
1166
                continue
×
1167
            if receiving and wallet.is_change(addr):
×
1168
                continue
×
1169
            if change and not wallet.is_change(addr):
×
1170
                continue
×
1171
            if unused and wallet.adb.is_used(addr):
×
1172
                continue
×
1173
            if funded and wallet.adb.is_empty(addr):
×
1174
                continue
×
1175
            item = addr
×
1176
            if labels or balance:
×
1177
                item = (item,)
×
1178
            if balance:
×
1179
                item += (util.format_satoshis(sum(wallet.get_addr_balance(addr))),)
×
1180
            if labels:
×
1181
                item += (repr(wallet.get_label_for_address(addr)),)
×
1182
            out.append(item)
×
1183
        return out
×
1184

1185
    @command('n')
5✔
1186
    async def gettransaction(self, txid, wallet: Abstract_Wallet = None):
5✔
1187
        """Retrieve a transaction.
1188

1189
        arg:txid:txid:Transaction ID
1190
        """
1191
        tx = None
×
1192
        if wallet:
×
1193
            tx = wallet.db.get_transaction(txid)
×
1194
        if tx is None:
×
1195
            raw = await self.network.get_transaction(txid)
×
1196
            if raw:
×
1197
                tx = Transaction(raw)
×
1198
            else:
1199
                raise UserFacingException("Unknown transaction")
×
1200
        if tx.txid() != txid:
×
1201
            raise UserFacingException("Mismatching txid")
×
1202
        return tx.serialize()
×
1203

1204
    @command('')
5✔
1205
    async def encrypt(self, pubkey, message) -> str:
5✔
1206
        """
1207
        Encrypt a message with a public key. Use quotes if the message contains whitespaces.
1208

1209
        arg:str:pubkey:Public key
1210
        arg:str:message:Clear text message. Use quotes if it contains spaces.
1211
        """
1212
        if not is_hex_str(pubkey):
5✔
1213
            raise UserFacingException(f"pubkey must be a hex string instead of {repr(pubkey)}")
×
1214
        try:
5✔
1215
            message = to_bytes(message)
5✔
1216
        except TypeError:
×
1217
            raise UserFacingException(f"message must be a string-like object instead of {repr(message)}")
×
1218
        public_key = ecc.ECPubkey(bfh(pubkey))
5✔
1219
        encrypted = crypto.ecies_encrypt_message(public_key, message)
5✔
1220
        return encrypted.decode('utf-8')
5✔
1221

1222
    @command('wp')
5✔
1223
    async def decrypt(self, pubkey, encrypted, password=None, wallet: Abstract_Wallet = None) -> str:
5✔
1224
        """Decrypt a message encrypted with a public key.
1225

1226
        arg:str:encrypted:Encrypted message
1227
        arg:str:pubkey:Public key of one of your wallet addresses
1228
        """
1229
        if not is_hex_str(pubkey):
5✔
1230
            raise UserFacingException(f"pubkey must be a hex string instead of {repr(pubkey)}")
×
1231
        if not isinstance(encrypted, (str, bytes, bytearray)):
5✔
1232
            raise UserFacingException(f"encrypted must be a string-like object instead of {repr(encrypted)}")
×
1233
        decrypted = wallet.decrypt_message(pubkey, encrypted, password)
5✔
1234
        return decrypted.decode('utf-8')
5✔
1235

1236
    @command('w')
5✔
1237
    async def get_request(self, request_id, wallet: Abstract_Wallet = None):
5✔
1238
        """Returns a payment request
1239

1240
        arg:str:request_id:The request ID, as seen in list_requests or add_request
1241
        """
1242
        r = wallet.get_request(request_id)
×
1243
        if not r:
×
1244
            raise UserFacingException("Request not found")
×
1245
        return wallet.export_request(r)
×
1246

1247
    @command('w')
5✔
1248
    async def get_invoice(self, invoice_id, wallet: Abstract_Wallet = None):
5✔
1249
        """
1250
        Returns an invoice (request for outgoing payment)
1251

1252
        arg:str:invoice_id:The invoice ID, as seen in list_invoices
1253
        """
1254
        r = wallet.get_invoice(invoice_id)
×
1255
        if not r:
×
1256
            raise UserFacingException("Request not found")
×
1257
        return wallet.export_invoice(r)
×
1258

1259
    def _filter_invoices(self, _list, wallet, pending, expired, paid):
5✔
1260
        if pending:
×
1261
            f = PR_UNPAID
×
1262
        elif expired:
×
1263
            f = PR_EXPIRED
×
1264
        elif paid:
×
1265
            f = PR_PAID
×
1266
        else:
1267
            f = None
×
1268
        if f is not None:
×
1269
            _list = [x for x in _list if f == wallet.get_invoice_status(x)]
×
1270
        return _list
×
1271

1272
    @command('w')
5✔
1273
    async def list_requests(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None):
5✔
1274
        """
1275
        Returns the list of incoming payment requests saved in the wallet.
1276
        arg:bool:paid:Show only paid requests
1277
        arg:bool:pending:Show only pending requests
1278
        arg:bool:expired:Show only expired requests
1279
        """
1280
        l = wallet.get_sorted_requests()
×
1281
        l = self._filter_invoices(l, wallet, pending, expired, paid)
×
1282
        return [wallet.export_request(x) for x in l]
×
1283

1284
    @command('w')
5✔
1285
    async def list_invoices(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None):
5✔
1286
        """
1287
        Returns the list of invoices (requests for outgoing payments) saved in the wallet.
1288
        arg:bool:paid:Show only paid invoices
1289
        arg:bool:pending:Show only pending invoices
1290
        arg:bool:expired:Show only expired invoices
1291
        """
1292
        l = wallet.get_invoices()
×
1293
        l = self._filter_invoices(l, wallet, pending, expired, paid)
×
1294
        return [wallet.export_invoice(x) for x in l]
×
1295

1296
    @command('w')
5✔
1297
    async def createnewaddress(self, wallet: Abstract_Wallet = None):
5✔
1298
        """Create a new receiving address, beyond the gap limit of the wallet"""
1299
        return wallet.create_new_address(False)
×
1300

1301
    @command('w')
5✔
1302
    async def changegaplimit(self, new_limit, iknowwhatimdoing=False, wallet: Abstract_Wallet = None):
5✔
1303
        """
1304
        Change the gap limit of the wallet.
1305

1306
        arg:int:new_limit:new gap limit
1307
        arg:bool:iknowwhatimdoing:Acknowledge that I understand the full implications of what I am about to do
1308
        """
1309
        if not iknowwhatimdoing:
×
1310
            raise UserFacingException(
×
1311
                "WARNING: Are you SURE you want to change the gap limit?\n"
1312
                "It makes recovering your wallet from seed difficult!\n"
1313
                "Please do your research and make sure you understand the implications.\n"
1314
                "Typically only merchants and power users might want to do this.\n"
1315
                "To proceed, try again, with the --iknowwhatimdoing option.")
1316
        if not isinstance(wallet, Deterministic_Wallet):
×
1317
            raise UserFacingException("This wallet is not deterministic.")
×
1318
        return wallet.change_gap_limit(new_limit)
×
1319

1320
    @command('wn')
5✔
1321
    async def getminacceptablegap(self, wallet: Abstract_Wallet = None):
5✔
1322
        """Returns the minimum value for gap limit that would be sufficient to discover all
1323
        known addresses in the wallet.
1324
        """
1325
        if not isinstance(wallet, Deterministic_Wallet):
×
1326
            raise UserFacingException("This wallet is not deterministic.")
×
1327
        if not wallet.is_up_to_date():
×
1328
            raise NotSynchronizedException("Wallet not fully synchronized.")
×
1329
        return wallet.min_acceptable_gap()
×
1330

1331
    @command('w')
5✔
1332
    async def getunusedaddress(self, wallet: Abstract_Wallet = None):
5✔
1333
        """Returns the first unused address of the wallet, or None if all addresses are used.
1334
        An address is considered as used if it has received a transaction, or if it is used in a payment request."""
1335
        return wallet.get_unused_address()
×
1336

1337
    @command('w')
5✔
1338
    async def add_request(self, amount, memo='', expiry=3600, lightning=False, force=False, wallet: Abstract_Wallet = None):
5✔
1339
        """Create a payment request, using the first unused address of the wallet.
1340

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

1344
        arg:decimal:amount:Requested amount (in btc)
1345
        arg:str:memo:Description of the request
1346
        arg:bool:force:Create new address beyond gap limit, if no more addresses are available.
1347
        arg:bool:lightning:Create lightning request.
1348
        arg:int:expiry:Time in seconds.
1349
        """
1350
        amount = satoshis(amount)
×
1351
        if not lightning:
×
1352
            addr = wallet.get_unused_address()
×
1353
            if addr is None:
×
1354
                if force:
×
1355
                    addr = wallet.create_new_address(False)
×
1356
                else:
1357
                    return False
×
1358
        else:
1359
            addr = None
×
1360
        expiry = int(expiry) if expiry else None
×
1361
        key = wallet.create_request(amount, memo, expiry, addr)
×
1362
        req = wallet.get_request(key)
×
1363
        return wallet.export_request(req)
×
1364

1365
    @command('wnl')
5✔
1366
    async def add_hold_invoice(
5✔
1367
            self,
1368
            preimage: str,
1369
            amount: Optional[Decimal] = None,
1370
            memo: str = "",
1371
            expiry: int = 3600,
1372
            min_final_cltv_expiry_delta: int = MIN_FINAL_CLTV_DELTA_FOR_INVOICE * 2,
1373
            wallet: Abstract_Wallet = None
1374
    ) -> dict:
1375
        """
1376
        Create a lightning hold invoice for the given preimage. Hold invoices have to get settled manually later.
1377
        HTLCs will get failed automatically if block_height + 144 > htlc.cltv_abs.
1378

1379
        arg:str:preimage:Hex encoded preimage to be used for the invoice
1380
        arg:decimal:amount:Optional requested amount (in btc)
1381
        arg:str:memo:Optional description of the invoice
1382
        arg:int:expiry:Optional expiry in seconds (default: 3600s)
1383
        arg:int:min_final_cltv_expiry_delta:Optional min final cltv expiry delta (default: 294 blocks)
1384
        """
1385
        assert len(preimage) == 64, f"Invalid preimage length: {len(preimage)} != 64"
5✔
1386
        payment_hash: str = crypto.sha256(bfh(preimage)).hex()
5✔
1387
        assert payment_hash not in wallet.lnworker._preimages, "Preimage already in use!"
5✔
1388
        assert payment_hash not in wallet.lnworker.payment_info, "Payment hash already used!"
5✔
1389
        assert payment_hash not in wallet.lnworker.dont_settle_htlcs, "Payment hash already used!"
5✔
1390
        assert MIN_FINAL_CLTV_DELTA_FOR_INVOICE < min_final_cltv_expiry_delta < 576, "Use a sane min_final_cltv_expiry_delta value"
5✔
1391
        amount = amount if amount and satoshis(amount) > 0 else None  # make amount either >0 or None
5✔
1392
        inbound_capacity = wallet.lnworker.num_sats_can_receive()
5✔
1393
        assert inbound_capacity > satoshis(amount or 0), \
5✔
1394
            f"Not enough inbound capacity [{inbound_capacity} sat] to receive this payment"
1395

1396
        lnaddr, invoice = wallet.lnworker.get_bolt11_invoice(
5✔
1397
            payment_hash=bfh(payment_hash),
1398
            amount_msat=satoshis(amount) * 1000 if amount else None,
1399
            message=memo,
1400
            expiry=expiry,
1401
            min_final_cltv_expiry_delta=min_final_cltv_expiry_delta,
1402
            fallback_address=None
1403
        )
1404
        wallet.lnworker.add_payment_info_for_hold_invoice(
5✔
1405
            bfh(payment_hash),
1406
            satoshis(amount) if amount else None,
1407
        )
1408
        wallet.lnworker.dont_settle_htlcs[payment_hash] = None
5✔
1409
        wallet.lnworker.save_preimage(bfh(payment_hash), bfh(preimage))
5✔
1410
        wallet.set_label(payment_hash, memo)
5✔
1411
        result = {
5✔
1412
            "invoice": invoice
1413
        }
1414
        return result
5✔
1415

1416
    @command('wnl')
5✔
1417
    async def settle_hold_invoice(self, payment_hash: str, wallet: Abstract_Wallet = None) -> dict:
5✔
1418
        """
1419
        Settles lightning hold invoice 'payment_hash' using the stored preimage.
1420
        Doesn't block until actual settlement of the HTLCs.
1421

1422
        arg:str:payment_hash:Hex encoded payment hash of the invoice to be settled
1423
        """
1424
        assert len(payment_hash) == 64, f"Invalid payment_hash length: {len(payment_hash)} != 64"
5✔
1425
        assert payment_hash in wallet.lnworker._preimages, f"Couldn't find preimage for {payment_hash}"
5✔
1426
        assert payment_hash in wallet.lnworker.dont_settle_htlcs, "Is already settled!"
5✔
1427
        assert payment_hash in wallet.lnworker.payment_info, \
5✔
1428
            f"Couldn't find lightning invoice for payment hash {payment_hash}"
1429
        assert wallet.lnworker.is_accepted_mpp(bfh(payment_hash)), \
5✔
1430
            f"MPP incomplete, cannot settle hold invoice {payment_hash} yet"
1431
        del wallet.lnworker.dont_settle_htlcs[payment_hash]
5✔
1432
        util.trigger_callback('wallet_updated', wallet)
5✔
1433
        result = {
5✔
1434
            "settled": payment_hash
1435
        }
1436
        return result
5✔
1437

1438
    @command('wnl')
5✔
1439
    async def cancel_hold_invoice(self, payment_hash: str, wallet: Abstract_Wallet = None) -> dict:
5✔
1440
        """
1441
        Cancels lightning hold invoice 'payment_hash'.
1442

1443
        arg:str:payment_hash:Payment hash in hex of the hold invoice
1444
        """
1445
        assert payment_hash in wallet.lnworker.payment_info, \
5✔
1446
            f"Couldn't find lightning invoice for payment hash {payment_hash}"
1447
        assert payment_hash in wallet.lnworker._preimages, "Nothing to cancel, no known preimage."
5✔
1448
        assert payment_hash in wallet.lnworker.dont_settle_htlcs, "Is already settled!"
5✔
1449
        del wallet.lnworker._preimages[payment_hash]
5✔
1450
        # set to PR_UNPAID so it can get deleted
1451
        wallet.lnworker.set_payment_status(bfh(payment_hash), PR_UNPAID)
5✔
1452
        wallet.lnworker.delete_payment_info(payment_hash)
5✔
1453
        wallet.set_label(payment_hash, None)
5✔
1454
        while wallet.lnworker.is_accepted_mpp(bfh(payment_hash)):
5✔
1455
            # wait until the htlcs got failed so the payment won't get settled accidentally in a race
1456
            await asyncio.sleep(0.1)
×
1457
        del wallet.lnworker.dont_settle_htlcs[payment_hash]
5✔
1458
        result = {
5✔
1459
            "cancelled": payment_hash
1460
        }
1461
        return result
5✔
1462

1463
    @command('wnl')
5✔
1464
    async def check_hold_invoice(self, payment_hash: str, wallet: Abstract_Wallet = None) -> dict:
5✔
1465
        """
1466
        Checks the status of a lightning hold invoice 'payment_hash'.
1467
        Possible states: unpaid, paid, settled, unknown (cancelled or not found)
1468

1469
        arg:str:payment_hash:Payment hash in hex of the hold invoice
1470
        """
1471
        assert len(payment_hash) == 64, f"Invalid payment_hash length: {len(payment_hash)} != 64"
5✔
1472
        info: Optional['PaymentInfo'] = wallet.lnworker.get_payment_info(bfh(payment_hash))
5✔
1473
        is_accepted_mpp: bool = wallet.lnworker.is_accepted_mpp(bfh(payment_hash))
5✔
1474
        amount_sat = (wallet.lnworker.get_payment_mpp_amount_msat(bfh(payment_hash)) or 0) // 1000
5✔
1475
        status = "unknown"
5✔
1476
        if info is None:
5✔
1477
            pass
×
1478
        elif not is_accepted_mpp:
5✔
1479
            status = "unpaid"
×
1480
        elif is_accepted_mpp and payment_hash in wallet.lnworker.dont_settle_htlcs:
5✔
1481
            status = "paid"
5✔
1482
        elif (payment_hash in wallet.lnworker._preimages
×
1483
                and payment_hash not in wallet.lnworker.dont_settle_htlcs
1484
                and is_accepted_mpp):
1485
            status = "settled"
×
1486
        result = {
5✔
1487
            "status": status,
1488
            "amount_sat": amount_sat
1489
        }
1490
        return result
5✔
1491

1492
    @command('w')
5✔
1493
    async def addtransaction(self, tx, wallet: Abstract_Wallet = None):
5✔
1494
        """
1495
        Add a transaction to the wallet history, without broadcasting it.
1496

1497
        arg:tx:tx:Transaction, in hexadecimal format.
1498
        """
1499
        tx = Transaction(tx)
×
1500
        if not wallet.adb.add_transaction(tx):
×
1501
            return False
×
1502
        wallet.save_db()
×
1503
        return tx.txid()
×
1504

1505
    @command('w')
5✔
1506
    async def delete_request(self, request_id, wallet: Abstract_Wallet = None):
5✔
1507
        """Remove an incoming payment request
1508

1509
        arg:str:request_id:The request ID, as returned in list_invoices
1510
        """
1511
        return wallet.delete_request(request_id)
×
1512

1513
    @command('w')
5✔
1514
    async def delete_invoice(self, invoice_id, wallet: Abstract_Wallet = None):
5✔
1515
        """Remove an outgoing payment invoice
1516

1517
        arg:str:invoice_id:The invoice ID, as returned in list_invoices
1518
        """
1519
        return wallet.delete_invoice(invoice_id)
×
1520

1521
    @command('w')
5✔
1522
    async def clear_requests(self, wallet: Abstract_Wallet = None):
5✔
1523
        """Remove all payment requests"""
1524
        wallet.clear_requests()
×
1525
        return True
×
1526

1527
    @command('w')
5✔
1528
    async def clear_invoices(self, wallet: Abstract_Wallet = None):
5✔
1529
        """Remove all invoices"""
1530
        wallet.clear_invoices()
×
1531
        return True
×
1532

1533
    @command('n')
5✔
1534
    async def notify(self, address: str, URL: Optional[str]):
5✔
1535
        """
1536
        Watch an address. Every time the address changes, a http POST is sent to the URL.
1537
        Call with an empty URL to stop watching an address.
1538

1539
        arg:str:address:Bitcoin address
1540
        arg:str:URL:The callback URL
1541
        """
1542
        if not hasattr(self, "_notifier"):
×
1543
            self._notifier = Notifier(self.network)
×
1544
        if URL:
×
1545
            await self._notifier.start_watching_addr(address, URL)
×
1546
        else:
1547
            await self._notifier.stop_watching_addr(address)
×
1548
        return True
×
1549

1550
    @command('wn')
5✔
1551
    async def is_synchronized(self, wallet: Abstract_Wallet = None):
5✔
1552
        """ return wallet synchronization status """
1553
        return wallet.is_up_to_date()
×
1554

1555
    @command('wn')
5✔
1556
    async def wait_for_sync(self, wallet: Abstract_Wallet = None):
5✔
1557
        """Block until the wallet synchronization finishes."""
1558
        while True:
×
1559
            if wallet.is_up_to_date():
×
1560
                return True
×
1561
            await wallet.up_to_date_changed_event.wait()
×
1562

1563
    @command('n')
5✔
1564
    async def getfeerate(self):
5✔
1565
        """
1566
        Return current fee estimate given network conditions (in sat/kvByte).
1567
        To change the fee policy, use 'getconfig/setconfig fee_policy'
1568
        """
1569
        fee_policy = FeePolicy(self.config.FEE_POLICY)
×
1570
        description = fee_policy.get_target_text()
×
1571
        feerate = fee_policy.fee_per_kb(self.network)
×
1572
        tooltip = fee_policy.get_estimate_text(self.network)
×
1573
        return {
×
1574
            'policy': fee_policy.get_descriptor(),
1575
            'description': description,
1576
            'sat/kvB': feerate,
1577
            'tooltip': tooltip,
1578
        }
1579

1580
    @command('w')
5✔
1581
    async def removelocaltx(self, txid, wallet: Abstract_Wallet = None):
5✔
1582
        """Remove a 'local' transaction from the wallet, and its dependent
1583
        transactions.
1584

1585
        arg:txid:txid:Transaction ID
1586
        """
1587
        height = wallet.adb.get_tx_height(txid).height
×
1588
        if height != TX_HEIGHT_LOCAL:
×
1589
            raise UserFacingException(
×
1590
                f'Only local transactions can be removed. '
1591
                f'This tx has height: {height} != {TX_HEIGHT_LOCAL}')
1592
        wallet.adb.remove_transaction(txid)
×
1593
        wallet.save_db()
×
1594

1595
    @command('wn')
5✔
1596
    async def get_tx_status(self, txid, wallet: Abstract_Wallet = None):
5✔
1597
        """Returns some information regarding the tx. For now, only confirmations.
1598
        The transaction must be related to the wallet.
1599

1600
        arg:txid:txid:Transaction ID
1601
        """
1602
        if not wallet.db.get_transaction(txid):
×
1603
            raise UserFacingException("Transaction not in wallet.")
×
1604
        return {
×
1605
            "confirmations": wallet.adb.get_tx_height(txid).conf,
1606
        }
1607

1608
    @command('')
5✔
1609
    async def help(self):
5✔
1610
        """Show help about a command"""
1611
        # for the python console
1612
        return sorted(known_commands.keys())
×
1613

1614
    # lightning network commands
1615
    @command('wnl')
5✔
1616
    async def add_peer(self, connection_string, timeout=20, gossip=False, wallet: Abstract_Wallet = None):
5✔
1617
        """
1618
        Connect to a lightning node
1619

1620
        arg:str:connection_string:Lightning network node ID or network address
1621
        arg:bool:gossip:Apply command to your gossip node instead of wallet node
1622
        arg:int:timeout:Timeout in seconds (default=20)
1623
        """
1624
        lnworker = self.network.lngossip if gossip else wallet.lnworker
×
1625
        await lnworker.add_peer(connection_string)
×
1626
        return True
×
1627

1628
    @command('wnl')
5✔
1629
    async def gossip_info(self, wallet: Abstract_Wallet = None):
5✔
1630
        """Display statistics about lightninig gossip"""
1631
        lngossip = self.network.lngossip
×
1632
        channel_db = lngossip.channel_db
×
1633
        forwarded = dict([(key.hex(), p._num_gossip_messages_forwarded) for key, p in wallet.lnworker.peers.items()]),
×
1634
        out = {
×
1635
            'received': {
1636
                'channel_announcements': lngossip._num_chan_ann,
1637
                'channel_updates': lngossip._num_chan_upd,
1638
                'channel_updates_good': lngossip._num_chan_upd_good,
1639
                'node_announcements': lngossip._num_node_ann,
1640
            },
1641
            'database': {
1642
                'nodes': channel_db.num_nodes,
1643
                'channels': channel_db.num_channels,
1644
                'channel_policies': channel_db.num_policies,
1645
            },
1646
            'forwarded': forwarded,
1647
        }
1648
        return out
×
1649

1650
    @command('wnl')
5✔
1651
    async def list_peers(self, gossip=False, wallet: Abstract_Wallet = None):
5✔
1652
        """
1653
        List lightning peers of your node
1654

1655
        arg:bool:gossip:Apply command to your gossip node instead of wallet node
1656
        """
1657
        lnworker = self.network.lngossip if gossip else wallet.lnworker
×
1658
        return [{
×
1659
            'node_id': p.pubkey.hex(),
1660
            'address': p.transport.name(),
1661
            'initialized': p.is_initialized(),
1662
            'features': str(LnFeatures(p.features)),
1663
            'channels': [c.funding_outpoint.to_str() for c in p.channels.values()],
1664
        } for p in lnworker.peers.values()]
1665

1666
    @command('wpnl')
5✔
1667
    async def open_channel(self, connection_string, amount, push_amount=0, public=False, zeroconf=False, password=None, wallet: Abstract_Wallet = None):
5✔
1668
        """
1669
        Open a lightning channel with a peer
1670

1671
        arg:str:connection_string:Lightning network node ID or network address
1672
        arg:decimal_or_max:amount:funding amount (in BTC)
1673
        arg:decimal:push_amount:Push initial amount (in BTC)
1674
        arg:bool:public:The channel will be announced
1675
        arg:bool:zeroconf:request zeroconf channel
1676
        """
1677
        if not wallet.can_have_lightning():
×
1678
            raise UserFacingException("This wallet cannot create new channels")
×
1679
        funding_sat = satoshis(amount)
×
1680
        push_sat = satoshis(push_amount)
×
1681
        peer = await wallet.lnworker.add_peer(connection_string)
×
1682
        chan, funding_tx = await wallet.lnworker.open_channel_with_peer(
×
1683
            peer, funding_sat,
1684
            push_sat=push_sat,
1685
            public=public,
1686
            zeroconf=zeroconf,
1687
            password=password)
1688
        return chan.funding_outpoint.to_str()
×
1689

1690
    @command('')
5✔
1691
    async def decode_invoice(self, invoice: str):
5✔
1692
        """
1693
        Decode a lightning invoice
1694

1695
        arg:str:invoice:Lightning invoice (bolt 11)
1696
        """
1697
        invoice = Invoice.from_bech32(invoice)
×
1698
        return invoice.to_debug_json()
×
1699

1700
    @command('wnpl')
5✔
1701
    async def lnpay(self, invoice, timeout=120, password=None, wallet: Abstract_Wallet = None):
5✔
1702
        """
1703
        Pay a lightning invoice
1704

1705
        arg:str:invoice:Lightning invoice (bolt 11)
1706
        arg:int:timeout:Timeout in seconds (default=20)
1707
        """
1708
        lnworker = wallet.lnworker
×
1709
        lnaddr = lnworker._check_bolt11_invoice(invoice)
×
1710
        payment_hash = lnaddr.paymenthash
×
1711
        invoice_obj = Invoice.from_bech32(invoice)
×
1712
        wallet.save_invoice(invoice_obj)
×
1713
        success, log = await lnworker.pay_invoice(invoice_obj)
×
1714
        return {
×
1715
            'payment_hash': payment_hash.hex(),
1716
            'success': success,
1717
            'preimage': lnworker.get_preimage(payment_hash).hex() if success else None,
1718
            'log': [x.formatted_tuple() for x in log]
1719
        }
1720

1721
    @command('wl')
5✔
1722
    async def nodeid(self, wallet: Abstract_Wallet = None):
5✔
1723
        """Return the Lightning Node ID of a wallet"""
1724
        listen_addr = self.config.LIGHTNING_LISTEN
×
1725
        return wallet.lnworker.node_keypair.pubkey.hex() + (('@' + listen_addr) if listen_addr else '')
×
1726

1727
    @command('wl')
5✔
1728
    async def list_channels(self, wallet: Abstract_Wallet = None):
5✔
1729
        """Return the list of Lightning channels in a wallet"""
1730
        # FIXME: we need to be online to display capacity of backups
1731
        from .lnutil import LOCAL, REMOTE, format_short_channel_id
×
1732
        channels = list(wallet.lnworker.channels.items())
×
1733
        backups = list(wallet.lnworker.channel_backups.items())
×
1734
        return [
×
1735
            {
1736
                'type': 'CHANNEL',
1737
                'short_channel_id': format_short_channel_id(chan.short_channel_id) if chan.short_channel_id else None,
1738
                'channel_id': chan.channel_id.hex(),
1739
                'channel_point': chan.funding_outpoint.to_str(),
1740
                'state': chan.get_state().name,
1741
                'peer_state': chan.peer_state.name,
1742
                'remote_pubkey': chan.node_id.hex(),
1743
                'local_balance': chan.balance(LOCAL)//1000,
1744
                'remote_balance': chan.balance(REMOTE)//1000,
1745
                'local_ctn': chan.get_latest_ctn(LOCAL),
1746
                'remote_ctn': chan.get_latest_ctn(REMOTE),
1747
                'local_reserve': chan.config[REMOTE].reserve_sat,  # their config has our reserve
1748
                'remote_reserve': chan.config[LOCAL].reserve_sat,
1749
                'local_unsettled_sent': chan.balance_tied_up_in_htlcs_by_direction(LOCAL, direction=SENT) // 1000,
1750
                'remote_unsettled_sent': chan.balance_tied_up_in_htlcs_by_direction(REMOTE, direction=SENT) // 1000,
1751
            } for channel_id, chan in channels
1752
        ] + [
1753
            {
1754
                'type': 'BACKUP',
1755
                'short_channel_id': format_short_channel_id(chan.short_channel_id) if chan.short_channel_id else None,
1756
                'channel_id': chan.channel_id.hex(),
1757
                'channel_point': chan.funding_outpoint.to_str(),
1758
                'state': chan.get_state().name,
1759
            } for channel_id, chan in backups
1760
        ]
1761

1762
    @command('wnl')
5✔
1763
    async def enable_htlc_settle(self, b: bool, wallet: Abstract_Wallet = None):
5✔
1764
        """
1765
        command used in regtests
1766

1767
        arg:bool:b:boolean
1768
        """
1769
        wallet.lnworker.enable_htlc_settle = b
×
1770

1771
    @command('n')
5✔
1772
    async def clear_ln_blacklist(self):
5✔
1773
        if self.network.path_finder:
×
1774
            self.network.path_finder.clear_blacklist()
×
1775

1776
    @command('n')
5✔
1777
    async def reset_liquidity_hints(self):
5✔
1778
        if self.network.path_finder:
×
1779
            self.network.path_finder.liquidity_hints.reset_liquidity_hints()
×
1780
            self.network.path_finder.clear_blacklist()
×
1781

1782
    @command('wnpl')
5✔
1783
    async def close_channel(self, channel_point, force=False, password=None, wallet: Abstract_Wallet = None):
5✔
1784
        """
1785
        Close a lightning channel.
1786
        Returns txid of closing tx.
1787

1788
        arg:str:channel_point:channel point
1789
        arg:bool:force:Force closes (broadcast local commitment transaction)
1790
        """
1791
        txid, index = channel_point.split(':')
×
1792
        chan_id, _ = channel_id_from_funding_tx(txid, int(index))
×
1793
        if chan_id not in wallet.lnworker.channels:
×
1794
            raise UserFacingException(f'Unknown channel {channel_point}')
×
1795
        coro = wallet.lnworker.force_close_channel(chan_id) if force else wallet.lnworker.close_channel(chan_id)
×
1796
        return await coro
×
1797

1798
    @command('wnpl')
5✔
1799
    async def request_force_close(self, channel_point, connection_string=None, password=None, wallet: Abstract_Wallet = None):
5✔
1800
        """
1801
        Requests the remote to force close a channel.
1802
        If a connection string is passed, can be used without having state or any backup for the channel.
1803
        Assumes that channel was originally opened with the same local peer (node_keypair).
1804

1805
        arg:str:connection_string:Lightning network node ID or network address
1806
        arg:str:channel_point:channel point
1807
        """
1808
        txid, index = channel_point.split(':')
×
1809
        chan_id, _ = channel_id_from_funding_tx(txid, int(index))
×
1810
        if chan_id not in wallet.lnworker.channels and chan_id not in wallet.lnworker.channel_backups:
×
1811
            raise UserFacingException(f'Unknown channel {channel_point}')
×
1812
        await wallet.lnworker.request_force_close(chan_id, connect_str=connection_string)
×
1813

1814
    @command('wpl')
5✔
1815
    async def export_channel_backup(self, channel_point, password=None, wallet: Abstract_Wallet = None):
5✔
1816
        """
1817
        Returns an encrypted channel backup
1818

1819
        arg:str:channel_point:Channel outpoint
1820
        """
1821
        txid, index = channel_point.split(':')
×
1822
        chan_id, _ = channel_id_from_funding_tx(txid, int(index))
×
1823
        if chan_id not in wallet.lnworker.channels:
×
1824
            raise UserFacingException(f'Unknown channel {channel_point}')
×
1825
        return wallet.lnworker.export_channel_backup(chan_id)
×
1826

1827
    @command('wl')
5✔
1828
    async def import_channel_backup(self, encrypted, wallet: Abstract_Wallet = None):
5✔
1829
        """
1830
        arg:str:encrypted:Encrypted channel backup
1831
        """
1832
        return wallet.lnworker.import_channel_backup(encrypted)
×
1833

1834
    @command('wnpl')
5✔
1835
    async def get_channel_ctx(self, channel_point, password=None, iknowwhatimdoing=False, wallet: Abstract_Wallet = None):
5✔
1836
        """
1837
        return the current commitment transaction of a channel
1838

1839
        arg:str:channel_point:Channel outpoint
1840
        arg:bool:iknowwhatimdoing:Acknowledge that I understand the full implications of what I am about to do
1841
        """
1842
        if not iknowwhatimdoing:
×
1843
            raise UserFacingException(
×
1844
                "WARNING: this command is potentially unsafe.\n"
1845
                "To proceed, try again, with the --iknowwhatimdoing option.")
1846
        txid, index = channel_point.split(':')
×
1847
        chan_id, _ = channel_id_from_funding_tx(txid, int(index))
×
1848
        if chan_id not in wallet.lnworker.channels:
×
1849
            raise UserFacingException(f'Unknown channel {channel_point}')
×
1850
        chan = wallet.lnworker.channels[chan_id]
×
1851
        tx = chan.force_close_tx()
×
1852
        return tx.serialize()
×
1853

1854
    @command('wnl')
5✔
1855
    async def get_watchtower_ctn(self, channel_point, wallet: Abstract_Wallet = None):
5✔
1856
        """
1857
        Return the local watchtower's ctn of channel. used in regtests
1858

1859
        arg:str:channel_point:Channel outpoint (txid:index)
1860
        """
1861
        return wallet.lnworker.get_watchtower_ctn(channel_point)
×
1862

1863
    @command('wnpl')
5✔
1864
    async def rebalance_channels(self, from_scid, dest_scid, amount, password=None, wallet: Abstract_Wallet = None):
5✔
1865
        """
1866
        Rebalance channels.
1867
        If trampoline is used, channels must be with different trampolines.
1868

1869
        arg:str:from_scid:Short channel ID
1870
        arg:str:dest_scid:Short channel ID
1871
        arg:decimal:amount:Amount (in BTC)
1872

1873
        """
1874
        from .lnutil import ShortChannelID
×
1875
        from_scid = ShortChannelID.from_str(from_scid)
×
1876
        dest_scid = ShortChannelID.from_str(dest_scid)
×
1877
        from_channel = wallet.lnworker.get_channel_by_short_id(from_scid)
×
1878
        dest_channel = wallet.lnworker.get_channel_by_short_id(dest_scid)
×
1879
        amount_sat = satoshis(amount)
×
1880
        success, log = await wallet.lnworker.rebalance_channels(
×
1881
            from_channel,
1882
            dest_channel,
1883
            amount_msat=amount_sat * 1000,
1884
        )
1885
        return {
×
1886
            'success': success,
1887
            'log': [x.formatted_tuple() for x in log]
1888
        }
1889

1890
    @command('wnpl')
5✔
1891
    async def normal_swap(self, onchain_amount, lightning_amount, password=None, wallet: Abstract_Wallet = None):
5✔
1892
        """
1893
        Normal submarine swap: send on-chain BTC, receive on Lightning
1894

1895
        arg:decimal_or_dryrun:lightning_amount:Amount to be received, in BTC. Set it to 'dryrun' to receive a value
1896
        arg:decimal_or_dryrun:onchain_amount:Amount to be sent, in BTC. Set it to 'dryrun' to receive a value
1897
        """
1898
        sm = wallet.lnworker.swap_manager
×
1899
        with sm.create_transport() as transport:
×
1900
            await sm.is_initialized.wait()
×
1901
            if lightning_amount == 'dryrun':
×
1902
                onchain_amount_sat = satoshis(onchain_amount)
×
1903
                lightning_amount_sat = sm.get_recv_amount(onchain_amount_sat, is_reverse=False)
×
1904
                txid = None
×
1905
            elif onchain_amount == 'dryrun':
×
1906
                lightning_amount_sat = satoshis(lightning_amount)
×
1907
                onchain_amount_sat = sm.get_send_amount(lightning_amount_sat, is_reverse=False)
×
1908
                txid = None
×
1909
            else:
1910
                lightning_amount_sat = satoshis(lightning_amount)
×
1911
                onchain_amount_sat = satoshis(onchain_amount)
×
1912
                txid = await wallet.lnworker.swap_manager.normal_swap(
×
1913
                    transport=transport,
1914
                    lightning_amount_sat=lightning_amount_sat,
1915
                    expected_onchain_amount_sat=onchain_amount_sat,
1916
                    password=password,
1917
                )
1918

1919
        return {
×
1920
            'txid': txid,
1921
            'lightning_amount': format_satoshis(lightning_amount_sat),
1922
            'onchain_amount': format_satoshis(onchain_amount_sat),
1923
        }
1924

1925
    @command('wnpl')
5✔
1926
    async def reverse_swap(self, lightning_amount, onchain_amount, password=None, wallet: Abstract_Wallet = None):
5✔
1927
        """
1928
        Reverse submarine swap: send on Lightning, receive on-chain
1929

1930
        arg:decimal_or_dryrun:lightning_amount:Amount to be sent, in BTC. Set it to 'dryrun' to receive a value
1931
        arg:decimal_or_dryrun:onchain_amount:Amount to be received, in BTC. Set it to 'dryrun' to receive a value
1932
        """
1933
        sm = wallet.lnworker.swap_manager
×
1934
        with sm.create_transport() as transport:
×
1935
            await sm.is_initialized.wait()
×
1936
            if onchain_amount == 'dryrun':
×
1937
                lightning_amount_sat = satoshis(lightning_amount)
×
1938
                onchain_amount_sat = sm.get_recv_amount(lightning_amount_sat, is_reverse=True)
×
1939
                funding_txid = None
×
1940
            elif lightning_amount == 'dryrun':
×
1941
                onchain_amount_sat = satoshis(onchain_amount)
×
1942
                lightning_amount_sat = sm.get_send_amount(onchain_amount_sat, is_reverse=True)
×
1943
                funding_txid = None
×
1944
            else:
1945
                lightning_amount_sat = satoshis(lightning_amount)
×
1946
                claim_fee = sm.get_fee_for_txbatcher()
×
1947
                onchain_amount_sat = satoshis(onchain_amount) + claim_fee
×
1948
                funding_txid = await wallet.lnworker.swap_manager.reverse_swap(
×
1949
                    transport=transport,
1950
                    lightning_amount_sat=lightning_amount_sat,
1951
                    expected_onchain_amount_sat=onchain_amount_sat,
1952
                )
1953
        return {
×
1954
            'funding_txid': funding_txid,
1955
            'lightning_amount': format_satoshis(lightning_amount_sat),
1956
            'onchain_amount': format_satoshis(onchain_amount_sat),
1957
        }
1958

1959
    @command('n')
5✔
1960
    async def convert_currency(self, from_amount=1, from_ccy='', to_ccy=''):
5✔
1961
        """
1962
        Converts the given amount of currency to another using the
1963
        configured exchange rate source.
1964

1965
        arg:decimal:from_amount:Amount to convert (default=1)
1966
        arg:decimal:from_ccy:Currency to convert from
1967
        arg:decimal:to_ccy:Currency to convert to
1968
        """
1969
        if not self.daemon.fx.is_enabled():
×
1970
            raise UserFacingException("FX is disabled. To enable, run: 'electrum setconfig use_exchange_rate true'")
×
1971
        # Currency codes are uppercase
1972
        from_ccy = from_ccy.upper()
×
1973
        to_ccy = to_ccy.upper()
×
1974
        # Default currencies
1975
        if from_ccy == '':
×
1976
            from_ccy = "BTC" if to_ccy != "BTC" else self.daemon.fx.ccy
×
1977
        if to_ccy == '':
×
1978
            to_ccy = "BTC" if from_ccy != "BTC" else self.daemon.fx.ccy
×
1979
        # Get current rates
1980
        rate_from = self.daemon.fx.exchange.get_cached_spot_quote(from_ccy)
×
1981
        rate_to = self.daemon.fx.exchange.get_cached_spot_quote(to_ccy)
×
1982
        # Test if currencies exist
1983
        if rate_from.is_nan():
×
1984
            raise UserFacingException(f'Currency to convert from ({from_ccy}) is unknown or rate is unavailable')
×
1985
        if rate_to.is_nan():
×
1986
            raise UserFacingException(f'Currency to convert to ({to_ccy}) is unknown or rate is unavailable')
×
1987
        # Conversion
1988
        try:
×
1989
            from_amount = to_decimal(from_amount)
×
1990
            to_amount = from_amount / rate_from * rate_to
×
1991
        except InvalidOperation:
×
1992
            raise Exception("from_amount is not a number")
×
1993
        return {
×
1994
            "from_amount": self.daemon.fx.ccy_amount_str(from_amount, add_thousands_sep=False, ccy=from_ccy),
1995
            "to_amount": self.daemon.fx.ccy_amount_str(to_amount, add_thousands_sep=False, ccy=to_ccy),
1996
            "from_ccy": from_ccy,
1997
            "to_ccy": to_ccy,
1998
            "source": self.daemon.fx.exchange.name(),
1999
        }
2000

2001
    @command('wnl')
5✔
2002
    async def send_onion_message(self, node_id_or_blinded_path_hex: str, message: str, wallet: Abstract_Wallet = None):
5✔
2003
        """
2004
        Send an onion message with onionmsg_tlv.message payload to node_id.
2005

2006
        arg:str:node_id_or_blinded_path_hex:node id or blinded path
2007
        arg:str:message:Message to send
2008
        """
2009
        assert wallet
×
2010
        assert wallet.lnworker
×
2011
        assert node_id_or_blinded_path_hex
×
2012
        assert message
×
2013

2014
        node_id_or_blinded_path = bfh(node_id_or_blinded_path_hex)
×
2015
        assert len(node_id_or_blinded_path) >= 33
×
2016

2017
        destination_payload = {
×
2018
            'message': {'text': message.encode('utf-8')}
2019
        }
2020

2021
        try:
×
2022
            send_onion_message_to(wallet.lnworker, node_id_or_blinded_path, destination_payload)
×
2023
            return {'success': True}
×
2024
        except Exception as e:
×
2025
            msg = str(e)
×
2026

2027
        return {
×
2028
            'success': False,
2029
            'msg': msg
2030
        }
2031

2032
    @command('wnl')
5✔
2033
    async def get_blinded_path_via(self, node_id: str, dummy_hops: int = 0, wallet: Abstract_Wallet = None):
5✔
2034
        """
2035
        Create a blinded path with node_id as introduction point. Introduction point must be direct peer of me.
2036

2037
        arg:str:node_id:Node pubkey in hex format
2038
        arg:int:dummy_hops:Number of dummy hops to add
2039
        """
2040
        # TODO: allow introduction_point to not be a direct peer and construct a route
2041
        assert wallet
×
2042
        assert node_id
×
2043

2044
        pubkey = bfh(node_id)
×
2045
        assert len(pubkey) == 33, 'invalid node_id'
×
2046

2047
        peer = wallet.lnworker.peers[pubkey]
×
2048
        assert peer, 'node_id not a peer'
×
2049

2050
        path = [pubkey, wallet.lnworker.node_keypair.pubkey]
×
2051
        session_key = os.urandom(32)
×
2052
        blinded_path = create_blinded_path(session_key, path=path, final_recipient_data={}, dummy_hops=dummy_hops)
×
2053

2054
        with io.BytesIO() as blinded_path_fd:
×
2055
            OnionWireSerializer.write_field(
×
2056
                fd=blinded_path_fd,
2057
                field_type='blinded_path',
2058
                count=1,
2059
                value=blinded_path)
2060
            encoded_blinded_path = blinded_path_fd.getvalue()
×
2061

2062
        return encoded_blinded_path.hex()
×
2063

2064

2065
def plugin_command(s, plugin_name):
5✔
2066
    """Decorator to register a cli command inside a plugin. To be used within a commands.py file
2067
    in the plugins root."""
2068
    def decorator(func):
×
2069
        assert len(plugin_name) > 0, "Plugin name must not be empty"
×
2070
        func.plugin_name = plugin_name
×
2071
        name = plugin_name + '_' + func.__name__
×
2072
        if name in known_commands or hasattr(Commands, name):
×
2073
            raise Exception(f"Command name {name} already exists. Plugin commands should not overwrite other commands.")
×
2074
        assert asyncio.iscoroutinefunction(func), f"Plugin commands must be a coroutine: {name}"
×
2075

2076
        @command(s)
×
2077
        @wraps(func)
×
2078
        async def func_wrapper(*args, **kwargs):
×
2079
            cmd_runner = args[0]  # type: Commands
×
2080
            daemon = cmd_runner.daemon
×
2081
            kwargs['plugin'] = daemon._plugins.get_plugin(plugin_name)
×
2082
            return await func(*args, **kwargs)
×
2083

2084
        setattr(Commands, name, func_wrapper)
×
2085
        return func_wrapper
×
2086
    return decorator
×
2087

2088

2089
def eval_bool(x: str) -> bool:
5✔
2090
    if x == 'false':
5✔
2091
        return False
5✔
2092
    if x == 'true':
5✔
2093
        return True
5✔
2094
    # assume python, raise if malformed
2095
    return bool(ast.literal_eval(x))
5✔
2096

2097

2098
# don't use floats because of rounding errors
2099
json_loads = lambda x: json.loads(x, parse_float=lambda x: str(to_decimal(x)))
5✔
2100

2101

2102
def check_txid(txid):
5✔
2103
    if not is_hash256_str(txid):
×
2104
        raise UserFacingException(f"{repr(txid)} is not a txid")
×
2105
    return txid
×
2106

2107

2108
arg_types = {
5✔
2109
    'int': int,
2110
    'bool': eval_bool,
2111
    'str': str,
2112
    'txid': check_txid,
2113
    'tx': convert_raw_tx_to_hex,
2114
    'json': json_loads,
2115
    'decimal': lambda x: str(to_decimal(x)),
2116
    'decimal_or_dryrun': lambda x: str(to_decimal(x)) if x != 'dryrun' else x,
2117
    'decimal_or_max': lambda x: str(to_decimal(x)) if not parse_max_spend(x) else x,
2118
}
2119

2120
config_variables = {
5✔
2121
    'addrequest': {
2122
        'ssl_privkey': 'Path to your SSL private key, needed to sign the request.',
2123
        'ssl_chain': 'Chain of SSL certificates, needed for signed requests. Put your certificate at the top and the root CA at the end',
2124
        'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"',
2125
    },
2126
    'listrequests': {
2127
        'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"',
2128
    }
2129
}
2130

2131

2132
def set_default_subparser(self, name, args=None):
5✔
2133
    """see http://stackoverflow.com/questions/5176691/argparse-how-to-specify-a-default-subcommand"""
2134
    subparser_found = False
×
2135
    for arg in sys.argv[1:]:
×
2136
        if arg in ['-h', '--help', '--version']:  # global help/version if no subparser
×
2137
            break
×
2138
    else:
2139
        for x in self._subparsers._actions:
×
2140
            if not isinstance(x, argparse._SubParsersAction):
×
2141
                continue
×
2142
            for sp_name in x._name_parser_map.keys():
×
2143
                if sp_name in sys.argv[1:]:
×
2144
                    subparser_found = True
×
2145
        if not subparser_found:
×
2146
            # insert default in first position, this implies no
2147
            # global options without a sub_parsers specified
2148
            if args is None:
×
2149
                sys.argv.insert(1, name)
×
2150
            else:
2151
                args.insert(0, name)
×
2152

2153

2154
argparse.ArgumentParser.set_default_subparser = set_default_subparser
5✔
2155

2156

2157
# workaround https://bugs.python.org/issue23058
2158
# see https://github.com/nickstenning/honcho/pull/121
2159

2160
def subparser_call(self, parser, namespace, values, option_string=None):
5✔
2161
    from argparse import ArgumentError, SUPPRESS, _UNRECOGNIZED_ARGS_ATTR
×
2162
    parser_name = values[0]
×
2163
    arg_strings = values[1:]
×
2164
    # set the parser name if requested
2165
    if self.dest is not SUPPRESS:
×
2166
        setattr(namespace, self.dest, parser_name)
×
2167
    # select the parser
2168
    try:
×
2169
        parser = self._name_parser_map[parser_name]
×
2170
    except KeyError:
×
2171
        tup = parser_name, ', '.join(self._name_parser_map)
×
2172
        msg = _('unknown parser {!r} (choices: {})').format(*tup)
×
2173
        raise ArgumentError(self, msg)
×
2174
    # parse all the remaining options into the namespace
2175
    # store any unrecognized options on the object, so that the top
2176
    # level parser can decide what to do with them
2177
    namespace, arg_strings = parser.parse_known_args(arg_strings, namespace)
×
2178
    if arg_strings:
×
2179
        vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, [])
×
2180
        getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings)
×
2181

2182

2183
argparse._SubParsersAction.__call__ = subparser_call
5✔
2184

2185

2186
def add_network_options(parser):
5✔
2187
    group = parser.add_argument_group('network options')
×
2188
    group.add_argument(
×
2189
        "-f", "--serverfingerprint", dest=SimpleConfig.NETWORK_SERVERFINGERPRINT.key(), default=None,
2190
        help="only allow connecting to servers with a matching SSL certificate SHA256 fingerprint. " +
2191
        "To calculate this yourself: '$ openssl x509 -noout -fingerprint -sha256 -inform pem -in mycertfile.crt'. Enter as 64 hex chars.")
2192
    group.add_argument(
×
2193
        "-1", "--oneserver", action="store_true", dest=SimpleConfig.NETWORK_ONESERVER.key(), default=None,
2194
        help="connect to one server only")
2195
    group.add_argument(
×
2196
        "-s", "--server", dest=SimpleConfig.NETWORK_SERVER.key(), default=None,
2197
        help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)")
2198
    group.add_argument(
×
2199
        "-p", "--proxy", dest=SimpleConfig.NETWORK_PROXY.key(), default=None,
2200
        help="set proxy [type:]host:port (or 'none' to disable proxy), where type is socks4 or socks5")
2201
    group.add_argument(
×
2202
        "--proxyuser", dest=SimpleConfig.NETWORK_PROXY_USER.key(), default=None,
2203
        help="set proxy username")
2204
    group.add_argument(
×
2205
        "--proxypassword", dest=SimpleConfig.NETWORK_PROXY_PASSWORD.key(), default=None,
2206
        help="set proxy password")
2207
    group.add_argument(
×
2208
        "--noonion", action="store_true", dest=SimpleConfig.NETWORK_NOONION.key(), default=None,
2209
        help="do not try to connect to onion servers")
2210
    group.add_argument(
×
2211
        "--skipmerklecheck", action="store_true", dest=SimpleConfig.NETWORK_SKIPMERKLECHECK.key(), default=None,
2212
        help="Tolerate invalid merkle proofs from Electrum server")
2213

2214

2215
def add_global_options(parser, suppress=False):
5✔
2216
    group = parser.add_argument_group('global options')
×
2217
    group.add_argument(
×
2218
        "-v", dest="verbosity", default='',
2219
        help=argparse.SUPPRESS if suppress else "Set verbosity (log levels)")
2220
    group.add_argument(
×
2221
        "-D", "--dir", dest="electrum_path",
2222
        help=argparse.SUPPRESS if suppress else "electrum directory")
2223
    group.add_argument(
×
2224
        "-w", "--wallet", dest="wallet_path",
2225
        help=argparse.SUPPRESS if suppress else "wallet path")
2226
    group.add_argument(
×
2227
        "-P", "--portable", action="store_true", dest="portable", default=False,
2228
        help=argparse.SUPPRESS if suppress else "Use local 'electrum_data' directory")
2229
    for chain in constants.NETS_LIST:
×
2230
        group.add_argument(
×
2231
            f"--{chain.cli_flag()}", action="store_true", dest=chain.config_key(), default=False,
2232
            help=argparse.SUPPRESS if suppress else f"Use {chain.NET_NAME} chain")
2233
    group.add_argument(
×
2234
        "-o", "--offline", action="store_true", dest=SimpleConfig.NETWORK_OFFLINE.key(), default=None,
2235
        help=argparse.SUPPRESS if suppress else "Run offline")
2236
    group.add_argument(
×
2237
        "--rpcuser", dest=SimpleConfig.RPC_USERNAME.key(), default=argparse.SUPPRESS,
2238
        help=argparse.SUPPRESS if suppress else "RPC user")
2239
    group.add_argument(
×
2240
        "--rpcpassword", dest=SimpleConfig.RPC_PASSWORD.key(), default=argparse.SUPPRESS,
2241
        help=argparse.SUPPRESS if suppress else "RPC password")
2242
    group.add_argument(
×
2243
        "--forgetconfig", action="store_true", dest=SimpleConfig.CONFIG_FORGET_CHANGES.key(), default=False,
2244
        help=argparse.SUPPRESS if suppress else "Forget config on exit")
2245

2246

2247
def get_simple_parser():
5✔
2248
    """ simple parser that figures out the path of the config file and ignore unknown args """
2249
    from optparse import OptionParser, BadOptionError, AmbiguousOptionError
×
2250

2251
    class PassThroughOptionParser(OptionParser):
×
2252
        # see https://stackoverflow.com/questions/1885161/how-can-i-get-optparses-optionparser-to-ignore-invalid-options
2253
        def _process_args(self, largs, rargs, values):
×
2254
            while rargs:
×
2255
                try:
×
2256
                    OptionParser._process_args(self, largs, rargs, values)
×
2257
                except (BadOptionError, AmbiguousOptionError) as e:
×
2258
                    largs.append(e.opt_str)
×
2259

2260
    parser = PassThroughOptionParser()
×
2261
    parser.add_option("-D", "--dir", dest="electrum_path", help="electrum directory")
×
2262
    parser.add_option("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum_data' directory")
×
2263
    for chain in constants.NETS_LIST:
×
2264
        parser.add_option(f"--{chain.cli_flag()}", action="store_true", dest=chain.config_key(), default=False, help=f"Use {chain.NET_NAME} chain")
×
2265
    return parser
×
2266

2267

2268
def get_parser():
5✔
2269
    # create main parser
2270
    parser = argparse.ArgumentParser(
×
2271
        epilog="Run 'electrum help <command>' to see the help for a command")
2272
    parser.add_argument("--version", dest="cmd", action='store_const', const='version', help="Return the version of Electrum.")
×
2273
    add_global_options(parser)
×
2274
    subparsers = parser.add_subparsers(dest='cmd', metavar='<command>')
×
2275
    # gui
2276
    parser_gui = subparsers.add_parser('gui', description="Run Electrum's Graphical User Interface.", help="Run GUI (default)")
×
2277
    parser_gui.add_argument("url", nargs='?', default=None, help="bitcoin URI (or bip70 file)")
×
2278
    parser_gui.add_argument("-g", "--gui", dest=SimpleConfig.GUI_NAME.key(), help="select graphical user interface", choices=['qt', 'text', 'stdio', 'qml'])
×
2279
    parser_gui.add_argument("-m", action="store_true", dest=SimpleConfig.GUI_QT_HIDE_ON_STARTUP.key(), default=False, help="hide GUI on startup")
×
2280
    parser_gui.add_argument("-L", "--lang", dest=SimpleConfig.LOCALIZATION_LANGUAGE.key(), default=None, help="default language used in GUI")
×
2281
    parser_gui.add_argument("--daemon", action="store_true", dest="daemon", default=False, help="keep daemon running after GUI is closed")
×
2282
    parser_gui.add_argument("--nosegwit", action="store_true", dest=SimpleConfig.WIZARD_DONT_CREATE_SEGWIT.key(), default=False, help="Do not create segwit wallets")
×
2283
    add_network_options(parser_gui)
×
2284
    add_global_options(parser_gui)
×
2285
    # daemon
2286
    parser_daemon = subparsers.add_parser('daemon', help="Run Daemon")
×
2287
    parser_daemon.add_argument("-d", "--detached", action="store_true", dest="detach", default=False, help="run daemon in detached mode")
×
2288
    # FIXME: all these options are rpc-server-side. The CLI client-side cannot use e.g. --rpcport,
2289
    #        instead it reads it from the daemon lockfile.
2290
    parser_daemon.add_argument("--rpchost", dest=SimpleConfig.RPC_HOST.key(), default=argparse.SUPPRESS, help="RPC host")
×
2291
    parser_daemon.add_argument("--rpcport", dest=SimpleConfig.RPC_PORT.key(), type=int, default=argparse.SUPPRESS, help="RPC port")
×
2292
    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'])
×
2293
    parser_daemon.add_argument("--rpcsockpath", dest=SimpleConfig.RPC_SOCKET_FILEPATH.key(), help="where to place RPC file socket")
×
2294
    add_network_options(parser_daemon)
×
2295
    add_global_options(parser_daemon)
×
2296
    # commands
2297
    for cmdname in sorted(known_commands.keys()):
×
2298
        cmd = known_commands[cmdname]
×
2299
        p = subparsers.add_parser(
×
2300
            cmdname,
2301
            description=cmd.description,
2302
            help=cmd.short_description,
2303
            epilog="Run 'electrum -h to see the list of global options",
2304
        )
2305
        for optname, default in zip(cmd.options, cmd.defaults):
×
2306
            if optname in ['wallet_path', 'wallet', 'plugin']:
×
2307
                continue
×
2308
            if optname == 'password':
×
2309
                p.add_argument("--password", dest='password', help="Wallet password. Use '--password :' if you want a prompt.")
×
2310
                continue
×
2311
            help = cmd.arg_descriptions.get(optname)
×
2312
            if not help:
×
2313
                print(f'undocumented argument {cmdname}::{optname}')
×
2314
            action = "store_true" if default is False else 'store'
×
2315
            if action == 'store':
×
2316
                type_descriptor = cmd.arg_types.get(optname)
×
2317
                _type = arg_types.get(type_descriptor, str)
×
2318
                p.add_argument('--' + optname, dest=optname, action=action, default=default, help=help, type=_type)
×
2319
            else:
2320
                p.add_argument('--' + optname, dest=optname, action=action, default=default, help=help)
×
2321
        add_global_options(p, suppress=True)
×
2322

2323
        for param in cmd.params:
×
2324
            if param in ['wallet_path', 'wallet']:
×
2325
                continue
×
2326
            help = cmd.arg_descriptions.get(param)
×
2327
            if not help:
×
2328
                print(f'undocumented argument {cmdname}::{param}')
×
2329
            type_descriptor = cmd.arg_types.get(param)
×
2330
            _type = arg_types.get(type_descriptor)
×
2331
            if help is not None and _type is None:
×
2332
                print(f'unknown type \'{_type}\' for {cmdname}::{param}')
×
2333
            p.add_argument(param, help=help, type=_type)
×
2334

2335
        cvh = config_variables.get(cmdname)
×
2336
        if cvh:
×
2337
            group = p.add_argument_group('configuration variables', '(set with setconfig/getconfig)')
×
2338
            for k, v in cvh.items():
×
2339
                group.add_argument(k, nargs='?', help=v)
×
2340

2341
    # 'gui' is the default command
2342
    # note: set_default_subparser modifies sys.argv
2343
    parser.set_default_subparser('gui')
×
2344
    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