diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index c99da05..0888d07 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -937,7 +937,9 @@ class BasicSwap(BaseApp): def start(self): import platform - self.log.info(f"Starting BasicSwap {__version__}, database v{self.db_version}\n\n") + self.log.info( + f"Starting BasicSwap {__version__}, database v{self.db_version}\n\n" + ) self.log.info(f"Python version: {platform.python_version()}") self.log.info(f"SQLite version: {sqlite3.sqlite_version}") self.log.info(f"Timezone offset: {time.timezone} ({time.tzname[0]})") @@ -7308,6 +7310,9 @@ class BasicSwap(BaseApp): if active_bid[2] != BidStates.SWAP_COMPLETED: num_not_completed += 1 max_concurrent_bids = opts.get("max_concurrent_bids", 1) + self.log.debug( + f"active_bids {num_not_completed}, max_concurrent_bids {max_concurrent_bids}" + ) if num_not_completed >= max_concurrent_bids: raise AutomationConstraint( "Already have {} bids to complete".format(num_not_completed) @@ -10524,6 +10529,7 @@ class BasicSwap(BaseApp): if filter_bid_id is not None: query_str += "AND bids.bid_id = :filter_bid_id " query_data["filter_bid_id"] = filter_bid_id + if offer_id is not None: query_str += "AND bids.offer_id = :filter_offer_id " query_data["filter_offer_id"] = offer_id @@ -10734,14 +10740,37 @@ class BasicSwap(BaseApp): finally: self.closeDB(cursor, commit=False) - def updateAutomationStrategy(self, strategy_id: int, data, note: str) -> None: + def updateAutomationStrategy(self, strategy_id: int, data: dict) -> None: + self.log.debug(f"updateAutomationStrategy {strategy_id}") try: cursor = self.openDB() strategy = firstOrNone( self.query(AutomationStrategy, cursor, {"record_id": strategy_id}) ) - strategy.data = json.dumps(data).encode("utf-8") - strategy.note = note + if "data" in data: + strategy.data = json.dumps(data["data"]).encode("utf-8") + self.log.debug("data {}".format(data["data"])) + if "note" in data: + strategy.note = data["note"] + if "label" in data: + strategy.label = data["label"] + if "only_known_identities" in data: + strategy.only_known_identities = int(data["only_known_identities"]) + + if "set_max_concurrent_bids" in data: + new_max_concurrent_bids = data["set_max_concurrent_bids"] + ensure( + isinstance(new_max_concurrent_bids, int), + "set_max_concurrent_bids must be an integer", + ) + strategy_data = ( + {} + if strategy.data is None + else json.loads(strategy.data.decode("utf-8")) + ) + strategy_data["max_concurrent_bids"] = new_max_concurrent_bids + strategy.data = json.dumps(strategy_data).encode("utf-8") + self.updateDB(strategy, cursor, ["record_id"]) finally: self.closeDB(cursor) diff --git a/basicswap/db.py b/basicswap/db.py index 5f3a07c..ac496e1 100644 --- a/basicswap/db.py +++ b/basicswap/db.py @@ -551,7 +551,7 @@ class AutomationStrategy(Table): label = Column("string") type_ind = Column("integer") only_known_identities = Column("integer") - num_concurrent = Column("integer") + num_concurrent = Column("integer") # Deprecated, use data["max_concurrent"] data = Column("blob") note = Column("string") diff --git a/basicswap/js_server.py b/basicswap/js_server.py index 7f9f16d..16a1a4e 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -718,6 +718,8 @@ def js_automationstrategies(self, url_split, post_string: str, is_json: bool) -> "sort_dir": "desc", } + strat_id = int(url_split[3]) if len(url_split) > 3 else None + if post_string != "": post_data = getFormData(post_string, is_json) @@ -738,15 +740,36 @@ def js_automationstrategies(self, url_split, post_string: str, is_json: bool) -> filters["limit"] > 0 and filters["limit"] <= PAGE_LIMIT ), "Invalid limit" - if len(url_split) > 3: - strat_id = int(url_split[3]) + set_data = {} + if have_data_entry(post_data, "set_label"): + set_data["label"] = get_data_entry(post_data, "set_label") + if have_data_entry(post_data, "set_data"): + set_data["data"] = json.loads(get_data_entry(post_data, "set_data")) + if have_data_entry(post_data, "set_note"): + set_data["note"] = get_data_entry(post_data, "set_note") + if have_data_entry(post_data, "set_only_known_identities"): + set_data["only_known_identities"] = get_data_entry( + post_data, "set_only_known_identities" + ) + + if have_data_entry(post_data, "set_max_concurrent_bids"): + if "data" in set_data: + raise ValueError("set_max_concurrent_bids can't be used with set_data") + set_data["set_max_concurrent_bids"] = int( + get_data_entry(post_data, "set_max_concurrent_bids") + ) + + if set_data: + ensure(strat_id is not None, "Must specify a strategy to modify") + swap_client.updateAutomationStrategy(strat_id, set_data) + + if strat_id is not None: strat_data = swap_client.getAutomationStrategy(strat_id) rv = { "record_id": strat_data.record_id, "label": strat_data.label, "type_ind": strat_data.type_ind, "only_known_identities": strat_data.only_known_identities, - "num_concurrent": strat_data.num_concurrent, "data": json.loads(strat_data.data.decode("utf-8")), "note": "" if strat_data.note is None else strat_data.note, } diff --git a/basicswap/ui/page_automation.py b/basicswap/ui/page_automation.py index 00df606..94b5518 100644 --- a/basicswap/ui/page_automation.py +++ b/basicswap/ui/page_automation.py @@ -115,9 +115,11 @@ def page_automation_strategy(self, url_split, post_string): show_edit_form = True if have_data_entry(form_data, "apply"): try: - data = json.loads(get_data_entry_or(form_data, "data", "")) - note = get_data_entry_or(form_data, "note", "") - swap_client.updateAutomationStrategy(strategy_id, data, note) + data = { + "data": json.loads(get_data_entry_or(form_data, "data", "")), + "note": get_data_entry_or(form_data, "note", ""), + } + swap_client.updateAutomationStrategy(strategy_id, data) messages.append("Updated") except Exception as e: err_messages.append(str(e)) diff --git a/tests/basicswap/extended/test_scripts.py b/tests/basicswap/extended/test_scripts.py index a6758ef..a2233ef 100644 --- a/tests/basicswap/extended/test_scripts.py +++ b/tests/basicswap/extended/test_scripts.py @@ -131,16 +131,31 @@ def clear_offers(delay_event, node_id) -> None: raise ValueError("clear_offers failed") -def wait_for_offers(delay_event, node_id, num_offers) -> None: +def wait_for_offers(delay_event, node_id, num_offers, offer_id=None) -> None: logging.info(f"Waiting for {num_offers} offers on node {node_id}") for i in range(20): delay_event.wait(1) - offers = read_json_api(UI_PORT + node_id, "offers") + offers = read_json_api( + UI_PORT + node_id, "offers" if offer_id is None else f"offers/{offer_id}" + ) if len(offers) >= num_offers: return raise ValueError("wait_for_offers failed") +def wait_for_bids(delay_event, node_id, num_bids, offer_id=None) -> None: + logging.info(f"Waiting for {num_bids} bids on node {node_id}") + for i in range(20): + delay_event.wait(1) + if offer_id is not None: + bids = read_json_api(UI_PORT + node_id, "bids", {"offer_id": offer_id}) + else: + bids = read_json_api(UI_PORT + node_id, "bids") + if len(bids) >= num_bids: + return bids + raise ValueError("wait_for_bids failed") + + def delete_file(filepath: str) -> None: if os.path.exists(filepath): os.remove(filepath) @@ -881,6 +896,126 @@ class Test(unittest.TestCase): assert math.isclose(float(bid["amt_from"]), 21.0) assert bid["addr_from"] == addr_bid_from + def test_auto_accept(self): + + waitForServer(self.delay_event, UI_PORT + 0) + waitForServer(self.delay_event, UI_PORT + 1) + + logging.info("Reset test") + clear_offers(self.delay_event, 0) + delete_file(self.node0_statefile) + delete_file(self.node1_statefile) + wait_for_offers(self.delay_event, 1, 0) + + logging.info("Prepare node 2 balance") + node2_xmr_wallet = read_json_api(UI_PORT + 2, "wallets/xmr") + node2_xmr_wallet_balance = float(node2_xmr_wallet["balance"]) + expect_balance = 300.0 + if node2_xmr_wallet_balance < expect_balance: + post_json = { + "value": expect_balance, + "address": node2_xmr_wallet["deposit_address"], + "sweepall": False, + } + json_rv = read_json_api(UI_PORT + 1, "wallets/xmr/withdraw", post_json) + assert len(json_rv["txid"]) == 64 + wait_for_balance( + self.delay_event, + f"http://127.0.0.1:{UI_PORT + 2}/json/wallets/xmr", + "balance", + expect_balance, + ) + + # Try post bids at the same time + from multiprocessing import Process + + def postBid(node_from, offer_id, amount): + post_json = {"offer_id": offer_id, "amount_from": amount} + read_json_api(UI_PORT + node_from, "bids/new", post_json) + + def test_bid_pair(amount_1, amount_2, expect_inactive, delay_event): + logging.debug(f"test_bid_pair {amount_1} {amount_2}, {expect_inactive}") + + wait_for_balance( + self.delay_event, + f"http://127.0.0.1:{UI_PORT + 2}/json/wallets/xmr", + "balance", + 100.0, + ) + + offer_json = { + "coin_from": "btc", + "coin_to": "xmr", + "amt_from": 10.0, + "amt_to": 100.0, + "amt_var": True, + "lockseconds": 3600, + "automation_strat_id": 1, + } + offer_id = read_json_api(UI_PORT + 0, "offers/new", offer_json)["offer_id"] + logging.debug(f"offer_id {offer_id}") + + wait_for_offers(self.delay_event, 1, 1, offer_id) + wait_for_offers(self.delay_event, 2, 1, offer_id) + + pbid1 = Process(target=postBid, args=(1, offer_id, amount_1)) + pbid2 = Process(target=postBid, args=(2, offer_id, amount_2)) + + pbid1.start() + pbid2.start() + pbid1.join() + pbid2.join() + + for i in range(5): + logging.info("Waiting for bids to settle") + + delay_event.wait(8) + bids = wait_for_bids(self.delay_event, 0, 2, offer_id) + + if any(bid["bid_state"] == "Receiving" for bid in bids): + continue + break + + num_received_state = 0 + for bid in bids: + if bid["bid_state"] == "Received": + num_received_state += 1 + assert num_received_state == expect_inactive + + # Bids with a combined value less than the offer value should both be accepted + test_bid_pair(1.1, 1.2, 0, self.delay_event) + + # Only one bid of bids with a combined value greater than the offer value should be accepted + test_bid_pair(1.1, 9.2, 1, self.delay_event) + + logging.debug("Change max_concurrent_bids to 1") + try: + json_rv = read_json_api(UI_PORT + 0, "automationstrategies/1") + assert json_rv["data"]["max_concurrent_bids"] == 5 + + data = json_rv["data"] + data["max_concurrent_bids"] = 1 + post_json = { + "set_label": "changed", + "set_note": "changed", + "set_data": json.dumps(data), + } + json_rv = read_json_api(UI_PORT + 0, "automationstrategies/1", post_json) + assert json_rv["data"]["max_concurrent_bids"] == 1 + assert json_rv["label"] == "changed" + assert json_rv["note"] == "changed" + + # Only one bid should be active + test_bid_pair(1.1, 1.2, 1, self.delay_event) + + finally: + logging.debug("Reset max_concurrent_bids") + post_json = { + "set_max_concurrent_bids": 5, + } + json_rv = read_json_api(UI_PORT + 0, "automationstrategies/1", post_json) + assert json_rv["data"]["max_concurrent_bids"] == 5 + if __name__ == "__main__": unittest.main() diff --git a/tests/basicswap/extended/test_xmr_persistent.py b/tests/basicswap/extended/test_xmr_persistent.py index b27320a..a833119 100644 --- a/tests/basicswap/extended/test_xmr_persistent.py +++ b/tests/basicswap/extended/test_xmr_persistent.py @@ -18,7 +18,7 @@ python tests/basicswap/extended/test_xmr_persistent.py # Copy coin releases to permanent storage for faster subsequent startups -cp -r ${TEST_PATH}/bin/ ~/tmp/basicswap_bin +cp -r ${TEST_PATH}/bin/ ~/tmp/basicswap_bin/ """