Browse Source

Merge #914: move client/server data encoding to twisted

2fdebb8 do not call reactor.stop() in test_commands.py (undeath)
e082c3c remove unneeded hex encoding/decoding from sent_tx + push_tx (undeath)
dfc82ab various whitespace fixes (undeath)
7dcd3f3 move client/server data encoding to twisted (undeath)
master
Adam Gibson 4 years ago
parent
commit
717d3145c5
No known key found for this signature in database
GPG Key ID: 141001A1AF77F20B
  1. 10
      jmbase/jmbase/arguments.py
  2. 50
      jmbase/jmbase/commands.py
  3. 30
      jmbase/test/test_commands.py
  4. 78
      jmclient/jmclient/client_protocol.py
  5. 6
      jmclient/jmclient/maker.py
  6. 4
      jmclient/jmclient/taker.py
  7. 25
      jmclient/test/test_client_protocol.py
  8. 6
      jmclient/test/test_coinjoin.py
  9. 85
      jmdaemon/jmdaemon/daemon_protocol.py
  10. 25
      jmdaemon/jmdaemon/message_channel.py
  11. 31
      jmdaemon/test/test_daemon_protocol.py
  12. 8
      jmdaemon/test/test_message_channel.py
  13. 8
      test/ygrunner.py

10
jmbase/jmbase/arguments.py

@ -0,0 +1,10 @@
import json
from twisted.protocols.amp import String
class JsonEncodable(String):
def toString(self, inObject):
return super().toString(json.dumps(inObject).encode('ascii'))
def fromString(self, inString):
return super().fromString(json.loads(inString))

50
jmbase/jmbase/commands.py

