Skip to content

Commit

Permalink
Integrate http_server (previously in electrum-merchant)
Browse files Browse the repository at this point in the history
Use submodule to fetch HTML and CSS files
  • Loading branch information
ecdsa committed Sep 4, 2019
1 parent bd57880 commit 747ab7a
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 237 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
[submodule "contrib/CalinsQRReader"]
path = contrib/osx/CalinsQRReader
url = https://github.com/spesmilo/CalinsQRReader
[submodule "electrum/www"]
path = electrum/www
url = git@github.com:spesmilo/electrum-http.git
1 change: 0 additions & 1 deletion electrum/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1039,7 +1039,6 @@ def eval_bool(x: str) -> bool:
config_variables = {

'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_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/\')\"',
Expand Down
79 changes: 79 additions & 0 deletions electrum/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import aiohttp
from aiohttp import web
from base64 import b64decode
from collections import defaultdict

import jsonrpcclient
import jsonrpcserver
Expand All @@ -41,6 +42,7 @@

from .network import Network
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 .storage import WalletStorage
from .commands import known_commands, Commands
Expand Down Expand Up @@ -168,6 +170,79 @@ async def get_ctn(self, *args):
async def add_sweep_tx(self, *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):
pass

Expand Down Expand Up @@ -197,6 +272,9 @@ def __init__(self, config: SimpleConfig, fd=None, *, listen_jsonrpc=True):
if listen_jsonrpc:
jobs.append(self.start_jsonrpc(config, fd))
# 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
if self.watchtower:
jobs.append(self.watchtower.run)
Expand Down Expand Up @@ -296,6 +374,7 @@ def load_wallet(self, path, password) -> Optional[Abstract_Wallet]:
wallet = Wallet(storage)
wallet.start_network(self.network)
self.wallets[path] = wallet
self.wallet = wallet
return wallet

def add_wallet(self, wallet: Abstract_Wallet):
Expand Down
2 changes: 1 addition & 1 deletion electrum/gui/qt/invoice_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,4 @@ def create_menu_bitcoin_payreq(self, menu, payreq_key):
def create_menu_ln_payreq(self, menu, payreq_key):
req = self.parent.wallet.lnworker.invoices[payreq_key][0]
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))
5 changes: 2 additions & 3 deletions electrum/gui/qt/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -1028,9 +1028,8 @@ def on_expiry(i):

return w


def delete_payment_request(self, addr):
self.wallet.remove_payment_request(addr, self.config)
def delete_request(self, key):
self.wallet.delete_request(key)
self.request_list.update()
self.clear_receive_tab()

Expand Down
63 changes: 26 additions & 37 deletions electrum/gui/qt/request_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,11 @@
from electrum.lnaddr import lndecode
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_RHASH_OR_ADDR = Qt.UserRole + 1
ROLE_KEY = Qt.UserRole + 1

class RequestList(MyTreeView):

Expand Down Expand Up @@ -76,7 +74,7 @@ def __init__(self, parent):
def select_key(self, key):
for i in range(self.model().rowCount()):
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:
self.selectionModel().setCurrentIndex(item, QItemSelectionModel.SelectCurrent | QItemSelectionModel.Rows)
break
Expand All @@ -85,12 +83,12 @@ def item_changed(self, idx):
# TODO use siblingAtColumn when min Qt version is >=5.11
item = self.model().itemFromIndex(idx.sibling(idx.row(), self.Columns.DATE))
request_type = item.data(ROLE_REQUEST_TYPE)
key = item.data(ROLE_RHASH_OR_ADDR)
is_lightning = request_type == REQUEST_TYPE_LN
req = self.wallet.get_request(key, is_lightning)
key = item.data(ROLE_KEY)
req = self.wallet.get_request(key)
if req is None:
self.update()
return
is_lightning = request_type == PR_TYPE_LN
text = req.get('invoice') if is_lightning else req.get('URI')
self.parent.receive_address_e.setText(text)

