make PayServer a plugin
This commit is contained in:
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -2,7 +2,7 @@
|
||||
path = contrib/deterministic-build/electrum-locale
|
||||
url = https://github.com/spesmilo/electrum-locale
|
||||
[submodule "electrum/www"]
|
||||
path = electrum/www
|
||||
path = electrum/plugins/payserver/www
|
||||
url = https://github.com/spesmilo/electrum-http.git
|
||||
[submodule "electrum/gui/kivy/theming/atlas"]
|
||||
path = electrum/gui/kivy/theming/atlas
|
||||
|
||||
@@ -53,6 +53,7 @@ from .simple_config import SimpleConfig
|
||||
from .exchange_rate import FxThread
|
||||
from .logging import get_logger, Logger
|
||||
from . import GuiImportError
|
||||
from .plugin import run_hook
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from electrum import gui
|
||||
@@ -357,111 +358,6 @@ class WatchTowerServer(AuthenticatedServer):
|
||||
return await self.lnwatcher.sweepstore.add_sweep_tx(*args)
|
||||
|
||||
|
||||
class PayServer(Logger, EventListener):
|
||||
|
||||
WWW_DIR = os.path.join(os.path.dirname(__file__), 'www')
|
||||
|
||||
def __init__(self, daemon: 'Daemon', netaddress):
|
||||
Logger.__init__(self)
|
||||
assert self.has_www_dir(), self.WWW_DIR
|
||||
self.addr = netaddress
|
||||
self.daemon = daemon
|
||||
self.config = daemon.config
|
||||
self.pending = defaultdict(asyncio.Event)
|
||||
self.register_callbacks()
|
||||
|
||||
@classmethod
|
||||
def has_www_dir(cls) -> bool:
|
||||
index_html = os.path.join(cls.WWW_DIR, "index.html")
|
||||
return os.path.exists(index_html)
|
||||
|
||||
@property
|
||||
def wallet(self):
|
||||
# FIXME specify wallet somehow?
|
||||
return list(self.daemon.get_wallets().values())[0]
|
||||
|
||||
@event_listener
|
||||
async def on_event_request_status(self, wallet, key, status):
|
||||
if status == PR_PAID:
|
||||
self.pending[key].set()
|
||||
|
||||
@ignore_exceptions
|
||||
@log_exceptions
|
||||
async def run(self):
|
||||
self.root = root = self.config.get('payserver_root', '/r')
|
||||
app = web.Application()
|
||||
app.add_routes([web.get('/api/get_invoice', self.get_request)])
|
||||
app.add_routes([web.get('/api/get_status', self.get_status)])
|
||||
app.add_routes([web.get('/bip70/{key}.bip70', self.get_bip70_request)])
|
||||
# 'follow_symlinks=True' allows symlinks to traverse out the parent directory.
|
||||
# This was requested by distro packagers for vendored libs, and we restrict it to only those
|
||||
# to minimise attack surface. note: "add_routes" call order matters (inner path goes first)
|
||||
app.add_routes([web.static(f"{root}/vendor", os.path.join(self.WWW_DIR, 'vendor'), follow_symlinks=True)])
|
||||
app.add_routes([web.static(root, self.WWW_DIR)])
|
||||
if self.config.get('payserver_allow_create_invoice'):
|
||||
app.add_routes([web.post('/api/create_invoice', self.create_request)])
|
||||
runner = web.AppRunner(app)
|
||||
await runner.setup()
|
||||
site = web.TCPSite(runner, host=str(self.addr.host), port=self.addr.port, ssl_context=self.config.get_ssl_context())
|
||||
await site.start()
|
||||
self.logger.info(f"now running and listening. addr={self.addr}")
|
||||
|
||||
async def create_request(self, request):
|
||||
params = await request.post()
|
||||
wallet = self.wallet
|
||||
if 'amount_sat' not in params or not params['amount_sat'].isdigit():
|
||||
raise web.HTTPUnsupportedMediaType()
|
||||
amount = int(params['amount_sat'])
|
||||
message = params['message'] or "donation"
|
||||
key = wallet.create_request(
|
||||
amount_sat=amount,
|
||||
message=message,
|
||||
exp_delay=3600,
|
||||
address=None)
|
||||
raise web.HTTPFound(self.root + '/pay?id=' + key)
|
||||
|
||||
async def get_request(self, r):
|
||||
key = r.query_string
|
||||
request = self.wallet.get_formatted_request(key)
|
||||
return web.json_response(request)
|
||||
|
||||
async def get_bip70_request(self, r):
|
||||
from .paymentrequest import make_request
|
||||
key = r.match_info['key']
|
||||
request = self.wallet.get_request(key)
|
||||
if not request:
|
||||
return web.HTTPNotFound()
|
||||
pr = make_request(self.config, request)
|
||||
return web.Response(body=pr.SerializeToString(), content_type='application/bitcoin-paymentrequest')
|
||||
|
||||
async def get_status(self, request):
|
||||
ws = web.WebSocketResponse()
|
||||
await ws.prepare(request)
|
||||
key = request.query_string
|
||||
info = self.wallet.get_formatted_request(key)
|
||||
if not info:
|
||||
await ws.send_str('unknown invoice')
|
||||
await ws.close()
|
||||
return ws
|
||||
if info.get('status') == PR_PAID:
|
||||
await ws.send_str(f'paid')
|
||||
await ws.close()
|
||||
return ws
|
||||
if info.get('status') == PR_EXPIRED:
|
||||
await ws.send_str(f'expired')
|
||||
await ws.close()
|
||||
return ws
|
||||
while True:
|
||||
try:
|
||||
await asyncio.wait_for(self.pending[key].wait(), 1)
|
||||
break
|
||||
except asyncio.TimeoutError:
|
||||
# send data on the websocket, to keep it alive
|
||||
await ws.send_str('waiting')
|
||||
await ws.send_str('paid')
|
||||
await ws.close()
|
||||
return ws
|
||||
|
||||
|
||||
|
||||
class Daemon(Logger):
|
||||
@@ -496,15 +392,6 @@ class Daemon(Logger):
|
||||
if listen_jsonrpc:
|
||||
self.commands_server = CommandsServer(self, fd)
|
||||
daemon_jobs.append(self.commands_server.run())
|
||||
# pay server
|
||||
self.pay_server = None
|
||||
payserver_address = self.config.get_netaddress('payserver_address')
|
||||
if not config.get('offline') and payserver_address:
|
||||
if PayServer.has_www_dir():
|
||||
self.pay_server = PayServer(self, payserver_address)
|
||||
daemon_jobs.append(self.pay_server.run())
|
||||
else:
|
||||
self.logger.error(f"PayServer configured but WWW_DIR missing or empty. skipping. ({PayServer.WWW_DIR})")
|
||||
# server-side watchtower
|
||||
self.watchtower = None
|
||||
watchtower_address = self.config.get_netaddress('watchtower_address')
|
||||
@@ -559,6 +446,7 @@ class Daemon(Logger):
|
||||
return
|
||||
wallet.start_network(self.network)
|
||||
self._wallets[path] = wallet
|
||||
run_hook('daemon_wallet_loaded', self, wallet)
|
||||
return wallet
|
||||
|
||||
@staticmethod
|
||||
|
||||
5
electrum/plugins/payserver/__init__.py
Normal file
5
electrum/plugins/payserver/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from electrum.i18n import _
|
||||
|
||||
fullname = _('PayServer')
|
||||
description = 'run a HTTP server for receiving payments'
|
||||
available_for = ['qt', 'cmdline']
|
||||
31
electrum/plugins/payserver/cmdline.py
Normal file
31
electrum/plugins/payserver/cmdline.py
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Electrum - Lightweight Bitcoin Client
|
||||
# Copyright (C) 2022 The Electrum Developers
|
||||
#
|
||||
# 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:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
from .payserver import PayServerPlugin
|
||||
|
||||
class Plugin(PayServerPlugin):
|
||||
pass
|
||||
|
||||
167
electrum/plugins/payserver/payserver.py
Normal file
167
electrum/plugins/payserver/payserver.py
Normal file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Electrum - Lightweight Bitcoin Client
|
||||
# Copyright (C) 2022 The Electrum Developers
|
||||
#
|
||||
# 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:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
|
||||
from aiohttp import ClientResponse
|
||||
from aiohttp import web, client_exceptions
|
||||
from aiorpcx import timeout_after, TaskTimeout, ignore_after
|
||||
from aiorpcx import NetAddress
|
||||
|
||||
|
||||
from electrum.util import log_exceptions, ignore_exceptions
|
||||
from electrum.crypto import sha256
|
||||
from electrum.plugin import BasePlugin, hook
|
||||
from electrum.logging import Logger
|
||||
|
||||
|
||||
from electrum.logging import Logger
|
||||
from electrum.util import EventListener, event_listener
|
||||
from electrum.invoices import PR_PAID, PR_EXPIRED
|
||||
|
||||
|
||||
|
||||
class PayServerPlugin(BasePlugin):
|
||||
|
||||
def __init__(self, parent, config, name):
|
||||
BasePlugin.__init__(self, parent, config, name)
|
||||
self.config = config
|
||||
self.server = None
|
||||
|
||||
@hook
|
||||
def daemon_wallet_loaded(self, daemon, wallet):
|
||||
# we use the first wallet loaded
|
||||
if self.server is not None:
|
||||
return
|
||||
if self.config.get('offline'):
|
||||
return
|
||||
self.server = PayServer(self.config, wallet)
|
||||
asyncio.run_coroutine_threadsafe(daemon._run(jobs=[self.server.run()]), daemon.asyncio_loop)
|
||||
|
||||
|
||||
|
||||
class PayServer(Logger, EventListener):
|
||||
|
||||
WWW_DIR = os.path.join(os.path.dirname(__file__), 'www')
|
||||
|
||||
def __init__(self, config, wallet):
|
||||
Logger.__init__(self)
|
||||
assert self.has_www_dir(), self.WWW_DIR
|
||||
self.config = config
|
||||
self.wallet = wallet
|
||||
url = self.config.get('payserver_address', 'localhost:8080')
|
||||
self.addr = NetAddress.from_string(url)
|
||||
self.pending = defaultdict(asyncio.Event)
|
||||
self.register_callbacks()
|
||||
|
||||
@classmethod
|
||||
def has_www_dir(cls) -> bool:
|
||||
index_html = os.path.join(cls.WWW_DIR, "index.html")
|
||||
return os.path.exists(index_html)
|
||||
|
||||
@event_listener
|
||||
async def on_event_request_status(self, wallet, key, status):
|
||||
if status == PR_PAID:
|
||||
self.pending[key].set()
|
||||
|
||||
@ignore_exceptions
|
||||
@log_exceptions
|
||||
async def run(self):
|
||||
self.root = root = self.config.get('payserver_root', '/r')
|
||||
app = web.Application()
|
||||
app.add_routes([web.get('/api/get_invoice', self.get_request)])
|
||||
app.add_routes([web.get('/api/get_status', self.get_status)])
|
||||
app.add_routes([web.get('/bip70/{key}.bip70', self.get_bip70_request)])
|
||||
# 'follow_symlinks=True' allows symlinks to traverse out the parent directory.
|
||||
# This was requested by distro packagers for vendored libs, and we restrict it to only those
|
||||
# to minimise attack surface. note: "add_routes" call order matters (inner path goes first)
|
||||
app.add_routes([web.static(f"{root}/vendor", os.path.join(self.WWW_DIR, 'vendor'), follow_symlinks=True)])
|
||||
app.add_routes([web.static(root, self.WWW_DIR)])
|
||||
if self.config.get('payserver_allow_create_invoice'):
|
||||
app.add_routes([web.post('/api/create_invoice', self.create_request)])
|
||||
runner = web.AppRunner(app)
|
||||
await runner.setup()
|
||||
site = web.TCPSite(runner, host=str(self.addr.host), port=self.addr.port, ssl_context=self.config.get_ssl_context())
|
||||
await site.start()
|
||||
self.logger.info(f"now running and listening. addr={self.addr}")
|
||||
|
||||
async def create_request(self, request):
|
||||
params = await request.post()
|
||||
wallet = self.wallet
|
||||
if 'amount_sat' not in params or not params['amount_sat'].isdigit():
|
||||
raise web.HTTPUnsupportedMediaType()
|
||||
amount = int(params['amount_sat'])
|
||||
message = params['message'] or "donation"
|
||||
key = wallet.create_request(
|
||||
amount_sat=amount,
|
||||
message=message,
|
||||
exp_delay=3600,
|
||||
address=None)
|
||||
raise web.HTTPFound(self.root + '/pay?id=' + key)
|
||||
|
||||
async def get_request(self, r):
|
||||
key = r.query_string
|
||||
request = self.wallet.get_formatted_request(key)
|
||||
return web.json_response(request)
|
||||
|
||||
async def get_bip70_request(self, r):
|
||||
from .paymentrequest import make_request
|
||||
key = r.match_info['key']
|
||||
request = self.wallet.get_request(key)
|
||||
if not request:
|
||||
return web.HTTPNotFound()
|
||||
pr = make_request(self.config, request)
|
||||
return web.Response(body=pr.SerializeToString(), content_type='application/bitcoin-paymentrequest')
|
||||
|
||||
async def get_status(self, request):
|
||||
ws = web.WebSocketResponse()
|
||||
await ws.prepare(request)
|
||||
key = request.query_string
|
||||
info = self.wallet.get_formatted_request(key)
|
||||
if not info:
|
||||
await ws.send_str('unknown invoice')
|
||||
await ws.close()
|
||||
return ws
|
||||
if info.get('status') == PR_PAID:
|
||||
await ws.send_str(f'paid')
|
||||
await ws.close()
|
||||
return ws
|
||||
if info.get('status') == PR_EXPIRED:
|
||||
await ws.send_str(f'expired')
|
||||
await ws.close()
|
||||
return ws
|
||||
while True:
|
||||
try:
|
||||
await asyncio.wait_for(self.pending[key].wait(), 1)
|
||||
break
|
||||
except asyncio.TimeoutError:
|
||||
# send data on the websocket, to keep it alive
|
||||
await ws.send_str('waiting')
|
||||
await ws.send_str('paid')
|
||||
await ws.close()
|
||||
return ws
|
||||
|
||||
29
electrum/plugins/payserver/qt.py
Normal file
29
electrum/plugins/payserver/qt.py
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Electrum - Lightweight Bitcoin Client
|
||||
# Copyright (C) 2022 The Electrum Developers
|
||||
#
|
||||
# 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:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from .payserver import PayServerPlugin
|
||||
|
||||
class Plugin(PayServerPlugin):
|
||||
pass
|
||||
Reference in New Issue
Block a user