@ -3,8 +3,10 @@ Commands defining client-server (daemon)
messaging protocol (*not* Joinmarket p2p protocol).
Used for AMP asynchronous messages.
"""
from twisted.protocols.amp import Boolean, Command, Integer, Unicode
from twisted.protocols.amp import Boolean, Command, Integer, Unicode, ListOf,\
String
from .bigstring import BigUnicode
from .arguments import JsonEncodable
class DaemonNotReady(Exception):
@ -29,7 +31,7 @@ class JMInit(JMCommand):
"""
arguments = [(b'bcsource', Unicode()),
(b'network', Unicode()),
(b'irc_configs', Unicode()),
(b'irc_configs', JsonEncodable()),
(b'minmakers', Integer()),
(b'maker_timeout_sec', Integer()),
(b'dust_threshold', Integer())]
@ -47,7 +49,7 @@ class JMSetup(JMCommand):
role, passes initial offers for announcement (for TAKER, this data is "none")
"""
arguments = [(b'role', Unicode()),
(b'offers', Unicode()),
(b'initdata', JsonEncodable()),
(b'use_fidelity_bond', Boolean())]
class JMMsgSignature(JMCommand):
@ -82,21 +84,21 @@ class JMFill(JMCommand):
arguments = [(b'amount', Integer()),
(b'commitment', Unicode()),
(b'revelation', Unicode()),
(b'filled_offers', Unicode())]
(b'filled_offers', JsonEncodable())]
class JMMakeTx(JMCommand):
"""Send a hex encoded raw bitcoin transaction
to a set of counterparties
"""
arguments = [(b'nick_list', Unicode()),
(b'txhex', Unicode())]
arguments = [(b'nick_list', ListOf(Unicode())),
(b'tx', String())]
class JMPushTx(JMCommand):
"""Pass a raw hex transaction to a specific
counterparty (maker) for pushing (anonymity feature in JM)
"""
arguments = [(b'nick', Unicode()),
(b'txhex', Unicode())]
(b'tx', String())]
"""MAKER specific commands
"""
@ -106,9 +108,9 @@ class JMAnnounceOffers(JMCommand):
to the daemon, along with new announcement
and cancellation lists (deltas).
"""
arguments = [(b'to_announce', Unicode()),
(b'to_cancel', Unicode()),
(b'offerlist', Unicode())]
arguments = [(b'to_announce', JsonEncodable()),
(b'to_cancel', JsonEncodable()),
(b'offerlist', JsonEncodable())]
class JMFidelityBondProof(JMCommand):
"""Send requested fidelity bond proof message"""
@ -120,7 +122,7 @@ class JMIOAuth(JMCommand):
verifying Taker's auth message
"""
arguments = [(b'nick', Unicode()),
(b'utxolist', Unicode()),
(b'utxolist', JsonEncodable()),
(b'pubkey', Unicode()),
(b'cjaddr', Unicode()),
(b'changeaddr', Unicode()),
@ -131,7 +133,7 @@ class JMTXSigs(JMCommand):
sent by TAKER
"""
arguments = [(b'nick', Unicode()),
(b'sigs', Unicode())]
(b'sigs', ListOf(Unicode()))]
"""COMMANDS FROM DAEMON TO CLIENT
=================================
@ -197,7 +199,7 @@ class JMFillResponse(JMCommand):
"""Returns ioauth data from MAKER if successful.
"""
arguments = [(b'success', Boolean()),
(b'ioauth_data', Unicode())]
(b'ioauth_data', JsonEncodable())]
class JMSigReceived(JMCommand):
"""Returns an individual bitcoin transaction signature
@ -221,9 +223,9 @@ class JMAuthReceived(JMCommand):
before setting up encryption and continuing.
"""
arguments = [(b'nick', Unicode()),
(b'offer', Unicode()),
(b'offer', JsonEncodable()),
(b'commitment', Unicode()),
(b'revelation', Unicode()),
(b'revelation', JsonEncodable()),
(b'amount', Integer()),
(b'kphex', Unicode())]
@ -232,8 +234,8 @@ class JMTXReceived(JMCommand):
by TAKER, along with offerdata to verify fees.
"""
arguments = [(b'nick', Unicode()),
(b'txhex', Unicode()),
(b'offer', Unicode())]
(b'tx', String()),
(b'offer', JsonEncodable())]
class JMTXBroadcast(JMCommand):
""" Accept a bitcoin transaction
@ -241,7 +243,7 @@ class JMTXBroadcast(JMCommand):
and relay it to the client for network
broadcast.
"""
arguments = [(b'txhex', Unicode())]
arguments = [(b'tx', String())]
"""SNICKER related commands.
"""
@ -251,12 +253,12 @@ class SNICKERReceiverInit(JMCommand):
See documentation of `netconfig` in
jmdaemon.HTTPPassThrough.on_INIT
"""
arguments = [(b'netconfig', Unicode())]
arguments = [(b'netconfig', JsonEncodable())]
class SNICKERProposerInit(JMCommand):
""" As for receiver.
"""
arguments = [(b'netconfig', Unicode())]
arguments = [(b'netconfig', JsonEncodable())]
class SNICKERReceiverUp(JMCommand):
arguments = []
@ -307,7 +309,7 @@ class BIP78SenderInit(JMCommand):
See documentation of `netconfig` in
jmdaemon.HTTPPassThrough.on_INIT
"""
arguments = [(b'netconfig', Unicode())]
arguments = [(b'netconfig', JsonEncodable())]
class BIP78SenderUp(JMCommand):
arguments = []
@ -319,7 +321,7 @@ class BIP78SenderOriginalPSBT(JMCommand):
to be sent as an http request to the receiver.
"""
arguments = [(b'body', BigUnicode()),
(b'params', Unicode())]
(b'params', JsonEncodable())]
class BIP78SenderReceiveProposal(JMCommand):
""" Sends the payjoin proposal PSBT, received
@ -341,7 +343,7 @@ class BIP78SenderReceiveError(JMCommand):
class BIP78ReceiverInit(JMCommand):
""" Initialization data for a BIP78 hidden service.
"""
arguments = [(b'netconfig', Unicode())]
arguments = [(b'netconfig', JsonEncodable())]
class BIP78ReceiverUp(JMCommand):
""" Returns onion hostname to client when
@ -356,7 +358,7 @@ class BIP78ReceiverOriginalPSBT(JMCommand):
parameters in the url, from the daemon to the client.
"""
arguments = [(b'body', BigUnicode()),
(b'params', Unicode())]
(b'params', JsonEncodable())]
class BIP78ReceiverSendProposal(JMCommand):
""" Receives a payjoin proposal PSBT from

30
jmbase/test/test_commands.py

@ -21,12 +21,12 @@ class JMBaseProtocol(amp.AMP):
is considered criticial.
"""
if 'accepted' not in response or not response['accepted']:
reactor.stop()
raise Exception(response)
def defaultErrback(self, failure):
failure.trap(ConnectionAborted, ConnectionClosed, ConnectionDone,
ConnectionLost, UnknownRemoteError)
reactor.stop()
raise Exception(failure)
def defaultCallbacks(self, d):
d.addCallback(self.checkClientResponse)
@ -63,8 +63,8 @@ class JMTestServerProtocol(JMBaseProtocol):
return {'accepted': True}
@JMSetup.responder
def on_JM_SETUP(self, role, offers, use_fidelity_bond):
show_receipt("JMSETUP", role, offers, use_fidelity_bond)
def on_JM_SETUP(self, role, initdata, use_fidelity_bond):
show_receipt("JMSETUP", role, initdata, use_fidelity_bond)
d = self.callRemote(JMSetupDone)
self.defaultCallbacks(d)
return {'accepted': True}
@ -85,12 +85,12 @@ class JMTestServerProtocol(JMBaseProtocol):
show_receipt("JMFILL", amount, commitment, revelation, filled_offers)
d = self.callRemote(JMFillResponse,
success=True,
ioauth_data = json.dumps(['dummy', 'list']))
ioauth_data=['dummy', 'list'])
return {'accepted': True}
@JMMakeTx.responder
def on_JM_MAKE_TX(self, nick_list, txhex):
show_receipt("JMMAKETX", nick_list, txhex)
def on_JM_MAKE_TX(self, nick_list, tx):
show_receipt("JMMAKETX", nick_list, tx)
d = self.callRemote(JMSigReceived,
nick="dummynick",
sig="xxxsig")
@ -113,7 +113,7 @@ class JMTestServerProtocol(JMBaseProtocol):
max_encoded=5,
hostid="hostid2")
self.defaultCallbacks(d3)
d4 = self.callRemote(JMTXBroadcast, txhex="deadbeef")
d4 = self.callRemote(JMTXBroadcast, tx=b"deadbeef")
self.defaultCallbacks(d4)
return {'accepted': True}
@ -137,7 +137,7 @@ class JMTestClientProtocol(JMBaseProtocol):
d = self.callRemote(JMInit,
bcsource="dummyblockchain",
network="dummynetwork",
irc_configs=json.dumps(['dummy', 'irc', 'config']),
irc_configs=['dummy', 'irc', 'config'],
minmakers=7,
maker_timeout_sec=8,
dust_threshold=1500)
@ -158,7 +158,7 @@ class JMTestClientProtocol(JMBaseProtocol):
show_receipt("JMUP")
d = self.callRemote(JMSetup,
role="TAKER",
offers="{}",
initdata=None,
use_fidelity_bond=False)
self.defaultCallbacks(d)
return {'accepted': True}
@ -174,8 +174,8 @@ class JMTestClientProtocol(JMBaseProtocol):
def on_JM_FILL_RESPONSE(self, success, ioauth_data):
show_receipt("JMFILLRESPONSE", success, ioauth_data)
d = self.callRemote(JMMakeTx,
nick_list= json.dumps(['nick1', 'nick2', 'nick3']),
txhex="deadbeef")
nick_list=['nick1', 'nick2', 'nick3'],
tx=b"deadbeef")
self.defaultCallbacks(d)
return {'accepted': True}
@ -186,7 +186,7 @@ class JMTestClientProtocol(JMBaseProtocol):
amount=100,
commitment="dummycommitment",
revelation="dummyrevelation",
filled_offers=json.dumps(['list', 'of', 'filled', 'offers']))
filled_offers=['list', 'of', 'filled', 'offers'])
self.defaultCallbacks(d)
return {'accepted': True}
@ -222,8 +222,8 @@ class JMTestClientProtocol(JMBaseProtocol):
return {'accepted': True}
@JMTXBroadcast.responder
def on_JM_TX_BROADCAST(self, txhex):
show_receipt("JMTXBROADCAST", txhex)
def on_JM_TX_BROADCAST(self, tx):
show_receipt("JMTXBROADCAST", tx)
return {"accepted": True}
class JMTestClientProtocolFactory(protocol.ClientFactory):

