commands.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