tcommands.py - electrum - Electrum Bitcoin wallet
HTML git clone https://git.parazyd.org/electrum
DIR Log
DIR Files
DIR Refs
DIR Submodules
---
tcommands.py (62776B)
---
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
26 import sys
27 import datetime
28 import copy
29 import argparse
30 import json
31 import ast
32 import base64
33 import operator
34 import asyncio
35 import inspect
36 from functools import wraps, partial
37 from itertools import repeat
38 from decimal import Decimal
39 from typing import Optional, TYPE_CHECKING, Dict, List
40
41 from .import util, ecc
42 from .util import (bfh, bh2u, format_satoshis, json_decode, json_normalize,
43 is_hash256_str, is_hex_str, to_bytes)
44 from . import bitcoin
45 from .bitcoin import is_address, hash_160, COIN
46 from .bip32 import BIP32Node
47 from .i18n import _
48 from .transaction import (Transaction, multisig_script, TxOutput, PartialTransaction, PartialTxOutput,
49 tx_from_any, PartialTxInput, TxOutpoint)
50 from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
51 from .synchronizer import Notifier
52 from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text, Deterministic_Wallet
53 from .address_synchronizer import TX_HEIGHT_LOCAL
54 from .mnemonic import Mnemonic
55 from .lnutil import SENT, RECEIVED
56 from .lnutil import LnFeatures
57 from .lnutil import ln_dummy_address
58 from .lnpeer import channel_id_from_funding_tx
59 from .plugin import run_hook
60 from .version import ELECTRUM_VERSION
61 from .simple_config import SimpleConfig
62 from .invoices import LNInvoice
63 from . import submarine_swaps
64
65
66 if TYPE_CHECKING:
67 from .network import Network
68 from .daemon import Daemon
69
70
71 known_commands = {} # type: Dict[str, Command]
72
73
74 class NotSynchronizedException(Exception):
75 pass
76
77
78 def satoshis(amount):
79 # satoshi conversion must not be performed by the parser
80 return int(COIN*Decimal(amount)) if amount not in ['!', None] else amount
81
82 def format_satoshis(x):
83 return str(Decimal(x)/COIN) if x is not None else None
84
85
86 class Command:
87 def __init__(self, func, s):
88 self.name = func.__name__
89 self.requires_network = 'n' in s
90 self.requires_wallet = 'w' in s
91 self.requires_password = 'p' in s
92 self.description = func.__doc__
93 self.help = self.description.split('.')[0] if self.description else None
94 varnames = func.__code__.co_varnames[1:func.__code__.co_argcount]
95 self.defaults = func.__defaults__
96 if self.defaults:
97 n = len(self.defaults)
98 self.params = list(varnames[:-n])
99 self.options = list(varnames[-n:])
100 else:
101 self.params = list(varnames)
102 self.options = []
103 self.defaults = []
104
105 # sanity checks
106 if self.requires_password:
107 assert self.requires_wallet
108 for varname in ('wallet_path', 'wallet'):
109 if varname in varnames:
110 assert varname in self.options
111 assert not ('wallet_path' in varnames and 'wallet' in varnames)
112 if self.requires_wallet:
113 assert 'wallet' in varnames
114
115
116 def command(s):
117 def decorator(func):
118 global known_commands
119 name = func.__name__
120 known_commands[name] = Command(func, s)
121 @wraps(func)
122 async def func_wrapper(*args, **kwargs):
123 cmd_runner = args[0] # type: Commands
124 cmd = known_commands[func.__name__] # type: Command
125 password = kwargs.get('password')
126 daemon = cmd_runner.daemon
127 if daemon:
128 if 'wallet_path' in cmd.options and kwargs.get('wallet_path') is None:
129 kwargs['wallet_path'] = daemon.config.get_wallet_path()
130 if cmd.requires_wallet and kwargs.get('wallet') is None:
131 kwargs['wallet'] = daemon.config.get_wallet_path()
132 if 'wallet' in cmd.options:
133 wallet_path = kwargs.get('wallet', None)
134 if isinstance(wallet_path, str):
135 wallet = daemon.get_wallet(wallet_path)
136 if wallet is None:
137 raise Exception('wallet not loaded')
138 kwargs['wallet'] = wallet
139 wallet = kwargs.get('wallet') # type: Optional[Abstract_Wallet]
140 if cmd.requires_wallet and not wallet:
141 raise Exception('wallet not loaded')
142 if cmd.requires_password and password is None and wallet.has_password():
143 raise Exception('Password required')
144 return await func(*args, **kwargs)
145 return func_wrapper
146 return decorator
147
148
149 class Commands:
150
151 def __init__(self, *, config: 'SimpleConfig',
152 network: 'Network' = None,
153 daemon: 'Daemon' = None, callback=None):
154 self.config = config
155 self.daemon = daemon
156 self.network = network
157 self._callback = callback
158
159 def _run(self, method, args, password_getter=None, **kwargs):
160 """This wrapper is called from unit tests and the Qt python console."""
161 cmd = known_commands[method]
162 password = kwargs.get('password', None)
163 wallet = kwargs.get('wallet', None)
164 if (cmd.requires_password and wallet and wallet.has_password()
165 and password is None):
166 password = password_getter()
167 if password is None:
168 return
169
170 f = getattr(self, method)
171 if cmd.requires_password:
172 kwargs['password'] = password
173
174 if 'wallet' in kwargs:
175 sig = inspect.signature(f)
176 if 'wallet' not in sig.parameters:
177 kwargs.pop('wallet')
178
179 coro = f(*args, **kwargs)
180 fut = asyncio.run_coroutine_threadsafe(coro, asyncio.get_event_loop())
181 result = fut.result()
182
183 if self._callback:
184 self._callback()
185 return result
186
187 @command('')
188 async def commands(self):
189 """List of commands"""
190 return ' '.join(sorted(known_commands.keys()))
191
192 @command('n')
193 async def getinfo(self):
194 """ network info """
195 net_params = self.network.get_parameters()
196 response = {
197 'path': self.network.config.path,
198 'server': net_params.server.host,
199 'blockchain_height': self.network.get_local_height(),
200 'server_height': self.network.get_server_height(),
201 'spv_nodes': len(self.network.get_interfaces()),
202 'connected': self.network.is_connected(),
203 'auto_connect': net_params.auto_connect,
204 'version': ELECTRUM_VERSION,
205 'default_wallet': self.config.get_wallet_path(),
206 'fee_per_kb': self.config.fee_per_kb(),
207 }
208 return response
209
210 @command('n')
211 async def stop(self):
212 """Stop daemon"""
213 self.daemon.stop()
214 return "Daemon stopped"
215
216 @command('n')
217 async def list_wallets(self):
218 """List wallets open in daemon"""
219 return [{'path': path, 'synchronized': w.is_up_to_date()}
220 for path, w in self.daemon.get_wallets().items()]
221
222 @command('n')
223 async def load_wallet(self, wallet_path=None, password=None):
224 """Open wallet in daemon"""
225 wallet = self.daemon.load_wallet(wallet_path, password, manual_upgrades=False)
226 if wallet is not None:
227 run_hook('load_wallet', wallet, None)
228 response = wallet is not None
229 return response
230
231 @command('n')
232 async def close_wallet(self, wallet_path=None):
233 """Close wallet"""
234 return self.daemon.stop_wallet(wallet_path)
235
236 @command('')
237 async def create(self, passphrase=None, password=None, encrypt_file=True, seed_type=None, wallet_path=None):
238 """Create a new wallet.
239 If you want to be prompted for an argument, type '?' or ':' (concealed)
240 """
241 d = create_new_wallet(path=wallet_path,
242 passphrase=passphrase,
243 password=password,
244 encrypt_file=encrypt_file,
245 seed_type=seed_type,
246 config=self.config)
247 return {
248 'seed': d['seed'],
249 'path': d['wallet'].storage.path,
250 'msg': d['msg'],
251 }
252
253 @command('')
254 async def restore(self, text, passphrase=None, password=None, encrypt_file=True, wallet_path=None):
255 """Restore a wallet from text. Text can be a seed phrase, a master
256 public key, a master private key, a list of bitcoin addresses
257 or bitcoin private keys.
258 If you want to be prompted for an argument, type '?' or ':' (concealed)
259 """
260 # TODO create a separate command that blocks until wallet is synced
261 d = restore_wallet_from_text(text,
262 path=wallet_path,
263 passphrase=passphrase,
264 password=password,
265 encrypt_file=encrypt_file,
266 config=self.config)
267 return {
268 'path': d['wallet'].storage.path,
269 'msg': d['msg'],
270 }
271
272 @command('wp')
273 async def password(self, password=None, new_password=None, wallet: Abstract_Wallet = None):
274 """Change wallet password. """
275 if wallet.storage.is_encrypted_with_hw_device() and new_password:
276 raise Exception("Can't change the password of a wallet encrypted with a hw device.")
277 b = wallet.storage.is_encrypted()
278 wallet.update_password(password, new_password, encrypt_storage=b)
279 wallet.save_db()
280 return {'password':wallet.has_password()}
281
282 @command('w')
283 async def get(self, key, wallet: Abstract_Wallet = None):
284 """Return item from wallet storage"""
285 return wallet.db.get(key)
286
287 @command('')
288 async def getconfig(self, key):
289 """Return a configuration variable. """
290 return self.config.get(key)
291
292 @classmethod
293 def _setconfig_normalize_value(cls, key, value):
294 if key not in ('rpcuser', 'rpcpassword'):
295 value = json_decode(value)
296 # call literal_eval for backward compatibility (see #4225)
297 try:
298 value = ast.literal_eval(value)
299 except:
300 pass
301 return value
302
303 @command('')
304 async def setconfig(self, key, value):
305 """Set a configuration variable. 'value' may be a string or a Python expression."""
306 value = self._setconfig_normalize_value(key, value)
307 if self.daemon and key == 'rpcuser':
308 self.daemon.commands_server.rpc_user = value
309 if self.daemon and key == 'rpcpassword':
310 self.daemon.commands_server.rpc_password = value
311 self.config.set_key(key, value)
312 return True
313
314 @command('')
315 async def get_ssl_domain(self):
316 """Check and return the SSL domain set in ssl_keyfile and ssl_certfile
317 """
318 return self.config.get_ssl_domain()
319
320 @command('')
321 async def make_seed(self, nbits=None, language=None, seed_type=None):
322 """Create a seed"""
323 from .mnemonic import Mnemonic
324 s = Mnemonic(language).make_seed(seed_type=seed_type, num_bits=nbits)
325 return s
326
327 @command('n')
328 async def getaddresshistory(self, address):
329 """Return the transaction history of any address. Note: This is a
330 walletless server query, results are not checked by SPV.
331 """
332 sh = bitcoin.address_to_scripthash(address)
333 return await self.network.get_history_for_scripthash(sh)
334
335 @command('w')
336 async def listunspent(self, wallet: Abstract_Wallet = None):
337 """List unspent outputs. Returns the list of unspent transaction
338 outputs in your wallet."""
339 coins = []
340 for txin in wallet.get_utxos():
341 d = txin.to_json()
342 v = d.pop("value_sats")
343 d["value"] = str(Decimal(v)/COIN) if v is not None else None
344 coins.append(d)
345 return coins
346
347 @command('n')
348 async def getaddressunspent(self, address):
349 """Returns the UTXO list of any address. Note: This
350 is a walletless server query, results are not checked by SPV.
351 """
352 sh = bitcoin.address_to_scripthash(address)
353 return await self.network.listunspent_for_scripthash(sh)
354
355 @command('')
356 async def serialize(self, jsontx):
357 """Create a transaction from json inputs.
358 Inputs must have a redeemPubkey.
359 Outputs must be a list of {'address':address, 'value':satoshi_amount}.
360 """
361 keypairs = {}
362 inputs = [] # type: List[PartialTxInput]
363 locktime = jsontx.get('locktime', 0)
364 for txin_dict in jsontx.get('inputs'):
365 if txin_dict.get('prevout_hash') is not None and txin_dict.get('prevout_n') is not None:
366 prevout = TxOutpoint(txid=bfh(txin_dict['prevout_hash']), out_idx=int(txin_dict['prevout_n']))
367 elif txin_dict.get('output'):
368 prevout = TxOutpoint.from_str(txin_dict['output'])
369 else:
370 raise Exception("missing prevout for txin")
371 txin = PartialTxInput(prevout=prevout)
372 txin._trusted_value_sats = int(txin_dict.get('value', txin_dict['value_sats']))
373 nsequence = txin_dict.get('nsequence', None)
374 if nsequence is not None:
375 txin.nsequence = nsequence
376 sec = txin_dict.get('privkey')
377 if sec:
378 txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)
379 pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)
380 keypairs[pubkey] = privkey, compressed
381 txin.script_type = txin_type
382 txin.pubkeys = [bfh(pubkey)]
383 txin.num_sig = 1
384 inputs.append(txin)
385
386 outputs = [PartialTxOutput.from_address_and_value(txout['address'], int(txout.get('value', txout['value_sats'])))
387 for txout in jsontx.get('outputs')]
388 tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
389 tx.sign(keypairs)
390 return tx.serialize()
391
392 @command('wp')
393 async def signtransaction(self, tx, privkey=None, password=None, wallet: Abstract_Wallet = None):
394 """Sign a transaction. The wallet keys will be used unless a private key is provided."""
395 tx = tx_from_any(tx)
396 if privkey:
397 txin_type, privkey2, compressed = bitcoin.deserialize_privkey(privkey)
398 pubkey = ecc.ECPrivkey(privkey2).get_public_key_bytes(compressed=compressed).hex()
399 tx.sign({pubkey:(privkey2, compressed)})
400 else:
401 wallet.sign_transaction(tx, password)
402 return tx.serialize()
403
404 @command('')
405 async def deserialize(self, tx):
406 """Deserialize a serialized transaction"""
407 tx = tx_from_any(tx)
408 return tx.to_json()
409
410 @command('n')
411 async def broadcast(self, tx):
412 """Broadcast a transaction to the network. """
413 tx = Transaction(tx)
414 await self.network.broadcast_transaction(tx)
415 return tx.txid()
416
417 @command('')
418 async def createmultisig(self, num, pubkeys):
419 """Create multisig address"""
420 assert isinstance(pubkeys, list), (type(num), type(pubkeys))
421 redeem_script = multisig_script(pubkeys, num)
422 address = bitcoin.hash160_to_p2sh(hash_160(bfh(redeem_script)))
423 return {'address':address, 'redeemScript':redeem_script}
424
425 @command('w')
426 async def freeze(self, address: str, wallet: Abstract_Wallet = None):
427 """Freeze address. Freeze the funds at one of your wallet\'s addresses"""
428 return wallet.set_frozen_state_of_addresses([address], True)
429
430 @command('w')
431 async def unfreeze(self, address: str, wallet: Abstract_Wallet = None):
432 """Unfreeze address. Unfreeze the funds at one of your wallet\'s address"""
433 return wallet.set_frozen_state_of_addresses([address], False)
434
435 @command('w')
436 async def freeze_utxo(self, coin: str, wallet: Abstract_Wallet = None):
437 """Freeze a UTXO so that the wallet will not spend it."""
438 wallet.set_frozen_state_of_coins([coin], True)
439 return True
440
441 @command('w')
442 async def unfreeze_utxo(self, coin: str, wallet: Abstract_Wallet = None):
443 """Unfreeze a UTXO so that the wallet might spend it."""
444 wallet.set_frozen_state_of_coins([coin], False)
445 return True
446
447 @command('wp')
448 async def getprivatekeys(self, address, password=None, wallet: Abstract_Wallet = None):
449 """Get private keys of addresses. You may pass a single wallet address, or a list of wallet addresses."""
450 if isinstance(address, str):
451 address = address.strip()
452 if is_address(address):
453 return wallet.export_private_key(address, password)
454 domain = address
455 return [wallet.export_private_key(address, password) for address in domain]
456
457 @command('wp')
458 async def getprivatekeyforpath(self, path, password=None, wallet: Abstract_Wallet = None):
459 """Get private key corresponding to derivation path (address index).
460 'path' can be either a str such as "m/0/50", or a list of ints such as [0, 50].
461 """
462 return wallet.export_private_key_for_path(path, password)
463
464 @command('w')
465 async def ismine(self, address, wallet: Abstract_Wallet = None):
466 """Check if address is in wallet. Return true if and only address is in wallet"""
467 return wallet.is_mine(address)
468
469 @command('')
470 async def dumpprivkeys(self):
471 """Deprecated."""
472 return "This command is deprecated. Use a pipe instead: 'electrum listaddresses | electrum getprivatekeys - '"
473
474 @command('')
475 async def validateaddress(self, address):
476 """Check that an address is valid. """
477 return is_address(address)
478
479 @command('w')
480 async def getpubkeys(self, address, wallet: Abstract_Wallet = None):
481 """Return the public keys for a wallet address. """
482 return wallet.get_public_keys(address)
483
484 @command('w')
485 async def getbalance(self, wallet: Abstract_Wallet = None):
486 """Return the balance of your wallet. """
487 c, u, x = wallet.get_balance()
488 l = wallet.lnworker.get_balance() if wallet.lnworker else None
489 out = {"confirmed": str(Decimal(c)/COIN)}
490 if u:
491 out["unconfirmed"] = str(Decimal(u)/COIN)
492 if x:
493 out["unmatured"] = str(Decimal(x)/COIN)
494 if l:
495 out["lightning"] = str(Decimal(l)/COIN)
496 return out
497
498 @command('n')
499 async def getaddressbalance(self, address):
500 """Return the balance of any address. Note: This is a walletless
501 server query, results are not checked by SPV.
502 """
503 sh = bitcoin.address_to_scripthash(address)
504 out = await self.network.get_balance_for_scripthash(sh)
505 out["confirmed"] = str(Decimal(out["confirmed"])/COIN)
506 out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN)
507 return out
508
509 @command('n')
510 async def getmerkle(self, txid, height):
511 """Get Merkle branch of a transaction included in a block. Electrum
512 uses this to verify transactions (Simple Payment Verification)."""
513 return await self.network.get_merkle_for_transaction(txid, int(height))
514
515 @command('n')
516 async def getservers(self):
517 """Return the list of known servers (candidates for connecting)."""
518 return self.network.get_servers()
519
520 @command('')
521 async def version(self):
522 """Return the version of Electrum."""
523 from .version import ELECTRUM_VERSION
524 return ELECTRUM_VERSION
525
526 @command('w')
527 async def getmpk(self, wallet: Abstract_Wallet = None):
528 """Get master public key. Return your wallet\'s master public key"""
529 return wallet.get_master_public_key()
530
531 @command('wp')
532 async def getmasterprivate(self, password=None, wallet: Abstract_Wallet = None):
533 """Get master private key. Return your wallet\'s master private key"""
534 return str(wallet.keystore.get_master_private_key(password))
535
536 @command('')
537 async def convert_xkey(self, xkey, xtype):
538 """Convert xtype of a master key. e.g. xpub -> ypub"""
539 try:
540 node = BIP32Node.from_xkey(xkey)
541 except:
542 raise Exception('xkey should be a master public/private key')
543 return node._replace(xtype=xtype).to_xkey()
544
545 @command('wp')
546 async def getseed(self, password=None, wallet: Abstract_Wallet = None):
547 """Get seed phrase. Print the generation seed of your wallet."""
548 s = wallet.get_seed(password)
549 return s
550
551 @command('wp')
552 async def importprivkey(self, privkey, password=None, wallet: Abstract_Wallet = None):
553 """Import a private key."""
554 if not wallet.can_import_privkey():
555 return "Error: This type of wallet cannot import private keys. Try to create a new wallet with that key."
556 try:
557 addr = wallet.import_private_key(privkey, password)
558 out = "Keypair imported: " + addr
559 except Exception as e:
560 out = "Error: " + repr(e)
561 return out
562
563 def _resolver(self, x, wallet):
564 if x is None:
565 return None
566 out = wallet.contacts.resolve(x)
567 if out.get('type') == 'openalias' and self.nocheck is False and out.get('validated') is False:
568 raise Exception('cannot verify alias', x)
569 return out['address']
570
571 @command('n')
572 async def sweep(self, privkey, destination, fee=None, nocheck=False, imax=100):
573 """Sweep private keys. Returns a transaction that spends UTXOs from
574 privkey to a destination address. The transaction is not
575 broadcasted."""
576 from .wallet import sweep
577 tx_fee = satoshis(fee)
578 privkeys = privkey.split()
579 self.nocheck = nocheck
580 #dest = self._resolver(destination)
581 tx = await sweep(
582 privkeys,
583 network=self.network,
584 config=self.config,
585 to_address=destination,
586 fee=tx_fee,
587 imax=imax,
588 )
589 return tx.serialize() if tx else None
590
591 @command('wp')
592 async def signmessage(self, address, message, password=None, wallet: Abstract_Wallet = None):
593 """Sign a message with a key. Use quotes if your message contains
594 whitespaces"""
595 sig = wallet.sign_message(address, message, password)
596 return base64.b64encode(sig).decode('ascii')
597
598 @command('')
599 async def verifymessage(self, address, signature, message):
600 """Verify a signature."""
601 sig = base64.b64decode(signature)
602 message = util.to_bytes(message)
603 return ecc.verify_message_with_address(address, sig, message)
604
605 @command('wp')
606 async def payto(self, destination, amount, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None,
607 nocheck=False, unsigned=False, rbf=None, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None):
608 """Create a transaction. """
609 self.nocheck = nocheck
610 tx_fee = satoshis(fee)
611 domain_addr = from_addr.split(',') if from_addr else None
612 domain_coins = from_coins.split(',') if from_coins else None
613 change_addr = self._resolver(change_addr, wallet)
614 domain_addr = None if domain_addr is None else map(self._resolver, domain_addr, repeat(wallet))
615 amount_sat = satoshis(amount)
616 outputs = [PartialTxOutput.from_address_and_value(destination, amount_sat)]
617 tx = wallet.create_transaction(
618 outputs,
619 fee=tx_fee,
620 feerate=feerate,
621 change_addr=change_addr,
622 domain_addr=domain_addr,
623 domain_coins=domain_coins,
624 unsigned=unsigned,
625 rbf=rbf,
626 password=password,
627 locktime=locktime)
628 result = tx.serialize()
629 if addtransaction:
630 await self.addtransaction(result, wallet=wallet)
631 return result
632
633 @command('wp')
634 async def paytomany(self, outputs, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None,
635 nocheck=False, unsigned=False, rbf=None, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None):
636 """Create a multi-output transaction. """
637 self.nocheck = nocheck
638 tx_fee = satoshis(fee)
639 domain_addr = from_addr.split(',') if from_addr else None
640 domain_coins = from_coins.split(',') if from_coins else None
641 change_addr = self._resolver(change_addr, wallet)
642 domain_addr = None if domain_addr is None else map(self._resolver, domain_addr, repeat(wallet))
643 final_outputs = []
644 for address, amount in outputs:
645 address = self._resolver(address, wallet)
646 amount_sat = satoshis(amount)
647 final_outputs.append(PartialTxOutput.from_address_and_value(address, amount_sat))
648 tx = wallet.create_transaction(
649 final_outputs,
650 fee=tx_fee,
651 feerate=feerate,
652 change_addr=change_addr,
653 domain_addr=domain_addr,
654 domain_coins=domain_coins,
655 unsigned=unsigned,
656 rbf=rbf,
657 password=password,
658 locktime=locktime)
659 result = tx.serialize()
660 if addtransaction:
661 await self.addtransaction(result, wallet=wallet)
662 return result
663
664 @command('w')
665 async def onchain_history(self, year=None, show_addresses=False, show_fiat=False, wallet: Abstract_Wallet = None,
666 from_height=None, to_height=None):
667 """Wallet onchain history. Returns the transaction history of your wallet."""
668 kwargs = {
669 'show_addresses': show_addresses,
670 'from_height': from_height,
671 'to_height': to_height,
672 }
673 if year:
674 import time
675 start_date = datetime.datetime(year, 1, 1)
676 end_date = datetime.datetime(year+1, 1, 1)
677 kwargs['from_timestamp'] = time.mktime(start_date.timetuple())
678 kwargs['to_timestamp'] = time.mktime(end_date.timetuple())
679 if show_fiat:
680 from .exchange_rate import FxThread
681 fx = FxThread(self.config, None)
682 kwargs['fx'] = fx
683
684 return json_normalize(wallet.get_detailed_history(**kwargs))
685
686 @command('w')
687 async def lightning_history(self, show_fiat=False, wallet: Abstract_Wallet = None):
688 """ lightning history """
689 lightning_history = wallet.lnworker.get_history() if wallet.lnworker else []
690 return json_normalize(lightning_history)
691
692 @command('w')
693 async def setlabel(self, key, label, wallet: Abstract_Wallet = None):
694 """Assign a label to an item. Item may be a bitcoin address or a
695 transaction ID"""
696 wallet.set_label(key, label)
697
698 @command('w')
699 async def listcontacts(self, wallet: Abstract_Wallet = None):
700 """Show your list of contacts"""
701 return wallet.contacts
702
703 @command('w')
704 async def getalias(self, key, wallet: Abstract_Wallet = None):
705 """Retrieve alias. Lookup in your list of contacts, and for an OpenAlias DNS record."""
706 return wallet.contacts.resolve(key)
707
708 @command('w')
709 async def searchcontacts(self, query, wallet: Abstract_Wallet = None):
710 """Search through contacts, return matching entries. """
711 results = {}
712 for key, value in wallet.contacts.items():
713 if query.lower() in key.lower():
714 results[key] = value
715 return results
716
717 @command('w')
718 async def listaddresses(self, receiving=False, change=False, labels=False, frozen=False, unused=False, funded=False, balance=False, wallet: Abstract_Wallet = None):
719 """List wallet addresses. Returns the list of all addresses in your wallet. Use optional arguments to filter the results."""
720 out = []
721 for addr in wallet.get_addresses():
722 if frozen and not wallet.is_frozen_address(addr):
723 continue
724 if receiving and wallet.is_change(addr):
725 continue
726 if change and not wallet.is_change(addr):
727 continue
728 if unused and wallet.is_used(addr):
729 continue
730 if funded and wallet.is_empty(addr):
731 continue
732 item = addr
733 if labels or balance:
734 item = (item,)
735 if balance:
736 item += (format_satoshis(sum(wallet.get_addr_balance(addr))),)
737 if labels:
738 item += (repr(wallet.get_label(addr)),)
739 out.append(item)
740 return out
741
742 @command('n')
743 async def gettransaction(self, txid, wallet: Abstract_Wallet = None):
744 """Retrieve a transaction. """
745 tx = None
746 if wallet:
747 tx = wallet.db.get_transaction(txid)
748 if tx is None:
749 raw = await self.network.get_transaction(txid)
750 if raw:
751 tx = Transaction(raw)
752 else:
753 raise Exception("Unknown transaction")
754 if tx.txid() != txid:
755 raise Exception("Mismatching txid")
756 return tx.serialize()
757
758 @command('')
759 async def encrypt(self, pubkey, message) -> str:
760 """Encrypt a message with a public key. Use quotes if the message contains whitespaces."""
761 if not is_hex_str(pubkey):
762 raise Exception(f"pubkey must be a hex string instead of {repr(pubkey)}")
763 try:
764 message = to_bytes(message)
765 except TypeError:
766 raise Exception(f"message must be a string-like object instead of {repr(message)}")
767 public_key = ecc.ECPubkey(bfh(pubkey))
768 encrypted = public_key.encrypt_message(message)
769 return encrypted.decode('utf-8')
770
771 @command('wp')
772 async def decrypt(self, pubkey, encrypted, password=None, wallet: Abstract_Wallet = None) -> str:
773 """Decrypt a message encrypted with a public key."""
774 if not is_hex_str(pubkey):
775 raise Exception(f"pubkey must be a hex string instead of {repr(pubkey)}")
776 if not isinstance(encrypted, (str, bytes, bytearray)):
777 raise Exception(f"encrypted must be a string-like object instead of {repr(encrypted)}")
778 decrypted = wallet.decrypt_message(pubkey, encrypted, password)
779 return decrypted.decode('utf-8')
780
781 @command('w')
782 async def getrequest(self, key, wallet: Abstract_Wallet = None):
783 """Return a payment request"""
784 r = wallet.get_request(key)
785 if not r:
786 raise Exception("Request not found")
787 return wallet.export_request(r)
788
789 #@command('w')
790 #async def ackrequest(self, serialized):
791 # """<Not implemented>"""
792 # pass
793
794 @command('w')
795 async def list_requests(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None):
796 """List the payment requests you made."""
797 if pending:
798 f = PR_UNPAID
799 elif expired:
800 f = PR_EXPIRED
801 elif paid:
802 f = PR_PAID
803 else:
804 f = None
805 out = wallet.get_sorted_requests()
806 if f is not None:
807 out = [req for req in out
808 if f == wallet.get_request_status(wallet.get_key_for_receive_request(req))]
809 return [wallet.export_request(x) for x in out]
810
811 @command('w')
812 async def createnewaddress(self, wallet: Abstract_Wallet = None):
813 """Create a new receiving address, beyond the gap limit of the wallet"""
814 return wallet.create_new_address(False)
815
816 @command('w')
817 async def changegaplimit(self, new_limit, iknowwhatimdoing=False, wallet: Abstract_Wallet = None):
818 """Change the gap limit of the wallet."""
819 if not iknowwhatimdoing:
820 raise Exception("WARNING: Are you SURE you want to change the gap limit?\n"
821 "It makes recovering your wallet from seed difficult!\n"
822 "Please do your research and make sure you understand the implications.\n"
823 "Typically only merchants and power users might want to do this.\n"
824 "To proceed, try again, with the --iknowwhatimdoing option.")
825 if not isinstance(wallet, Deterministic_Wallet):
826 raise Exception("This wallet is not deterministic.")
827 return wallet.change_gap_limit(new_limit)
828
829 @command('wn')
830 async def getminacceptablegap(self, wallet: Abstract_Wallet = None):
831 """Returns the minimum value for gap limit that would be sufficient to discover all
832 known addresses in the wallet.
833 """
834 if not isinstance(wallet, Deterministic_Wallet):
835 raise Exception("This wallet is not deterministic.")
836 if not wallet.is_up_to_date():
837 raise NotSynchronizedException("Wallet not fully synchronized.")
838 return wallet.min_acceptable_gap()
839
840 @command('w')
841 async def getunusedaddress(self, wallet: Abstract_Wallet = None):
842 """Returns the first unused address of the wallet, or None if all addresses are used.
843 An address is considered as used if it has received a transaction, or if it is used in a payment request."""
844 return wallet.get_unused_address()
845
846 @command('w')
847 async def add_request(self, amount, memo='', expiration=3600, force=False, wallet: Abstract_Wallet = None):
848 """Create a payment request, using the first unused address of the wallet.
849 The address will be considered as used after this operation.
850 If no payment is received, the address will be considered as unused if the payment request is deleted from the wallet."""
851 addr = wallet.get_unused_address()
852 if addr is None:
853 if force:
854 addr = wallet.create_new_address(False)
855 else:
856 return False
857 amount = satoshis(amount)
858 expiration = int(expiration) if expiration else None
859 req = wallet.make_payment_request(addr, amount, memo, expiration)
860 wallet.add_payment_request(req)
861 wallet.save_db()
862 return wallet.export_request(req)
863
864 @command('wn')
865 async def add_lightning_request(self, amount, memo='', expiration=3600, wallet: Abstract_Wallet = None):
866 amount_sat = int(satoshis(amount))
867 key = await wallet.lnworker._add_request_coro(amount_sat, memo, expiration)
868 wallet.save_db()
869 return wallet.get_formatted_request(key)
870
871 @command('w')
872 async def addtransaction(self, tx, wallet: Abstract_Wallet = None):
873 """ Add a transaction to the wallet history """
874 tx = Transaction(tx)
875 if not wallet.add_transaction(tx):
876 return False
877 wallet.save_db()
878 return tx.txid()
879
880 @command('wp')
881 async def signrequest(self, address, password=None, wallet: Abstract_Wallet = None):
882 "Sign payment request with an OpenAlias"
883 alias = self.config.get('alias')
884 if not alias:
885 raise Exception('No alias in your configuration')
886 alias_addr = wallet.contacts.resolve(alias)['address']
887 wallet.sign_payment_request(address, alias, alias_addr, password)
888
889 @command('w')
890 async def rmrequest(self, address, wallet: Abstract_Wallet = None):
891 """Remove a payment request"""
892 result = wallet.remove_payment_request(address)
893 wallet.save_db()
894 return result
895
896 @command('w')
897 async def clear_requests(self, wallet: Abstract_Wallet = None):
898 """Remove all payment requests"""
899 wallet.clear_requests()
900 return True
901
902 @command('w')
903 async def clear_invoices(self, wallet: Abstract_Wallet = None):
904 """Remove all invoices"""
905 wallet.clear_invoices()
906 return True
907
908 @command('n')
909 async def notify(self, address: str, URL: Optional[str]):
910 """Watch an address. Every time the address changes, a http POST is sent to the URL.
911 Call with an empty URL to stop watching an address.
912 """
913 if not hasattr(self, "_notifier"):
914 self._notifier = Notifier(self.network)
915 if URL:
916 await self._notifier.start_watching_addr(address, URL)
917 else:
918 await self._notifier.stop_watching_addr(address)
919 return True
920
921 @command('wn')
922 async def is_synchronized(self, wallet: Abstract_Wallet = None):
923 """ return wallet synchronization status """
924 return wallet.is_up_to_date()
925
926 @command('n')
927 async def getfeerate(self, fee_method=None, fee_level=None):
928 """Return current suggested fee rate (in sat/kvByte), according to config
929 settings or supplied parameters.
930 """
931 if fee_method is None:
932 dyn, mempool = None, None
933 elif fee_method.lower() == 'static':
934 dyn, mempool = False, False
935 elif fee_method.lower() == 'eta':
936 dyn, mempool = True, False
937 elif fee_method.lower() == 'mempool':
938 dyn, mempool = True, True
939 else:
940 raise Exception('Invalid fee estimation method: {}'.format(fee_method))
941 if fee_level is not None:
942 fee_level = Decimal(fee_level)
943 return self.config.fee_per_kb(dyn=dyn, mempool=mempool, fee_level=fee_level)
944
945 @command('w')
946 async def removelocaltx(self, txid, wallet: Abstract_Wallet = None):
947 """Remove a 'local' transaction from the wallet, and its dependent
948 transactions.
949 """
950 if not is_hash256_str(txid):
951 raise Exception(f"{repr(txid)} is not a txid")
952 height = wallet.get_tx_height(txid).height
953 if height != TX_HEIGHT_LOCAL:
954 raise Exception(f'Only local transactions can be removed. '
955 f'This tx has height: {height} != {TX_HEIGHT_LOCAL}')
956 wallet.remove_transaction(txid)
957 wallet.save_db()
958
959 @command('wn')
960 async def get_tx_status(self, txid, wallet: Abstract_Wallet = None):
961 """Returns some information regarding the tx. For now, only confirmations.
962 The transaction must be related to the wallet.
963 """
964 if not is_hash256_str(txid):
965 raise Exception(f"{repr(txid)} is not a txid")
966 if not wallet.db.get_transaction(txid):
967 raise Exception("Transaction not in wallet.")
968 return {
969 "confirmations": wallet.get_tx_height(txid).conf,
970 }
971
972 @command('')
973 async def help(self):
974 # for the python console
975 return sorted(known_commands.keys())
976
977 # lightning network commands
978 @command('wn')
979 async def add_peer(self, connection_string, timeout=20, gossip=False, wallet: Abstract_Wallet = None):
980 lnworker = self.network.lngossip if gossip else wallet.lnworker
981 await lnworker.add_peer(connection_string)
982 return True
983
984 @command('wn')
985 async def list_peers(self, gossip=False, wallet: Abstract_Wallet = None):
986 lnworker = self.network.lngossip if gossip else wallet.lnworker
987 return [{
988 'node_id':p.pubkey.hex(),
989 'address':p.transport.name(),
990 'initialized':p.is_initialized(),
991 'features': str(LnFeatures(p.features)),
992 'channels': [c.funding_outpoint.to_str() for c in p.channels.values()],
993 } for p in lnworker.peers.values()]
994
995 @command('wpn')
996 async def open_channel(self, connection_string, amount, push_amount=0, password=None, wallet: Abstract_Wallet = None):
997 funding_sat = satoshis(amount)
998 push_sat = satoshis(push_amount)
999 dummy_output = PartialTxOutput.from_address_and_value(ln_dummy_address(), funding_sat)
1000 funding_tx = wallet.mktx(outputs = [dummy_output], rbf=False, sign=False, nonlocal_only=True)
1001 chan, funding_tx = await wallet.lnworker._open_channel_coroutine(connect_str=connection_string,
1002 funding_tx=funding_tx,
1003 funding_sat=funding_sat,
1004 push_sat=push_sat,
1005 password=password)
1006 return chan.funding_outpoint.to_str()
1007
1008 @command('')
1009 async def decode_invoice(self, invoice: str):
1010 invoice = LNInvoice.from_bech32(invoice)
1011 return invoice.to_debug_json()
1012
1013 @command('wn')
1014 async def lnpay(self, invoice, attempts=1, timeout=30, wallet: Abstract_Wallet = None):
1015 lnworker = wallet.lnworker
1016 lnaddr = lnworker._check_invoice(invoice)
1017 payment_hash = lnaddr.paymenthash
1018 wallet.save_invoice(LNInvoice.from_bech32(invoice))
1019 success, log = await lnworker.pay_invoice(invoice, attempts=attempts)
1020 return {
1021 'payment_hash': payment_hash.hex(),
1022 'success': success,
1023 'preimage': lnworker.get_preimage(payment_hash).hex() if success else None,
1024 'log': [x.formatted_tuple() for x in log]
1025 }
1026
1027 @command('w')
1028 async def nodeid(self, wallet: Abstract_Wallet = None):
1029 listen_addr = self.config.get('lightning_listen')
1030 return bh2u(wallet.lnworker.node_keypair.pubkey) + (('@' + listen_addr) if listen_addr else '')
1031
1032 @command('w')
1033 async def list_channels(self, wallet: Abstract_Wallet = None):
1034 # we output the funding_outpoint instead of the channel_id because lnd uses channel_point (funding outpoint) to identify channels
1035 from .lnutil import LOCAL, REMOTE, format_short_channel_id
1036 l = list(wallet.lnworker.channels.items())
1037 return [
1038 {
1039 'short_channel_id': format_short_channel_id(chan.short_channel_id) if chan.short_channel_id else None,
1040 'channel_id': bh2u(chan.channel_id),
1041 'channel_point': chan.funding_outpoint.to_str(),
1042 'state': chan.get_state().name,
1043 'peer_state': chan.peer_state.name,
1044 'remote_pubkey': bh2u(chan.node_id),
1045 'local_balance': chan.balance(LOCAL)//1000,
1046 'remote_balance': chan.balance(REMOTE)//1000,
1047 'local_reserve': chan.config[REMOTE].reserve_sat, # their config has our reserve
1048 'remote_reserve': chan.config[LOCAL].reserve_sat,
1049 'local_unsettled_sent': chan.balance_tied_up_in_htlcs_by_direction(LOCAL, direction=SENT) // 1000,
1050 'remote_unsettled_sent': chan.balance_tied_up_in_htlcs_by_direction(REMOTE, direction=SENT) // 1000,
1051 } for channel_id, chan in l
1052 ]
1053
1054 @command('wn')
1055 async def dumpgraph(self, wallet: Abstract_Wallet = None):
1056 return wallet.lnworker.channel_db.to_dict()
1057
1058 @command('n')
1059 async def inject_fees(self, fees):
1060 import ast
1061 self.network.config.fee_estimates = ast.literal_eval(fees)
1062 self.network.notify('fee')
1063
1064 @command('wn')
1065 async def enable_htlc_settle(self, b: bool, wallet: Abstract_Wallet = None):
1066 e = wallet.lnworker.enable_htlc_settle
1067 e.set() if b else e.clear()
1068
1069 @command('n')
1070 async def clear_ln_blacklist(self):
1071 self.network.path_finder.blacklist.clear()
1072
1073 @command('w')
1074 async def list_invoices(self, wallet: Abstract_Wallet = None):
1075 l = wallet.get_invoices()
1076 return [wallet.export_invoice(x) for x in l]
1077
1078 @command('wn')
1079 async def close_channel(self, channel_point, force=False, wallet: Abstract_Wallet = None):
1080 txid, index = channel_point.split(':')
1081 chan_id, _ = channel_id_from_funding_tx(txid, int(index))
1082 coro = wallet.lnworker.force_close_channel(chan_id) if force else wallet.lnworker.close_channel(chan_id)
1083 return await coro
1084
1085 @command('wn')
1086 async def request_force_close(self, channel_point, wallet: Abstract_Wallet = None):
1087 txid, index = channel_point.split(':')
1088 chan_id, _ = channel_id_from_funding_tx(txid, int(index))
1089 return await wallet.lnworker.request_force_close_from_backup(chan_id)
1090
1091 @command('w')
1092 async def export_channel_backup(self, channel_point, wallet: Abstract_Wallet = None):
1093 txid, index = channel_point.split(':')
1094 chan_id, _ = channel_id_from_funding_tx(txid, int(index))
1095 return wallet.lnworker.export_channel_backup(chan_id)
1096
1097 @command('w')
1098 async def import_channel_backup(self, encrypted, wallet: Abstract_Wallet = None):
1099 return wallet.lnworker.import_channel_backup(encrypted)
1100
1101 @command('wn')
1102 async def get_channel_ctx(self, channel_point, iknowwhatimdoing=False, wallet: Abstract_Wallet = None):
1103 """ return the current commitment transaction of a channel """
1104 if not iknowwhatimdoing:
1105 raise Exception("WARNING: this command is potentially unsafe.\n"
1106 "To proceed, try again, with the --iknowwhatimdoing option.")
1107 txid, index = channel_point.split(':')
1108 chan_id, _ = channel_id_from_funding_tx(txid, int(index))
1109 chan = wallet.lnworker.channels[chan_id]
1110 tx = chan.force_close_tx()
1111 return tx.serialize()
1112
1113 @command('wn')
1114 async def get_watchtower_ctn(self, channel_point, wallet: Abstract_Wallet = None):
1115 """ return the local watchtower's ctn of channel. used in regtests """
1116 return await self.network.local_watchtower.sweepstore.get_ctn(channel_point, None)
1117
1118 @command('wnp')
1119 async def normal_swap(self, onchain_amount, lightning_amount, password=None, wallet: Abstract_Wallet = None):
1120 """
1121 Normal submarine swap: send on-chain BTC, receive on Lightning
1122 Note that your funds will be locked for 24h if you do not have enough incoming capacity.
1123 """
1124 sm = wallet.lnworker.swap_manager
1125 if lightning_amount == 'dryrun':
1126 await sm.get_pairs()
1127 onchain_amount_sat = satoshis(onchain_amount)
1128 lightning_amount_sat = sm.get_recv_amount(onchain_amount_sat, is_reverse=False)
1129 txid = None
1130 elif onchain_amount == 'dryrun':
1131 await sm.get_pairs()
1132 lightning_amount_sat = satoshis(lightning_amount)
1133 onchain_amount_sat = sm.get_send_amount(lightning_amount_sat, is_reverse=False)
1134 txid = None
1135 else:
1136 lightning_amount_sat = satoshis(lightning_amount)
1137 onchain_amount_sat = satoshis(onchain_amount)
1138 txid = await wallet.lnworker.swap_manager.normal_swap(
1139 lightning_amount_sat=lightning_amount_sat,
1140 expected_onchain_amount_sat=onchain_amount_sat,
1141 password=password,
1142 )
1143 return {
1144 'txid': txid,
1145 'lightning_amount': format_satoshis(lightning_amount_sat),
1146 'onchain_amount': format_satoshis(onchain_amount_sat),
1147 }
1148
1149 @command('wn')
1150 async def reverse_swap(self, lightning_amount, onchain_amount, wallet: Abstract_Wallet = None):
1151 """Reverse submarine swap: send on Lightning, receive on-chain
1152 """
1153 sm = wallet.lnworker.swap_manager
1154 if onchain_amount == 'dryrun':
1155 await sm.get_pairs()
1156 lightning_amount_sat = satoshis(lightning_amount)
1157 onchain_amount_sat = sm.get_recv_amount(lightning_amount_sat, is_reverse=True)
1158 success = None
1159 elif lightning_amount == 'dryrun':
1160 await sm.get_pairs()
1161 onchain_amount_sat = satoshis(onchain_amount)
1162 lightning_amount_sat = sm.get_send_amount(onchain_amount_sat, is_reverse=True)
1163 success = None
1164 else:
1165 lightning_amount_sat = satoshis(lightning_amount)
1166 onchain_amount_sat = satoshis(onchain_amount)
1167 success = await wallet.lnworker.swap_manager.reverse_swap(
1168 lightning_amount_sat=lightning_amount_sat,
1169 expected_onchain_amount_sat=onchain_amount_sat,
1170 )
1171 return {
1172 'success': success,
1173 'lightning_amount': format_satoshis(lightning_amount_sat),
1174 'onchain_amount': format_satoshis(onchain_amount_sat),
1175 }
1176
1177
1178 def eval_bool(x: str) -> bool:
1179 if x == 'false': return False
1180 if x == 'true': return True
1181 try:
1182 return bool(ast.literal_eval(x))
1183 except:
1184 return bool(x)
1185
1186 param_descriptions = {
1187 'privkey': 'Private key. Type \'?\' to get a prompt.',
1188 'destination': 'Bitcoin address, contact or alias',
1189 'address': 'Bitcoin address',
1190 'seed': 'Seed phrase',
1191 'txid': 'Transaction ID',
1192 'pos': 'Position',
1193 'height': 'Block height',
1194 'tx': 'Serialized transaction (hexadecimal)',
1195 'key': 'Variable name',
1196 'pubkey': 'Public key',
1197 'message': 'Clear text message. Use quotes if it contains spaces.',
1198 'encrypted': 'Encrypted message',
1199 'amount': 'Amount to be sent (in BTC). Type \'!\' to send the maximum available.',
1200 'requested_amount': 'Requested amount (in BTC).',
1201 'outputs': 'list of ["address", amount]',
1202 'redeem_script': 'redeem script (hexadecimal)',
1203 'lightning_amount': "Amount sent or received in a submarine swap. Set it to 'dryrun' to receive a value",
1204 'onchain_amount': "Amount sent or received in a submarine swap. Set it to 'dryrun' to receive a value",
1205 }
1206
1207 command_options = {
1208 'password': ("-W", "Password"),
1209 'new_password':(None, "New Password"),
1210 'encrypt_file':(None, "Whether the file on disk should be encrypted with the provided password"),
1211 'receiving': (None, "Show only receiving addresses"),
1212 'change': (None, "Show only change addresses"),
1213 'frozen': (None, "Show only frozen addresses"),
1214 'unused': (None, "Show only unused addresses"),
1215 'funded': (None, "Show only funded addresses"),
1216 'balance': ("-b", "Show the balances of listed addresses"),
1217 'labels': ("-l", "Show the labels of listed addresses"),
1218 'nocheck': (None, "Do not verify aliases"),
1219 'imax': (None, "Maximum number of inputs"),
1220 'fee': ("-f", "Transaction fee (absolute, in BTC)"),
1221 'feerate': (None, "Transaction fee rate (in sat/byte)"),
1222 'from_addr': ("-F", "Source address (must be a wallet address; use sweep to spend from non-wallet address)."),
1223 'from_coins': (None, "Source coins (must be in wallet; use sweep to spend from non-wallet address)."),
1224 'change_addr': ("-c", "Change address. Default is a spare address, or the source address if it's not in the wallet"),
1225 'nbits': (None, "Number of bits of entropy"),
1226 'seed_type': (None, "The type of seed to create, e.g. 'standard' or 'segwit'"),
1227 'language': ("-L", "Default language for wordlist"),
1228 'passphrase': (None, "Seed extension"),
1229 'privkey': (None, "Private key. Set to '?' to get a prompt."),
1230 'unsigned': ("-u", "Do not sign transaction"),
1231 'rbf': (None, "Whether to signal opt-in Replace-By-Fee in the transaction (true/false)"),
1232 'locktime': (None, "Set locktime block number"),
1233 'addtransaction': (None,'Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet'),
1234 'domain': ("-D", "List of addresses"),
1235 'memo': ("-m", "Description of the request"),
1236 'expiration': (None, "Time in seconds"),
1237 'attempts': (None, "Number of payment attempts"),
1238 'timeout': (None, "Timeout in seconds"),
1239 'force': (None, "Create new address beyond gap limit, if no more addresses are available."),
1240 'pending': (None, "Show only pending requests."),
1241 'push_amount': (None, 'Push initial amount (in BTC)'),
1242 'expired': (None, "Show only expired requests."),
1243 'paid': (None, "Show only paid requests."),
1244 'show_addresses': (None, "Show input and output addresses"),
1245 'show_fiat': (None, "Show fiat value of transactions"),
1246 'show_fees': (None, "Show miner fees paid by transactions"),
1247 'year': (None, "Show history for a given year"),
1248 'fee_method': (None, "Fee estimation method to use"),
1249 'fee_level': (None, "Float between 0.0 and 1.0, representing fee slider position"),
1250 'from_height': (None, "Only show transactions that confirmed after given block height"),
1251 'to_height': (None, "Only show transactions that confirmed before given block height"),
1252 'iknowwhatimdoing': (None, "Acknowledge that I understand the full implications of what I am about to do"),
1253 'gossip': (None, "Apply command to gossip node instead of wallet"),
1254 }
1255
1256
1257 # don't use floats because of rounding errors
1258 from .transaction import convert_raw_tx_to_hex
1259 json_loads = lambda x: json.loads(x, parse_float=lambda x: str(Decimal(x)))
1260 arg_types = {
1261 'num': int,
1262 'nbits': int,
1263 'imax': int,
1264 'year': int,
1265 'from_height': int,
1266 'to_height': int,
1267 'tx': convert_raw_tx_to_hex,
1268 'pubkeys': json_loads,
1269 'jsontx': json_loads,
1270 'inputs': json_loads,
1271 'outputs': json_loads,
1272 'fee': lambda x: str(Decimal(x)) if x is not None else None,
1273 'amount': lambda x: str(Decimal(x)) if x != '!' else '!',
1274 'locktime': int,
1275 'addtransaction': eval_bool,
1276 'fee_method': str,
1277 'fee_level': json_loads,
1278 'encrypt_file': eval_bool,
1279 'rbf': eval_bool,
1280 'timeout': float,
1281 'attempts': int,
1282 }
1283
1284 config_variables = {
1285
1286 'addrequest': {
1287 'ssl_privkey': 'Path to your SSL private key, needed to sign the request.',
1288 'ssl_chain': 'Chain of SSL certificates, needed for signed requests. Put your certificate at the top and the root CA at the end',
1289 'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"',
1290 },
1291 'listrequests':{
1292 'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"',
1293 }
1294 }
1295
1296 def set_default_subparser(self, name, args=None):
1297 """see http://stackoverflow.com/questions/5176691/argparse-how-to-specify-a-default-subcommand"""
1298 subparser_found = False
1299 for arg in sys.argv[1:]:
1300 if arg in ['-h', '--help']: # global help if no subparser
1301 break
1302 else:
1303 for x in self._subparsers._actions:
1304 if not isinstance(x, argparse._SubParsersAction):
1305 continue
1306 for sp_name in x._name_parser_map.keys():
1307 if sp_name in sys.argv[1:]:
1308 subparser_found = True
1309 if not subparser_found:
1310 # insert default in first position, this implies no
1311 # global options without a sub_parsers specified
1312 if args is None:
1313 sys.argv.insert(1, name)
1314 else:
1315 args.insert(0, name)
1316
1317 argparse.ArgumentParser.set_default_subparser = set_default_subparser
1318
1319
1320 # workaround https://bugs.python.org/issue23058
1321 # see https://github.com/nickstenning/honcho/pull/121
1322
1323 def subparser_call(self, parser, namespace, values, option_string=None):
1324 from argparse import ArgumentError, SUPPRESS, _UNRECOGNIZED_ARGS_ATTR
1325 parser_name = values[0]
1326 arg_strings = values[1:]
1327 # set the parser name if requested
1328 if self.dest is not SUPPRESS:
1329 setattr(namespace, self.dest, parser_name)
1330 # select the parser
1331 try:
1332 parser = self._name_parser_map[parser_name]
1333 except KeyError:
1334 tup = parser_name, ', '.join(self._name_parser_map)
1335 msg = _('unknown parser {!r} (choices: {})').format(*tup)
1336 raise ArgumentError(self, msg)
1337 # parse all the remaining options into the namespace
1338 # store any unrecognized options on the object, so that the top
1339 # level parser can decide what to do with them
1340 namespace, arg_strings = parser.parse_known_args(arg_strings, namespace)
1341 if arg_strings:
1342 vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, [])
1343 getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings)
1344
1345 argparse._SubParsersAction.__call__ = subparser_call
1346
1347
1348 def add_network_options(parser):
1349 parser.add_argument("-f", "--serverfingerprint", dest="serverfingerprint", default=None, help="only allow connecting to servers with a matching SSL certificate SHA256 fingerprint." + " " +
1350 "To calculate this yourself: '$ openssl x509 -noout -fingerprint -sha256 -inform pem -in mycertfile.crt'. Enter as 64 hex chars.")
1351 parser.add_argument("-1", "--oneserver", action="store_true", dest="oneserver", default=None, help="connect to one server only")
1352 parser.add_argument("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)")
1353 parser.add_argument("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port] (or 'none' to disable proxy), where type is socks4,socks5 or http")
1354 parser.add_argument("--noonion", action="store_true", dest="noonion", default=None, help="do not try to connect to onion servers")
1355 parser.add_argument("--skipmerklecheck", action="store_true", dest="skipmerklecheck", default=None, help="Tolerate invalid merkle proofs from server")
1356
1357 def add_global_options(parser):
1358 group = parser.add_argument_group('global options')
1359 group.add_argument("-v", dest="verbosity", help="Set verbosity (log levels)", default='')
1360 group.add_argument("-V", dest="verbosity_shortcuts", help="Set verbosity (shortcut-filter list)", default='')
1361 group.add_argument("-D", "--dir", dest="electrum_path", help="electrum directory")
1362 group.add_argument("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum_data' directory")
1363 group.add_argument("--testnet", action="store_true", dest="testnet", default=False, help="Use Testnet")
1364 group.add_argument("--regtest", action="store_true", dest="regtest", default=False, help="Use Regtest")
1365 group.add_argument("--simnet", action="store_true", dest="simnet", default=False, help="Use Simnet")
1366 group.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline")
1367
1368 def add_wallet_option(parser):
1369 parser.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path")
1370 parser.add_argument("--forgetconfig", action="store_true", dest="forget_config", default=False, help="Forget config on exit")
1371
1372 def get_parser():
1373 # create main parser
1374 parser = argparse.ArgumentParser(
1375 epilog="Run 'electrum help <command>' to see the help for a command")
1376 add_global_options(parser)
1377 add_wallet_option(parser)
1378 subparsers = parser.add_subparsers(dest='cmd', metavar='<command>')
1379 # gui
1380 parser_gui = subparsers.add_parser('gui', description="Run Electrum's Graphical User Interface.", help="Run GUI (default)")
1381 parser_gui.add_argument("url", nargs='?', default=None, help="bitcoin URI (or bip70 file)")
1382 parser_gui.add_argument("-g", "--gui", dest="gui", help="select graphical user interface", choices=['qt', 'kivy', 'text', 'stdio'])
1383 parser_gui.add_argument("-m", action="store_true", dest="hide_gui", default=False, help="hide GUI on startup")
1384 parser_gui.add_argument("-L", "--lang", dest="language", default=None, help="default language used in GUI")
1385 parser_gui.add_argument("--daemon", action="store_true", dest="daemon", default=False, help="keep daemon running after GUI is closed")
1386 parser_gui.add_argument("--nosegwit", action="store_true", dest="nosegwit", default=False, help="Do not create segwit wallets")
1387 add_wallet_option(parser_gui)
1388 add_network_options(parser_gui)
1389 add_global_options(parser_gui)
1390 # daemon
1391 parser_daemon = subparsers.add_parser('daemon', help="Run Daemon")
1392 parser_daemon.add_argument("-d", "--detached", action="store_true", dest="detach", default=False, help="run daemon in detached mode")
1393 add_network_options(parser_daemon)
1394 add_global_options(parser_daemon)
1395 # commands
1396 for cmdname in sorted(known_commands.keys()):
1397 cmd = known_commands[cmdname]
1398 p = subparsers.add_parser(cmdname, help=cmd.help, description=cmd.description)
1399 for optname, default in zip(cmd.options, cmd.defaults):
1400 if optname in ['wallet_path', 'wallet']:
1401 add_wallet_option(p)
1402 continue
1403 a, help = command_options[optname]
1404 b = '--' + optname
1405 action = "store_true" if default is False else 'store'
1406 args = (a, b) if a else (b,)
1407 if action == 'store':
1408 _type = arg_types.get(optname, str)
1409 p.add_argument(*args, dest=optname, action=action, default=default, help=help, type=_type)
1410 else:
1411 p.add_argument(*args, dest=optname, action=action, default=default, help=help)
1412 add_global_options(p)
1413
1414 for param in cmd.params:
1415 if param in ['wallet_path', 'wallet']:
1416 continue
1417 h = param_descriptions.get(param, '')
1418 _type = arg_types.get(param, str)
1419 p.add_argument(param, help=h, type=_type)
1420
1421 cvh = config_variables.get(cmdname)
1422 if cvh:
1423 group = p.add_argument_group('configuration variables', '(set with setconfig/getconfig)')
1424 for k, v in cvh.items():
1425 group.add_argument(k, nargs='?', help=v)
1426
1427 # 'gui' is the default command
1428 parser.set_default_subparser('gui')
1429 return parser