78
jmclient/jmclient/client_protocol.py

@ -73,13 +73,13 @@ class BIP78ClientProtocol(BaseClientProtocol):
"tls_whitelist": ",".join(self.tls_whitelist),
"servers": [self.manager.server]}
d = self.callRemote(commands.BIP78SenderInit,
netconfig=json.dumps(netconfig))
netconfig=netconfig)
else:
netconfig = {"port": 80,
"tor_control_host": jcg("PAYJOIN", "tor_control_host"),
"tor_control_port": jcg("PAYJOIN", "tor_control_port")}
d = self.callRemote(commands.BIP78ReceiverInit,
netconfig=json.dumps(netconfig))
netconfig=netconfig)
self.defaultCallbacks(d)
@commands.BIP78ReceiverUp.responder
@ -89,7 +89,6 @@ class BIP78ClientProtocol(BaseClientProtocol):
@commands.BIP78ReceiverOriginalPSBT.responder
def on_BIP78_RECEIVER_ORIGINAL_PSBT(self, body, params):
params = json.loads(params)
# TODO: we don't need binary key/vals client side, but will have to edit
# PayjoinConverter for that:
retval = self.success_callback(body.encode("utf-8"), bdict_sdict_convert(
@ -121,7 +120,7 @@ class BIP78ClientProtocol(BaseClientProtocol):
def on_BIP78_SENDER_UP(self):
d = self.callRemote(commands.BIP78SenderOriginalPSBT,
body=self.manager.initial_psbt.to_base64(),
params=json.dumps(self.params))
params=self.params)
self.defaultCallbacks(d)
return {"accepted": True}
@ -168,10 +167,10 @@ class SNICKERClientProtocol(BaseClientProtocol):
if isinstance(self.client, SNICKERReceiver):
d = self.callRemote(commands.SNICKERReceiverInit,
netconfig=json.dumps(netconfig))
netconfig=netconfig)
else:
d = self.callRemote(commands.SNICKERProposerInit,
netconfig=json.dumps(netconfig))
netconfig=netconfig)
self.defaultCallbacks(d)
def shutdown(self):
@ -361,10 +360,10 @@ class JMClientProtocol(BaseClientProtocol):
self.defaultCallbacks(d)
return {'accepted': True}
def make_tx(self, nick_list, txhex):
def make_tx(self, nick_list, tx):
d = self.callRemote(commands.JMMakeTx,
nick_list= json.dumps(nick_list),
txhex=txhex)
nick_list=nick_list,
tx=tx)
self.defaultCallbacks(d)
class JMMakerClientProtocol(JMClientProtocol):
@ -394,7 +393,7 @@ class JMMakerClientProtocol(JMClientProtocol):
self.offers_ready_loop.stop()
d = self.callRemote(commands.JMSetup,
role="MAKER",
offers=json.dumps(self.client.offerlist),
initdata=self.client.offerlist,
use_fidelity_bond=(self.client.fidelity_bond is not None))
self.defaultCallbacks(d)
@ -423,7 +422,7 @@ class JMMakerClientProtocol(JMClientProtocol):
d = self.callRemote(commands.JMInit,
bcsource=blockchain_source,
network=network,
irc_configs=json.dumps(irc_configs),
irc_configs=irc_configs,
minmakers=minmakers,
maker_timeout_sec=maker_timeout_sec,
dust_threshold=jm_single().DUST_THRESHOLD)
@ -443,8 +442,6 @@ class JMMakerClientProtocol(JMClientProtocol):
@commands.JMAuthReceived.responder
def on_JM_AUTH_RECEIVED(self, nick, offer, commitment, revelation, amount,
kphex):
offer = json.loads(offer)
revelation = json.loads(revelation)
retval = self.client.on_auth_received(nick, offer,
commitment, revelation, amount, kphex)
if not retval[0]:
@ -461,7 +458,7 @@ class JMMakerClientProtocol(JMClientProtocol):
auth_pub_hex = bintohex(auth_pub)
d = self.callRemote(commands.JMIOAuth,
nick=nick,
utxolist=json.dumps(utxos_strkeyed),
utxolist=utxos_strkeyed,
pubkey=auth_pub_hex,
cjaddr=cj_addr,
changeaddr=change_addr,
@ -470,17 +467,15 @@ class JMMakerClientProtocol(JMClientProtocol):
return {"accepted": True}
@commands.JMTXReceived.responder
def on_JM_TX_RECEIVED(self, nick, txhex, offer):
offer = json.loads(offer)
retval = self.client.on_tx_received(nick, txhex, offer)
def on_JM_TX_RECEIVED(self, nick, tx, offer):
retval = self.client.on_tx_received(nick, tx, offer)
if not retval[0]:
jlog.info("Maker refuses to continue on receipt of tx")
else:
sigs = retval[1]
self.finalized_offers[nick] = offer
tx = btc.CMutableTransaction.deserialize(hextobin(txhex))
tx = btc.CMutableTransaction.deserialize(tx)
self.finalized_offers[nick]["txd"] = tx
txid = tx.GetTxid()[::-1]
# we index the callback by the out-set of the transaction,
# because the txid is not known until all scriptSigs collected
# (hence this is required for Makers, but not Takers).
@ -497,14 +492,12 @@ class JMMakerClientProtocol(JMClientProtocol):
txinfo, self.unconfirm_callback, "unconfirmed",
"transaction with outputs: " + str(txinfo) + " not broadcast.")
d = self.callRemote(commands.JMTXSigs,
nick=nick,
sigs=json.dumps(sigs))
d = self.callRemote(commands.JMTXSigs, nick=nick, sigs=sigs)
self.defaultCallbacks(d)
return {"accepted": True}
@commands.JMTXBroadcast.responder
def on_JM_TX_BROADCAST(self, txhex):
def on_JM_TX_BROADCAST(self, tx):
""" Makers have no issue broadcasting anything,
so only need to prevent crashes.
Note in particular we don't check the return value,
@ -512,11 +505,10 @@ class JMMakerClientProtocol(JMClientProtocol):
our (maker)'s concern.
"""
try:
txbin = hextobin(txhex)
jm_single().bc_interface.pushtx(txbin)
jm_single().bc_interface.pushtx(tx)
except:
jlog.info("We received an invalid transaction broadcast "
"request: " + txhex)
"request: " + tx.hex())
return {"accepted": True}
def tx_match(self, txd):
@ -547,9 +539,9 @@ class JMMakerClientProtocol(JMClientProtocol):
"transaction with outputs " + str(txinfo) + " not confirmed.")
d = self.callRemote(commands.JMAnnounceOffers,
to_announce=json.dumps(to_announce),
to_cancel=json.dumps(to_cancel),
offerlist=json.dumps(self.client.offerlist))
to_announce=to_announce,
to_cancel=to_cancel,
offerlist=self.client.offerlist)
self.defaultCallbacks(d)
return True
@ -564,9 +556,9 @@ class JMMakerClientProtocol(JMClientProtocol):
txid, confirms)
self.client.modify_orders(to_cancel, to_announce)
d = self.callRemote(commands.JMAnnounceOffers,
to_announce=json.dumps(to_announce),
to_cancel=json.dumps(to_cancel),
offerlist=json.dumps(self.client.offerlist))
to_announce=to_announce,
to_cancel=to_cancel,
offerlist=self.client.offerlist)
self.defaultCallbacks(d)
return True
@ -601,7 +593,7 @@ class JMTakerClientProtocol(JMClientProtocol):
d = self.callRemote(commands.JMInit,
bcsource=blockchain_source,
network=network,
irc_configs=json.dumps(irc_configs),
irc_configs=irc_configs,
minmakers=minmakers,
maker_timeout_sec=maker_timeout_sec,
dust_threshold=jm_single().DUST_THRESHOLD)
@ -641,7 +633,7 @@ class JMTakerClientProtocol(JMClientProtocol):
def on_JM_UP(self):
d = self.callRemote(commands.JMSetup,
role="TAKER",
offers="{}",
initdata=None,
use_fidelity_bond=False)
self.defaultCallbacks(d)
return {'accepted': True}
@ -669,13 +661,12 @@ class JMTakerClientProtocol(JMClientProtocol):
the ioauth data and returns the proposed
transaction, passes the phase 2 initiating data to the daemon.
"""
ioauth_data = json.loads(ioauth_data)
if not success:
jlog.info("Makers who didnt respond: " + str(ioauth_data))
self.client.add_ignored_makers(ioauth_data)
return {'accepted': True}
else:
jlog.info("Makers responded with: " + json.dumps(ioauth_data))
jlog.info("Makers responded with: " + str(ioauth_data))
retval = self.client.receive_utxos(ioauth_data)
if not retval[0]:
jlog.info("Taker is not continuing, phase 2 abandoned.")
@ -686,8 +677,8 @@ class JMTakerClientProtocol(JMClientProtocol):
self.client.on_finished_callback(False, False, 0.0)
return {'accepted': False}
else:
nick_list, txhex = retval[1:]
reactor.callLater(0, self.make_tx, nick_list, txhex)
nick_list, tx = retval[1:]
reactor.callLater(0, self.make_tx, nick_list, tx)
return {'accepted': True}
@commands.JMOffers.responder
@ -717,7 +708,7 @@ class JMTakerClientProtocol(JMClientProtocol):
amount=amt,
commitment=str(cmt),
revelation=str(rev),
filled_offers=json.dumps(foffers))
filled_offers=foffers)
self.defaultCallbacks(d)
return {'accepted': True}
@ -725,17 +716,16 @@ class JMTakerClientProtocol(JMClientProtocol):
def on_JM_SIG_RECEIVED(self, nick, sig):
retval = self.client.on_sig(nick, sig)
if retval:
nick_to_use, txhex = retval
self.push_tx(nick_to_use, txhex)
nick_to_use, tx = retval
self.push_tx(nick_to_use, tx)
return {'accepted': True}
def get_offers(self):
d = self.callRemote(commands.JMRequestOffers)
self.defaultCallbacks(d)
def push_tx(self, nick_to_push, txhex_to_push):
d = self.callRemote(commands.JMPushTx, nick=str(nick_to_push),
txhex=str(txhex_to_push))
def push_tx(self, nick_to_push, tx):
d = self.callRemote(commands.JMPushTx, nick=str(nick_to_push), tx=tx)
self.defaultCallbacks(d)
class SNICKERClientProtocolFactory(protocol.ClientFactory):

6
jmclient/jmclient/maker.py

@ -118,7 +118,7 @@ class Maker(object):
return (True, utxos, auth_pub, cj_addr, change_addr, btc_sig)
@hexbin
def on_tx_received(self, nick, tx_from_taker, offerinfo):
def on_tx_received(self, nick, tx, offerinfo):
"""Called when the counterparty has sent an unsigned
transaction. Sigs are created and returned if and only
if the transaction passes verification checks (see
@ -129,9 +129,9 @@ class Maker(object):
if not isinstance(offerinfo["offer"]["cjfee"], str):
offerinfo["offer"]["cjfee"] = bintohex(offerinfo["offer"]["cjfee"])
try:
tx = btc.CMutableTransaction.deserialize(tx_from_taker)
tx = btc.CMutableTransaction.deserialize(tx)
except Exception as e:
return (False, 'malformed txhex. ' + repr(e))
return (False, 'malformed tx. ' + repr(e))
# if the above deserialization was successful, the human readable
# parsing will be also:
jlog.info('obtained tx\n' + btc.human_readable_transaction(tx))

4
jmclient/jmclient/taker.py

@ -524,7 +524,7 @@ class Taker(object):
self.taker_info_callback("INFO", "Built tx, sending to counterparties.")
return (True, list(self.maker_utxo_data.keys()),
bintohex(self.latest_tx.serialize()))
self.latest_tx.serialize())
def _verify_ioauth_data(self, ioauth_data):
verified_data = []
@ -943,7 +943,7 @@ class Taker(object):
self.on_finished_callback(False, fromtx=True)
else:
if nick_to_use:
return (nick_to_use, bintohex(self.latest_tx.serialize()))
return (nick_to_use, self.latest_tx.serialize())
#if push was not successful, return None
def self_sign_and_push(self):

