From 08d3f05c1ca5173939141c7a12e3f6f2c2d46da5 Mon Sep 17 00:00:00 2001
From: tecnovert <tecnovert@tecnovert.net>
Date: Sun, 9 Jul 2023 17:41:52 +0200
Subject: [PATCH] tests: Add more bid intent test cases.

Send bid intent amount and rate un-reversed.
---
 basicswap/basicswap.py          | 141 +++-----
 basicswap/interface/xmr.py      |   5 +-
 basicswap/messages.proto        |   9 +-
 basicswap/messages_pb2.py       |  10 +-
 tests/basicswap/test_btc_xmr.py | 601 ++++++++++++++++++--------------
 tests/basicswap/test_xmr.py     |   2 +-
 6 files changed, 409 insertions(+), 359 deletions(-)

diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py
index 2f5952e..d1a32f9 100644
--- a/basicswap/basicswap.py
+++ b/basicswap/basicswap.py
@@ -954,15 +954,13 @@ class BasicSwap(BaseApp):
         session.add(kv)
 
     def setIntKV(self, str_key: str, int_val: int) -> None:
-        self.mxDB.acquire()
+        session = self.openSession()
         try:
             session = scoped_session(self.session_factory)
             self.setIntKVInSession(str_key, int_val, session)
             session.commit()
         finally:
-            session.close()
-            session.remove()
-            self.mxDB.release()
+            self.closeSession(session, commit=False)
 
     def setStringKV(self, str_key: str, str_val: str, session=None) -> None:
         try:
@@ -2468,7 +2466,6 @@ class BasicSwap(BaseApp):
             reverse_bid: bool = coin_from in self.scriptless_coins
             if reverse_bid:
                 reversed_rate: int = ci_to.make_int(amount / amount_to, r=1)
-
                 amount_from: int = int((int(amount_to) * reversed_rate) // ci_to.COIN())
                 ensure(abs(amount_from - amount) < 20, 'invalid bid amount')  # TODO: Tolerance?
 
@@ -2476,8 +2473,9 @@ class BasicSwap(BaseApp):
                 msg_buf.protocol_version = PROTOCOL_VERSION_ADAPTOR_SIG
                 msg_buf.offer_msg_id = offer_id
                 msg_buf.time_valid = valid_for_seconds
-                msg_buf.amount = int(amount_to)
-                msg_buf.rate = reversed_rate
+                msg_buf.amount_from = amount
+                msg_buf.amount_to = amount_to
+                msg_buf.rate = bid_rate
 
                 bid_bytes = msg_buf.SerializeToString()
                 payload_hex = str.format('{:02x}', MessageTypes.ADS_BID_LF) + bid_bytes.hex()
@@ -2494,11 +2492,11 @@ class BasicSwap(BaseApp):
                     active_ind=1,
                     bid_id=xmr_swap.bid_id,
                     offer_id=offer_id,
-                    amount=msg_buf.amount,
-                    rate=msg_buf.rate,
+                    amount=msg_buf.amount_to,
+                    rate=reversed_rate,
                     created_at=bid_created_at,
                     contract_count=xmr_swap.contract_count,
-                    amount_to=(msg_buf.amount * msg_buf.rate) // ci_to.COIN(),
+                    amount_to=msg_buf.amount_from,
                     expire_at=bid_created_at + msg_buf.time_valid,
                     bid_addr=bid_addr,
                     was_sent=True,
@@ -2507,13 +2505,12 @@ class BasicSwap(BaseApp):
 
                 bid.setState(BidStates.BID_REQUEST_SENT)
 
+                session = self.openSession()
                 try:
-                    session = scoped_session(self.session_factory)
                     self.saveBidInSession(xmr_swap.bid_id, bid, session, xmr_swap)
                     session.commit()
                 finally:
-                    session.close()
-                    session.remove()
+                    self.closeSession(session, commit=False)
 
                 self.log.info('Sent ADS_BID_LF %s', xmr_swap.bid_id.hex())
                 return xmr_swap.bid_id
@@ -3922,6 +3919,10 @@ class BasicSwap(BaseApp):
             ensure(bid, 'Bid not found: {}.'.format(bid_id.hex()))
             ensure(xmr_swap, 'Adaptor-sig swap not found: {}.'.format(bid_id.hex()))
 
+            if BidStates(bid.state) == BidStates.BID_STALLED_FOR_TEST:
+                self.log.debug('Bid stalled %s', bid_id.hex())
+                return
+
             offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False)
             ensure(offer, 'Offer not found: {}.'.format(bid.offer_id.hex()))
             ensure(xmr_offer, 'Adaptor-sig offer not found: {}.'.format(bid.offer_id.hex()))
@@ -4466,7 +4467,7 @@ class BasicSwap(BaseApp):
                 raise AutomationConstraint('Bidder has too many failed swaps')
         return True
 
-    def shouldAutoAcceptBid(self, offer, bid, session=None) -> bool:
+    def shouldAutoAcceptBid(self, offer, bid, session=None, options={}) -> bool:
         try:
             use_session = self.openSession(session)
 
@@ -4481,30 +4482,22 @@ class BasicSwap(BaseApp):
             bid_amount: int = bid.amount
             bid_rate: int = bid.rate
 
-            reverse_bid: bool = coin_from in self.scriptless_coins
-            if reverse_bid:
-                bid_amount, bid_rate = xmr_swap_1.reverseBidAmountAndRate(self, bid, offer)
+            if options.get('reverse_bid', False):
+                bid_amount = bid.amount_to
+                bid_rate = options.get('bid_rate')
 
             self.log.debug('Evaluating against strategy {}'.format(strategy.record_id))
 
             if not offer.amount_negotiable:
-                if reverse_bid:
-                    if abs(bid_amount - offer.amount_from) >= 20:  # TODO: Tolerance?
-                        raise AutomationConstraint('Need exact amount match')
-                else:
-                    if bid_amount != offer.amount_from:
-                        raise AutomationConstraint('Need exact amount match')
+                if bid_amount != offer.amount_from:
+                    raise AutomationConstraint('Need exact amount match')
 
             if bid_amount < offer.min_bid_amount:
                 raise AutomationConstraint('Bid amount below offer minimum')
 
             if opts.get('exact_rate_only', False) is True:
-                if reverse_bid:
-                    if abs(bid_rate - offer.rate) >= 20:  # TODO: Tolerance?
-                        raise AutomationConstraint('Need exact rate match')
-                else:
-                    if bid_rate != offer.rate:
-                        raise AutomationConstraint('Need exact rate match')
+                if bid_rate != offer.rate:
+                    raise AutomationConstraint('Need exact rate match')
 
             active_bids, total_bids_value = self.getCompletedAndActiveBidsValue(offer, use_session)
 
@@ -4772,7 +4765,7 @@ class BasicSwap(BaseApp):
 
         bid.setState(BidStates.BID_RECEIVED)
 
-        if self.shouldAutoAcceptBid(offer, bid, session) or reverse_bid:
+        if reverse_bid or self.shouldAutoAcceptBid(offer, bid, session):
             delay = random.randrange(self.min_delay_event, self.max_delay_event)
             self.log.info('Auto accepting %sadaptor-sig bid %s in %d seconds', 'reverse ' if reverse_bid else '', bid.bid_id.hex(), delay)
             self.createActionInSession(delay, ActionTypes.ACCEPT_XMR_BID, bid.bid_id, session)
@@ -5135,13 +5128,14 @@ class BasicSwap(BaseApp):
         xmr_swap.a_lock_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_spend_tx)
         prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap)
         xmr_swap.al_lock_spend_tx_esig = ci_from.signTxOtVES(kal, xmr_swap.pkasf, xmr_swap.a_lock_spend_tx, 0, xmr_swap.a_lock_tx_script, prevout_amount)
-
+        '''
         # Double check a_lock_spend_tx is valid
+        # Fails for part_blind
         ci_from.verifySCLockSpendTx(
             xmr_swap.a_lock_spend_tx,
             xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script,
             xmr_swap.dest_af, a_fee_rate, xmr_swap.vkbv)
-
+        '''
         delay = random.randrange(self.min_delay_event_short, self.max_delay_event_short)
         self.log.info('Sending lock spend tx message for bid %s in %d seconds', bid_id.hex(), delay)
         self.createActionInSession(delay, ActionTypes.SEND_XMR_SWAP_LOCK_SPEND_MSG, bid_id, session)
