mirror of
https://github.com/JoinMarket-Org/joinmarket-clientserver.git
synced 2026-08-17 13:07:51 +02:00
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
This commit is contained in:
parent
868e188206
commit
80e17df4a6
10 changed files with 712 additions and 23 deletions
118
docs/JSON-RPC-API-using-jmwalletd.md
Normal file
118
docs/JSON-RPC-API-using-jmwalletd.md
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Normal file
420
scripts/jmwalletd.py
Normal file
|
|
@ -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…
Add table
Add a link
Reference in a new issue