25
jmclient/test/test_client_protocol.py

@ -20,6 +20,7 @@ from commontest import default_max_cj_fee
import json
import jmbitcoin as bitcoin
import twisted
import base64
twisted.internet.base.DelayedCall.debug = True
test_completed = False
@ -64,7 +65,8 @@ class DummyTaker(Taker):
if self.failutxos:
return (False, "dummyreason")
else:
return (True, [x*64 + ":01" for x in ["a", "b", "c"]], t_raw_signed_tx)
return (True, [x*64 + ":01" for x in ["a", "b", "c"]],
base64.b16decode(t_raw_signed_tx, casefold=True))
def on_sig(self, nick, sigb64):
@ -99,7 +101,7 @@ class DummyMaker(Maker):
# success, utxos, auth_pub, cj_addr, change_addr, btc_sig
return True, [], b"", '', '', ''
def on_tx_received(self, nick, txhex, offerinfo):
def on_tx_received(self, nick, tx, offerinfo):
# success, sigs
return True, []
@ -185,8 +187,8 @@ class JMTestServerProtocol(JMBaseProtocol):
return {'accepted': True}
@JMSetup.responder
def on_JM_SETUP(self, role, offers, use_fidelity_bond):
show_receipt("JMSETUP", role, offers, use_fidelity_bond)
def on_JM_SETUP(self, role, initdata, use_fidelity_bond):
show_receipt("JMSETUP", role, initdata, use_fidelity_bond)
d = self.callRemote(JMSetupDone)
self.defaultCallbacks(d)
return {'accepted': True}
@ -209,12 +211,12 @@ class JMTestServerProtocol(JMBaseProtocol):
show_receipt("JMFILL", amount, commitment, revelation, filled_offers)
d = self.callRemote(JMFillResponse,
success=success,
ioauth_data = json.dumps(['dummy', 'list']))
ioauth_data=['dummy', 'list'])
return {'accepted': True}
@JMMakeTx.responder
def on_JM_MAKE_TX(self, nick_list, txhex):
show_receipt("JMMAKETX", nick_list, txhex)
def on_JM_MAKE_TX(self, nick_list, tx):
show_receipt("JMMAKETX", nick_list, tx)
d = self.callRemote(JMSigReceived,
nick="dummynick",
sig="xxxsig")
@ -390,13 +392,14 @@ class TestMakerClientProtocol(unittest.TestCase):
def test_JMAuthReceived(self):
yield self.init_client()
yield self.callClient(
JMAuthReceived, nick='testnick', offer='{}',
commitment='testcommitment', revelation='{}', amount=100000,
JMAuthReceived, nick='testnick', offer={},
commitment='testcommitment', revelation={}, amount=100000,
kphex='testkphex')
@inlineCallbacks
def test_JMTXReceived(self):
yield self.init_client()
yield self.callClient(
JMTXReceived, nick='testnick', txhex=t_raw_signed_tx,
offer='{"cjaddr":"2MwfecDHsQTm4Gg3RekQdpqAMR15BJrjfRF"}')
JMTXReceived, nick='testnick',
tx=base64.b16decode(t_raw_signed_tx, casefold=True),
offer={"cjaddr":"2MwfecDHsQTm4Gg3RekQdpqAMR15BJrjfRF"})