@@ -5438,7 +5432,7 @@ class BasicSwap(BaseApp):
         try:
             if offer.coin_to == Coins.XMR:
                 address_to = self.getCachedMainWalletAddress(ci_to)
-            elif coin_to == Coins.PART_BLIND:
+            elif coin_to in (Coins.PART_BLIND, Coins.PART_ANON):
                 address_to = self.getCachedStealthAddressForCoin(coin_to)
             else:
                 address_to = self.getReceiveAddressFromPool(coin_to, bid_id, TxTypes.XMR_SWAP_B_LOCK_REFUND)
@@ -5739,22 +5733,14 @@ class BasicSwap(BaseApp):
         self.validateBidValidTime(offer.swap_type, offer.coin_from, offer.coin_to, bid_data.time_valid)
         ensure(now <= msg['sent'] + bid_data.time_valid, 'Bid expired')
 
-        # Reverse the rate and amount to test against the offer
-        amount_from: int = bid_data.amount
-        amount_to: int = int((int(amount_from) * bid_data.rate) // ci_from.COIN())
-        reversed_rate: int = ci_to.make_int(amount_from / amount_to, r=1)
+        amount_from: int = bid_data.amount_from
+        amount_to: int = (bid_data.amount_from * bid_data.rate) // ci_to.COIN()
+        ensure(abs(amount_to - bid_data.amount_to) < 10, 'invalid bid amount_to')  # TODO: Tolerance?
+        reversed_rate: int = ci_from.make_int(amount_from / bid_data.amount_to, r=1)
+        amount_from_recovered: int = int((amount_to * reversed_rate) // ci_from.COIN())
+        ensure(abs(amount_from - amount_from_recovered) < 10, 'invalid bid amount_from')  # TODO: Tolerance?
 
-        # Snap the amount to the offer amount if it's close enough
-        amount_to_check = amount_to
-        if abs(amount_to_check - offer.min_bid_amount) < 10:
-            amount_to_check = offer.min_bid_amount
-
-        reversed_rate_check = reversed_rate
-        # Snap the rate to the rate if it's close enough
-        if abs(reversed_rate_check - offer.rate) < 10:
-            reversed_rate_check = offer.rate
-
-        self.validateBidAmount(offer, amount_to_check, reversed_rate_check)
+        self.validateBidAmount(offer, amount_from, bid_data.rate)
 
         bid_id = bytes.fromhex(msg['msgid'])
 
@@ -5765,10 +5751,10 @@ class BasicSwap(BaseApp):
                 bid_id=bid_id,
                 offer_id=offer_id,
                 protocol_version=bid_data.protocol_version,
-                amount=bid_data.amount,
-                rate=bid_data.rate,
+                amount=amount_to,
+                rate=reversed_rate,
                 created_at=msg['sent'],
-                amount_to=(bid_data.amount * bid_data.rate) // ci_from.COIN(),
+                amount_to=amount_from,
                 expire_at=msg['sent'] + bid_data.time_valid,
                 bid_addr=msg['from'],
                 was_sent=False,
@@ -5799,7 +5785,8 @@ class BasicSwap(BaseApp):
             session = self.openSession()
             self.notify(NT.BID_RECEIVED, {'type': 'as_reversed', 'bid_id': bid.bid_id.hex(), 'offer_id': bid.offer_id.hex()}, session)
 
-            if self.shouldAutoAcceptBid(offer, bid, session):
+            options = {'reverse_bid': True, 'bid_rate': bid_data.rate}
+            if self.shouldAutoAcceptBid(offer, bid, session, options=options):
                 delay = random.randrange(self.min_delay_event, self.max_delay_event)
                 self.log.info('Auto accepting reverse adaptor-sig bid %s in %d seconds', bid.bid_id.hex(), delay)
                 self.createActionInSession(delay, ActionTypes.ACCEPT_AS_REV_BID, bid.bid_id, session)
@@ -6336,10 +6323,9 @@ class BasicSwap(BaseApp):
 
     def addWalletInfoRecord(self, coin, info_type, wi) -> None:
         coin_id = int(coin)
-        self.mxDB.acquire()
+        session = self.openSession()
         try:
             now: int = self.getTime()
-            session = scoped_session(self.session_factory)
             session.add(Wallets(coin_id=coin, balance_type=info_type, wallet_data=json.dumps(wi), created_at=now))
             query_str = f'DELETE FROM wallets WHERE (coin_id = {coin_id} AND balance_type = {info_type}) AND record_id NOT IN (SELECT record_id FROM wallets WHERE coin_id = {coin_id} AND balance_type = {info_type} ORDER BY created_at DESC LIMIT 3 )'
             session.execute(query_str)
@@ -6347,9 +6333,7 @@ class BasicSwap(BaseApp):
         except Exception as e:
             self.log.error(f'addWalletInfoRecord {e}')
         finally:
-            session.close()
-            session.remove()
-            self.mxDB.release()
+            self.closeSession(session, commit=False)
 
     def updateWalletInfo(self, coin) -> None:
         # Store wallet info to db so it's available after startup
@@ -6452,25 +6436,21 @@ class BasicSwap(BaseApp):
         return rv
 
     def countAcceptedBids(self, offer_id: bytes = None) -> int:
-        self.mxDB.acquire()
+        session = self.openSession()
         try:
-            session = scoped_session(self.session_factory)
             if offer_id:
                 q = session.execute('SELECT COUNT(*) FROM bids WHERE state >= {} AND offer_id = x\'{}\''.format(BidStates.BID_ACCEPTED, offer_id.hex())).first()
             else:
                 q = session.execute('SELECT COUNT(*) FROM bids WHERE state >= {}'.format(BidStates.BID_ACCEPTED)).first()
             return q[0]
         finally:
-            session.close()
-            session.remove()
-            self.mxDB.release()
+            self.closeSession(session, commit=False)
 
     def listOffers(self, sent: bool = False, filters={}, with_bid_info: bool = False):
-        self.mxDB.acquire()
+        session = self.openSession()
         try:
             rv = []
             now: int = self.getTime()
-            session = scoped_session(self.session_factory)
 
             if with_bid_info:
                 subquery = session.query(sa.func.sum(Bid.amount).label('completed_bid_amount')).filter(sa.and_(Bid.offer_id == Offer.offer_id, Bid.state == BidStates.SWAP_COMPLETED)).correlate(Offer).scalar_subquery()
@@ -6533,9 +6513,7 @@ class BasicSwap(BaseApp):
                     rv.append(offer)
             return rv
         finally:
-            session.close()
-            session.remove()
-            self.mxDB.release()
+            self.closeSession(session, commit=False)
 
     def activeBidsQueryStr(self, now: int, offer_table: str = 'offers', bids_table: str = 'bids') -> str:
         offers_inset = f' AND {offer_table}.expire_at > {now}' if offer_table != '' else ''
@@ -6544,11 +6522,10 @@ class BasicSwap(BaseApp):
         return f' ({bids_table}.state NOT IN ({inactive_states_str}) AND ({bids_table}.state > {BidStates.BID_RECEIVED} OR ({bids_table}.expire_at > {now}{offers_inset}))) '
 
     def listBids(self, sent: bool = False, offer_id: bytes = None, for_html: bool = False, filters={}):
-        self.mxDB.acquire()
+        session = self.openSession()
         try:
             rv = []
             now: int = self.getTime()
-            session = scoped_session(self.session_factory)
 
             query_str = 'SELECT bids.created_at, bids.expire_at, bids.bid_id, bids.offer_id, bids.amount, bids.state, bids.was_received, tx1.state, tx2.state, offers.coin_from, bids.rate, bids.bid_addr FROM bids ' + \
                         'LEFT JOIN offers ON offers.offer_id = bids.offer_id ' + \
@@ -6594,9 +6571,7 @@ class BasicSwap(BaseApp):
                 rv.append(row)
             return rv
         finally:
-            session.close()
-            session.remove()
-            self.mxDB.release()
+            self.closeSession(session, commit=False)
 
     def listSwapsInProgress(self, for_html=False):
         self.mxDB.acquire()
@@ -6795,9 +6770,8 @@ class BasicSwap(BaseApp):
                 self.closeSession(use_session)
 
     def addSMSGAddress(self, pubkey_hex: str, addressnote: str = None) -> None:
-        self.mxDB.acquire()
+        session = self.openSession()
         try:
-            session = scoped_session(self.session_factory)
             now: int = self.getTime()
             ci = self.ci(Coins.PART)
             add_addr = ci.pubkey_to_address(bytes.fromhex(pubkey_hex))
@@ -6805,26 +6779,20 @@ class BasicSwap(BaseApp):
             self.callrpc('smsglocalkeys', ['anon', '-', add_addr])
 
             session.add(SmsgAddress(addr=add_addr, use_type=AddressTypes.SEND_OFFER, active_ind=1, created_at=now, note=addressnote, pubkey=pubkey_hex))
-            session.commit()
             return add_addr
         finally:
-            session.close()
-            session.remove()
-            self.mxDB.release()
+            self.closeSession(session)
 
     def editSMSGAddress(self, address: str, active_ind: int, addressnote: str) -> None:
-        self.mxDB.acquire()
+        session = self.openSession()
         try:
-            session = scoped_session(self.session_factory)
             mode = '-' if active_ind == 0 else '+'
             self.callrpc('smsglocalkeys', ['recv', mode, address])
 
             session.execute('UPDATE smsgaddresses SET active_ind = :active_ind, note = :note WHERE addr = :addr', {'active_ind': active_ind, 'note': addressnote, 'addr': address})
             session.commit()
         finally:
-            session.close()
-            session.remove()
-            self.mxDB.release()
+            self.closeSession(session)
 
     def createCoinALockRefundSwipeTx(self, ci, bid, offer, xmr_swap, xmr_offer):
         self.log.debug('Creating %s lock refund swipe tx', ci.coin_name())
@@ -6887,18 +6855,15 @@ class BasicSwap(BaseApp):
         self.swaps_in_progress[bid.bid_id] = (bid, swap_in_progress[1])
 
     def getAddressLabel(self, addresses):
-        self.mxDB.acquire()
+        session = self.openSession()
         try:
-            session = scoped_session(self.session_factory)
             rv = []
             for a in addresses:
                 v = session.query(KnownIdentity).filter_by(address=a).first()
                 rv.append('' if (not v or not v.label) else v.label)
             return rv
         finally:
-            session.close()
-            session.remove()
-            self.mxDB.release()
+            self.closeSession(session, commit=False)
 
     def add_connection(self, host, port, peer_pubkey):
         self.log.info('add_connection %s %d %s', host, port, peer_pubkey.hex())
diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py
index adfdfb9..2fd0141 100644
--- a/basicswap/interface/xmr.py
+++ b/basicswap/interface/xmr.py
@@ -367,6 +367,10 @@ class XMRInterface(CoinInterface):
             return None
 
     def spendBLockTx(self, chain_b_lock_txid, address_to, kbv, kbs, cb_swap_value, b_fee_rate, restore_height, spend_actual_balance=False):
+        '''
+        Notes:
+        "Error: No unlocked balance in the specified subaddress(es)" can mean not enough funds after tx fee.
+        '''
         with self._mx_wallet:
             Kbv = self.getPubkey(kbv)
             Kbs = self.getPubkey(kbs)
@@ -390,7 +394,6 @@ class XMRInterface(CoinInterface):
 
             self.rpc_wallet_cb('refresh')
             rv = self.rpc_wallet_cb('get_balance')
-
             if rv['balance'] < cb_swap_value:
                 self._log.warning('Balance is too low, checking for existing spend.')
                 txns = self.rpc_wallet_cb('get_transfers', {'out': True})
diff --git a/basicswap/messages.proto b/basicswap/messages.proto
index ab3ab0b..73be986 100644
--- a/basicswap/messages.proto
+++ b/basicswap/messages.proto
@@ -40,8 +40,6 @@ message BidMessage {
     bytes offer_msg_id = 1;
     uint64 time_valid = 2;          /* seconds bid is valid for */
     uint64 amount = 3;              /* amount of amount_from bid is for */
-
-    /* optional */
     uint64 rate = 4;
     bytes pkhash_buyer = 5;         /* buyer's address to receive amount_from */
     string proof_address = 6;
@@ -136,10 +134,11 @@ message ADSBidIntentMessage {
     /* L -> F Sent from bidder, construct a reverse bid */
     bytes offer_msg_id = 1;
     uint64 time_valid = 2;          /* seconds bid is valid for */
-    uint64 amount = 3;              /* amount of amount_from bid is for */
-    uint64 rate = 4;
+    uint64 amount_from = 3;         /* amount of offer.coin_from bid is for */
+    uint64 amount_to = 4;           /* amount of offer.coin_to bid is for, equivalent to bid.amount */
+    uint64 rate = 5;                /* amount of offer.coin_from bid is for */
 
-    uint32 protocol_version = 5;
+    uint32 protocol_version = 6;
 }
 
 message ADSBidIntentAcceptMessage {
diff --git a/basicswap/messages_pb2.py b/basicswap/messages_pb2.py
index b42ef59..7399d9e 100644
--- a/basicswap/messages_pb2.py
+++ b/basicswap/messages_pb2.py
@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
 
 
 
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0emessages.proto\x12\tbasicswap\"\xa6\x04\n\x0cOfferMessage\x12\x11\n\tcoin_from\x18\x01 \x01(\r\x12\x0f\n\x07\x63oin_to\x18\x02 \x01(\r\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x16\n\x0emin_bid_amount\x18\x05 \x01(\x04\x12\x12\n\ntime_valid\x18\x06 \x01(\x04\x12\x33\n\tlock_type\x18\x07 \x01(\x0e\x32 .basicswap.OfferMessage.LockType\x12\x12\n\nlock_value\x18\x08 \x01(\r\x12\x11\n\tswap_type\x18\t \x01(\r\x12\x15\n\rproof_address\x18\n \x01(\t\x12\x17\n\x0fproof_signature\x18\x0b \x01(\t\x12\x15\n\rpkhash_seller\x18\x0c \x01(\x0c\x12\x13\n\x0bsecret_hash\x18\r \x01(\x0c\x12\x15\n\rfee_rate_from\x18\x0e \x01(\x04\x12\x13\n\x0b\x66\x65\x65_rate_to\x18\x0f \x01(\x04\x12\x18\n\x10protocol_version\x18\x10 \x01(\r\x12\x19\n\x11\x61mount_negotiable\x18\x11 \x01(\x08\x12\x17\n\x0frate_negotiable\x18\x12 \x01(\x08\"q\n\x08LockType\x12\x0b\n\x07NOT_SET\x10\x00\x12\x18\n\x14SEQUENCE_LOCK_BLOCKS\x10\x01\x12\x16\n\x12SEQUENCE_LOCK_TIME\x10\x02\x12\x13\n\x0f\x41\x42S_LOCK_BLOCKS\x10\x03\x12\x11\n\rABS_LOCK_TIME\x10\x04\"\xb4\x01\n\nBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x05 \x01(\x0c\x12\x15\n\rproof_address\x18\x06 \x01(\t\x12\x17\n\x0fproof_signature\x18\x07 \x01(\t\x12\x18\n\x10protocol_version\x18\x08 \x01(\r\"V\n\x10\x42idAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x15\n\rinitiate_txid\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63ontract_script\x18\x03 \x01(\x0c\"=\n\x12OfferRevokeMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\";\n\x10\x42idRejectMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x13\n\x0breject_code\x18\x02 \x01(\r\"\xb2\x01\n\rXmrBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x0c\n\x04pkaf\x18\x05 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x06 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x07 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x08 \x01(\x0c\x12\x18\n\x10protocol_version\x18\t \x01(\r\"T\n\x0fXmrSplitMessage\x12\x0e\n\x06msg_id\x18\x01 \x01(\x0c\x12\x10\n\x08msg_type\x18\x02 \x01(\r\x12\x10\n\x08sequence\x18\x03 \x01(\r\x12\r\n\x05\x64leag\x18\x04 \x01(\x0c\"\x80\x02\n\x13XmrBidAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkal\x18\x03 \x01(\x0c\x12\x0c\n\x04kbvl\x18\x04 \x01(\x0c\x12\x12\n\nkbsl_dleag\x18\x05 \x01(\x0c\x12\x11\n\ta_lock_tx\x18\x06 \x01(\x0c\x12\x18\n\x10\x61_lock_tx_script\x18\x07 \x01(\x0c\x12\x18\n\x10\x61_lock_refund_tx\x18\x08 \x01(\x0c\x12\x1f\n\x17\x61_lock_refund_tx_script\x18\t \x01(\x0c\x12\x1e\n\x16\x61_lock_refund_spend_tx\x18\n \x01(\x0c\x12\x1d\n\x15\x61l_lock_refund_tx_sig\x18\x0b \x01(\x0c\"r\n\x17XmrBidLockTxSigsMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12$\n\x1c\x61\x66_lock_refund_spend_tx_esig\x18\x02 \x01(\x0c\x12\x1d\n\x15\x61\x66_lock_refund_tx_sig\x18\x03 \x01(\x0c\"X\n\x18XmrBidLockSpendTxMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x17\n\x0f\x61_lock_spend_tx\x18\x02 \x01(\x0c\x12\x0f\n\x07kal_sig\x18\x03 \x01(\x0c\"M\n\x18XmrBidLockReleaseMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x1d\n\x15\x61l_lock_spend_tx_esig\x18\x02 \x01(\x0c\"w\n\x13\x41\x44SBidIntentMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x18\n\x10protocol_version\x18\x05 \x01(\r\"p\n\x19\x41\x44SBidIntentAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkaf\x18\x02 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x03 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x04 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x05 \x01(\x0c\x62\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0emessages.proto\x12\tbasicswap\"\xa6\x04\n\x0cOfferMessage\x12\x11\n\tcoin_from\x18\x01 \x01(\r\x12\x0f\n\x07\x63oin_to\x18\x02 \x01(\r\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x16\n\x0emin_bid_amount\x18\x05 \x01(\x04\x12\x12\n\ntime_valid\x18\x06 \x01(\x04\x12\x33\n\tlock_type\x18\x07 \x01(\x0e\x32 .basicswap.OfferMessage.LockType\x12\x12\n\nlock_value\x18\x08 \x01(\r\x12\x11\n\tswap_type\x18\t \x01(\r\x12\x15\n\rproof_address\x18\n \x01(\t\x12\x17\n\x0fproof_signature\x18\x0b \x01(\t\x12\x15\n\rpkhash_seller\x18\x0c \x01(\x0c\x12\x13\n\x0bsecret_hash\x18\r \x01(\x0c\x12\x15\n\rfee_rate_from\x18\x0e \x01(\x04\x12\x13\n\x0b\x66\x65\x65_rate_to\x18\x0f \x01(\x04\x12\x18\n\x10protocol_version\x18\x10 \x01(\r\x12\x19\n\x11\x61mount_negotiable\x18\x11 \x01(\x08\x12\x17\n\x0frate_negotiable\x18\x12 \x01(\x08\"q\n\x08LockType\x12\x0b\n\x07NOT_SET\x10\x00\x12\x18\n\x14SEQUENCE_LOCK_BLOCKS\x10\x01\x12\x16\n\x12SEQUENCE_LOCK_TIME\x10\x02\x12\x13\n\x0f\x41\x42S_LOCK_BLOCKS\x10\x03\x12\x11\n\rABS_LOCK_TIME\x10\x04\"\xb4\x01\n\nBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x05 \x01(\x0c\x12\x15\n\rproof_address\x18\x06 \x01(\t\x12\x17\n\x0fproof_signature\x18\x07 \x01(\t\x12\x18\n\x10protocol_version\x18\x08 \x01(\r\"V\n\x10\x42idAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x15\n\rinitiate_txid\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63ontract_script\x18\x03 \x01(\x0c\"=\n\x12OfferRevokeMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\";\n\x10\x42idRejectMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x13\n\x0breject_code\x18\x02 \x01(\r\"\xb2\x01\n\rXmrBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x0c\n\x04pkaf\x18\x05 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x06 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x07 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x08 \x01(\x0c\x12\x18\n\x10protocol_version\x18\t \x01(\r\"T\n\x0fXmrSplitMessage\x12\x0e\n\x06msg_id\x18\x01 \x01(\x0c\x12\x10\n\x08msg_type\x18\x02 \x01(\r\x12\x10\n\x08sequence\x18\x03 \x01(\r\x12\r\n\x05\x64leag\x18\x04 \x01(\x0c\"\x80\x02\n\x13XmrBidAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkal\x18\x03 \x01(\x0c\x12\x0c\n\x04kbvl\x18\x04 \x01(\x0c\x12\x12\n\nkbsl_dleag\x18\x05 \x01(\x0c\x12\x11\n\ta_lock_tx\x18\x06 \x01(\x0c\x12\x18\n\x10\x61_lock_tx_script\x18\x07 \x01(\x0c\x12\x18\n\x10\x61_lock_refund_tx\x18\x08 \x01(\x0c\x12\x1f\n\x17\x61_lock_refund_tx_script\x18\t \x01(\x0c\x12\x1e\n\x16\x61_lock_refund_spend_tx\x18\n \x01(\x0c\x12\x1d\n\x15\x61l_lock_refund_tx_sig\x18\x0b \x01(\x0c\"r\n\x17XmrBidLockTxSigsMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12$\n\x1c\x61\x66_lock_refund_spend_tx_esig\x18\x02 \x01(\x0c\x12\x1d\n\x15\x61\x66_lock_refund_tx_sig\x18\x03 \x01(\x0c\"X\n\x18XmrBidLockSpendTxMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x17\n\x0f\x61_lock_spend_tx\x18\x02 \x01(\x0c\x12\x0f\n\x07kal_sig\x18\x03 \x01(\x0c\"M\n\x18XmrBidLockReleaseMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x1d\n\x15\x61l_lock_spend_tx_esig\x18\x02 \x01(\x0c\"\x8f\x01\n\x13\x41\x44SBidIntentMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x11\n\tamount_to\x18\x04 \x01(\x04\x12\x0c\n\x04rate\x18\x05 \x01(\x04\x12\x18\n\x10protocol_version\x18\x06 \x01(\r\"p\n\x19\x41\x44SBidIntentAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkaf\x18\x02 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x03 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x04 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x05 \x01(\x0c\x62\x06proto3')
 
 _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
 _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'messages_pb2', globals())
@@ -44,8 +44,8 @@ if _descriptor._USE_C_DESCRIPTORS == False:
   _XMRBIDLOCKSPENDTXMESSAGE._serialized_end=1707
   _XMRBIDLOCKRELEASEMESSAGE._serialized_start=1709
   _XMRBIDLOCKRELEASEMESSAGE._serialized_end=1786
-  _ADSBIDINTENTMESSAGE._serialized_start=1788
-  _ADSBIDINTENTMESSAGE._serialized_end=1907
-  _ADSBIDINTENTACCEPTMESSAGE._serialized_start=1909
-  _ADSBIDINTENTACCEPTMESSAGE._serialized_end=2021
+  _ADSBIDINTENTMESSAGE._serialized_start=1789
+  _ADSBIDINTENTMESSAGE._serialized_end=1932
+  _ADSBIDINTENTACCEPTMESSAGE._serialized_start=1934
+  _ADSBIDINTENTACCEPTMESSAGE._serialized_end=2046
 # @@protoc_insertion_point(module_scope)
diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py
index b960615..0407bcb 100644
--- a/tests/basicswap/test_btc_xmr.py
+++ b/tests/basicswap/test_btc_xmr.py
@@ -22,6 +22,7 @@ from basicswap.util import (
     make_int,
     format_amount,
 )
+from basicswap.interface import Curves
 from tests.basicswap.util import (
     read_json_api,
 )
@@ -51,11 +52,24 @@ from .test_xmr import BaseTest, test_delay_event, callnoderpc
 logger = logging.getLogger()
 
 
-class BasicSwapTest(BaseTest):
+class TestFunctions(BaseTest):
     base_rpc_port = None
 
-    def getBalance(self, js_wallets, coin):
-        return float(js_wallets[coin.name]['balance']) + float(js_wallets[coin.name]['unconfirmed'])
+    def getBalance(self, js_wallets, coin) -> float:
+        if coin == Coins.PART_BLIND:
+            coin_ticker: str = 'PART'
+            balance_type: str = 'blind_balance'
+            unconfirmed_name: str = 'blind_unconfirmed'
+        elif coin == Coins.PART_ANON:
+            coin_ticker: str = 'PART'
+            balance_type: str = 'anon_balance'
+            unconfirmed_name: str = 'anon_pending'
+        else:
+            coin_ticker: str = coin.name
+            balance_type: str = 'balance'
+            unconfirmed_name: str = 'unconfirmed'
+
+        return float(js_wallets[coin_ticker][balance_type]) + float(js_wallets[coin_ticker][unconfirmed_name])
 
     def callnoderpc(self, method, params=[], wallet=None, node_id=0):
         return callnoderpc(node_id, method, params, wallet, self.base_rpc_port)
@@ -63,17 +77,276 @@ class BasicSwapTest(BaseTest):
     def mineBlock(self, num_blocks=1):
         self.callnoderpc('generatetoaddress', [num_blocks, self.btc_addr])
 
-    def prepare_balance(self, coin_ticker: str, amount: float, port_target_node: int, port_take_from_node: int) -> None:
+    def prepare_balance(self, coin, amount: float, port_target_node: int, port_take_from_node: int) -> None:
+        if coin == Coins.PART_BLIND:
+            coin_ticker: str = 'PART'
+            balance_type: str = 'blind_balance'
+            address_type: str = 'stealth_address'
+            type_to: str = 'blind'
+        elif coin == Coins.PART_ANON:
+            coin_ticker: str = 'PART'
+            balance_type: str = 'anon_balance'
+            address_type: str = 'stealth_address'
+            type_to: str = 'anon'
+        else:
+            coin_ticker: str = coin.name
+            balance_type: str = 'balance'
+            address_type: str = 'deposit_address'
         js_w = read_json_api(port_target_node, 'wallets')
-        if float(js_w[coin_ticker]['balance']) < amount:
-            post_json = {
-                'value': amount,
-                'address': js_w[coin_ticker]['deposit_address'],
-                'subfee': False,
-            }
-            json_rv = read_json_api(port_take_from_node, 'wallets/{}/withdraw'.format(coin_ticker.lower()), post_json)
-            assert (len(json_rv['txid']) == 64)
-            wait_for_balance(test_delay_event, 'http://127.0.0.1:{}/json/wallets/{}'.format(port_target_node, coin_ticker.lower()), 'balance', amount)
+        if float(js_w[coin_ticker][balance_type]) >= amount:
+            return
+        post_json = {
+            'value': amount,
+            'address': js_w[coin_ticker][address_type],
+            'subfee': False,
+        }
+        if coin in (Coins.PART_BLIND, Coins.PART_ANON):
+            post_json['type_to'] = type_to
+        json_rv = read_json_api(port_take_from_node, 'wallets/{}/withdraw'.format(coin_ticker.lower()), post_json)
+        assert (len(json_rv['txid']) == 64)
+        wait_for_balance(test_delay_event, 'http://127.0.0.1:{}/json/wallets/{}'.format(port_target_node, coin_ticker.lower()), balance_type, amount)
+
+    def do_test_01_full_swap(self, coin_from: Coins, coin_to: Coins) -> None:
+        logging.info('---------- Test {} to {}'.format(coin_from.name, coin_to.name))
+
+        swap_clients = self.swap_clients
+        reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins
+        ci_from = swap_clients[0].ci(coin_from)
+        ci_to = swap_clients[1].ci(coin_to)
+        ci_part0 = swap_clients[0].ci(Coins.PART)
+        ci_part1 = swap_clients[1].ci(Coins.PART)
+
+        # Offerer sends the offer
+        # Bidder sends the bid
+        id_offerer: int = 0
+        id_bidder: int = 1
+
+        # Leader sends the initial (chain a) lock tx.
+        # Follower sends the participate (chain b) lock tx.
+        id_leader: int = 1 if reverse_bid else 0
+        id_follower: int = 0 if reverse_bid else 1
+        logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}')
+
+        js_0 = read_json_api(1800, 'wallets')
+        node0_from_before: float = self.getBalance(js_0, coin_from)
+
+        js_1 = read_json_api(1801, 'wallets')
+        node1_from_before: float = self.getBalance(js_1, coin_from)
+
+        node0_sent_messages_before: int = ci_part0.rpc_callback('smsgoutbox', ['count',])['num_messages']
+        node1_sent_messages_before: int = ci_part1.rpc_callback('smsgoutbox', ['count',])['num_messages']
+
+        amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
+        rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
+        offer_id = swap_clients[id_offerer].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP)
+        wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id)
+        offer = swap_clients[id_bidder].listOffers(filters={'offer_id': offer_id})[0]
+        assert (offer.offer_id == offer_id)
+
+        bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from)
+
+        wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED)
+        swap_clients[id_offerer].acceptBid(bid_id)
+
+        wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.SWAP_COMPLETED, wait_for=180)
+        wait_for_bid(test_delay_event, swap_clients[id_bidder], bid_id, BidStates.SWAP_COMPLETED, sent=True)
+
+        amount_from = float(ci_from.format_amount(amt_swap))
+        js_1_after = read_json_api(1801, 'wallets')
+        node1_from_after = self.getBalance(js_1_after, coin_from)
+        if coin_from is not Coins.PART:  # TODO: staking
+            assert (node1_from_after > node1_from_before + (amount_from - 0.05))
+
+        js_0_after = read_json_api(1800, 'wallets')
+        node0_from_after: float = self.getBalance(js_0_after, coin_from)
+        # TODO: Discard block rewards
+        # assert (node0_from_after < node0_from_before - amount_from)
+
+        scale_from = 8
+        amount_to = int((amt_swap * rate_swap) // (10 ** scale_from))
+        amount_to_float = float(ci_to.format_amount(amount_to))
+        node1_to_after: float = self.getBalance(js_1_after, coin_to)
+        node1_to_before: float = self.getBalance(js_1, coin_to)
+        if False:  # TODO: set stakeaddress and xmr rewards to non wallet addresses
+            assert (node1_to_after < node1_to_before - amount_to_float)
+
+        node0_sent_messages_after: int = ci_part0.rpc_callback('smsgoutbox', ['count',])['num_messages']
+        node1_sent_messages_after: int = ci_part1.rpc_callback('smsgoutbox', ['count',])['num_messages']
+        node0_sent_messages: int = node0_sent_messages_after - node0_sent_messages_before
+        node1_sent_messages: int = node1_sent_messages_after - node1_sent_messages_before
+        split_msgs: int = 2 if (ci_from.curve_type() != Curves.secp256k1 or ci_to.curve_type() != Curves.secp256k1) else 0
+        assert (node0_sent_messages == (3 + split_msgs if reverse_bid else 4 + split_msgs))
+        assert (node1_sent_messages == (4 + split_msgs if reverse_bid else 2 + split_msgs))
+
+    def do_test_02_leader_recover_a_lock_tx(self, coin_from: Coins, coin_to: Coins) -> None:
+        logging.info('---------- Test {} to {} leader recovers coin a lock tx'.format(coin_from.name, coin_to.name))
+
+        swap_clients = self.swap_clients
+        reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins
+        ci_from = swap_clients[0].ci(coin_from)
+        ci_to = swap_clients[0].ci(coin_to)
+
+        id_offerer: int = 0
+        id_bidder: int = 1
+        id_leader: int = 1 if reverse_bid else 0
+        id_follower: int = 0 if reverse_bid else 1
+        logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}')
+
+        js_wl_before = read_json_api(1800 + id_leader, 'wallets')
+        wl_from_before = self.getBalance(js_wl_before, coin_from)
+
+        amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
+        rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
+        offer_id = swap_clients[id_offerer].postOffer(
+            coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
+            lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32)
+        wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id)
+        offer = swap_clients[id_bidder].getOffer(offer_id)
+
+        bid_id = swap_clients[id_bidder].postXmrBid(offer_id, offer.amount_from)
+        wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED)
+
+        swap_clients[id_follower].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK)
+        swap_clients[id_offerer].acceptBid(bid_id)
+
+        leader_sent_bid: bool = True if reverse_bid else False
+        wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=leader_sent_bid, wait_for=180)
+        wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, [BidStates.BID_STALLED_FOR_TEST, BidStates.XMR_SWAP_FAILED], sent=(not leader_sent_bid))
+
+        js_wl_after = read_json_api(1800 + id_leader, 'wallets')
+        wl_from_after = self.getBalance(js_wl_after, coin_from)
+
+        # TODO: Discard block rewards
+        # assert (node0_from_before - node0_from_after < 0.02)
+
+    def do_test_03_follower_recover_a_lock_tx(self, coin_from, coin_to, lock_value=32):
+        logging.info('---------- Test {} to {} follower recovers coin a lock tx'.format(coin_from.name, coin_to.name))
+
+        # Leader is too slow to recover the coin a lock tx and follower swipes it
+        # coin b lock tx remains unspent
+
+        swap_clients = self.swap_clients
+        reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins
+        ci_from = swap_clients[0].ci(coin_from)
+        ci_to = swap_clients[0].ci(coin_to)
+
+        id_offerer: int = 0
+        id_bidder: int = 1
+        id_leader: int = 1 if reverse_bid else 0
+        id_follower: int = 0 if reverse_bid else 1
+        logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}')
+
+        js_w0_before = read_json_api(1800, 'wallets')
+        js_w1_before = read_json_api(1801, 'wallets')
+
+        amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
+        rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
+        offer_id = swap_clients[id_offerer].postOffer(
+            coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
+            lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=lock_value)
+        wait_for_offer(test_delay_event, swap_clients[1], offer_id)
+        offer = swap_clients[id_bidder].getOffer(offer_id)
+
+        bid_id = swap_clients[id_bidder].postXmrBid(offer_id, offer.amount_from)
+        wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED)
+
+        swap_clients[id_follower].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK)
+        swap_clients[id_leader].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND)
+
+        swap_clients[id_offerer].acceptBid(bid_id)
+
+        leader_sent_bid: bool = True if reverse_bid else False
+        wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.BID_STALLED_FOR_TEST, wait_for=180, sent=leader_sent_bid)
+        wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=80, sent=(not leader_sent_bid))
+
+        js_w1_after = read_json_api(1801, 'wallets')
+
+        node1_from_before = self.getBalance(js_w1_before, coin_from)
+        node1_from_after = self.getBalance(js_w1_after, coin_from)
+        amount_from = float(format_amount(amt_swap, 8))
+        # TODO: Discard block rewards
+        # assert (node1_from_after - node1_from_before > (amount_from - 0.02))
+
+        swap_clients[0].abandonBid(bid_id)
+
+        wait_for_none_active(test_delay_event, 1800)
+        wait_for_none_active(test_delay_event, 1801)
+
+    def do_test_04_follower_recover_b_lock_tx(self, coin_from, coin_to):
+        logging.info('---------- Test {} to {} follower recovers coin b lock tx'.format(coin_from.name, coin_to.name))
+
+        swap_clients = self.swap_clients
+        reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins
+        ci_from = swap_clients[0].ci(coin_from)
+        ci_to = swap_clients[0].ci(coin_to)
+
+        id_offerer: int = 0
+        id_bidder: int = 1
+        id_leader: int = 1 if reverse_bid else 0
+        id_follower: int = 0 if reverse_bid else 1
+        logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}')
+
+        js_w0_before = read_json_api(1800, 'wallets')
+        js_w1_before = read_json_api(1801, 'wallets')
+
+        amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
+        rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
+        offer_id = swap_clients[id_offerer].postOffer(
+            coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
+            lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32)
+        wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id)
+        offer = swap_clients[id_bidder].getOffer(offer_id)
+
+        bid_id = swap_clients[id_bidder].postXmrBid(offer_id, offer.amount_from)
+        wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED)
+
+        swap_clients[id_follower].setBidDebugInd(bid_id, DebugTypes.CREATE_INVALID_COIN_B_LOCK)
+        swap_clients[id_offerer].acceptBid(bid_id)
+
+        leader_sent_bid: bool = True if reverse_bid else False
+        wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=200, sent=leader_sent_bid)
+        wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=(not leader_sent_bid))
+
+        js_w0_after = read_json_api(1800, 'wallets')
+        js_w1_after = read_json_api(1801, 'wallets')
+
+        node0_from_before = self.getBalance(js_w0_before, coin_from)
+        node0_from_after = self.getBalance(js_w0_after, coin_from)
+        logging.info('node0 end coin_from balance {}, diff {}'.format(node0_from_after, node0_from_after - node0_from_before))
+        node0_to_before = self.getBalance(js_w0_before, coin_to)
+        node0_to_after = self.getBalance(js_w0_after, coin_to)
+        logging.info('node0 end coin_to balance {}, diff {}'.format(node0_to_after, node0_to_after - node0_to_before))
+        max_fee_from: float = 0.1 if coin_from == Coins.PART_ANON else 0.02
+        if coin_from != Coins.PART:  # TODO: Discard block rewards
+            assert (node0_from_before - node0_from_after < max_fee_from)
+
+        node1_from_before = self.getBalance(js_w1_before, coin_from)
+        node1_from_after = self.getBalance(js_w1_after, coin_from)
+        logging.info('node1 end coin_from balance {}, diff {}'.format(node1_from_after, node1_from_after - node1_from_before))
+        node1_to_before = self.getBalance(js_w1_before, coin_to)
+        node1_to_after = self.getBalance(js_w1_after, coin_to)
+        logging.info('node1 end coin_to balance {}, diff {}'.format(node1_to_after, node1_to_after - node1_to_before))
+
+        max_fee_to: float = 0.1 if coin_to == Coins.PART_ANON else 0.02
+        assert (node1_to_before - node1_to_after < max_fee_to)
+
+    def do_test_05_self_bid(self, coin_from, coin_to):
+        logging.info('---------- Test {} to {} same client'.format(coin_from.name, coin_to.name))
+
+        swap_clients = self.swap_clients
+        ci_from = swap_clients[0].ci(coin_from)
+        ci_to = swap_clients[0].ci(coin_to)
+
+        amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
+        rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
+
+        offer_id = swap_clients[1].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, auto_accept_bids=True)
+        bid_id = swap_clients[1].postXmrBid(offer_id, amt_swap)
+
+        wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=180)
+
+
+class BasicSwapTest(TestFunctions):
 
     def test_001_nested_segwit(self):
         logging.info('---------- Test {} p2sh nested segwit'.format(self.test_coin_from.name))
