Browse Source

Add jmwalletd script as RPC server.

Uses Klein to provide HTTP server support.
Adds cookie based auth to requests (made JWT token
based in later commits).
Basic routes are: /unlock, /lock, /display,
/create of wallet.
Encapsulates WalletDaemon as a Service
Add snicker receiver service start, stop
Adds yg/maker function as stoppable service.

Adds a JMShutdown command to
the AMP protocol, allowing a clean shutdown
of a long running bot (e.g. maker) by shutting
down its message channel connections, without
shutting down the entire process.

Adds payment(direct send) request, first draft
master
Adam Gibson 5 years ago
parent
commit
80e17df4a6
No known key found for this signature in database
GPG Key ID: 141001A1AF77F20B
  1. 118
      docs/JSON-RPC-API-using-jmwalletd.md
  2. 7
      jmbase/jmbase/commands.py
  3. 9
      jmclient/jmclient/__init__.py
  4. 49
      jmclient/jmclient/client_protocol.py
  5. 47
      jmclient/jmclient/snicker_receiver.py
  6. 33
      jmclient/jmclient/wallet_utils.py
  7. 34
      jmclient/jmclient/yieldgenerator.py
  8. 6
      jmdaemon/jmdaemon/daemon_protocol.py
  9. 12
      jmdaemon/jmdaemon/irc.py
  10. 420
      scripts/jmwalletd.py

118
docs/JSON-RPC-API-using-jmwalletd.md

File diff suppressed because one or more lines are too long

7
jmbase/jmbase/commands.py

@ -70,6 +70,13 @@ class JMMsgSignatureVerify(JMCommand):
(b'fullmsg', Unicode()),
(b'hostid', Unicode())]
class JMShutdown(JMCommand):
""" Requests shutdown of the current
message channel connections (to be used
when the client is shutting down).
"""
arguments = []
"""TAKER specific commands
"""

9
jmclient/jmclient/__init__.py

