mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
Integrate http_server (previously in electrum-merchant)
Use submodule to fetch HTML and CSS files
This commit is contained in:
parent
bd57880799
commit
747ab7a0a2
10 changed files with 124 additions and 238 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -4,3 +4,6 @@
|
||||||
[submodule "contrib/CalinsQRReader"]
|
[submodule "contrib/CalinsQRReader"]
|
||||||
path = contrib/osx/CalinsQRReader
|
path = contrib/osx/CalinsQRReader
|
||||||
url = https://github.com/spesmilo/CalinsQRReader
|
url = https://github.com/spesmilo/CalinsQRReader
|
||||||
|
[submodule "electrum/www"]
|
||||||
|
path = electrum/www
|
||||||
|
url = git@github.com:spesmilo/electrum-http.git
|
||||||
|
|
|
@ -1039,7 +1039,6 @@ arg_types = {
|
||||||
config_variables = {
|
config_variables = {
|
||||||
|
|
||||||
'addrequest': {
|
'addrequest': {
|
||||||
'requests_dir': 'directory where a bip70 file will be written.',
|
|
||||||
'ssl_privkey': 'Path to your SSL private key, needed to sign the request.',
|
'ssl_privkey': 'Path to your SSL private key, needed to sign the request.',
|
||||||
'ssl_chain': 'Chain of SSL certificates, needed for signed requests. Put your certificate at the top and the root CA at the end',
|
'ssl_chain': 'Chain of SSL certificates, needed for signed requests. Put your certificate at the top and the root CA at the end',
|
||||||
'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"',
|
'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"',
|
||||||
|
|
|
@ -33,6 +33,7 @@ from typing import Dict, Optional, Tuple
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import jsonrpcclient
|
import jsonrpcclient
|
||||||
import jsonrpcserver
|
import jsonrpcserver
|
||||||
|
@ -41,6 +42,7 @@ from jsonrpcclient.clients.aiohttp_client import AiohttpClient
|
||||||
|
|
||||||
from .network import Network
|
from .network import Network
|
||||||
from .util import (json_decode, to_bytes, to_string, profiler, standardize_path, constant_time_compare)
|
from .util import (json_decode, to_bytes, to_string, profiler, standardize_path, constant_time_compare)
|
||||||
|
from .util import PR_PAID, PR_EXPIRED, get_request_status
|
||||||
from .wallet import Wallet, Abstract_Wallet
|
from .wallet import Wallet, Abstract_Wallet
|
||||||
from .storage import WalletStorage
|
from .storage import WalletStorage
|
||||||
from .commands import known_commands, Commands
|
from .commands import known_commands, Commands
|
||||||
|
@ -168,6 +170,79 @@ class WatchTowerServer(Logger):
|
||||||
async def add_sweep_tx(self, *args):
|
async def add_sweep_tx(self, *args):
|
||||||
return await self.lnwatcher.sweepstore.add_sweep_tx(*args)
|
return await self.lnwatcher.sweepstore.add_sweep_tx(*args)
|
||||||
|
|
||||||
|
class HttpServer(Logger):
|
||||||
|
|
||||||
|
def __init__(self, daemon):
|
||||||
|
Logger.__init__(self)
|
||||||
|
self.daemon = daemon
|
||||||
|
self.config = daemon.config
|
||||||
|
self.pending = defaultdict(asyncio.Event)
|
||||||
|
self.daemon.network.register_callback(self.on_payment, ['payment_received'])
|
||||||
|
|
||||||
|
async def on_payment(self, evt, *args):
|
||||||
|
print(evt, args)
|
||||||
|
#await self.pending[key].set()
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
from aiohttp import helpers
|
||||||
|
app = web.Application()
|
||||||
|
#app.on_response_prepare.append(http_server.on_response_prepare)
|
||||||
|
app.add_routes([web.post('/api/create_invoice', self.create_request)])
|
||||||
|
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.static('/electrum', 'electrum/www')])
|
||||||
|
runner = web.AppRunner(app)
|
||||||
|
await runner.setup()
|
||||||
|
host = self.config.get('http_host', 'localhost')
|
||||||
|
port = self.config.get('http_port', 8000)
|
||||||
|
site = web.TCPSite(runner, port=port, host=host)
|
||||||
|
await site.start()
|
||||||
|
|
||||||
|
async def create_request(self, request):
|
||||||
|
params = await request.post()
|
||||||
|
wallet = self.daemon.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"
|
||||||
|
payment_hash = await wallet.lnworker._add_invoice_coro(amount, message, 3600)
|
||||||
|
key = payment_hash.hex()
|
||||||
|
raise web.HTTPFound('/electrum/index.html?id=' + key)
|
||||||
|
|
||||||
|
async def get_request(self, r):
|
||||||
|
key = r.query_string
|
||||||
|
request = self.daemon.wallet.get_request(key)
|
||||||
|
return web.json_response(request)
|
||||||
|
|
||||||
|
async def get_status(self, request):
|
||||||
|
ws = web.WebSocketResponse()
|
||||||
|
await ws.prepare(request)
|
||||||
|
key = request.query_string
|
||||||
|
info = self.daemon.wallet.get_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'already paid')
|
||||||
|
await ws.close()
|
||||||
|
return ws
|
||||||
|
if info.get('status') == PR_EXPIRED:
|
||||||
|
await ws.send_str(f'invoice 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 AuthenticationError(Exception):
|
class AuthenticationError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -197,6 +272,9 @@ class Daemon(Logger):
|
||||||
if listen_jsonrpc:
|
if listen_jsonrpc:
|
||||||
jobs.append(self.start_jsonrpc(config, fd))
|
jobs.append(self.start_jsonrpc(config, fd))
|
||||||
# server-side watchtower
|
# server-side watchtower
|
||||||
|
self.http_server = HttpServer(self)
|
||||||
|
if self.http_server:
|
||||||
|
jobs.append(self.http_server.run())
|
||||||
self.watchtower = WatchTowerServer(self.network) if self.config.get('watchtower_host') else None
|
self.watchtower = WatchTowerServer(self.network) if self.config.get('watchtower_host') else None
|
||||||
if self.watchtower:
|
if self.watchtower:
|
||||||
jobs.append(self.watchtower.run)
|
jobs.append(self.watchtower.run)
|
||||||
|
@ -296,6 +374,7 @@ class Daemon(Logger):
|
||||||
wallet = Wallet(storage)
|
wallet = Wallet(storage)
|
||||||
wallet.start_network(self.network)
|
wallet.start_network(self.network)
|
||||||
self.wallets[path] = wallet
|
self.wallets[path] = wallet
|
||||||
|
self.wallet = wallet
|
||||||
return wallet
|
return wallet
|
||||||
|
|
||||||
def add_wallet(self, wallet: Abstract_Wallet):
|
def add_wallet(self, wallet: Abstract_Wallet):
|
||||||
|
|
|
@ -151,4 +151,4 @@ class InvoiceList(MyTreeView):
|
||||||
def create_menu_ln_payreq(self, menu, payreq_key):
|
def create_menu_ln_payreq(self, menu, payreq_key):
|
||||||
req = self.parent.wallet.lnworker.invoices[payreq_key][0]
|
req = self.parent.wallet.lnworker.invoices[payreq_key][0]
|
||||||
menu.addAction(_("Copy Lightning invoice"), lambda: self.parent.do_copy('Lightning invoice', req))
|
menu.addAction(_("Copy Lightning invoice"), lambda: self.parent.do_copy('Lightning invoice', req))
|
||||||
menu.addAction(_("Delete"), lambda: self.parent.delete_lightning_payreq(payreq_key))
|
menu.addAction(_("Delete"), lambda: self.parent.delete_invoice(payreq_key))
|
||||||
|
|
|
@ -1028,9 +1028,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
||||||
|
|
||||||
return w
|
return w
|
||||||
|
|
||||||
|
def delete_request(self, key):
|
||||||
def delete_payment_request(self, addr):
|
self.wallet.delete_request(key)
|
||||||
self.wallet.remove_payment_request(addr, self.config)
|
|
||||||
self.request_list.update()
|
self.request_list.update()
|
||||||
self.clear_receive_tab()
|
self.clear_receive_tab()
|
||||||
|
|
||||||
|
|
|
@ -40,13 +40,11 @@ from electrum.bitcoin import COIN
|
||||||
from electrum.lnaddr import lndecode
|
from electrum.lnaddr import lndecode
|
||||||
import electrum.constants as constants
|
import electrum.constants as constants
|
||||||
|
|
||||||
from .util import MyTreeView, pr_icons, read_QIcon
|
from .util import MyTreeView, pr_icons, read_QIcon, webopen
|
||||||
|
|
||||||
REQUEST_TYPE_BITCOIN = 0
|
|
||||||
REQUEST_TYPE_LN = 1
|
|
||||||
|
|
||||||
ROLE_REQUEST_TYPE = Qt.UserRole
|
ROLE_REQUEST_TYPE = Qt.UserRole
|
||||||
ROLE_RHASH_OR_ADDR = Qt.UserRole + 1
|
ROLE_KEY = Qt.UserRole + 1
|
||||||
|
|
||||||
class RequestList(MyTreeView):
|
class RequestList(MyTreeView):
|
||||||
|
|
||||||
|
@ -76,7 +74,7 @@ class RequestList(MyTreeView):
|
||||||
def select_key(self, key):
|
def select_key(self, key):
|
||||||
for i in range(self.model().rowCount()):
|
for i in range(self.model().rowCount()):
|
||||||
item = self.model().index(i, self.Columns.DATE)
|
item = self.model().index(i, self.Columns.DATE)
|
||||||
row_key = item.data(ROLE_RHASH_OR_ADDR)
|
row_key = item.data(ROLE_KEY)
|
||||||
if key == row_key:
|
if key == row_key:
|
||||||
self.selectionModel().setCurrentIndex(item, QItemSelectionModel.SelectCurrent | QItemSelectionModel.Rows)
|
self.selectionModel().setCurrentIndex(item, QItemSelectionModel.SelectCurrent | QItemSelectionModel.Rows)
|
||||||
break
|
break
|
||||||
|
@ -85,12 +83,12 @@ class RequestList(MyTreeView):
|
||||||
# TODO use siblingAtColumn when min Qt version is >=5.11
|
# TODO use siblingAtColumn when min Qt version is >=5.11
|
||||||
item = self.model().itemFromIndex(idx.sibling(idx.row(), self.Columns.DATE))
|
item = self.model().itemFromIndex(idx.sibling(idx.row(), self.Columns.DATE))
|
||||||
request_type = item.data(ROLE_REQUEST_TYPE)
|
request_type = item.data(ROLE_REQUEST_TYPE)
|
||||||
key = item.data(ROLE_RHASH_OR_ADDR)
|
key = item.data(ROLE_KEY)
|
||||||
is_lightning = request_type == REQUEST_TYPE_LN
|
req = self.wallet.get_request(key)
|
||||||
req = self.wallet.get_request(key, is_lightning)
|
|
||||||
if req is None:
|
if req is None:
|
||||||
self.update()
|
self.update()
|
||||||
return
|
return
|
||||||
|
is_lightning = request_type == PR_TYPE_LN
|
||||||
text = req.get('invoice') if is_lightning else req.get('URI')
|
text = req.get('invoice') if is_lightning else req.get('URI')
|
||||||
self.parent.receive_address_e.setText(text)
|
self.parent.receive_address_e.setText(text)
|
||||||
|
|
||||||
|
@ -101,9 +99,9 @@ class RequestList(MyTreeView):
|
||||||
date_idx = idx.sibling(idx.row(), self.Columns.DATE)
|
date_idx = idx.sibling(idx.row(), self.Columns.DATE)
|
||||||
date_item = m.itemFromIndex(date_idx)
|
date_item = m.itemFromIndex(date_idx)
|
||||||
status_item = m.itemFromIndex(idx)
|
status_item = m.itemFromIndex(idx)
|
||||||
key = date_item.data(ROLE_RHASH_OR_ADDR)
|
key = date_item.data(ROLE_KEY)
|
||||||
is_lightning = date_item.data(ROLE_REQUEST_TYPE) == REQUEST_TYPE_LN
|
is_lightning = date_item.data(ROLE_REQUEST_TYPE) == PR_TYPE_LN
|
||||||
req = self.wallet.get_request(key, is_lightning)
|
req = self.wallet.get_request(key)
|
||||||
if req:
|
if req:
|
||||||
status = req['status']
|
status = req['status']
|
||||||
status_str = get_request_status(req)
|
status_str = get_request_status(req)
|
||||||
|
@ -121,7 +119,7 @@ class RequestList(MyTreeView):
|
||||||
if status == PR_PAID:
|
if status == PR_PAID:
|
||||||
continue
|
continue
|
||||||
is_lightning = req['type'] == PR_TYPE_LN
|
is_lightning = req['type'] == PR_TYPE_LN
|
||||||
request_type = REQUEST_TYPE_LN if is_lightning else REQUEST_TYPE_BITCOIN
|
request_type = req['type']
|
||||||
timestamp = req.get('time', 0)
|
timestamp = req.get('time', 0)
|
||||||
amount = req.get('amount')
|
amount = req.get('amount')
|
||||||
message = req['message'] if is_lightning else req['memo']
|
message = req['message'] if is_lightning else req['memo']
|
||||||
|
@ -133,18 +131,17 @@ class RequestList(MyTreeView):
|
||||||
self.set_editability(items)
|
self.set_editability(items)
|
||||||
items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE)
|
items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE)
|
||||||
items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status)))
|
items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status)))
|
||||||
if request_type == REQUEST_TYPE_LN:
|
if request_type == PR_TYPE_LN:
|
||||||
items[self.Columns.DATE].setData(req['rhash'], ROLE_RHASH_OR_ADDR)
|
items[self.Columns.DATE].setData(req['rhash'], ROLE_KEY)
|
||||||
items[self.Columns.DATE].setIcon(read_QIcon("lightning.png"))
|
items[self.Columns.DATE].setIcon(read_QIcon("lightning.png"))
|
||||||
items[self.Columns.DATE].setData(REQUEST_TYPE_LN, ROLE_REQUEST_TYPE)
|
elif request_type == PR_TYPE_ADDRESS:
|
||||||
else:
|
|
||||||
address = req['address']
|
address = req['address']
|
||||||
if address not in domain:
|
if address not in domain:
|
||||||
continue
|
continue
|
||||||
expiration = req.get('exp', None)
|
expiration = req.get('exp', None)
|
||||||
signature = req.get('sig')
|
signature = req.get('sig')
|
||||||
requestor = req.get('name', '')
|
requestor = req.get('name', '')
|
||||||
items[self.Columns.DATE].setData(address, ROLE_RHASH_OR_ADDR)
|
items[self.Columns.DATE].setData(address, ROLE_KEY)
|
||||||
if signature is not None:
|
if signature is not None:
|
||||||
items[self.Columns.DATE].setIcon(read_QIcon("seal.png"))
|
items[self.Columns.DATE].setIcon(read_QIcon("seal.png"))
|
||||||
items[self.Columns.DATE].setToolTip(f'signed by {requestor}')
|
items[self.Columns.DATE].setToolTip(f'signed by {requestor}')
|
||||||
|
@ -167,13 +164,9 @@ class RequestList(MyTreeView):
|
||||||
item = self.model().itemFromIndex(idx.sibling(idx.row(), self.Columns.DATE))
|
item = self.model().itemFromIndex(idx.sibling(idx.row(), self.Columns.DATE))
|
||||||
if not item:
|
if not item:
|
||||||
return
|
return
|
||||||
addr = item.data(ROLE_RHASH_OR_ADDR)
|
key = item.data(ROLE_KEY)
|
||||||
request_type = item.data(ROLE_REQUEST_TYPE)
|
request_type = item.data(ROLE_REQUEST_TYPE)
|
||||||
assert request_type in [REQUEST_TYPE_BITCOIN, REQUEST_TYPE_LN]
|
req = self.wallet.get_request(key)
|
||||||
if request_type == REQUEST_TYPE_BITCOIN:
|
|
||||||
req = self.wallet.receive_requests.get(addr)
|
|
||||||
elif request_type == REQUEST_TYPE_LN:
|
|
||||||
req = self.wallet.lnworker.invoices[addr][0]
|
|
||||||
if req is None:
|
if req is None:
|
||||||
self.update()
|
self.update()
|
||||||
return
|
return
|
||||||
|
@ -184,19 +177,15 @@ class RequestList(MyTreeView):
|
||||||
if column == self.Columns.AMOUNT:
|
if column == self.Columns.AMOUNT:
|
||||||
column_data = column_data.strip()
|
column_data = column_data.strip()
|
||||||
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.do_copy(column_title, column_data))
|
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.do_copy(column_title, column_data))
|
||||||
if request_type == REQUEST_TYPE_BITCOIN:
|
|
||||||
self.create_menu_bitcoin_payreq(menu, addr)
|
#menu.addAction(_("Copy Address"), lambda: self.parent.do_copy('Address', addr))
|
||||||
elif request_type == REQUEST_TYPE_LN:
|
menu.addAction(_("Copy Request"), lambda: self.parent.do_copy('URI', self.wallet.get_request_URI(addr)))
|
||||||
self.create_menu_ln_payreq(menu, addr, req)
|
if 'http_url' in req:
|
||||||
|
menu.addAction(_("View in web browser"), lambda: webopen(req['http_url']))
|
||||||
|
|
||||||
|
# do bip70 only for browser access
|
||||||
|
# so, each request should have an ID, regardless
|
||||||
|
#menu.addAction(_("Save as BIP70 file"), lambda: self.parent.export_payment_request(addr))
|
||||||
|
menu.addAction(_("Delete"), lambda: self.parent.delete_request(key))
|
||||||
|
run_hook('receive_list_menu', menu, key)
|
||||||
menu.exec_(self.viewport().mapToGlobal(position))
|
menu.exec_(self.viewport().mapToGlobal(position))
|
||||||
|
|
||||||
def create_menu_bitcoin_payreq(self, menu, addr):
|
|
||||||
menu.addAction(_("Copy Address"), lambda: self.parent.do_copy('Address', addr))
|
|
||||||
menu.addAction(_("Copy URI"), lambda: self.parent.do_copy('URI', self.wallet.get_request_URI(addr)))
|
|
||||||
menu.addAction(_("Save as BIP70 file"), lambda: self.parent.export_payment_request(addr))
|
|
||||||
menu.addAction(_("Delete"), lambda: self.parent.delete_payment_request(addr))
|
|
||||||
run_hook('receive_list_menu', menu, addr)
|
|
||||||
|
|
||||||
def create_menu_ln_payreq(self, menu, payreq_key, req):
|
|
||||||
menu.addAction(_("Copy Lightning invoice"), lambda: self.parent.do_copy('Lightning invoice', req))
|
|
||||||
menu.addAction(_("Delete"), lambda: self.parent.delete_lightning_payreq(payreq_key))
|
|
||||||
|
|
|
@ -1279,32 +1279,6 @@ class Abstract_Wallet(AddressSynchronizer):
|
||||||
out['status'] = status
|
out['status'] = status
|
||||||
if conf is not None:
|
if conf is not None:
|
||||||
out['confirmations'] = conf
|
out['confirmations'] = conf
|
||||||
# check if bip70 file exists
|
|
||||||
rdir = config.get('requests_dir')
|
|
||||||
if rdir:
|
|
||||||
key = out.get('id', addr)
|
|
||||||
path = os.path.join(rdir, 'req', key[0], key[1], key)
|
|
||||||
if os.path.exists(path):
|
|
||||||
baseurl = 'file://' + rdir
|
|
||||||
rewrite = config.get('url_rewrite')
|
|
||||||
if rewrite:
|
|
||||||
try:
|
|
||||||
baseurl = baseurl.replace(*rewrite)
|
|
||||||
except BaseException as e:
|
|
||||||
self.logger.info(f'Invalid config setting for "url_rewrite". err: {e}')
|
|
||||||
out['request_url'] = os.path.join(baseurl, 'req', key[0], key[1], key, key)
|
|
||||||
out['URI'] += '&r=' + out['request_url']
|
|
||||||
out['index_url'] = os.path.join(baseurl, 'index.html') + '?id=' + key
|
|
||||||
websocket_server_announce = config.get('websocket_server_announce')
|
|
||||||
if websocket_server_announce:
|
|
||||||
out['websocket_server'] = websocket_server_announce
|
|
||||||
else:
|
|
||||||
out['websocket_server'] = config.get('websocket_server', 'localhost')
|
|
||||||
websocket_port_announce = config.get('websocket_port_announce')
|
|
||||||
if websocket_port_announce:
|
|
||||||
out['websocket_port'] = websocket_port_announce
|
|
||||||
else:
|
|
||||||
out['websocket_port'] = config.get('websocket_port', 9999)
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def get_request_URI(self, addr):
|
def get_request_URI(self, addr):
|
||||||
|
@ -1346,11 +1320,19 @@ class Abstract_Wallet(AddressSynchronizer):
|
||||||
status = PR_INFLIGHT if conf <= 0 else PR_PAID
|
status = PR_INFLIGHT if conf <= 0 else PR_PAID
|
||||||
return status, conf
|
return status, conf
|
||||||
|
|
||||||
def get_request(self, key, is_lightning):
|
def get_request(self, key):
|
||||||
if not is_lightning:
|
from .simple_config import get_config
|
||||||
|
config = get_config()
|
||||||
|
if key in self.receive_requests:
|
||||||
req = self.get_payment_request(key, {})
|
req = self.get_payment_request(key, {})
|
||||||
else:
|
else:
|
||||||
req = self.lnworker.get_request(key)
|
req = self.lnworker.get_request(key)
|
||||||
|
if not req:
|
||||||
|
return
|
||||||
|
if config.get('http_port', 8000):
|
||||||
|
host = config.get('http_host', 'localhost')
|
||||||
|
port = config.get('http_port', 8000)
|
||||||
|
req['http_url'] = 'http://%s:%d/electrum/index.html?id=%s'%(host, port, key)
|
||||||
return req
|
return req
|
||||||
|
|
||||||
def receive_tx_callback(self, tx_hash, tx, tx_height):
|
def receive_tx_callback(self, tx_hash, tx, tx_height):
|
||||||
|
@ -1389,24 +1371,6 @@ class Abstract_Wallet(AddressSynchronizer):
|
||||||
self.receive_requests[addr] = req
|
self.receive_requests[addr] = req
|
||||||
self.storage.put('payment_requests', self.receive_requests)
|
self.storage.put('payment_requests', self.receive_requests)
|
||||||
self.set_label(addr, message) # should be a default label
|
self.set_label(addr, message) # should be a default label
|
||||||
|
|
||||||
rdir = config.get('requests_dir')
|
|
||||||
if rdir and amount is not None:
|
|
||||||
key = req.get('id', addr)
|
|
||||||
pr = paymentrequest.make_request(config, req)
|
|
||||||
path = os.path.join(rdir, 'req', key[0], key[1], key)
|
|
||||||
if not os.path.exists(path):
|
|
||||||
try:
|
|
||||||
os.makedirs(path)
|
|
||||||
except OSError as exc:
|
|
||||||
if exc.errno != errno.EEXIST:
|
|
||||||
raise
|
|
||||||
with open(os.path.join(path, key), 'wb') as f:
|
|
||||||
f.write(pr.SerializeToString())
|
|
||||||
# reload
|
|
||||||
req = self.get_payment_request(addr, config)
|
|
||||||
with open(os.path.join(path, key + '.json'), 'w', encoding='utf-8') as f:
|
|
||||||
f.write(json.dumps(req))
|
|
||||||
return req
|
return req
|
||||||
|
|
||||||
def delete_request(self, key):
|
def delete_request(self, key):
|
||||||
|
@ -1427,14 +1391,7 @@ class Abstract_Wallet(AddressSynchronizer):
|
||||||
def remove_payment_request(self, addr, config):
|
def remove_payment_request(self, addr, config):
|
||||||
if addr not in self.receive_requests:
|
if addr not in self.receive_requests:
|
||||||
return False
|
return False
|
||||||
r = self.receive_requests.pop(addr)
|
self.receive_requests.pop(addr)
|
||||||
rdir = config.get('requests_dir')
|
|
||||||
if rdir:
|
|
||||||
key = r.get('id', addr)
|
|
||||||
for s in ['.json', '']:
|
|
||||||
n = os.path.join(rdir, 'req', key[0], key[1], key, key + s)
|
|
||||||
if os.path.exists(n):
|
|
||||||
os.unlink(n)
|
|
||||||
self.storage.put('payment_requests', self.receive_requests)
|
self.storage.put('payment_requests', self.receive_requests)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
|
@ -1,132 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Electrum - lightweight Bitcoin client
|
|
||||||
# Copyright (C) 2015 Thomas Voegtlin
|
|
||||||
#
|
|
||||||
# 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 threading
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
from collections import defaultdict
|
|
||||||
import asyncio
|
|
||||||
from typing import Dict, List, Tuple, TYPE_CHECKING
|
|
||||||
import traceback
|
|
||||||
import sys
|
|
||||||
|
|
||||||
try:
|
|
||||||
from SimpleWebSocketServer import WebSocket, SimpleSSLWebSocketServer
|
|
||||||
except ImportError:
|
|
||||||
sys.exit("install SimpleWebSocketServer")
|
|
||||||
|
|
||||||
from . import bitcoin
|
|
||||||
from .synchronizer import SynchronizerBase
|
|
||||||
from .logging import Logger
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .network import Network
|
|
||||||
from .simple_config import SimpleConfig
|
|
||||||
|
|
||||||
|
|
||||||
request_queue = asyncio.Queue()
|
|
||||||
|
|
||||||
|
|
||||||
class ElectrumWebSocket(WebSocket, Logger):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
WebSocket.__init__(self)
|
|
||||||
Logger.__init__(self)
|
|
||||||
|
|
||||||
def handleMessage(self):
|
|
||||||
assert self.data[0:3] == 'id:'
|
|
||||||
self.logger.info(f"message received {self.data}")
|
|
||||||
request_id = self.data[3:]
|
|
||||||
asyncio.run_coroutine_threadsafe(
|
|
||||||
request_queue.put((self, request_id)), asyncio.get_event_loop())
|
|
||||||
|
|
||||||
def handleConnected(self):
|
|
||||||
self.logger.info(f"connected {self.address}")
|
|
||||||
|
|
||||||
def handleClose(self):
|
|
||||||
self.logger.info(f"closed {self.address}")
|
|
||||||
|
|
||||||
|
|
||||||
class BalanceMonitor(SynchronizerBase):
|
|
||||||
|
|
||||||
def __init__(self, config: 'SimpleConfig', network: 'Network'):
|
|
||||||
SynchronizerBase.__init__(self, network)
|
|
||||||
self.config = config
|
|
||||||
self.expected_payments = defaultdict(list) # type: Dict[str, List[Tuple[WebSocket, int]]]
|
|
||||||
|
|
||||||
def make_request(self, request_id):
|
|
||||||
# read json file
|
|
||||||
rdir = self.config.get('requests_dir')
|
|
||||||
n = os.path.join(rdir, 'req', request_id[0], request_id[1], request_id, request_id + '.json')
|
|
||||||
with open(n, encoding='utf-8') as f:
|
|
||||||
s = f.read()
|
|
||||||
d = json.loads(s)
|
|
||||||
addr = d.get('address')
|
|
||||||
amount = d.get('amount')
|
|
||||||
return addr, amount
|
|
||||||
|
|
||||||
async def main(self):
|
|
||||||
# resend existing subscriptions if we were restarted
|
|
||||||
for addr in self.expected_payments:
|
|
||||||
await self._add_address(addr)
|
|
||||||
# main loop
|
|
||||||
while True:
|
|
||||||
ws, request_id = await request_queue.get()
|
|
||||||
try:
|
|
||||||
addr, amount = self.make_request(request_id)
|
|
||||||
except Exception:
|
|
||||||
self.logger.exception('')
|
|
||||||
continue
|
|
||||||
self.expected_payments[addr].append((ws, amount))
|
|
||||||
await self._add_address(addr)
|
|
||||||
|
|
||||||
async def _on_address_status(self, addr, status):
|
|
||||||
self.logger.info(f'new status for addr {addr}')
|
|
||||||
sh = bitcoin.address_to_scripthash(addr)
|
|
||||||
balance = await self.network.get_balance_for_scripthash(sh)
|
|
||||||
for ws, amount in self.expected_payments[addr]:
|
|
||||||
if not ws.closed:
|
|
||||||
if sum(balance.values()) >= amount:
|
|
||||||
ws.sendMessage('paid')
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketServer(threading.Thread):
|
|
||||||
|
|
||||||
def __init__(self, config: 'SimpleConfig', network: 'Network'):
|
|
||||||
threading.Thread.__init__(self)
|
|
||||||
self.config = config
|
|
||||||
self.network = network
|
|
||||||
asyncio.set_event_loop(network.asyncio_loop)
|
|
||||||
self.daemon = True
|
|
||||||
self.balance_monitor = BalanceMonitor(self.config, self.network)
|
|
||||||
self.start()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
asyncio.set_event_loop(self.network.asyncio_loop)
|
|
||||||
host = self.config.get('websocket_server')
|
|
||||||
port = self.config.get('websocket_port', 9999)
|
|
||||||
certfile = self.config.get('ssl_chain')
|
|
||||||
keyfile = self.config.get('ssl_privkey')
|
|
||||||
self.server = SimpleSSLWebSocketServer(host, port, ElectrumWebSocket, certfile, keyfile)
|
|
||||||
self.server.serveforever()
|
|
1
electrum/www
Submodule
1
electrum/www
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 538fa508d41512e670fb84970f821a5db71836d9
|
|
@ -375,15 +375,6 @@ if __name__ == '__main__':
|
||||||
# run daemon
|
# run daemon
|
||||||
init_plugins(config, 'cmdline')
|
init_plugins(config, 'cmdline')
|
||||||
d = daemon.Daemon(config, fd)
|
d = daemon.Daemon(config, fd)
|
||||||
if config.get('websocket_server'):
|
|
||||||
from electrum import websockets
|
|
||||||
websockets.WebSocketServer(config, d.network)
|
|
||||||
if config.get('requests_dir'):
|
|
||||||
path = os.path.join(config.get('requests_dir'), 'index.html')
|
|
||||||
if not os.path.exists(path):
|
|
||||||
print("Requests directory not configured.")
|
|
||||||
print("You can configure it using https://github.com/spesmilo/electrum-merchant")
|
|
||||||
sys_exit(1)
|
|
||||||
d.run_daemon()
|
d.run_daemon()
|
||||||
sys_exit(0)
|
sys_exit(0)
|
||||||
else:
|
else:
|
||||||
|
|
Loading…
Add table
Reference in a new issue