@@ -275,83 +548,6 @@ class BasicSwapTest(BaseTest):
         rv = read_json_api(1800, 'getcoinseed', {'coin': 'XMR'})
         assert (rv['address'] == '47H7UDLzYEsR28BWttxp59SP1UVSxs4VKDJYSfmz7Wd4Fue5VWuoV9x9eejunwzVSmHWN37gBkaAPNf9VD4bTvwQKsBVWyK')
 
-    def do_test_01_full_swap(self, coin_from: Coins, coin_to: Coins) -> None:
-        logging.info('---------- Test {} to {}'.format(coin_from.name, coin_to.name))
-
-        swap_clients = self.swap_clients
-        reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins
-        ci_from = swap_clients[0].ci(coin_from)
-        ci_to = swap_clients[1].ci(coin_to)
-        ci_part0 = swap_clients[0].ci(Coins.PART)
-        ci_part1 = swap_clients[1].ci(Coins.PART)
-
-        # Offerer sends the offer
-        # Bidder sends the bid
-        id_offerer: int = 0
-        id_bidder: int = 1
-
-        # Leader sends the initial (chain a) lock tx.
-        # Follower sends the participate (chain b) lock tx.
-        id_leader: int = 1 if reverse_bid else 0
-        id_follower: int = 0 if reverse_bid else 1
-        logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}')
-
-        js_0 = read_json_api(1800, 'wallets')
-        node0_from_before = self.getBalance(js_0, coin_from)
-
-        js_1 = read_json_api(1801, 'wallets')
-        node1_from_before = self.getBalance(js_1, coin_from)
-
-        js_0_to = read_json_api(1800, 'wallets/{}'.format(coin_to.name.lower()))
-        js_1_to = read_json_api(1801, 'wallets/{}'.format(coin_to.name.lower()))
-
-        node0_sent_messages_before: int = ci_part0.rpc_callback('smsgoutbox', ['count',])['num_messages']
-        node1_sent_messages_before: int = ci_part1.rpc_callback('smsgoutbox', ['count',])['num_messages']
-
-        amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
-        rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
-        offer_id = swap_clients[id_offerer].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP)
-        wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id)
-        offer = swap_clients[id_bidder].listOffers(filters={'offer_id': offer_id})[0]
-        assert (offer.offer_id == offer_id)
-
-        bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from)
-
-        wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED)
-        swap_clients[id_offerer].acceptBid(bid_id)
-
-        wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.SWAP_COMPLETED, wait_for=180)
-        wait_for_bid(test_delay_event, swap_clients[id_bidder], bid_id, BidStates.SWAP_COMPLETED, sent=True)
-
-        amount_from = float(ci_from.format_amount(amt_swap))
-        js_1 = read_json_api(1801, 'wallets')
-        node1_from_after = self.getBalance(js_1, coin_from)
-        if coin_from is not Coins.PART:  # TODO: staking
-            assert (node1_from_after > node1_from_before + (amount_from - 0.05))
-
-        js_0 = read_json_api(1800, 'wallets')
-        node0_from_after = self.getBalance(js_0, coin_from)
-        # TODO: Discard block rewards
-        # assert (node0_from_after < node0_from_before - amount_from)
-
-        js_0_to_after = read_json_api(1800, 'wallets/{}'.format(coin_to.name.lower()))
-        js_1_to_after = read_json_api(1801, 'wallets/{}'.format(coin_to.name.lower()))
-
-        scale_from = 8
-        amount_to = int((amt_swap * rate_swap) // (10 ** scale_from))
-        amount_to_float = float(ci_to.format_amount(amount_to))
-        node1_to_after = float(js_1_to_after['unconfirmed']) + float(js_1_to_after['balance'])
-        node1_to_before = float(js_1_to['unconfirmed']) + float(js_1_to['balance'])
-        if False:  # TODO: set stakeaddress and xmr rewards to non wallet addresses
-            assert (node1_to_after < node1_to_before - amount_to_float)
-
-        node0_sent_messages_after: int = ci_part0.rpc_callback('smsgoutbox', ['count',])['num_messages']
-        node1_sent_messages_after: int = ci_part1.rpc_callback('smsgoutbox', ['count',])['num_messages']
-        node0_sent_messages: int = node0_sent_messages_after - node0_sent_messages_before
-        node1_sent_messages: int = node1_sent_messages_after - node1_sent_messages_before
-        assert (node0_sent_messages == (5 if reverse_bid else 6))
-        assert (node1_sent_messages == (6 if reverse_bid else 4))
-
     def test_01_a_full_swap(self):
         if not self.has_segwit:
             return
@@ -360,7 +556,7 @@ class BasicSwapTest(BaseTest):
     def test_01_b_full_swap_reverse(self):
         if not self.has_segwit:
             return
-        self.prepare_balance(Coins.XMR.name, 100.0, 1800, 1801)
+        self.prepare_balance(Coins.XMR, 100.0, 1800, 1801)
         self.do_test_01_full_swap(Coins.XMR, self.test_coin_from)
 
     def test_01_c_full_swap_to_part(self):
@@ -371,47 +567,6 @@ class BasicSwapTest(BaseTest):
     def test_01_d_full_swap_from_part(self):
         self.do_test_01_full_swap(Coins.PART, self.test_coin_from)
 
-    def do_test_02_leader_recover_a_lock_tx(self, coin_from: Coins, coin_to: Coins) -> None:
-        logging.info('---------- Test {} to {} leader recovers coin a lock tx'.format(coin_from.name, coin_to.name))
-
-        swap_clients = self.swap_clients
-        reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins
-        ci_from = swap_clients[0].ci(coin_from)
-        ci_to = swap_clients[0].ci(coin_to)
-
-        id_offerer: int = 0
-        id_bidder: int = 1
-        id_leader: int = 1 if reverse_bid else 0
-        id_follower: int = 0 if reverse_bid else 1
-        logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}')
-
-        js_wl_before = read_json_api(1800 + id_leader, 'wallets')
-        wl_from_before = self.getBalance(js_wl_before, coin_from)
-
-        amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
-        rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
-        offer_id = swap_clients[id_offerer].postOffer(
-            coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
-            lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32)
-        wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id)
-        offer = swap_clients[id_bidder].getOffer(offer_id)
-
-        bid_id = swap_clients[id_bidder].postXmrBid(offer_id, offer.amount_from)
-        wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED)
-
-        swap_clients[id_follower].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK)
-        swap_clients[id_offerer].acceptBid(bid_id)
-
-        leader_sent_bid: bool = True if reverse_bid else False
-        wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=leader_sent_bid, wait_for=180)
-        wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, [BidStates.BID_STALLED_FOR_TEST, BidStates.XMR_SWAP_FAILED], sent=(not leader_sent_bid))
-
-        js_wl_after = read_json_api(1800 + id_leader, 'wallets')
-        wl_from_after = self.getBalance(js_wl_after, coin_from)
-
-        # TODO: Discard block rewards
-        # assert (node0_from_before - node0_from_after < 0.02)
-
     def test_02_a_leader_recover_a_lock_tx(self):
         if not self.has_segwit:
             return
