2013-02-26 13:56:48 +01:00
#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2011 thomasv@gitorious
#
2016-02-23 11:36:42 +01:00
# 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:
2013-02-26 13:56:48 +01:00
#
2016-02-23 11:36:42 +01:00
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
2013-02-26 13:56:48 +01:00
#
2016-02-23 11:36:42 +01:00
# 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.
2024-11-22 14:46:21 +01:00
import io
2015-05-28 15:22:30 +02:00
import sys
2014-06-24 16:12:43 +03:00
import datetime
2025-03-31 13:08:09 +02:00
import time
2015-05-28 15:22:30 +02:00
import argparse
2015-05-30 18:49:58 +02:00
import json
2015-05-31 22:42:34 +02:00
import ast
2025-06-03 17:50:43 +00:00
import binascii
2015-07-14 16:37:04 +02:00
import base64
2019-08-15 13:17:16 +02:00
import asyncio
2019-09-12 19:22:55 +02:00
import inspect
2025-10-28 12:32:02 +01:00
from asyncio import CancelledError
2021-06-08 16:33:55 +02:00
from collections import defaultdict
2025-06-03 11:26:23 +02:00
from functools import wraps
2022-12-05 18:49:21 +02:00
from decimal import Decimal , InvalidOperation
2025-08-21 17:38:27 +00:00
from typing import Optional , TYPE_CHECKING , Dict , List , Any , Union
2022-04-11 17:05:26 +02:00
import os
2025-03-18 11:15:16 +01:00
import re
2015-05-28 15:22:30 +02:00
2024-06-17 13:38:54 +02:00
import electrum_ecc as ecc
from . import util
2024-11-22 14:46:21 +01:00
from . lnmsg import OnionWireSerializer
2025-10-28 12:32:02 +01:00
from . lnworker import LN_P2P_NETWORK_TIMEOUT
2024-11-22 14:46:21 +01:00
from . logging import Logger
from . onion_message import create_blinded_path , send_onion_message_to
2025-08-20 18:25:02 +02:00
from . submarine_swaps import NostrTransport
2025-06-03 11:26:23 +02:00
from . util import (
bfh , json_decode , json_normalize , is_hash256_str , is_hex_str , to_bytes , parse_max_spend , to_decimal ,
UserFacingException , InvalidPassword
)
2018-07-11 17:38:47 +02:00
from . import bitcoin
2019-10-23 17:09:41 +02:00
from . bitcoin import is_address , hash_160 , COIN
2019-02-21 22:17:06 +01:00
from . bip32 import BIP32Node
2017-11-12 16:15:06 -06:00
from . i18n import _
2025-06-03 11:26:23 +02:00
from . transaction import (
Transaction , multisig_script , PartialTransaction , PartialTxOutput , tx_from_any , PartialTxInput , TxOutpoint ,
convert_raw_tx_to_hex
)
2023-09-07 13:18:23 +00:00
from . import transaction
2025-06-03 11:26:23 +02:00
from . invoices import Invoice , PR_PAID , PR_UNPAID , PR_EXPIRED
2018-10-03 17:13:46 +02:00
from . synchronizer import Notifier
2025-06-03 11:26:23 +02:00
from . wallet import (
Abstract_Wallet , create_new_wallet , restore_wallet_from_text , Deterministic_Wallet , BumpFeeStrategy ,
Imported_Wallet
)
2019-02-20 18:01:43 +01:00
from . address_synchronizer import TX_HEIGHT_LOCAL
2018-05-23 15:46:30 +02:00
from . mnemonic import Mnemonic
2025-11-28 16:22:22 +01:00
from . lnutil import ( channel_id_from_funding_tx , LnFeatures , SENT , RECEIVED , MIN_FINAL_CLTV_DELTA_ACCEPTED ,
2025-07-21 14:13:21 +02:00
PaymentFeeBudget , NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE )
2023-09-08 14:55:22 +00:00
from . plugin import run_hook , DeviceMgr , Plugins
2019-08-19 12:46:31 +02:00
from . version import ELECTRUM_VERSION
2019-09-01 17:52:02 +02:00
from . simple_config import SimpleConfig
2025-06-28 04:45:24 +00:00
from . fee_policy import FeePolicy , FEE_ETA_TARGETS , FEERATE_DEFAULT_RELAY
2022-04-11 17:05:26 +02:00
from . import GuiImportError
from . import crypto
2022-10-30 15:25:12 +11:00
from . import constants
2023-02-26 12:14:25 +00:00
from . import descriptor
2018-10-25 23:01:53 +02:00
if TYPE_CHECKING :
from . network import Network
2019-09-04 20:16:47 +02:00
from . daemon import Daemon
2025-03-31 13:08:09 +02:00
from electrum . lnworker import PaymentInfo
2018-10-22 16:41:25 +02:00
2017-09-04 14:43:31 +02:00
2019-09-09 22:15:11 +02:00
known_commands = { } # type: Dict[str, Command]
2015-06-01 06:10:06 +02:00
2017-02-22 11:23:12 +01:00
2024-02-12 19:02:02 +00:00
class NotSynchronizedException ( UserFacingException ) :
2020-04-08 03:09:08 +00:00
pass
2021-04-16 16:03:25 +02:00
def satoshis_or_max ( amount ) :
2021-09-15 20:05:15 +05:30
return satoshis ( amount ) if not parse_max_spend ( amount ) else amount
2021-04-16 16:03:25 +02:00
2025-01-23 12:58:28 +01:00
2017-02-22 11:23:12 +01:00
def satoshis ( amount ) :
# satoshi conversion must not be performed by the parser
2023-03-22 12:22:36 +00:00
return int ( COIN * to_decimal ( amount ) ) if amount is not None else None
2017-02-22 11:23:12 +01:00
2025-01-23 12:58:28 +01:00
2025-08-22 13:30:26 +00:00
def format_satoshis ( x : Union [ float , int , Decimal , None ] ) - > Optional [ str ] :
2025-08-21 17:38:27 +00:00
"""
input: satoshis as a Number
output: str formatted as bitcoin amount
"""
if x is None :
return None
return util . format_satoshis_plain ( x , is_max_allowed = False )
2017-02-22 11:23:12 +01:00
2020-02-04 17:56:52 +01:00
2015-06-01 06:10:06 +02:00
class Command :
2025-03-16 11:12:04 +01:00
def __init__ ( self , func , name , s ) :
self . name = name
2025-09-05 18:23:49 +00:00
self . requires_network = ' n ' in s # better name would be "requires daemon"
2015-06-01 06:10:06 +02:00
self . requires_wallet = ' w ' in s
self . requires_password = ' p ' in s
2021-07-07 16:44:27 +03:00
self . requires_lightning = ' l ' in s
2025-03-18 11:15:16 +01:00
self . parse_docstring ( func . __doc__ )
2017-01-22 21:25:24 +03:00
varnames = func . __code__ . co_varnames [ 1 : func . __code__ . co_argcount ]
self . defaults = func . __defaults__
2015-06-01 06:10:06 +02:00
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 = [ ]
2020-05-14 16:33:02 +02:00
# sanity checks
if self . requires_password :
assert self . requires_wallet
for varname in ( ' wallet_path ' , ' wallet ' ) :
if varname in varnames :
2025-03-06 11:43:50 +01:00
assert varname in self . options , f " cmd: { self . name } : { varname } not in options { self . options } "
2020-05-14 16:33:02 +02:00
assert not ( ' wallet_path ' in varnames and ' wallet ' in varnames )
if self . requires_wallet :
assert ' wallet ' in varnames
2025-03-18 11:15:16 +01:00
def parse_docstring ( self , docstring ) :
docstring = docstring or ' '
2025-05-28 15:25:26 +02:00
docstring = docstring . strip ( )
2025-03-18 11:15:16 +01:00
self . description = docstring
self . arg_descriptions = { }
self . arg_types = { }
for x in re . finditer ( r ' arg:(.*?):(.*?):(.*)$ ' , docstring , flags = re . MULTILINE ) :
self . arg_descriptions [ x . group ( 2 ) ] = x . group ( 3 )
self . arg_types [ x . group ( 2 ) ] = x . group ( 1 )
self . description = self . description . replace ( x . group ( ) , ' ' )
2025-05-28 15:25:26 +02:00
self . short_description = self . description . split ( ' . ' ) [ 0 ]
2025-03-18 11:15:16 +01:00
2015-06-01 06:10:06 +02:00
def command ( s ) :
def decorator ( func ) :
2025-03-16 11:12:04 +01:00
if hasattr ( func , ' __wrapped__ ' ) :
# plugin command function
name = func . plugin_name + ' _ ' + func . __name__
known_commands [ name ] = Command ( func . __wrapped__ , name , s )
else :
# regular command function
name = func . __name__
known_commands [ name ] = Command ( func , name , s )
2025-03-06 11:43:50 +01:00
2015-06-01 06:10:06 +02:00
@wraps ( func )
2019-09-06 11:06:08 +02:00
async def func_wrapper ( * args , * * kwargs ) :
2019-09-09 22:15:11 +02:00
cmd_runner = args [ 0 ] # type: Commands
2025-03-16 11:12:04 +01:00
cmd = known_commands [ name ] # type: Command
2017-10-07 08:19:14 +02:00
password = kwargs . get ( ' password ' )
2019-09-06 11:06:08 +02:00
daemon = cmd_runner . daemon
if daemon :
2025-06-04 12:44:07 +02:00
if ' wallet_path ' in cmd . options or cmd . requires_wallet :
kwargs [ ' wallet_path ' ] = daemon . config . maybe_complete_wallet_path ( kwargs . get ( ' wallet_path ' ) )
2020-05-14 16:33:02 +02:00
if ' wallet ' in cmd . options :
2025-06-04 14:23:25 +02:00
wallet_path = kwargs . pop ( ' wallet_path ' , None ) # unit tests may set wallet and not wallet_path
wallet = kwargs . get ( ' wallet ' , None ) # run_offline_command sets both
2025-07-07 09:56:50 +02:00
if wallet is None and wallet_path is not None :
2025-06-04 14:23:25 +02:00
wallet = daemon . get_wallet ( wallet_path )
2020-05-14 16:33:02 +02:00
if wallet is None :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( ' wallet not loaded ' )
2020-05-14 16:33:02 +02:00
kwargs [ ' wallet ' ] = wallet
2025-07-07 09:56:50 +02:00
if cmd . requires_password and password is None and wallet and wallet . has_password ( ) :
2023-09-15 15:54:25 +02:00
password = wallet . get_unlocked_password ( )
if password :
kwargs [ ' password ' ] = password
else :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( ' Password required. Unlock the wallet, or add a --password option to your command ' )
2020-05-14 16:33:02 +02:00
wallet = kwargs . get ( ' wallet ' ) # type: Optional[Abstract_Wallet]
if cmd . requires_wallet and not wallet :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( ' wallet not loaded ' )
2024-10-08 15:15:42 +00:00
if cmd . requires_password and wallet . has_password ( ) :
if password is None :
raise UserFacingException ( ' Password required ' )
try :
wallet . check_password ( password )
except InvalidPassword as e :
raise UserFacingException ( str ( e ) ) from None
2021-07-07 16:44:27 +03:00
if cmd . requires_lightning and ( not wallet or not wallet . has_lightning ( ) ) :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( ' Lightning not enabled in this wallet ' )
2019-09-06 12:38:13 +02:00
return await func ( * args , * * kwargs )
2015-06-01 06:10:06 +02:00
return func_wrapper
return decorator
2024-11-22 14:46:21 +01:00
class Commands ( Logger ) :
2013-02-26 13:56:48 +01:00
2019-09-05 18:30:04 +02:00
def __init__ ( self , * , config : ' SimpleConfig ' ,
network : ' Network ' = None ,
2019-09-04 20:16:47 +02:00
daemon : ' Daemon ' = None , callback = None ) :
2024-11-22 14:46:21 +01:00
Logger . __init__ ( self )
2015-05-30 12:35:58 +02:00
self . config = config
2019-08-19 12:46:31 +02:00
self . daemon = daemon
2013-09-15 11:19:48 +02:00
self . network = network
2013-02-27 12:40:16 +01:00
self . _callback = callback
2013-02-26 13:56:48 +01:00
2019-08-15 13:17:16 +02:00
def _run ( self , method , args , password_getter = None , * * kwargs ) :
""" This wrapper is called from unit tests and the Qt python console. """
2013-10-03 12:39:42 +02:00
cmd = known_commands [ method ]
2019-08-06 05:20:53 +02:00
password = kwargs . get ( ' password ' , None )
2019-09-05 18:30:04 +02:00
wallet = kwargs . get ( ' wallet ' , None )
if ( cmd . requires_password and wallet and wallet . has_password ( )
2019-08-06 05:20:53 +02:00
and password is None ) :
2017-08-01 05:22:18 +02:00
password = password_getter ( )
2017-07-02 11:44:48 +02:00
if password is None :
2016-07-01 16:19:26 +02:00
return
2017-08-03 21:32:25 +03:00
2013-11-11 22:03:20 -08:00
f = getattr ( self , method )
2017-07-28 08:04:32 -04:00
if cmd . requires_password :
2019-08-06 05:20:53 +02:00
kwargs [ ' password ' ] = password
2019-08-15 13:17:16 +02:00
2019-09-12 19:22:55 +02:00
if ' wallet ' in kwargs :
sig = inspect . signature ( f )
if ' wallet ' not in sig . parameters :
kwargs . pop ( ' wallet ' )
2019-08-15 13:17:16 +02:00
coro = f ( * args , * * kwargs )
2022-04-29 18:24:49 +02:00
fut = asyncio . run_coroutine_threadsafe ( coro , util . get_asyncio_loop ( ) )
2019-08-15 13:17:16 +02:00
result = fut . result ( )
2017-08-01 05:22:18 +02:00
2013-02-27 12:40:16 +01:00
if self . _callback :
2017-08-01 05:22:18 +02:00
self . _callback ( )
2013-02-26 17:57:48 +01:00
return result
2013-02-26 13:56:48 +01:00
2019-08-19 12:46:31 +02:00
@command ( ' n ' )
async def getinfo ( self ) :
""" network info """
net_params = self . network . get_parameters ( )
response = {
2022-10-31 15:26:27 +00:00
' network ' : constants . net . NET_NAME ,
2019-08-19 12:46:31 +02:00
' path ' : self . network . config . path ,
2020-04-16 20:30:53 +02:00
' server ' : net_params . server . host ,
2019-08-19 12:46:31 +02:00
' 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 ,
2025-02-24 12:20:44 +01:00
' fee_estimates ' : self . network . fee_estimates . get_data ( )
2019-08-19 12:46:31 +02:00
}
return response
@command ( ' n ' )
async def stop ( self ) :
""" Stop daemon """
2021-03-17 19:16:07 +01:00
await self . daemon . stop ( )
2019-08-19 12:46:31 +02:00
return " Daemon stopped "
@command ( ' n ' )
async def list_wallets ( self ) :
""" List wallets open in daemon """
2023-10-07 17:42:21 +02:00
return [
{
2025-06-04 10:45:46 +02:00
' path ' : w . db . storage . path ,
2023-10-07 17:42:21 +02:00
' synchronized ' : w . is_up_to_date ( ) ,
2025-06-01 10:52:52 +02:00
' unlocked ' : not w . has_password ( ) or ( w . get_unlocked_password ( ) is not None ) ,
2023-10-07 17:42:21 +02:00
}
2025-06-04 10:45:46 +02:00
for w in self . daemon . get_wallets ( ) . values ( )
2023-10-07 17:42:21 +02:00
]
2019-08-19 12:46:31 +02:00
@command ( ' n ' )
2024-06-08 11:10:27 +02:00
async def load_wallet ( self , wallet_path = None , password = None ) :
2023-10-07 17:42:21 +02:00
"""
Load the wallet in memory
"""
2023-09-22 11:49:53 +02:00
wallet = self . daemon . load_wallet ( wallet_path , password , upgrade = True )
2023-10-07 17:42:21 +02:00
if wallet is None :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( ' could not load wallet ' )
2023-10-07 17:42:21 +02:00
run_hook ( ' load_wallet ' , wallet , None )
2025-05-31 11:45:57 +02:00
return wallet_path
2019-08-19 12:46:31 +02:00
@command ( ' n ' )
2019-09-06 11:06:08 +02:00
async def close_wallet ( self , wallet_path = None ) :
2019-08-19 12:46:31 +02:00
""" Close wallet """
2021-06-17 12:35:31 +02:00
return await self . daemon . _stop_wallet ( wallet_path )
2019-08-19 12:46:31 +02:00
2015-06-01 06:10:06 +02:00
@command ( ' ' )
2019-09-06 11:06:08 +02:00
async def create ( self , passphrase = None , password = None , encrypt_file = True , seed_type = None , wallet_path = None ) :
2019-03-09 16:47:19 +01:00
""" Create a new wallet.
If you want to be prompted for an argument, type ' ? ' or ' : ' (concealed)
2025-03-18 11:15:16 +01:00
arg:str:passphrase:Seed extension
arg:str:seed_type:The type of wallet to create, e.g. ' standard ' or ' segwit '
arg:bool:encrypt_file:Whether the file on disk should be encrypted with the provided password
2019-03-09 16:47:19 +01:00
"""
2025-03-18 11:15:16 +01:00
d = create_new_wallet (
path = wallet_path ,
passphrase = passphrase ,
password = password ,
encrypt_file = encrypt_file ,
seed_type = seed_type ,
config = self . config )
2019-02-28 20:12:24 +01:00
return {
' seed ' : d [ ' seed ' ] ,
' path ' : d [ ' wallet ' ] . storage . path ,
' msg ' : d [ ' msg ' ] ,
}
2015-05-31 23:17:44 +02:00
2018-10-10 20:29:51 +02:00
@command ( ' ' )
2019-09-06 11:06:08 +02:00
async def restore ( self , text , passphrase = None , password = None , encrypt_file = True , wallet_path = None ) :
2015-10-28 09:33:35 +01:00
""" Restore a wallet from text. Text can be a seed phrase, a master
public key, a master private key, a list of bitcoin addresses
2019-03-09 16:47:19 +01:00
or bitcoin private keys.
If you want to be prompted for an argument, type ' ? ' or ' : ' (concealed)
2025-03-18 11:15:16 +01:00
arg:str:text:seed phrase
arg:str:passphrase:Seed extension
arg:bool:encrypt_file:Whether the file on disk should be encrypted with the provided password
2019-03-09 16:47:19 +01:00
"""
2019-09-04 20:15:54 +02:00
# TODO create a separate command that blocks until wallet is synced
2025-03-18 11:15:16 +01:00
d = restore_wallet_from_text (
text ,
path = wallet_path ,
passphrase = passphrase ,
password = password ,
encrypt_file = encrypt_file ,
config = self . config )
2019-02-28 20:12:24 +01:00
return {
' path ' : d [ ' wallet ' ] . storage . path ,
' msg ' : d [ ' msg ' ] ,
}
2015-05-31 23:17:44 +02:00
2015-06-01 06:10:06 +02:00
@command ( ' wp ' )
2022-05-26 21:41:29 +02:00
async def password ( self , password = None , new_password = None , encrypt_file = None , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Change wallet password.
arg:bool:encrypt_file:Whether the file on disk should be encrypted with the provided password (default=true)
2025-04-16 17:48:40 +02:00
arg:str:new_password:New Password
2025-03-18 11:15:16 +01:00
"""
2019-09-05 17:57:51 +02:00
if wallet . storage . is_encrypted_with_hw_device ( ) and new_password :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( " Can ' t change the password of a wallet encrypted with a hw device. " )
2022-05-26 21:41:29 +02:00
if encrypt_file is None :
if not password and new_password :
# currently no password, setting one now: we encrypt by default
encrypt_file = True
else :
encrypt_file = wallet . storage . is_encrypted ( )
wallet . update_password ( password , new_password , encrypt_storage = encrypt_file )
2020-02-05 15:13:37 +01:00
wallet . save_db ( )
2025-06-03 11:26:23 +02:00
return { ' password ' : wallet . has_password ( ) }
2015-05-31 22:42:34 +02:00
2019-01-26 16:50:51 +01:00
@command ( ' w ' )
2019-09-21 02:14:22 +02:00
async def get ( self , key , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Return item from wallet storage
arg:str:key:storage key
"""
2020-02-05 15:13:37 +01:00
return wallet . db . get ( key )
2019-01-26 16:50:51 +01:00
2015-06-01 06:10:06 +02:00
@command ( ' ' )
2019-08-15 13:17:16 +02:00
async def getconfig ( self , key ) :
2025-03-18 11:15:16 +01:00
""" Return the current value of a configuration variable.
arg:str:key:name of the configuration variable
"""
2023-09-08 14:55:22 +00:00
if Plugins . is_plugin_enabler_config_key ( key ) :
return self . config . get ( key )
else :
cv = self . config . cv . from_key ( key )
return cv . get ( )
2015-05-31 22:42:34 +02:00
2018-04-06 18:53:13 +02:00
@classmethod
def _setconfig_normalize_value ( cls , key , value ) :
2023-05-24 17:41:44 +00:00
if key not in ( SimpleConfig . RPC_USERNAME . key ( ) , SimpleConfig . RPC_PASSWORD . key ( ) ) :
2018-04-06 18:53:13 +02:00
value = json_decode ( value )
2020-11-25 11:47:25 +01:00
# call literal_eval for backward compatibility (see #4225)
2018-04-06 18:53:13 +02:00
try :
value = ast . literal_eval ( value )
2023-04-23 01:33:12 +00:00
except Exception :
2018-04-06 18:53:13 +02:00
pass
return value
2025-05-18 19:51:26 +02:00
def _setconfig ( self , key , value ) :
2018-04-06 18:53:13 +02:00
value = self . _setconfig_normalize_value ( key , value )
2026-03-24 17:07:26 +00:00
if self . daemon and key in (
SimpleConfig . RPC_USERNAME . key ( ) ,
SimpleConfig . RPC_PASSWORD . key ( ) ,
SimpleConfig . RPC_HOST . key ( ) ,
SimpleConfig . RPC_PORT . key ( ) ,
SimpleConfig . RPC_SOCKET_TYPE . key ( ) ,
SimpleConfig . RPC_SOCKET_FILEPATH . key ( ) ,
) :
raise UserFacingException (
" error: RPC server settings cannot be changed for already running daemon. "
" Stop the daemon first, and run ' setconfig ' in --offline mode. "
" \n For example: ' $ electrum -o setconfig rpcport 7777 ' . "
)
2023-09-08 14:55:22 +00:00
if Plugins . is_plugin_enabler_config_key ( key ) :
self . config . set_key ( key , value )
else :
cv = self . config . cv . from_key ( key )
cv . set ( value )
2015-05-31 22:42:34 +02:00
2025-05-18 19:51:26 +02:00
@command ( ' ' )
async def setconfig ( self , key , value ) :
"""
Set a configuration variable.
arg:str:key:name of the configuration variable
arg:str:value:value. may be a string or a Python expression.
"""
self . _setconfig ( key , value )
@command ( ' ' )
2025-06-12 19:56:14 +02:00
async def unsetconfig ( self , key ) :
2025-05-18 19:51:26 +02:00
"""
Clear a configuration variable.
The variable will be reset to its default value.
arg:str:key:name of the configuration variable
"""
self . _setconfig ( key , None )
2024-04-30 13:22:36 +02:00
@command ( ' ' )
async def listconfig ( self ) :
""" Returns the list of all configuration variables. """
return self . config . list_config_vars ( )
@command ( ' ' )
2025-01-17 11:27:12 +01:00
async def helpconfig ( self , key ) :
2025-03-18 11:15:16 +01:00
""" Returns help about a configuration variable.
arg:str:key:name of the configuration variable
"""
2024-04-30 13:22:36 +02:00
cv = self . config . cv . from_key ( key )
2025-01-17 11:27:12 +01:00
short = cv . get_short_desc ( )
long = cv . get_long_desc ( )
if short and long :
return short + " \n --- \n \n " + long
elif short or long :
return short or long
else :
return f " No description available for ' { key } ' "
2024-04-30 13:22:36 +02:00
2015-06-01 06:10:06 +02:00
@command ( ' ' )
2020-12-08 10:00:34 +01:00
async def make_seed ( self , nbits = None , language = None , seed_type = None ) :
2025-03-18 11:15:16 +01:00
"""
Create a seed
arg:int:nbits:Number of bits of entropy
arg:str:seed_type:The type of seed to create, e.g. ' standard ' or ' segwit '
arg:str:language:Default language for wordlist
"""
2020-12-08 10:00:34 +01:00
s = Mnemonic ( language ) . make_seed ( seed_type = seed_type , num_bits = nbits )
2017-08-27 09:53:22 +02:00
return s
2014-08-28 15:37:42 +02:00
2015-06-01 06:10:06 +02:00
@command ( ' n ' )
2019-08-15 13:17:16 +02:00
async def getaddresshistory ( self , address ) :
2025-03-18 11:15:16 +01:00
"""
Return the transaction history of any address. Note: This is a
2015-06-03 09:12:38 +02:00
walletless server query, results are not checked by SPV.
2025-03-18 11:15:16 +01:00
arg:str:address:Bitcoin address
2015-06-03 09:12:38 +02:00
"""
2018-02-02 23:26:25 +01:00
sh = bitcoin . address_to_scripthash ( address )
2019-08-15 13:17:16 +02:00
return await self . network . get_history_for_scripthash ( sh )
2014-01-23 17:06:47 +01:00
2024-06-08 11:10:27 +02:00
@command ( ' wp ' )
async def unlock ( self , wallet : Abstract_Wallet = None , password = None ) :
""" Unlock the wallet (store the password in memory). """
wallet . unlock ( password )
2015-12-23 15:59:32 +01:00
@command ( ' w ' )
2019-09-21 02:14:22 +02:00
async def listunspent ( self , wallet : Abstract_Wallet = None ) :
2015-06-03 09:12:38 +02:00
""" List unspent outputs. Returns the list of unspent transaction
outputs in your wallet. """
2019-10-23 17:09:41 +02:00
coins = [ ]
for txin in wallet . get_utxos ( ) :
d = txin . to_json ( )
v = d . pop ( " value_sats " )
2025-08-21 17:38:27 +00:00
d [ " value " ] = format_satoshis ( v )
2019-10-23 17:09:41 +02:00
coins . append ( d )
return coins
2013-02-26 13:56:48 +01:00
2015-06-01 06:10:06 +02:00
@command ( ' n ' )
2019-08-15 13:17:16 +02:00
async def getaddressunspent ( self , address ) :
2025-03-18 11:15:16 +01:00
"""
Returns the UTXO list of any address. Note: This
2015-06-03 09:12:38 +02:00
is a walletless server query, results are not checked by SPV.
2025-03-18 11:15:16 +01:00
arg:str:address:Bitcoin address
2015-06-03 09:12:38 +02:00
"""
2018-02-02 23:26:25 +01:00
sh = bitcoin . address_to_scripthash ( address )
2019-08-15 13:17:16 +02:00
return await self . network . listunspent_for_scripthash ( sh )
2014-01-23 17:06:47 +01:00
2016-05-16 14:39:01 +02:00
@command ( ' ' )
2019-08-15 13:17:16 +02:00
async def serialize ( self , jsontx ) :
2023-03-19 18:34:43 +00:00
""" Create a signed raw transaction from a json tx template.
Example value for " jsontx " arg: {
" inputs " : [
{ " prevout_hash " : " 9d221a69ca3997cbeaf5624d723e7dc5f829b1023078c177d37bdae95f37c539 " , " prevout_n " : 1,
" value_sats " : 1000000, " privkey " : " p2wpkh:cVDXzzQg6RoCTfiKpe8MBvmm5d5cJc6JLuFApsFDKwWa6F5TVHpD " }
],
" outputs " : [
{ " address " : " tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd " , " value_sats " : 990000}
]
}
2025-07-25 12:48:51 +02:00
arg:json:jsontx:Transaction in json
2016-05-16 14:39:01 +02:00
"""
keypairs = { }
2019-10-23 17:09:41 +02:00
inputs = [ ] # type: List[PartialTxInput]
2020-08-05 01:47:35 -05:00
locktime = jsontx . get ( ' locktime ' , 0 )
2024-02-12 19:02:02 +00:00
for txin_idx , txin_dict in enumerate ( jsontx . get ( ' inputs ' ) ) :
2019-10-23 17:09:41 +02:00
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 :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( f " missing prevout for txin { txin_idx } " )
2019-10-23 17:09:41 +02:00
txin = PartialTxInput ( prevout = prevout )
2023-03-19 18:34:43 +00:00
try :
txin . _trusted_value_sats = int ( txin_dict . get ( ' value ' ) or txin_dict [ ' value_sats ' ] )
except KeyError :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( f " missing ' value_sats ' field for txin { txin_idx } " )
2020-06-28 17:26:52 +02:00
nsequence = txin_dict . get ( ' nsequence ' , None )
if nsequence is not None :
txin . nsequence = nsequence
2019-10-23 17:09:41 +02:00
sec = txin_dict . get ( ' privkey ' )
2017-10-09 11:53:47 +02:00
if sec :
txin_type , privkey , compressed = bitcoin . deserialize_privkey ( sec )
2024-04-29 16:32:19 +00:00
pubkey = ecc . ECPrivkey ( privkey ) . get_public_key_bytes ( compressed = compressed )
keypairs [ pubkey ] = privkey
desc = descriptor . get_singlesig_descriptor_from_legacy_leaf ( pubkey = pubkey . hex ( ) , script_type = txin_type )
2023-02-26 12:14:25 +00:00
txin . script_descriptor = desc
2019-10-23 17:09:41 +02:00
inputs . append ( txin )
2023-03-19 18:34:43 +00:00
outputs = [ ] # type: List[PartialTxOutput]
2024-02-12 19:02:02 +00:00
for txout_idx , txout_dict in enumerate ( jsontx . get ( ' outputs ' ) ) :
2023-03-19 18:34:43 +00:00
try :
txout_addr = txout_dict [ ' address ' ]
except KeyError :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( f " missing ' address ' field for txout { txout_idx } " )
2023-03-19 18:34:43 +00:00
try :
txout_val = int ( txout_dict . get ( ' value ' ) or txout_dict [ ' value_sats ' ] )
except KeyError :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( f " missing ' value_sats ' field for txout { txout_idx } " )
2023-03-19 18:34:43 +00:00
txout = PartialTxOutput . from_address_and_value ( txout_addr , txout_val )
outputs . append ( txout )
2019-10-23 17:09:41 +02:00
tx = PartialTransaction . from_io ( inputs , outputs , locktime = locktime )
2016-05-16 14:39:01 +02:00
tx . sign ( keypairs )
2019-10-23 17:09:41 +02:00
return tx . serialize ( )
2013-02-26 13:56:48 +01:00
2021-06-08 16:33:55 +02:00
@command ( ' ' )
async def signtransaction_with_privkey ( self , tx , privkey ) :
2025-05-29 16:44:01 +02:00
""" Sign a transaction with private keys passed as parameter.
2025-03-18 11:15:16 +01:00
arg:tx:tx:Transaction to sign
2025-05-29 16:44:01 +02:00
arg:str:privkey:private key or list of private keys
2025-03-18 11:15:16 +01:00
"""
2020-08-26 19:48:35 +02:00
tx = tx_from_any ( tx )
2021-06-08 16:33:55 +02:00
txins_dict = defaultdict ( list )
for txin in tx . inputs ( ) :
txins_dict [ txin . address ] . append ( txin )
if not isinstance ( privkey , list ) :
privkey = [ privkey ]
for priv in privkey :
txin_type , priv2 , compressed = bitcoin . deserialize_privkey ( priv )
pubkey = ecc . ECPrivkey ( priv2 ) . get_public_key_bytes ( compressed = compressed )
2023-02-26 12:14:25 +00:00
desc = descriptor . get_singlesig_descriptor_from_legacy_leaf ( pubkey = pubkey . hex ( ) , script_type = txin_type )
address = desc . expand ( ) . address ( )
2021-06-08 16:33:55 +02:00
if address in txins_dict . keys ( ) :
for txin in txins_dict [ address ] :
2023-02-26 12:14:25 +00:00
txin . script_descriptor = desc
2024-04-29 16:32:19 +00:00
tx . sign ( { pubkey : priv2 } )
2021-06-08 16:33:55 +02:00
return tx . serialize ( )
@command ( ' wp ' )
2025-03-18 11:15:16 +01:00
async def signtransaction ( self , tx , password = None , wallet : Abstract_Wallet = None , ignore_warnings : bool = False ) :
"""
2025-05-29 16:44:01 +02:00
Sign a transaction with the current wallet.
2025-03-18 11:15:16 +01:00
arg:tx:tx:transaction
arg:bool:ignore_warnings:ignore warnings
"""
2021-06-08 16:33:55 +02:00
tx = tx_from_any ( tx )
2025-03-18 11:15:16 +01:00
wallet . sign_transaction ( tx , password , ignore_warnings = ignore_warnings )
2019-10-23 17:09:41 +02:00
return tx . serialize ( )
2013-02-26 13:56:48 +01:00
2015-06-01 06:10:06 +02:00
@command ( ' ' )
2019-08-15 13:17:16 +02:00
async def deserialize ( self , tx ) :
2025-03-18 11:15:16 +01:00
"""
Deserialize a transaction
arg:str:tx:Serialized transaction
"""
2019-10-23 17:09:41 +02:00
tx = tx_from_any ( tx )
return tx . to_json ( )
2013-02-26 13:56:48 +01:00
2015-06-01 06:10:06 +02:00
@command ( ' n ' )
2019-08-15 13:17:16 +02:00
async def broadcast ( self , tx ) :
2025-03-18 11:15:16 +01:00
"""
Broadcast a transaction to the network.
arg:str:tx:Serialized transaction (must be hexadecimal)
"""
2016-03-20 19:05:38 +01:00
tx = Transaction ( tx )
2019-08-15 13:17:16 +02:00
await self . network . broadcast_transaction ( tx )
2019-01-18 19:59:12 +01:00
return tx . txid ( )
2013-02-26 13:56:48 +01:00
2015-06-01 06:10:06 +02:00
@command ( ' ' )
2019-08-15 13:17:16 +02:00
async def createmultisig ( self , num , pubkeys ) :
2025-03-18 11:15:16 +01:00
"""
Create multisig ' n of m ' address
arg:int:num:Number of cosigners required
arg:json:pubkeys:List of public keys
"""
2015-05-30 18:49:58 +02:00
assert isinstance ( pubkeys , list ) , ( type ( num ) , type ( pubkeys ) )
2017-12-06 18:30:02 +01:00
redeem_script = multisig_script ( pubkeys , num )
2024-04-26 20:09:00 +00:00
address = bitcoin . hash160_to_p2sh ( hash_160 ( redeem_script ) )
return { ' address ' : address , ' redeemScript ' : redeem_script . hex ( ) }
2014-06-24 16:12:43 +03:00
2015-06-01 06:10:06 +02:00
@command ( ' w ' )
2021-01-22 16:47:22 +01:00
async def freeze ( self , address : str , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Freeze address. Freeze the funds at one of your wallet \' s addresses
arg:str:address:Bitcoin address
"""
2019-09-05 17:57:51 +02:00
return wallet . set_frozen_state_of_addresses ( [ address ] , True )
2014-06-24 16:12:43 +03:00
2015-06-01 06:10:06 +02:00
@command ( ' w ' )
2021-01-22 16:47:22 +01:00
async def unfreeze ( self , address : str , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Unfreeze address. Unfreeze the funds at one of your wallet \' s address
arg:str:address:Bitcoin address
"""
2019-09-05 17:57:51 +02:00
return wallet . set_frozen_state_of_addresses ( [ address ] , False )
2013-02-26 13:56:48 +01:00
2021-01-22 16:47:22 +01:00
@command ( ' w ' )
async def freeze_utxo ( self , coin : str , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Freeze a UTXO so that the wallet will not spend it.
arg:str:coin:outpoint, in the <txid:index> format
"""
2021-01-22 16:47:22 +01:00
wallet . set_frozen_state_of_coins ( [ coin ] , True )
return True
@command ( ' w ' )
async def unfreeze_utxo ( self , coin : str , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
""" Unfreeze a UTXO so that the wallet might spend it.
arg:str:coin:outpoint
"""
2021-01-22 16:47:22 +01:00
wallet . set_frozen_state_of_coins ( [ coin ] , False )
return True
2015-06-01 06:10:06 +02:00
@command ( ' wp ' )
2019-09-21 02:14:22 +02:00
async def getprivatekeys ( self , address , password = None , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Get private keys of addresses. You may pass a single wallet address, or a list of wallet addresses.
arg:str:address:Bitcoin address
"""
2017-11-19 23:46:59 +01:00
if isinstance ( address , str ) :
address = address . strip ( )
2016-06-11 16:55:19 +02:00
if is_address ( address ) :
2019-10-23 17:09:41 +02:00
return wallet . export_private_key ( address , password )
2017-03-15 06:12:26 +01:00
domain = address
2019-10-23 17:09:41 +02:00
return [ wallet . export_private_key ( address , password ) for address in domain ]
2013-02-26 16:03:04 +01:00
2020-03-31 05:50:18 +02:00
@command ( ' wp ' )
async def getprivatekeyforpath ( self , path , password = None , wallet : Abstract_Wallet = None ) :
""" Get private key corresponding to derivation path (address index).
2025-03-18 11:15:16 +01:00
arg:str:path:Derivation path. Can be either a str such as " m/0/50 " , or a list of ints such as [0, 50].
2020-03-31 05:50:18 +02:00
"""
return wallet . export_private_key_for_path ( path , password )
2015-06-01 06:10:06 +02:00
@command ( ' w ' )
2019-09-21 02:14:22 +02:00
async def ismine ( self , address , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Check if address is in wallet. Return true if and only address is in wallet
arg:str:address:Bitcoin address
"""
2019-09-05 17:57:51 +02:00
return wallet . is_mine ( address )
2015-01-11 20:37:08 +01:00
2015-08-16 16:30:55 +02:00
@command ( ' ' )
2019-08-15 13:17:16 +02:00
async def dumpprivkeys ( self ) :
2015-08-16 16:30:55 +02:00
""" Deprecated. """
return " This command is deprecated. Use a pipe instead: ' electrum listaddresses | electrum getprivatekeys - ' "
2013-02-26 13:56:48 +01:00
2015-06-01 06:10:06 +02:00
@command ( ' ' )
2019-08-15 13:17:16 +02:00
async def validateaddress ( self , address ) :
2025-03-18 11:15:16 +01:00
""" Check that an address is valid.
arg:str:address:Bitcoin address
"""
2015-05-31 22:42:34 +02:00
return is_address ( address )
2013-10-03 13:31:59 +02:00
2015-06-01 06:10:06 +02:00
@command ( ' w ' )
2019-09-21 02:14:22 +02:00
async def getpubkeys ( self , address , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Return the public keys for a wallet address.
arg:str:address:Bitcoin address
"""
2019-09-05 17:57:51 +02:00
return wallet . get_public_keys ( address )
2013-02-26 13:56:48 +01:00
2015-12-23 15:59:32 +01:00
@command ( ' w ' )
2019-09-21 02:14:22 +02:00
async def getbalance ( self , wallet : Abstract_Wallet = None ) :
2015-12-23 15:59:32 +01:00
""" Return the balance of your wallet. """
2019-09-05 17:57:51 +02:00
c , u , x = wallet . get_balance ( )
l = wallet . lnworker . get_balance ( ) if wallet . lnworker else None
2025-08-21 17:38:27 +00:00
out = { " confirmed " : format_satoshis ( c ) }
2015-05-05 20:52:14 +02:00
if u :
2025-08-21 17:38:27 +00:00
out [ " unconfirmed " ] = format_satoshis ( u )
2015-05-05 20:52:14 +02:00
if x :
2025-08-21 17:38:27 +00:00
out [ " unmatured " ] = format_satoshis ( x )
2019-01-30 19:16:04 +01:00
if l :
2025-08-21 17:38:27 +00:00
out [ " lightning " ] = format_satoshis ( l )
2013-04-16 16:05:45 +02:00
return out
2013-02-27 10:24:53 +01:00
2015-06-01 06:10:06 +02:00
@command ( ' n ' )
2019-08-15 13:17:16 +02:00
async def getaddressbalance ( self , address ) :
2025-03-18 11:15:16 +01:00
"""
Return the balance of any address. Note: This is a walletless
2015-06-03 09:12:38 +02:00
server query, results are not checked by SPV.
2025-03-18 11:15:16 +01:00
arg:str:address:Bitcoin address
2015-06-03 09:12:38 +02:00
"""
2018-02-02 23:26:25 +01:00
sh = bitcoin . address_to_scripthash ( address )
2019-08-15 13:17:16 +02:00
out = await self . network . get_balance_for_scripthash ( sh )
2025-08-21 17:38:27 +00:00
out [ " confirmed " ] = format_satoshis ( out [ " confirmed " ] )
out [ " unconfirmed " ] = format_satoshis ( out [ " unconfirmed " ] )
2014-03-02 10:31:34 +01:00
return out
2015-06-01 06:10:06 +02:00
@command ( ' n ' )
2019-08-15 13:17:16 +02:00
async def getmerkle ( self , txid , height ) :
2015-06-12 10:34:45 +02:00
""" Get Merkle branch of a transaction included in a block. Electrum
2025-03-18 11:15:16 +01:00
uses this to verify transactions (Simple Payment Verification).
arg:txid:txid:Transaction ID
arg:int:height:Block height
"""
2019-08-15 13:17:16 +02:00
return await self . network . get_merkle_for_transaction ( txid , int ( height ) )
2015-05-12 12:30:26 +02:00
2015-06-01 06:10:06 +02:00
@command ( ' n ' )
2019-08-15 13:17:16 +02:00
async def getservers ( self ) :
2020-03-30 22:50:25 +00:00
""" Return the list of known servers (candidates for connecting). """
2013-10-02 13:50:36 +02:00
return self . network . get_servers ( )
2013-02-26 13:56:48 +01:00
2015-06-01 06:10:06 +02:00
@command ( ' ' )
2019-08-15 13:17:16 +02:00
async def version ( self ) :
2018-04-15 20:45:30 +03:00
""" Return the version of Electrum. """
2016-10-11 12:08:56 +02:00
return ELECTRUM_VERSION
2014-06-24 16:12:43 +03:00
2022-04-11 17:05:26 +02:00
@command ( ' ' )
async def version_info ( self ) :
""" Return information about dependencies, such as their version and path. """
ret = {
" electrum.version " : ELECTRUM_VERSION ,
" electrum.path " : os . path . dirname ( os . path . realpath ( __file__ ) ) ,
2023-06-02 21:47:04 +00:00
" python.version " : sys . version ,
" python.path " : sys . executable ,
2022-04-11 17:05:26 +02:00
}
# add currently running GUI
if self . daemon and self . daemon . gui_object :
ret . update ( self . daemon . gui_object . version_info ( ) )
# always add Qt GUI, so we get info even when running this from CLI
try :
from . gui . qt import ElectrumGui as QtElectrumGui
ret . update ( QtElectrumGui . version_info ( ) )
except GuiImportError :
pass
# Add shared libs (.so/.dll), and non-pure-python dependencies.
# Such deps can be installed in various ways - often via the Linux distro's pkg manager,
# instead of using pip, hence it is useful to list them for debugging.
2024-06-17 13:38:54 +02:00
from electrum_ecc import ecc_fast
2022-04-11 17:05:26 +02:00
ret . update ( ecc_fast . version_info ( ) )
from . import qrscanner
ret . update ( qrscanner . version_info ( ) )
ret . update ( DeviceMgr . version_info ( ) )
ret . update ( crypto . version_info ( ) )
# add some special cases
import aiohttp
ret [ " aiohttp.version " ] = aiohttp . __version__
import aiorpcx
ret [ " aiorpcx.version " ] = aiorpcx . _version_str
import certifi
ret [ " certifi.version " ] = certifi . __version__
import dns
ret [ " dnspython.version " ] = dns . __version__
2025-11-30 06:03:08 +00:00
import ssl
ret [ " openssl.version " ] = ssl . OPENSSL_VERSION
2022-04-11 17:05:26 +02:00
return ret
2015-06-01 06:10:06 +02:00
@command ( ' w ' )
2019-09-21 02:14:22 +02:00
async def getmpk ( self , wallet : Abstract_Wallet = None ) :
2016-06-11 16:10:45 +02:00
""" Get master public key. Return your wallet \' s master public key """
2019-09-05 17:57:51 +02:00
return wallet . get_master_public_key ( )
2013-11-03 14:14:35 +01:00
2015-08-14 15:23:50 +02:00
@command ( ' wp ' )
2019-09-21 02:14:22 +02:00
async def getmasterprivate ( self , password = None , wallet : Abstract_Wallet = None ) :
2015-08-14 15:23:50 +02:00
""" Get master private key. Return your wallet \' s master private key """
2019-09-05 17:57:51 +02:00
return str ( wallet . keystore . get_master_private_key ( password ) )
2015-08-14 15:23:50 +02:00
2019-01-20 15:49:42 +01:00
@command ( ' ' )
2019-08-15 13:17:16 +02:00
async def convert_xkey ( self , xkey , xtype ) :
2025-03-18 11:15:16 +01:00
""" Convert xtype of a master key. e.g. xpub -> ypub
arg:str:xkey:the key
arg:str:xtype:the type, eg ' xpub '
"""
2019-02-21 22:17:06 +01:00
try :
node = BIP32Node . from_xkey ( xkey )
2023-04-23 01:33:12 +00:00
except Exception :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( ' xkey should be a master public/private key ' )
2019-02-21 22:17:06 +01:00
return node . _replace ( xtype = xtype ) . to_xkey ( )
2019-01-20 15:49:42 +01:00
2015-06-01 06:10:06 +02:00
@command ( ' wp ' )
2019-09-21 02:14:22 +02:00
async def getseed ( self , password = None , wallet : Abstract_Wallet = None ) :
2015-05-31 23:17:44 +02:00
""" Get seed phrase. Print the generation seed of your wallet. """
2019-09-05 17:57:51 +02:00
s = wallet . get_seed ( password )
2017-08-27 09:53:22 +02:00
return s
2013-02-26 13:56:48 +01:00
2015-06-01 06:10:06 +02:00
@command ( ' wp ' )
2019-09-21 02:14:22 +02:00
async def importprivkey ( self , privkey , password = None , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
""" Import a private key or a list of private keys.
arg:str:privkey:Private key. Type \' ? \' to get a prompt.
"""
2019-09-05 17:57:51 +02:00
if not wallet . can_import_privkey ( ) :
2017-06-30 12:20:19 +02:00
return " Error: This type of wallet cannot import private keys. Try to create a new wallet with that key. "
2021-03-31 04:33:33 +02:00
assert isinstance ( wallet , Imported_Wallet )
keys = privkey . split ( )
if not keys :
return " Error: no keys given "
elif len ( keys ) == 1 :
try :
addr = wallet . import_private_key ( keys [ 0 ] , password )
out = " Keypair imported: " + addr
except Exception as e :
out = " Error: " + repr ( e )
return out
else :
good_inputs , bad_inputs = wallet . import_private_keys ( keys , password )
return {
" good_keys " : len ( good_inputs ) ,
" bad_keys " : len ( bad_inputs ) ,
}
2013-02-26 13:56:48 +01:00
2025-05-15 14:31:00 +02:00
async def _resolver ( self , x , wallet : Abstract_Wallet ) :
2015-06-11 12:08:38 +02:00
if x is None :
return None
2025-05-15 14:31:00 +02:00
out = await wallet . contacts . resolve ( x )
2015-06-11 12:08:38 +02:00
return out [ ' address ' ]
2017-11-29 13:45:02 +01:00
@command ( ' n ' )
2025-12-05 16:21:38 +00:00
async def sweep ( self , privkey , destination , fee = None , feerate = None , imax = 100 ) :
2025-03-18 11:15:16 +01:00
"""
Sweep private keys. Returns a transaction that spends UTXOs from
privkey to a destination address. The transaction will not be broadcast.
arg:str:privkey:Private key. Type \' ? \' to get a prompt.
arg:str:destination:Bitcoin address, contact or alias
2025-11-19 16:54:33 +01:00
arg:decimal:fee:Transaction fee (absolute, in BTC)
arg:decimal:feerate:Transaction fee rate (in sat/vbyte)
2025-03-18 11:15:16 +01:00
arg:int:imax:Maximum number of inputs
"""
2017-11-29 13:45:02 +01:00
from . wallet import sweep
2025-02-24 12:20:44 +01:00
fee_policy = self . _get_fee_policy ( fee , feerate )
2017-10-05 20:08:16 +02:00
privkeys = privkey . split ( )
2017-11-29 13:45:02 +01:00
#dest = self._resolver(destination)
2020-12-08 12:21:56 +01:00
tx = await sweep (
privkeys ,
network = self . network ,
to_address = destination ,
2025-02-24 12:20:44 +01:00
fee_policy = fee_policy ,
2020-12-08 12:21:56 +01:00
imax = imax ,
)
2019-10-23 17:09:41 +02:00
return tx . serialize ( ) if tx else None
2014-04-25 17:23:26 +02:00
2015-06-01 06:10:06 +02:00
@command ( ' wp ' )
2019-09-21 02:14:22 +02:00
async def signmessage ( self , address , message , password = None , wallet : Abstract_Wallet = None ) :
2015-05-31 23:23:13 +02:00
""" Sign a message with a key. Use quotes if your message contains
2025-03-18 11:15:16 +01:00
whitespaces
arg:str:address:Bitcoin address
arg:str:message:Clear text message. Use quotes if it contains spaces.
"""
2019-09-05 17:57:51 +02:00
sig = wallet . sign_message ( address , message , password )
2017-10-17 20:15:33 +02:00
return base64 . b64encode ( sig ) . decode ( ' ascii ' )
2013-02-26 13:56:48 +01:00
2015-06-01 06:10:06 +02:00
@command ( ' ' )
2019-08-15 13:17:16 +02:00
async def verifymessage ( self , address , signature , message ) :
2025-03-18 11:15:16 +01:00
""" Verify a signature.
arg:str:address:Bitcoin address
arg:str:message:Clear text message. Use quotes if it contains spaces.
2025-06-03 17:50:43 +00:00
arg:str:signature:The signature, base64-encoded.
2025-03-18 11:15:16 +01:00
"""
2025-06-03 17:50:43 +00:00
try :
sig = base64 . b64decode ( signature , validate = True )
except binascii . Error :
return False
2017-12-04 17:36:57 +01:00
message = util . to_bytes ( message )
2024-06-17 11:20:37 +02:00
return bitcoin . verify_usermessage_with_address ( address , sig , message )
2013-02-26 13:56:48 +01:00
2025-11-19 16:54:33 +01:00
def _get_fee_policy ( self , fee : str , feerate : str ) :
2025-02-24 12:20:44 +01:00
if fee is not None and feerate is not None :
raise Exception ( ' Cannot set both fee and feerate ' )
if fee is not None :
fee_sats = satoshis ( fee )
fee_policy = FeePolicy ( f ' fixed: { fee_sats } ' )
elif feerate is not None :
2025-11-19 16:54:33 +01:00
sat_per_kvbyte = int ( 1000 * to_decimal ( feerate ) )
fee_policy = FeePolicy ( f ' feerate: { sat_per_kvbyte } ' )
2025-02-24 12:20:44 +01:00
else :
fee_policy = FeePolicy ( self . config . FEE_POLICY )
return fee_policy
2015-12-23 15:59:32 +01:00
@command ( ' wp ' )
2019-09-21 20:07:16 +00:00
async def payto ( self , destination , amount , fee = None , feerate = None , from_addr = None , from_coins = None , change_addr = None ,
2025-12-05 16:21:38 +00:00
unsigned = False , rbf = True , password = None , locktime = None , addtransaction = False , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
""" Create an on-chain transaction.
arg:str:destination:Bitcoin address, contact or alias
arg:decimal_or_max:amount:Amount to be sent (in BTC). Type ' ! ' to send the maximum available.
arg:decimal:fee:Transaction fee (absolute, in BTC)
2025-11-19 16:54:33 +01:00
arg:decimal:feerate:Transaction fee rate (in sat/vbyte)
2025-03-18 11:15:16 +01:00
arg:str:from_addr:Source address (must be a wallet address; use sweep to spend from non-wallet address)
arg:str:change_addr:Change address. Default is a spare address, or the source address if it ' s not in the wallet
arg:bool:rbf:Whether to signal opt-in Replace-By-Fee in the transaction (true/false)
arg:bool:addtransaction:Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet
arg:int:locktime:Set locktime block number
arg:bool:unsigned:Do not sign transaction
arg:json:from_coins:Source coins (must be in wallet; use sweep to spend from non-wallet address)
"""
2025-03-14 17:30:15 +00:00
return await self . paytomany (
outputs = [ ( destination , amount ) , ] ,
fee = fee ,
feerate = feerate ,
from_addr = from_addr ,
from_coins = from_coins ,
2020-05-21 10:51:08 +02:00
change_addr = change_addr ,
2025-03-14 17:30:15 +00:00
unsigned = unsigned ,
2020-05-21 10:51:08 +02:00
rbf = rbf ,
2025-03-14 17:30:15 +00:00
password = password ,
2025-03-14 17:03:49 +00:00
locktime = locktime ,
2025-03-14 17:30:15 +00:00
addtransaction = addtransaction ,
wallet = wallet ,
2025-03-14 17:03:49 +00:00
)
2015-05-30 18:49:58 +02:00
2015-12-23 15:59:32 +01:00
@command ( ' wp ' )
2019-09-21 20:07:16 +00:00
async def paytomany ( self , outputs , fee = None , feerate = None , from_addr = None , from_coins = None , change_addr = None ,
2025-12-05 16:21:38 +00:00
unsigned = False , rbf = True , password = None , locktime = None , addtransaction = False , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
""" Create a multi-output transaction.
arg:json:outputs:json list of [ " address " , " amount in BTC " ]
arg:bool:rbf:Whether to signal opt-in Replace-By-Fee in the transaction (true/false)
2025-11-19 16:54:33 +01:00
arg:decimal:fee:Transaction fee (absolute, in BTC)
arg:decimal:feerate:Transaction fee rate (in sat/vbyte)
2025-03-18 11:15:16 +01:00
arg:str:from_addr:Source address (must be a wallet address; use sweep to spend from non-wallet address)
arg:str:change_addr:Change address. Default is a spare address, or the source address if it ' s not in the wallet
arg:bool:addtransaction:Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet
arg:int:locktime:Set locktime block number
arg:bool:unsigned:Do not sign transaction
arg:json:from_coins:Source coins (must be in wallet; use sweep to spend from non-wallet address)
"""
2025-02-24 12:20:44 +01:00
fee_policy = self . _get_fee_policy ( fee , feerate )
2019-09-21 20:07:16 +00:00
domain_addr = from_addr . split ( ' , ' ) if from_addr else None
domain_coins = from_coins . split ( ' , ' ) if from_coins else None
2025-05-15 14:31:00 +02:00
change_addr = await self . _resolver ( change_addr , wallet )
if domain_addr is not None :
resolvers = [ self . _resolver ( addr , wallet ) for addr in domain_addr ]
domain_addr = await asyncio . gather ( * resolvers )
2020-05-21 10:51:08 +02:00
final_outputs = [ ]
for address , amount in outputs :
2025-05-15 14:31:00 +02:00
address = await self . _resolver ( address , wallet )
2021-04-16 16:03:25 +02:00
amount_sat = satoshis_or_max ( amount )
2020-05-21 10:51:08 +02:00
final_outputs . append ( PartialTxOutput . from_address_and_value ( address , amount_sat ) )
2025-03-14 17:14:59 +00:00
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 ) ]
2025-03-14 17:19:41 +00:00
tx = wallet . make_unsigned_transaction (
outputs = final_outputs ,
2025-02-24 12:20:44 +01:00
fee_policy = fee_policy ,
2020-05-21 10:51:08 +02:00
change_addr = change_addr ,
2025-03-14 17:14:59 +00:00
coins = coins ,
2020-05-21 10:51:08 +02:00
rbf = rbf ,
2025-03-14 17:03:49 +00:00
locktime = locktime ,
)
2025-03-14 16:44:46 +00:00
if not unsigned :
wallet . sign_transaction ( tx , password )
2020-09-01 22:25:36 +03:00
result = tx . serialize ( )
if addtransaction :
await self . addtransaction ( result , wallet = wallet )
return result
2013-04-08 23:40:51 +01:00
2025-08-13 15:09:04 +00:00
def get_year_timestamps ( self , year : int ) - > dict [ str , Any ] :
2025-02-16 16:59:19 +01:00
kwargs = { }
2018-02-09 15:28:28 +01:00
if year :
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 ( ) )
2025-02-16 16:59:19 +01:00
return kwargs
2021-02-19 12:45:45 +02:00
2025-02-16 16:59:19 +01:00
@command ( ' w ' )
async def onchain_capital_gains ( self , year = None , wallet : Abstract_Wallet = None ) :
"""
Capital gains, using utxo pricing.
This cannot be used with lightning.
2025-03-18 11:15:16 +01:00
arg:int:year:Show cap gains for a given year
2025-02-16 16:59:19 +01:00
"""
kwargs = self . get_year_timestamps ( year )
from . exchange_rate import FxThread
fx = self . daemon . fx if self . daemon else FxThread ( config = self . config )
return json_normalize ( wallet . get_onchain_capital_gains ( fx , * * kwargs ) )
2019-06-14 13:01:23 +02:00
2021-08-11 17:00:26 +05:30
@command ( ' wp ' )
2022-09-26 14:40:28 +02:00
async def bumpfee ( self , tx , new_fee_rate , from_coins = None , decrease_payment = False , password = None , unsigned = False , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Bump the fee for an unconfirmed transaction.
2023-09-07 13:18:23 +00:00
' tx ' can be either a raw hex tx or a txid. If txid, the corresponding tx must already be part of the wallet history.
2025-03-18 11:15:16 +01:00
arg:str:tx:Serialized transaction (hexadecimal)
arg:str:new_fee_rate: The Updated/Increased Transaction fee rate (in sats/vbyte)
arg:bool:decrease_payment:Whether payment amount will be decreased (true/false)
arg:bool:unsigned:Do not sign transaction
arg:json:from_coins:Coins that may be used to inncrease the fee (must be in wallet)
2023-09-07 13:18:23 +00:00
"""
if is_hash256_str ( tx ) : # txid
tx = wallet . db . get_transaction ( tx )
if tx is None :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( " Transaction not in wallet. " )
2023-09-07 13:18:23 +00:00
else : # raw tx
try :
tx = Transaction ( tx )
tx . deserialize ( )
except transaction . SerializationError as e :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( f " Failed to deserialize transaction: { e } " ) from e
2021-08-11 17:00:26 +05:30
domain_coins = from_coins . split ( ' , ' ) if from_coins else None
coins = wallet . get_spendable_coins ( None )
if domain_coins is not None :
coins = [ coin for coin in coins if ( coin . prevout . to_str ( ) in domain_coins ) ]
2023-03-10 21:17:05 +00:00
tx . add_info_from_wallet ( wallet )
await tx . add_info_from_network ( self . network )
2021-08-11 17:00:26 +05:30
new_tx = wallet . bump_fee (
tx = tx ,
coins = coins ,
2023-11-20 18:00:23 +00:00
strategy = BumpFeeStrategy . DECREASE_PAYMENT if decrease_payment else BumpFeeStrategy . PRESERVE_PAYMENT ,
2021-08-11 17:00:26 +05:30
new_fee_rate = new_fee_rate )
if not unsigned :
wallet . sign_transaction ( new_tx , password )
return new_tx . serialize ( )
2025-02-16 16:59:19 +01:00
@command ( ' w ' )
2025-08-13 15:09:04 +00:00
async def onchain_history (
self , show_fiat = False , year = None , show_addresses = False ,
from_height = None , to_height = None ,
wallet : Abstract_Wallet = None ,
) :
2025-03-18 11:15:16 +01:00
""" Wallet onchain history. Returns the transaction history of your wallet.
arg:bool:show_addresses:Show input and output addresses
arg:bool:show_fiat:Show fiat value of transactions
arg:int:year:Show history for a given year
2025-08-13 15:09:04 +00:00
arg:int:from_height:Only show transactions that confirmed after(inclusive) given block height
arg:int:to_height:Only show transactions that confirmed before(exclusive) given block height
2025-03-18 11:15:16 +01:00
"""
2025-05-06 11:59:53 +02:00
# trigger lnwatcher callbacks for their side effects: setting labels and accounting_addresses
2025-05-19 13:20:25 +02:00
if not self . network and wallet . lnworker :
await wallet . lnworker . lnwatcher . trigger_callbacks ( requires_synchronizer = False )
2025-05-06 11:59:53 +02:00
2025-02-16 16:59:19 +01:00
kwargs = self . get_year_timestamps ( year )
2025-08-13 15:09:04 +00:00
kwargs [ ' from_height ' ] = from_height
kwargs [ ' to_height ' ] = to_height
2025-02-16 16:59:19 +01:00
onchain_history = wallet . get_onchain_history ( * * kwargs )
out = [ x . to_dict ( ) for x in onchain_history . values ( ) ]
if show_fiat :
from . exchange_rate import FxThread
fx = self . daemon . fx if self . daemon else FxThread ( config = self . config )
else :
fx = None
for item in out :
if show_addresses :
tx = wallet . db . get_transaction ( item [ ' txid ' ] )
item [ ' inputs ' ] = list ( map ( lambda x : x . to_json ( ) , tx . inputs ( ) ) )
item [ ' outputs ' ] = list ( map ( lambda x : { ' address ' : x . get_ui_address_str ( ) , ' value_sat ' : x . value } ,
tx . outputs ( ) ) )
if fx :
fiat_fields = wallet . get_tx_item_fiat ( tx_hash = item [ ' txid ' ] , amount_sat = item [ ' amount_sat ' ] , fx = fx , tx_fee = item [ ' fee_sat ' ] )
item . update ( fiat_fields )
return json_normalize ( out )
2021-07-07 16:44:27 +03:00
@command ( ' wl ' )
2025-02-16 16:59:19 +01:00
async def lightning_history ( self , wallet : Abstract_Wallet = None ) :
""" lightning history. """
lightning_history = wallet . lnworker . get_lightning_history ( ) if wallet . lnworker else { }
sorted_hist = sorted ( lightning_history . values ( ) , key = lambda x : x . timestamp )
return json_normalize ( [ x . to_dict ( ) for x in sorted_hist ] )
2013-02-26 18:10:29 +01:00
2015-06-01 06:10:06 +02:00
@command ( ' w ' )
2019-09-21 02:14:22 +02:00
async def setlabel ( self , key , label , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Assign a label to an item. Item may be a bitcoin address or a
transaction ID
arg:str:key:Key
arg:str:label:Label
"""
2019-09-05 17:57:51 +02:00
wallet . set_label ( key , label )
2013-09-08 20:10:43 +02:00
2017-03-06 17:12:27 +01:00
@command ( ' w ' )
2019-09-21 02:14:22 +02:00
async def listcontacts ( self , wallet : Abstract_Wallet = None ) :
2015-05-31 23:17:44 +02:00
""" Show your list of contacts """
2019-09-05 17:57:51 +02:00
return wallet . contacts
2013-02-26 13:56:48 +01:00
2017-03-06 17:12:27 +01:00
@command ( ' w ' )
2025-05-29 16:44:01 +02:00
async def getopenalias ( self , key , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Retrieve alias. Lookup in your list of contacts, and for an OpenAlias DNS record.
arg:str:key:the alias to be retrieved
"""
2025-12-05 16:21:38 +00:00
d = await wallet . contacts . resolve ( key )
if d . get ( " type " ) == " openalias " :
# we always validate DNSSEC now
d [ " validated " ] = True
return d
2015-05-31 15:06:52 +02:00
2017-03-06 17:12:27 +01:00
@command ( ' w ' )
2019-09-21 02:14:22 +02:00
async def searchcontacts ( self , query , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Search through your wallet contacts, return matching entries.
arg:str:query:Search query
"""
2014-11-05 18:02:44 -05:00
results = { }
2019-09-05 17:57:51 +02:00
for key , value in wallet . contacts . items ( ) :
2015-05-30 12:35:58 +02:00
if query . lower ( ) in key . lower ( ) :
results [ key ] = value
2014-11-05 18:02:44 -05:00
return results
2015-06-01 06:10:06 +02:00
@command ( ' w ' )
2019-09-21 02:14:22 +02:00
async def listaddresses ( self , receiving = False , change = False , labels = False , frozen = False , unused = False , funded = False , balance = False , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
""" List wallet addresses. Returns the list of all addresses in your wallet. Use optional arguments to filter the results.
arg:bool:receiving:Show only receiving addresses
arg:bool:change:Show only change addresses
arg:bool:frozen:Show only frozen addresses
arg:bool:unused:Show only unused addresses
arg:bool:funded:Show only funded addresses
arg:bool:balance:Show the balances of listed addresses
arg:bool:labels:Show the labels of listed addresses
"""
2013-02-26 18:10:29 +01:00
out = [ ]
2019-09-05 17:57:51 +02:00
for addr in wallet . get_addresses ( ) :
if frozen and not wallet . is_frozen_address ( addr ) :
2015-05-30 18:49:58 +02:00
continue
2019-09-05 17:57:51 +02:00
if receiving and wallet . is_change ( addr ) :
2015-06-11 12:49:14 +02:00
continue
2019-09-05 17:57:51 +02:00
if change and not wallet . is_change ( addr ) :
2015-05-30 18:49:58 +02:00
continue
2022-06-01 23:03:35 +02:00
if unused and wallet . adb . is_used ( addr ) :
2015-05-30 18:49:58 +02:00
continue
2022-06-01 23:03:35 +02:00
if funded and wallet . adb . is_empty ( addr ) :
2015-05-30 18:49:58 +02:00
continue
item = addr
2017-10-07 09:48:20 +02:00
if labels or balance :
item = ( item , )
if balance :
2025-08-21 17:38:27 +00:00
item + = ( format_satoshis ( sum ( wallet . get_addr_balance ( addr ) ) ) , )
2017-10-07 09:48:20 +02:00
if labels :
2022-08-09 17:13:44 +02:00
item + = ( repr ( wallet . get_label_for_address ( addr ) ) , )
2015-05-30 18:49:58 +02:00
out . append ( item )
2013-02-26 18:10:29 +01:00
return out
2014-06-24 16:12:43 +03:00
2017-03-12 12:33:52 +01:00
@command ( ' n ' )
2019-09-21 02:14:22 +02:00
async def gettransaction ( self , txid , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
""" Retrieve a transaction.
arg:txid:txid:Transaction ID
"""
2019-02-28 20:21:53 +01:00
tx = None
2019-09-05 17:57:51 +02:00
if wallet :
tx = wallet . db . get_transaction ( txid )
2019-02-28 20:21:53 +01:00
if tx is None :
2019-08-15 13:17:16 +02:00
raw = await self . network . get_transaction ( txid )
2015-05-31 08:20:09 +02:00
if raw :
tx = Transaction ( raw )
else :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( " Unknown transaction " )
2019-11-02 05:46:52 +01:00
if tx . txid ( ) != txid :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( " Mismatching txid " )
2019-10-23 17:09:41 +02:00
return tx . serialize ( )
2014-01-23 17:06:47 +01:00
2015-06-01 06:10:06 +02:00
@command ( ' ' )
2019-08-15 13:17:16 +02:00
async def encrypt ( self , pubkey , message ) - > str :
2025-03-18 11:15:16 +01:00
"""
Encrypt a message with a public key. Use quotes if the message contains whitespaces.
arg:str:pubkey:Public key
arg:str:message:Clear text message. Use quotes if it contains spaces.
"""
2019-05-03 03:10:31 +02:00
if not is_hex_str ( pubkey ) :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( f " pubkey must be a hex string instead of { repr ( pubkey ) } " )
2019-05-03 03:10:31 +02:00
try :
message = to_bytes ( message )
except TypeError :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( f " message must be a string-like object instead of { repr ( message ) } " )
2018-05-24 18:57:13 +02:00
public_key = ecc . ECPubkey ( bfh ( pubkey ) )
2024-06-17 11:20:37 +02:00
encrypted = crypto . ecies_encrypt_message ( public_key , message )
2019-05-03 03:10:31 +02:00
return encrypted . decode ( ' utf-8 ' )
2014-03-03 10:39:10 +01:00
2015-06-01 06:10:06 +02:00
@command ( ' wp ' )
2019-09-21 02:14:22 +02:00
async def decrypt ( self , pubkey , encrypted , password = None , wallet : Abstract_Wallet = None ) - > str :
2025-03-18 11:15:16 +01:00
""" Decrypt a message encrypted with a public key.
arg:str:encrypted:Encrypted message
arg:str:pubkey:Public key of one of your wallet addresses
"""
2019-05-03 03:10:31 +02:00
if not is_hex_str ( pubkey ) :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( f " pubkey must be a hex string instead of { repr ( pubkey ) } " )
2019-05-03 03:10:31 +02:00
if not isinstance ( encrypted , ( str , bytes , bytearray ) ) :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( f " encrypted must be a string-like object instead of { repr ( encrypted ) } " )
2019-09-05 17:57:51 +02:00
decrypted = wallet . decrypt_message ( pubkey , encrypted , password )
2019-05-03 03:10:31 +02:00
return decrypted . decode ( ' utf-8 ' )
2015-05-31 22:42:34 +02:00
2015-12-23 15:59:32 +01:00
@command ( ' w ' )
2022-08-15 14:14:25 +02:00
async def get_request ( self , request_id , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
""" Returns a payment request
arg:str:request_id:The request ID, as seen in list_requests or add_request
"""
2022-08-15 14:14:25 +02:00
r = wallet . get_request ( request_id )
2015-06-07 18:44:33 +02:00
if not r :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( " Request not found " )
2020-05-31 12:49:49 +02:00
return wallet . export_request ( r )
2015-06-07 18:44:33 +02:00
2022-08-15 14:14:25 +02:00
@command ( ' w ' )
async def get_invoice ( self , invoice_id , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Returns an invoice (request for outgoing payment)
arg:str:invoice_id:The invoice ID, as seen in list_invoices
"""
2022-08-15 14:14:25 +02:00
r = wallet . get_invoice ( invoice_id )
if not r :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( " Request not found " )
2022-08-15 14:14:25 +02:00
return wallet . export_invoice ( r )
def _filter_invoices ( self , _list , wallet , pending , expired , paid ) :
2015-06-12 09:46:21 +02:00
if pending :
f = PR_UNPAID
elif expired :
f = PR_EXPIRED
elif paid :
f = PR_PAID
else :
f = None
2015-06-12 20:15:53 +02:00
if f is not None :
2022-08-15 14:14:25 +02:00
_list = [ x for x in _list if f == wallet . get_invoice_status ( x ) ]
return _list
@command ( ' w ' )
async def list_requests ( self , pending = False , expired = False , paid = False , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Returns the list of incoming payment requests saved in the wallet.
arg:bool:paid:Show only paid requests
arg:bool:pending:Show only pending requests
arg:bool:expired:Show only expired requests
"""
2022-08-15 14:14:25 +02:00
l = wallet . get_sorted_requests ( )
l = self . _filter_invoices ( l , wallet , pending , expired , paid )
return [ wallet . export_request ( x ) for x in l ]
@command ( ' w ' )
async def list_invoices ( self , pending = False , expired = False , paid = False , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Returns the list of invoices (requests for outgoing payments) saved in the wallet.
arg:bool:paid:Show only paid invoices
arg:bool:pending:Show only pending invoices
arg:bool:expired:Show only expired invoices
"""
2022-08-15 14:14:25 +02:00
l = wallet . get_invoices ( )
l = self . _filter_invoices ( l , wallet , pending , expired , paid )
return [ wallet . export_invoice ( x ) for x in l ]
2015-05-31 22:42:34 +02:00
2017-01-21 08:04:37 +01:00
@command ( ' w ' )
2019-09-21 02:14:22 +02:00
async def createnewaddress ( self , wallet : Abstract_Wallet = None ) :
2017-10-11 12:07:41 +02:00
""" Create a new receiving address, beyond the gap limit of the wallet """
2019-09-05 17:57:51 +02:00
return wallet . create_new_address ( False )
2017-01-21 08:04:37 +01:00
2020-03-02 19:07:59 +01:00
@command ( ' w ' )
async def changegaplimit ( self , new_limit , iknowwhatimdoing = False , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Change the gap limit of the wallet.
arg:int:new_limit:new gap limit
arg:bool:iknowwhatimdoing:Acknowledge that I understand the full implications of what I am about to do
"""
2020-03-02 19:07:59 +01:00
if not iknowwhatimdoing :
2024-02-12 19:02:02 +00:00
raise UserFacingException (
" 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. " )
2020-03-02 19:07:59 +01:00
if not isinstance ( wallet , Deterministic_Wallet ) :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( " This wallet is not deterministic. " )
2020-03-02 19:07:59 +01:00
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 ) :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( " This wallet is not deterministic. " )
2020-03-02 19:07:59 +01:00
if not wallet . is_up_to_date ( ) :
2020-04-08 03:09:08 +00:00
raise NotSynchronizedException ( " Wallet not fully synchronized. " )
2020-03-02 19:07:59 +01:00
return wallet . min_acceptable_gap ( )
2017-10-11 12:07:41 +02:00
@command ( ' w ' )
2019-09-21 02:14:22 +02:00
async def getunusedaddress ( self , wallet : Abstract_Wallet = None ) :
2017-10-11 12:07:41 +02:00
""" 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. """
2019-09-05 17:57:51 +02:00
return wallet . get_unused_address ( )
2017-01-21 08:04:37 +01:00
2015-06-01 13:02:09 +02:00
@command ( ' w ' )
2025-02-23 12:21:41 +01:00
async def add_request ( self , amount , memo = ' ' , expiry = 3600 , lightning = False , force = False , wallet : Abstract_Wallet = None ) :
2017-10-11 12:07:41 +02:00
""" Create a payment request, using the first unused address of the wallet.
2025-03-18 11:15:16 +01:00
2018-04-15 20:45:30 +03:00
The address will be considered as used after this operation.
2025-03-18 11:15:16 +01:00
If no payment is received, the address will be considered as unused if the payment request is deleted from the wallet.
arg:decimal:amount:Requested amount (in btc)
arg:str:memo:Description of the request
arg:bool:force:Create new address beyond gap limit, if no more addresses are available.
arg:bool:lightning:Create lightning request.
arg:int:expiry:Time in seconds.
"""
2017-02-22 11:23:12 +01:00
amount = satoshis ( amount )
2025-02-23 12:21:41 +01:00
if not lightning :
addr = wallet . get_unused_address ( )
if addr is None :
if force :
addr = wallet . create_new_address ( False )
else :
return False
else :
addr = None
2023-03-31 14:25:40 +02:00
expiry = int ( expiry ) if expiry else None
key = wallet . create_request ( amount , memo , expiry , addr )
2022-03-29 14:07:53 +02:00
req = wallet . get_request ( key )
2020-05-31 12:49:49 +02:00
return wallet . export_request ( req )
2015-06-01 13:02:09 +02:00
2025-03-31 13:08:09 +02:00
@command ( ' wnl ' )
async def add_hold_invoice (
self ,
2025-06-29 19:57:17 +02:00
payment_hash : str ,
2025-03-31 13:08:09 +02:00
amount : Optional [ Decimal ] = None ,
memo : str = " " ,
expiry : int = 3600 ,
2025-09-26 16:11:20 +02:00
min_final_cltv_expiry_delta : int = MIN_FINAL_CLTV_DELTA_ACCEPTED * 2 ,
2025-03-31 13:08:09 +02:00
wallet : Abstract_Wallet = None
2025-06-03 11:26:23 +02:00
) - > dict :
2025-03-31 13:08:09 +02:00
"""
2025-06-29 19:57:17 +02:00
Create a lightning hold invoice for the given payment hash. Hold invoices have to get settled manually later.
2025-11-24 13:02:42 +01:00
HTLCs will get failed automatically if block_height + 144 > htlc.cltv_abs, if the intention is to
settle them as late as possible a safety margin of some blocks should be used to prevent them
from getting failed accidentally.
2025-03-31 13:08:09 +02:00
2025-06-29 19:57:17 +02:00
arg:str:payment_hash:Hex encoded payment hash to be used for the invoice
2025-03-31 13:08:09 +02:00
arg:decimal:amount:Optional requested amount (in btc)
arg:str:memo:Optional description of the invoice
arg:int:expiry:Optional expiry in seconds (default: 3600s)
arg:int:min_final_cltv_expiry_delta:Optional min final cltv expiry delta (default: 294 blocks)
"""
2025-06-29 19:57:17 +02:00
assert len ( payment_hash ) == 64 , f " Invalid payment hash length: { len ( payment_hash ) } != 64 "
2025-11-28 16:22:22 +01:00
assert not wallet . lnworker . get_payment_info ( bfh ( payment_hash ) , direction = RECEIVED ) , " Payment hash already used! "
2025-11-24 13:02:42 +01:00
assert payment_hash not in wallet . lnworker . dont_expire_htlcs , " Payment hash already used! "
2025-06-29 19:57:17 +02:00
assert wallet . lnworker . get_preimage ( bfh ( payment_hash ) ) is None , " Already got a preimage for this payment hash! "
2025-09-26 16:11:20 +02:00
assert MIN_FINAL_CLTV_DELTA_ACCEPTED < min_final_cltv_expiry_delta < 576 , " Use a sane min_final_cltv_expiry_delta value "
2025-03-31 13:08:09 +02:00
amount = amount if amount and satoshis ( amount ) > 0 else None # make amount either >0 or None
inbound_capacity = wallet . lnworker . num_sats_can_receive ( )
assert inbound_capacity > satoshis ( amount or 0 ) , \
f " Not enough inbound capacity [ { inbound_capacity } sat] to receive this payment "
2025-09-26 16:11:20 +02:00
wallet . lnworker . add_payment_info_for_hold_invoice (
bfh ( payment_hash ) ,
lightning_amount_sat = satoshis ( amount ) if amount else None ,
min_final_cltv_delta = min_final_cltv_expiry_delta ,
exp_delay = expiry ,
)
2025-11-28 16:22:22 +01:00
info = wallet . lnworker . get_payment_info ( bfh ( payment_hash ) , direction = RECEIVED )
2025-03-31 13:08:09 +02:00
lnaddr , invoice = wallet . lnworker . get_bolt11_invoice (
2025-09-26 16:11:20 +02:00
payment_info = info ,
2025-03-31 13:08:09 +02:00
message = memo ,
fallback_address = None
)
2025-11-24 13:02:42 +01:00
# this prevents incoming htlcs from getting expired while the preimage isn't set.
# If their blocks to expiry fall below MIN_FINAL_CLTV_DELTA_ACCEPTED they will get failed.
wallet . lnworker . dont_expire_htlcs [ payment_hash ] = MIN_FINAL_CLTV_DELTA_ACCEPTED
2025-03-31 13:08:09 +02:00
wallet . set_label ( payment_hash , memo )
result = {
" invoice " : invoice
}
return result
@command ( ' wnl ' )
2025-06-29 19:57:17 +02:00
async def settle_hold_invoice ( self , preimage : str , wallet : Abstract_Wallet = None ) - > dict :
2025-03-31 13:08:09 +02:00
"""
2025-06-29 19:57:17 +02:00
Settles lightning hold invoice with the given preimage.
2025-03-31 13:08:09 +02:00
Doesn ' t block until actual settlement of the HTLCs.
2025-06-29 19:57:17 +02:00
arg:str:preimage:Hex encoded preimage of the invoice to be settled
2025-03-31 13:08:09 +02:00
"""
2025-06-29 19:57:17 +02:00
assert len ( preimage ) == 64 , f " Invalid payment_hash length: { len ( preimage ) } != 64 "
payment_hash : str = crypto . sha256 ( bfh ( preimage ) ) . hex ( )
assert payment_hash not in wallet . lnworker . _preimages , f " Invoice { payment_hash =} already settled "
2025-11-28 16:22:22 +01:00
info = wallet . lnworker . get_payment_info ( bfh ( payment_hash ) , direction = RECEIVED )
assert info , f " Couldn ' t find lightning invoice for { payment_hash =} "
2025-11-24 13:02:42 +01:00
assert payment_hash in wallet . lnworker . dont_expire_htlcs , f " Invoice { payment_hash =} not a hold invoice? "
2025-09-16 10:52:26 +02:00
assert wallet . lnworker . is_complete_mpp ( bfh ( payment_hash ) ) , \
2025-03-31 13:08:09 +02:00
f " MPP incomplete, cannot settle hold invoice { payment_hash } yet "
2025-07-20 12:46:11 +02:00
assert ( wallet . lnworker . get_payment_mpp_amount_msat ( bfh ( payment_hash ) ) or 0 ) > = ( info . amount_msat or 0 )
2025-06-29 19:57:17 +02:00
wallet . lnworker . save_preimage ( bfh ( payment_hash ) , bfh ( preimage ) )
2025-03-31 13:08:09 +02:00
util . trigger_callback ( ' wallet_updated ' , wallet )
result = {
" settled " : payment_hash
}
return result
@command ( ' wnl ' )
async def cancel_hold_invoice ( self , payment_hash : str , wallet : Abstract_Wallet = None ) - > dict :
"""
Cancels lightning hold invoice ' payment_hash ' .
arg:str:payment_hash:Payment hash in hex of the hold invoice
"""
2025-11-28 16:22:22 +01:00
assert wallet . lnworker . get_payment_info ( bfh ( payment_hash ) , direction = RECEIVED ) , \
2025-03-31 13:08:09 +02:00
f " Couldn ' t find lightning invoice for payment hash { payment_hash } "
2025-06-29 19:57:17 +02:00
assert payment_hash not in wallet . lnworker . _preimages , " Cannot cancel anymore, preimage already given. "
2025-11-24 13:02:42 +01:00
assert payment_hash in wallet . lnworker . dont_expire_htlcs , f " { payment_hash =} not a hold invoice? "
2025-03-31 13:08:09 +02:00
# set to PR_UNPAID so it can get deleted
2025-11-28 16:22:22 +01:00
wallet . lnworker . set_payment_status ( bfh ( payment_hash ) , PR_UNPAID , direction = RECEIVED )
wallet . lnworker . delete_payment_info ( payment_hash , direction = RECEIVED )
2025-03-31 13:08:09 +02:00
wallet . set_label ( payment_hash , None )
2025-11-24 13:02:42 +01:00
del wallet . lnworker . dont_expire_htlcs [ payment_hash ]
2025-09-16 10:52:26 +02:00
while wallet . lnworker . is_complete_mpp ( bfh ( payment_hash ) ) :
2025-11-24 13:02:42 +01:00
# block until the htlcs got failed
2025-03-31 13:08:09 +02:00
await asyncio . sleep ( 0.1 )
result = {
" cancelled " : payment_hash
}
return result
@command ( ' wnl ' )
async def check_hold_invoice ( self , payment_hash : str , wallet : Abstract_Wallet = None ) - > dict :
"""
Checks the status of a lightning hold invoice ' payment_hash ' .
2025-07-25 11:44:26 +02:00
Returns: {
" status " : unpaid | paid | settled | unknown (cancelled or not found),
" received_amount_sat " : currently received amount (pending htlcs or final after settling),
" invoice_amount_sat " : Invoice amount, Optional (only if invoice is found),
" closest_htlc_expiry_height " : Closest absolute expiry height of all received htlcs
(Note: HTLCs will get failed automatically if block_height + 144 > htlc_expiry_height)
}
2025-03-31 13:08:09 +02:00
arg:str:payment_hash:Payment hash in hex of the hold invoice
"""
assert len ( payment_hash ) == 64 , f " Invalid payment_hash length: { len ( payment_hash ) } != 64 "
2025-11-28 16:22:22 +01:00
info : Optional [ ' PaymentInfo ' ] = wallet . lnworker . get_payment_info ( bfh ( payment_hash ) , direction = RECEIVED )
2025-09-16 10:52:26 +02:00
is_complete_mpp : bool = wallet . lnworker . is_complete_mpp ( bfh ( payment_hash ) )
2025-03-31 13:08:09 +02:00
amount_sat = ( wallet . lnworker . get_payment_mpp_amount_msat ( bfh ( payment_hash ) ) or 0 ) / / 1000
2025-07-25 11:44:26 +02:00
result = {
" status " : " unknown " ,
" received_amount_sat " : amount_sat ,
}
2025-03-31 13:08:09 +02:00
if info is None :
pass
2025-09-16 10:52:26 +02:00
elif not is_complete_mpp and not wallet . lnworker . get_preimage_hex ( payment_hash ) :
# is_complete_mpp is False for settled payments
2025-07-25 11:44:26 +02:00
result [ " status " ] = " unpaid "
2025-11-24 13:02:42 +01:00
elif is_complete_mpp and payment_hash in wallet . lnworker . dont_expire_htlcs :
2025-07-25 11:44:26 +02:00
result [ " status " ] = " paid "
payment_key : str = wallet . lnworker . _get_payment_key ( bfh ( payment_hash ) ) . hex ( )
htlc_status = wallet . lnworker . received_mpp_htlcs [ payment_key ]
result [ " closest_htlc_expiry_height " ] = min (
2025-09-09 13:10:17 +02:00
mpp_htlc . htlc . cltv_abs for mpp_htlc in htlc_status . htlcs
2025-07-25 11:44:26 +02:00
)
2025-11-24 13:02:42 +01:00
elif wallet . lnworker . get_preimage_hex ( payment_hash ) is not None :
2025-07-25 11:44:26 +02:00
result [ " status " ] = " settled "
2025-07-20 12:46:11 +02:00
plist = wallet . lnworker . get_payments ( status = ' settled ' ) [ bfh ( payment_hash ) ]
2025-11-28 16:22:22 +01:00
_dir , amount_msat , _fee , _ts = wallet . lnworker . get_payment_value ( None , plist )
2025-07-25 11:44:26 +02:00
result [ " received_amount_sat " ] = amount_msat / / 1000
2025-09-29 18:06:12 +02:00
result [ ' preimage ' ] = wallet . lnworker . get_preimage_hex ( payment_hash )
2025-07-20 17:47:33 +02:00
if info is not None :
result [ " invoice_amount_sat " ] = ( info . amount_msat or 0 ) / / 1000
2025-03-31 13:08:09 +02:00
return result
2025-09-29 18:06:12 +02:00
@command ( ' wl ' )
async def export_lightning_preimage ( self , payment_hash : str , wallet : ' Abstract_Wallet ' = None ) - > Optional [ str ] :
"""
Returns the stored preimage of the given payment_hash if it is known.
2026-04-28 15:16:42 +00:00
note: Exporting a preimage does not require the wallet password (RPC access is enough).
We don ' t consider preimages as sensitive as private keys.
2025-09-29 18:06:12 +02:00
arg:str:payment_hash: Hash of the preimage
"""
preimage = wallet . lnworker . get_preimage_hex ( payment_hash )
assert preimage is None or crypto . sha256 ( bytes . fromhex ( preimage ) ) . hex ( ) == payment_hash
return preimage
2018-01-18 11:56:21 +01:00
@command ( ' w ' )
2019-09-21 02:14:22 +02:00
async def addtransaction ( self , tx , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Add a transaction to the wallet history, without broadcasting it.
arg:tx:tx:Transaction, in hexadecimal format.
"""
2018-01-18 11:56:21 +01:00
tx = Transaction ( tx )
2022-06-01 23:03:35 +02:00
if not wallet . adb . add_transaction ( tx ) :
2018-02-06 05:39:26 +01:00
return False
2020-02-05 15:13:37 +01:00
wallet . save_db ( )
2018-01-18 11:56:21 +01:00
return tx . txid ( )
2015-06-01 13:02:09 +02:00
@command ( ' w ' )
2022-08-15 14:14:25 +02:00
async def delete_request ( self , request_id , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
""" Remove an incoming payment request
arg:str:request_id:The request ID, as returned in list_invoices
"""
2022-08-15 14:14:25 +02:00
return wallet . delete_request ( request_id )
@command ( ' w ' )
async def delete_invoice ( self , invoice_id , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
""" Remove an outgoing payment invoice
arg:str:invoice_id:The invoice ID, as returned in list_invoices
"""
2022-08-15 14:14:25 +02:00
return wallet . delete_invoice ( invoice_id )
2015-05-31 22:42:34 +02:00
2015-07-22 15:46:53 +02:00
@command ( ' w ' )
2019-09-21 02:14:22 +02:00
async def clear_requests ( self , wallet : Abstract_Wallet = None ) :
2015-07-22 15:46:53 +02:00
""" Remove all payment requests """
2020-06-26 09:47:16 +02:00
wallet . clear_requests ( )
return True
2015-07-22 15:46:53 +02:00
2019-09-08 11:59:03 +02:00
@command ( ' w ' )
2019-09-21 02:14:22 +02:00
async def clear_invoices ( self , wallet : Abstract_Wallet = None ) :
2019-09-08 11:59:03 +02:00
""" Remove all invoices """
wallet . clear_invoices ( )
return True
2015-11-30 10:54:15 +01:00
@command ( ' n ' )
2020-04-24 15:34:55 +02:00
async def notify ( self , address : str , URL : Optional [ str ] ) :
2025-03-18 11:15:16 +01:00
"""
Watch an address. Every time the address changes, a http POST is sent to the URL.
2020-04-24 15:34:55 +02:00
Call with an empty URL to stop watching an address.
2025-03-18 11:15:16 +01:00
arg:str:address:Bitcoin address
arg:str:URL:The callback URL
2020-04-24 15:34:55 +02:00
"""
2018-10-03 17:13:46 +02:00
if not hasattr ( self , " _notifier " ) :
self . _notifier = Notifier ( self . network )
2020-04-24 15:34:55 +02:00
if URL :
await self . _notifier . start_watching_addr ( address , URL )
else :
await self . _notifier . stop_watching_addr ( address )
2015-12-01 08:58:00 +01:00
return True
2015-07-22 15:46:53 +02:00
2016-04-08 16:14:39 +02:00
@command ( ' wn ' )
2019-09-21 02:14:22 +02:00
async def is_synchronized ( self , wallet : Abstract_Wallet = None ) :
2016-04-08 20:29:43 +02:00
""" return wallet synchronization status """
2019-09-05 17:57:51 +02:00
return wallet . is_up_to_date ( )
2016-04-08 16:14:39 +02:00
2025-05-13 16:34:06 +00:00
@command ( ' wn ' )
async def wait_for_sync ( self , wallet : Abstract_Wallet = None ) :
""" Block until the wallet synchronization finishes. """
while True :
if wallet . is_up_to_date ( ) :
return True
await wallet . up_to_date_changed_event . wait ( )
2025-02-24 12:20:44 +01:00
@command ( ' n ' )
2023-10-08 17:43:42 +02:00
async def getfeerate ( self ) :
2018-06-15 17:02:44 +02:00
"""
2025-02-24 12:20:44 +01:00
Return current fee estimate given network conditions (in sat/kvByte).
To change the fee policy, use ' getconfig/setconfig fee_policy '
"""
fee_policy = FeePolicy ( self . config . FEE_POLICY )
description = fee_policy . get_target_text ( )
feerate = fee_policy . fee_per_kb ( self . network )
tooltip = fee_policy . get_estimate_text ( self . network )
2023-10-08 17:43:42 +02:00
return {
2025-02-24 12:20:44 +01:00
' policy ' : fee_policy . get_descriptor ( ) ,
' description ' : description ,
2023-10-08 17:43:42 +02:00
' sat/kvB ' : feerate ,
' tooltip ' : tooltip ,
}
2025-06-28 04:45:24 +00:00
@command ( ' n ' )
async def test_inject_fee_etas ( self , fee_est ) :
"""
Inject fee estimates into the network object, as if they were coming from connected servers.
2025-10-31 13:31:10 +01:00
`setconfig ' test_disable_automatic_fee_eta_update ' true` to prevent Network from overriding
the configured fees.
2025-06-28 04:45:24 +00:00
Useful on regtest.
arg:str:fee_est:dict of ETA-based fee estimates, encoded as str
"""
if not isinstance ( fee_est , dict ) :
fee_est = ast . literal_eval ( fee_est )
assert isinstance ( fee_est , dict ) , f " unexpected type for fee_est. got { repr ( fee_est ) } "
# populate missing high-block-number estimates using default relay fee.
# e.g. {"25": 2222} -> {"25": 2222, "144": 1000, "1008": 1000}
furthest_estimate = max ( fee_est . keys ( ) ) if fee_est else 0
further_fee_est = {
eta_target : FEERATE_DEFAULT_RELAY for eta_target in FEE_ETA_TARGETS
if eta_target > furthest_estimate
}
fee_est . update ( further_fee_est )
self . network . update_fee_estimates ( fee_est = fee_est )
2019-02-20 18:01:43 +01:00
@command ( ' w ' )
2019-09-21 02:14:22 +02:00
async def removelocaltx ( self , txid , wallet : Abstract_Wallet = None ) :
2019-02-20 18:01:43 +01:00
""" Remove a ' local ' transaction from the wallet, and its dependent
transactions.
2025-03-18 11:15:16 +01:00
arg:txid:txid:Transaction ID
2019-02-20 18:01:43 +01:00
"""
2025-09-10 15:24:28 +00:00
height = wallet . adb . get_tx_height ( txid ) . height ( )
2019-02-20 18:01:43 +01:00
if height != TX_HEIGHT_LOCAL :
2024-02-12 19:02:02 +00:00
raise UserFacingException (
f ' Only local transactions can be removed. '
f ' This tx has height: { height } != { TX_HEIGHT_LOCAL } ' )
2022-06-01 23:03:35 +02:00
wallet . adb . remove_transaction ( txid )
2020-02-05 15:13:37 +01:00
wallet . save_db ( )
2019-02-20 18:01:43 +01:00
2019-03-05 17:01:54 +01:00
@command ( ' wn ' )
2019-09-21 02:14:22 +02:00
async def get_tx_status ( self , txid , wallet : Abstract_Wallet = None ) :
2019-03-05 17:01:54 +01:00
""" Returns some information regarding the tx. For now, only confirmations.
The transaction must be related to the wallet.
2025-03-18 11:15:16 +01:00
arg:txid:txid:Transaction ID
2019-03-05 17:01:54 +01:00
"""
2019-09-05 17:57:51 +02:00
if not wallet . db . get_transaction ( txid ) :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( " Transaction not in wallet. " )
2019-03-05 17:01:54 +01:00
return {
2022-06-01 23:03:35 +02:00
" confirmations " : wallet . adb . get_tx_height ( txid ) . conf ,
2019-03-05 17:01:54 +01:00
}
2016-02-29 09:57:56 +01:00
@command ( ' ' )
2019-08-15 13:17:16 +02:00
async def help ( self ) :
2025-05-29 16:44:01 +02:00
""" Show help about a command """
2016-02-29 09:57:56 +01:00
# for the python console
return sorted ( known_commands . keys ( ) )
2018-05-28 11:55:20 +02:00
# lightning network commands
2021-07-07 16:44:27 +03:00
@command ( ' wnl ' )
2020-04-16 12:39:12 +02:00
async def add_peer ( self , connection_string , timeout = 20 , gossip = False , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Connect to a lightning node
arg:str:connection_string:Lightning network node ID or network address
arg:bool:gossip:Apply command to your gossip node instead of wallet node
arg:int:timeout:Timeout in seconds (default=20)
"""
2020-04-16 12:39:12 +02:00
lnworker = self . network . lngossip if gossip else wallet . lnworker
2025-12-17 15:16:05 +00:00
peer = await lnworker . lnpeermgr . add_peer ( connection_string )
2025-10-28 12:32:02 +01:00
try :
await util . wait_for2 ( peer . initialized , timeout = LN_P2P_NETWORK_TIMEOUT )
except ( CancelledError , Exception ) as e :
# FIXME often simply CancelledError and real cause (e.g. timeout) remains hidden
raise UserFacingException ( f " Connection failed: { repr ( e ) } " )
2019-07-23 20:14:59 +02:00
return True
2019-06-05 11:08:16 +02:00
2025-03-04 14:33:54 +01:00
@command ( ' wnl ' )
async def gossip_info ( self , wallet : Abstract_Wallet = None ) :
""" Display statistics about lightninig gossip """
lngossip = self . network . lngossip
channel_db = lngossip . channel_db
2025-12-17 15:16:05 +00:00
forwarded = dict ( [ ( key . hex ( ) , p . _num_gossip_messages_forwarded ) for key , p in wallet . lnworker . lnpeermgr . peers . items ( ) ] ) ,
2025-03-04 14:33:54 +01:00
out = {
' received ' : {
' channel_announcements ' : lngossip . _num_chan_ann ,
' channel_updates ' : lngossip . _num_chan_upd ,
' channel_updates_good ' : lngossip . _num_chan_upd_good ,
' node_announcements ' : lngossip . _num_node_ann ,
} ,
' database ' : {
' nodes ' : channel_db . num_nodes ,
' channels ' : channel_db . num_channels ,
' channel_policies ' : channel_db . num_policies ,
} ,
' forwarded ' : forwarded ,
}
return out
2021-07-07 16:44:27 +03:00
@command ( ' wnl ' )
2020-04-16 12:39:12 +02:00
async def list_peers ( self , gossip = False , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
List lightning peers of your node
arg:bool:gossip:Apply command to your gossip node instead of wallet node
"""
2020-04-16 12:39:12 +02:00
lnworker = self . network . lngossip if gossip else wallet . lnworker
2020-04-12 12:48:44 +02:00
return [ {
2025-06-03 11:26:23 +02:00
' node_id ' : p . pubkey . hex ( ) ,
' address ' : p . transport . name ( ) ,
' initialized ' : p . is_initialized ( ) ,
2020-04-16 12:39:12 +02:00
' features ' : str ( LnFeatures ( p . features ) ) ,
2020-04-12 12:48:44 +02:00
' channels ' : [ c . funding_outpoint . to_str ( ) for c in p . channels . values ( ) ] ,
2025-12-17 15:16:05 +00:00
} for p in lnworker . lnpeermgr . peers . values ( ) ]
2020-04-12 12:48:44 +02:00
2021-07-07 16:44:27 +03:00
@command ( ' wpnl ' )
2023-08-08 05:09:58 +02:00
async def open_channel ( self , connection_string , amount , push_amount = 0 , public = False , zeroconf = False , password = None , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Open a lightning channel with a peer
arg:str:connection_string:Lightning network node ID or network address
arg:decimal_or_max:amount:funding amount (in BTC)
arg:decimal:push_amount:Push initial amount (in BTC)
arg:bool:public:The channel will be announced
arg:bool:zeroconf:request zeroconf channel
"""
2025-03-17 09:01:04 +01:00
if not wallet . can_have_lightning ( ) :
raise UserFacingException ( " This wallet cannot create new channels " )
2019-11-13 09:20:19 +01:00
funding_sat = satoshis ( amount )
push_sat = satoshis ( push_amount )
2025-12-17 15:16:05 +00:00
peer = await wallet . lnworker . lnpeermgr . add_peer ( connection_string )
2023-10-13 15:49:13 +02:00
chan , funding_tx = await wallet . lnworker . open_channel_with_peer (
peer , funding_sat ,
push_sat = push_sat ,
public = public ,
2023-08-08 05:09:58 +02:00
zeroconf = zeroconf ,
2023-10-13 15:49:13 +02:00
password = password )
2019-08-12 17:54:27 +02:00
return chan . funding_outpoint . to_str ( )
2018-05-28 11:55:20 +02:00
2020-04-11 12:02:38 +02:00
@command ( ' ' )
2020-06-22 22:37:58 +02:00
async def decode_invoice ( self , invoice : str ) :
2025-03-18 11:15:16 +01:00
"""
Decode a lightning invoice
arg:str:invoice:Lightning invoice (bolt 11)
"""
2022-03-15 13:03:34 +01:00
invoice = Invoice . from_bech32 ( invoice )
2020-06-22 22:37:58 +02:00
return invoice . to_debug_json ( )
2020-04-11 12:02:38 +02:00
2024-10-08 13:29:27 +02:00
@command ( ' wnpl ' )
2025-07-21 14:13:21 +02:00
async def lnpay (
self ,
invoice : str ,
timeout : int = 120 ,
max_cltv : Optional [ int ] = None ,
max_fee_msat : Optional [ int ] = None ,
password = None ,
wallet : Abstract_Wallet = None
) :
2025-03-18 11:15:16 +01:00
"""
Pay a lightning invoice
2025-06-28 10:02:40 +00:00
Note: it is *not* safe to try paying the same invoice multiple times with a timeout.
It is only safe to retry paying the same invoice if there are no more pending HTLCs
with the same payment_hash. # FIXME should there even be a default timeout? just block forever.
2025-03-18 11:15:16 +01:00
arg:str:invoice:Lightning invoice (bolt 11)
2025-06-28 10:02:40 +00:00
arg:int:timeout:Timeout in seconds (default=120)
2025-07-21 14:13:21 +02:00
arg:int:max_cltv:Maximum total time lock for the route (default=4032+invoice_final_cltv_delta)
arg:int:max_fee_msat:Maximum absolute fee budget for the payment (if unset, the default is a percentage fee based on config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS)
2025-03-18 11:15:16 +01:00
"""
2025-06-28 10:02:40 +00:00
# note: The "timeout" param works via black magic.
# The CLI-parser stores it in the config, and the argname matches config.cv.CLI_TIMEOUT.key().
# - it works when calling the CLI and there is also a daemon (online command)
# - FIXME it does NOT work when calling an offline command (-o)
# - FIXME it does NOT work when calling RPC directly (e.g. curl)
2020-02-23 12:40:40 +01:00
lnworker = wallet . lnworker
2025-07-21 14:13:21 +02:00
lnaddr = lnworker . _check_bolt11_invoice ( invoice ) # also checks if amount is given
2020-02-23 12:40:40 +01:00
payment_hash = lnaddr . paymenthash
2025-02-20 16:05:45 +01:00
invoice_obj = Invoice . from_bech32 ( invoice )
2025-07-21 14:13:21 +02:00
assert not max_fee_msat or max_fee_msat < max ( invoice_obj . amount_msat / / 2 , 1_000_000 ) , \
f " { max_fee_msat =} > max(invoice amount msat / 2, 1_000_000) "
2025-02-20 16:05:45 +01:00
wallet . save_invoice ( invoice_obj )
2025-07-21 14:13:21 +02:00
if max_cltv is not None :
# The cltv budget excludes the final cltv delta which is why it is deducted here
# so the whole used cltv is <= max_cltv
assert max_cltv < = NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE , \
f " { max_cltv =} > { NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE =} "
max_cltv_remaining = max_cltv - lnaddr . get_min_final_cltv_delta ( )
assert max_cltv_remaining > 0 , f " { max_cltv =} - { lnaddr . get_min_final_cltv_delta ( ) =} < 1 "
max_cltv = max_cltv_remaining
2025-08-01 15:06:33 +00:00
budget = PaymentFeeBudget . from_invoice_amount (
2025-07-21 14:13:21 +02:00
config = wallet . config ,
invoice_amount_msat = invoice_obj . amount_msat ,
max_cltv_delta = max_cltv ,
max_fee_msat = max_fee_msat ,
)
success , log = await lnworker . pay_invoice ( invoice_obj , budget = budget )
2020-02-23 12:40:40 +01:00
return {
' payment_hash ' : payment_hash . hex ( ) ,
' success ' : success ,
' preimage ' : lnworker . get_preimage ( payment_hash ) . hex ( ) if success else None ,
2020-05-05 18:32:43 +02:00
' log ' : [ x . formatted_tuple ( ) for x in log ]
2020-02-23 12:40:40 +01:00
}
2018-05-28 11:55:20 +02:00
2021-07-07 16:44:27 +03:00
@command ( ' wl ' )
2019-09-21 02:14:22 +02:00
async def nodeid ( self , wallet : Abstract_Wallet = None ) :
2025-05-28 15:25:26 +02:00
""" Return the Lightning Node ID of a wallet """
2023-05-24 17:41:44 +00:00
listen_addr = self . config . LIGHTNING_LISTEN
2023-02-17 11:35:03 +00:00
return wallet . lnworker . node_keypair . pubkey . hex ( ) + ( ( ' @ ' + listen_addr ) if listen_addr else ' ' )
2018-10-04 14:03:29 +02:00
2021-07-07 16:44:27 +03:00
@command ( ' wl ' )
2026-03-19 11:29:29 +01:00
async def list_channels ( self , public : bool = False , private : bool = False , active : bool = False , open : bool = False , wallet : Abstract_Wallet = None ) :
""" Return the list of channels in the wallet
2026-02-09 11:34:21 +01:00
2026-03-19 11:29:29 +01:00
arg:bool:public:list only public channels
arg:bool:private:list only private channels
arg:bool:open:list only open channels
arg:bool:active:list only active channels
2026-02-09 11:34:21 +01:00
"""
2020-02-20 10:30:30 +01:00
from . lnutil import LOCAL , REMOTE , format_short_channel_id
2026-03-19 11:29:29 +01:00
if public and private :
raise Exception ( " incompatible options " )
def _filter ( chan ) :
if public and not chan . is_public ( ) :
return False
if private and chan . is_public ( ) :
return False
if active and not chan . is_redeemed ( ) :
return False
if open and not chan . is_open ( ) :
return False
return True
2020-02-20 10:30:30 +01:00
return [
{
2020-03-06 09:57:37 +01:00
' short_channel_id ' : format_short_channel_id ( chan . short_channel_id ) if chan . short_channel_id else None ,
2021-03-12 18:53:09 +01:00
' channel_id ' : chan . channel_id . hex ( ) ,
2020-02-20 10:30:30 +01:00
' channel_point ' : chan . funding_outpoint . to_str ( ) ,
2025-06-25 17:13:30 +00:00
' closing_txid ' : chan . get_closing_height ( ) [ 0 ] if chan . get_closing_height ( ) else None ,
2020-02-20 10:30:30 +01:00
' state ' : chan . get_state ( ) . name ,
2020-03-06 09:57:37 +01:00
' peer_state ' : chan . peer_state . name ,
2023-02-17 11:35:03 +00:00
' remote_pubkey ' : chan . node_id . hex ( ) ,
2020-02-20 10:30:30 +01:00
' local_balance ' : chan . balance ( LOCAL ) / / 1000 ,
' remote_balance ' : chan . balance ( REMOTE ) / / 1000 ,
2022-09-01 09:56:46 +02:00
' local_ctn ' : chan . get_latest_ctn ( LOCAL ) ,
' remote_ctn ' : chan . get_latest_ctn ( REMOTE ) ,
2025-06-03 11:26:23 +02:00
' local_reserve ' : chan . config [ REMOTE ] . reserve_sat , # their config has our reserve
2020-04-20 18:48:41 +02:00
' remote_reserve ' : chan . config [ LOCAL ] . reserve_sat ,
2020-03-26 05:43:26 +01:00
' 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 ,
2026-03-19 11:29:29 +01:00
} for chan in wallet . lnworker . channels . values ( ) if _filter ( chan )
2026-02-09 11:34:21 +01:00
]
@command ( ' wl ' )
async def list_channel_backups ( self , wallet : Abstract_Wallet = None ) :
""" Return the list of channel backups in the wallet """
# FIXME: we need to be online to display capacity of backups
from . lnutil import LOCAL , REMOTE , format_short_channel_id
return [
2021-03-12 18:53:09 +01:00
{
' short_channel_id ' : format_short_channel_id ( chan . short_channel_id ) if chan . short_channel_id else None ,
' channel_id ' : chan . channel_id . hex ( ) ,
' channel_point ' : chan . funding_outpoint . to_str ( ) ,
2025-06-25 17:13:30 +00:00
' closing_txid ' : chan . get_closing_height ( ) [ 0 ] if chan . get_closing_height ( ) else None ,
2021-03-12 18:53:09 +01:00
' state ' : chan . get_state ( ) . name ,
2026-02-09 11:34:21 +01:00
} for chan in wallet . lnworker . channel_backups . values ( )
2020-02-20 10:30:30 +01:00
]
2018-05-28 12:06:37 +02:00
2021-07-07 16:44:27 +03:00
@command ( ' wnl ' )
2020-02-27 20:53:50 +01:00
async def enable_htlc_settle ( self , b : bool , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
command used in regtests
arg:bool:b:boolean
"""
2021-03-18 07:48:30 +01:00
wallet . lnworker . enable_htlc_settle = b
2020-02-27 20:53:50 +01:00
2018-09-24 18:09:35 +02:00
@command ( ' n ' )
2019-08-15 13:17:16 +02:00
async def clear_ln_blacklist ( self ) :
2021-04-26 08:43:50 +02:00
if self . network . path_finder :
2023-08-15 14:59:10 +00:00
self . network . path_finder . clear_blacklist ( )
2018-09-24 18:09:35 +02:00
2021-04-09 09:40:33 +02:00
@command ( ' n ' )
async def reset_liquidity_hints ( self ) :
2021-04-26 08:43:50 +02:00
if self . network . path_finder :
self . network . path_finder . liquidity_hints . reset_liquidity_hints ( )
2023-08-15 14:59:10 +00:00
self . network . path_finder . clear_blacklist ( )
2018-09-24 18:09:35 +02:00
2024-10-09 18:04:26 +02:00
@command ( ' wnpl ' )
async def close_channel ( self , channel_point , force = False , password = None , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
2025-05-15 14:19:49 +00:00
Close a lightning channel.
Returns txid of closing tx.
2025-03-18 11:15:16 +01:00
arg:str:channel_point:channel point
arg:bool:force:Force closes (broadcast local commitment transaction)
"""
2019-02-01 15:27:50 +01:00
txid , index = channel_point . split ( ' : ' )
chan_id , _ = channel_id_from_funding_tx ( txid , int ( index ) )
2024-11-29 10:38:57 +01:00
if chan_id not in wallet . lnworker . channels :
raise UserFacingException ( f ' Unknown channel { channel_point } ' )
2019-09-05 17:57:51 +02:00
coro = wallet . lnworker . force_close_channel ( chan_id ) if force else wallet . lnworker . close_channel ( chan_id )
2019-08-15 13:17:16 +02:00
return await coro
2019-01-24 18:21:46 +01:00
2024-10-09 18:04:26 +02:00
@command ( ' wnpl ' )
async def request_force_close ( self , channel_point , connection_string = None , password = None , wallet : Abstract_Wallet = None ) :
2021-03-17 09:15:40 +01:00
"""
Requests the remote to force close a channel.
If a connection string is passed, can be used without having state or any backup for the channel.
Assumes that channel was originally opened with the same local peer (node_keypair).
2025-03-18 11:15:16 +01:00
arg:str:connection_string:Lightning network node ID or network address
arg:str:channel_point:channel point
2021-03-17 09:15:40 +01:00
"""
2021-03-12 16:10:02 +01:00
txid , index = channel_point . split ( ' : ' )
chan_id , _ = channel_id_from_funding_tx ( txid , int ( index ) )
2024-11-29 10:38:57 +01:00
if chan_id not in wallet . lnworker . channels and chan_id not in wallet . lnworker . channel_backups :
raise UserFacingException ( f ' Unknown channel { channel_point } ' )
2021-03-17 09:15:40 +01:00
await wallet . lnworker . request_force_close ( chan_id , connect_str = connection_string )
2021-03-12 16:10:02 +01:00
2024-10-09 18:04:26 +02:00
@command ( ' wpl ' )
async def export_channel_backup ( self , channel_point , password = None , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Returns an encrypted channel backup
arg:str:channel_point:Channel outpoint
"""
2020-03-13 11:44:29 +01:00
txid , index = channel_point . split ( ' : ' )
chan_id , _ = channel_id_from_funding_tx ( txid , int ( index ) )
2024-11-29 10:38:57 +01:00
if chan_id not in wallet . lnworker . channels :
raise UserFacingException ( f ' Unknown channel { channel_point } ' )
2020-03-13 11:44:29 +01:00
return wallet . lnworker . export_channel_backup ( chan_id )
2021-07-07 16:44:27 +03:00
@command ( ' wl ' )
2020-03-13 11:44:29 +01:00
async def import_channel_backup ( self , encrypted , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
arg:str:encrypted:Encrypted channel backup
"""
2021-03-10 15:26:39 +01:00
return wallet . lnworker . import_channel_backup ( encrypted )
2020-03-13 11:44:29 +01:00
2024-10-08 13:29:27 +02:00
@command ( ' wnpl ' )
async def get_channel_ctx ( self , channel_point , password = None , iknowwhatimdoing = False , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
return the current commitment transaction of a channel
arg:str:channel_point:Channel outpoint
arg:bool:iknowwhatimdoing:Acknowledge that I understand the full implications of what I am about to do
"""
2020-03-05 17:27:43 +01:00
if not iknowwhatimdoing :
2024-02-12 19:02:02 +00:00
raise UserFacingException (
" WARNING: this command is potentially unsafe. \n "
" To proceed, try again, with the --iknowwhatimdoing option. " )
2019-03-14 18:31:41 +01:00
txid , index = channel_point . split ( ' : ' )
chan_id , _ = channel_id_from_funding_tx ( txid , int ( index ) )
2024-11-29 10:38:57 +01:00
if chan_id not in wallet . lnworker . channels :
raise UserFacingException ( f ' Unknown channel { channel_point } ' )
2019-09-05 17:57:51 +02:00
chan = wallet . lnworker . channels [ chan_id ]
2019-03-14 18:31:41 +01:00
tx = chan . force_close_tx ( )
2019-10-23 17:09:41 +02:00
return tx . serialize ( )
2019-03-14 18:31:41 +01:00
2021-07-07 16:44:27 +03:00
@command ( ' wnl ' )
2020-02-02 14:50:08 +01:00
async def get_watchtower_ctn ( self , channel_point , wallet : Abstract_Wallet = None ) :
2025-03-18 11:15:16 +01:00
"""
Return the local watchtower ' s ctn of channel. used in regtests
arg:str:channel_point:Channel outpoint (txid:index)
"""
2024-12-20 14:02:54 +01:00
return wallet . lnworker . get_watchtower_ctn ( channel_point )
2020-02-02 14:50:08 +01:00
2024-10-09 18:04:26 +02:00
@command ( ' wnpl ' )
async def rebalance_channels ( self , from_scid , dest_scid , amount , password = None , wallet : Abstract_Wallet = None ) :
2022-07-13 10:14:34 +02:00
"""
Rebalance channels.
2022-12-06 00:31:16 +02:00
If trampoline is used, channels must be with different trampolines.
2025-03-18 11:15:16 +01:00
arg:str:from_scid:Short channel ID
arg:str:dest_scid:Short channel ID
arg:decimal:amount:Amount (in BTC)
2022-07-13 10:14:34 +02:00
"""
from . lnutil import ShortChannelID
from_scid = ShortChannelID . from_str ( from_scid )
dest_scid = ShortChannelID . from_str ( dest_scid )
2023-09-17 11:06:25 +02:00
from_channel = wallet . lnworker . get_channel_by_short_id ( from_scid )
dest_channel = wallet . lnworker . get_channel_by_short_id ( dest_scid )
2022-07-13 10:14:34 +02:00
amount_sat = satoshis ( amount )
2023-06-04 03:07:06 +00:00
success , log = await wallet . lnworker . rebalance_channels (
from_channel ,
dest_channel ,
amount_msat = amount_sat * 1000 ,
)
2022-07-13 10:14:34 +02:00
return {
' success ' : success ,
' log ' : [ x . formatted_tuple ( ) for x in log ]
}
2025-08-20 18:25:02 +02:00
@command ( ' wnl ' )
async def get_submarine_swap_providers ( self , query_time = 15 , wallet : Abstract_Wallet = None ) :
"""
Queries nostr relays for available submarine swap providers.
To configure one of the providers use:
setconfig swapserver_npub ' npub... '
arg:int:query_time:Optional timeout how long the relays should be queried for provider announcements. Default: 15 sec
"""
sm = wallet . lnworker . swap_manager
async with sm . create_transport ( ) as transport :
assert isinstance ( transport , NostrTransport )
await asyncio . sleep ( query_time )
offers = transport . get_recent_offers ( )
result = { }
for offer in offers :
result [ offer . server_npub ] = {
2026-03-16 13:42:53 +01:00
" percentage_fee " : float ( offer . pairs . percentage ) ,
2025-08-20 18:25:02 +02:00
" max_forward_sat " : offer . pairs . max_forward ,
" max_reverse_sat " : offer . pairs . max_reverse ,
" min_amount_sat " : offer . pairs . min_amount ,
2025-08-21 17:38:31 +02:00
" prepayment " : 2 * offer . pairs . mining_fee ,
2025-08-20 18:25:02 +02:00
}
return result
2021-07-07 16:44:27 +03:00
@command ( ' wnpl ' )
2020-05-28 13:11:32 +02:00
async def normal_swap ( self , onchain_amount , lightning_amount , password = None , wallet : Abstract_Wallet = None ) :
"""
Normal submarine swap: send on-chain BTC, receive on Lightning
2025-03-18 11:15:16 +01:00
arg:decimal_or_dryrun:lightning_amount:Amount to be received, in BTC. Set it to ' dryrun ' to receive a value
arg:decimal_or_dryrun:onchain_amount:Amount to be sent, in BTC. Set it to ' dryrun ' to receive a value
2020-05-28 13:11:32 +02:00
"""
sm = wallet . lnworker . swap_manager
2025-08-20 18:25:02 +02:00
assert self . config . SWAPSERVER_NPUB or self . config . SWAPSERVER_URL , \
" Configure swap provider first. See ' get_submarine_swap_providers ' . "
2025-08-20 15:10:04 +02:00
async with sm . create_transport ( ) as transport :
2025-08-20 18:25:02 +02:00
try :
await asyncio . wait_for ( sm . is_initialized . wait ( ) , timeout = 15 )
except asyncio . TimeoutError :
raise TimeoutError ( " Could not find configured swap provider. Setup another one. See ' get_submarine_swap_providers ' " )
2024-10-10 12:30:27 +02:00
if lightning_amount == ' dryrun ' :
onchain_amount_sat = satoshis ( onchain_amount )
lightning_amount_sat = sm . get_recv_amount ( onchain_amount_sat , is_reverse = False )
txid = None
elif onchain_amount == ' dryrun ' :
lightning_amount_sat = satoshis ( lightning_amount )
onchain_amount_sat = sm . get_send_amount ( lightning_amount_sat , is_reverse = False )
txid = None
else :
lightning_amount_sat = satoshis ( lightning_amount )
onchain_amount_sat = satoshis ( onchain_amount )
txid = await wallet . lnworker . swap_manager . normal_swap (
2025-06-10 16:18:42 +00:00
transport = transport ,
2024-10-10 12:30:27 +02:00
lightning_amount_sat = lightning_amount_sat ,
expected_onchain_amount_sat = onchain_amount_sat ,
password = password ,
)
2020-05-28 13:11:32 +02:00
return {
' txid ' : txid ,
' lightning_amount ' : format_satoshis ( lightning_amount_sat ) ,
' onchain_amount ' : format_satoshis ( onchain_amount_sat ) ,
}
2020-05-21 10:36:22 +02:00
2024-10-09 18:04:26 +02:00
@command ( ' wnpl ' )
2025-08-20 16:20:52 +00:00
async def reverse_swap (
2025-08-21 17:38:31 +02:00
self , lightning_amount , onchain_amount , prepayment = ' dryrun ' , password = None , wallet : Abstract_Wallet = None ,
2025-08-20 16:20:52 +00:00
) :
2025-03-18 11:15:16 +01:00
"""
Reverse submarine swap: send on Lightning, receive on-chain
arg:decimal_or_dryrun:lightning_amount:Amount to be sent, in BTC. Set it to ' dryrun ' to receive a value
arg:decimal_or_dryrun:onchain_amount:Amount to be received, in BTC. Set it to ' dryrun ' to receive a value
2025-08-21 17:38:31 +02:00
arg:decimal_or_dryrun:prepayment:Lightning payment required by the swap provider in order to cover their mining fees. This is included in lightning_amount. However, this part of the operation is not trustless; the provider is trusted to fail this payment if the swap fails.
2020-05-28 13:11:32 +02:00
"""
sm = wallet . lnworker . swap_manager
2025-08-20 18:25:02 +02:00
assert self . config . SWAPSERVER_NPUB or self . config . SWAPSERVER_URL , \
" Configure swap provider first. See ' get_submarine_swap_providers ' . "
2025-08-20 15:10:04 +02:00
async with sm . create_transport ( ) as transport :
2025-08-20 18:25:02 +02:00
try :
await asyncio . wait_for ( sm . is_initialized . wait ( ) , timeout = 15 )
except asyncio . TimeoutError :
raise TimeoutError ( " Could not find configured swap provider. Setup another one. See ' get_submarine_swap_providers ' " )
2024-10-10 12:30:27 +02:00
if onchain_amount == ' dryrun ' :
lightning_amount_sat = satoshis ( lightning_amount )
onchain_amount_sat = sm . get_recv_amount ( lightning_amount_sat , is_reverse = True )
2025-08-21 17:38:31 +02:00
assert prepayment == " dryrun " , f " Cannot use { prepayment =} in dryrun. Set it to ' dryrun ' . "
prepayment_sat = 2 * sm . mining_fee
2024-10-10 12:30:27 +02:00
funding_txid = None
elif lightning_amount == ' dryrun ' :
onchain_amount_sat = satoshis ( onchain_amount )
lightning_amount_sat = sm . get_send_amount ( onchain_amount_sat , is_reverse = True )
2025-08-21 17:38:31 +02:00
assert prepayment == " dryrun " , f " Cannot use { prepayment =} in dryrun. Set it to ' dryrun ' . "
prepayment_sat = 2 * sm . mining_fee
2024-10-10 12:30:27 +02:00
funding_txid = None
else :
lightning_amount_sat = satoshis ( lightning_amount )
2025-06-09 16:56:11 +02:00
claim_fee = sm . get_fee_for_txbatcher ( )
2024-10-10 12:30:27 +02:00
onchain_amount_sat = satoshis ( onchain_amount ) + claim_fee
2025-08-21 17:38:31 +02:00
assert prepayment != " dryrun " , " Provide the ' prepayment ' obtained from the dryrun. "
prepayment_sat = satoshis ( prepayment )
2024-10-10 12:30:27 +02:00
funding_txid = await wallet . lnworker . swap_manager . reverse_swap (
2025-06-10 16:18:42 +00:00
transport = transport ,
2024-10-10 12:30:27 +02:00
lightning_amount_sat = lightning_amount_sat ,
expected_onchain_amount_sat = onchain_amount_sat ,
2025-08-21 17:38:31 +02:00
prepayment_sat = prepayment_sat ,
2024-10-10 12:30:27 +02:00
)
2020-05-28 13:11:32 +02:00
return {
2023-06-28 14:23:32 +02:00
' funding_txid ' : funding_txid ,
2020-05-28 13:11:32 +02:00
' lightning_amount ' : format_satoshis ( lightning_amount_sat ) ,
' onchain_amount ' : format_satoshis ( onchain_amount_sat ) ,
2025-08-21 17:38:31 +02:00
' prepayment ' : format_satoshis ( prepayment_sat )
2020-05-28 13:11:32 +02:00
}
2020-05-22 10:39:30 +02:00
2022-12-05 18:49:21 +02:00
@command ( ' n ' )
2025-03-18 11:15:16 +01:00
async def convert_currency ( self , from_amount = 1 , from_ccy = ' ' , to_ccy = ' ' ) :
"""
Converts the given amount of currency to another using the
2022-12-05 18:49:21 +02:00
configured exchange rate source.
2025-03-18 11:15:16 +01:00
arg:decimal:from_amount:Amount to convert (default=1)
2025-06-27 15:15:12 +00:00
arg:str:from_ccy:Currency to convert from
arg:str:to_ccy:Currency to convert to
2022-12-05 18:49:21 +02:00
"""
2022-12-06 03:30:24 +02:00
if not self . daemon . fx . is_enabled ( ) :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( " FX is disabled. To enable, run: ' electrum setconfig use_exchange_rate true ' " )
2022-12-05 18:49:21 +02:00
# Currency codes are uppercase
2022-12-06 03:27:57 +02:00
from_ccy = from_ccy . upper ( )
2022-12-05 18:49:21 +02:00
to_ccy = to_ccy . upper ( )
2022-12-06 16:23:59 +02:00
# Default currencies
if from_ccy == ' ' :
from_ccy = " BTC " if to_ccy != " BTC " else self . daemon . fx . ccy
if to_ccy == ' ' :
to_ccy = " BTC " if from_ccy != " BTC " else self . daemon . fx . ccy
2022-12-05 18:49:21 +02:00
# Get current rates
rate_from = self . daemon . fx . exchange . get_cached_spot_quote ( from_ccy )
rate_to = self . daemon . fx . exchange . get_cached_spot_quote ( to_ccy )
# Test if currencies exist
if rate_from . is_nan ( ) :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( f ' Currency to convert from ( { from_ccy } ) is unknown or rate is unavailable ' )
2022-12-05 18:49:21 +02:00
if rate_to . is_nan ( ) :
2024-02-12 19:02:02 +00:00
raise UserFacingException ( f ' Currency to convert to ( { to_ccy } ) is unknown or rate is unavailable ' )
2022-12-05 18:49:21 +02:00
# Conversion
try :
2023-03-22 12:22:36 +00:00
from_amount = to_decimal ( from_amount )
2022-12-06 03:27:57 +02:00
to_amount = from_amount / rate_from * rate_to
2022-12-05 18:49:21 +02:00
except InvalidOperation :
raise Exception ( " from_amount is not a number " )
return {
2023-01-10 14:45:35 +00:00
" from_amount " : self . daemon . fx . ccy_amount_str ( from_amount , add_thousands_sep = False , ccy = from_ccy ) ,
" to_amount " : self . daemon . fx . ccy_amount_str ( to_amount , add_thousands_sep = False , ccy = to_ccy ) ,
2022-12-05 18:49:21 +02:00
" from_ccy " : from_ccy ,
" to_ccy " : to_ccy ,
" source " : self . daemon . fx . exchange . name ( ) ,
}
2024-11-22 14:46:21 +01:00
@command ( ' wnl ' )
async def send_onion_message ( self , node_id_or_blinded_path_hex : str , message : str , wallet : Abstract_Wallet = None ) :
"""
Send an onion message with onionmsg_tlv.message payload to node_id.
2025-03-18 11:15:16 +01:00
arg:str:node_id_or_blinded_path_hex:node id or blinded path
arg:str:message:Message to send
2024-11-22 14:46:21 +01:00
"""
assert wallet
assert wallet . lnworker
assert node_id_or_blinded_path_hex
assert message
node_id_or_blinded_path = bfh ( node_id_or_blinded_path_hex )
assert len ( node_id_or_blinded_path ) > = 33
destination_payload = {
' message ' : { ' text ' : message . encode ( ' utf-8 ' ) }
}
try :
2024-12-03 15:58:10 +01:00
send_onion_message_to ( wallet . lnworker , node_id_or_blinded_path , destination_payload )
2024-11-22 14:46:21 +01:00
return { ' success ' : True }
except Exception as e :
msg = str ( e )
return {
' success ' : False ,
' msg ' : msg
}
@command ( ' wnl ' )
async def get_blinded_path_via ( self , node_id : str , dummy_hops : int = 0 , wallet : Abstract_Wallet = None ) :
"""
Create a blinded path with node_id as introduction point. Introduction point must be direct peer of me.
2025-03-18 11:15:16 +01:00
arg:str:node_id:Node pubkey in hex format
arg:int:dummy_hops:Number of dummy hops to add
2024-11-22 14:46:21 +01:00
"""
# TODO: allow introduction_point to not be a direct peer and construct a route
assert wallet
assert node_id
pubkey = bfh ( node_id )
assert len ( pubkey ) == 33 , ' invalid node_id '
2025-12-17 15:16:05 +00:00
peer = wallet . lnworker . lnpeermgr . peers [ pubkey ]
2024-11-22 14:46:21 +01:00
assert peer , ' node_id not a peer '
path = [ pubkey , wallet . lnworker . node_keypair . pubkey ]
session_key = os . urandom ( 32 )
blinded_path = create_blinded_path ( session_key , path = path , final_recipient_data = { } , dummy_hops = dummy_hops )
with io . BytesIO ( ) as blinded_path_fd :
2025-02-12 14:28:30 +01:00
OnionWireSerializer . write_field (
2025-01-22 16:43:33 +01:00
fd = blinded_path_fd ,
field_type = ' blinded_path ' ,
count = 1 ,
value = blinded_path )
2024-11-22 14:46:21 +01:00
encoded_blinded_path = blinded_path_fd . getvalue ( )
return encoded_blinded_path . hex ( )
2025-06-03 11:26:23 +02:00
2025-03-16 11:12:04 +01:00
def plugin_command ( s , plugin_name ) :
2025-03-06 11:43:50 +01:00
""" Decorator to register a cli command inside a plugin. To be used within a commands.py file
in the plugins root. """
2025-09-05 18:23:49 +00:00
# atm all plugin commands require a daemon, cannot be run in 'offline' mode:
if ' n ' not in s :
s + = ' n '
2025-03-06 11:43:50 +01:00
def decorator ( func ) :
2025-03-17 17:29:38 +01:00
assert len ( plugin_name ) > 0 , " Plugin name must not be empty "
2025-03-16 11:12:04 +01:00
func . plugin_name = plugin_name
name = plugin_name + ' _ ' + func . __name__
2025-03-06 11:43:50 +01:00
if name in known_commands or hasattr ( Commands , name ) :
2025-03-17 17:29:38 +01:00
raise Exception ( f " Command name { name } already exists. Plugin commands should not overwrite other commands. " )
2025-09-05 10:26:58 +02:00
assert inspect . iscoroutinefunction ( func ) , f " Plugin commands must be a coroutine: { name } "
2025-06-03 11:26:23 +02:00
2025-03-06 11:43:50 +01:00
@command ( s )
@wraps ( func )
async def func_wrapper ( * args , * * kwargs ) :
2025-03-16 12:25:59 +01:00
cmd_runner = args [ 0 ] # type: Commands
daemon = cmd_runner . daemon
2025-09-05 18:23:49 +00:00
assert daemon is not None
2025-03-16 12:25:59 +01:00
kwargs [ ' plugin ' ] = daemon . _plugins . get_plugin ( plugin_name )
2025-03-16 11:12:04 +01:00
return await func ( * args , * * kwargs )
2025-06-03 11:26:23 +02:00
2025-03-16 11:12:04 +01:00
setattr ( Commands , name , func_wrapper )
2025-03-06 11:43:50 +01:00
return func_wrapper
return decorator
2020-02-02 14:50:08 +01:00
2018-10-10 20:29:51 +02:00
def eval_bool ( x : str ) - > bool :
2025-03-18 11:15:16 +01:00
if x == ' false ' :
return False
if x == ' true ' :
return True
# assume python, raise if malformed
return bool ( ast . literal_eval ( x ) )
2015-05-31 22:42:34 +02:00
2015-12-15 11:33:04 +01:00
# don't use floats because of rounding errors
2023-03-22 12:22:36 +00:00
json_loads = lambda x : json . loads ( x , parse_float = lambda x : str ( to_decimal ( x ) ) )
2025-06-03 11:26:23 +02:00
2025-03-18 11:15:16 +01:00
def check_txid ( txid ) :
if not is_hash256_str ( txid ) :
raise UserFacingException ( f " { repr ( txid ) } is not a txid " )
return txid
2025-06-03 11:26:23 +02:00
2015-05-31 22:42:34 +02:00
arg_types = {
2025-03-18 11:15:16 +01:00
' int ' : int ,
' bool ' : eval_bool ,
' str ' : str ,
' txid ' : check_txid ,
2019-11-08 15:01:18 +01:00
' tx ' : convert_raw_tx_to_hex ,
2025-03-18 11:15:16 +01:00
' json ' : json_loads ,
' decimal ' : lambda x : str ( to_decimal ( x ) ) ,
' decimal_or_dryrun ' : lambda x : str ( to_decimal ( x ) ) if x != ' dryrun ' else x ,
' decimal_or_max ' : lambda x : str ( to_decimal ( x ) ) if not parse_max_spend ( x ) else x ,
2015-05-31 22:42:34 +02:00
}
2015-06-03 11:34:52 +02:00
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 ' ,
2015-06-09 09:58:40 +02:00
' url_rewrite ' : ' Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \" ( \' file:///var/www/ \' , \' https://electrum.org/ \' ) \" ' ,
2015-06-03 11:34:52 +02:00
} ,
2025-06-03 11:26:23 +02:00
' listrequests ' : {
2015-06-03 11:34:52 +02:00
' url_rewrite ' : ' Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \" ( \' file:///var/www/ \' , \' https://electrum.org/ \' ) \" ' ,
}
}
2015-05-31 22:42:34 +02:00
2025-01-23 12:58:28 +01:00
2015-05-31 22:42:34 +02:00
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 : ] :
2022-02-15 19:51:20 +01:00
if arg in [ ' -h ' , ' --help ' , ' --version ' ] : # global help/version if no subparser
2015-05-31 22:42:34 +02:00
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 )
2025-01-23 12:58:28 +01:00
2015-05-31 22:42:34 +02:00
argparse . ArgumentParser . set_default_subparser = set_default_subparser
2015-06-07 18:44:33 +02:00
2017-02-22 09:32:35 +01:00
# 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 )
2018-02-04 07:26:55 +01:00
msg = _ ( ' unknown parser {!r} (choices: {} ) ' ) . format ( * tup )
2017-02-22 09:32:35 +01:00
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 )
2025-01-23 12:58:28 +01:00
2017-02-22 09:32:35 +01:00
argparse . _SubParsersAction . __call__ = subparser_call
2015-06-07 18:44:33 +02:00
2015-05-31 22:42:34 +02:00
def add_network_options ( parser ) :
2025-03-18 09:35:09 +01:00
group = parser . add_argument_group ( ' network options ' )
group . add_argument (
" -f " , " --serverfingerprint " , dest = SimpleConfig . NETWORK_SERVERFINGERPRINT . key ( ) , default = None ,
help = " only allow connecting to servers with a matching SSL certificate SHA256 fingerprint. " +
" To calculate this yourself: ' $ openssl x509 -noout -fingerprint -sha256 -inform pem -in mycertfile.crt ' . Enter as 64 hex chars. " )
group . add_argument (
" -1 " , " --oneserver " , action = " store_true " , dest = SimpleConfig . NETWORK_ONESERVER . key ( ) , default = None ,
help = " connect to one server only " )
group . add_argument (
" -s " , " --server " , dest = SimpleConfig . NETWORK_SERVER . key ( ) , default = None ,
help = " set server host:port:protocol, where protocol is either t (tcp) or s (ssl) " )
group . add_argument (
" -p " , " --proxy " , dest = SimpleConfig . NETWORK_PROXY . key ( ) , default = None ,
help = " set proxy [type:]host:port (or ' none ' to disable proxy), where type is socks4 or socks5 " )
group . add_argument (
" --proxyuser " , dest = SimpleConfig . NETWORK_PROXY_USER . key ( ) , default = None ,
help = " set proxy username " )
group . add_argument (
" --proxypassword " , dest = SimpleConfig . NETWORK_PROXY_PASSWORD . key ( ) , default = None ,
help = " set proxy password " )
group . add_argument (
" --noonion " , action = " store_true " , dest = SimpleConfig . NETWORK_NOONION . key ( ) , default = None ,
help = " do not try to connect to onion servers " )
group . add_argument (
" --skipmerklecheck " , action = " store_true " , dest = SimpleConfig . NETWORK_SKIPMERKLECHECK . key ( ) , default = None ,
help = " Tolerate invalid merkle proofs from Electrum server " )
def add_global_options ( parser , suppress = False ) :
2017-02-22 09:32:35 +01:00
group = parser . add_argument_group ( ' global options ' )
2025-03-18 09:35:09 +01:00
group . add_argument (
" -v " , dest = " verbosity " , default = ' ' ,
help = argparse . SUPPRESS if suppress else " Set verbosity (log levels) " )
group . add_argument (
" -D " , " --dir " , dest = " electrum_path " ,
help = argparse . SUPPRESS if suppress else " electrum directory " )
group . add_argument (
" -w " , " --wallet " , dest = " wallet_path " ,
help = argparse . SUPPRESS if suppress else " wallet path " )
group . add_argument (
" -P " , " --portable " , action = " store_true " , dest = " portable " , default = False ,
help = argparse . SUPPRESS if suppress else " Use local ' electrum_data ' directory " )
2025-05-29 18:14:40 +00:00
for chain in constants . NETS_LIST :
group . add_argument (
f " -- { chain . cli_flag ( ) } " , action = " store_true " , dest = chain . config_key ( ) , default = False ,
help = argparse . SUPPRESS if suppress else f " Use { chain . NET_NAME } chain " )
2025-03-18 09:35:09 +01:00
group . add_argument (
" -o " , " --offline " , action = " store_true " , dest = SimpleConfig . NETWORK_OFFLINE . key ( ) , default = None ,
help = argparse . SUPPRESS if suppress else " Run offline " )
group . add_argument (
" --rpcuser " , dest = SimpleConfig . RPC_USERNAME . key ( ) , default = argparse . SUPPRESS ,
help = argparse . SUPPRESS if suppress else " RPC user " )
group . add_argument (
" --rpcpassword " , dest = SimpleConfig . RPC_PASSWORD . key ( ) , default = argparse . SUPPRESS ,
help = argparse . SUPPRESS if suppress else " RPC password " )
group . add_argument (
2026-01-19 11:30:53 +00:00
" --forgetconfig " , action = " store_true " , dest = SimpleConfig . CONFIG_FORGET_CHANGES . key ( ) , default = None ,
2025-03-18 09:35:09 +01:00
help = argparse . SUPPRESS if suppress else " Forget config on exit " )
2017-02-22 09:32:35 +01:00
2025-01-23 12:58:28 +01:00
2025-03-16 15:00:24 +01:00
def get_simple_parser ( ) :
""" simple parser that figures out the path of the config file and ignore unknown args """
from optparse import OptionParser , BadOptionError , AmbiguousOptionError
2025-06-03 11:26:23 +02:00
2025-03-16 15:00:24 +01:00
class PassThroughOptionParser ( OptionParser ) :
# see https://stackoverflow.com/questions/1885161/how-can-i-get-optparses-optionparser-to-ignore-invalid-options
def _process_args ( self , largs , rargs , values ) :
while rargs :
try :
2025-06-03 11:26:23 +02:00
OptionParser . _process_args ( self , largs , rargs , values )
except ( BadOptionError , AmbiguousOptionError ) as e :
2025-03-16 15:00:24 +01:00
largs . append ( e . opt_str )
2025-06-03 11:26:23 +02:00
2025-03-16 15:00:24 +01:00
parser = PassThroughOptionParser ( )
parser . add_option ( " -D " , " --dir " , dest = " electrum_path " , help = " electrum directory " )
parser . add_option ( " -P " , " --portable " , action = " store_true " , dest = " portable " , default = False , help = " Use local ' electrum_data ' directory " )
2025-05-29 18:14:40 +00:00
for chain in constants . NETS_LIST :
parser . add_option ( f " -- { chain . cli_flag ( ) } " , action = " store_true " , dest = chain . config_key ( ) , default = False , help = f " Use { chain . NET_NAME } chain " )
2025-03-16 15:00:24 +01:00
return parser
2017-02-22 09:32:35 +01:00
def get_parser ( ) :
2015-05-31 22:42:34 +02:00
# create main parser
parser = argparse . ArgumentParser (
epilog = " Run ' electrum help <command> ' to see the help for a command " )
2022-02-15 19:51:20 +01:00
parser . add_argument ( " --version " , dest = " cmd " , action = ' store_const ' , const = ' version ' , help = " Return the version of Electrum. " )
2017-02-22 09:32:35 +01:00
add_global_options ( parser )
2015-05-31 22:42:34 +02:00
subparsers = parser . add_subparsers ( dest = ' cmd ' , metavar = ' <command> ' )
# gui
2017-02-21 12:41:24 +01:00
parser_gui = subparsers . add_parser ( ' gui ' , description = " Run Electrum ' s Graphical User Interface. " , help = " Run GUI (default) " )
2026-03-20 15:51:07 +00:00
parser_gui . add_argument ( " url " , nargs = ' ? ' , default = None , help = " bitcoin URI " )
2023-08-30 13:11:33 +00:00
parser_gui . add_argument ( " -g " , " --gui " , dest = SimpleConfig . GUI_NAME . key ( ) , help = " select graphical user interface " , choices = [ ' qt ' , ' text ' , ' stdio ' , ' qml ' ] )
2023-05-24 17:41:44 +00:00
parser_gui . add_argument ( " -m " , action = " store_true " , dest = SimpleConfig . GUI_QT_HIDE_ON_STARTUP . key ( ) , default = False , help = " hide GUI on startup " )
parser_gui . add_argument ( " -L " , " --lang " , dest = SimpleConfig . LOCALIZATION_LANGUAGE . key ( ) , default = None , help = " default language used in GUI " )
2018-09-13 16:25:56 +02:00
parser_gui . add_argument ( " --daemon " , action = " store_true " , dest = " daemon " , default = False , help = " keep daemon running after GUI is closed " )
2023-05-24 17:41:44 +00:00
parser_gui . add_argument ( " --nosegwit " , action = " store_true " , dest = SimpleConfig . WIZARD_DONT_CREATE_SEGWIT . key ( ) , default = False , help = " Do not create segwit wallets " )
2015-05-31 22:42:34 +02:00
add_network_options ( parser_gui )
2017-02-22 09:32:35 +01:00
add_global_options ( parser_gui )
2015-05-31 22:42:34 +02:00
# daemon
2017-02-21 12:41:24 +01:00
parser_daemon = subparsers . add_parser ( ' daemon ' , help = " Run Daemon " )
2019-09-02 19:04:08 +02:00
parser_daemon . add_argument ( " -d " , " --detached " , action = " store_true " , dest = " detach " , default = False , help = " run daemon in detached mode " )
2022-05-13 16:48:50 +02:00
# FIXME: all these options are rpc-server-side. The CLI client-side cannot use e.g. --rpcport,
# instead it reads it from the daemon lockfile.
2023-05-24 17:41:44 +00:00
parser_daemon . add_argument ( " --rpchost " , dest = SimpleConfig . RPC_HOST . key ( ) , default = argparse . SUPPRESS , help = " RPC host " )
parser_daemon . add_argument ( " --rpcport " , dest = SimpleConfig . RPC_PORT . key ( ) , type = int , default = argparse . SUPPRESS , help = " RPC port " )
parser_daemon . add_argument ( " --rpcsock " , dest = SimpleConfig . RPC_SOCKET_TYPE . key ( ) , default = None , help = " what socket type to which to bind RPC daemon " , choices = [ ' unix ' , ' tcp ' , ' auto ' ] )
parser_daemon . add_argument ( " --rpcsockpath " , dest = SimpleConfig . RPC_SOCKET_FILEPATH . key ( ) , help = " where to place RPC file socket " )
2015-05-31 22:42:34 +02:00
add_network_options ( parser_daemon )
2017-02-22 09:32:35 +01:00
add_global_options ( parser_daemon )
2015-05-31 22:42:34 +02:00
# commands
for cmdname in sorted ( known_commands . keys ( ) ) :
cmd = known_commands [ cmdname ]
2025-03-18 09:35:09 +01:00
p = subparsers . add_parser (
2025-05-28 15:25:26 +02:00
cmdname ,
description = cmd . description ,
help = cmd . short_description ,
2025-07-25 12:42:50 +02:00
formatter_class = argparse . RawDescriptionHelpFormatter ,
2025-07-25 12:48:51 +02:00
epilog = " Run ' electrum -h ' to see the list of global options " ,
2025-03-18 09:35:09 +01:00
)
2015-06-01 00:17:50 +02:00
for optname , default in zip ( cmd . options , cmd . defaults ) :
2025-03-18 09:35:09 +01:00
if optname in [ ' wallet_path ' , ' wallet ' , ' plugin ' ] :
2025-03-16 12:25:59 +01:00
continue
2025-03-18 11:15:16 +01:00
if optname == ' password ' :
2025-05-29 15:44:53 +02:00
p . add_argument ( " --password " , dest = ' password ' , help = " Wallet password. Use ' --password : ' if you want a prompt. " )
2025-03-18 11:15:16 +01:00
continue
help = cmd . arg_descriptions . get ( optname )
if not help :
2025-11-05 14:34:37 +01:00
print ( f ' undocumented argument { cmdname } :: { optname } ' , file = sys . stderr )
2018-10-10 20:29:51 +02:00
action = " store_true " if default is False else ' store '
2015-05-31 22:42:34 +02:00
if action == ' store ' :
2025-03-18 11:15:16 +01:00
type_descriptor = cmd . arg_types . get ( optname )
_type = arg_types . get ( type_descriptor , str )
p . add_argument ( ' -- ' + optname , dest = optname , action = action , default = default , help = help , type = _type )
2015-05-31 22:42:34 +02:00
else :
2025-03-18 11:15:16 +01:00
p . add_argument ( ' -- ' + optname , dest = optname , action = action , default = default , help = help )
2025-03-18 09:35:09 +01:00
add_global_options ( p , suppress = True )
2015-05-31 22:42:34 +02:00
for param in cmd . params :
2019-09-06 11:06:08 +02:00
if param in [ ' wallet_path ' , ' wallet ' ] :
2019-09-05 17:57:51 +02:00
continue
2025-03-18 11:15:16 +01:00
help = cmd . arg_descriptions . get ( param )
if not help :
2025-11-05 14:34:37 +01:00
print ( f ' undocumented argument { cmdname } :: { param } ' , file = sys . stderr )
2025-03-18 11:15:16 +01:00
type_descriptor = cmd . arg_types . get ( param )
_type = arg_types . get ( type_descriptor )
if help is not None and _type is None :
2025-11-05 14:34:37 +01:00
print ( f ' unknown type \' { _type } \' for { cmdname } :: { param } ' , file = sys . stderr )
2025-03-18 11:15:16 +01:00
p . add_argument ( param , help = help , type = _type )
2015-06-03 11:34:52 +02:00
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 )
2015-05-31 22:42:34 +02:00
# 'gui' is the default command
2025-03-15 11:40:26 +01:00
# note: set_default_subparser modifies sys.argv
2015-05-31 22:42:34 +02:00
parser . set_default_subparser ( ' gui ' )
return parser