6
jmclient/test/test_coinjoin.py

@ -9,7 +9,7 @@ import pytest
import copy
from twisted.internet import reactor
from jmbase import get_log, hextobin
from jmbase import get_log
from jmclient import load_test_config, jm_single,\
YieldGeneratorBasic, Taker, LegacyWallet, SegwitLegacyWallet, SegwitWallet,\
NO_ROUNDING
@ -206,7 +206,7 @@ def test_coinjoin_mixdepth_wrap_taker(monkeypatch, tmpdir, setup_cj):
taker_final_result = do_tx_signing(taker, makers, active_orders, txdata)
assert taker_final_result is not False
tx = btc.CMutableTransaction.deserialize(hextobin(txdata[2]))
tx = btc.CMutableTransaction.deserialize(txdata[2])
wallet_service = wallet_services[-1]
# TODO change for new tx monitoring:
@ -261,7 +261,7 @@ def test_coinjoin_mixdepth_wrap_maker(monkeypatch, tmpdir, setup_cj):
taker_final_result = do_tx_signing(taker, makers, active_orders, txdata)
assert taker_final_result is not False
tx = btc.CMutableTransaction.deserialize(hextobin(txdata[2]))
tx = btc.CMutableTransaction.deserialize(txdata[2])
for i in range(MAKER_NUM):
wallet_service = wallet_services[i]

85
jmdaemon/jmdaemon/daemon_protocol.py