@@ -420,7 +575,7 @@ class BasicSwapTest(BaseTest):
     def test_02_b_leader_recover_a_lock_tx_reverse(self):
         if not self.has_segwit:
             return
-        self.prepare_balance(Coins.XMR.name, 100.0, 1800, 1801)
+        self.prepare_balance(Coins.XMR, 100.0, 1800, 1801)
         self.do_test_02_leader_recover_a_lock_tx(Coins.XMR, self.test_coin_from)
 
     def test_02_c_leader_recover_a_lock_tx_to_part(self):
@@ -431,59 +586,6 @@ class BasicSwapTest(BaseTest):
     def test_02_leader_recover_a_lock_tx_from_part(self):
         self.do_test_02_leader_recover_a_lock_tx(Coins.PART, self.test_coin_from)
 
-    def do_test_03_follower_recover_a_lock_tx(self, coin_from, coin_to):
-        logging.info('---------- Test {} to {} follower recovers coin a lock tx'.format(coin_from.name, coin_to.name))
-
-        # Leader is too slow to recover the coin a lock tx and follower swipes it
-        # coin b lock tx remains unspent
-
-        swap_clients = self.swap_clients
-        reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins
-        ci_from = swap_clients[0].ci(coin_from)
-        ci_to = swap_clients[0].ci(coin_to)
-
-        id_offerer: int = 0
-        id_bidder: int = 1
-        id_leader: int = 1 if reverse_bid else 0
-        id_follower: int = 0 if reverse_bid else 1
-        logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}')
-
-        js_w0_before = read_json_api(1800, 'wallets')
-        js_w1_before = read_json_api(1801, 'wallets')
-
-        amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
-        rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
-        offer_id = swap_clients[id_offerer].postOffer(
-            coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
-            lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32)
-        wait_for_offer(test_delay_event, swap_clients[1], offer_id)
-        offer = swap_clients[id_bidder].getOffer(offer_id)
-
-        bid_id = swap_clients[id_bidder].postXmrBid(offer_id, offer.amount_from)
-        wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED)
-
-        swap_clients[id_follower].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK)
-        swap_clients[id_leader].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND)
-
-        swap_clients[id_offerer].acceptBid(bid_id)
-
-        leader_sent_bid: bool = True if reverse_bid else False
-        wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.BID_STALLED_FOR_TEST, wait_for=180, sent=leader_sent_bid)
-        wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=80, sent=(not leader_sent_bid))
-
-        js_w1_after = read_json_api(1801, 'wallets')
-
-        node1_from_before = self.getBalance(js_w1_before, coin_from)
-        node1_from_after = self.getBalance(js_w1_after, coin_from)
-        amount_from = float(format_amount(amt_swap, 8))
-        # TODO: Discard block rewards
-        # assert (node1_from_after - node1_from_before > (amount_from - 0.02))
-
-        swap_clients[0].abandonBid(bid_id)
-
-        wait_for_none_active(test_delay_event, 1800)
-        wait_for_none_active(test_delay_event, 1801)
-
     def test_03_a_follower_recover_a_lock_tx(self):
         if not self.has_segwit:
             return
