|
|
|
|
@ -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) |