#! /usr/bin/env python from __future__ import absolute_import, print_function import base64 import pprint import random import sqlite3 import sys import time import threading import json from decimal import InvalidOperation, Decimal from jmdaemon.protocol import JM_VERSION from jmbase.support import get_log, joinmarket_alert, DUST_THRESHOLD from jmdaemon.irc import B_PER_SEC log = get_log() class JMTakerError(Exception): pass class OrderbookWatch(object): def set_msgchan(self, msgchan): self.msgchan = msgchan self.msgchan.register_orderbookwatch_callbacks(self.on_order_seen, self.on_order_cancel) self.msgchan.register_channel_callbacks( self.on_welcome, self.on_set_topic, None, self.on_disconnect, self.on_nick_leave, None) self.dblock = threading.Lock() con = sqlite3.connect(":memory:", check_same_thread=False) con.row_factory = sqlite3.Row self.db = con.cursor() self.db.execute("CREATE TABLE orderbook(counterparty TEXT, " "oid INTEGER, ordertype TEXT, minsize INTEGER, " "maxsize INTEGER, txfee INTEGER, cjfee TEXT);") @staticmethod def on_set_topic(newtopic): chunks = newtopic.split('|') for msg in chunks[1:]: try: msg = msg.strip() params = msg.split(' ') min_version = int(params[0]) max_version = int(params[1]) alert = msg[msg.index(params[1]) + len(params[1]):].strip() except ValueError, IndexError: continue if min_version < JM_VERSION < max_version: print('=' * 60) print('JOINMARKET ALERT') print(alert) print('=' * 60) joinmarket_alert[0] = alert def on_order_seen(self, counterparty, oid, ordertype, minsize, maxsize, txfee, cjfee): try: self.dblock.acquire(True) if int(oid) < 0 or int(oid) > sys.maxint: log.debug("Got invalid order ID: " + oid + " from " + counterparty) return (False, []) # delete orders eagerly, so in case a buggy maker sends an # invalid offer, we won't accidentally !fill based on the ghost # of its previous message. self.db.execute( ("DELETE FROM orderbook WHERE counterparty=? " "AND oid=?;"), (counterparty, oid)) # now validate the remaining fields if int(minsize) < 0 or int(minsize) > 21 * 10**14: log.debug("Got invalid minsize: {} from {}".format( minsize, counterparty)) return (False, []) if int(minsize) < DUST_THRESHOLD: minsize = DUST_THRESHOLD log.debug("{} has dusty minsize, capping at {}".format( counterparty, minsize)) # do not pass return, go not drop this otherwise fine offer if int(maxsize) < 0 or int(maxsize) > 21 * 10**14: log.debug("Got invalid maxsize: " + maxsize + " from " + counterparty) return (False, []) if int(txfee) < 0: log.debug("Got invalid txfee: {} from {}".format(txfee, counterparty)) return (False, []) if int(minsize) > int(maxsize): fmt = ("Got minsize bigger than maxsize: {} - {} " "from {}").format log.debug(fmt(minsize, maxsize, counterparty)) return (False, []) if ordertype == 'absoffer' and not isinstance(cjfee, int): try: cjfee = int(cjfee) except ValueError: log.debug("Got non integer coinjoin fee: " + str(cjfee) + " for an absoffer from " + counterparty) return (False, []) self.db.execute( 'INSERT INTO orderbook VALUES(?, ?, ?, ?, ?, ?, ?);', (counterparty, oid, ordertype, minsize, maxsize, txfee, str(Decimal(cjfee)))) # any parseable Decimal is a valid cjfee except InvalidOperation: log.debug("Got invalid cjfee: " + cjfee + " from " + counterparty) except Exception as e: log.debug("Error parsing order " + oid + " from " + counterparty) log.debug("Exception was: " + repr(e)) finally: self.dblock.release() return (True, []) def on_order_cancel(self, counterparty, oid): with self.dblock: self.db.execute( ("DELETE FROM orderbook WHERE " "counterparty=? AND oid=?;"), (counterparty, oid)) def on_nick_leave(self, nick): with self.dblock: self.db.execute('DELETE FROM orderbook WHERE counterparty=?;', (nick,)) def on_disconnect(self): with self.dblock: self.db.execute('DELETE FROM orderbook;')