@@ -492,7 +594,7 @@ class BasicSwapTest(BaseTest):
     def test_03_b_follower_recover_a_lock_tx_reverse(self):
         if not self.has_segwit:
             return
-        self.prepare_balance(Coins.XMR.name, 100.0, 1800, 1801)
+        self.prepare_balance(Coins.XMR, 100.0, 1800, 1801)
         self.do_test_03_follower_recover_a_lock_tx(Coins.XMR, self.test_coin_from)
 
     def test_03_c_follower_recover_a_lock_tx_to_part(self):
@@ -503,61 +605,6 @@ class BasicSwapTest(BaseTest):
     def test_03_d_follower_recover_a_lock_tx_from_part(self):
         self.do_test_03_follower_recover_a_lock_tx(Coins.PART, self.test_coin_from)
 
-    def do_test_04_follower_recover_b_lock_tx(self, coin_from, coin_to):
-        logging.info('---------- Test {} to {} follower recovers coin b lock tx'.format(coin_from.name, coin_to.name))
-
-        swap_clients = self.swap_clients
-        reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins
-        ci_from = swap_clients[0].ci(coin_from)
-        ci_to = swap_clients[0].ci(coin_to)
-
-        id_offerer: int = 0
-        id_bidder: int = 1
-        id_leader: int = 1 if reverse_bid else 0
-        id_follower: int = 0 if reverse_bid else 1
-        logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}')
-
-        js_w0_before = read_json_api(1800, 'wallets')
-        js_w1_before = read_json_api(1801, 'wallets')
-
-        amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
-        rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
-        offer_id = swap_clients[id_offerer].postOffer(
-            coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
-            lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32)
-        wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id)
-        offer = swap_clients[id_bidder].getOffer(offer_id)
-
-        bid_id = swap_clients[id_bidder].postXmrBid(offer_id, offer.amount_from)
-        wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED)
-
-        swap_clients[id_follower].setBidDebugInd(bid_id, DebugTypes.CREATE_INVALID_COIN_B_LOCK)
-        swap_clients[id_offerer].acceptBid(bid_id)
-
-        leader_sent_bid: bool = True if reverse_bid else False
-        wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=200, sent=leader_sent_bid)
-        wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=(not leader_sent_bid))
-
-        js_w0_after = read_json_api(1800, 'wallets')
-        js_w1_after = read_json_api(1801, 'wallets')
-
-        node0_from_before = self.getBalance(js_w0_before, coin_from)
-        node0_from_after = self.getBalance(js_w0_after, coin_from)
-        logging.info('node0 end coin_from balance {}, diff {}'.format(node0_from_after, node0_from_after - node0_from_before))
-        node0_to_before = self.getBalance(js_w0_before, coin_to)
-        node0_to_after = self.getBalance(js_w0_after, coin_to)
-        logging.info('node0 end coin_to balance {}, diff {}'.format(node0_to_after, node0_to_after - node0_to_before))
-        if coin_from != Coins.PART:  # TODO: Discard block rewards
-            assert (node0_from_before - node0_from_after < 0.02)
-
-        node1_from_before = self.getBalance(js_w1_before, coin_from)
-        node1_from_after = self.getBalance(js_w1_after, coin_from)
-        logging.info('node1 end coin_from balance {}, diff {}'.format(node1_from_after, node1_from_after - node1_from_before))
-        node1_to_before = self.getBalance(js_w1_before, coin_to)
-        node1_to_after = self.getBalance(js_w1_after, coin_to)
-        logging.info('node1 end coin_to balance {}, diff {}'.format(node1_to_after, node1_to_after - node1_to_before))
-        assert (node1_to_before - node1_to_after < 0.02)
-
     def test_04_a_follower_recover_b_lock_tx(self):
         if not self.has_segwit:
             return
