from __future__ import absolute_import, print_function import BaseHTTPServer import SimpleHTTPServer import base64 import io import json import threading import time import hashlib import os import sys import urllib2 from decimal import Decimal from optparse import OptionParser from twisted.internet import reactor # 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(0) from jmclient import jm_single, load_program_config, get_log, calc_cj_fee, get_irc_mchannels 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 = filter(lambda x: x[0:2] == 'sw', offername_list) pkoffers = 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 create_depth_chart(db, cj_amount, args=None): if args is None: args = {} rows = db.execute('SELECT * FROM orderbook;').fetchall() sqlorders = [o for o in rows if o["ordertype"] in filtered_offername_list] orderfees = sorted([calc_cj_fee(o['ordertype'], o['cjfee'], cj_amount) / 1e8 for o in sqlorders if o['minsize'] <= cj_amount <= o[ 'maxsize']]) if len(orderfees) == 0: return 'No orders at amount ' + str(cj_amount / 1e8) fig = plt.figure() scale = args.get("scale") if (scale is not None) and (scale[0] == "log"): orderfees = [float(fee) for fee in orderfees] if orderfees[0] > 0: ratio = orderfees[-1] / orderfees[0] step = ratio ** 0.0333 # 1/30 bins = [orderfees[0] * (step ** i) for i in range(30)] else: ratio = orderfees[-1] / 1e-8 # single satoshi placeholder step = ratio ** 0.0333 # 1/30 bins = [1e-8 * (step ** i) for i in range(30)] bins[0] = orderfees[0] # replace placeholder plt.xscale('log') else: bins = 30 if len(orderfees) == 1: # these days we have liquidity, but just in case... plt.hist(orderfees, bins, rwidth=0.8, range=(0, orderfees[0] * 2)) else: plt.hist(orderfees, bins, rwidth=0.8) plt.grid() plt.title('CoinJoin Orderbook Depth Chart for amount=' + str(cj_amount / 1e8) + 'btc') plt.xlabel('CoinJoin Fee / btc') plt.ylabel('Frequency') return get_graph_html(fig) def create_size_histogram(db, args): rows = db.execute('SELECT maxsize, ordertype FROM orderbook;').fetchall() rows = [o for o in rows if o["ordertype"] in filtered_offername_list] ordersizes = sorted([r['maxsize'] / 1e8 for r in rows]) fig = plt.figure() scale = args.get("scale") if (scale is not None) and (scale[0] == "log"): ratio = ordersizes[-1] / ordersizes[0] step = ratio ** 0.0333 # 1/30 bins = [ordersizes[0] * (step ** i) for i in range(30)] else: bins = 30 plt.hist(ordersizes, bins, histtype='bar', rwidth=0.8) if bins is not 30: fig.axes[0].set_xscale('log') plt.grid() plt.xlabel('Order sizes / btc') plt.ylabel('Frequency') return get_graph_html(fig) + ("
log scale" if bins == 30 else "
linear") def get_graph_html(fig): imbuf = io.BytesIO() fig.savefig(imbuf, format='png') b64 = base64.b64encode(imbuf.getvalue()) 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_orderbook_table(db, btc_unit, rel_unit): result = '' rows = db.execute('SELECT * FROM orderbook;').fetchall() if not rows: return 0, result #print("len rows before filter: " + str(len(rows))) rows = [o for o in rows if o["ordertype"] in filtered_offername_list] order_keys_display = (('ordertype', ordertype_display), ('counterparty', do_nothing), ('oid', order_str), ('cjfee', cjfee_display), ('txfee', satoshi_to_unit), ('minsize', satoshi_to_unit), ('maxsize', satoshi_to_unit)) # somewhat complex sorting to sort by cjfee but with swabsoffers on top def orderby_cmp(x, y): if x['ordertype'] == y['ordertype']: return cmp(Decimal(x['cjfee']), Decimal(y['cjfee'])) return cmp(offername_list.index(x['ordertype']), offername_list.index(y['ordertype'])) for o in sorted(rows, cmp=orderby_cmp): result += ' \n' for key, displayer in order_keys_display: result += ' ' + displayer(o[key], o, btc_unit, rel_unit) + '\n' result += ' \n' return len(rows), result 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') } elif self.path == '/ordersize': replacements = { 'PAGETITLE': 'JoinMarket Browser Interface', 'MAINHEADING': 'Order Sizes', 'SECONDHEADING': 'Order Size Histogram' + alert_msg, 'MAINBODY': create_size_histogram(self.taker.db, args) } elif self.path.startswith('/depth'): # if self.path[6] == '?': # quantity = cj_amounts = [10 ** cja for cja in range(4, 12, 1)] mainbody = [create_depth_chart(self.taker.db, 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 replacements.iteritems(): 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) 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) httpd = BaseHTTPServer.HTTPServer(self.hostport, OrderbookPageRequestHeader) 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(): load_program_config(config_path='..') parser = OptionParser( usage='usage: %prog [options]', description='Runs a webservice which shows the orderbook.') 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() 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')