@ -9,7 +9,7 @@ from .protocol import (COMMAND_PREFIX, ORDER_KEYS, NICK_HASH_LENGTH,
COMMITMENT_PREFIXES)
from .irc import IRCMessageChannel
from jmbase import (hextobin, is_hs_uri, get_tor_agent, JMHiddenService,
from jmbase import (is_hs_uri, get_tor_agent, JMHiddenService,
get_nontor_agent, BytesProducer, wrapped_urlparse,
bdict_sdict_convert, JMHTTPResource)
from jmbase.commands import *
@ -32,7 +32,6 @@ import os
from io import BytesIO
import copy
from functools import wraps
from numbers import Integral
"""Joinmarket application protocol control flow.
For documentation on protocol (formats, message sequence) see
@ -158,6 +157,7 @@ class BIP78ReceiverResource(JMHTTPResource):
self.info_callback("Shutting down, payjoin negotiation failed.")
self.shutdown_callback()
class HTTPPassThrough(amp.AMP):
""" This class supports passing through
requests over HTTPS or over a socks proxy to a remote
@ -174,7 +174,6 @@ class HTTPPassThrough(amp.AMP):
filterconfig (not yet defined)
credentials (not yet defined)
"""
netconfig = json.loads(netconfig)
self.socks5_host = netconfig["socks5_host"]
self.socks5_port = int(netconfig["socks5_port"])
self.servers = [a for a in netconfig["servers"] if a != ""]
@ -272,7 +271,6 @@ class HTTPPassThrough(amp.AMP):
class BIP78ServerProtocol(HTTPPassThrough):
@BIP78ReceiverInit.responder
def on_BIP78_RECEIVER_INIT(self, netconfig):
netconfig = json.loads(netconfig)
self.serving_port = int(netconfig["port"])
self.tor_control_host = netconfig["tor_control_host"]
self.tor_control_port = int(netconfig["tor_control_port"])
@ -325,7 +323,7 @@ class BIP78ServerProtocol(HTTPPassThrough):
"""
self.post_request = request
d = self.callRemote(BIP78ReceiverOriginalPSBT, body=body,
params=json.dumps(bdict_sdict_convert(params)))
params=bdict_sdict_convert(params))
self.defaultCallbacks(d)
@BIP78ReceiverSendProposal.responder
@ -355,7 +353,7 @@ class BIP78ServerProtocol(HTTPPassThrough):
def on_BIP78_SENDER_ORIGINAL_PSBT(self, body, params):
self.postRequest(body, self.servers[0],
self.bip78_receiver_response,
params=json.loads(params),
params=params,
headers=Headers({"Content-Type": ["text/plain"]}))
return {"accepted": True}
@ -475,6 +473,8 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
self.sig_lock = threading.Lock()
self.active_orders = {}
self.use_fidelity_bond = False
self.offerlist = None
self.kp = None
def checkClientResponse(self, response):
"""A generic check of client acceptance; any failure
@ -502,11 +502,9 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
If a new message channel configuration is required, the current
one is shutdown in preparation.
"""
self.maker_timeout_sec = int(maker_timeout_sec)
# used in OrderbookWatch:
self.maker_timeout_sec = maker_timeout_sec
self.minmakers = minmakers
self.dust_threshold = int(dust_threshold)
self.minmakers = int(minmakers)
irc_configs = json.loads(irc_configs)
#(bitcoin) network only referenced in channel name construction
self.network = network
if irc_configs == self.irc_configs:
@ -554,7 +552,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
return {'accepted': True}
@JMSetup.responder
def on_JM_SETUP(self, role, offers, use_fidelity_bond):
def on_JM_SETUP(self, role, initdata, use_fidelity_bond):
assert self.jm_state == 0
self.role = role
self.crypto_boxes = {}
@ -568,7 +566,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
if self.role == "TAKER":
self.mcc.pubmsg(COMMAND_PREFIX + "orderbook")
elif self.role == "MAKER":
self.offerlist = json.loads(offers)
self.offerlist = initdata
self.use_fidelity_bond = use_fidelity_bond
self.mcc.announce_orders(self.offerlist, None, None, None)
self.jm_state = 1
@ -614,15 +612,14 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
"""Takes the necessary data from the Taker and initiates the Stage 1
interaction with the Makers.
"""
if not (self.jm_state == 1 and isinstance(amount, Integral)
and amount >= 0):
if self.jm_state != 1 or amount < 0:
return {'accepted': False}
self.cjamount = amount
self.commitment = commitment
self.revelation = revelation
#Reset utxo data to null for this new transaction
self.ioauth_data = {}
self.active_orders = json.loads(filled_offers)
self.active_orders = filled_offers
for nick, offer_dict in self.active_orders.items():
offer_fill_msg = " ".join([str(offer_dict["oid"]), str(amount),
self.kp.hex_pk().decode('ascii'), str(commitment)])
@ -632,20 +629,19 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
return {'accepted': True}
@JMMakeTx.responder
def on_JM_MAKE_TX(self, nick_list, txhex):
def on_JM_MAKE_TX(self, nick_list, tx):
"""Taker sends the prepared unsigned transaction
to all the Makers in nick_list
"""
if not self.jm_state == 4:
log.msg("Make tx was called in wrong state, rejecting")
return {'accepted': False}
nick_list = json.loads(nick_list)
self.mcc.send_tx(nick_list, txhex)
self.mcc.send_tx(nick_list, tx)
return {'accepted': True}
@JMPushTx.responder
def on_JM_PushTx(self, nick, txhex):
self.mcc.push_tx(nick, txhex)
def on_JM_PushTx(self, nick, tx):
self.mcc.push_tx(nick, tx)
return {'accepted': True}
"""Maker specific responders
@ -659,9 +655,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
"""
if self.role != "MAKER":
return
to_announce = json.loads(to_announce)
to_cancel = json.loads(to_cancel)
self.offerlist = json.loads(offerlist)
self.offerlist = offerlist
if len(to_cancel) > 0:
self.mcc.cancel_orders(to_cancel)
if len(to_announce) > 0:
@ -682,14 +676,13 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
"""
if not self.role == "MAKER":
return
if not nick in self.active_orders:
if nick not in self.active_orders:
return
utxos= json.loads(utxolist)
#completed population of order/offer object
self.active_orders[nick]["cjaddr"] = cjaddr
self.active_orders[nick]["changeaddr"] = changeaddr
self.active_orders[nick]["utxos"] = utxos
msg = str(",".join(utxos.keys())) + " " + " ".join(
self.active_orders[nick]["utxos"] = utxolist
msg = str(",".join(utxolist)) + " " + " ".join(
[pubkey, cjaddr, changeaddr, pubkeysig])
self.mcc.prepare_privmsg(nick, "ioauth", msg)
#In case of *blacklisted (ie already used) commitments, we already
@ -709,7 +702,6 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
broadcast one by one. TODO: could shorten this,
have more than one sig per message.
"""
sigs = json.loads(sigs)
for sig in sigs:
self.mcc.prepare_privmsg(nick, "sig", sig)
return {"accepted": True}
@ -798,15 +790,15 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
commitment revelation against the existing commitment,
which was already stored in active_orders[nick].
"""
if not nick in self.active_orders:
if nick not in self.active_orders:
return
ao = self.active_orders[nick]
#ask the client to validate the commitment and prepare the utxo data
d = self.callRemote(JMAuthReceived,
nick=nick,
offer=json.dumps(ao["offer"]),
offer=ao["offer"],
commitment=ao["commit"],
revelation=json.dumps(commitment_revelation),
revelation=commitment_revelation,
amount=ao["amount"],
kphex=ao["kp"].hex_pk().decode('ascii'))
self.defaultCallbacks(d)
@ -831,20 +823,14 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
self.mcc.pubmsg("!hp2 " + commitment)
@maker_only
def on_push_tx(self, nick, txhex):
"""Broadcast unquestioningly, except checking
hex format.
def on_push_tx(self, nick, tx):
"""Broadcast unquestioningly
"""
try:
dummy = hextobin(txhex)
except:
return
d = self.callRemote(JMTXBroadcast,
txhex=txhex)
d = self.callRemote(JMTXBroadcast, tx=tx)
self.defaultCallbacks(d)
@maker_only
def on_seen_tx(self, nick, txhex):
def on_seen_tx(self, nick, tx):
"""Passes the txhex to the Maker for verification
and signing. Note the security checks occur in Maker.
"""
@ -855,10 +841,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
ao = copy.deepcopy(self.active_orders[nick])
del ao["crypto_box"]
del ao["kp"]
d = self.callRemote(JMTXReceived,
nick=nick,
txhex=txhex,
offer=json.dumps(ao))
d = self.callRemote(JMTXReceived, nick=nick, tx=tx, offer=ao)
self.defaultCallbacks(d)
@taker_only
@ -898,9 +881,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
def on_sig(self, nick, sig):
"""Pass signature through to Taker.
"""
d = self.callRemote(JMSigReceived,
nick=nick,
sig=sig)
d = self.callRemote(JMSigReceived, nick=nick, sig=sig)
self.defaultCallbacks(d)
def on_error(self, msg):
@ -990,12 +971,12 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
self.jm_state = 3
if not accepted:
#use ioauth data field to return the list of non-responsive makers
nonresponders = [x for x in self.active_orders.keys() if x not
in self.ioauth_data.keys()]
nonresponders = [x for x in self.active_orders
if x not in self.ioauth_data]
ioauth_data = self.ioauth_data if accepted else nonresponders
d = self.callRemote(JMFillResponse,
success=accepted,
ioauth_data = json.dumps(ioauth_data))
ioauth_data=ioauth_data)
if not accepted:
#Client simply accepts failure TODO
self.defaultCallbacks(d)
@ -1010,7 +991,7 @@ class JMDaemonServerProtocol(amp.AMP, OrderbookWatch):
either send success + ioauth data if enough makers,
else send failure to client.
"""
response = True if len(self.ioauth_data.keys()) >= self.minmakers else False
response = True if len(self.ioauth_data) >= self.minmakers else False
self.respondToIoauths(response)
def checkUtxosAccepted(self, accepted):

25
jmdaemon/jmdaemon/message_channel.py

@ -1,7 +1,6 @@
#! /usr/bin/env python
import abc
import base64
import binascii
import threading
from twisted.internet import reactor
from jmdaemon import encrypt_encode, decode_decrypt, COMMAND_PREFIX,\
@ -351,14 +350,14 @@ class MessageChannelCollection(object):
self.prepare_privmsg(nick, "error", errormsg)
@check_privmsg
def push_tx(self, nick, txhex):
def push_tx(self, nick, tx):
#TODO supporting sending to arbitrary nicks
#adds quite a bit of complexity, not supported
#initially; will fail if nick is not part of TX
txb64 = base64.b64encode(binascii.unhexlify(txhex)).decode('ascii')
txb64 = base64.b64encode(tx).decode('ascii')
self.prepare_privmsg(nick, "push", txb64)
def send_tx(self, nick_list, txhex):
def send_tx(self, nick_list, tx):
"""Push out the transaction to nicks
in groups by their message channel.
"""
@ -376,10 +375,10 @@ class MessageChannelCollection(object):
else:
tx_nick_sets[self.active_channels[nick]].append(nick)
for mc, nl in tx_nick_sets.items():
self.prepare_send_tx(mc, nl, txhex)
self.prepare_send_tx(mc, nl, tx)
def prepare_send_tx(self, mc, nick_list, txhex):
txb64 = base64.b64encode(binascii.unhexlify(txhex)).decode('ascii')
def prepare_send_tx(self, mc, nick_list, tx):
txb64 = base64.b64encode(tx).decode('ascii')
for nick in nick_list:
self.prepare_privmsg(nick, "tx", txb64, mc=mc)
@ -853,10 +852,10 @@ class MessageChannel(object):
msg += ' ' + commitment
self.privmsg(c, 'fill', msg)
def push_tx(self, nick, txhex):
def push_tx(self, nick, tx):
#Note: not currently used; will require prepare_privmsg call so
#not in this class (see send_error)
txb64 = base64.b64encode(binascii.unhexlify(txhex)).decode('ascii')
txb64 = base64.b64encode(tx).decode('ascii')
self.privmsg(nick, 'push', txb64)
def send_error(self, nick, errormsg):
@ -1028,21 +1027,21 @@ class MessageChannel(object):
elif _chunks[0] == 'tx':
b64tx = _chunks[1]
try:
txhex = binascii.hexlify(base64.b64decode(b64tx)).decode('ascii')
tx = base64.b64decode(b64tx)
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)
self.on_seen_tx(nick, tx)
elif _chunks[0] == 'push':
b64tx = _chunks[1]
try:
txhex = binascii.hexlify(base64.b64decode(b64tx)).decode('ascii')
tx = base64.b64decode(b64tx)
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)
self.on_push_tx(nick, tx)
except (IndexError, ValueError):
# TODO proper error handling
log.debug('cj peer error TODO handle')

31
jmdaemon/test/test_daemon_protocol.py

@ -19,7 +19,6 @@ from twisted.protocols import amp
from twisted.trial import unittest
from jmbase.commands import *
from msgdata import *
import json
import base64
import sys
from dummy_mc import DummyMessageChannel
@ -64,7 +63,7 @@ class JMTestClientProtocol(JMBaseProtocol):
d = self.callRemote(JMInit,
bcsource="dummyblockchain",
network="dummynetwork",
irc_configs=json.dumps(irc),
irc_configs=irc,
minmakers=2,
maker_timeout_sec=3,
dust_threshold=27300)
@ -85,7 +84,7 @@ class JMTestClientProtocol(JMBaseProtocol):
show_receipt("JMUP")
d = self.callRemote(JMSetup,
role="TAKER",
offers="{}",
initdata=None,
use_fidelity_bond=False)
self.defaultCallbacks(d)
return {'accepted': True}
@ -104,11 +103,10 @@ class JMTestClientProtocol(JMBaseProtocol):
return {'accepted': True}
def maketx(self, ioauth_data):
ioauth_data = json.loads(ioauth_data)
nl = list(ioauth_data.keys())
nl = list(ioauth_data)
d = self.callRemote(JMMakeTx,
nick_list= json.dumps(nl),
txhex="deadbeef")
nick_list=nl,
tx=b"deadbeef")
self.defaultCallbacks(d)
@JMOffers.responder
@ -136,7 +134,7 @@ class JMTestClientProtocol(JMBaseProtocol):
amount=100,
commitment="dummycommitment",
revelation="dummyrevelation",
filled_offers=json.dumps(t_chosen_orders))
filled_offers=t_chosen_orders)
self.defaultCallbacks(d)
return {'accepted': True}
@ -215,9 +213,9 @@ class JMDaemonTestServerProtocol(JMDaemonServerProtocol):
@JMInit.responder
def on_JM_INIT(self, bcsource, network, irc_configs, minmakers,
maker_timeout_sec, dust_threshold):
self.maker_timeout_sec = int(maker_timeout_sec)
self.maker_timeout_sec = maker_timeout_sec
self.dust_threshold = int(dust_threshold)
self.minmakers = int(minmakers)
self.minmakers = minmakers
mcs = [DummyMC(None)]
self.mcc = MessageChannelCollection(mcs)
#The following is a hack to get the counterparties marked seen/active;
@ -241,30 +239,29 @@ class JMDaemonTestServerProtocol(JMDaemonServerProtocol):
@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, list(tmpfo.keys())[0], dummypub + "deadbeef")
reactor.callLater(2, self.on_pubkey, list(filled_offers)[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(list(tmpfo.keys())[0]), 6, 7, self.mcc.mchannels[0].hostid)
str(list(filled_offers)[0]), 6, 7, self.mcc.mchannels[0].hostid)
#send "valid" onpubkey, onioauth messages
for k, v in tmpfo.items():
for k, v in filled_offers.items():
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().on_JM_FILL(amount, commitment, revelation, filled_offers)
@JMMakeTx.responder
def on_JM_MAKE_TX(self, nick_list, txhex):
for n in json.loads(nick_list):
def on_JM_MAKE_TX(self, nick_list, tx):
for n in nick_list:
reactor.callLater(1, self.on_sig, n, "dummytxsig")
return super().on_JM_MAKE_TX(nick_list, txhex)
return super().on_JM_MAKE_TX(nick_list, tx)

8
jmdaemon/test/test_message_channel.py

@ -160,7 +160,7 @@ def test_setup_mc():
mcc.privmsg(cp1+"XXX", "fill", "0")
#trigger check_privmsg decorator
mcc.send_error(cp1, "errormsg")
mcc.push_tx(cp1, "deadbeef")
mcc.push_tx(cp1, b"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.
@ -215,13 +215,13 @@ def test_setup_mc():
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")
mcc.send_tx(cps, b"deadbeef")
#Now initialize the boxes
for c in cps:
dummydaemon.crypto_boxes[c] = ["a", DummyBox()]
mcc.send_tx(cps, "deadbeef")
mcc.send_tx(cps, b"deadbeef")
#try to send the transaction to a wrong cp:
mcc.send_tx(["notrealcp"], "deadbeef")
mcc.send_tx(["notrealcp"], b"deadbeef")
#At this stage, dmcs0,2 should be "up" and 1 should be "down":
assert mcc.mc_status[dmcs[0]] == 1

8
test/ygrunner.py

@ -54,12 +54,12 @@ class MaliciousYieldGenerator(YieldGeneratorBasic):
jmprint("Counterparty commitment rejected maliciously", "debug")
return (False,)
return super().on_auth_received(nick, offer, commitment, cr, amount, kphex)
def on_tx_received(self, nick, txhex, offerinfo):
def on_tx_received(self, nick, tx, offerinfo):
if self.txmal:
if random.randint(1, 100) < self.mfrac:
jmprint("Counterparty tx rejected maliciously", "debug")
return (False, "malicious tx rejection")
return super().on_tx_received(nick, txhex, offerinfo)
return super().on_tx_received(nick, tx, offerinfo)
class DeterministicMaliciousYieldGenerator(YieldGeneratorBasic):
"""Overrides, randomly chosen persistently, some maker functions
@ -85,11 +85,11 @@ class DeterministicMaliciousYieldGenerator(YieldGeneratorBasic):
jmprint("Counterparty commitment rejected maliciously", "debug")
return (False,)
return super().on_auth_received(nick, offer, commitment, cr, amount, kphex)
def on_tx_received(self, nick, txhex, offerinfo):
def on_tx_received(self, nick, tx, offerinfo):
if self.txmal:
jmprint("Counterparty tx rejected maliciously", "debug")
return (False, "malicious tx rejection")
return super().on_tx_received(nick, txhex, offerinfo)
return super().on_tx_received(nick, tx, offerinfo)
@pytest.mark.parametrize(

Loading…
Cancel
Save