@@ -566,7 +613,7 @@ class BasicSwapTest(BaseTest):
     def test_04_b_follower_recover_b_lock_tx_reverse(self):
         if not self.has_segwit:
             return
-        self.prepare_balance(Coins.XMR.name, 100.0, 1800, 1801)
+        self.prepare_balance(Coins.XMR, 100.0, 1800, 1801)
         self.do_test_04_follower_recover_b_lock_tx(Coins.XMR, self.test_coin_from)
 
     def test_04_c_follower_recover_b_lock_tx_to_part(self):
@@ -577,20 +624,6 @@ class BasicSwapTest(BaseTest):
     def test_04_d_follower_recover_b_lock_tx_from_part(self):
         self.do_test_04_follower_recover_b_lock_tx(Coins.PART, self.test_coin_from)
 
-    def do_test_05_self_bid(self, coin_from, coin_to):
-        logging.info('---------- Test {} to {} same client'.format(coin_from.name, coin_to.name))
-
-        swap_clients = self.swap_clients
-        ci_to = swap_clients[0].ci(coin_to)
-
-        amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1)
-        rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
-
-        offer_id = swap_clients[1].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, auto_accept_bids=True)
-        bid_id = swap_clients[1].postXmrBid(offer_id, amt_swap)
-
-        wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=180)
-
     def test_05_self_bid(self):
         if not self.has_segwit:
             return
