#!/usr/bin/env python3
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 urllib.parse import parse_qs
from decimal import Decimal
from optparse import OptionParser
from twisted.internet import reactor
from datetime import datetime, timedelta
if sys.version_info < (3, 7):
print("ERROR: this script requires at least python 3.7")
exit(1)
from jmbase.support import EXIT_FAILURE
from jmbase import bintohex
from jmclient import FidelityBondMixin, get_interest_rate, check_and_start_tor
from jmclient.fidelity_bond import FidelityBondProof
import sybil_attack_calculations as sybil
from jmbase import get_log
log = get_log()
try:
import matplotlib
except:
log.warning("matplotlib not found, charts will not be available. "
"Do `pip install matplotlib` in the joinmarket virtual environment.")
if 'matplotlib' in sys.modules:
# https://stackoverflow.com/questions/2801882/generating-a-png-with-matplotlib-when-display-is-undefined
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from jmclient import jm_single, load_program_config, calc_cj_fee, \
get_mchannels, add_base_options
from jmdaemon import (OrderbookWatch, MessageChannelCollection,
OnionMessageChannel, IRCMessageChannel)
#TODO this is only for base58, find a solution for a client without jmbitcoin
import jmbitcoin as btc
from jmdaemon.protocol import *
bond_exponent = None
#Initial state: allow only SW offer types
sw0offers = list(filter(lambda x: x[0:3] == 'sw0', offername_list))
swoffers = list(filter(lambda x: x[0:3] == 'swa' or x[0:3] == 'swr', offername_list))
filtered_offername_list = sw0offers
rotateObform = '
\n')
}
elif self.path == '/fidelitybonds':
btc_unit = args['btcunit'][0] if 'btcunit' in args else sorted_units[0]
if btc_unit not in sorted_units:
btc_unit = sorted_units[0]
heading2, mainbody = self.create_fidelity_bond_table(btc_unit)
replacements = {
'PAGETITLE': 'JoinMarket Browser Interface',
'MAINHEADING': 'Fidelity Bonds',
'SECONDHEADING': heading2,
'MAINBODY': mainbody
}
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 == '/sybilresistance':
btc_unit = args['btcunit'][0] if 'btcunit' in args else sorted_units[0]
if btc_unit not in sorted_units:
btc_unit = sorted_units[0]
heading2, mainbody = self.create_sybil_resistance_page(btc_unit)
replacements = {
'PAGETITLE': 'JoinMarket Browser Interface',
'MAINHEADING': 'Resistance to Sybil Attacks from Fidelity Bonds',
'SECONDHEADING': heading2,
'MAINBODY': mainbody
}
elif self.path == '/orderbook.json':
replacements = {}
orderbook_fmt = json.dumps(self.create_orderbook_obj())
orderbook_page = orderbook_fmt
for key, rep in replacements.items():
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 = ['/refreshorderbook', '/rotateOb']
if self.path not in pages:
return
if self.path == '/refreshorderbook':
with self.taker.dblock:
self.taker.db.execute("DELETE FROM orderbook;")
self.taker.db.execute("DELETE FROM fidelitybonds;")
self.taker.msgchan.request_orderbook()
time.sleep(5)
self.path = '/'
self.do_GET()
elif self.path == '/rotateOb':
if filtered_offername_list == sw0offers:
log.debug('Showing nested segwit orderbook')
filtered_offername_list = swoffers
elif filtered_offername_list == swoffers:
log.debug('Showing native segwit orderbook')
filtered_offername_list = sw0offers
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)
# in client-server, this is passed by client
# in INIT message. Here, we have no Joinmarket client,
# but we have access to the client config in this script:
self.dust_threshold = jm_single().DUST_THRESHOLD
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()
"""An override for MessageChannel classes,
to allow receipt of privmsgs without the
verification hooks in client-daemon communication."""
def on_privmsg(inst, 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:
inst.check_for_orders(nick, _chunks)
inst.check_for_fidelity_bond(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.base58.encode(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():
global bond_exponent
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)
# needed to display notional units of FB valuation
bond_exponent = jm_single().config.get("POLICY", "bond_value_exponent")
try:
float(bond_exponent)
except ValueError:
log.error("Invalid entry for bond_value_exponent, should be decimal "
"number: {}".format(bond_exponent))
sys.exit(EXIT_FAILURE)
check_and_start_tor()
hostport = (options.host, options.port)
mcs = []
chan_configs = get_mchannels(mode="PASSIVE")
for c in chan_configs:
if "type" in c and c["type"] == "onion":
mcs.append(OnionMessageChannel(c))
else:
# default is IRC; TODO allow others
mcs.append(IRCMessageChannel(c))
IRCMessageChannel.on_privmsg = on_privmsg
OnionMessageChannel.on_privmsg = on_privmsg
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')