#!/usr/bin/env python3
from functools import cmp_to_key
import base64
import hashlib
import html
import http.server
import io
import json
import os
import threading
import time
import sys
from datetime import datetime, timedelta
from decimal import Decimal
from optparse import OptionParser
from typing import Tuple, Union
from twisted.internet import reactor
from urllib.parse import parse_qs
from jmbase import bintohex
from jmbase.support import EXIT_FAILURE
from jmbitcoin import bitcoin_unit_to_power, sat_to_unit, sat_to_unit_power
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')