You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1308 lines
56 KiB
1308 lines
56 KiB
#!/usr/bin/env python |
|
# |
|
# Electrum - lightweight Bitcoin client |
|
# Copyright (C) 2011 thomasv@gitorious |
|
# |
|
# Permission is hereby granted, free of charge, to any person |
|
# obtaining a copy of this software and associated documentation files |
|
# (the "Software"), to deal in the Software without restriction, |
|
# including without limitation the rights to use, copy, modify, merge, |
|
# publish, distribute, sublicense, and/or sell copies of the Software, |
|
# and to permit persons to whom the Software is furnished to do so, |
|
# subject to the following conditions: |
|
# |
|
# The above copyright notice and this permission notice shall be |
|
# included in all copies or substantial portions of the Software. |
|
# |
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
# SOFTWARE. |
|
|
|
import sys |
|
import datetime |
|
import copy |
|
import argparse |
|
import json |
|
import ast |
|
import base64 |
|
import operator |
|
import asyncio |
|
import inspect |
|
from functools import wraps, partial |
|
from itertools import repeat |
|
from decimal import Decimal |
|
from typing import Optional, TYPE_CHECKING, Dict, List |
|
|
|
from .import util, ecc |
|
from .util import bfh, bh2u, format_satoshis, json_decode, json_encode, is_hash256_str, is_hex_str, to_bytes, timestamp_to_datetime |
|
from .util import standardize_path |
|
from . import bitcoin |
|
from .bitcoin import is_address, hash_160, COIN |
|
from .bip32 import BIP32Node |
|
from .i18n import _ |
|
from .transaction import (Transaction, multisig_script, TxOutput, PartialTransaction, PartialTxOutput, |
|
tx_from_any, PartialTxInput, TxOutpoint) |
|
from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED |
|
from .synchronizer import Notifier |
|
from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text, Deterministic_Wallet |
|
from .address_synchronizer import TX_HEIGHT_LOCAL |
|
from .mnemonic import Mnemonic |
|
from .lnutil import SENT, RECEIVED |
|
from .lnutil import ln_dummy_address |
|
from .lnpeer import channel_id_from_funding_tx |
|
from .plugin import run_hook |
|
from .version import ELECTRUM_VERSION |
|
from .simple_config import SimpleConfig |
|
|
|
|
|
if TYPE_CHECKING: |
|
from .network import Network |
|
from .daemon import Daemon |
|
|
|
|
|
known_commands = {} # type: Dict[str, Command] |
|
|
|
|
|
def satoshis(amount): |
|
# satoshi conversion must not be performed by the parser |
|
return int(COIN*Decimal(amount)) if amount not in ['!', None] else amount |
|
|
|
|
|
def json_normalize(x): |
|
# note: The return value of commands, when going through the JSON-RPC interface, |
|
# is json-encoded. The encoder used there cannot handle some types, e.g. electrum.util.Satoshis. |
|
# note: We should not simply do "json_encode(x)" here, as then later x would get doubly json-encoded. |
|
# see #5868 |
|
return json_decode(json_encode(x)) |
|
|
|
|
|
class Command: |
|
def __init__(self, func, s): |
|
self.name = func.__name__ |
|
self.requires_network = 'n' in s |
|
self.requires_wallet = 'w' in s |
|
self.requires_password = 'p' in s |
|
self.description = func.__doc__ |
|
self.help = self.description.split('.')[0] if self.description else None |
|
varnames = func.__code__.co_varnames[1:func.__code__.co_argcount] |
|
self.defaults = func.__defaults__ |
|
if self.defaults: |
|
n = len(self.defaults) |
|
self.params = list(varnames[:-n]) |
|
self.options = list(varnames[-n:]) |
|
else: |
|
self.params = list(varnames) |
|
self.options = [] |
|
self.defaults = [] |
|
|
|
|
|
def command(s): |
|
def decorator(func): |
|
global known_commands |
|
name = func.__name__ |
|
known_commands[name] = Command(func, s) |
|
@wraps(func) |
|
async def func_wrapper(*args, **kwargs): |
|
cmd_runner = args[0] # type: Commands |
|
cmd = known_commands[func.__name__] # type: Command |
|
password = kwargs.get('password') |
|
daemon = cmd_runner.daemon |
|
if daemon: |
|
if (cmd.requires_wallet or 'wallet_path' in cmd.options) and kwargs.get('wallet_path') is None: |
|
# using JSON-RPC, sometimes the "wallet" kwarg needs to be used to specify a wallet |
|
kwargs['wallet_path'] = kwargs.pop('wallet', None) or daemon.config.get_wallet_path() |
|
if cmd.requires_wallet: |
|
wallet_path = kwargs.pop('wallet_path') |
|
wallet = daemon.get_wallet(wallet_path) |
|
if wallet is None: |
|
raise Exception('wallet not loaded') |
|
kwargs['wallet'] = wallet |
|
else: |
|
# we are offline. the wallet must have been passed if required |
|
wallet = kwargs.get('wallet') |
|
if cmd.requires_password and password is None and wallet.has_password(): |
|
raise Exception('Password required') |
|
return await func(*args, **kwargs) |
|
return func_wrapper |
|
return decorator |
|
|
|
|
|
class Commands: |
|
|
|
def __init__(self, *, config: 'SimpleConfig', |
|
network: 'Network' = None, |
|
daemon: 'Daemon' = None, callback=None): |
|
self.config = config |
|
self.daemon = daemon |
|
self.network = network |
|
self._callback = callback |
|
|
|
def _run(self, method, args, password_getter=None, **kwargs): |
|
"""This wrapper is called from unit tests and the Qt python console.""" |
|
cmd = known_commands[method] |
|
password = kwargs.get('password', None) |
|
wallet = kwargs.get('wallet', None) |
|
if (cmd.requires_password and wallet and wallet.has_password() |
|
and password is None): |
|
password = password_getter() |
|
if password is None: |
|
return |
|
|
|
f = getattr(self, method) |
|
if cmd.requires_password: |
|
kwargs['password'] = password |
|
|
|
if 'wallet' in kwargs: |
|
sig = inspect.signature(f) |
|
if 'wallet' not in sig.parameters: |
|
kwargs.pop('wallet') |
|
|
|
coro = f(*args, **kwargs) |
|
fut = asyncio.run_coroutine_threadsafe(coro, asyncio.get_event_loop()) |
|
result = fut.result() |
|
|
|
if self._callback: |
|
self._callback() |
|
return result |
|
|
|
@command('') |
|
async def commands(self): |
|
"""List of commands""" |
|
return ' '.join(sorted(known_commands.keys())) |
|
|
|
@command('n') |
|
async def getinfo(self): |
|
""" network info """ |
|
net_params = self.network.get_parameters() |
|
response = { |
|
'path': self.network.config.path, |
|
'server': net_params.host, |
|
'blockchain_height': self.network.get_local_height(), |
|
'server_height': self.network.get_server_height(), |
|
'spv_nodes': len(self.network.get_interfaces()), |
|
'connected': self.network.is_connected(), |
|
'auto_connect': net_params.auto_connect, |
|
'version': ELECTRUM_VERSION, |
|
'default_wallet': self.config.get_wallet_path(), |
|
'fee_per_kb': self.config.fee_per_kb(), |
|
} |
|
return response |
|
|
|
@command('n') |
|
async def stop(self): |
|
"""Stop daemon""" |
|
self.daemon.stop() |
|
return "Daemon stopped" |
|
|
|
@command('n') |
|
async def list_wallets(self): |
|
"""List wallets open in daemon""" |
|
return [{'path': path, 'synchronized': w.is_up_to_date()} |
|
for path, w in self.daemon.get_wallets().items()] |
|
|
|
@command('n') |
|
async def load_wallet(self, wallet_path=None, password=None): |
|
"""Open wallet in daemon""" |
|
wallet = self.daemon.load_wallet(wallet_path, password, manual_upgrades=False) |
|
if wallet is not None: |
|
run_hook('load_wallet', wallet, None) |
|
response = wallet is not None |
|
return response |
|
|
|
@command('n') |
|
async def close_wallet(self, wallet_path=None): |
|
"""Close wallet""" |
|
return self.daemon.stop_wallet(wallet_path) |
|
|
|
@command('') |
|
async def create(self, passphrase=None, password=None, encrypt_file=True, seed_type=None, wallet_path=None): |
|
"""Create a new wallet. |
|
If you want to be prompted for an argument, type '?' or ':' (concealed) |
|
""" |
|
d = create_new_wallet(path=wallet_path, |
|
passphrase=passphrase, |
|
password=password, |
|
encrypt_file=encrypt_file, |
|
seed_type=seed_type, |
|
config=self.config) |
|
return { |
|
'seed': d['seed'], |
|
'path': d['wallet'].storage.path, |
|
'msg': d['msg'], |
|
} |
|
|
|
@command('') |
|
async def restore(self, text, passphrase=None, password=None, encrypt_file=True, wallet_path=None): |
|
"""Restore a wallet from text. Text can be a seed phrase, a master |
|
public key, a master private key, a list of bitcoin addresses |
|
or bitcoin private keys. |
|
If you want to be prompted for an argument, type '?' or ':' (concealed) |
|
""" |
|
# TODO create a separate command that blocks until wallet is synced |
|
d = restore_wallet_from_text(text, |
|
path=wallet_path, |
|
passphrase=passphrase, |
|
password=password, |
|
encrypt_file=encrypt_file, |
|
config=self.config) |
|
return { |
|
'path': d['wallet'].storage.path, |
|
'msg': d['msg'], |
|
} |
|
|
|
@command('wp') |
|
async def password(self, password=None, new_password=None, wallet: Abstract_Wallet = None): |
|
"""Change wallet password. """ |
|
if wallet.storage.is_encrypted_with_hw_device() and new_password: |
|
raise Exception("Can't change the password of a wallet encrypted with a hw device.") |
|
b = wallet.storage.is_encrypted() |
|
wallet.update_password(password, new_password, encrypt_storage=b) |
|
wallet.save_db() |
|
return {'password':wallet.has_password()} |
|
|
|
@command('w') |
|
async def get(self, key, wallet: Abstract_Wallet = None): |
|
"""Return item from wallet storage""" |
|
return wallet.db.get(key) |
|
|
|
@command('') |
|
async def getconfig(self, key): |
|
"""Return a configuration variable. """ |
|
return self.config.get(key) |
|
|
|
@classmethod |
|
def _setconfig_normalize_value(cls, key, value): |
|
if key not in ('rpcuser', 'rpcpassword'): |
|
value = json_decode(value) |
|
try: |
|
value = ast.literal_eval(value) |
|
except: |
|
pass |
|
return value |
|
|
|
@command('') |
|
async def setconfig(self, key, value): |
|
"""Set a configuration variable. 'value' may be a string or a Python expression.""" |
|
value = self._setconfig_normalize_value(key, value) |
|
self.config.set_key(key, value) |
|
return True |
|
|
|
@command('') |
|
async def make_seed(self, nbits=132, language=None, seed_type=None): |
|
"""Create a seed""" |
|
from .mnemonic import Mnemonic |
|
s = Mnemonic(language).make_seed(seed_type, num_bits=nbits) |
|
return s |
|
|
|
@command('n') |
|
async def getaddresshistory(self, address): |
|
"""Return the transaction history of any address. Note: This is a |
|
walletless server query, results are not checked by SPV. |
|
""" |
|
sh = bitcoin.address_to_scripthash(address) |
|
return await self.network.get_history_for_scripthash(sh) |
|
|
|
@command('w') |
|
async def listunspent(self, wallet: Abstract_Wallet = None): |
|
"""List unspent outputs. Returns the list of unspent transaction |
|
outputs in your wallet.""" |
|
coins = [] |
|
for txin in wallet.get_utxos(): |
|
d = txin.to_json() |
|
v = d.pop("value_sats") |
|
d["value"] = str(Decimal(v)/COIN) if v is not None else None |
|
coins.append(d) |
|
return coins |
|
|
|
@command('n') |
|
async def getaddressunspent(self, address): |
|
"""Returns the UTXO list of any address. Note: This |
|
is a walletless server query, results are not checked by SPV. |
|
""" |
|
sh = bitcoin.address_to_scripthash(address) |
|
return await self.network.listunspent_for_scripthash(sh) |
|
|
|
@command('') |
|
async def serialize(self, jsontx): |
|
"""Create a transaction from json inputs. |
|
Inputs must have a redeemPubkey. |
|
Outputs must be a list of {'address':address, 'value':satoshi_amount}. |
|
""" |
|
keypairs = {} |
|
inputs = [] # type: List[PartialTxInput] |
|
locktime = jsontx.get('lockTime', 0) |
|
for txin_dict in jsontx.get('inputs'): |
|
if txin_dict.get('prevout_hash') is not None and txin_dict.get('prevout_n') is not None: |
|
prevout = TxOutpoint(txid=bfh(txin_dict['prevout_hash']), out_idx=int(txin_dict['prevout_n'])) |
|
elif txin_dict.get('output'): |
|
prevout = TxOutpoint.from_str(txin_dict['output']) |
|
else: |
|
raise Exception("missing prevout for txin") |
|
txin = PartialTxInput(prevout=prevout) |
|
txin._trusted_value_sats = int(txin_dict['value']) |
|
sec = txin_dict.get('privkey') |
|
if sec: |
|
txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec) |
|
pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) |
|
keypairs[pubkey] = privkey, compressed |
|
txin.script_type = txin_type |
|
txin.pubkeys = [bfh(pubkey)] |
|
txin.num_sig = 1 |
|
inputs.append(txin) |
|
|
|
outputs = [PartialTxOutput.from_address_and_value(txout['address'], int(txout['value'])) |
|
for txout in jsontx.get('outputs')] |
|
tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime) |
|
tx.sign(keypairs) |
|
return tx.serialize() |
|
|
|
@command('wp') |
|
async def signtransaction(self, tx, privkey=None, password=None, wallet: Abstract_Wallet = None): |
|
"""Sign a transaction. The wallet keys will be used unless a private key is provided.""" |
|
tx = PartialTransaction(tx) |
|
if privkey: |
|
txin_type, privkey2, compressed = bitcoin.deserialize_privkey(privkey) |
|
pubkey = ecc.ECPrivkey(privkey2).get_public_key_bytes(compressed=compressed).hex() |
|
tx.sign({pubkey:(privkey2, compressed)}) |
|
else: |
|
wallet.sign_transaction(tx, password) |
|
return tx.serialize() |
|
|
|
@command('') |
|
async def deserialize(self, tx): |
|
"""Deserialize a serialized transaction""" |
|
tx = tx_from_any(tx) |
|
return tx.to_json() |
|
|
|
@command('n') |
|
async def broadcast(self, tx): |
|
"""Broadcast a transaction to the network. """ |
|
tx = Transaction(tx) |
|
await self.network.broadcast_transaction(tx) |
|
return tx.txid() |
|
|
|
@command('') |
|
async def createmultisig(self, num, pubkeys): |
|
"""Create multisig address""" |
|
assert isinstance(pubkeys, list), (type(num), type(pubkeys)) |
|
redeem_script = multisig_script(pubkeys, num) |
|
address = bitcoin.hash160_to_p2sh(hash_160(bfh(redeem_script))) |
|
return {'address':address, 'redeemScript':redeem_script} |
|
|
|
@command('w') |
|
async def freeze(self, address, wallet: Abstract_Wallet = None): |
|
"""Freeze address. Freeze the funds at one of your wallet\'s addresses""" |
|
return wallet.set_frozen_state_of_addresses([address], True) |
|
|
|
@command('w') |
|
async def unfreeze(self, address, wallet: Abstract_Wallet = None): |
|
"""Unfreeze address. Unfreeze the funds at one of your wallet\'s address""" |
|
return wallet.set_frozen_state_of_addresses([address], False) |
|
|
|
@command('wp') |
|
async def getprivatekeys(self, address, password=None, wallet: Abstract_Wallet = None): |
|
"""Get private keys of addresses. You may pass a single wallet address, or a list of wallet addresses.""" |
|
if isinstance(address, str): |
|
address = address.strip() |
|
if is_address(address): |
|
return wallet.export_private_key(address, password) |
|
domain = address |
|
return [wallet.export_private_key(address, password) for address in domain] |
|
|
|
@command('wp') |
|
async def getprivatekeyforpath(self, path, password=None, wallet: Abstract_Wallet = None): |
|
"""Get private key corresponding to derivation path (address index). |
|
'path' can be either a str such as "m/0/50", or a list of ints such as [0, 50]. |
|
""" |
|
return wallet.export_private_key_for_path(path, password) |
|
|
|
@command('w') |
|
async def ismine(self, address, wallet: Abstract_Wallet = None): |
|
"""Check if address is in wallet. Return true if and only address is in wallet""" |
|
return wallet.is_mine(address) |
|
|
|
@command('') |
|
async def dumpprivkeys(self): |
|
"""Deprecated.""" |
|
return "This command is deprecated. Use a pipe instead: 'electrum listaddresses | electrum getprivatekeys - '" |
|
|
|
@command('') |
|
async def validateaddress(self, address): |
|
"""Check that an address is valid. """ |
|
return is_address(address) |
|
|
|
@command('w') |
|
async def getpubkeys(self, address, wallet: Abstract_Wallet = None): |
|
"""Return the public keys for a wallet address. """ |
|
return wallet.get_public_keys(address) |
|
|
|
@command('w') |
|
async def getbalance(self, wallet: Abstract_Wallet = None): |
|
"""Return the balance of your wallet. """ |
|
c, u, x = wallet.get_balance() |
|
l = wallet.lnworker.get_balance() if wallet.lnworker else None |
|
out = {"confirmed": str(Decimal(c)/COIN)} |
|
if u: |
|
out["unconfirmed"] = str(Decimal(u)/COIN) |
|
if x: |
|
out["unmatured"] = str(Decimal(x)/COIN) |
|
if l: |
|
out["lightning"] = str(Decimal(l)/COIN) |
|
return out |
|
|
|
@command('n') |
|
async def getaddressbalance(self, address): |
|
"""Return the balance of any address. Note: This is a walletless |
|
server query, results are not checked by SPV. |
|
""" |
|
sh = bitcoin.address_to_scripthash(address) |
|
out = await self.network.get_balance_for_scripthash(sh) |
|
out["confirmed"] = str(Decimal(out["confirmed"])/COIN) |
|
out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN) |
|
return out |
|
|
|
@command('n') |
|
async def getmerkle(self, txid, height): |
|
"""Get Merkle branch of a transaction included in a block. Electrum |
|
uses this to verify transactions (Simple Payment Verification).""" |
|
return await self.network.get_merkle_for_transaction(txid, int(height)) |
|
|
|
@command('n') |
|
async def getservers(self): |
|
"""Return the list of known servers (candidates for connecting).""" |
|
return self.network.get_servers() |
|
|
|
@command('') |
|
async def version(self): |
|
"""Return the version of Electrum.""" |
|
from .version import ELECTRUM_VERSION |
|
return ELECTRUM_VERSION |
|
|
|
@command('w') |
|
async def getmpk(self, wallet: Abstract_Wallet = None): |
|
"""Get master public key. Return your wallet\'s master public key""" |
|
return wallet.get_master_public_key() |
|
|
|
@command('wp') |
|
async def getmasterprivate(self, password=None, wallet: Abstract_Wallet = None): |
|
"""Get master private key. Return your wallet\'s master private key""" |
|
return str(wallet.keystore.get_master_private_key(password)) |
|
|
|
@command('') |
|
async def convert_xkey(self, xkey, xtype): |
|
"""Convert xtype of a master key. e.g. xpub -> ypub""" |
|
try: |
|
node = BIP32Node.from_xkey(xkey) |
|
except: |
|
raise Exception('xkey should be a master public/private key') |
|
return node._replace(xtype=xtype).to_xkey() |
|
|
|
@command('wp') |
|
async def getseed(self, password=None, wallet: Abstract_Wallet = None): |
|
"""Get seed phrase. Print the generation seed of your wallet.""" |
|
s = wallet.get_seed(password) |
|
return s |
|
|
|
@command('wp') |
|
async def importprivkey(self, privkey, password=None, wallet: Abstract_Wallet = None): |
|
"""Import a private key.""" |
|
if not wallet.can_import_privkey(): |
|
return "Error: This type of wallet cannot import private keys. Try to create a new wallet with that key." |
|
try: |
|
addr = wallet.import_private_key(privkey, password) |
|
out = "Keypair imported: " + addr |
|
except Exception as e: |
|
out = "Error: " + repr(e) |
|
return out |
|
|
|
def _resolver(self, x, wallet): |
|
if x is None: |
|
return None |
|
out = wallet.contacts.resolve(x) |
|
if out.get('type') == 'openalias' and self.nocheck is False and out.get('validated') is False: |
|
raise Exception('cannot verify alias', x) |
|
return out['address'] |
|
|
|
@command('n') |
|
async def sweep(self, privkey, destination, fee=None, nocheck=False, imax=100): |
|
"""Sweep private keys. Returns a transaction that spends UTXOs from |
|
privkey to a destination address. The transaction is not |
|
broadcasted.""" |
|
from .wallet import sweep |
|
tx_fee = satoshis(fee) |
|
privkeys = privkey.split() |
|
self.nocheck = nocheck |
|
#dest = self._resolver(destination) |
|
tx = sweep(privkeys, |
|
network=self.network, |
|
config=self.config, |
|
to_address=destination, |
|
fee=tx_fee, |
|
imax=imax) |
|
return tx.serialize() if tx else None |
|
|
|
@command('wp') |
|
async def signmessage(self, address, message, password=None, wallet: Abstract_Wallet = None): |
|
"""Sign a message with a key. Use quotes if your message contains |
|
whitespaces""" |
|
sig = wallet.sign_message(address, message, password) |
|
return base64.b64encode(sig).decode('ascii') |
|
|
|
@command('') |
|
async def verifymessage(self, address, signature, message): |
|
"""Verify a signature.""" |
|
sig = base64.b64decode(signature) |
|
message = util.to_bytes(message) |
|
return ecc.verify_message_with_address(address, sig, message) |
|
|
|
def _mktx(self, wallet: Abstract_Wallet, outputs, *, fee=None, feerate=None, change_addr=None, domain_addr=None, domain_coins=None, |
|
nocheck=False, unsigned=False, rbf=None, password=None, locktime=None): |
|
if fee is not None and feerate is not None: |
|
raise Exception("Cannot specify both 'fee' and 'feerate' at the same time!") |
|
self.nocheck = nocheck |
|
change_addr = self._resolver(change_addr, wallet) |
|
domain_addr = None if domain_addr is None else map(self._resolver, domain_addr, repeat(wallet)) |
|
final_outputs = [] |
|
for address, amount in outputs: |
|
address = self._resolver(address, wallet) |
|
amount = satoshis(amount) |
|
final_outputs.append(PartialTxOutput.from_address_and_value(address, amount)) |
|
|
|
coins = wallet.get_spendable_coins(domain_addr) |
|
if domain_coins is not None: |
|
coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)] |
|
if feerate is not None: |
|
fee_per_kb = 1000 * Decimal(feerate) |
|
fee_estimator = partial(SimpleConfig.estimate_fee_for_feerate, fee_per_kb) |
|
else: |
|
fee_estimator = fee |
|
tx = wallet.make_unsigned_transaction(coins=coins, |
|
outputs=final_outputs, |
|
fee=fee_estimator, |
|
change_addr=change_addr) |
|
if locktime is not None: |
|
tx.locktime = locktime |
|
if rbf is None: |
|
rbf = self.config.get('use_rbf', True) |
|
if rbf: |
|
tx.set_rbf(True) |
|
if not unsigned: |
|
wallet.sign_transaction(tx, password) |
|
return tx |
|
|
|
@command('wp') |
|
async def payto(self, destination, amount, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None, |
|
nocheck=False, unsigned=False, rbf=None, password=None, locktime=None, wallet: Abstract_Wallet = None): |
|
"""Create a transaction. """ |
|
tx_fee = satoshis(fee) |
|
domain_addr = from_addr.split(',') if from_addr else None |
|
domain_coins = from_coins.split(',') if from_coins else None |
|
tx = self._mktx(wallet, |
|
[(destination, amount)], |
|
fee=tx_fee, |
|
feerate=feerate, |
|
change_addr=change_addr, |
|
domain_addr=domain_addr, |
|
domain_coins=domain_coins, |
|
nocheck=nocheck, |
|
unsigned=unsigned, |
|
rbf=rbf, |
|
password=password, |
|
locktime=locktime) |
|
return tx.serialize() |
|
|
|
@command('wp') |
|
async def paytomany(self, outputs, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None, |
|
nocheck=False, unsigned=False, rbf=None, password=None, locktime=None, wallet: Abstract_Wallet = None): |
|
"""Create a multi-output transaction. """ |
|
tx_fee = satoshis(fee) |
|
domain_addr = from_addr.split(',') if from_addr else None |
|
domain_coins = from_coins.split(',') if from_coins else None |
|
tx = self._mktx(wallet, |
|
outputs, |
|
fee=tx_fee, |
|
feerate=feerate, |
|
change_addr=change_addr, |
|
domain_addr=domain_addr, |
|
domain_coins=domain_coins, |
|
nocheck=nocheck, |
|
unsigned=unsigned, |
|
rbf=rbf, |
|
password=password, |
|
locktime=locktime) |
|
return tx.serialize() |
|
|
|
@command('w') |
|
async def onchain_history(self, year=None, show_addresses=False, show_fiat=False, wallet: Abstract_Wallet = None): |
|
"""Wallet onchain history. Returns the transaction history of your wallet.""" |
|
kwargs = { |
|
'show_addresses': show_addresses, |
|
} |
|
if year: |
|
import time |
|
start_date = datetime.datetime(year, 1, 1) |
|
end_date = datetime.datetime(year+1, 1, 1) |
|
kwargs['from_timestamp'] = time.mktime(start_date.timetuple()) |
|
kwargs['to_timestamp'] = time.mktime(end_date.timetuple()) |
|
if show_fiat: |
|
from .exchange_rate import FxThread |
|
fx = FxThread(self.config, None) |
|
kwargs['fx'] = fx |
|
return json_normalize(wallet.get_detailed_history(**kwargs)) |
|
|
|
@command('w') |
|
async def init_lightning(self, wallet: Abstract_Wallet = None): |
|
"""Enable lightning payments""" |
|
wallet.init_lightning() |
|
return "Lightning keys have been created." |
|
|
|
@command('w') |
|
async def remove_lightning(self, wallet: Abstract_Wallet = None): |
|
"""Disable lightning payments""" |
|
wallet.remove_lightning() |
|
|
|
@command('w') |
|
async def lightning_history(self, show_fiat=False, wallet: Abstract_Wallet = None): |
|
""" lightning history """ |
|
lightning_history = wallet.lnworker.get_history() if wallet.lnworker else [] |
|
return json_normalize(lightning_history) |
|
|
|
@command('w') |
|
async def setlabel(self, key, label, wallet: Abstract_Wallet = None): |
|
"""Assign a label to an item. Item may be a bitcoin address or a |
|
transaction ID""" |
|
wallet.set_label(key, label) |
|
|
|
@command('w') |
|
async def listcontacts(self, wallet: Abstract_Wallet = None): |
|
"""Show your list of contacts""" |
|
return wallet.contacts |
|
|
|
@command('w') |
|
async def getalias(self, key, wallet: Abstract_Wallet = None): |
|
"""Retrieve alias. Lookup in your list of contacts, and for an OpenAlias DNS record.""" |
|
return wallet.contacts.resolve(key) |
|
|
|
@command('w') |
|
async def searchcontacts(self, query, wallet: Abstract_Wallet = None): |
|
"""Search through contacts, return matching entries. """ |
|
results = {} |
|
for key, value in wallet.contacts.items(): |
|
if query.lower() in key.lower(): |
|
results[key] = value |
|
return results |
|
|
|
@command('w') |
|
async def listaddresses(self, receiving=False, change=False, labels=False, frozen=False, unused=False, funded=False, balance=False, wallet: Abstract_Wallet = None): |
|
"""List wallet addresses. Returns the list of all addresses in your wallet. Use optional arguments to filter the results.""" |
|
out = [] |
|
for addr in wallet.get_addresses(): |
|
if frozen and not wallet.is_frozen_address(addr): |
|
continue |
|
if receiving and wallet.is_change(addr): |
|
continue |
|
if change and not wallet.is_change(addr): |
|
continue |
|
if unused and wallet.is_used(addr): |
|
continue |
|
if funded and wallet.is_empty(addr): |
|
continue |
|
item = addr |
|
if labels or balance: |
|
item = (item,) |
|
if balance: |
|
item += (format_satoshis(sum(wallet.get_addr_balance(addr))),) |
|
if labels: |
|
item += (repr(wallet.labels.get(addr, '')),) |
|
out.append(item) |
|
return out |
|
|
|
@command('n') |
|
async def gettransaction(self, txid, wallet: Abstract_Wallet = None): |
|
"""Retrieve a transaction. """ |
|
tx = None |
|
if wallet: |
|
tx = wallet.db.get_transaction(txid) |
|
if tx is None: |
|
raw = await self.network.get_transaction(txid) |
|
if raw: |
|
tx = Transaction(raw) |
|
else: |
|
raise Exception("Unknown transaction") |
|
if tx.txid() != txid: |
|
raise Exception("Mismatching txid") |
|
return tx.serialize() |
|
|
|
@command('') |
|
async def encrypt(self, pubkey, message) -> str: |
|
"""Encrypt a message with a public key. Use quotes if the message contains whitespaces.""" |
|
if not is_hex_str(pubkey): |
|
raise Exception(f"pubkey must be a hex string instead of {repr(pubkey)}") |
|
try: |
|
message = to_bytes(message) |
|
except TypeError: |
|
raise Exception(f"message must be a string-like object instead of {repr(message)}") |
|
public_key = ecc.ECPubkey(bfh(pubkey)) |
|
encrypted = public_key.encrypt_message(message) |
|
return encrypted.decode('utf-8') |
|
|
|
@command('wp') |
|
async def decrypt(self, pubkey, encrypted, password=None, wallet: Abstract_Wallet = None) -> str: |
|
"""Decrypt a message encrypted with a public key.""" |
|
if not is_hex_str(pubkey): |
|
raise Exception(f"pubkey must be a hex string instead of {repr(pubkey)}") |
|
if not isinstance(encrypted, (str, bytes, bytearray)): |
|
raise Exception(f"encrypted must be a string-like object instead of {repr(encrypted)}") |
|
decrypted = wallet.decrypt_message(pubkey, encrypted, password) |
|
return decrypted.decode('utf-8') |
|
|
|
def _format_request(self, out): |
|
from .util import get_request_status |
|
out['amount_BTC'] = format_satoshis(out.get('amount')) |
|
out['status'], out['status_str'] = get_request_status(out) |
|
return out |
|
|
|
@command('w') |
|
async def getrequest(self, key, wallet: Abstract_Wallet = None): |
|
"""Return a payment request""" |
|
r = wallet.get_request(key) |
|
if not r: |
|
raise Exception("Request not found") |
|
return self._format_request(r) |
|
|
|
#@command('w') |
|
#async def ackrequest(self, serialized): |
|
# """<Not implemented>""" |
|
# pass |
|
|
|
@command('w') |
|
async def list_requests(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None): |
|
"""List the payment requests you made.""" |
|
out = wallet.get_sorted_requests() |
|
if pending: |
|
f = PR_UNPAID |
|
elif expired: |
|
f = PR_EXPIRED |
|
elif paid: |
|
f = PR_PAID |
|
else: |
|
f = None |
|
if f is not None: |
|
out = list(filter(lambda x: x.get('status')==f, out)) |
|
return list(map(self._format_request, out)) |
|
|
|
@command('w') |
|
async def createnewaddress(self, wallet: Abstract_Wallet = None): |
|
"""Create a new receiving address, beyond the gap limit of the wallet""" |
|
return wallet.create_new_address(False) |
|
|
|
@command('w') |
|
async def changegaplimit(self, new_limit, iknowwhatimdoing=False, wallet: Abstract_Wallet = None): |
|
"""Change the gap limit of the wallet.""" |
|
if not iknowwhatimdoing: |
|
raise Exception("WARNING: Are you SURE you want to change the gap limit?\n" |
|
"It makes recovering your wallet from seed difficult!\n" |
|
"Please do your research and make sure you understand the implications.\n" |
|
"Typically only merchants and power users might want to do this.\n" |
|
"To proceed, try again, with the --iknowwhatimdoing option.") |
|
if not isinstance(wallet, Deterministic_Wallet): |
|
raise Exception("This wallet is not deterministic.") |
|
return wallet.change_gap_limit(new_limit) |
|
|
|
@command('wn') |
|
async def getminacceptablegap(self, wallet: Abstract_Wallet = None): |
|
"""Returns the minimum value for gap limit that would be sufficient to discover all |
|
known addresses in the wallet. |
|
""" |
|
if not isinstance(wallet, Deterministic_Wallet): |
|
raise Exception("This wallet is not deterministic.") |
|
if not wallet.is_up_to_date(): |
|
raise Exception("Wallet not fully synchronized.") |
|
return wallet.min_acceptable_gap() |
|
|
|
@command('w') |
|
async def getunusedaddress(self, wallet: Abstract_Wallet = None): |
|
"""Returns the first unused address of the wallet, or None if all addresses are used. |
|
An address is considered as used if it has received a transaction, or if it is used in a payment request.""" |
|
return wallet.get_unused_address() |
|
|
|
@command('w') |
|
async def add_request(self, amount, memo='', expiration=3600, force=False, wallet: Abstract_Wallet = None): |
|
"""Create a payment request, using the first unused address of the wallet. |
|
The address will be considered as used after this operation. |
|
If no payment is received, the address will be considered as unused if the payment request is deleted from the wallet.""" |
|
addr = wallet.get_unused_address() |
|
if addr is None: |
|
if force: |
|
addr = wallet.create_new_address(False) |
|
else: |
|
return False |
|
amount = satoshis(amount) |
|
expiration = int(expiration) if expiration else None |
|
req = wallet.make_payment_request(addr, amount, memo, expiration) |
|
wallet.add_payment_request(req) |
|
out = wallet.get_request(addr) |
|
return self._format_request(out) |
|
|
|
@command('wn') |
|
async def add_lightning_request(self, amount, memo='', expiration=3600, wallet: Abstract_Wallet = None): |
|
amount_sat = int(satoshis(amount)) |
|
key = await wallet.lnworker._add_request_coro(amount_sat, memo, expiration) |
|
return wallet.get_request(key)['invoice'] |
|
|
|
@command('w') |
|
async def addtransaction(self, tx, wallet: Abstract_Wallet = None): |
|
""" Add a transaction to the wallet history """ |
|
tx = Transaction(tx) |
|
if not wallet.add_transaction(tx): |
|
return False |
|
wallet.save_db() |
|
return tx.txid() |
|
|
|
@command('wp') |
|
async def signrequest(self, address, password=None, wallet: Abstract_Wallet = None): |
|
"Sign payment request with an OpenAlias" |
|
alias = self.config.get('alias') |
|
if not alias: |
|
raise Exception('No alias in your configuration') |
|
alias_addr = wallet.contacts.resolve(alias)['address'] |
|
wallet.sign_payment_request(address, alias, alias_addr, password) |
|
|
|
@command('w') |
|
async def rmrequest(self, address, wallet: Abstract_Wallet = None): |
|
"""Remove a payment request""" |
|
return wallet.remove_payment_request(address) |
|
|
|
@command('w') |
|
async def clear_requests(self, wallet: Abstract_Wallet = None): |
|
"""Remove all payment requests""" |
|
for k in list(wallet.receive_requests.keys()): |
|
wallet.remove_payment_request(k) |
|
|
|
@command('w') |
|
async def clear_invoices(self, wallet: Abstract_Wallet = None): |
|
"""Remove all invoices""" |
|
wallet.clear_invoices() |
|
return True |
|
|
|
@command('n') |
|
async def notify(self, address: str, URL: str): |
|
"""Watch an address. Every time the address changes, a http POST is sent to the URL.""" |
|
if not hasattr(self, "_notifier"): |
|
self._notifier = Notifier(self.network) |
|
await self._notifier.start_watching_queue.put((address, URL)) |
|
return True |
|
|
|
@command('wn') |
|
async def is_synchronized(self, wallet: Abstract_Wallet = None): |
|
""" return wallet synchronization status """ |
|
return wallet.is_up_to_date() |
|
|
|
@command('n') |
|
async def getfeerate(self, fee_method=None, fee_level=None): |
|
"""Return current suggested fee rate (in sat/kvByte), according to config |
|
settings or supplied parameters. |
|
""" |
|
if fee_method is None: |
|
dyn, mempool = None, None |
|
elif fee_method.lower() == 'static': |
|
dyn, mempool = False, False |
|
elif fee_method.lower() == 'eta': |
|
dyn, mempool = True, False |
|
elif fee_method.lower() == 'mempool': |
|
dyn, mempool = True, True |
|
else: |
|
raise Exception('Invalid fee estimation method: {}'.format(fee_method)) |
|
if fee_level is not None: |
|
fee_level = Decimal(fee_level) |
|
return self.config.fee_per_kb(dyn=dyn, mempool=mempool, fee_level=fee_level) |
|
|
|
@command('w') |
|
async def removelocaltx(self, txid, wallet: Abstract_Wallet = None): |
|
"""Remove a 'local' transaction from the wallet, and its dependent |
|
transactions. |
|
""" |
|
if not is_hash256_str(txid): |
|
raise Exception(f"{repr(txid)} is not a txid") |
|
height = wallet.get_tx_height(txid).height |
|
to_delete = {txid} |
|
if height != TX_HEIGHT_LOCAL: |
|
raise Exception(f'Only local transactions can be removed. ' |
|
f'This tx has height: {height} != {TX_HEIGHT_LOCAL}') |
|
to_delete |= wallet.get_depending_transactions(txid) |
|
for tx_hash in to_delete: |
|
wallet.remove_transaction(tx_hash) |
|
wallet.save_db() |
|
|
|
@command('wn') |
|
async def get_tx_status(self, txid, wallet: Abstract_Wallet = None): |
|
"""Returns some information regarding the tx. For now, only confirmations. |
|
The transaction must be related to the wallet. |
|
""" |
|
if not is_hash256_str(txid): |
|
raise Exception(f"{repr(txid)} is not a txid") |
|
if not wallet.db.get_transaction(txid): |
|
raise Exception("Transaction not in wallet.") |
|
return { |
|
"confirmations": wallet.get_tx_height(txid).conf, |
|
} |
|
|
|
@command('') |
|
async def help(self): |
|
# for the python console |
|
return sorted(known_commands.keys()) |
|
|
|
# lightning network commands |
|
@command('wn') |
|
async def add_peer(self, connection_string, timeout=20, wallet: Abstract_Wallet = None): |
|
await wallet.lnworker.add_peer(connection_string) |
|
return True |
|
|
|
@command('wpn') |
|
async def open_channel(self, connection_string, amount, push_amount=0, password=None, wallet: Abstract_Wallet = None): |
|
funding_sat = satoshis(amount) |
|
push_sat = satoshis(push_amount) |
|
dummy_output = PartialTxOutput.from_address_and_value(ln_dummy_address(), funding_sat) |
|
funding_tx = wallet.mktx(outputs = [dummy_output], rbf=False, sign=False, nonlocal_only=True) |
|
chan, funding_tx = await wallet.lnworker._open_channel_coroutine(connect_str=connection_string, |
|
funding_tx=funding_tx, |
|
funding_sat=funding_sat, |
|
push_sat=push_sat, |
|
password=password) |
|
return chan.funding_outpoint.to_str() |
|
|
|
@command('wn') |
|
async def lnpay(self, invoice, attempts=1, timeout=10, wallet: Abstract_Wallet = None): |
|
lnworker = wallet.lnworker |
|
lnaddr = lnworker._check_invoice(invoice, None) |
|
payment_hash = lnaddr.paymenthash |
|
success = await lnworker._pay(invoice, attempts=attempts) |
|
return { |
|
'payment_hash': payment_hash.hex(), |
|
'success': success, |
|
'preimage': lnworker.get_preimage(payment_hash).hex() if success else None, |
|
} |
|
|
|
@command('w') |
|
async def nodeid(self, wallet: Abstract_Wallet = None): |
|
listen_addr = self.config.get('lightning_listen') |
|
return bh2u(wallet.lnworker.node_keypair.pubkey) + (('@' + listen_addr) if listen_addr else '') |
|
|
|
@command('w') |
|
async def list_channels(self, wallet: Abstract_Wallet = None): |
|
# we output the funding_outpoint instead of the channel_id because lnd uses channel_point (funding outpoint) to identify channels |
|
from .lnutil import LOCAL, REMOTE, format_short_channel_id |
|
encoder = util.MyEncoder() |
|
l = list(wallet.lnworker.channels.items()) |
|
return [ |
|
{ |
|
'short_channel_id': format_short_channel_id(chan.short_channel_id) if chan.short_channel_id else None, |
|
'channel_id': bh2u(chan.channel_id), |
|
'channel_point': chan.funding_outpoint.to_str(), |
|
'state': chan.get_state().name, |
|
'peer_state': chan.peer_state.name, |
|
'remote_pubkey': bh2u(chan.node_id), |
|
'local_balance': chan.balance(LOCAL)//1000, |
|
'remote_balance': chan.balance(REMOTE)//1000, |
|
'local_reserve': chan.config[LOCAL].reserve_sat, |
|
'remote_reserve': chan.config[REMOTE].reserve_sat, |
|
'local_unsettled_sent': chan.balance_tied_up_in_htlcs_by_direction(LOCAL, direction=SENT) // 1000, |
|
'remote_unsettled_sent': chan.balance_tied_up_in_htlcs_by_direction(REMOTE, direction=SENT) // 1000, |
|
} for channel_id, chan in l |
|
] |
|
|
|
@command('wn') |
|
async def dumpgraph(self, wallet: Abstract_Wallet = None): |
|
return list(map(bh2u, wallet.lnworker.channel_db.nodes.keys())) |
|
|
|
@command('n') |
|
async def inject_fees(self, fees): |
|
import ast |
|
self.network.config.fee_estimates = ast.literal_eval(fees) |
|
self.network.notify('fee') |
|
|
|
@command('wn') |
|
async def enable_htlc_settle(self, b: bool, wallet: Abstract_Wallet = None): |
|
e = wallet.lnworker.enable_htlc_settle |
|
e.set() if b else e.clear() |
|
|
|
@command('n') |
|
async def clear_ln_blacklist(self): |
|
self.network.path_finder.blacklist.clear() |
|
|
|
@command('w') |
|
async def list_invoices(self, wallet: Abstract_Wallet = None): |
|
return wallet.get_invoices() |
|
|
|
@command('wn') |
|
async def close_channel(self, channel_point, force=False, wallet: Abstract_Wallet = None): |
|
txid, index = channel_point.split(':') |
|
chan_id, _ = channel_id_from_funding_tx(txid, int(index)) |
|
coro = wallet.lnworker.force_close_channel(chan_id) if force else wallet.lnworker.close_channel(chan_id) |
|
return await coro |
|
|
|
@command('wn') |
|
async def get_channel_ctx(self, channel_point, iknowwhatimdoing=False, wallet: Abstract_Wallet = None): |
|
""" return the current commitment transaction of a channel """ |
|
if not iknowwhatimdoing: |
|
raise Exception("WARNING: this command is potentially unsafe.\n" |
|
"To proceed, try again, with the --iknowwhatimdoing option.") |
|
txid, index = channel_point.split(':') |
|
chan_id, _ = channel_id_from_funding_tx(txid, int(index)) |
|
chan = wallet.lnworker.channels[chan_id] |
|
tx = chan.force_close_tx() |
|
return tx.serialize() |
|
|
|
@command('wn') |
|
async def get_watchtower_ctn(self, channel_point, wallet: Abstract_Wallet = None): |
|
""" return the local watchtower's ctn of channel. used in regtests """ |
|
return await self.network.local_watchtower.sweepstore.get_ctn(channel_point, None) |
|
|
|
|
|
def eval_bool(x: str) -> bool: |
|
if x == 'false': return False |
|
if x == 'true': return True |
|
try: |
|
return bool(ast.literal_eval(x)) |
|
except: |
|
return bool(x) |
|
|
|
param_descriptions = { |
|
'privkey': 'Private key. Type \'?\' to get a prompt.', |
|
'destination': 'Bitcoin address, contact or alias', |
|
'address': 'Bitcoin address', |
|
'seed': 'Seed phrase', |
|
'txid': 'Transaction ID', |
|
'pos': 'Position', |
|
'height': 'Block height', |
|
'tx': 'Serialized transaction (hexadecimal)', |
|
'key': 'Variable name', |
|
'pubkey': 'Public key', |
|
'message': 'Clear text message. Use quotes if it contains spaces.', |
|
'encrypted': 'Encrypted message', |
|
'amount': 'Amount to be sent (in BTC). Type \'!\' to send the maximum available.', |
|
'requested_amount': 'Requested amount (in BTC).', |
|
'outputs': 'list of ["address", amount]', |
|
'redeem_script': 'redeem script (hexadecimal)', |
|
} |
|
|
|
command_options = { |
|
'password': ("-W", "Password"), |
|
'new_password':(None, "New Password"), |
|
'encrypt_file':(None, "Whether the file on disk should be encrypted with the provided password"), |
|
'receiving': (None, "Show only receiving addresses"), |
|
'change': (None, "Show only change addresses"), |
|
'frozen': (None, "Show only frozen addresses"), |
|
'unused': (None, "Show only unused addresses"), |
|
'funded': (None, "Show only funded addresses"), |
|
'balance': ("-b", "Show the balances of listed addresses"), |
|
'labels': ("-l", "Show the labels of listed addresses"), |
|
'nocheck': (None, "Do not verify aliases"), |
|
'imax': (None, "Maximum number of inputs"), |
|
'fee': ("-f", "Transaction fee (absolute, in BTC)"), |
|
'feerate': (None, "Transaction fee rate (in sat/byte)"), |
|
'from_addr': ("-F", "Source address (must be a wallet address; use sweep to spend from non-wallet address)."), |
|
'from_coins': (None, "Source coins (must be in wallet; use sweep to spend from non-wallet address)."), |
|
'change_addr': ("-c", "Change address. Default is a spare address, or the source address if it's not in the wallet"), |
|
'nbits': (None, "Number of bits of entropy"), |
|
'seed_type': (None, "The type of seed to create, e.g. 'standard' or 'segwit'"), |
|
'language': ("-L", "Default language for wordlist"), |
|
'passphrase': (None, "Seed extension"), |
|
'privkey': (None, "Private key. Set to '?' to get a prompt."), |
|
'unsigned': ("-u", "Do not sign transaction"), |
|
'rbf': (None, "Whether to signal opt-in Replace-By-Fee in the transaction (true/false)"), |
|
'locktime': (None, "Set locktime block number"), |
|
'domain': ("-D", "List of addresses"), |
|
'memo': ("-m", "Description of the request"), |
|
'expiration': (None, "Time in seconds"), |
|
'attempts': (None, "Number of payment attempts"), |
|
'timeout': (None, "Timeout in seconds"), |
|
'force': (None, "Create new address beyond gap limit, if no more addresses are available."), |
|
'pending': (None, "Show only pending requests."), |
|
'push_amount': (None, 'Push initial amount (in BTC)'), |
|
'expired': (None, "Show only expired requests."), |
|
'paid': (None, "Show only paid requests."), |
|
'show_addresses': (None, "Show input and output addresses"), |
|
'show_fiat': (None, "Show fiat value of transactions"), |
|
'show_fees': (None, "Show miner fees paid by transactions"), |
|
'year': (None, "Show history for a given year"), |
|
'fee_method': (None, "Fee estimation method to use"), |
|
'fee_level': (None, "Float between 0.0 and 1.0, representing fee slider position"), |
|
'from_height': (None, "Only show transactions that confirmed after given block height"), |
|
'to_height': (None, "Only show transactions that confirmed before given block height"), |
|
'iknowwhatimdoing': (None, "Acknowledge that I understand the full implications of what I am about to do"), |
|
} |
|
|
|
|
|
# don't use floats because of rounding errors |
|
from .transaction import convert_raw_tx_to_hex |
|
json_loads = lambda x: json.loads(x, parse_float=lambda x: str(Decimal(x))) |
|
arg_types = { |
|
'num': int, |
|
'nbits': int, |
|
'imax': int, |
|
'year': int, |
|
'from_height': int, |
|
'to_height': int, |
|
'tx': convert_raw_tx_to_hex, |
|
'pubkeys': json_loads, |
|
'jsontx': json_loads, |
|
'inputs': json_loads, |
|
'outputs': json_loads, |
|
'fee': lambda x: str(Decimal(x)) if x is not None else None, |
|
'amount': lambda x: str(Decimal(x)) if x != '!' else '!', |
|
'locktime': int, |
|
'fee_method': str, |
|
'fee_level': json_loads, |
|
'encrypt_file': eval_bool, |
|
'rbf': eval_bool, |
|
'timeout': float, |
|
} |
|
|
|
config_variables = { |
|
|
|
'addrequest': { |
|
'ssl_privkey': 'Path to your SSL private key, needed to sign the request.', |
|
'ssl_chain': 'Chain of SSL certificates, needed for signed requests. Put your certificate at the top and the root CA at the end', |
|
'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"', |
|
}, |
|
'listrequests':{ |
|
'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"', |
|
} |
|
} |
|
|
|
def set_default_subparser(self, name, args=None): |
|
"""see http://stackoverflow.com/questions/5176691/argparse-how-to-specify-a-default-subcommand""" |
|
subparser_found = False |
|
for arg in sys.argv[1:]: |
|
if arg in ['-h', '--help']: # global help if no subparser |
|
break |
|
else: |
|
for x in self._subparsers._actions: |
|
if not isinstance(x, argparse._SubParsersAction): |
|
continue |
|
for sp_name in x._name_parser_map.keys(): |
|
if sp_name in sys.argv[1:]: |
|
subparser_found = True |
|
if not subparser_found: |
|
# insert default in first position, this implies no |
|
# global options without a sub_parsers specified |
|
if args is None: |
|
sys.argv.insert(1, name) |
|
else: |
|
args.insert(0, name) |
|
|
|
argparse.ArgumentParser.set_default_subparser = set_default_subparser |
|
|
|
|
|
# workaround https://bugs.python.org/issue23058 |
|
# see https://github.com/nickstenning/honcho/pull/121 |
|
|
|
def subparser_call(self, parser, namespace, values, option_string=None): |
|
from argparse import ArgumentError, SUPPRESS, _UNRECOGNIZED_ARGS_ATTR |
|
parser_name = values[0] |
|
arg_strings = values[1:] |
|
# set the parser name if requested |
|
if self.dest is not SUPPRESS: |
|
setattr(namespace, self.dest, parser_name) |
|
# select the parser |
|
try: |
|
parser = self._name_parser_map[parser_name] |
|
except KeyError: |
|
tup = parser_name, ', '.join(self._name_parser_map) |
|
msg = _('unknown parser {!r} (choices: {})').format(*tup) |
|
raise ArgumentError(self, msg) |
|
# parse all the remaining options into the namespace |
|
# store any unrecognized options on the object, so that the top |
|
# level parser can decide what to do with them |
|
namespace, arg_strings = parser.parse_known_args(arg_strings, namespace) |
|
if arg_strings: |
|
vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, []) |
|
getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings) |
|
|
|
argparse._SubParsersAction.__call__ = subparser_call |
|
|
|
|
|
def add_network_options(parser): |
|
parser.add_argument("-1", "--oneserver", action="store_true", dest="oneserver", default=None, help="connect to one server only") |
|
parser.add_argument("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)") |
|
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") |
|
parser.add_argument("--noonion", action="store_true", dest="noonion", default=None, help="do not try to connect to onion servers") |
|
parser.add_argument("--skipmerklecheck", action="store_true", dest="skipmerklecheck", default=False, help="Tolerate invalid merkle proofs from server") |
|
|
|
def add_global_options(parser): |
|
group = parser.add_argument_group('global options') |
|
group.add_argument("-v", dest="verbosity", help="Set verbosity (log levels)", default='') |
|
group.add_argument("-V", dest="verbosity_shortcuts", help="Set verbosity (shortcut-filter list)", default='') |
|
group.add_argument("-D", "--dir", dest="electrum_path", help="electrum directory") |
|
group.add_argument("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum_data' directory") |
|
group.add_argument("--testnet", action="store_true", dest="testnet", default=False, help="Use Testnet") |
|
group.add_argument("--regtest", action="store_true", dest="regtest", default=False, help="Use Regtest") |
|
group.add_argument("--simnet", action="store_true", dest="simnet", default=False, help="Use Simnet") |
|
group.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline") |
|
|
|
def add_wallet_option(parser): |
|
parser.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path") |
|
parser.add_argument("--forgetconfig", action="store_true", dest="forget_config", default=False, help="Forget config on exit") |
|
|
|
def get_parser(): |
|
# create main parser |
|
parser = argparse.ArgumentParser( |
|
epilog="Run 'electrum help <command>' to see the help for a command") |
|
add_global_options(parser) |
|
subparsers = parser.add_subparsers(dest='cmd', metavar='<command>') |
|
# gui |
|
parser_gui = subparsers.add_parser('gui', description="Run Electrum's Graphical User Interface.", help="Run GUI (default)") |
|
parser_gui.add_argument("url", nargs='?', default=None, help="bitcoin URI (or bip70 file)") |
|
parser_gui.add_argument("-g", "--gui", dest="gui", help="select graphical user interface", choices=['qt', 'kivy', 'text', 'stdio']) |
|
parser_gui.add_argument("-m", action="store_true", dest="hide_gui", default=False, help="hide GUI on startup") |
|
parser_gui.add_argument("-L", "--lang", dest="language", default=None, help="default language used in GUI") |
|
parser_gui.add_argument("--daemon", action="store_true", dest="daemon", default=False, help="keep daemon running after GUI is closed") |
|
add_wallet_option(parser_gui) |
|
add_network_options(parser_gui) |
|
add_global_options(parser_gui) |
|
# daemon |
|
parser_daemon = subparsers.add_parser('daemon', help="Run Daemon") |
|
parser_daemon.add_argument("-d", "--detached", action="store_true", dest="detach", default=False, help="run daemon in detached mode") |
|
add_network_options(parser_daemon) |
|
add_global_options(parser_daemon) |
|
# commands |
|
for cmdname in sorted(known_commands.keys()): |
|
cmd = known_commands[cmdname] |
|
p = subparsers.add_parser(cmdname, help=cmd.help, description=cmd.description) |
|
for optname, default in zip(cmd.options, cmd.defaults): |
|
if optname in ['wallet_path', 'wallet']: |
|
add_wallet_option(p) |
|
continue |
|
a, help = command_options[optname] |
|
b = '--' + optname |
|
action = "store_true" if default is False else 'store' |
|
args = (a, b) if a else (b,) |
|
if action == 'store': |
|
_type = arg_types.get(optname, str) |
|
p.add_argument(*args, dest=optname, action=action, default=default, help=help, type=_type) |
|
else: |
|
p.add_argument(*args, dest=optname, action=action, default=default, help=help) |
|
add_global_options(p) |
|
|
|
for param in cmd.params: |
|
if param in ['wallet_path', 'wallet']: |
|
continue |
|
h = param_descriptions.get(param, '') |
|
_type = arg_types.get(param, str) |
|
p.add_argument(param, help=h, type=_type) |
|
|
|
cvh = config_variables.get(cmdname) |
|
if cvh: |
|
group = p.add_argument_group('configuration variables', '(set with setconfig/getconfig)') |
|
for k, v in cvh.items(): |
|
group.add_argument(k, nargs='?', help=v) |
|
|
|
# 'gui' is the default command |
|
parser.set_default_subparser('gui') |
|
return parser
|
|
|