2 changed files with 561 additions and 0 deletions
@ -0,0 +1,456 @@ |
|||||||
|
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 all SW+legacy offer types |
||||||
|
filtered_offername_list = offername_list |
||||||
|
|
||||||
|
shutdownform = '<form action="shutdown" method="post"><input type="submit" value="Shutdown" /></form>' |
||||||
|
shutdownpage = '<html><body><center><h1>Successfully Shut down</h1></center></body></html>' |
||||||
|
toggleSWform = '<form action="toggleSW" method="post"><input type="submit" value="Toggle non-segwit" /></form>' |
||||||
|
refresh_orderbook_form = '<form action="refreshorderbook" method="post"><input type="submit" value="Check for timed-out counterparties" /></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 = {} |
||||||
|
sqlorders = db.execute('SELECT * FROM orderbook;').fetchall() |
||||||
|
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 FROM orderbook;').fetchall() |
||||||
|
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) + ("<br/><a href='?scale=log'>log scale</a>" if |
||||||
|
bins == 30 else "<br/><a href='?'>linear</a>") |
||||||
|
|
||||||
|
|
||||||
|
def get_graph_html(fig): |
||||||
|
imbuf = io.BytesIO() |
||||||
|
fig.savefig(imbuf, format='png') |
||||||
|
b64 = base64.b64encode(imbuf.getvalue()) |
||||||
|
return '<img src="data:image/png;base64,' + b64 + '" />' |
||||||
|
|
||||||
|
|
||||||
|
# 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 += ' <tr>\n' |
||||||
|
for key, displayer in order_keys_display: |
||||||
|
result += ' <td>' + displayer(o[key], o, btc_unit, |
||||||
|
rel_unit) + '</td>\n' |
||||||
|
result += ' </tr>\n' |
||||||
|
return len(rows), result |
||||||
|
|
||||||
|
|
||||||
|
def create_table_heading(btc_unit, rel_unit): |
||||||
|
col = ' <th>{1}</th>\n' # .format(field,label) |
||||||
|
tableheading = '<table class="tftable sortable" border="1">\n <tr>' + ''.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) |
||||||
|
]) + ' </tr>' |
||||||
|
return tableheading |
||||||
|
|
||||||
|
|
||||||
|
def create_choose_units_form(selected_btc, selected_rel): |
||||||
|
choose_units_form = ( |
||||||
|
'<form method="get" action="">' + |
||||||
|
'<select name="btcunit" onchange="this.form.submit();">' + |
||||||
|
''.join(('<option>' + u + ' </option>' for u in sorted_units)) + |
||||||
|
'</select><select name="relunit" onchange="this.form.submit();">' + |
||||||
|
''.join(('<option>' + u + ' </option>' for u in sorted_rel_units)) + |
||||||
|
'</select></form>') |
||||||
|
choose_units_form = choose_units_form.replace( |
||||||
|
'<option>' + selected_btc, |
||||||
|
'<option selected="selected">' + selected_btc) |
||||||
|
choose_units_form = choose_units_form.replace( |
||||||
|
'<option>' + selected_rel, |
||||||
|
'<option selected="selected">' + selected_rel) |
||||||
|
return choose_units_form |
||||||
|
|
||||||
|
|
||||||
|
class OrderbookPageRequestHeader(SimpleHTTPServer.SimpleHTTPRequestHandler): |
||||||
|
def __init__(self, request, client_address, base_server): |
||||||
|
self.taker = base_server.taker |
||||||
|
self.base_server = base_server |
||||||
|
SimpleHTTPServer.SimpleHTTPRequestHandler.__init__( |
||||||
|
self, request, client_address, base_server) |
||||||
|
|
||||||
|
def create_orderbook_obj(self): |
||||||
|
rows = self.taker.db.execute('SELECT * FROM orderbook;').fetchall() |
||||||
|
if not rows: |
||||||
|
return [] |
||||||
|
|
||||||
|
result = [] |
||||||
|
for row in rows: |
||||||
|
o = dict(row) |
||||||
|
if 'cjfee' in o: |
||||||
|
o['cjfee'] = int(o['cjfee']) if o[ |
||||||
|
'ordertype'] == 'swabsoffer' else float( |
||||||
|
o['cjfee']) |
||||||
|
result.append(o) |
||||||
|
return result |
||||||
|
|
||||||
|
def get_counterparty_count(self): |
||||||
|
counterparties = self.taker.db.execute( |
||||||
|
'SELECT DISTINCT counterparty FROM orderbook;').fetchall() |
||||||
|
return str(len(counterparties)) |
||||||
|
|
||||||
|
def do_GET(self): |
||||||
|
# SimpleHTTPServer.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 = urllib2.urlparse.parse_qs(query) |
||||||
|
pages = ['/', '/ordersize', '/depth', '/orderbook.json'] |
||||||
|
if self.path not in pages: |
||||||
|
return |
||||||
|
fd = open('orderbook.html', 'r') |
||||||
|
orderbook_fmt = fd.read() |
||||||
|
fd.close() |
||||||
|
alert_msg = '' |
||||||
|
if jm_single().joinmarket_alert[0]: |
||||||
|
alert_msg = '<br />JoinMarket Alert Message:<br />' + \ |
||||||
|
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 = create_orderbook_table( |
||||||
|
self.taker.db, 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 + '</table>\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] + \ |
||||||
|
["<br/><a href='?'>linear</a>" if args.get("scale") \ |
||||||
|
else "<br/><a href='?scale=log'>log scale</a>"] |
||||||
|
replacements = { |
||||||
|
'PAGETITLE': 'JoinMarket Browser Interface', |
||||||
|
'MAINHEADING': 'Depth Chart', |
||||||
|
'SECONDHEADING': 'Orderbook Depth' + alert_msg, |
||||||
|
'MAINBODY': '<br />'.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 == offername_list: |
||||||
|
filtered_offername_list = ["swreloffer", "swabsoffer"] |
||||||
|
else: |
||||||
|
filtered_offername_list = offername_list |
||||||
|
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.""" |
||||||
|
import binascii |
||||||
|
nick_pkh_raw = hashlib.sha256(os.urandom(10)).digest()[:NICK_HASH_LENGTH] |
||||||
|
nick_pkh = btc.changebase(nick_pkh_raw, 256, 58) |
||||||
|
#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') |
||||||
@ -0,0 +1,105 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
|
||||||
|
<html lang="en"> |
||||||
|
|
||||||
|
<head> |
||||||
|
<title>PAGETITLE</title> |
||||||
|
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet"> |
||||||
|
<script src="https://www.kryogenix.org/code/browser/sorttable/sorttable.js"></script> |
||||||
|
|
||||||
|
<style> |
||||||
|
body { |
||||||
|
padding-top: 50px; |
||||||
|
} |
||||||
|
body { |
||||||
|
background-color: linen; |
||||||
|
} |
||||||
|
h1 { |
||||||
|
color: black |
||||||
|
} |
||||||
|
h2 { |
||||||
|
color: gray; |
||||||
|
} |
||||||
|
table, th, td, tr { |
||||||
|
border: 1px solid black; |
||||||
|
border-collapse: collapse; |
||||||
|
text-align: center; |
||||||
|
padding: 5px; |
||||||
|
} |
||||||
|
.tftable {font-size:12px;color:#fbfbfb;border-width: 1px;border-color: #686767;border-collapse: collapse;margin: 10px auto;} |
||||||
|
.tftable th {font-size:12px;background-color:#171515;border-width: 1px;padding: 8px;border-style: solid;border-color: #686767;text-align:center;} |
||||||
|
.tftable tr {background-color:#2f2f2f;} |
||||||
|
.tftable td {font-size:12px;border-width: 1px;padding: 8px;border-style: solid;border-color: #686767;} |
||||||
|
.tftable tr:hover {background-color:#171515;} |
||||||
|
.tftable tr.selected {background-color:#171515;} |
||||||
|
</style> |
||||||
|
|
||||||
|
|
||||||
|
<script> |
||||||
|
|
||||||
|
function myboolean(value) { |
||||||
|
return !(value=="false" || value=="" || value==null); |
||||||
|
} |
||||||
|
function getOrderKey(tr) { |
||||||
|
var row = $(tr).find("td"); |
||||||
|
if(row.length==0){ return null; } |
||||||
|
return $(row[1]).text().replace(/_*$/g,"")+"/"+$(row[2]).text(); |
||||||
|
} |
||||||
|
|
||||||
|
var selected; |
||||||
|
$(document).ready(function(){ |
||||||
|
var rows = $("table.tftable tr"); |
||||||
|
var i; |
||||||
|
/*for(i=1 ; i<rows.length ; i++){ |
||||||
|
$($(rows[i]).find("td")[1]).css( "background-color", "red" ); |
||||||
|
}*/ |
||||||
|
for(i=1 ; i<rows.length ; i++) { |
||||||
|
var key = getOrderKey(rows[i]); |
||||||
|
if(key!=null && myboolean(localStorage.getItem(key))){ |
||||||
|
$(rows[i]).addClass("selected"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
$(".tftable tr").click(function(){ |
||||||
|
$(this).toggleClass("selected"); |
||||||
|
var element = getOrderKey(this); |
||||||
|
// toggle: |
||||||
|
localStorage.setItem(element,String(!myboolean(localStorage.getItem(element)))); |
||||||
|
}); |
||||||
|
|
||||||
|
}); |
||||||
|
</script> |
||||||
|
|
||||||
|
</head> |
||||||
|
|
||||||
|
|
||||||
|
<body> |
||||||
|
<nav class="navbar navbar-inverse navbar-fixed-top"> |
||||||
|
<div class="container"> |
||||||
|
<div class="navbar-header"> |
||||||
|
<a class="navbar-brand" href="/">Join Market</a> |
||||||
|
</div> |
||||||
|
<div id="navbar" class="collapse navbar-collapse"> |
||||||
|
<ul class="nav navbar-nav"> |
||||||
|
<li class="active"><a href="/">Orders</a></li> |
||||||
|
<li><a href="/ordersize">Size Distribution</a></li> |
||||||
|
<li><a href="/depth">Depth</a></li> |
||||||
|
<li><a href="/orderbook.json">Export orders</a></li> |
||||||
|
<li><a target="_blank" href="https://github.com/AdamISZ/joinmarket-clientserver/releases">New segwit version</a></li> |
||||||
|
<li><a target="_blank" href="https://github.com/Joinmarket-Org/joinmarket/releases">Non segwit version</a></li> |
||||||
|
</ul> |
||||||
|
</div><!--/.nav-collapse --> |
||||||
|
</div> |
||||||
|
</nav> |
||||||
|
|
||||||
|
<div style="text-align:center; margin:0px auto;"> |
||||||
|
<h1>MAINHEADING</h1> |
||||||
|
<h2>SECONDHEADING</h2> |
||||||
|
|
||||||
|
MAINBODY |
||||||
|
<br> |
||||||
|
</div> |
||||||
|
</body> |
||||||
|
|
||||||
|
</html> |
||||||
Loading…
Reference in new issue