diff --git a/.coveragerc b/.coveragerc index 9d6f03a..a0e8796 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,3 +10,7 @@ omit = jmbitcoin/setup.py jmbase/test/* jmbase/setup.py + jmdaemon/test/* + jmdaemon/setup.py + jmdaemon/jmdaemon/socks.py + jmdaemon/jmdaemon/irc.py diff --git a/conftest.py b/conftest.py index 1e323d0..abfa846 100644 --- a/conftest.py +++ b/conftest.py @@ -7,6 +7,7 @@ bitcoin_path = None bitcoin_conf = None bitcoin_rpcpassword = None bitcoin_rpcusername = None +miniircd_procs = [] def local_command(command, bg=False, redirect=''): if redirect == 'NULL': @@ -46,8 +47,17 @@ def pytest_addoption(parser): action="store", default='bitcoinrpc', help="the RPC username for your test bitcoin instance (default=bitcoinrpc)") + parser.addoption("--nirc", + type="int", + action="store", + default=1, + help="the number of local miniircd instances") def teardown(): + #didn't find a stop command in miniircd, so just kill + global miniircd_procs + for m in miniircd_procs: + m.kill() #shut down bitcoin and remove the regtest dir local_command([bitcoin_path + "bitcoin-cli", "-regtest", "-rpcuser=" + bitcoin_rpcusername, "-rpcpassword=" + bitcoin_rpcpassword, "stop"]) @@ -65,6 +75,17 @@ def setup(request): bitcoin_rpcpassword = request.config.getoption("--btcpwd") bitcoin_rpcusername = request.config.getoption("--btcuser") + #start up miniircd + #minor bug in miniircd (seems); need *full* unqualified path for motd file + cwd = os.getcwd() + n_irc = request.config.getoption("--nirc") + global miniircd_procs + for i in range(n_irc): + miniircd_proc = local_command( + ["./miniircd/miniircd", "--ports=" + str(6667+i), + "--motd=" + cwd + "/miniircd/testmotd"], + bg=True) + miniircd_procs.append(miniircd_proc) #start up regtest blockchain btc_proc = subprocess.call([bitcoin_path + "bitcoind", "-regtest", "-daemon", "-conf=" + bitcoin_conf]) diff --git a/jmdaemon/jmdaemon/__init__.py b/jmdaemon/jmdaemon/__init__.py index 8dd27fd..2b20230 100644 --- a/jmdaemon/jmdaemon/__init__.py +++ b/jmdaemon/jmdaemon/__init__.py @@ -10,11 +10,13 @@ from .message_channel import MessageChannel, MessageChannelCollection from .orderbookwatch import OrderbookWatch from jmbase import commands from .daemon_protocol import JMDaemonServerProtocolFactory - +from .protocol import (COMMAND_PREFIX, ORDER_KEYS, NICK_HASH_LENGTH, + NICK_MAX_ENCODED, JM_VERSION, JOINMARKET_NICK_HEADER) +from .message_channel import MessageChannelCollection # Set default logging handler to avoid "No handler found" warnings. try: from logging import NullHandler -except ImportError: +except ImportError: #pragma: no cover class NullHandler(logging.Handler): def emit(self, record): pass diff --git a/jmdaemon/jmdaemon/daemon_protocol.py b/jmdaemon/jmdaemon/daemon_protocol.py index adc9053..46edb1d 100644 --- a/jmdaemon/jmdaemon/daemon_protocol.py +++ b/jmdaemon/jmdaemon/daemon_protocol.py @@ -56,6 +56,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): self.restart_mc_required = False self.irc_configs = None self.mcc = None + self.crypto_boxes = {} self.sig_lock = threading.Lock() def checkClientResponse(self, response): @@ -63,7 +64,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch): is considered criticial. """ if 'accepted' not in response or not response['accepted']: - reactor.stop() + reactor.stop() #pragma: no cover def defaultErrback(self, failure): failure.trap(ConnectionAborted, ConnectionClosed, ConnectionDone, ConnectionLost) diff --git a/jmdaemon/jmdaemon/irc.py b/jmdaemon/jmdaemon/irc.py index 685cb0b..4fbb23a 100644 --- a/jmdaemon/jmdaemon/irc.py +++ b/jmdaemon/jmdaemon/irc.py @@ -404,6 +404,12 @@ class IRCMessageChannel(MessageChannel): self.pingQ = Queue.Queue() self.throttleQ = Queue.Queue() self.obQ = Queue.Queue() + self.reconnect_interval = 30 + + def set_reconnect_interval(self, interval): + """For testing reconnection functions. + """ + self.reconnect_interval = interval def run(self): self.give_up = False @@ -467,6 +473,6 @@ class IRCMessageChannel(MessageChannel): log.info("disconnected from irc host %s" % (self.hostid)) if not self.give_up: - time.sleep(30) + time.sleep(self.reconnect_interval) log.info('ending irc') self.give_up = True diff --git a/jmdaemon/jmdaemon/message_channel.py b/jmdaemon/jmdaemon/message_channel.py index 5b2d975..7a76c7b 100644 --- a/jmdaemon/jmdaemon/message_channel.py +++ b/jmdaemon/jmdaemon/message_channel.py @@ -148,7 +148,7 @@ class MessageChannelCollection(object): for mc in self.mchannels: mc.daemon = daemon - def add_channel(self, mchannel): + def add_channel(self, mchannel): #pragma: no cover """TODO Not currently in use, may be some issues with intialization. """ @@ -277,7 +277,7 @@ class MessageChannelCollection(object): matching_channels = [x for x in self.available_channels() if mc == x.hostid] - if len(matching_channels) != 1: + if len(matching_channels) != 1: #pragma: no cover #raise because implies logic error raise Exception( "Tried to privmsg on an unavailable message channel.") @@ -850,7 +850,6 @@ class MessageChannel(object): def send_error(self, nick, errormsg): log.info('error<%s> : %s' % (nick, errormsg)) self.privmsg(nick, 'error', errormsg) - raise CJPeerError() def pubmsg(self, message): log.debug('>>pubmsg ' + message) @@ -892,7 +891,7 @@ class MessageChannel(object): elif _chunks[0] == 'orderbook': if self.on_orderbook_requested: self.on_orderbook_requested(nick, self) - else: + else: #pragma: no cover # TODO this is for testing/debugging, should be removed, see taker.py if hasattr(self, 'debug_on_pubmsg_cmd'): self.debug_on_pubmsg_cmd(nick, _chunks) @@ -949,8 +948,8 @@ class MessageChannel(object): to_decrypt = ''.join(_chunks[1:]) try: decrypted = decode_decrypt(to_decrypt, box) - except ValueError as e: - log.debug('valueerror when decrypting, skipping: ' + + except (ValueError, TypeError) as e: + log.debug('Error when decrypting, skipping: ' + repr(e)) return #rebuild the chunks array as if it had been plaintext @@ -997,13 +996,13 @@ class MessageChannel(object): commit = None except (ValueError, IndexError) as e: self.send_error(nick, str(e)) + return if self.on_order_fill: self.on_order_fill(nick, oid, amount, taker_pk, commit) elif _chunks[0] == 'auth': - try: - cr = _chunks[1] - except (ValueError, IndexError) as e: - self.send_error(nick, str(e)) + #Note index error logically impossible, would have thrown + #in sig check (zero message after cmd not allowed) + cr = _chunks[1] if self.on_seen_auth: self.on_seen_auth(nick, cr) elif _chunks[0] == 'tx': @@ -1012,6 +1011,7 @@ class MessageChannel(object): txhex = base64.b64decode(b64tx).encode('hex') except TypeError as e: self.send_error(nick, 'bad base64 tx. ' + repr(e)) + return if self.on_seen_tx: self.on_seen_tx(nick, txhex) elif _chunks[0] == 'push': @@ -1020,9 +1020,10 @@ class MessageChannel(object): txhex = base64.b64decode(b64tx).encode('hex') except TypeError as e: self.send_error(nick, 'bad base64 tx. ' + repr(e)) + return if self.on_push_tx: self.on_push_tx(nick, txhex) - except CJPeerError: + except (IndexError, ValueError): # TODO proper error handling log.debug('cj peer error TODO handle') continue diff --git a/jmdaemon/jmdaemon/orderbookwatch.py b/jmdaemon/jmdaemon/orderbookwatch.py index 1e5b33a..28e2588 100644 --- a/jmdaemon/jmdaemon/orderbookwatch.py +++ b/jmdaemon/jmdaemon/orderbookwatch.py @@ -65,7 +65,7 @@ class OrderbookWatch(object): if int(oid) < 0 or int(oid) > sys.maxint: log.debug("Got invalid order ID: " + oid + " from " + counterparty) - return (False, []) + return # 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. @@ -76,7 +76,7 @@ class OrderbookWatch(object): if int(minsize) < 0 or int(minsize) > 21 * 10**14: log.debug("Got invalid minsize: {} from {}".format( minsize, counterparty)) - return (False, []) + return if int(minsize) < DUST_THRESHOLD: minsize = DUST_THRESHOLD log.debug("{} has dusty minsize, capping at {}".format( @@ -85,24 +85,24 @@ class OrderbookWatch(object): if int(maxsize) < 0 or int(maxsize) > 21 * 10**14: log.debug("Got invalid maxsize: " + maxsize + " from " + counterparty) - return (False, []) + return if int(txfee) < 0: log.debug("Got invalid txfee: {} from {}".format(txfee, counterparty)) - return (False, []) + return if int(minsize) > int(maxsize): fmt = ("Got minsize bigger than maxsize: {} - {} " "from {}").format log.debug(fmt(minsize, maxsize, counterparty)) - return (False, []) + return 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, []) + return self.db.execute( 'INSERT INTO orderbook VALUES(?, ?, ?, ?, ?, ?, ?);', (counterparty, oid, ordertype, minsize, maxsize, txfee, @@ -114,7 +114,6 @@ class OrderbookWatch(object): log.debug("Exception was: " + repr(e)) finally: self.dblock.release() - return (True, []) def on_order_cancel(self, counterparty, oid): with self.dblock: diff --git a/jmdaemon/test/dummy_mc.py b/jmdaemon/test/dummy_mc.py new file mode 100644 index 0000000..edcb0d7 --- /dev/null +++ b/jmdaemon/test/dummy_mc.py @@ -0,0 +1,69 @@ +from __future__ import absolute_import, print_function + +import base64 +import random +import socket +import ssl +import threading +import time + + + +from jmdaemon.message_channel import MessageChannel +from jmdaemon.protocol import * +from jmclient import get_log +from msgdata import * + +log = get_log() + +# handle one channel at a time +class DummyMessageChannel(MessageChannel): + + def __init__(self, + configdata, + username='username', + realname='realname', + password=None, + daemon=None, + hostid=None): + MessageChannel.__init__(self, daemon=daemon) + self.give_up = False + self.counterparties = [x['counterparty'] for x in t_orderbook] + self.hostid = "dummy" + if hostid: + self.hostid = hostid + self.serverport = self.hostid + + def __str__(self): + return self.hostid + + def run(self): + """Simplest possible event loop.""" + i = 0 + while True: + if self.give_up: + break + time.sleep(0.5) + if i == 1: + if self.on_welcome: + log.debug("Calling on welcome") + self.on_welcome(self) + i += 1 + + def shutdown(self): + self.give_up = True + + def close(self): + self.shutdown() + + def _pubmsg(self, msg): + pass + def _privmsg(self, nick, cmd, message): + """As for pubmsg + """ + pass + def _announce_orders(self, orderlist): + pass + def change_nick(self, new_nick): + print("Changing nick supposedly") + diff --git a/jmdaemon/test/msgdata.py b/jmdaemon/test/msgdata.py new file mode 100644 index 0000000..0b3dc67 --- /dev/null +++ b/jmdaemon/test/msgdata.py @@ -0,0 +1,190 @@ +#orderbook +t_orderbook = [{u'counterparty': u'J5FA1Gj7Ln4vSGne', u'ordertype': u'reloffer', u'oid': 0, + u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, + {u'counterparty': u'J5CFffuuewjG44UJ', u'ordertype': u'reloffer', u'oid': 0, + u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, + {u'counterparty': u'J55z23xdjxJjC7er', u'ordertype': u'reloffer', u'oid': 0, + u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, + {u'counterparty': u'J54Ghp5PXCdY9H3t', u'ordertype': u'reloffer', u'oid': 0, + u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, + {u'counterparty': u'J559UPUSLLjHJpaB', u'ordertype': u'reloffer', u'oid': 0, + u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, + {u'counterparty': u'J5cBx1FwUVh9zzoO', u'ordertype': u'reloffer', u'oid': 0, + u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}] + +t_dest_addr = "mvw1NazKDRbeNufFANqpYNAANafsMC2zVU" + +t_chosen_orders = {u'J559UPUSLLjHJpaB': {u'cjfee': u'0.0002', + u'counterparty': u'J559UPUSLLjHJpaB', + u'maxsize': 599972700, + u'minsize': 7500000, + u'oid': 0, + u'ordertype': u'reloffer', + u'txfee': 1000}, + u'J55z23xdjxJjC7er': {u'cjfee': u'0.0002', + u'counterparty': u'J55z23xdjxJjC7er', + u'maxsize': 599972700, + u'minsize': 7500000, + u'oid': 0, + u'ordertype': u'reloffer', + u'txfee': 1000}, + u'J5CFffuuewjG44UJ': {u'cjfee': u'0.0002', + u'counterparty': u'J5CFffuuewjG44UJ', + u'maxsize': 599972700, + u'minsize': 7500000, + u'oid': 0, + u'ordertype': u'reloffer', + u'txfee': 1000}} + +""" +2016-12-01 15:27:33,351 [MainThread ] [INFO ] total cj fee = 63000 +2016-12-01 15:27:33,351 [MainThread ] [INFO ] total coinjoin fee = 0.0573% +2016-12-01 15:27:34,887 [MainThread ] [DEBUG] INFO:Preparing bitcoin data.. +2016-12-01 15:27:34,888 [MainThread ] [DEBUG] rpc: getaccount ['myzi6K9vt88rdiXpYayfJkU1x33G1wz2fP'] +2016-12-01 15:27:34,889 [MainThread ] [DEBUG] total estimated amount spent = 110093000 +""" +t_utxos_by_mixdepth = {0: {u'534b635ed8891f16c4ec5b8236ae86164783903e8e8bb47fa9ef2ca31f3c2d7a:0': {'address': u'mrcNu71ztWjAQA6ww9kHiW3zBWSQidHXTQ', + 'value': 200000000}}, + 1: {u'0780d6e5e381bff01a3519997bb4fcba002493103a198fde334fd264f9835d75:1': {'address': u'mvtY8DVgn3TtvjHbVsauYoSQjAhNqVyqmM', + 'value': 200000000}, + u'7e574db96a4d43a99786b3ea653cda9e4388f377848f489332577e018380cff1:0': {'address': u'n3nELhmU2D7ebGYzJnGFWgVDK3cYErmTcQ', + 'value': 200000000}, + u'dd9711a2ef340750db21efb761f5f7d665d94b312332dc354e252c77e9c48349:0': {'address': u'mxeLuX8PP7qLkcM8uarHmdZyvP1b5e1Ynf', + 'value': 200000000}}, + 2: {}, + 3: {}, + 4: {}} + +t_selected_utxos = [{'utxo': u'0780d6e5e381bff01a3519997bb4fcba002493103a198fde334fd264f9835d75:1', + 'value': 200000000}] + +t_generated_podle = {'P': '025a2e04dc6bd5f58fe4eb13045b27f0dd17c39524264639f48607347cf6d69c4e', + 'P2': '0223e54e9917d8482f1b54ead8e941907c17051b95397e8bc110adc6681d8d44c8', + 'commit': 'aa0545c9ed918e66f86df467c96a4978529b836aa4688df682a2db4e27d4ed9d', + 'e': '5b7ab1fa21287bbf0df4a0c46f6c31c3f17887ee9ea6ae584fc3a861ae9f1e9d', + 'sig': 'ebe25d7b2d667de802677c30c6fea07386f0cd67d4e4c795e4a6ebc39b21eb39', + 'used': 'False', + 'utxo': u'0780d6e5e381bff01a3519997bb4fcba002493103a198fde334fd264f9835d75:1'} + +t_maker_response = {"J559UPUSLLjHJpaB": + [["03243f4a659e278a1333f8308f6aaf32db4692ee7df0340202750fd6c09150f6:1"], + "03a2d1cbe977b1feaf8d0d5cc28c686859563d1520b28018be0c2661cf1ebe4857", + "mrKTGvFfYUEqk52qPKUroumZJcpjHLQ6pn", + "mxPnzFkCQpPzVQdajNLoT4us5pTPsQZZZp", + "MEQCIBeGrtxxVrj5tSUX6vEetmzE8nRBG/guSXq3SrqypIt5AiAnIZzDUXu8DtODgF2p1Bo27L8VcG1GJSfatZbS23YZQQ==", + "5bcc7ae1a3530e454812668620aced47d774bf06a1f5870d531422a1a958b629"], + "J55z23xdjxJjC7er": + [["498faa8b22534f3b443c6b0ce202f31e12f21668b4f0c7a005146808f250d4c3:0"], + "02b4b749d54e96b04066b0803e372a43d6ffa16e75a001ae0ed4b235674ab286be", + "mhatyHdna3Qt5FtnfwWaMVV1dohCaDYF3T", + "mjJoVN2HCUGVDvNebiFnHdB3zF56bxQm5z", + "MEQCIBlMF7DRbhr14e74He9m+UYjR5y8jjvP7TvUh8valebmAiBoIGjl436fsYim9pKSTbCKiBmT82hQ98LvIOGSLprk0A==", + "8204d1cba30d4cdabab16a5e8d10d17464e24c78a6f887ae2d920b223c030d28"], + "J5CFffuuewjG44UJ": + [["3f3ea820d706e08ad8dc1d2c392c98facb1b067ae4c671043ae9461057bd2a3c:1"], + "023bcbafb4f68455e0d1d117c178b0e82a84e66414f0987453d78da034b299c3a9", + "mpAEocXy8ckcJBo3fhQg9Mv1kfEzAuUivX", + "n29NWbsyq5MjCMC5ykjStd78zwfjvCvJJZ", + "MEUCIQDAM5Aa0aU5iKI0b9YnNtwH0m+6sz3zeTL8f398CPjuQAIgLeU9mCJX8SupNNMkA+bsUJeRYe3kiLnzq3OlmXTxck0=", + "7377d03477485884e0129dbdb2d79f4956f5b74366d805385b6f127509a8433f"]} + +""" +2016-12-01 15:27:39,914 [MainThread ] [DEBUG] rpc: gettxout [u'03243f4a659e278a1333f8308f6aaf32db4692ee7df0340202750fd6c09150f6', 1, False] +2016-12-01 15:27:39,915 [MainThread ] [DEBUG] fee breakdown for J559UPUSLLjHJpaB totalin=200000000 cjamount=110000000 txfee=1000 realcjfee=22000 +2016-12-01 15:27:39,915 [MainThread ] [DEBUG] rpc: gettxout [u'498faa8b22534f3b443c6b0ce202f31e12f21668b4f0c7a005146808f250d4c3', 0, False] +2016-12-01 15:27:39,915 [MainThread ] [DEBUG] fee breakdown for J55z23xdjxJjC7er totalin=200000000 cjamount=110000000 txfee=1000 realcjfee=22000 +2016-12-01 15:27:39,916 [MainThread ] [DEBUG] rpc: gettxout [u'3f3ea820d706e08ad8dc1d2c392c98facb1b067ae4c671043ae9461057bd2a3c', 1, False] +2016-12-01 15:27:39,916 [MainThread ] [DEBUG] fee breakdown for J5CFffuuewjG44UJ totalin=200000000 cjamount=110000000 txfee=1000 realcjfee=22000 +2016-12-01 15:27:39,916 [MainThread ] [DEBUG] INFO:Got all parts, enough to build a tx +2016-12-01 15:27:39,917 [MainThread ] [DEBUG] Estimated transaction size: 870 +2016-12-01 15:27:39,917 [MainThread ] [DEBUG] rpc: estimatefee [3] +2016-12-01 15:27:39,917 [MainThread ] [DEBUG] got estimated tx bytes: 870 +2016-12-01 15:27:39,917 [MainThread ] [INFO ] Based on initial guess: 30000, we estimated a miner fee of: 26100 +2016-12-01 15:27:39,918 [MainThread ] [INFO ] fee breakdown for me totalin=200000000 my_txfee=23100 makers_txfee=3000 cjfee_total=66000 => changevalue=89910900 +""" + +t_obtained_tx = {'ins': [{'outpoint': {'hash': '03243f4a659e278a1333f8308f6aaf32db4692ee7df0340202750fd6c09150f6', + 'index': 1}, + 'script': '', + 'sequence': 4294967295}, + {'outpoint': {'hash': '3f3ea820d706e08ad8dc1d2c392c98facb1b067ae4c671043ae9461057bd2a3c', + 'index': 1}, + 'script': '', + 'sequence': 4294967295}, + {'outpoint': {'hash': '498faa8b22534f3b443c6b0ce202f31e12f21668b4f0c7a005146808f250d4c3', + 'index': 0}, + 'script': '', + 'sequence': 4294967295}, + {'outpoint': {'hash': '0780d6e5e381bff01a3519997bb4fcba002493103a198fde334fd264f9835d75', + 'index': 1}, + 'script': '', + 'sequence': 4294967295}], + 'locktime': 0, + 'outs': [{'script': '76a914767c956efe6092a775fea39a06d1cac9aae956d788ac', + 'value': 110000000}, + {'script': '76a914cab20e3270988ac99651b8f079a3b4c93b996a6888ac', + 'value': 89910900}, + {'script': '76a914b91f75254b5fa1510cc944a2206ed72235d0d88188ac', + 'value': 90021000}, + {'script': '76a914a916707952c2df28a3abf3ee692dfbbd5a4d74dc88ac', + 'value': 110000000}, + {'script': '76a914e245b480b46bcbc9d13e68766ad19909decd135288ac', + 'value': 90021000}, + {'script': '76a91416af241bb1db02dfd7c65989bbab190ac489ccc188ac', + 'value': 110000000}, + {'script': '76a9142994295a9d4d083eb792e669e3211007dc78928888ac', + 'value': 90021000}, + {'script': '76a9145ece2dac945c8ff5b2b6635360ca0478ade305d488ac', + 'value': 110000000}], + 'version': 1} + +#signatures from makers +""" +nick=J5CFffuuewjG44UJ message=!sig xH9IAMo2fvG+g+DAbLNOPsGsJCDm6r+ZY5QM7p+SRsixbqSwXcBQAn7Mnw1rS+uGlrJkM8ossX5VHKjdKDhTXQVLawR7XgiVFFFiO+/FjdFhqVuS4Q/NgOlb7nCBe/UaBebd9NpuURG+8u/V+46jtqKRtVsSO1+QZQBt2nSpYCqxWIjxMowRxS4O/zlrOVbyjv/AjchOajufKJwckkrkJDyQDYlUdW+eqs43tf0XsJ9k4NHRVVHAQQ== 036558f550b1d398d2325d892e50ef25b0f663ae13f70d0b304a15f07030061ace MEUCIQCE9MgU+HfcHkKE8zNzNeCEdDBJuQatA6C2sTJ9mVKK7wIgX4w9r0tz4s9qeuW0UjNliDatJ4X7pS3/atADSqPat0U= +nick=J559UPUSLLjHJpaB message=!sig geHTf1n88eKeUnVOj7bIrJF1KFCN03IQhZD0cR17Q7jPSn2DZrrvMaRNkjZRyF+zGnWFwd69kwLRU0ftCaMf/3lw+05UovVCREiyXWUtPJa7XAY2NW4iMmTnGTp8f9RLgDcDhiZayKXTpzBDC9r6WAt6wiD0lej5uw7dmluKSUyfXW8sOYPmLm4iJAPcbGeJiQfiR9zBeX8w+6Kz4bkaiue41SzQP/h9avPV2XIX4kVQQ3jLfQyHww== 038f90ab260df440cef82a981146b509eb9df019884e145158230e8babc17d7be4 MEQCIEo5Pau9zqW2lw+B2AYTYuTO5TDbBkgsOk0bqT+SQctKAiBO1nbsmYTy7E0Qd7jAxko1Gq6Yk0Q6DerByuEuk5IBSQ== +nick=J55z23xdjxJjC7er message=!sig A5CWvqmYCOiZBEEi9iHVpQL0oO9B7VIIzuU9QhkzXOw+iD916C9b+Yk3eTxrtf+qaLARQ7eui6zdPNek95EdmqCEqM/myeeuBVSy9KrcB9xU0sdnuCu4+g13jVe9Pkvd1iizZ8GCNP7SejEzeltNr0a1lR+M0kKtj4XI+nDTxhisSzL8PDXsqoOMcrDjegna3TZsJeKviu8r/1T/zWwTQtRCXqruLnflqXNLtZoyFmoaO1GurgkNHA== 029a8beadec242f04f2295787ac0175b960e2d68d115ec65c4310de7ce3fa2cec0 MEQCIHpTxVkwtvm7agbp47Z5V0We8jxXkfZDUFsW2tZwTZdHAiA9JnYvo74hF3RihzHw2l+ufTOmC/3ddBpxkB9+AdZvzA== +""" + +""" +2016-12-01 15:27:39,921 [MainThread ] [DEBUG] INFO:Built tx, sending to counterparties. +2016-12-01 15:27:39,968 [MainThread ] [DEBUG] rpc: gettxout ['03243f4a659e278a1333f8308f6aaf32db4692ee7df0340202750fd6c09150f6', 1, False] +2016-12-01 15:27:39,968 [MainThread ] [DEBUG] rpc: gettxout ['3f3ea820d706e08ad8dc1d2c392c98facb1b067ae4c671043ae9461057bd2a3c', 1, False] +2016-12-01 15:27:39,969 [MainThread ] [DEBUG] rpc: gettxout ['498faa8b22534f3b443c6b0ce202f31e12f21668b4f0c7a005146808f250d4c3', 0, False] +2016-12-01 15:27:39,971 [MainThread ] [DEBUG] found good sig at index=1 +2016-12-01 15:27:39,971 [MainThread ] [DEBUG] nick = J5CFffuuewjG44UJ sent all sigs, removing from nonrespondant list +2016-12-01 15:27:39,971 [MainThread ] [DEBUG] rpc: gettxout ['03243f4a659e278a1333f8308f6aaf32db4692ee7df0340202750fd6c09150f6', 1, False] +2016-12-01 15:27:39,972 [MainThread ] [DEBUG] rpc: gettxout ['498faa8b22534f3b443c6b0ce202f31e12f21668b4f0c7a005146808f250d4c3', 0, False] +2016-12-01 15:27:39,973 [MainThread ] [DEBUG] found good sig at index=0 +2016-12-01 15:27:39,973 [MainThread ] [DEBUG] nick = J559UPUSLLjHJpaB sent all sigs, removing from nonrespondant list +2016-12-01 15:27:43,937 [MainThread ] [DEBUG] rpc: gettxout ['498faa8b22534f3b443c6b0ce202f31e12f21668b4f0c7a005146808f250d4c3', 0, False] +2016-12-01 15:27:43,938 [MainThread ] [DEBUG] found good sig at index=2 +2016-12-01 15:27:43,938 [MainThread ] [DEBUG] nick = J55z23xdjxJjC7er sent all sigs, removing from nonrespondant list +2016-12-01 15:27:43,938 [MainThread ] [DEBUG] all makers have sent their signatures +2016-12-01 15:27:43,938 [MainThread ] [DEBUG] INFO:Transaction is valid, signing.. +2016-12-01 15:27:43,943 [MainThread ] [DEBUG] +""" + +t_raw_signed_tx = "0100000004f65091c0d60f75020234f07dee9246db32af6a8f30f833138a279e654a3f2403010000006b483045022100ad522388ce9eacf2760e4d6bd6a114a0e15b88879b430fbb2e60df947494df2402201f49338726599eb0980873aef268d8d890de2792967ff28f0c11eb35e54ff07a012103a2d1cbe977b1feaf8d0d5cc28c686859563d1520b28018be0c2661cf1ebe4857ffffffff3c2abd571046e93a0471c6e47a061bcbfa982c392c1ddcd88ae006d720a83e3f010000006a473044022012bbfa6ef7b0416e00001d90b022d6663f5fd57d9a07bb70b887510f7c44902d022059b382bfb1ff5588a518fc69c55b2ff67d3facc11088e984d7c09c336d4875330121023bcbafb4f68455e0d1d117c178b0e82a84e66414f0987453d78da034b299c3a9ffffffffc3d450f208681405a0c7f0b46816f2121ef302e20c6b3c443b4f53228baa8f49000000006b48304502210081019ea7b68130da4230fd748668c776043004843de50e07bb5fcb42e7632aed022000a34878274e583eec64815d1b587e7fbcd9ac714e722773ac1e69f1209f2e10012102b4b749d54e96b04066b0803e372a43d6ffa16e75a001ae0ed4b235674ab286beffffffff755d83f964d24f33de8f193a10932400bafcb47b9919351af0bf81e3e5d68007010000006b483045022100add68e9532a50ca5585999290531f26e515bdf3d001519b0de8dd6b981daec7f02200b34c58ce61e6673c9efc5bf82cacd4d02673bc1c6cbaba45b5c65579776b8180121025a2e04dc6bd5f58fe4eb13045b27f0dd17c39524264639f48607347cf6d69c4effffffff0880778e06000000001976a914767c956efe6092a775fea39a06d1cac9aae956d788ac74ee5b05000000001976a914cab20e3270988ac99651b8f079a3b4c93b996a6888ac889c5d05000000001976a914b91f75254b5fa1510cc944a2206ed72235d0d88188ac80778e06000000001976a914a916707952c2df28a3abf3ee692dfbbd5a4d74dc88ac889c5d05000000001976a914e245b480b46bcbc9d13e68766ad19909decd135288ac80778e06000000001976a91416af241bb1db02dfd7c65989bbab190ac489ccc188ac889c5d05000000001976a9142994295a9d4d083eb792e669e3211007dc78928888ac80778e06000000001976a9145ece2dac945c8ff5b2b6635360ca0478ade305d488ac00000000" +t_txid = "4d5bfad9bbfb93eb1e25fb2e6c832323d1bf39e63f6ed2319b65e85354c7ca70" + +t_dummy_ext = {"used": [], "external": { + "79f1b8df7d0978f30028487c6c4e0eae96d1aa18e01f13bb4cba6788590cd431:1": { + "reveal": { + "1": { + "P2": "0329d4b4bb28c1a0747c1a5daad59763a9021b5e1fa957887a90c7849789a683b6", + "s": "a303cad939fb773dd16a81c44f210afe0b985a2cf9a63b033139455b70c77be6", + "e": "64f5b9861b95434ab84bd044b93a28f85ea94b474237992d899bd4302eef3820" + }, + "0": { + "P2": "02681ed66595daf98b12d6d69d8afb8d14a531eeaea1161bce8b9f2666ea55f157", + "s": "ed994ad173431bd0f53c82fee70d202e9c2adce492b6226d3cb4116cc3a08383", + "e": "1dd7f56fe83ca66e89b3ec3b73fa44edacab0ef4524652c415065dbf91500c85" + }, + "2": { + "P2": "02cdd5ced7e79bdb651d6d1883e0047509793a9a9e3da4ae516b8a853b9cdd8e98", + "s": "39a19287c4bacc823559d0e1b907e311c31d8a13f45fe30d10b133561113515c", + "e": "a0e7cd319c7e51c6f9e503e95d08c3d2398f9b546c2d64178b6c113c63c29d78" + } + }, + "P": "033749d513d0e0239a75892556a6ce01c3e48f82e75169129abe8ef370ab992c94" + }}} diff --git a/jmdaemon/test/test_daemon_protocol.py b/jmdaemon/test/test_daemon_protocol.py new file mode 100644 index 0000000..0a59b4e --- /dev/null +++ b/jmdaemon/test/test_daemon_protocol.py @@ -0,0 +1,318 @@ +#! /usr/bin/env python +from __future__ import absolute_import +'''test daemon-protocol interfacae.''' + +import pytest +from jmdaemon import (JMDaemonServerProtocolFactory, MessageChannelCollection) +from jmdaemon.orderbookwatch import OrderbookWatch +from jmdaemon.daemon_protocol import JMDaemonServerProtocol +from jmdaemon.protocol import (COMMAND_PREFIX, ORDER_KEYS, NICK_HASH_LENGTH, + NICK_MAX_ENCODED, JM_VERSION, JOINMARKET_NICK_HEADER) +from jmclient import (load_program_config, get_log, jm_single, get_irc_mchannels, + JMTakerClientProtocolFactory, Taker, AbstractWallet) +import os +from twisted.python.log import startLogging, err +from twisted.python.log import msg as tmsg +from twisted.internet import protocol, reactor, task +from twisted.internet.protocol import ServerFactory, ClientCreator +from twisted.internet.error import (ConnectionLost, ConnectionAborted, + ConnectionClosed, ConnectionDone) +from twisted.protocols.amp import UnknownRemoteError +from twisted.python import failure +from twisted.protocols import amp +from twisted.trial import unittest +from jmbase.commands import * +from msgdata import * +import json +import time +import base64 +from dummy_mc import DummyMessageChannel +test_completed = False +end_early = False +jlog = get_log() + +class JMProtocolError(Exception): + pass + +class JMBaseProtocol(amp.AMP): + def checkClientResponse(self, response): + """A generic check of client acceptance; any failure + is considered criticial. + """ + if 'accepted' not in response or not response['accepted']: + reactor.stop() + + def defaultErrback(self, failure): + failure.trap(ConnectionAborted, ConnectionClosed, ConnectionDone, + ConnectionLost, UnknownRemoteError) + reactor.stop() + + def defaultCallbacks(self, d): + d.addCallback(self.checkClientResponse) + d.addErrback(self.defaultErrback) + +class JMTestClientProtocol(JMBaseProtocol): + + def connectionMade(self): + self.clientStart() + + def clientStart(self): + self.sigs_received = 0 + irc = get_irc_mchannels() + d = self.callRemote(JMInit, + bcsource="dummyblockchain", + network="dummynetwork", + irc_configs=json.dumps(irc), + minmakers=2, + maker_timeout_sec=3) + self.defaultCallbacks(d) + + @JMInitProto.responder + def on_JM_INIT_PROTO(self, nick_hash_length, nick_max_encoded, + joinmarket_nick_header, joinmarket_version): + show_receipt("JMINITPROTO", nick_hash_length, nick_max_encoded, + joinmarket_nick_header, joinmarket_version) + d = self.callRemote(JMStartMC, + nick="dummynick") + self.defaultCallbacks(d) + return {'accepted': True} + + @JMUp.responder + def on_JM_UP(self): + show_receipt("JMUP") + d = self.callRemote(JMSetup, + role="TAKER", + n_counterparties=4) #TODO this number should be set + self.defaultCallbacks(d) + return {'accepted': True} + + @JMSetupDone.responder + def on_JM_SETUP_DONE(self): + show_receipt("JMSETUPDONE") + d = self.callRemote(JMRequestOffers) + self.defaultCallbacks(d) + return {'accepted': True} + + @JMFillResponse.responder + def on_JM_FILL_RESPONSE(self, success, ioauth_data): + show_receipt("JMFILLRESPONSE", success, ioauth_data) + reactor.callLater(1, self.maketx, ioauth_data) + return {'accepted': True} + + def maketx(self, ioauth_data): + ioauth_data = json.loads(ioauth_data) + nl = ioauth_data.keys() + d = self.callRemote(JMMakeTx, + nick_list= json.dumps(nl), + txhex="deadbeef") + self.defaultCallbacks(d) + + @JMOffers.responder + def on_JM_OFFERS(self, orderbook): + if end_early: + return {'accepted': True} + jlog.debug("JMOFFERS" + str(orderbook)) + #Trigger receipt of verified privmsgs, including unverified + nick = str(t_chosen_orders.keys()[0]) + b64tx = base64.b64encode("deadbeef") + d1 = self.callRemote(JMMsgSignatureVerify, + verif_result=True, + nick=nick, + fullmsg="!push " + b64tx + " abc def", + hostid="dummy") + self.defaultCallbacks(d1) + #unverified + d2 = self.callRemote(JMMsgSignatureVerify, + verif_result=False, + nick=nick, + fullmsg="!push " + b64tx + " abc def", + hostid="dummy") + self.defaultCallbacks(d2) + d = self.callRemote(JMFill, + amount=100, + commitment="dummycommitment", + revelation="dummyrevelation", + filled_offers=json.dumps(t_chosen_orders)) + self.defaultCallbacks(d) + return {'accepted': True} + + @JMSigReceived.responder + def on_JM_SIG_RECEIVED(self, nick, sig): + show_receipt("JMSIGRECEIVED", nick, sig) + self.sigs_received += 1 + if self.sigs_received == 3: + #end of test + reactor.callLater(1, end_test) + return {'accepted': True} + + @JMRequestMsgSig.responder + def on_JM_REQUEST_MSGSIG(self, nick, cmd, msg, msg_to_be_signed, hostid): + show_receipt("JMREQUESTMSGSIG", nick, cmd, msg, msg_to_be_signed, hostid) + d = self.callRemote(JMMsgSignature, + nick=nick, + cmd=cmd, + msg_to_return="xxxcreatedsigxx", + hostid=hostid) + self.defaultCallbacks(d) + return {'accepted': True} + + @JMRequestMsgSigVerify.responder + def on_JM_REQUEST_MSGSIG_VERIFY(self, msg, fullmsg, sig, pubkey, nick, + hashlen, max_encoded, hostid): + show_receipt("JMREQUESTMSGSIGVERIFY", msg, fullmsg, sig, pubkey, + nick, hashlen, max_encoded, hostid) + d = self.callRemote(JMMsgSignatureVerify, + verif_result=True, + nick=nick, + fullmsg=fullmsg, + hostid=hostid) + self.defaultCallbacks(d) + return {'accepted': True} + +class JMTestClientProtocolFactory(protocol.ClientFactory): + protocol = JMTestClientProtocol + + +def show_receipt(name, *args): + tmsg("Received msgtype: " + name + ", args: " + ",".join([str(x) for x in args])) + +def end_test(): + global test_completed + test_completed = True + + + +class JMDaemonTestServerProtocol(JMDaemonServerProtocol): + + def __init__(self, factory): + super(JMDaemonTestServerProtocol, self).__init__(factory) + #respondtoioauths should do nothing unless jmstate = 2 + self.respondToIoauths(True) + #calling on_JM_MAKE_TX should also do nothing in wrong state + assert super(JMDaemonTestServerProtocol, self).on_JM_MAKE_TX( + 1, 2) == {'accepted': False} + #calling on_JM_FILL with negative amount should reject + assert super(JMDaemonTestServerProtocol, self).on_JM_FILL( + -1000, 2, 3, 4) == {'accepted': False} + #checkutxos also does nothing for rejection at the moment + self.checkUtxosAccepted(False) + #None should be returned requesting a cryptobox for an unknown cp + assert self.get_crypto_box_from_nick("notrealcp") == None + #does nothing yet + self.on_error() + + @JMRequestOffers.responder + def on_JM_REQUEST_OFFERS(self): + for o in t_orderbook: + #counterparty, oid, ordertype, minsize, maxsize,txfee, cjfee): + self.on_order_seen(o["counterparty"], o["oid"], o["ordertype"], + o["minsize"], o["maxsize"], + o["txfee"], o["cjfee"]) + return super(JMDaemonTestServerProtocol, self).on_JM_REQUEST_OFFERS() + + @JMInit.responder + def on_JM_INIT(self, bcsource, network, irc_configs, minmakers, + maker_timeout_sec): + self.maker_timeout_sec = int(maker_timeout_sec) + self.minmakers = int(minmakers) + mcs = [DummyMessageChannel(None)] + self.mcc = MessageChannelCollection(mcs) + #The following is a hack to get the counterparties marked seen/active; + #note it must happen before callign set_msgchan for OrderbookWatch + self.mcc.on_order_seen = None + for c in [o['counterparty'] for o in t_orderbook]: + self.mcc.on_order_seen_trigger(mcs[0], c, "a", "b", "c", "d", "e", "f") + OrderbookWatch.set_msgchan(self, self.mcc) + #register taker-specific msgchan callbacks here + self.mcc.register_taker_callbacks(self.on_error, self.on_pubkey, + self.on_ioauth, self.on_sig) + self.mcc.set_daemon(self) + self.restart_mc_required = True + d = self.callRemote(JMInitProto, + nick_hash_length=NICK_HASH_LENGTH, + nick_max_encoded=NICK_MAX_ENCODED, + joinmarket_nick_header=JOINMARKET_NICK_HEADER, + joinmarket_version=JM_VERSION) + self.defaultCallbacks(d) + return {'accepted': True} + + @JMFill.responder + def on_JM_FILL(self, amount, commitment, revelation, filled_offers): + tmpfo = json.loads(filled_offers) + dummypub = "073732a7ca60470f709f23c602b2b8a6b1ba62ee8f3f83a61e5484ab5cbf9c3d" + #trigger invalid on_pubkey conditions + reactor.callLater(1, self.on_pubkey, "notrealcp", dummypub) + reactor.callLater(2, self.on_pubkey, tmpfo.keys()[0], dummypub + "deadbeef") + #trigger invalid on_ioauth condition + reactor.callLater(2, self.on_ioauth, "notrealcp", 1, 2, 3, 4, 5) + #trigger msg sig verify request operation for a dummy message + #currently a pass-through + reactor.callLater(1, self.request_signature_verify, "1", + "!push abcd abc def", "3", "4", + str(tmpfo.keys()[0]), 6, 7, self.mcc.mchannels[0].hostid) + #send "valid" onpubkey, onioauth messages + for k, v in tmpfo.iteritems(): + reactor.callLater(1, self.on_pubkey, k, dummypub) + reactor.callLater(2, self.on_ioauth, k, ['a', 'b'], "auth_pub", + "cj_addr", "change_addr", "btc_sig") + return super(JMDaemonTestServerProtocol, self).on_JM_FILL(amount, + commitment, revelation, filled_offers) + + @JMMakeTx.responder + def on_JM_MAKE_TX(self, nick_list, txhex): + for n in nick_list: + reactor.callLater(1, self.on_sig, n, "dummytxsig") + return super(JMDaemonTestServerProtocol, self).on_JM_MAKE_TX(nick_list, + txhex) + + + +class JMDaemonTestServerProtocolFactory(ServerFactory): + protocol = JMDaemonTestServerProtocol + + def buildProtocol(self, addr): + return JMDaemonTestServerProtocol(self) + + +class TrialTestJMDaemonProto(unittest.TestCase): + + def setUp(self): + load_program_config() + jm_single().maker_timeout_sec = 1 + self.port = reactor.listenTCP(27184, JMDaemonTestServerProtocolFactory()) + self.addCleanup(self.port.stopListening) + clientconn = reactor.connectTCP("localhost", 27184, + JMTestClientProtocolFactory()) + self.addCleanup(clientconn.disconnect) + print("Got here") + + def test_waiter(self): + print("test_main()") + return task.deferLater(reactor, 12, self._called_by_deffered) + + def _called_by_deffered(self): + pass + + +class TestJMDaemonProtoInit(unittest.TestCase): + + def setUp(self): + global end_early + end_early = True + print("setUp()") + load_program_config() + jm_single().maker_timeout_sec = 1 + self.port = reactor.listenTCP(27184, JMDaemonServerProtocolFactory()) + self.addCleanup(self.port.stopListening) + clientconn = reactor.connectTCP("localhost", 27184, + JMTestClientProtocolFactory()) + self.addCleanup(clientconn.disconnect) + print("Got here") + + def test_waiter(self): + print("test_main()") + return task.deferLater(reactor, 5, self._called_by_deffered) + + def _called_by_deffered(self): + global end_early + end_early = False diff --git a/jmdaemon/test/test_enc_wrapper.py b/jmdaemon/test/test_enc_wrapper.py index bd3e940..58a1b98 100644 --- a/jmdaemon/test/test_enc_wrapper.py +++ b/jmdaemon/test/test_enc_wrapper.py @@ -5,7 +5,7 @@ import random import pytest from jmdaemon import (init_keypair, get_pubkey, init_pubkey, as_init_encryption, - NaclError) + NaclError, encrypt_encode, decode_decrypt) @pytest.mark.parametrize("ab_message,ba_message,num_iterations", @@ -43,6 +43,7 @@ def test_enc_wrapper(alice_bob_boxes, ab_message, ba_message, num_iterations): alice_ptext = alice_box.decrypt(otw_bmsg) assert alice_ptext == ba_message, "Encryption test: FAILED. Bob sent: %s, Alice received: " % ( ba_message, alice_ptext) + assert decode_decrypt(encrypt_encode(ab_message, bob_box), bob_box) == ab_message @pytest.mark.parametrize("invalid_pubkey", [ @@ -68,8 +69,8 @@ def test_invalid_nacl_keys(alice_bob_boxes, invalid_pubkey): @pytest.fixture() def alice_bob_boxes(): - alice_kp = init_keypair() - bob_kp = init_keypair() + alice_kp = init_keypair("alicekey") + bob_kp = init_keypair("bobkey") # this is the DH key exchange part bob_otwpk = get_pubkey(bob_kp, True) @@ -77,7 +78,7 @@ def alice_bob_boxes(): bob_pk = init_pubkey(bob_otwpk) alice_box = as_init_encryption(alice_kp, bob_pk) - alice_pk = init_pubkey(alice_otwpk) + alice_pk = init_pubkey(alice_otwpk, "alicepubkey") bob_box = as_init_encryption(bob_kp, alice_pk) # now Alice and Bob can use their 'box' diff --git a/jmdaemon/test/test_irc_messaging.py b/jmdaemon/test/test_irc_messaging.py new file mode 100644 index 0000000..6c498a8 --- /dev/null +++ b/jmdaemon/test/test_irc_messaging.py @@ -0,0 +1,154 @@ +#! /usr/bin/env python +from __future__ import absolute_import +'''Tests of joinmarket bots end-to-end (including IRC and bitcoin) ''' + +import subprocess +import signal +import os +import pytest +import time +import threading +import hashlib +import jmbitcoin as btc +from jmdaemon import (JOINMARKET_NICK_HEADER, NICK_HASH_LENGTH, + NICK_MAX_ENCODED, IRCMessageChannel) +from jmdaemon.message_channel import CJPeerError +import jmdaemon +#needed for test framework +from jmclient import (load_program_config, get_irc_mchannels, jm_single) + +python_cmd = "python2" +yg_cmd = "yield-generator-basic.py" +yg_name = "ygtest" +si = 3 +class DummyDaemon(object): + def request_signature_verify(self, a, b, c, d, e, + f, g, h): + return True + +class DummyMC(IRCMessageChannel): + def __init__(self, configdata, nick, daemon): + super(DummyMC, self).__init__(configdata, daemon=daemon) + """ + #hacked in here to allow auth without mc-collection + nick_priv = hashlib.sha256(os.urandom(16)).hexdigest() + '01' + nick_pubkey = btc.privtopub(nick_priv) + nick_pkh_raw = hashlib.sha256(nick_pubkey).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_single().JM_VERSION) + nick_pkh + jm_single().nickname = nick + """ + self.daemon = daemon + self.set_nick(nick) + +def on_connect(x): + print('simulated on-connect') +def on_welcome(x): + print('simulated on-welcome') +def on_disconnect(x): + print('simulated on-disconnect') + +def on_order_seen(dummy, counterparty, oid, ordertype, minsize, + maxsize, txfee, cjfee): + global yg_name + yg_name = counterparty + +def on_pubkey(pubkey): + print "received pubkey: " + pubkey + +class RawIRCThread(threading.Thread): + + def __init__(self, ircmsgchan): + threading.Thread.__init__(self, name='RawIRCThread') + self.daemon = True + self.ircmsgchan = ircmsgchan + + def run(self): + self.ircmsgchan.run() + +def test_junk_messages(setup_messaging): + #start a yg bot just to receive messages + """ + wallets = make_wallets(1, + wallet_structures=[[1,0,0,0,0]], + mean_amt=1) + wallet = wallets[0]['wallet'] + ygp = local_command([python_cmd, yg_cmd,\ + str(wallets[0]['seed'])], bg=True) + """ + #time.sleep(90) + #start a raw IRCMessageChannel instance in a thread; + #then call send_* on it with various errant messages + dm = DummyDaemon() + mc = DummyMC(get_irc_mchannels()[0], "irc_ping_test", dm) + mc.register_orderbookwatch_callbacks(on_order_seen=on_order_seen) + mc.register_taker_callbacks(on_pubkey=on_pubkey) + mc.on_connect = on_connect + mc.on_disconnect = on_disconnect + mc.on_welcome = on_welcome + RawIRCThread(mc).start() + #start up a fake counterparty + mc2 = DummyMC(get_irc_mchannels()[0], yg_name, dm) + RawIRCThread(mc2).start() + time.sleep(si) + mc.request_orderbook() + time.sleep(si) + #now try directly + mc.pubmsg("!orderbook") + time.sleep(si) + #should be ignored; can we check? + mc.pubmsg("!orderbook!orderbook") + time.sleep(si) + #assuming MAX_PRIVMSG_LEN is not something crazy + #big like 550, this should fail + with pytest.raises(AssertionError) as e_info: + mc.pubmsg("junk and crap"*40) + time.sleep(si) + #assuming MAX_PRIVMSG_LEN is not something crazy + #small like 180, this should succeed + mc.pubmsg("junk and crap"*15) + time.sleep(si) + #try a long order announcement in public + #because we don't want to build a real orderbook, + #call the underlying IRC announce function. + #TODO: how to test that the sent format was correct? + mc._announce_orders(["!abc def gh 0001"]*30) + time.sleep(si) + #send a fill with an invalid pubkey to the existing yg; + #this should trigger a NaclError but should NOT kill it. + mc._privmsg(yg_name, "fill", "0 10000000 abcdef") + #Test that null privmsg does not cause crash; TODO check maker log? + mc.send_raw("PRIVMSG " + yg_name + " :") + time.sleep(si) + #Try with ob flag + mc._pubmsg("!reloffer stuff") + time.sleep(si) + #Trigger throttling with large messages + mc._privmsg(yg_name, "tx", "aa"*5000) + time.sleep(si) + #with pytest.raises(CJPeerError) as e_info: + mc.send_error(yg_name, "fly you fools!") + time.sleep(si) + #Test the effect of shutting down the connection + mc.set_reconnect_interval(si-1) + mc.close() + mc._announce_orders(["!abc def gh 0001"]*30) + time.sleep(si+2) + #kill the connection at socket level + mc.shutdown() + +@pytest.fixture(scope="module") +def setup_messaging(): + #Trigger PING LAG sending artificially + jmdaemon.irc.PING_INTERVAL = 3 + load_program_config() + + + + diff --git a/jmdaemon/test/test_message_channel.py b/jmdaemon/test/test_message_channel.py new file mode 100644 index 0000000..b86333a --- /dev/null +++ b/jmdaemon/test/test_message_channel.py @@ -0,0 +1,374 @@ +#! /usr/bin/env python +from __future__ import absolute_import +'''test messagechannel management code.''' + +import pytest +from jmdaemon import (JMDaemonServerProtocolFactory, MessageChannelCollection) +from jmdaemon.message_channel import MChannelThread +from jmdaemon.orderbookwatch import OrderbookWatch +from jmdaemon.daemon_protocol import JMDaemonServerProtocol +from jmdaemon.protocol import (COMMAND_PREFIX, ORDER_KEYS, NICK_HASH_LENGTH, + NICK_MAX_ENCODED, JM_VERSION, JOINMARKET_NICK_HEADER) +from jmclient import (load_program_config, get_log, jm_single, get_irc_mchannels, + JMTakerClientProtocolFactory, Taker, AbstractWallet) +import os +from jmbase.commands import * +from msgdata import * +import json +import time +import hashlib +import base64 +import traceback +import threading +import jmbitcoin as bitcoin +from dummy_mc import DummyMessageChannel + +jlog = get_log() + +def make_valid_nick(i=0): + nick_priv = hashlib.sha256(chr(i)*16).hexdigest() + '01' + nick_pubkey = bitcoin.privtopub(nick_priv) + nick_pkh_raw = hashlib.sha256(nick_pubkey).digest()[:NICK_HASH_LENGTH] + nick_pkh = bitcoin.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 + return JOINMARKET_NICK_HEADER + str(JM_VERSION) + nick_pkh + +class DummyBox(object): + def encrypt(self, msg): + return msg + def decrypt(self, msg): + return msg + +class DaemonForSigns(object): + """The following functions handle requests and responses + from client for messaging signing and verifying. + """ + def __init__(self, mcc): + self.siglock = threading.Lock() + self.mcc = mcc + self.crypto_boxes = {} + + def request_signed_message(self, nick, cmd, msg, msg_to_be_signed, hostid): + with self.siglock: + #Here we have to pretend we signed it and + #send it to privmsg + self.mcc.privmsg(nick, cmd, msg, mc=hostid) + + def request_signature_verify(self, msg, fullmsg, sig, pubkey, nick, hashlen, + max_encoded, hostid): + with self.siglock: + #Here we must pretend we verified it and send it to on_verified_privmsg + self.mcc.on_verified_privmsg(nick, fullmsg, hostid) + + def get_crypto_box_from_nick(self, nick): + if nick in self.crypto_boxes and self.crypto_boxes[nick] != None: + return self.crypto_boxes[nick][1] # libsodium encryption object + else: + jlog.debug('something wrong, no crypto object, nick=' + nick + + ', message will be dropped') + return None + +def dummy_on_welcome(): + jlog.debug("On welcome called") + +def don_error(): + jlog.debug("called: " + traceback.extract_stack(None, 2)[0][2]) + +def don_ioauth(nick, utxo_list, auth_pub, cj_addr, + change_addr, btc_sig): + jlog.debug("onioauth callback") + jlog.debug("Args are: " + ",".join([str(x) for x in nick, + utxo_list, auth_pub, cj_addr, + change_addr, btc_sig])) + +def don_sig(nick, sig): + jlog.debug("calledback on-sig") + +don_pubkey = don_sig + + +def don_orderbook_requested(nick, mc): + jlog.debug("called oobr") + +def don_commitment_seen(nick, cmt): + jlog.debug("called doncommitmentseen") + jlog.debug("Nick, cmt was: " + str(nick) + " , " + str(cmt)) + +def don_seen_auth(nick, cr): + jlog.debug("called donseen auth") + jlog.debug("Cr was: " + str(cr)) + +def don_push_tx(nick, txhex): + jlog.debug("called donpushtx with thex: " + str(txhex)) + +def don_seen_tx(nick, txhex): + jlog.debug("called donseentx with txhex: " + str(txhex)) + +def don_commitment_transferred(nick, cmt): + jlog.debug("called doncommitmenttransferred") + +def don_order_fill(nick, oid, amount, taker_pk, commit): + jlog.debug("donorderfill called with: " + ",".join( + [str(x) for x in [nick, oid, amount, taker_pk, commit]])) + +def test_setup_mc(): + ob = OrderbookWatch() + ob.on_welcome = dummy_on_welcome + dmcs = [DummyMessageChannel(None, hostid="hostid"+str(x)) for x in range(3)] + mcc = MessageChannelCollection(dmcs) + #this sets orderbookwatch callbacks + ob.set_msgchan(mcc) + #we want to set all the callbacks, maker and taker + mcc.register_taker_callbacks(don_error, don_pubkey, don_ioauth, don_sig) + mcc.register_maker_callbacks(on_orderbook_requested=don_orderbook_requested, + on_order_fill=don_order_fill, + on_seen_auth=don_seen_auth, on_seen_tx=don_seen_tx, + on_push_tx=don_push_tx, + on_commitment_seen=don_commitment_seen, + on_commitment_transferred=don_commitment_transferred) + mcc.set_nick("testnick") + dummydaemon = DaemonForSigns(mcc) + mcc.set_daemon(dummydaemon) + for mc in dmcs: + mc.on_welcome(mc) + #instead of calling mcc.run, we'll start threads for mcs manually so we + #can probe them + for mc in dmcs: + MChannelThread(mc).start() + for m in dmcs: + m.on_pubmsg("testmaker", "!orderbook") + #receive invalid pubmsgs + for msg in ["!orderbook!orderbook", "!notacommand a b c", "no command prefix", + "!reloffer 0 4000 5000 100"]: + dmcs[2].on_pubmsg("testmaker", msg) + + mcc.request_orderbook() + mcc.pubmsg("outward pubmsg") + #now create a verifiable counterparty nick; + #to get it into active state, need to receive an orderbook from it + cp1 = make_valid_nick() + #Simulate order receipt on 2 of 3 msgchans from this nick; + #note that it will have its active chan set to mc "1" because that + #is the last it was seen on: + dmcs[0].on_privmsg(cp1, "!reloffer 0 4000 5000 100 0.2 abc def") + dmcs[1].on_privmsg(cp1, "!reloffer 0 4000 5000 100 0.2 abc def") + time.sleep(0.5) + #send back a response + mcc.privmsg(cp1, "fill", "0") + #trigger failure to find nick in privmsg + mcc.privmsg(cp1+"XXX", "fill", "0") + #trigger check_privmsg decorator + mcc.send_sigs(cp1, ["abc", "def"]) + mcc.send_pubkey(cp1, "testpubkey") + mcc.send_ioauth(cp1, ["abc", "def"], "testpub", "testaddr1", "testaddr2", + "testsig") + mcc.send_error(cp1, "errormsg") + mcc.push_tx(cp1, "deadbeef") + #kill the chan on which the cp is marked active; + #note dummychannel has no actual shutdown (call it anyway), + #so change its status manually. + dmcs[2].shutdown() + mcc.mc_status[dmcs[1]] = 2 + time.sleep(0.5) + #Flush removes references to inactive channels (in this case dmcs[1]). + #Dynamic switching of cp1 should occur to the other seen channel (dmcs[0]). + mcc.flush_nicks() + #force cp1 to be unseen on mc 0: + mcc.unsee_nick(cp1, dmcs[0]) + del mcc.active_channels[cp1] + #try sending a privmsg again; this time it should just print a warning, + #as cp1 is not seen anywhere + mcc.send_sigs(cp1, ["abc", "def"]) + #simulate order cancels (even though we have none) + mcc.cancel_orders([0,1,2]) + #let cp1 be seen on mc2 without having got into active channels; + #note that this is an illegal pubmsg and is ignored for everything *except* + #nick_seen (what we need here) + dmcs[2].on_pubmsg(cp1, "random") + mcc.send_sigs(cp1, ["abc", "def"]) + #Try using the proper way of setting up privsmgs + #first try without box + mcc.prepare_privmsg(cp1, "auth", "a b c") + dummydaemon.crypto_boxes[cp1] = ["a", DummyBox()] + #now conditions are correct, should succeed: + mcc.prepare_privmsg(cp1, "auth", "a b c") + #try again but this time there is no active channel + del mcc.active_channels[cp1] + mcc.prepare_privmsg(cp1, "auth", "a b c") + #try announcing orders; first public + mcc.announce_orders(t_orderbook) + #try on fake mc + mcc.announce_orders(t_orderbook, new_mc="fakemc") + #direct to one cp + mcc.announce_orders(t_orderbook, nick=cp1) + #direct to one cp on one mc + mcc.announce_orders(t_orderbook, nick=cp1, new_mc=dmcs[0]) + + #Next, set up 6 counterparties and fill their offers, + #send txs to them + cps = [make_valid_nick(i) for i in range(1, 7)] + #reuse t_chosen_orders data, but swap out the counterparty names + offervals = t_chosen_orders.values() + new_offers = dict(zip(cps, offervals)) + #first, pretend they all showed up on all 3 mcs: + for m in dmcs: + for cp in cps: + m.on_privmsg(cp, "!reloffer 0 400000 500000 100 0.002 abc def") + #next, call main fill function + mcc.fill_orders(new_offers, 1000, "dummypubkey", "dummycommit") + #now send a dummy transaction to this same set. + #first fails with no crypto box. + mcc.send_tx(cps, "deadbeef") + #Now initialize the boxes + for c in cps: + dummydaemon.crypto_boxes[c] = ["a", DummyBox()] + mcc.send_tx(cps, "deadbeef") + #try to send the transaction to a wrong cp: + mcc.send_tx(["notrealcp"], "deadbeef") + + #At this stage, dmcs0,2 should be "up" and 1 should be "down" + assert mcc.mc_status[dmcs[0]] == 1 + assert mcc.mc_status[dmcs[1]] == 2 + assert mcc.mc_status[dmcs[2]] == 1 + #simulate re-connection of dmcs[1] ; note that this code isn't used atm + mcc.on_connect_trigger(dmcs[1]) + assert mcc.mc_status[dmcs[1]] == 1 + #Now trigger disconnection code; each mc one by one; the last should trigger + #on_disconnect callback + for m in dmcs: + mcc.on_disconnect_trigger(m) + #reconnect; effect is all nick references are flushed + for m in dmcs: + mcc.on_connect_trigger(m) + assert mcc.active_channels == {} + #have the cps rearrive + for m in dmcs: + for cp in cps: + m.on_privmsg(cp, "!reloffer 0 4000 5000 100 0.2 abc def") + + ##################################################################### + #next series of messages are to test various normal and abnormal + #message receipts under normal connection conditions + ##################################################################### + + #simulate receipt of commitments + #valid + dmcs[0].on_pubmsg(cps[2], "!hp2 deadbeef") + #invalid missing field + dmcs[0].on_pubmsg(cps[2], "!hp2") + #receive commitment via privmsg to trigger commitment_transferred + dmcs[0].on_privmsg(cps[2], "!hp2 deadbeef abc def") + #simulate receipt of order cancellation + #valid + dmcs[0].on_pubmsg(cps[2], "!cancel 2") + #invalid oid + dmcs[0].on_pubmsg(cps[2], "!cancel x") + #too short privmsg (can't even have a signature) + dmcs[0].on_privmsg(cps[2], COMMAND_PREFIX) + #not using correct protocol start character + dmcs[0].on_privmsg(cps[2], "A B C") + #unrecognized command + dmcs[0].on_privmsg(cps[2], "!fakecommand A B C D") + #Perhaps dubious, but currently msg after command must be non-zero + dmcs[0].on_privmsg(cps[2], "!reloffer sig1 sig2") + #Simulating receipt of encrypted messages: + #ioauth + dummy_on_ioauth_msg = "deadbeef:0,deadbeef:1 XauthpubX XcjaddrX XchangeaddrX XbtcsigX" + b64dummyioauth = base64.b64encode(dummy_on_ioauth_msg) + dmcs[0].on_privmsg(cps[3], "!ioauth " + b64dummyioauth + " sig1 sig2") + #Try with a garbage b64 (but decodable); should throw index error at least + dmcs[0].on_privmsg(cps[3], "!ioauth _*_ sig1 sig2") + #Try also for receipt from an unknown counterparty; should fail with no enc box + dmcs[0].on_privmsg("notrealcp", "!ioauth " + b64dummyioauth + " sig1 sig2") + #Try same message from valid cp but with corrupted b64 + b64dummyioauth = "999" + dmcs[0].on_privmsg(cps[3], "!ioauth " + b64dummyioauth + " sig1 sig2") + #sig + dummy_on_sig_msg = "dummysig" + b64dummysig = base64.b64encode(dummy_on_sig_msg) + dmcs[0].on_privmsg(cps[3], "!sig " + b64dummysig + " sig1 sig2") + #auth + dummy_auth_msg = "dummyauth" + b64dummyauth = base64.b64encode(dummy_auth_msg) + dmcs[0].on_privmsg(cps[2], "!auth " + b64dummyauth + " sig1 sig2") + #invalid auth (only no message is invalid) + dmcs[0].on_privmsg(cps[3], "!auth " +base64.b64encode("") + " sig1 sig2") + #tx + #valid + dummy_tx = "deadbeefdeadbeef" + b64dummytx = base64.b64encode(dummy_tx) + b642dummytx = base64.b64encode(b64dummytx) + dmcs[0].on_privmsg(cps[2], "!tx " + b642dummytx + " sig1 sig2") + badbase64tx = "999" + badbase64tx2 = base64.b64encode(badbase64tx) + #invalid txhex; here the first round will work (msg decryption), second shouldn't + dmcs[0].on_privmsg(cps[2], "!tx " + badbase64tx2 + " sig1 sig2") + #push + #valid + dmcs[0].on_privmsg(cps[2], "!push " + b642dummytx + " sig1 sig2") + #invalid + dmcs[0].on_privmsg(cps[2], "!push 999 sig1 sig2") + #fill + #valid, no commit + dmcs[0].on_privmsg(cps[4], "!fill 0 4000 dummypub sig1 sig2") + #valid with commit + dmcs[0].on_privmsg(cps[4], "!fill 0 4000 dummypub dummycommit sig1 sig2") + #invalid length + dmcs[0].on_privmsg(cps[4], "!fill 0 sig1 sig2") + #pubkey + dmcs[0].on_privmsg(cps[4], "!pubkey dummypub sig1 sig2") + ############################################################## + #End message receipts + ############################################################## + + #simulate loss of conncetion to cp[0] + for m in dmcs[::-1]: + mcc.on_nick_leave_trigger(cps[0], m) + #call onnickleave for something not in the ac list + mcc.on_nick_leave_trigger("notrealcp", dmcs[0]) + #make mcs 0,1 go down so that when cp[1] tries to dynamic switch, it fails + mcc.on_disconnect_trigger(dmcs[0]) + mcc.on_disconnect_trigger(dmcs[1]) + mcc.on_nick_leave_trigger(cps[1], dmcs[2]) + mcc.shutdown() + +@pytest.mark.parametrize( + "failuretype, mcindex, wait", + [("shutdown", 0, 1), + ("break", 1, 1), + ("bad", 1, 1), + ]) +def test_mc_run(failuretype, mcindex, wait): + ob = OrderbookWatch() + ob.on_welcome = dummy_on_welcome + dmcs = [DummyMessageChannel(None, hostid="hostid"+str(x)) for x in range(3)] + mcc = MessageChannelCollection(dmcs) + #this sets orderbookwatch callbacks + ob.set_msgchan(mcc) + dummydaemon = DaemonForSigns(mcc) + mcc.set_daemon(dummydaemon) + #to externally trigger give up condition, start mcc itself in a thread + MChannelThread(mcc).start() + time.sleep(0.2) + mcc.give_up = True + time.sleep(1.2) + #wipe state, this time use failure injections + mcc = MessageChannelCollection(dmcs) + #to test exception raise on bad failure inject, don't use thread: + if failuretype == "bad": + with pytest.raises(NotImplementedError) as e_info: + mcc.run(failures=[failuretype, mcindex, wait]) + else: + #need to override thread run() + class FIThread(MChannelThread): + def run(self): + self.mc.run(failures=self.failures) + fi = FIThread(mcc) + fi.failures = [failuretype, mcindex, wait] + fi.start() + time.sleep(wait+0.5) + + \ No newline at end of file diff --git a/jmdaemon/test/test_orderbookwatch.py b/jmdaemon/test/test_orderbookwatch.py new file mode 100644 index 0000000..4caba2f --- /dev/null +++ b/jmdaemon/test/test_orderbookwatch.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python +from __future__ import print_function + +import pytest + +from jmdaemon.orderbookwatch import OrderbookWatch +from jmdaemon import IRCMessageChannel +from jmclient import get_irc_mchannels, load_program_config +from jmdaemon.protocol import JM_VERSION, ORDER_KEYS +class DummyDaemon(object): + def request_signature_verify(self, a, b, c, d, e, + f, g, h): + return True + +class DummyMC(IRCMessageChannel): + def __init__(self, configdata, nick, daemon): + super(DummyMC, self).__init__(configdata, daemon=daemon) + self.daemon = daemon + self.set_nick(nick) + +def on_welcome(x): + print("Simulated on-welcome") + +def get_ob(): + load_program_config() + dm = DummyDaemon() + mc = DummyMC(get_irc_mchannels()[0], "test", dm) + ob = OrderbookWatch() + ob.on_welcome = on_welcome + ob.set_msgchan(mc) + return ob + +@pytest.mark.parametrize( + "badtopic", + [("abc|"), + ("abcd|def"), + ("abc| 0 a qvd"), + ]) +def test_ob(badtopic): + ob = get_ob() + topic = ("JoinMarket open outcry pit. /r/joinmarket Discussion in #joinmarket" + "| 0 5 LATEST RELEASE v0.2.2. Useful new features. Update ASAP, and " + "do not use pre-0.2.0! https://bitcointalk.org/index.php?topic=91911" + "6.msg16714124#msg16714124") + ob.on_set_topic(topic) + #should not throw: + ob.on_set_topic(badtopic) + #test old version + future_ver = str(JM_VERSION + 2) + + deprecated = topic.replace("| 0 5", "| 0 "+future_ver) + ob.on_set_topic(deprecated) + +@pytest.mark.parametrize( + "counterparty, oid, ordertype, minsize, maxsize, txfee, cjfee, expected", + [ + #good absoffer + ("test", "0", "absoffer", "3000", "4000", "2", "300", True), + #good reloffer + ("test", "0", "reloffer", "3000", "4000", "2", "0.3", True), + #dusty minsize OK + ("test", "0", "reloffer", "1000", "4000", "2", "0.3", True), + #invalid oid + ("test", "-2", "reloffer", "3000", "4000", "2", "0.3", False), + #invalid minsize + ("test", "2", "reloffer", "-3000", "4000", "2", "0.3", False), + #invalid maxsize + ("test", "2", "reloffer", "3000", "2200000000000000", "2", "0.3", False), + #invalid txfee + ("test", "2", "reloffer", "3000", "4000", "-1", "0.3", False), + #min bigger than max + ("test", "2", "reloffer", "4000", "3000", "2", "0.3", False), + #non-integer absoffer + ("test", "2", "absoffer", "3000", "4000", "2", "0.3", False), + #invalid syntax for cjfee + ("test", "2", "reloffer", "3000", "4000", "2", "0.-1", False), + #invalid type for oid + ("test", "xxx", "reloffer", "3000", "4000", "2", "0.3", False), + ]) +def test_order_seen_cancel(counterparty, oid, ordertype, minsize, maxsize, txfee, + cjfee, expected): + ob = get_ob() + ob.on_order_seen(counterparty, oid, ordertype, minsize, maxsize, + txfee, cjfee) + if expected: + #offer should now be in the orderbook + rows = ob.db.execute('SELECT * FROM orderbook;').fetchall() + orderbook = [dict([(k, o[k]) for k in ORDER_KEYS]) for o in rows] + assert len(orderbook) == 1 + #test it can be removed + ob.on_order_cancel(counterparty, oid) + rows = ob.db.execute('SELECT * FROM orderbook;').fetchall() + orderbook = [dict([(k, o[k]) for k in ORDER_KEYS]) for o in rows] + assert len(orderbook) == 0 + +def test_disconnect_leave(): + ob = get_ob() + t_orderbook = [{u'counterparty': u'J5FA1Gj7Ln4vSGne', u'ordertype': u'reloffer', u'oid': 0, + u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, + {u'counterparty': u'J5CFffuuewjG44UJ', u'ordertype': u'reloffer', u'oid': 0, + u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, + {u'counterparty': u'J55z23xdjxJjC7er', u'ordertype': u'reloffer', u'oid': 0, + u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, + {u'counterparty': u'J54Ghp5PXCdY9H3t', u'ordertype': u'reloffer', u'oid': 0, + u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, + {u'counterparty': u'J559UPUSLLjHJpaB', u'ordertype': u'reloffer', u'oid': 0, + u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, + {u'counterparty': u'J5cBx1FwUVh9zzoO', u'ordertype': u'reloffer', u'oid': 0, + u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}] + for o in t_orderbook: + ob.on_order_seen(o['counterparty'], o['oid'], o['ordertype'], + o['minsize'], o['maxsize'], o['txfee'], o['cjfee']) + rows = ob.db.execute('SELECT * FROM orderbook;').fetchall() + orderbook = [dict([(k, o[k]) for k in ORDER_KEYS]) for o in rows] + assert len(orderbook) == 6 + #simulate one cp leaves: + ob.on_nick_leave("J5cBx1FwUVh9zzoO") + rows = ob.db.execute('SELECT * FROM orderbook;').fetchall() + orderbook = [dict([(k, o[k]) for k in ORDER_KEYS]) for o in rows] + assert len(orderbook) == 5 + #simulate quit + ob.on_disconnect() + rows = ob.db.execute('SELECT * FROM orderbook;').fetchall() + orderbook = [dict([(k, o[k]) for k in ORDER_KEYS]) for o in rows] + assert len(orderbook) == 0 + + + + diff --git a/setup.cfg b/setup.cfg index f4a55d4..c3686e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ # content of pytest.ini [tool:pytest] norecursedirs = test/* -testpaths = jmbitcoin jmclient jmbase +testpaths = jmbitcoin jmclient jmbase jmdaemon