@@ -604,12 +637,17 @@ class BasicSwapTest(BaseTest):
     def test_05_self_bid_from_part(self):
         self.do_test_05_self_bid(Coins.PART, self.test_coin_from)
 
+    def test_05_self_bid_rev(self):
+        if not self.has_segwit:
+            return
+        self.do_test_05_self_bid(Coins.XMR, self.test_coin_from)
+
     def test_06_preselect_inputs(self):
         tla_from = self.test_coin_from.name
         logging.info('---------- Test {} Preselected inputs'.format(tla_from))
         swap_clients = self.swap_clients
 
-        self.prepare_balance(tla_from, 100.0, 1802, 1800)
+        self.prepare_balance(self.test_coin_from, 100.0, 1802, 1800)
 
         js_w2 = read_json_api(1802, 'wallets')
         assert (float(js_w2[tla_from]['balance']) >= 100.0)
@@ -739,7 +777,52 @@ class TestBTC(BasicSwapTest):
         assert (js_0['PART']['encrypted'] is True)
         assert (js_0['PART']['locked'] is False)
 
-        super().test_01_full_swap()
+        super().test_01_a_full_swap()
+
+
+class TestBTC_PARTB(TestFunctions):
+    __test__ = True
+    test_coin_from = Coins.BTC
+    test_coin_to = Coins.PART_BLIND
+    start_ltc_nodes = False
+    base_rpc_port = BTC_BASE_RPC_PORT
+
+    def test_01_a_full_swap(self):
+        self.prepare_balance(self.test_coin_to, 100.0, 1801, 1800)
+        self.do_test_01_full_swap(self.test_coin_from, self.test_coin_to)
+
+    def test_01_b_full_swap_reverse(self):
+        self.prepare_balance(self.test_coin_to, 100.0, 1800, 1800)
+        self.do_test_01_full_swap(self.test_coin_to, self.test_coin_from)
+
+    def test_02_a_leader_recover_a_lock_tx(self):
+        self.prepare_balance(self.test_coin_to, 100.0, 1801, 1800)
+        self.do_test_02_leader_recover_a_lock_tx(self.test_coin_from, self.test_coin_to)
+
+    def test_02_b_leader_recover_a_lock_tx_reverse(self):
+        self.prepare_balance(self.test_coin_to, 100.0, 1800, 1800)
+        self.do_test_02_leader_recover_a_lock_tx(self.test_coin_to, self.test_coin_from)
+
+    def test_03_a_follower_recover_a_lock_tx(self):
+        self.prepare_balance(self.test_coin_to, 100.0, 1801, 1800)
+        self.do_test_03_follower_recover_a_lock_tx(self.test_coin_from, self.test_coin_to)
+
+    def test_03_b_follower_recover_a_lock_tx_reverse(self):
+        self.prepare_balance(self.test_coin_to, 100.0, 1800, 1800)
+        self.do_test_03_follower_recover_a_lock_tx(self.test_coin_to, self.test_coin_from, lock_value=12)
+
+    def test_04_a_follower_recover_b_lock_tx(self):
+        self.prepare_balance(self.test_coin_to, 100.0, 1801, 1800)
+        self.do_test_04_follower_recover_b_lock_tx(self.test_coin_from, self.test_coin_to)
+
+    def test_04_b_follower_recover_b_lock_tx_reverse(self):
+        self.prepare_balance(self.test_coin_to, 100.0, 1800, 1800)
+        self.do_test_04_follower_recover_b_lock_tx(self.test_coin_to, self.test_coin_from)
+
+
+class TestBTC_PARTA(TestBTC_PARTB):
+    __test__ = True
+    test_coin_to = Coins.PART_ANON
 
 
 if __name__ == '__main__':
diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py
index 8f4515e..baad3f6 100644
--- a/tests/basicswap/test_xmr.py
+++ b/tests/basicswap/test_xmr.py
@@ -565,7 +565,7 @@ class BaseTest(unittest.TestCase):
                 for i in range(8):
                     sx_addr = callnoderpc(1, 'getnewstealthaddress')
                     outputs.append({'address': sx_addr, 'amount': 0.5})
-                for i in range(6):
+                for i in range(7):
                     callnoderpc(0, 'sendtypeto', ['part', 'anon', outputs])
 
                 part_addr1 = callnoderpc(1, 'getnewaddress', ['initial addr'])