@ -31,7 +31,8 @@ from .blockchaininterface import (BlockchainInterface,
from .snicker_receiver import SNICKERError, SNICKERReceiver
from .client_protocol import (JMTakerClientProtocol, JMClientProtocolFactory,
start_reactor, SNICKERClientProtocolFactory,
BIP78ClientProtocolFactory)
BIP78ClientProtocolFactory,
get_daemon_serving_params)
from .podle import (set_commitment_file, get_commitment_file,
add_external_commitments,
PoDLE, generate_podle, get_podle_commitments,
@ -58,8 +59,10 @@ from .wallet_utils import (
wallet_change_passphrase)
from .wallet_service import WalletService
from .maker import Maker
from .yieldgenerator import YieldGenerator, YieldGeneratorBasic, ygmain
from .payjoin import (parse_payjoin_setup, send_payjoin,
from .yieldgenerator import YieldGenerator, YieldGeneratorBasic, ygmain, \
YieldGeneratorService
from .snicker_receiver import SNICKERError, SNICKERReceiver, SNICKERReceiverService
from .payjoin import (parse_payjoin_setup, send_payjoin, PayjoinServer,
JMBIP78ReceiverManager)
# Set default logging handler to avoid "No handler found" warnings.

49
jmclient/jmclient/client_protocol.py

@ -20,6 +20,15 @@ from jmclient import (jm_single, get_irc_mchannels,
SNICKERReceiver, process_shutdown)
import jmbitcoin as btc
# module level variable representing the port
# on which the daemon is running.
# note that this var is only set if we are running
# client+daemon in one process.
daemon_serving_port = -1
daemon_serving_host = ""
def get_daemon_serving_params():
return (daemon_serving_host, daemon_serving_port)
jlog = get_log()
@ -366,6 +375,15 @@ class JMClientProtocol(BaseClientProtocol):
tx=tx)
self.defaultCallbacks(d)
def request_mc_shutdown(self):
""" To ensure that lingering message channel
connections are shut down when the client itself
is shutting down.
"""
d = self.callRemote(commands.JMShutdown)
self.defaultCallbacks(d)
return {'accepted': True}
class JMMakerClientProtocol(JMClientProtocol):
def __init__(self, factory, maker, nick_priv=None):
self.factory = factory
@ -779,9 +797,9 @@ def start_reactor(host, port, factory=None, snickerfactory=None,
#(Cannot start the reactor in tests)
#Not used in prod (twisted logging):
#startLogging(stdout)
usessl = True if jm_single().config.get("DAEMON",
"use_ssl") != 'false' else False
global daemon_serving_host
global daemon_serving_port
usessl = True if jm_single().config.get("DAEMON", "use_ssl") != 'false' else False
jmcport, snickerport, bip78port = [port]*3
if daemon:
try:
@ -821,10 +839,13 @@ def start_reactor(host, port, factory=None, snickerfactory=None,
p[0] += 1
return p[0]
if jm_coinjoin:
# TODO either re-apply this port incrementing logic
# to other protocols, or re-work how the ports work entirely.
jmcport = start_daemon_on_port(port_a, dfactory, "Joinmarket", 0)
daemon_serving_port = jmcport
daemon_serving_host = host
# (See above) For now these other two are just on ports that are 1K offsets.
if snickerfactory:
snickerport = start_daemon_on_port(port_a, sdfactory, "SNICKER", 1000) - 1000
@ -840,17 +861,17 @@ def start_reactor(host, port, factory=None, snickerfactory=None,
# Note the reactor.connect*** entries do not include BIP78 which
# starts in jmclient.payjoin:
if usessl:
if factory:
reactor.connectSSL(host, jmcport, factory, ClientContextFactory())
if snickerfactory:
reactor.connectSSL(host, snickerport, snickerfactory,
ClientContextFactory())
else:
if factory:
reactor.connectTCP(host, jmcport, factory)
if snickerfactory:
reactor.connectTCP(host, snickerport, snickerfactory)
if usessl:
if factory:
reactor.connectSSL(host, jmcport, factory, ClientContextFactory())
if snickerfactory:
reactor.connectSSL(host, snickerport, snickerfactory,
ClientContextFactory())
else:
if factory:
reactor.connectTCP(host, jmcport, factory)
if snickerfactory:
reactor.connectTCP(host, snickerport, snickerfactory)
if rs:
if not gui:
reactor.run(installSignalHandlers=ish)

47
jmclient/jmclient/snicker_receiver.py

@ -1,5 +1,8 @@
#! /usr/bin/env python
import os
from twisted.application.service import Service
from twisted.internet import task
import jmbitcoin as btc
from jmclient.configure import jm_single
from jmbase import (get_log, utxo_to_utxostr,
@ -11,7 +14,45 @@ jlog = get_log()
class SNICKERError(Exception):
pass
class SNICKERReceiver(Service):
class SNICKERReceiverService(Service):
def __init__(self, receiver):
assert isinstance(receiver, SNICKERReceiver)
self.receiver = receiver
# main monitor loop
self.monitor_loop = task.LoopingCall(self.receiver.poll_for_proposals)
def startService(self):
""" Encapsulates start up actions.
This service depends on the receiver's
wallet service to start, so wait for that.
"""
self.wait_for_wallet = task.LoopingCall(self.wait_for_wallet_sync)
self.wait_for_wallet.start(5.0)
def wait_for_wallet_sync(self):
if self.receiver.wallet_service.isRunning():
jlog.info("SNICKER service starting because wallet service is up.")
self.wait_for_wallet.stop()
self.monitor_loop.start(5.0)
super().startService()
def stopService(self, wallet=False):
""" Encapsulates shut down actions.
Optionally also shut down the underlying
wallet service (default False).
"""
if self.monitor_loop:
self.monitor_loop.stop()
if wallet:
self.receiver.wallet_service.stopService()
super().stopService()
def isRunning(self):
if self.running == 1:
return True
return False
class SNICKERReceiver(object):
supported_flags = []
def __init__(self, wallet_service, acceptance_callback=None,
@ -66,6 +107,10 @@ class SNICKERReceiver(Service):
def default_info_callback(self, msg):
jlog.info(msg)
if not os.path.exists(self.proposals_source):
with open(self.proposals_source, "wb") as f:
jlog.info("created proposals source file.")
def default_acceptance_callback(self, our_ins, their_ins,
our_outs, their_outs):

33
jmclient/jmclient/wallet_utils.py

@ -177,6 +177,12 @@ class WalletViewEntry(WalletViewBase):
extradata = self.serialize_extra_data()
return self.serclass(self.separator.join([left, addr, amounts, extradata]))
def serialize_json(self):
return {"hd_path": self.wallet_path_repr,
"address": self.serialize_address(),
"amount": self.serialize_amounts(),
"labels": self.serialize_extra_data()}
def serialize_wallet_position(self):
return self.wallet_path_repr.ljust(20)
@ -229,6 +235,14 @@ class WalletViewBranch(WalletViewBase):
lines.append(footer)
return self.serclass(entryseparator.join(lines))
def serialize_json(self, summarize=False):
if summarize:
return {}
else:
return {"branch": self.serialize_branch_header(),
"balance": self.get_fmt_balance(),
"entries": [x.serialize_json() for x in self.branchentries]}
def serialize_branch_header(self):
start = "external addresses" if self.address_type == 0 else "internal addresses"
if self.address_type == -1:
@ -263,6 +277,14 @@ class WalletViewAccount(WalletViewBase):
return self.serclass(entryseparator.join([header] + [
x.serialize(entryseparator) for x in self.branches] + [footer]))
def serialize_json(self, summarize=False):
result = {"account": str(self.account),
"account_balance": self.get_fmt_balance()}
if summarize:
return result
result["branches"] = [x.serialize_json() for x in self.branches]
return result
class WalletView(WalletViewBase):
def __init__(self, wallet_path_repr, accounts, wallet_name="JM wallet",
serclass=str, custom_separator=None):
@ -286,6 +308,10 @@ class WalletView(WalletViewBase):
return self.serclass(entryseparator.join([header] + [
x.serialize(entryseparator, summarize=False) for x in self.accounts] + [footer]))
def serialize_json(self, summarize=False):
return {"wallet_name": self.wallet_name,
"total_balance": self.get_fmt_balance(),
"accounts": [x.serialize_json(summarize=summarize) for x in self.accounts]}
def get_tx_info(txid, tx_cache=None):
"""
@ -393,7 +419,7 @@ def wallet_showutxos(wallet_service, showprivkey):
def wallet_display(wallet_service, showprivkey, displayall=False,
serialized=True, summarized=False, mixdepth=None):
serialized=True, summarized=False, mixdepth=None, jsonified=False):
"""build the walletview object,
then return its serialization directly if serialized,
else return the WalletView object.
@ -546,7 +572,10 @@ def wallet_display(wallet_service, showprivkey, displayall=False,
path = wallet_service.get_path_repr(wallet_service.get_path())
walletview = WalletView(path, acctlist)
if serialized:
return walletview.serialize(summarize=summarized)
if jsonified:
return walletview.serialize_json(summarize=summarized)
else:
return walletview.serialize(summarize=summarized)
else:
return walletview

34
jmclient/jmclient/yieldgenerator.py

@ -6,6 +6,7 @@ import time
import abc
import base64
from twisted.python.log import startLogging
from twisted.application.service import Service
from optparse import OptionParser
from jmbase import get_log
from jmclient import (Maker, jm_single, load_program_config,
@ -263,6 +264,39 @@ class YieldGeneratorBasic(YieldGenerator):
cjoutmix = (input_mixdepth + 1) % (self.wallet_service.mixdepth + 1)
return self.wallet_service.get_internal_addr(cjoutmix)
class YieldGeneratorService(Service):
def __init__(self, wallet_service, daemon_host, daemon_port, yg_config):
self.wallet_service = wallet_service
self.daemon_host = daemon_host
self.daemon_port = daemon_port
self.yg_config = yg_config
self.yieldgen = None
def startService(self):
""" We instantiate the Maker class only
here as its constructor will automatically
create orders based on the wallet.
Note makers already intrinsically handle
not-yet-synced wallet services, so there is
no need to check this here.
"""
# TODO genericise to any YG class:
self.yieldgen = YieldGeneratorBasic(self.wallet_service, self.yg_config)
self.clientfactory = JMClientProtocolFactory(self.yieldgen, proto_type="MAKER")
# here 'start_reactor' does not start the reactor but instantiates
# the connection to the daemon backend; note daemon=False, i.e. the daemon
# backend is assumed to be started elsewhere; we just connect to it with a client.
start_reactor(self.daemon_host, self.daemon_port, self.clientfactory, rs=False)
super().startService()
def stopService(self):
""" TODO need a method exposed to gracefully
shut down a maker bot.
"""
if self.running:
jlog.info("Shutting down YieldGenerator service.")
self.clientfactory.proto_client.request_mc_shutdown()
super().stopService()
def ygmain(ygclass, nickserv_password='', gaplimit=6):
import sys

6
jmdaemon/jmdaemon/daemon_protocol.py

@ -585,6 +585,12 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
self.mcc.on_verified_privmsg(nick, fullmsg, hostid)
return {'accepted': True}
@JMShutdown.responder
def on_JM_SHUTDOWN(self):
self.mc_shutdown()
self.jm_state = 0
return {'accepted': True}
"""Taker specific responders
"""

12
jmdaemon/jmdaemon/irc.py

@ -105,6 +105,10 @@ class IRCMessageChannel(MessageChannel):
self.tx_irc_client = None
#TODO can be configuration var, how long between reconnect attempts:
self.reconnect_interval = 10
# service is used to wrap endpoints for Tor connections:
self.reconnecting_service = None
#implementation of abstract base class methods;
#these are mostly but not exclusively acting as pass through
#to the wrapped twisted IRC client protocol
@ -115,6 +119,8 @@ class IRCMessageChannel(MessageChannel):
def shutdown(self):
self.tx_irc_client.quit()
self.give_up = True
if self.reconnecting_service:
self.reconnecting_service.stopService()
def _pubmsg(self, msg):
self.tx_irc_client._pubmsg(msg)
@ -157,8 +163,8 @@ class IRCMessageChannel(MessageChannel):
use_tls = False
ircEndpoint = TorSocksEndpoint(torEndpoint, self.serverport[0],
self.serverport[1], tls=use_tls)
myRS = ClientService(ircEndpoint, factory)
myRS.startService()
self.reconnecting_service = ClientService(ircEndpoint, factory)
self.reconnecting_service.startService()
else:
try:
factory = TxIRCFactory(self)
@ -203,7 +209,7 @@ class txIRC_Client(irc.IRCClient, object):
def connectionLost(self, reason=protocol.connectionDone):
wlog("INFO", "Lost IRC connection to: " + str(self.hostname)
+ " . Should reconnect automatically soon.")
if self.wrapper.on_disconnect:
if not self.wrapper.give_up and self.wrapper.on_disconnect:
reactor.callLater(0.0, self.wrapper.on_disconnect, self.wrapper)
return irc.IRCClient.connectionLost(self, reason)

420
scripts/jmwalletd.py

@ -0,0 +1,420 @@
#! /usr/bin/env python
import datetime
import os
import time
import abc
import json
import atexit
from io import BytesIO
from twisted.python.log import startLogging
from twisted.internet import endpoints, reactor, ssl, task
from twisted.web.server import Site
from twisted.application.service import Service
from klein import Klein
from optparse import OptionParser
from jmbase import get_log
from jmbitcoin import human_readable_transaction
from jmclient import Maker, jm_single, load_program_config, \
JMClientProtocolFactory, start_reactor, calc_cj_fee, \
WalletService, add_base_options, get_wallet_path, direct_send, \
open_test_wallet_maybe, wallet_display, SegwitLegacyWallet, \
SegwitWallet, get_daemon_serving_params, YieldGeneratorService, \
SNICKERReceiverService, SNICKERReceiver, create_wallet, \
StorageError, StoragePasswordError
from jmbase.support import EXIT_ARGERROR, EXIT_FAILURE
jlog = get_log()
# for debugging; twisted.web.server.Request objects do not easily serialize:
def print_req(request):
print(request)
print(request.method)
print(request.uri)
print(request.args)
print(request.path)
print(request.content)
print(list(request.requestHeaders.getAllRawHeaders()))
class NotAuthorized(Exception):
pass
class NoWalletFound(Exception):
pass
class InvalidRequestFormat(Exception):
pass
class BackendNotReady(Exception):
pass
# error class for services which are only
# started once:
class ServiceAlreadyStarted(Exception):
pass
# for the special case of the wallet service:
class WalletAlreadyUnlocked(Exception):
pass
class ServiceNotStarted(Exception):
pass
def get_ssl_context(cert_directory):
"""Construct an SSL context factory from the user's privatekey/cert.
TODO:
Currently just hardcoded for tests.
"""
return ssl.DefaultOpenSSLContextFactory(os.path.join(cert_directory, "key.pem"),
os.path.join(cert_directory, "cert.pem"))
def response(request, succeed=True, status=200, **kwargs):
"""
Build the response body as JSON and set the proper content-type
header.
"""
request.setHeader('Content-Type', 'application/json')
request.setHeader('Access-Control-Allow-Origin', '*')
request.setResponseCode(status)
return json.dumps(
[{'succeed': succeed, 'status': status, **kwargs}])
class JMWalletDaemon(Service):
""" This class functions as an HTTP/TLS server,
with acccess control, allowing a single client(user)
to control functioning of encapsulated Joinmarket services.
"""
app = Klein()
def __init__(self, port):
""" Port is the port to serve this daemon
(using HTTP/TLS).
"""
# cookie tracks single user's state.
self.cookie = None
self.port = port
# the collection of services which this
# daemon may switch on and off:
self.services = {}
# master single wallet service which we
# allow the client to start/stop.
self.services["wallet"] = None
# label for convenience:
self.wallet_service = self.services["wallet"]
# Client may start other services, but only
# one instance.
self.services["snicker"] = None
self.services["maker"] = None
# ensure shut down does not leave dangling services:
atexit.register(self.stopService)
def startService(self):
""" Encapsulates start up actions.
Here starting the TLS server.
"""
super().startService()
# we do not auto-start any service, including the base
# wallet service, since the client must actively request
# that with the appropriate credential (password).
reactor.listenSSL(self.port, Site(self.app.resource()),
contextFactory=get_ssl_context("."))
def stopService(self):
""" Encapsulates shut down actions.
"""
# Currently valid authorization tokens must be removed
# from the daemon:
self.cookie = None
# if the wallet-daemon is shut down, all services
# it encapsulates must also be shut down.
for name, service in self.services.items():
if service:
service.stopService()
super().stopService()
@app.handle_errors(NotAuthorized)
def not_authorized(self, request, failure):
request.setResponseCode(401)
return "Invalid credentials."
@app.handle_errors(NoWalletFound)
def no_wallet_found(self, request, failure):
request.setResponseCode(404)
return "No wallet loaded."
@app.handle_errors(BackendNotReady)
def backend_not_ready(self, request, failure):
request.setResponseCode(500)
return "Backend daemon not available"
@app.handle_errors(InvalidRequestFormat)
def invalid_request_format(self, request, failure):
request.setResponseCode(401)
return "Invalid request format."
@app.handle_errors(ServiceAlreadyStarted)
def service_already_started(self, request, failure):
request.setResponseCode(401)
return "Service already started."
@app.handle_errors(WalletAlreadyUnlocked)
def wallet_already_unlocked(self, request, failure):
request.setResponseCode(401)
return "Wallet already unlocked."
def service_not_started(self, request, failure):
request.setResponseCode(401)
return "Service cannot be stopped as it is not running."
def check_cookie(self, request):
request_cookie = request.getHeader(b"JMCookie")
if self.cookie != request_cookie:
jlog.warn("Invalid cookie: " + str(
request_cookie) + ", request rejected.")
raise NotAuthorized()
@app.route('/wallet/<string:walletname>/display', methods=['GET'])
def displaywallet(self, request, walletname):
print_req(request)
self.check_cookie(request)
if not self.wallet_service:
print("called display but no wallet loaded")
raise NoWalletFound()
else:
walletinfo = wallet_display(self.wallet_service, False, jsonified=True)
return response(request, walletname=walletname, walletinfo=walletinfo)
# handling CORS preflight for any route:
@app.route('/', branch=True, methods=['OPTIONS'])
def preflight(self, request):
print_req(request)
request.setHeader("Access-Control-Allow-Origin", "*")
request.setHeader("Access-Control-Allow-Methods", "POST")
# "Cookie" is reserved so we specifically allow our custom cookie using
# name "JMCookie".
request.setHeader("Access-Control-Allow-Headers", "Content-Type, JMCookie")
@app.route('/wallet/<string:walletname>/snicker/start', methods=['GET'])
def start_snicker(self, request, walletname):
self.check_cookie(request)
if not self.wallet_service:
raise NoWalletFound()
if self.services["snicker"] and self.services["snicker"].isRunning():
raise ServiceAlreadyStarted()
# TODO: allow client to inject acceptance callbacks to Receiver
self.services["snicker"] = SNICKERReceiverService(
SNICKERReceiver(self.wallet_service))
self.services["snicker"].startService()
# TODO waiting for startup seems perhaps not needed here?
return response(request, walletname=walletname)
@app.route('/wallet/<string:walletname>/snicker/stop', methods=['GET'])
def stop_snicker(self, request, walletname):
self.check_cookie(request)
if not self.wallet_service:
raise NoWalletFound()
if not self.services["snicker"]:
raise ServiceNotStarted()
self.services["snicker"].stopService()
return response(request, walletname=walletname)
@app.route('/wallet/<string:walletname>/taker/direct-send', methods=['POST'])
def send_direct(self, request, walletname):
""" Use the contents of the POST body to do a direct send from
the active wallet at the chosen mixdepth.
"""
assert isinstance(request.content, BytesIO)
payment_info_json = self.get_POST_body(request, ["mixdepth", "amount_sats",
"destination"])
if not payment_info_json:
raise InvalidRequestFormat()
if not self.wallet_service:
raise NoWalletFound()
tx = direct_send(self.wallet_service, payment_info_json["amount_sats"],
payment_info_json["mixdepth"],
optin_rbf=payment_info_json["optin_rbf"],
return_transaction=True)
return response(request, walletname=walletname,
txinfo=human_readable_transaction(tx))
@app.route('/wallet/<string:walletname>/maker/start', methods=['POST'])
def start_maker(self, request, walletname):
""" Use the configuration in the POST body to start the yield generator:
"""
assert isinstance(request.content, BytesIO)
config_json = self.get_POST_body(request, ["txfee", "cjfee_a", "cjfee_r",
"ordertype", "minsize"])
if not config_json:
raise InvalidRequestFormat()
if not self.wallet_service:
raise NoWalletFound()
# daemon must be up before this is started; check:
daemon_serving_host, daemon_serving_port = get_daemon_serving_params()
if daemon_serving_port == -1 or daemon_serving_host == "":
raise BackendNotReady()
self.services["maker"] = YieldGeneratorService(self.wallet_service,
daemon_serving_host, daemon_serving_port,
[config_json[x] for x in ["txfee", "cjfee_a",
"cjfee_r", "ordertype", "minsize"]])
self.services["maker"].startService()
return response(request, walletname=walletname)
@app.route('/wallet/<string:walletname>/maker/stop', methods=['GET'])
def stop_maker(self, request, walletname):
self.check_cookie(request)
if not self.wallet_service:
raise NoWalletFound()
if not self.services["maker"]:
raise ServiceNotStarted()
self.services["maker"].stopService()
return response(request, walletname=walletname)
@app.route('/wallet/<string:walletname>/lock', methods=['GET'])
def lockwallet(self, request, walletname):
print_req(request)
self.check_cookie(request)
if not self.wallet_service:
print("called lock but no wallet loaded")
raise NoWalletFound()
else:
self.wallet_service.stopService()
self.wallet_service = None
# success status implicit:
return response(request, walletname=walletname)
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.
If *any* of the keys are not present, return False, else
returns a dict of those key-value pairs.
"""
assert isinstance(request.content, BytesIO)
json_data = json.loads(request.content.read().decode("utf-8"))
retval = {}
for k in keys:
if k in json_data:
retval[k] = json_data[k]
else:
return False
return retval
@app.route('/wallet/create', methods=["POST"])
def createwallet(self, request):
print_req(request)
# we only handle one wallet at a time;
# if there is a currently unlocked wallet,
# refuse to process the request:
if self.wallet_service:
raise WalletAlreadyUnlocked()
request_data = self.get_POST_body(request,
["walletname", "password", "wallettype"])
if not request_data or request_data["wallettype"] not in [
"sw", "sw-legacy"]:
raise InvalidRequestFormat()
wallet_cls = SegwitWallet if request_data[
"wallettype"]=="sw" else SegwitLegacyWallet
# use the config's data location combined with the json
# data to construct the wallet path:
wallet_root_path = os.path.join(jm_single().datadir, "wallets")
wallet_name = os.path.join(wallet_root_path, request_data["walletname"])
try:
wallet = create_wallet(wallet_name, request_data["password"],
4, wallet_cls=wallet_cls)
except StorageError as e:
raise NotAuthorized(repr(e))
# finally, after the wallet is successfully created, we should
# start the wallet service:
return self.initialize_wallet_service(request, wallet)
def initialize_wallet_service(self, request, wallet):
""" Called only when the wallet has loaded correctly, so
authorization is passed, so set cookie for this wallet
(currently THE wallet, daemon does not yet support multiple).
This is maintained for as long as the daemon is active (i.e.
no expiry currently implemented), or until the user switches
to a new wallet.
"""
self.cookie = request.getHeader(b"JMCookie")
if self.cookie is None:
raise NotAuthorized("No cookie")
# the daemon blocks here until the wallet synchronization
# from the blockchain interface completes; currently this is
# fine as long as the client handles the response asynchronously:
self.wallet_service = WalletService(wallet)
while not self.wallet_service.synced:
self.wallet_service.sync_wallet(fast=True)
self.wallet_service.startService()
# now that the WalletService instance is active and ready to
# respond to requests, we return the status to the client:
return response(request,
walletname=self.wallet_service.get_wallet_name(),
already_loaded=False)
@app.route('/wallet/<string:walletname>/unlock', methods=['POST'])
def unlockwallet(self, request, walletname):
print_req(request)
assert isinstance(request.content, BytesIO)
auth_json = self.get_POST_body(request, ["password"])
if not auth_json:
raise InvalidRequestFormat()
password = auth_json["password"]
if self.wallet_service is None:
wallet_path = get_wallet_path(walletname, None)
try:
wallet = open_test_wallet_maybe(
wallet_path, walletname, 4,
password=password.encode("utf-8"),
ask_for_password=False)
except StoragePasswordError:
raise NotAuthorized("invalid password")
except StorageError as e:
# e.g. .lock file exists:
raise NotAuthorized(repr(e))
return self.initialize_wallet_service(request, wallet)
else:
print('wallet was already unlocked.')
return response(request,
walletname=self.wallet_service.get_wallet_name(),
already_loaded=True)
def jmwalletd_main():
import sys
parser = OptionParser(usage='usage: %prog [options] [wallet file]')
parser.add_option('-p', '--port', action='store', type='int',
dest='port', default=28183,
help='the port over which to serve RPC, default 28183')
# TODO: remove the non-relevant base options:
add_base_options(parser)
(options, args) = parser.parse_args()
load_program_config(config_path=options.datadir)
if jm_single().bc_interface is None:
jlog.error("Running jmwallet-daemon requires configured " +
"blockchain source.")
sys.exit(EXIT_FAILURE)
jlog.info("Starting jmwalletd on port: " + str(options.port))
jm_wallet_daemon = JMWalletDaemon(options.port)
jm_wallet_daemon.startService()
nodaemon = jm_single().config.getint("DAEMON", "no_daemon")
daemon = True if nodaemon == 1 else False
if jm_single().config.get("BLOCKCHAIN", "network") in ["regtest", "testnet"]:
startLogging(sys.stdout)
start_reactor(jm_single().config.get("DAEMON", "daemon_host"),
jm_single().config.getint("DAEMON", "daemon_port"),
None, daemon=daemon)
if __name__ == "__main__":
jmwalletd_main()
Loading…
Cancel
Save