diff --git a/basicswap/__init__.py b/basicswap/__init__.py index 16d515c..6cec4c2 100644 --- a/basicswap/__init__.py +++ b/basicswap/__init__.py @@ -1,3 +1,3 @@ name = "basicswap" -__version__ = "0.11.50" +__version__ = "0.11.51" diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index e90f6c3..d9dca0d 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -4674,12 +4674,17 @@ class BasicSwap(BaseApp): self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer) return + unlock_time = 0 if bid.debug_ind == DebugTypes.CREATE_INVALID_COIN_B_LOCK: bid.amount_to -= int(bid.amount_to * 0.1) self.log.debug('XMR bid %s: Debug %d - Reducing lock b txn amount by 10%% to %s.', bid_id.hex(), bid.debug_ind, ci_to.format_amount(bid.amount_to)) self.logBidEvent(bid.bid_id, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), session) + if bid.debug_ind == DebugTypes.SEND_LOCKED_XMR: + unlock_time = 10000 + self.log.debug('XMR bid %s: Debug %d - Sending locked XMR.', bid_id.hex(), bid.debug_ind) + self.logBidEvent(bid.bid_id, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), session) try: - b_lock_tx_id = ci_to.publishBLockTx(xmr_swap.pkbv, xmr_swap.pkbs, bid.amount_to, xmr_offer.b_fee_rate) + b_lock_tx_id = ci_to.publishBLockTx(xmr_swap.pkbv, xmr_swap.pkbs, bid.amount_to, xmr_offer.b_fee_rate, unlock_time=unlock_time) except Exception as ex: error_msg = 'publishBLockTx failed for bid {} with error {}'.format(bid_id.hex(), str(ex)) num_retries = self.countBidEvents(bid, EventLogTypes.FAILED_TX_B_LOCK_PUBLISH, session) diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index 7d691dd..bc83d61 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -184,6 +184,7 @@ class DebugTypes(IntEnum): MAKE_INVALID_PTX = auto() DONT_SPEND_ITX = auto() SKIP_LOCK_TX_REFUND = auto() + SEND_LOCKED_XMR = auto() def strOfferState(state): diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 6206f85..89dfa16 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -993,7 +993,7 @@ class BTCInterface(CoinInterface): def encodeSharedAddress(self, Kbv, Kbs): return self.pubkey_to_segwit_address(Kbs) - def publishBLockTx(self, Kbv, Kbs, output_amount, feerate): + def publishBLockTx(self, Kbv, Kbs, output_amount, feerate, delay_for=10, unlock_time=0): b_lock_tx = self.createBLockTx(Kbs, output_amount) b_lock_tx = self.fundTx(b_lock_tx, feerate) diff --git a/basicswap/interface/part.py b/basicswap/interface/part.py index cddf673..ade3b64 100644 --- a/basicswap/interface/part.py +++ b/basicswap/interface/part.py @@ -639,7 +639,7 @@ class PARTInterfaceAnon(PARTInterface): def coin_name(self): return super().coin_name() + ' Anon' - def publishBLockTx(self, Kbv, Kbs, output_amount, feerate): + def publishBLockTx(self, Kbv, Kbs, output_amount, feerate, delay_for=10, unlock_time=0): sx_addr = self.formatStealthAddress(Kbv, Kbs) self._log.debug('sx_addr: {}'.format(sx_addr)) diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index dc2b9c8..48ddcc7 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -252,13 +252,13 @@ class XMRInterface(CoinInterface): def encodeSharedAddress(self, Kbv, Kbs): return xmr_util.encode_address(Kbv, Kbs) - def publishBLockTx(self, Kbv, Kbs, output_amount, feerate, delay_for=10): + def publishBLockTx(self, Kbv, Kbs, output_amount, feerate, delay_for=10, unlock_time=0): with self._mx_wallet: self.openWallet(self._wallet_filename) shared_addr = xmr_util.encode_address(Kbv, Kbs) - params = {'destinations': [{'amount': output_amount, 'address': shared_addr}]} + params = {'destinations': [{'amount': output_amount, 'address': shared_addr}], 'unlock_time': unlock_time} if self._fee_priority > 0: params['priority'] = self._fee_priority rv = self.rpc_wallet_cb('transfer', params) @@ -316,15 +316,20 @@ class XMRInterface(CoinInterface): # and (current_height is None or current_height - transfer['block_height'] > cb_block_confirmed): ''' params = {'transfer_type': 'available'} - rv = self.rpc_wallet_cb('incoming_transfers', params) - if 'transfers' in rv: - for transfer in rv['transfers']: + transfers = self.rpc_wallet_cb('incoming_transfers', params) + rv = None + if 'transfers' in transfers: + for transfer in transfers['transfers']: + if not transfer['unlocked']: + self._log.warning('Coin b lock txn is locked: {}'.format(transfer['tx_hash'])) + rv = -1 + continue if transfer['amount'] == cb_swap_value: return {'txid': transfer['tx_hash'], 'amount': transfer['amount'], 'height': 0 if 'block_height' not in transfer else transfer['block_height']} else: self._log.warning('Incorrect amount detected for coin b lock txn: {}'.format(transfer['tx_hash'])) - return -1 - return None + rv = -1 + return rv def waitForLockTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height): with self._mx_wallet: diff --git a/docker/production/notes.md b/docker/production/notes.md index 36607f3..580bd27 100644 --- a/docker/production/notes.md +++ b/docker/production/notes.md @@ -3,6 +3,11 @@ This will setup Basicswap so that each coin runs in it's own container. +Install dependencies: + + sudo apt install basez docker-compose + + Copy and edit .env config: cp example.env .env diff --git a/tests/basicswap/common.py b/tests/basicswap/common.py index 660fcf8..162c2b3 100644 --- a/tests/basicswap/common.py +++ b/tests/basicswap/common.py @@ -159,7 +159,7 @@ def wait_for_bid_tx_state(delay_event, swap_client, bid_id, initiate_state, part raise ValueError('wait_for_bid_tx_state timed out.') -def wait_for_event(delay_event, swap_client, linked_type, linked_id, wait_for=20): +def wait_for_event(delay_event, swap_client, linked_type, linked_id, event_type=None, wait_for=20): logging.info('wait_for_event') for i in range(wait_for): @@ -167,8 +167,10 @@ def wait_for_event(delay_event, swap_client, linked_type, linked_id, wait_for=20 raise ValueError('Test stopped.') delay_event.wait(1) rv = swap_client.getEvents(linked_type, linked_id) - if len(rv) > 0: - return rv + + for event in rv: + if event_type is None or event.event_type == event_type: + return event raise ValueError('wait_for_event timed out.') diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index 4812707..993b3c5 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -30,6 +30,7 @@ from basicswap.basicswap import ( ) from basicswap.basicswap_util import ( TxLockTypes, + EventLogTypes, ) from basicswap.util import ( COIN, @@ -1037,8 +1038,8 @@ class Test(BaseTest): } bid_id = swap_clients[1].postBid(offer_id, below_min_bid, extra_options=extra_bid_options) - events = wait_for_event(test_delay_event, swap_clients[0], Concepts.NETWORK_MESSAGE, bid_id) - assert ('Bid amount below minimum' in events[0].event_msg) + event = wait_for_event(test_delay_event, swap_clients[0], Concepts.NETWORK_MESSAGE, bid_id) + assert ('Bid amount below minimum' in event.event_msg) bid_ids = [] for i in range(5): @@ -1047,8 +1048,8 @@ class Test(BaseTest): # Should fail > max concurrent test_delay_event.wait(1.0) bid_id = swap_clients[1].postBid(offer_id, min_bid) - events = wait_for_event(test_delay_event, swap_clients[0], Concepts.AUTOMATION, bid_id) - assert ('Already have 5 bids to complete' in events[0].event_msg) + event = wait_for_event(test_delay_event, swap_clients[0], Concepts.AUTOMATION, bid_id) + assert ('Already have 5 bids to complete' in event.event_msg) for bid_id in bid_ids: wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) @@ -1059,8 +1060,8 @@ class Test(BaseTest): # Should fail > total value amt_bid += 1 bid_id = swap_clients[1].postBid(offer_id, amt_bid) - events = wait_for_event(test_delay_event, swap_clients[0], Concepts.AUTOMATION, bid_id) - assert ('Over remaining offer value' in events[0].event_msg) + event = wait_for_event(test_delay_event, swap_clients[0], Concepts.AUTOMATION, bid_id) + assert ('Over remaining offer value' in event.event_msg) # Should pass amt_bid -= 1 @@ -1219,6 +1220,30 @@ class Test(BaseTest): node0_blind_after = js_0['blind_balance'] + js_0['blind_unconfirmed'] assert (node0_blind_after < node0_blind_before - amount_from) + def test_13_locked_xmr(self): + logging.info('---------- Test PART to XMR leader recovers coin a lock tx') + swap_clients = self.swap_clients + + amt_swap = make_int(random.uniform(0.1, 10.0), scale=8, r=1) + rate_swap = make_int(random.uniform(2.0, 20.0), scale=12, r=1) + offer_id = swap_clients[0].postOffer(Coins.PART, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP) + + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) + swap_clients[1].setBidDebugInd(bid_id, DebugTypes.SEND_LOCKED_XMR) + swap_clients[0].acceptXmrBid(bid_id) + + wait_for_event(test_delay_event, swap_clients[0], Concepts.BID, bid_id, event_type=EventLogTypes.LOCK_TX_B_INVALID, wait_for=180) + + wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_SCRIPT_COIN_LOCKED, sent=True) + + swap_clients[0].abandonBid(bid_id) + swap_clients[1].abandonBid(bid_id) + def test_98_withdraw_all(self): logging.info('---------- Test XMR withdrawal all') try: