From f5d4b8dc0d510bae20e12d9d212d9c923cb2196b Mon Sep 17 00:00:00 2001 From: tecnovert Date: Wed, 24 Jan 2024 23:12:18 +0200 Subject: [PATCH] Show error when auto-accepting a bid fails. --- basicswap/basicswap.py | 35 +++++++++++++++--- basicswap/http_server.py | 14 +++++--- basicswap/interface/btc.py | 16 ++++++++- basicswap/interface/part.py | 13 ++++++- basicswap/js_server.py | 5 ++- tests/basicswap/test_btc_xmr.py | 63 ++++++++++++++++++++++++--------- tests/basicswap/test_run.py | 3 +- tests/basicswap/test_xmr.py | 4 +-- 8 files changed, 121 insertions(+), 32 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 736c96e..977f9c9 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -2432,7 +2432,6 @@ class BasicSwap(BaseApp): reverse_bid: bool = self.is_reverse_ads_bid(offer.coin_from) if reverse_bid: return self.acceptADSReverseBid(bid_id) - return self.acceptXmrBid(bid_id) if bid.contract_count is None: @@ -4301,23 +4300,28 @@ class BasicSwap(BaseApp): self.closeSession(session) def countQueuedActions(self, session, bid_id: bytes, action_type) -> int: - q = session.query(Action).filter(sa.and_(Action.active_ind == 1, Action.linked_id == bid_id, Action.action_type == int(action_type))) + q = session.query(Action).filter(sa.and_(Action.active_ind == 1, Action.linked_id == bid_id)) + if action_type is not None: + q.filter(Action.action_type == int(action_type)) return q.count() def checkQueuedActions(self) -> None: self.mxDB.acquire() now: int = self.getTime() session = None - reload_in_progress = False + reload_in_progress: bool = False try: session = scoped_session(self.session_factory) q = session.query(Action).filter(sa.and_(Action.active_ind == 1, Action.trigger_at <= now)) for row in q: + accepting_bid: bool = False try: if row.action_type == ActionTypes.ACCEPT_BID: + accepting_bid = True self.acceptBid(row.linked_id) elif row.action_type == ActionTypes.ACCEPT_XMR_BID: + accepting_bid = True self.acceptXmrBid(row.linked_id) elif row.action_type == ActionTypes.SIGN_XMR_SWAP_LOCK_TX_A: self.sendXmrBidTxnSigsFtoL(row.linked_id, session) @@ -4338,11 +4342,34 @@ class BasicSwap(BaseApp): elif row.action_type == ActionTypes.REDEEM_ITX: atomic_swap_1.redeemITx(self, row.linked_id, session) elif row.action_type == ActionTypes.ACCEPT_AS_REV_BID: + accepting_bid = True self.acceptADSReverseBid(row.linked_id) else: self.log.warning('Unknown event type: %d', row.event_type) except Exception as ex: - self.logException(f'checkQueuedActions failed: {ex}') + err_msg = f'checkQueuedActions failed: {ex}' + self.logException(err_msg) + + bid_id = row.linked_id + # Failing to accept a bid should not set an error state as the bid has not begun yet + if accepting_bid: + self.logEvent(Concepts.BID, + bid_id, + EventLogTypes.ERROR, + err_msg, + session) + + # If delaying with no (further) queued actions reset state + if self.countQueuedActions(session, bid_id, None) < 2: + bid = self.getBid(bid_id, session) + if bid and bid.state == BidStates.SWAP_DELAYING: + bid.setState(BidStates.BID_RECEIVED) + self.saveBidInSession(bid_id, bid, session) + else: + bid = self.getBid(bid_id, session) + if bid: + bid.setState(BidStates.BID_ERROR, err_msg) + self.saveBidInSession(bid_id, bid, session) if self.debug: session.execute('UPDATE actions SET active_ind = 2 WHERE trigger_at <= :now', {'now': now}) diff --git a/basicswap/http_server.py b/basicswap/http_server.py index 75ce134..22e36a9 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2019-2023 tecnovert +# Copyright (c) 2019-2024 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -327,11 +327,15 @@ class HttpHandler(BaseHTTPRequestHandler): template = env.get_template('rpc.html') coins = listAvailableCoins(swap_client, with_variants=False) + with_xmr: bool = any(c[0] == Coins.XMR for c in coins) coins = [c for c in coins if c[0] != Coins.XMR] - coins.append((-5, 'Litecoin MWEB Wallet')) - coins.append((-2, 'Monero')) - coins.append((-3, 'Monero JSON')) - coins.append((-4, 'Monero Wallet')) + + if any(c[0] == Coins.LTC for c in coins): + coins.append((-5, 'Litecoin MWEB Wallet')) + if with_xmr: + coins.append((-2, 'Monero')) + coins.append((-3, 'Monero JSON')) + coins.append((-4, 'Monero Wallet')) return self.render_template(template, { 'messages': messages, diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 175647a..2b213c2 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright (c) 2020-2023 tecnovert +# Copyright (c) 2020-2024 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -1375,6 +1375,20 @@ class BTCInterface(CoinInterface): for u in unspent: if u['spendable'] is not True: continue + if 'address' not in u: + continue + if 'desc' in u: + desc = u['desc'] + if self.using_segwit: + if self.use_p2shp2wsh(): + if not desc.startswith('sh(wpkh'): + continue + else: + if not desc.startswith('wpkh'): + continue + else: + if not desc.startswith('pkh'): + continue unspent_addr[u['address']] = unspent_addr.get(u['address'], 0) + self.make_int(u['amount'], r=1) return unspent_addr diff --git a/basicswap/interface/part.py b/basicswap/interface/part.py index 38d3f58..34758bd 100644 --- a/basicswap/interface/part.py +++ b/basicswap/interface/part.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright (c) 2020-2023 tecnovert +# Copyright (c) 2020-2024 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -141,6 +141,17 @@ class PARTInterface(BTCInterface): tx_vsize += 204 if redeem else 187 return tx_vsize + def getUnspentsByAddr(self): + unspent_addr = dict() + unspent = self.rpc_wallet('listunspent') + for u in unspent: + if u['spendable'] is not True: + continue + if 'address' not in u: + continue + unspent_addr[u['address']] = unspent_addr.get(u['address'], 0) + self.make_int(u['amount'], r=1) + return unspent_addr + class PARTInterfaceBlind(PARTInterface): @staticmethod diff --git a/basicswap/js_server.py b/basicswap/js_server.py index 6d95456..eb9ed5d 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -84,11 +84,14 @@ def js_coins(self, url_split, post_string, is_json) -> bytes: for coin in Coins: cc = swap_client.coin_clients[coin] coin_chainparams = chainparams[cc['coin']] + coin_active: bool = False if cc['connection_type'] == 'none' else True + if coin == Coins.LTC_MWEB: + coin_active = False entry = { 'id': int(coin), 'ticker': coin_chainparams['ticker'], 'name': getCoinName(coin), - 'active': False if cc['connection_type'] == 'none' else True, + 'active': coin_active, 'decimal_places': coin_chainparams['decimal_places'], } if coin == Coins.PART_ANON: diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py index 05ab447..ba25d84 100644 --- a/tests/basicswap/test_btc_xmr.py +++ b/tests/basicswap/test_btc_xmr.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2021-2023 tecnovert +# Copyright (c) 2021-2024 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -9,6 +9,9 @@ import random import logging import unittest +from basicswap.db import ( + Concepts, +) from basicswap.basicswap import ( Coins, SwapTypes, @@ -17,6 +20,7 @@ from basicswap.basicswap import ( ) from basicswap.basicswap_util import ( TxLockTypes, + EventLogTypes, ) from basicswap.util import ( make_int, @@ -28,6 +32,7 @@ from tests.basicswap.util import ( ) from tests.basicswap.common import ( wait_for_bid, + wait_for_event, wait_for_offer, wait_for_balance, wait_for_unspent, @@ -59,6 +64,7 @@ class TestFunctions(BaseTest): node_a_id = 0 node_b_id = 1 + node_c_id = 2 def callnoderpc(self, method, params=[], wallet=None, node_id=0): return callnoderpc(node_id, method, params, wallet, self.base_rpc_port) @@ -166,9 +172,6 @@ class TestFunctions(BaseTest): post_json = {'with_extra_info': True} offer0 = read_json_api(1800 + id_offerer, f'offers/{offer_id.hex()}', post_json)[0] offer1 = read_json_api(1800 + id_offerer, f'offers/{offer_id.hex()}', post_json)[0] - from basicswap.util import dumpj - logging.info('offer0 {} '.format(dumpj(offer0))) - logging.info('offer1 {} '.format(dumpj(offer1))) assert ('lock_time_1' in offer0) assert ('lock_time_1' in offer1) @@ -193,9 +196,7 @@ class TestFunctions(BaseTest): found: bool = False bids0 = read_json_api(1800 + id_offerer, 'bids') - logging.info('bids0 {} '.format(bids0)) for bid in bids0: - logging.info('bid {} '.format(bid)) if bid['bid_id'] != bid_id.hex(): continue assert (bid['amount_from'] == bid1['amt_from']) @@ -239,8 +240,6 @@ class TestFunctions(BaseTest): post_json = {'show_extra': True} bid0 = read_json_api(1800 + id_offerer, f'bids/{bid_id.hex()}', post_json) bid1 = read_json_api(1800 + id_bidder, f'bids/{bid_id.hex()}', post_json) - logging.info('bid0 {} '.format(dumpj(bid0))) - logging.info('bid1 {} '.format(dumpj(bid1))) chain_a_lock_txid = None chain_b_lock_txid = None @@ -417,20 +416,19 @@ class TestFunctions(BaseTest): def do_test_05_self_bid(self, coin_from, coin_to): logging.info('---------- Test {} to {} same client'.format(coin_from.name, coin_to.name)) - id_offerer: int = self.node_a_id - id_bidder: int = self.node_b_id + id_both: int = self.node_b_id swap_clients = self.swap_clients - ci_from = swap_clients[id_offerer].ci(coin_from) - ci_to = swap_clients[id_offerer].ci(coin_to) + ci_from = swap_clients[id_both].ci(coin_from) + ci_to = swap_clients[id_both].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[id_bidder].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, auto_accept_bids=True) - bid_id = swap_clients[id_bidder].postXmrBid(offer_id, amt_swap) + offer_id = swap_clients[id_both].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, auto_accept_bids=True) + bid_id = swap_clients[id_both].postXmrBid(offer_id, amt_swap) - wait_for_bid(test_delay_event, swap_clients[id_bidder], bid_id, BidStates.SWAP_COMPLETED, wait_for=(self.extra_wait_time + 180)) + wait_for_bid(test_delay_event, swap_clients[id_both], bid_id, BidStates.SWAP_COMPLETED, wait_for=(self.extra_wait_time + 180)) class BasicSwapTest(TestFunctions): @@ -689,7 +687,7 @@ class BasicSwapTest(TestFunctions): utxo = unspents[0] txout = ci.rpc('gettxout', [utxo['txid'], utxo['vout']]) - if 'address' in txout: + if 'address' in txout['scriptPubKey']: assert (addr_1 == txout['scriptPubKey']['address']) else: assert (addr_1 in txout['scriptPubKey']['addresses']) @@ -1000,6 +998,8 @@ class BasicSwapTest(TestFunctions): self.do_test_05_self_bid(self.test_coin_from, Coins.PART) def test_05_self_bid_from_part(self): + if not self.has_segwit: + return self.do_test_05_self_bid(Coins.PART, self.test_coin_from) def test_05_self_bid_rev(self): @@ -1094,6 +1094,37 @@ class BasicSwapTest(TestFunctions): swap_clients[0].check_expired_seconds = old_check_expired_seconds swap_clients[0].setMockTimeOffset(0) + def test_08_insufficient_funds(self): + tla_from = self.test_coin_from.name + logging.info('---------- Test {} Insufficient Funds'.format(tla_from)) + swap_clients = self.swap_clients + coin_from = self.test_coin_from + coin_to = Coins.XMR + + self.prepare_balance(self.test_coin_from, 10.0, 1802, 1800) + + id_offerer: int = self.node_c_id + id_bidder: int = self.node_b_id + + swap_clients = self.swap_clients + ci_from = swap_clients[id_offerer].ci(coin_from) + ci_to = swap_clients[id_bidder].ci(coin_to) + + js_0 = read_json_api(1800 + id_offerer, 'wallets') + node0_from_before: float = self.getBalance(js_0, coin_from) + + amt_swap: int = ci_from.make_int(node0_from_before, r=1) + rate_swap: int = ci_to.make_int(2.0, r=1) + offer_id = swap_clients[id_offerer].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, auto_accept_bids=True) + wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id) + + bid_id = swap_clients[id_bidder].postXmrBid(offer_id, amt_swap) + + event = wait_for_event(test_delay_event, swap_clients[id_offerer], Concepts.BID, bid_id, event_type=EventLogTypes.ERROR, wait_for=60) + assert ('Insufficient funds' in event.event_msg) + + wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED, wait_for=20) + class TestBTC(BasicSwapTest): __test__ = True diff --git a/tests/basicswap/test_run.py b/tests/basicswap/test_run.py index f44b833..341e53a 100644 --- a/tests/basicswap/test_run.py +++ b/tests/basicswap/test_run.py @@ -112,7 +112,6 @@ class Test(BaseTest): def test_001_js_coins(self): js_coins = read_json_api(1800, 'coins') - for c in Coins: coin = next((x for x in js_coins if x['id'] == int(c)), None) if c in (Coins.PART, Coins.BTC, Coins.LTC, Coins.PART_ANON, Coins.PART_BLIND): @@ -130,7 +129,7 @@ class Test(BaseTest): assert ('coingecko' in rv) rv = read_json_api(1800, 'rateslist?from=PART&to=BTC') - assert len(rv) == 2 + assert len(rv) == 1 def test_003_api(self): logging.info('---------- Test API') diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index ccffb28..695d055 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -204,9 +204,9 @@ def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey, with_c 'check_events_seconds': 1, 'check_xmr_swaps_seconds': 1, 'min_delay_event': 1, - 'max_delay_event': 5, + 'max_delay_event': 4, 'min_delay_event_short': 1, - 'max_delay_event_short': 5, + 'max_delay_event_short': 3, 'min_delay_retry': 2, 'max_delay_retry': 10, 'debug_ui': True,