@ -143,8 +143,6 @@ class JMWalletDaemon(Service):
# allow the client to start/stop.
self . services [ " wallet " ] = None
self . wallet_name = " None "
# label for convenience:
self . wallet_service = self . services [ " wallet " ]
# Client may start other services, but only
# one instance.
self . services [ " snicker " ] = None
@ -324,6 +322,23 @@ class JMWalletDaemon(Service):
request_cookie ) + " , request rejected. " )
raise NotAuthorized ( )
def set_token ( self , wallet_name ) :
""" This function creates a new JWT token and sets it as our
' cookie ' for API and WS . Note this always creates a new fresh token ,
there is no option to manually set it , intentionally .
"""
# any random secret is OK, as long as it is not deducible/predictable:
secret_key = bintohex ( os . urandom ( 16 ) )
encoded_token = jwt . encode ( { " wallet " : wallet_name ,
" exp " : datetime . datetime . utcnow (
) + datetime . timedelta ( minutes = 30 ) } ,
secret_key )
self . cookie = encoded_token . strip ( )
# We want to make sure that any websocket clients use the correct
# token. The wss_factory should have been created on JMWalletDaemon
# startup, so any failure to exist here is a logic error:
self . wss_factory . valid_token = self . cookie
def get_POST_body ( self , request , keys ) :
""" given a request object, retrieve values corresponding
to keys keys in a dict , assuming they were encoded using JSON .
@ -349,24 +364,15 @@ class JMWalletDaemon(Service):
Here we must also register transaction update callbacks , to fire
events in the websocket connection .
"""
if self . wallet_service :
if self . services [ " wallet " ] :
# we allow a new successful authorization (with password)
# to shut down the currently running service(s), if there
# are any.
# This will stop all supporting services and wipe
# state (so wallet, maker service and cookie/token):
self . stopService ( )
# any random secret is OK, as long as it is not deducible/predictable:
secret_key = bintohex ( os . urandom ( 16 ) )
encoded_token = jwt . encode ( { " wallet " : wallet_name ,
" exp " : datetime . datetime . utcnow (
) + datetime . timedelta ( minutes = 30 ) } ,
secret_key )
encoded_token = encoded_token . strip ( )
self . cookie = encoded_token
if self . cookie is None :
raise NotAuthorized ( " No cookie " )
self . wallet_service = WalletService ( wallet )
self . services [ " wallet " ] = WalletService ( wallet )
# restart callback needed, otherwise wallet creation will
# automatically lead to shutdown.
# TODO: this means that it's possible, in non-standard usage
@ -375,28 +381,26 @@ class JMWalletDaemon(Service):
# or requesting rescans, none are implemented yet.
def dummy_restart_callback ( msg ) :
jlog . warn ( " Ignoring rescan request from backend wallet service: " + msg )
self . wallet_service . add_restart_callback ( dummy_restart_callback )
self . services [ " wallet " ] . add_restart_callback ( dummy_restart_callback )
self . wallet_name = wallet_name
self . wallet_service . register_callbacks (
self . services [ " wallet " ] . register_callbacks (
[ self . wss_factory . sendTxNotification ] , None )
self . wallet_service . startService ( )
# now that the base WalletService is started, we want to
# make sure that any websocket clients use the correct
# token. The wss_factory should have been created on JMWalletDaemon
# startup, so any failure to exist here is a logic error:
self . wss_factory . valid_token = encoded_token
self . services [ " wallet " ] . startService ( )
# now that the WalletService instance is active and ready to
# respond to requests, we return the status to the client:
# First, prepare authentication for the calling client:
self . set_token ( wallet_name )
if ( ' seedphrase ' in kwargs ) :
return make_jmwalletd_response ( request ,
status = 201 ,
walletname = self . wallet_name ,
token = encoded_token ,
token = self . cookie ,
seedphrase = kwargs . get ( ' seedphrase ' ) )
else :
return make_jmwalletd_response ( request ,
walletname = self . wallet_name ,
token = encoded_token )
token = self . cookie )
def taker_finished ( self , res , fromtx = False , waittime = 0.0 , txdetails = None ) :
# This is a slimmed down version compared with what is seen in
@ -468,14 +472,14 @@ class JMWalletDaemon(Service):
def displaywallet ( self , request , walletname ) :
print_req ( request )
self . check_cookie ( request )
if not self . wallet_service :
if not self . services [ " wallet " ] :
jlog . warn ( " displaywallet called, but no wallet loaded " )
raise NoWalletFound ( )
if not self . wallet_name == walletname :
jlog . warn ( " called displaywallet with wrong wallet " )
raise InvalidRequestFormat ( )
else :
walletinfo = wallet_display ( self . wallet_service , False , jsonified = True )
walletinfo = wallet_display ( self . services [ " wallet " ] , False , jsonified = True )
return make_jmwalletd_response ( request , walletname = walletname , walletinfo = walletinfo )
@app . route ( ' /session ' , methods = [ ' GET ' ] )
@ -489,8 +493,8 @@ class JMWalletDaemon(Service):
session = not self . cookie == None
maker_running = self . coinjoin_state == CJ_MAKER_RUNNING
coinjoin_in_process = self . coinjoin_state == CJ_TAKER_RUNNING
if self . wallet_service :
if self . wallet_service . isRunning ( ) :
if self . services [ " wallet " ] :
if self . services [ " wallet " ] . isRunning ( ) :
wallet_name = self . wallet_name
else :
wallet_name = " not yet loaded "
@ -512,12 +516,12 @@ class JMWalletDaemon(Service):
" destination " ] )
if not payment_info_json :
raise InvalidRequestFormat ( )
if not self . wallet_service :
if not self . services [ " wallet " ] :
raise NoWalletFound ( )
if not self . wallet_name == walletname :
raise InvalidRequestFormat ( )
try :
tx = direct_send ( self . wallet_service ,
tx = direct_send ( self . services [ " wallet " ] ,
int ( payment_info_json [ " amount_sats " ] ) ,
int ( payment_info_json [ " mixdepth " ] ) ,
destination = payment_info_json [ " destination " ] ,
@ -542,7 +546,7 @@ class JMWalletDaemon(Service):
" ordertype " , " minsize " ] )
if not config_json :
raise InvalidRequestFormat ( )
if not self . wallet_service :
if not self . services [ " wallet " ] :
raise NoWalletFound ( )
if not self . wallet_name == walletname :
raise InvalidRequestFormat ( )
@ -563,7 +567,7 @@ class JMWalletDaemon(Service):
config_json [ " cjfee_factor " ] = None
config_json [ " size_factor " ] = None
self . services [ " maker " ] = YieldGeneratorService ( self . wallet_service ,
self . services [ " maker " ] = YieldGeneratorService ( self . services [ " wallet " ] ,
dhost , dport ,
[ config_json [ x ] for x in [ " txfee " , " cjfee_a " ,
" cjfee_r " , " ordertype " , " minsize " ,
@ -585,7 +589,7 @@ class JMWalletDaemon(Service):
# sync has already happened (this is different from CLI yg).
# note: an edge case of dusty amounts is lost here; it will get
# picked up by Maker.try_to_create_my_orders().
if not len ( self . wallet_service . get_balance_by_mixdepth (
if not len ( self . services [ " wallet " ] . get_balance_by_mixdepth (
verbose = False , minconfs = 1 ) ) > 0 :
raise NotEnoughCoinsForMaker ( )
@ -600,7 +604,7 @@ class JMWalletDaemon(Service):
@app . route ( ' /wallet/<string:walletname>/maker/stop ' , methods = [ ' GET ' ] )
def stop_maker ( self , request , walletname ) :
self . check_cookie ( request )
if not self . wallet_service :
if not self . services [ " wallet " ] :
raise NoWalletFound ( )
if not self . wallet_name == walletname :
raise InvalidRequestFormat ( )
@ -643,19 +647,20 @@ class JMWalletDaemon(Service):
def lockwallet ( self , request , walletname ) :
print_req ( request )
self . check_cookie ( request )
if self . wallet_service and not self . wallet_name == walletname :
if self . services [ " wallet " ] and not self . wallet_name == walletname :
raise InvalidRequestFormat ( )
if not self . wallet_service :
if not self . services [ " wallet " ] :
jlog . warn ( " Called lock, but no wallet loaded " )
# we could raise NoWalletFound here, but is
# easier for clients if they can gracefully call
# lock multiple times:
already_locked = True
else :
self . wallet_service . stopService ( )
self . services [ " wallet " ] . stopService ( )
self . cookie = None
self . wss_factory . valid_token = None
self . wallet_service = None
self . services [ " wallet " ] = None
self . wallet_name = None
already_locked = False
return make_jmwalletd_response ( request , walletname = walletname ,
already_locked = already_locked )
@ -666,7 +671,7 @@ class JMWalletDaemon(Service):
# we only handle one wallet at a time;
# if there is a currently unlocked wallet,
# refuse to process the request:
if self . wallet_service :
if self . services [ " wallet " ] :
raise WalletAlreadyUnlocked ( )
request_data = self . get_POST_body ( request ,
[ " walletname " , " password " , " wallettype " ] )
@ -716,8 +721,44 @@ class JMWalletDaemon(Service):
if not auth_json :
raise InvalidRequestFormat ( )
password = auth_json [ " password " ]
wallet_path = get_wallet_path ( walletname , None )
# for someone trying to re-access the wallet, using their
# password as authentication, and get a fresh token, we still
# need to authenticate against the password, but we cannot directly
# re-open the wallet file (it is currently locked), but we don't
# yet want to bother to shut down services - because if their
# authentication is successful, we can happily leave everything
# running (wallet service and e.g. a yieldgenerator).
# Hence here, if it is the same name, we do a read-only open
# and proceed to issue a new token if the open is successful,
# otherwise error.
if walletname == self . wallet_name :
try :
# returned wallet object is ditched:
open_test_wallet_maybe (
wallet_path , walletname , 4 ,
password = password . encode ( " utf-8 " ) ,
ask_for_password = False ,
read_only = True )
except StoragePasswordError :
# actually effects authentication
raise NotAuthorized ( )
except StorageError :
# wallet is not openable, this should not happen
raise NoWalletFound ( )
except Exception :
# wallet file doesn't exist or is wrong format,
# this also shouldn't happen so raise:
raise NoWalletFound ( )
# no exceptions raised means we just return token:
self . set_token ( self . wallet_name )
return make_jmwalletd_response ( request ,
walletname = self . wallet_name ,
token = self . cookie )
# This is a different wallet than the one currently open;
# try to open it, then initialize the service(s):
try :
wallet = open_test_wallet_maybe (
wallet_path , walletname , 4 ,
@ -756,7 +797,7 @@ class JMWalletDaemon(Service):
@app . route ( ' /wallet/<string:walletname>/address/new/<string:mixdepth> ' , methods = [ ' GET ' ] )
def getaddress ( self , request , walletname , mixdepth ) :
self . check_cookie ( request )
if not self . wallet_service :
if not self . services [ " wallet " ] :
raise NoWalletFound ( )
if not self . wallet_name == walletname :
raise InvalidRequestFormat ( )
@ -764,19 +805,19 @@ class JMWalletDaemon(Service):
mixdepth = int ( mixdepth )
except ValueError :
raise InvalidRequestFormat ( )
address = self . wallet_service . get_external_addr ( mixdepth )
address = self . services [ " wallet " ] . get_external_addr ( mixdepth )
return make_jmwalletd_response ( request , address = address )
@app . route ( ' /wallet/<string:walletname>/address/timelock/new/<string:lockdate> ' , methods = [ ' GET ' ] )
def gettimelockaddress ( self , request , walletname , lockdate ) :
self . check_cookie ( request )
if not self . wallet_service :
if not self . services [ " wallet " ] :
raise NoWalletFound ( )
if not self . wallet_name == walletname :
raise InvalidRequestFormat ( )
try :
timelockaddress = wallet_gettimelockaddress (
self . wallet_service . wallet , lockdate )
self . services [ " wallet " ] . wallet , lockdate )
except Exception :
raise InvalidRequestFormat ( )
if timelockaddress == " " :
@ -847,7 +888,7 @@ class JMWalletDaemon(Service):
# note: this does not raise or fail if the applied
# disable state (true/false) is the same as the current
# one; that is accepted and not an error.
self . wallet_service . disable_utxo ( txid , index , to_disable )
self . services [ " wallet " ] . disable_utxo ( txid , index , to_disable )
except AssertionError :
# should be impossible because format checked by
# utxostr_to_utxo:
@ -865,13 +906,13 @@ class JMWalletDaemon(Service):
@app . route ( ' /wallet/<string:walletname>/utxos ' , methods = [ ' GET ' ] )
def listutxos ( self , request , walletname ) :
self . check_cookie ( request )
if not self . wallet_service :
if not self . services [ " wallet " ] :
raise NoWalletFound ( )
if not self . wallet_name == walletname :
raise InvalidRequestFormat ( )
# note: the output of `showutxos` is already a string for CLI;
# but we return json:
utxos = json . loads ( wallet_showutxos ( self . wallet_service , False ) )
utxos = json . loads ( wallet_showutxos ( self . services [ " wallet " ] , False ) )
utxos_response = self . get_listutxos_response ( utxos )
return make_jmwalletd_response ( request , utxos = utxos_response )
@ -879,7 +920,7 @@ class JMWalletDaemon(Service):
@app . route ( ' /wallet/<string:walletname>/taker/stop ' , methods = [ ' GET ' ] )
def stopcoinjoin ( self , request , walletname ) :
self . check_cookie ( request )
if not self . wallet_service :
if not self . services [ " wallet " ] :
raise NoWalletFound ( )
if not self . wallet_name == walletname :
raise InvalidRequestFormat ( )
@ -892,7 +933,7 @@ class JMWalletDaemon(Service):
@app . route ( ' /wallet/<string:walletname>/taker/coinjoin ' , methods = [ ' POST ' ] )
def docoinjoin ( self , request , walletname ) :
self . check_cookie ( request )
if not self . wallet_service :
if not self . services [ " wallet " ] :
raise NoWalletFound ( )
if not self . wallet_name == walletname :
raise InvalidRequestFormat ( )
@ -928,7 +969,7 @@ class JMWalletDaemon(Service):
raise ConfigNotPresent ( )
max_cj_fee = get_max_cj_fee_values ( jm_single ( ) . config ,
None , user_callback = dummy_user_callback )
self . taker = Taker ( self . wallet_service , schedule ,
self . taker = Taker ( self . services [ " wallet " ] , schedule ,
max_cj_fee = max_cj_fee ,
callbacks = ( self . filter_orders_callback ,
None , self . taker_finished ) )
@ -948,9 +989,9 @@ class JMWalletDaemon(Service):
@app . route ( ' /wallet/<walletname>/getseed ' , methods = [ ' GET ' ] )
def getseed ( self , request , walletname ) :
self . check_cookie ( request )
if not self . wallet_service :
if not self . services [ " wallet " ] :
raise NoWalletFound ( )
if not self . wallet_name == walletname :
raise InvalidRequestFormat ( )
seedphrase , _ = self . wallet_service . get_mnemonic_words ( )
seedphrase , _ = self . services [ " wallet " ] . get_mnemonic_words ( )
return make_jmwalletd_response ( request , seedphrase = seedphrase )