#!/usr/bin/env python3 from future.utils import iteritems from past.builtins import cmp from functools import cmp_to_key import http.server import base64 import io import json import threading import time import hashlib import os import sys from future.moves.urllib.parse import parse_qs from decimal import Decimal from optparse import OptionParser from twisted.internet import reactor from jmbase.support import EXIT_FAILURE # https://stackoverflow.com/questions/2801882/generating-a-png-with-matplotlib-when-display-is-undefined try: import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt except: print("matplotlib not found; do `pip install matplotlib` " "in the joinmarket virtualenv.") sys.exit(EXIT_FAILURE) from jmbase import get_log from jmclient import jm_single, load_program_config, calc_cj_fee, \ get_irc_mchannels, add_base_options from jmdaemon import OrderbookWatch, MessageChannelCollection, IRCMessageChannel #TODO this is only for base58, find a solution for a client without jmbitcoin import jmbitcoin as btc from jmdaemon.protocol import * log = get_log() #Initial state: allow only SW offer types swoffers = list(filter(lambda x: x[0:2] == 'sw', offername_list)) pkoffers = list(filter(lambda x: x[0:2] != 'sw', offername_list)) filtered_offername_list = swoffers shutdownform = '
' shutdownpage = '

Successfully Shut down

' toggleSWform = '
' refresh_orderbook_form = '
' sorted_units = ('BTC', 'mBTC', 'μBTC', 'satoshi') unit_to_power = {'BTC': 8, 'mBTC': 5, 'μBTC': 2, 'satoshi': 0} sorted_rel_units = ('%', '‱', 'ppm') rel_unit_to_factor = {'%': 100, '‱': 1e4, 'ppm': 1e6} def calc_depth_data(db, value): pass def get_graph_html(fig): imbuf = io.BytesIO() fig.savefig(imbuf, format='png') b64 = base64.b64encode(imbuf.getvalue()).decode('utf-8') return '' # callback functions for displaying order data def do_nothing(arg, order, btc_unit, rel_unit): return arg def ordertype_display(ordertype, order, btc_unit, rel_unit): ordertypes = {'swabsoffer': 'SW Absolute Fee', 'swreloffer': 'SW Relative Fee', 'absoffer': 'Absolute Fee', 'reloffer': 'Relative Fee'} return ordertypes[ordertype] def cjfee_display(cjfee, order, btc_unit, rel_unit): if order['ordertype'] in ['absoffer', 'swabsoffer']: return satoshi_to_unit(cjfee, order, btc_unit, rel_unit) elif order['ordertype'] in ['reloffer', 'swreloffer']: return str(float(cjfee) * rel_unit_to_factor[rel_unit]) + rel_unit def satoshi_to_unit(sat, order, btc_unit, rel_unit): power = unit_to_power[btc_unit] return ("%." + str(power) + "f") % float( Decimal(sat) / Decimal(10 ** power)) def order_str(s, order, btc_unit, rel_unit): return str(s) def create_table_heading(btc_unit, rel_unit): col = ' {1}\n' # .format(field,label) tableheading = '\n ' + ''.join( [ col.format('ordertype', 'Type'), col.format( 'counterparty', 'Counterparty'), col.format('oid', 'Order ID'), col.format('cjfee', 'Fee'), col.format( 'txfee', 'Miner Fee Contribution / ' + btc_unit), col.format( 'minsize', 'Minimum Size / ' + btc_unit), col.format( 'maxsize', 'Maximum Size / ' + btc_unit) ]) + ' ' return tableheading def create_choose_units_form(selected_btc, selected_rel): choose_units_form = ( '' + '') choose_units_form = choose_units_form.replace( '\n' for key, displayer in order_keys_display: result += ' \n' result += ' \n' return len(rows), result def get_counterparty_count(self): try: self.taker.dblock.acquire(True) counterparties = self.taker.db.execute( 'SELECT DISTINCT counterparty FROM orderbook WHERE ordertype=? OR ordertype=?;', filtered_offername_list).fetchall() finally: self.taker.dblock.release() return str(len(counterparties)) def do_GET(self): # http.server.SimpleHTTPRequestHandler.do_GET(self) # print 'httpd received ' + self.path + ' request' self.path, query = self.path.split('?', 1) if '?' in self.path else ( self.path, '') args = parse_qs(query) pages = ['/', '/ordersize', '/depth', '/orderbook.json'] if self.path not in pages: return fd = open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'orderbook.html'), 'r') orderbook_fmt = fd.read() fd.close() alert_msg = '' if jm_single().joinmarket_alert[0]: alert_msg = '
JoinMarket Alert Message:
' + \ jm_single().joinmarket_alert[0] if self.path == '/': btc_unit = args['btcunit'][ 0] if 'btcunit' in args else sorted_units[0] rel_unit = args['relunit'][ 0] if 'relunit' in args else sorted_rel_units[0] if btc_unit not in sorted_units: btc_unit = sorted_units[0] if rel_unit not in sorted_rel_units: rel_unit = sorted_rel_units[0] ordercount, ordertable = self.create_orderbook_table( btc_unit, rel_unit) choose_units_form = create_choose_units_form(btc_unit, rel_unit) table_heading = create_table_heading(btc_unit, rel_unit) replacements = { 'PAGETITLE': 'JoinMarket Browser Interface', 'MAINHEADING': 'JoinMarket Orderbook', 'SECONDHEADING': (str(ordercount) + ' orders found by ' + self.get_counterparty_count() + ' counterparties' + alert_msg), 'MAINBODY': ( toggleSWform + refresh_orderbook_form + choose_units_form + table_heading + ordertable + '
' + displayer(o[key], o, btc_unit, rel_unit) + '
\n') } elif self.path == '/ordersize': replacements = { 'PAGETITLE': 'JoinMarket Browser Interface', 'MAINHEADING': 'Order Sizes', 'SECONDHEADING': 'Order Size Histogram' + alert_msg, 'MAINBODY': self.create_size_histogram(args) } elif self.path.startswith('/depth'): # if self.path[6] == '?': # quantity = cj_amounts = [10 ** cja for cja in range(4, 12, 1)] mainbody = [self.create_depth_chart(cja, args) \ for cja in cj_amounts] + \ ["
linear" if args.get("scale") \ else "
log scale"] replacements = { 'PAGETITLE': 'JoinMarket Browser Interface', 'MAINHEADING': 'Depth Chart', 'SECONDHEADING': 'Orderbook Depth' + alert_msg, 'MAINBODY': '
'.join(mainbody) } elif self.path == '/orderbook.json': replacements = {} orderbook_fmt = json.dumps(self.create_orderbook_obj()) orderbook_page = orderbook_fmt for key, rep in iteritems(replacements): orderbook_page = orderbook_page.replace(key, rep) self.send_response(200) if self.path.endswith('.json'): self.send_header('Content-Type', 'application/json') else: self.send_header('Content-Type', 'text/html') self.send_header('Content-Length', len(orderbook_page)) self.end_headers() self.wfile.write(orderbook_page.encode('utf-8')) def do_POST(self): global filtered_offername_list pages = ['/shutdown', '/refreshorderbook', '/toggleSW'] if self.path not in pages: return if self.path == '/shutdown': self.taker.msgchan.shutdown() self.send_response(200) self.send_header('Content-Type', 'text/html') self.send_header('Content-Length', len(shutdownpage)) self.end_headers() self.wfile.write(shutdownpage) self.base_server.__shutdown_request = True elif self.path == '/refreshorderbook': self.taker.msgchan.request_orderbook() time.sleep(5) self.path = '/' self.do_GET() elif self.path == '/toggleSW': if filtered_offername_list == swoffers: filtered_offername_list = pkoffers else: filtered_offername_list = swoffers self.path = '/' self.do_GET() class HTTPDThread(threading.Thread): def __init__(self, taker, hostport): threading.Thread.__init__(self, name='HTTPDThread') self.daemon = True self.taker = taker self.hostport = hostport def run(self): # hostport = ('localhost', 62601) try: httpd = http.server.HTTPServer(self.hostport, OrderbookPageRequestHeader) except Exception as e: print("Failed to start HTTP server: " + str(e)) os._exit(EXIT_FAILURE) httpd.taker = self.taker print('\nstarted http server, visit http://{0}:{1}/\n'.format( *self.hostport)) httpd.serve_forever() class ObBasic(OrderbookWatch): """Dummy orderbook watch class with hooks for triggering orderbook request""" def __init__(self, msgchan, hostport): self.hostport = hostport self.set_msgchan(msgchan) def on_welcome(self): """TODO: It will probably be a bit simpler, and more consistent, to use a twisted http server here instead of a thread.""" HTTPDThread(self, self.hostport).start() self.request_orderbook() def request_orderbook(self): self.msgchan.request_orderbook() class ObIRCMessageChannel(IRCMessageChannel): """A customisation of the message channel to allow receipt of privmsgs without the verification hooks in client-daemon communication.""" def on_privmsg(self, nick, message): if len(message) < 2: return if message[0] != COMMAND_PREFIX: log.debug('message not a cmd') return cmd_string = message[1:].split(' ')[0] if cmd_string not in offername_list: log.debug('non-offer ignored') return #Ignore sigs (TODO better to include check) sig = message[1:].split(' ')[-2:] #reconstruct original message without cmd pref rawmessage = ' '.join(message[1:].split(' ')[:-2]) for command in rawmessage.split(COMMAND_PREFIX): _chunks = command.split(" ") try: self.check_for_orders(nick, _chunks) except: pass def get_dummy_nick(): """In Joinmarket-CS nick creation is negotiated between client and server/daemon so as to allow client to sign for messages; here we only ever publish an orderbook request, so no such need, but for better privacy, a conformant nick is created based on a random pseudo-pubkey.""" nick_pkh_raw = hashlib.sha256(os.urandom(10)).digest()[:NICK_HASH_LENGTH] nick_pkh = btc.b58encode(nick_pkh_raw) #right pad to maximum possible; b58 is not fixed length. #Use 'O' as one of the 4 not included chars in base58. nick_pkh += 'O' * (NICK_MAX_ENCODED - len(nick_pkh)) #The constructed length will be 1 + 1 + NICK_MAX_ENCODED nick = JOINMARKET_NICK_HEADER + str(JM_VERSION) + nick_pkh jm_single().nickname = nick return nick def main(): parser = OptionParser( usage='usage: %prog [options]', description='Runs a webservice which shows the orderbook.') add_base_options(parser) parser.add_option('-H', '--host', action='store', type='string', dest='host', default='localhost', help='hostname or IP to bind to, default=localhost') parser.add_option('-p', '--port', action='store', type='int', dest='port', help='port to listen on, default=62601', default=62601) (options, args) = parser.parse_args() load_program_config(config_path=options.datadir) hostport = (options.host, options.port) mcs = [ObIRCMessageChannel(c) for c in get_irc_mchannels()] mcc = MessageChannelCollection(mcs) mcc.set_nick(get_dummy_nick()) taker = ObBasic(mcc, hostport) log.info("Starting ob-watcher") mcc.run() if __name__ == "__main__": main() reactor.run() print('done')