Expand All @@ -101,9 +99,9 @@ def refresh_status(self):
date_idx = idx.sibling(idx.row(), self.Columns.DATE)
date_item = m.itemFromIndex(date_idx)
status_item = m.itemFromIndex(idx)
key = date_item.data(ROLE_RHASH_OR_ADDR)
is_lightning = date_item.data(ROLE_REQUEST_TYPE) == REQUEST_TYPE_LN
req = self.wallet.get_request(key, is_lightning)
key = date_item.data(ROLE_KEY)
is_lightning = date_item.data(ROLE_REQUEST_TYPE) == PR_TYPE_LN
req = self.wallet.get_request(key)
if req:
status = req['status']
status_str = get_request_status(req)
Expand All @@ -121,7 +119,7 @@ def update(self):
if status == PR_PAID:
continue
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)
amount = req.get('amount')
message = req['message'] if is_lightning else req['memo']
Expand All @@ -133,18 +131,17 @@ def update(self):
self.set_editability(items)
items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE)
items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status)))
if request_type == REQUEST_TYPE_LN:
items[self.Columns.DATE].setData(req['rhash'], ROLE_RHASH_OR_ADDR)
if request_type == PR_TYPE_LN:
items[self.Columns.DATE].setData(req['rhash'], ROLE_KEY)
items[self.Columns.DATE].setIcon(read_QIcon("lightning.png"))
items[self.Columns.DATE].setData(REQUEST_TYPE_LN, ROLE_REQUEST_TYPE)
else:
elif request_type == PR_TYPE_ADDRESS:
address = req['address']
if address not in domain:
continue
expiration = req.get('exp', None)
signature = req.get('sig')
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:
items[self.Columns.DATE].setIcon(read_QIcon("seal.png"))
items[self.Columns.DATE].setToolTip(f'signed by {requestor}')
Expand All @@ -167,13 +164,9 @@ def create_menu(self, position):
item = self.model().itemFromIndex(idx.sibling(idx.row(), self.Columns.DATE))
if not item:
return
addr = item.data(ROLE_RHASH_OR_ADDR)
key = item.data(ROLE_KEY)
request_type = item.data(ROLE_REQUEST_TYPE)
assert request_type in [REQUEST_TYPE_BITCOIN, REQUEST_TYPE_LN]
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]
req = self.wallet.get_request(key)
if req is None:
self.update()
return
Expand All @@ -184,19 +177,15 @@ def create_menu(self, position):
if column == self.Columns.AMOUNT:
column_data = column_data.strip()
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)
elif request_type == REQUEST_TYPE_LN:
self.create_menu_ln_payreq(menu, addr, req)
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)
#menu.addAction(_("Copy Address"), lambda: self.parent.do_copy('Address', addr))
menu.addAction(_("Copy Request"), lambda: self.parent.do_copy('URI', self.wallet.get_request_URI(addr)))
if 'http_url' in req:
menu.addAction(_("View in web browser"), lambda: webopen(req['http_url']))

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))
# 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))
65 changes: 11 additions & 54 deletions electrum/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -1279,32 +1279,6 @@ def get_payment_request(self, addr, config):
out['status'] = status
if conf is not None:
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

def get_request_URI(self, addr):
Expand Down Expand Up @@ -1346,11 +1320,19 @@ def get_request_status(self, key):
status = PR_INFLIGHT if conf <= 0 else PR_PAID
return status, conf

def get_request(self, key, is_lightning):
if not is_lightning:
def get_request(self, key):
from .simple_config import get_config
config = get_config()
if key in self.receive_requests:
req = self.get_payment_request(key, {})
else:
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

def receive_tx_callback(self, tx_hash, tx, tx_height):
Expand Down Expand Up @@ -1389,24 +1371,6 @@ def add_payment_request(self, req, config):
self.receive_requests[addr] = req
self.storage.put('payment_requests', self.receive_requests)
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

def delete_request(self, key):
Expand All @@ -1427,14 +1391,7 @@ def delete_invoice(self, key):
def remove_payment_request(self, addr, config):
if addr not in self.receive_requests:
return False
r = 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.receive_requests.pop(addr)
self.storage.put('payment_requests', self.receive_requests)
return True

Expand Down
Loading

0 comments on commit 747ab7a

Please sign in to comment.