#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2023-2024 tecnovert # Copyright (c) 2024-2025 The Basicswap developers # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. """ Create offers { "min_seconds_between_offers": Add a random delay between creating offers between min and max, default 60. "max_seconds_between_offers": ^, default "min_seconds_between_offers" * 4 "min_seconds_between_bids": Add a random delay between creating bids between min and max, default 60. "max_seconds_between_bids": ^, default "min_seconds_between_bids" * 4 "wallet_port_override": Used for testing. "prune_state_delay": Seconds between pruning old state data, set to 0 to disable pruning. "main_loop_delay": Seconds between main loop iterations. "prune_state_after_seconds": Seconds to keep old state data for. "auth": Basicswap API auth string, e.g., "admin:password". Ignored if client auth is not enabled. "offers": [ { "name": Offer template name, eg "Offer 0", will be automatically renamed if not unique. "coin_from": Coin you send. "coin_to": Coin you receive. "amount": Amount to create the offer for. "minrate": Rate below which the offer won't drop. "ratetweakpercent": modify the offer rate from the fetched value, can be negative. "amount_variable": bool, bidder can set a different amount "address": Address offer is sent from, default will generate a new address per offer. "min_coin_from_amt": Won't generate offers if the wallet would drop below min_coin_from_amt. "offer_valid_seconds": Seconds that the generated offers will be valid for. # Optional "enabled": Set to false to ignore offer template. "swap_type": Type of swap, defaults to "adaptor_sig" "min_swap_amount": Sets "amt_bid_min" on the offer, minimum purchase quantity when offer amount is variable. "amount_step": If set offers will be created for amount values between "amount" and "min_coin_from_amt" in decrements of "amount_step". }, ... ], "bids": [ { "name": Bid template name, must be unique, eg "Bid 0", will be automatically renamed if not unique. "coin_from": Coin you receive. "coin_to": Coin you send. "amount": amount to bid. "max_rate": Maximum rate for bids. "min_coin_to_balance": Won't send bids if wallet amount of "coin_to" would drop below. # Optional "enabled": Set to false to ignore bid template. "max_concurrent": Maximum number of bids to have active at once, default 1. "amount_variable": Can send bids below the set "amount" where possible if true. "max_coin_from_balance": Won't send bids if wallet amount of "coin_from" would be above. "address": Address offer is sent from, default will generate a new address per bid. }, ... ] } """ __version__ = "0.3" import argparse import json import os import random import shutil import signal import sys import threading import time import traceback import urllib import urllib.error import base64 from urllib.request import urlopen delay_event = threading.Event() coins_map = {} read_json_api = None read_json_api_wallet = None DEFAULT_CONFIG_FILE: str = "createoffers.json" DEFAULT_STATE_FILE: str = "createoffers_state.json" def post_req(url: str, json_data=None, auth_header_val=None): req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) if auth_header_val: req.add_header("Authorization", auth_header_val) if json_data: req.add_header("Content-Type", "application/json; charset=utf-8") post_bytes = json.dumps(json_data).encode("utf-8") req.add_header("Content-Length", len(post_bytes)) else: post_bytes = None return urlopen(req, data=post_bytes, timeout=300) def make_json_api_func(host: str, port: int, auth_string: str = None): host = host port = port _auth_header_val = None _auth_required_confirmed = False if auth_string: try: if auth_string and ":" in auth_string: try: auth_bytes = auth_string.encode("utf-8") _auth_header_val = "Basic " + base64.b64encode(auth_bytes).decode( "ascii" ) except Exception as e: print( f"Warning: Could not process auth string '{auth_string}': {e}" ) _auth_header_val = None elif auth_string: print( "Warning: Auth string is not in 'username:password' format. Ignoring." ) except Exception as e: print(f"Error processing authentication: {e}") def api_func(path=None, json_data=None, timeout=300): nonlocal _auth_required_confirmed url = f"http://{host}:{port}/json" if path is not None: url += "/" + path current_auth_header = _auth_header_val if _auth_required_confirmed else None try: if json_data is not None: response_obj = post_req( url, json_data, auth_header_val=current_auth_header ) else: headers = {"User-Agent": "Mozilla/5.0"} if current_auth_header: headers["Authorization"] = current_auth_header req = urllib.request.Request(url, headers=headers) response_obj = urlopen(req, timeout=timeout) response_bytes = response_obj.read() return json.loads(response_bytes) except urllib.error.HTTPError as e: if e.code == 401 and not _auth_required_confirmed: if _auth_header_val: print( "Server requires authentication, retrying with credentials..." ) _auth_required_confirmed = True try: if json_data is not None: response_obj = post_req( url, json_data, auth_header_val=_auth_header_val ) else: headers = { "User-Agent": "Mozilla/5.0", "Authorization": _auth_header_val, } req = urllib.request.Request(url, headers=headers) response_obj = urlopen(req, timeout=timeout) response_bytes = response_obj.read() return json.loads(response_bytes) except urllib.error.HTTPError as retry_e: if retry_e.code == 401: raise ValueError( "Authentication failed: Invalid credentials provided in 'auth' key." ) else: print(f"Error during authenticated API request: {retry_e}") raise retry_e except Exception as retry_e: print(f"Error during authenticated API request: {retry_e}") raise retry_e else: raise ValueError( "Server requires authentication (401), but no 'auth' key found or properly formatted in config file." ) else: if e.code == 401 and _auth_required_confirmed: raise ValueError( "Authentication failed: Invalid credentials provided in 'auth' key." ) else: raise e except Exception as e: print(f"Error during API connection: {e}") raise e return api_func def signal_handler(sig, frame) -> None: os.write(sys.stdout.fileno(), f"Signal {sig} detected.\n".encode("utf-8")) delay_event.set() def findCoin(coin: str, known_coins) -> str: for known_coin in known_coins: if ( known_coin["name"].lower() == coin.lower() or known_coin["ticker"].lower() == coin.lower() ): if known_coin["active"] is False: raise ValueError(f"Inactive coin {coin}") return known_coin["name"] raise ValueError(f"Unknown coin {coin}") def readConfig(args, known_coins): config_path: str = args.configfile num_changes: int = 0 with open(config_path) as fs: config = json.load(fs) if "offers" not in config: config["offers"] = [] if "bids" not in config: config["bids"] = [] if "min_seconds_between_offers" not in config: config["min_seconds_between_offers"] = 60 print("Set min_seconds_between_offers", config["min_seconds_between_offers"]) num_changes += 1 if "max_seconds_between_offers" not in config: config["max_seconds_between_offers"] = config["min_seconds_between_offers"] * 4 print("Set max_seconds_between_offers", config["max_seconds_between_offers"]) num_changes += 1 if "min_seconds_between_bids" not in config: config["min_seconds_between_bids"] = 60 print("Set min_seconds_between_bids", config["min_seconds_between_bids"]) num_changes += 1 if "max_seconds_between_bids" not in config: config["max_seconds_between_bids"] = config["min_seconds_between_bids"] * 4 print("Set max_seconds_between_bids", config["max_seconds_between_bids"]) num_changes += 1 offer_templates = config["offers"] offer_templates_map = {} num_enabled = 0 for i, offer_template in enumerate(offer_templates): num_enabled += 1 if offer_template.get("enabled", True) else 0 if "name" not in offer_template: print("Naming offer template", i) offer_template["name"] = f"Offer {i}" num_changes += 1 if offer_template["name"] in offer_templates_map: print("Renaming offer template", offer_template["name"]) original_name = offer_template["name"] offset = 2 while f"{original_name}_{offset}" in offer_templates_map: offset += 1 offer_template["name"] = f"{original_name}_{offset}" num_changes += 1 offer_templates_map[offer_template["name"]] = offer_template if "amount_step" not in offer_template: if offer_template.get("min_coin_from_amt", 0) < offer_template["amount"]: print("Setting min_coin_from_amt for", offer_template["name"]) offer_template["min_coin_from_amt"] = offer_template["amount"] num_changes += 1 else: if "min_coin_from_amt" not in offer_template: print("Setting min_coin_from_amt for", offer_template["name"]) offer_template["min_coin_from_amt"] = 0 num_changes += 1 if "address" not in offer_template: print("Setting address to auto for offer", offer_template["name"]) offer_template["address"] = "auto" num_changes += 1 if "ratetweakpercent" not in offer_template: print("Setting ratetweakpercent to 0 for offer", offer_template["name"]) offer_template["ratetweakpercent"] = 0 num_changes += 1 if "amount_variable" not in offer_template: print("Setting amount_variable to True for offer", offer_template["name"]) offer_template["amount_variable"] = True num_changes += 1 if offer_template.get("enabled", True) is False: continue offer_template["coin_from"] = findCoin(offer_template["coin_from"], known_coins) offer_template["coin_to"] = findCoin(offer_template["coin_to"], known_coins) config["num_enabled_offers"] = num_enabled bid_templates = config["bids"] bid_templates_map = {} num_enabled = 0 for i, bid_template in enumerate(bid_templates): num_enabled += 1 if bid_template.get("enabled", True) else 0 if "name" not in bid_template: print("Naming bid template", i) bid_template["name"] = f"Bid {i}" num_changes += 1 if bid_template["name"] in bid_templates_map: print("Renaming bid template", bid_template["name"]) original_name = bid_template["name"] offset = 2 while f"{original_name}_{offset}" in bid_templates_map: offset += 1 bid_template["name"] = f"{original_name}_{offset}" num_changes += 1 bid_templates_map[bid_template["name"]] = bid_template if bid_template.get("min_swap_amount", 0.0) < 0.001: print("Setting min_swap_amount for bid template", bid_template["name"]) bid_template["min_swap_amount"] = 0.001 if "address" not in bid_template: print("Setting address to auto for bid", bid_template["name"]) bid_template["address"] = "auto" num_changes += 1 if bid_template.get("enabled", True) is False: continue bid_template["coin_from"] = findCoin(bid_template["coin_from"], known_coins) bid_template["coin_to"] = findCoin(bid_template["coin_to"], known_coins) config["num_enabled_bids"] = num_enabled config["main_loop_delay"] = config.get("main_loop_delay", 60) if config["main_loop_delay"] < 10: print("Setting main_loop_delay to 10") config["main_loop_delay"] = 10 num_changes += 1 if config["main_loop_delay"] > 1000: print("Setting main_loop_delay to 1000") config["main_loop_delay"] = 1000 num_changes += 1 config["prune_state_delay"] = config.get("prune_state_delay", 120) seconds_in_day: int = 86400 config["prune_state_after_seconds"] = config.get( "prune_state_after_seconds", seconds_in_day * 7 ) if config["prune_state_after_seconds"] < seconds_in_day: print(f"Setting prune_state_after_seconds to {seconds_in_day}") config["prune_state_after_seconds"] = seconds_in_day num_changes += 1 if num_changes > 0: shutil.copyfile(config_path, config_path + ".last") with open(config_path, "w") as fp: json.dump(config, fp, indent=4) return config def write_state(statefile, script_state): if os.path.exists(statefile): shutil.copyfile(statefile, statefile + ".last") with open(statefile, "w") as fp: json.dump(script_state, fp, indent=4) def process_offers(args, config, script_state) -> None: offer_templates = config["offers"] if len(offer_templates) < 1: return if args.debug: print( "Processing {} offer template{}".format( config["num_enabled_offers"], "s" if config["num_enabled_offers"] != 1 else "", ) ) random.shuffle(offer_templates) sent_offers = read_json_api("sentoffers", {"active": "active"}) for offer_template in offer_templates: if offer_template.get("enabled", True) is False: continue offers_found = 0 coin_from_data = coins_map[offer_template["coin_from"]] coin_to_data = coins_map[offer_template["coin_to"]] wallet_from = read_json_api_wallet( "wallets/{}".format(coin_from_data["ticker"]) ) coin_ticker = coin_from_data["ticker"] if coin_ticker == "PART" and "variant" in coin_from_data: coin_variant = coin_from_data["variant"] if coin_variant == "Anon": coin_from_data_name = "PART_ANON" wallet_balance: float = float(wallet_from["anon_balance"]) elif coin_variant == "Blind": coin_from_data_name = "PART_BLIND" wallet_balance: float = float(wallet_from["blind_balance"]) else: raise ValueError(f"{coin_ticker} variant {coin_variant} not handled") else: coin_from_data_name = coin_ticker wallet_balance: float = float(wallet_from["balance"]) for offer in sent_offers: created_offers = script_state.get("offers", {}) prev_template_offers = created_offers.get(offer_template["name"], {}) if next( (x for x in prev_template_offers if x["offer_id"] == offer["offer_id"]), None, ): offers_found += 1 if wallet_balance <= float(offer_template["min_coin_from_amt"]): offer_id = offer["offer_id"] print( "Revoking offer {}, wallet from balance below minimum".format( offer_id ) ) result = read_json_api(f"revokeoffer/{offer_id}") print("revokeoffer", result) if offers_found > 0: continue max_offer_amount: float = offer_template["amount"] min_offer_amount: float = offer_template.get("amount_step", max_offer_amount) min_wallet_from_amount: float = float(offer_template["min_coin_from_amt"]) if wallet_balance - min_offer_amount <= min_wallet_from_amount: print( "Skipping template {}, wallet from balance below minimum".format( offer_template["name"] ) ) continue offer_amount: float = max_offer_amount if wallet_balance - max_offer_amount <= min_wallet_from_amount: available_balance: float = wallet_balance - min_wallet_from_amount min_steps: int = available_balance // min_offer_amount assert min_steps > 0 # Should not be possible, checked above offer_amount = min_offer_amount * min_steps delay_next_offer_before = script_state.get("delay_next_offer_before", 0) if delay_next_offer_before > int(time.time()): print("Delaying offers until {}".format(delay_next_offer_before)) break """ received_offers = read_json_api(args.port, 'offers', {'active': 'active', 'include_sent': False, 'coin_from': coin_from_data['id'], 'coin_to': coin_to_data['id']}) print('received_offers', received_offers) TODO - adjust rates based on existing offers """ rates = read_json_api( "rates", {"coin_from": coin_from_data["id"], "coin_to": coin_to_data["id"]}, ) print("Rates", rates) coingecko_rate = float(rates["coingecko"]["rate_inferred"]) use_rate = coingecko_rate if offer_template["ratetweakpercent"] != 0: print( "Adjusting rate {} by {}%.".format( use_rate, offer_template["ratetweakpercent"] ) ) tweak = offer_template["ratetweakpercent"] / 100.0 use_rate += use_rate * tweak if use_rate < offer_template["minrate"]: print("Warning: Clamping rate to minimum.") use_rate = offer_template["minrate"] print("Creating offer for: {} at rate: {}".format(offer_template, use_rate)) template_from_addr = offer_template["address"] offer_data = { "addr_from": (-1 if template_from_addr == "auto" else template_from_addr), "coin_from": coin_from_data_name, "coin_to": coin_to_data["ticker"], "amt_from": offer_amount, "amt_var": offer_template["amount_variable"], "valid_for_seconds": offer_template.get( "offer_valid_seconds", config.get("offer_valid_seconds", 3600) ), "rate": use_rate, "swap_type": offer_template.get("swap_type", "adaptor_sig"), "lockhrs": "24", "automation_strat_id": 1, } if "min_swap_amount" in offer_template: offer_data["amt_bid_min"] = offer_template["min_swap_amount"] if args.debug: print("offer data {}".format(offer_data)) new_offer = read_json_api("offers/new", offer_data) if "error" in new_offer: raise ValueError( "Server failed to create offer: {}".format(new_offer["error"]) ) print("New offer: {}".format(new_offer["offer_id"])) if "offers" not in script_state: script_state["offers"] = {} template_name = offer_template["name"] if template_name not in script_state["offers"]: script_state["offers"][template_name] = [] script_state["offers"][template_name].append( {"offer_id": new_offer["offer_id"], "time": int(time.time())} ) max_seconds_between_offers = config["max_seconds_between_offers"] min_seconds_between_offers = config["min_seconds_between_offers"] time_between_offers = min_seconds_between_offers if max_seconds_between_offers > min_seconds_between_offers: time_between_offers = random.randint( min_seconds_between_offers, max_seconds_between_offers ) script_state["delay_next_offer_before"] = int(time.time()) + time_between_offers write_state(args.statefile, script_state) def process_bids(args, config, script_state) -> None: bid_templates = config["bids"] if len(bid_templates) < 1: return random.shuffle(bid_templates) if args.debug: print( "Processing {} bid template{}".format( config["num_enabled_bids"], "s" if config["num_enabled_bids"] != 1 else "", ) ) for bid_template in bid_templates: if bid_template.get("enabled", True) is False: continue delay_next_bid_before = script_state.get("delay_next_bid_before", 0) if delay_next_bid_before > int(time.time()): print("Delaying bids until {}".format(delay_next_bid_before)) break # Check bids in progress max_concurrent = bid_template.get("max_concurrent", 1) if "bids" not in script_state: script_state["bids"] = {} template_name = bid_template["name"] if template_name not in script_state["bids"]: script_state["bids"][template_name] = [] previous_bids = script_state["bids"][template_name] bids_in_progress: int = 0 for previous_bid in previous_bids: if not previous_bid["active"]: continue previous_bid_id = previous_bid["bid_id"] previous_bid_info = read_json_api(f"bids/{previous_bid_id}") bid_state = previous_bid_info["bid_state"] if bid_state in ( "Completed", "Timed-out", "Abandoned", "Error", "Rejected", ): print(f"Marking bid inactive {previous_bid_id}, state {bid_state}") previous_bid["active"] = False write_state(args.statefile, script_state) continue if bid_state in ("Sent", "Received") and previous_bid_info[ "expired_at" ] < int(time.time()): print(f"Marking bid inactive {previous_bid_id}, expired") previous_bid["active"] = False write_state(args.statefile, script_state) continue bids_in_progress += 1 if bids_in_progress >= max_concurrent: print("Max concurrent bids reached for template") continue # Bidder sends coin_to and receives coin_from coin_from_data = coins_map[bid_template["coin_from"]] coin_to_data = coins_map[bid_template["coin_to"]] page_limit: int = 25 offers_options = { "active": "active", "include_sent": False, "coin_from": coin_from_data["id"], "coin_to": coin_to_data["id"], "with_extra_info": True, "sort_by": "rate", "sort_dir": "asc", "offset": 0, "limit": page_limit, } received_offers = [] for i in range(1000000): # for i in itertools.count() page_offers = read_json_api("offers", offers_options) if len(page_offers) < 1: break received_offers += page_offers offers_options["offset"] = offers_options["offset"] + page_limit if i > 100: print(f"Warning: Broke offers loop at: {i}") break if args.debug: print("Received Offers", received_offers) for offer in received_offers: offer_id = offer["offer_id"] offer_amount = float(offer["amount_from"]) offer_rate = float(offer["rate"]) bid_amount = bid_template["amount"] min_swap_amount = bid_template.get( "min_swap_amount", 0.01 ) # TODO: Make default vary per coin can_adjust_offer_amount: bool = offer["amount_negotiable"] can_adjust_bid_amount: bool = bid_template.get("amount_variable", True) can_adjust_amount: bool = can_adjust_offer_amount and can_adjust_bid_amount if offer_amount < min_swap_amount: if args.debug: print(f"Offer amount below min swap amount bid {offer_id}") continue if can_adjust_offer_amount is False and offer_amount > bid_amount: if args.debug: print(f"Bid amount too low for offer {offer_id}") continue if bid_amount > offer_amount: if can_adjust_bid_amount: bid_amount = offer_amount else: if args.debug: print(f"Bid amount too high for offer {offer_id}") continue if offer_rate > bid_template["maxrate"]: if args.debug: print(f"Bid rate too low for offer {offer_id}") continue sent_bids = read_json_api( "sentbids", { "offer_id": offer["offer_id"], "with_available_or_active": True, }, ) if len(sent_bids) > 0: if args.debug: print(f"Already bidding on offer {offer_id}") continue offer_identity = read_json_api("identities/{}".format(offer["addr_from"])) if "address" in offer_identity: id_offer_from = offer_identity automation_override = id_offer_from["automation_override"] if automation_override == 2: if args.debug: print( f"Not bidding on offer {offer_id}, automation_override ({automation_override})." ) continue if automation_override == 1: if args.debug: print( "Offer address from {}, set to always accept.".format( offer["addr_from"] ) ) else: successful_sent_bids = id_offer_from["num_sent_bids_successful"] failed_sent_bids = id_offer_from["num_sent_bids_failed"] if failed_sent_bids > 3 and failed_sent_bids > successful_sent_bids: if args.debug: print( f"Not bidding on offer {offer_id}, too many failed bids ({failed_sent_bids})." ) continue validateamount: bool = False max_coin_from_balance = bid_template.get("max_coin_from_balance", -1) if max_coin_from_balance > 0: wallet_from = read_json_api_wallet( "wallets/{}".format(coin_from_data["ticker"]) ) total_balance_from = float(wallet_from["balance"]) + float( wallet_from["unconfirmed"] ) if args.debug: print(f"Total coin from balance {total_balance_from}") if total_balance_from + bid_amount > max_coin_from_balance: if ( can_adjust_amount and max_coin_from_balance - total_balance_from > min_swap_amount ): bid_amount = max_coin_from_balance - total_balance_from validateamount = True print(f"Reduced bid amount to {bid_amount}") else: if args.debug: print( f"Bid amount would exceed maximum wallet total for offer {offer_id}" ) continue min_coin_to_balance = bid_template["min_coin_to_balance"] if min_coin_to_balance > 0: wallet_to = read_json_api_wallet( "wallets/{}".format(coin_to_data["ticker"]) ) total_balance_to = float(wallet_to["balance"]) + float( wallet_to["unconfirmed"] ) if args.debug: print(f"Total coin to balance {total_balance_to}") swap_amount_to = bid_amount * offer_rate if total_balance_to - swap_amount_to < min_coin_to_balance: if can_adjust_amount: adjusted_swap_amount_to = total_balance_to - min_coin_to_balance adjusted_bid_amount = adjusted_swap_amount_to / offer_rate if adjusted_bid_amount > min_swap_amount: bid_amount = adjusted_bid_amount validateamount = True print(f"Reduced bid amount to {bid_amount}") swap_amount_to = adjusted_bid_amount * offer_rate if total_balance_to - swap_amount_to < min_coin_to_balance: if args.debug: print( f"Bid amount would exceed minimum coin to wallet total for offer {offer_id}" ) continue if validateamount: bid_amount = read_json_api( "validateamount", { "coin": coin_from_data["ticker"], "amount": bid_amount, "method": "rounddown", }, ) bid_data = { "offer_id": offer["offer_id"], "amount_from": bid_amount, } if "address" in bid_template: addr_from = bid_template["address"] if addr_from != -1 and addr_from != "auto": bid_data["addr_from"] = addr_from if config.get("test_mode", False): print("Would create bid: {}".format(bid_data)) bid_id = "simulated" else: if args.debug: print("Creating bid: {}".format(bid_data)) new_bid = read_json_api("bids/new", bid_data) if "error" in new_bid: raise ValueError( "Server failed to create bid: {}".format(new_bid["error"]) ) print( "New bid: {} on offer {}".format( new_bid["bid_id"], offer["offer_id"] ) ) bid_id = new_bid["bid_id"] script_state["bids"][template_name].append( {"bid_id": bid_id, "time": int(time.time()), "active": True} ) max_seconds_between_bids = config["max_seconds_between_bids"] min_seconds_between_bids = config["min_seconds_between_bids"] if max_seconds_between_bids > min_seconds_between_bids: time_between_bids = random.randint( min_seconds_between_bids, max_seconds_between_bids ) else: time_between_bids = min_seconds_between_bids script_state["delay_next_bid_before"] = int(time.time()) + time_between_bids write_state(args.statefile, script_state) break # Create max one bid per iteration def prune_script_state(now, args, config, script_state): if args.debug: print("Pruning script state.") removed_offers: int = 0 removed_bids: int = 0 max_ttl: int = config["prune_state_after_seconds"] if "offers" in script_state: for template_name, template_group in script_state["offers"].items(): offers_to_remove = [] for offer in template_group: if now - offer["time"] > max_ttl: offers_to_remove.append(offer["offer_id"]) for offer_id in offers_to_remove: for i, offer in enumerate(template_group): if offer_id == offer["offer_id"]: del template_group[i] removed_offers += 1 break if "bids" in script_state: for template_name, template_group in script_state["bids"].items(): bids_to_remove = [] for bid in template_group: if now - bid["time"] > max_ttl: bids_to_remove.append(bid["bid_id"]) for bid_id in bids_to_remove: for i, bid in enumerate(template_group): if bid_id == bid["bid_id"]: del template_group[i] removed_bids += 1 break if removed_offers > 0 or removed_bids > 0: print( "Pruned {} offer{} and {} bid{} from script state.".format( removed_offers, "s" if removed_offers != 1 else "", removed_bids, "s" if removed_bids != 1 else "", ) ) script_state["time_last_pruned_state"] = now write_state(args.statefile, script_state) def main(): global read_json_api, read_json_api_wallet parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "-v", "--version", action="version", version="%(prog)s {version}".format(version=__version__), ) parser.add_argument( "--host", dest="host", help="RPC host (default=127.0.0.1)", type=str, default="127.0.0.1", required=False, ) parser.add_argument( "--port", dest="port", help="RPC port (default=12700)", type=int, default=12700, required=False, ) parser.add_argument( "--oneshot", dest="oneshot", help="Exit after one iteration (default=false)", required=False, action="store_true", ) parser.add_argument( "--debug", dest="debug", help="Print extra debug messages (default=false)", required=False, action="store_true", ) parser.add_argument( "--configfile", dest="configfile", help=f"config file path (default={DEFAULT_CONFIG_FILE})", type=str, default=DEFAULT_CONFIG_FILE, required=False, ) parser.add_argument( "--statefile", dest="statefile", help=f"state file path (default={DEFAULT_STATE_FILE})", type=str, default=DEFAULT_STATE_FILE, required=False, ) args = parser.parse_args() if not os.path.exists(args.configfile): raise ValueError(f'Config file "{args.configfile}" not found.') try: with open(args.configfile) as fs: initial_config = json.load(fs) except Exception as e: print(f"Error reading config file {args.configfile}: {e}") sys.exit(1) auth_info = initial_config.get("auth") read_json_api = make_json_api_func(args.host, args.port, auth_info) wallet_api_port_override = initial_config.get("wallet_port_override") if wallet_api_port_override: read_json_api_wallet_auth = make_json_api_func( args.host, int(wallet_api_port_override), auth_info ) else: read_json_api_wallet_auth = read_json_api_wallet try: print("Checking API connection...") known_coins = read_json_api("coins") for known_coin in known_coins: coins_map[known_coin["name"]] = known_coin print("API connection successful.") except ValueError as e: print(f"\nError: {e}") print( 'Please ensure the \'auth\' key in your config file is correct (e.g., "auth": "username:password")' ) sys.exit(1) except urllib.error.URLError as e: print( f"\nError: Could not connect to Basicswap API at http://{args.host}:{args.port}" ) print(f"Reason: {e.reason}") print("Please ensure Basicswap is running and accessible.") sys.exit(1) except Exception as e: print(f"\nError during initial API connection: {e}") if args.debug: traceback.print_exc() sys.exit(1) script_state = {} if os.path.exists(args.statefile): with open(args.statefile) as fs: script_state = json.load(fs) signal.signal(signal.SIGINT, signal_handler) while not delay_event.is_set(): # Read config each iteration so it can be modified without restarting config = readConfig(args, known_coins) # override wallet api calls for testing if "wallet_port_override" in config: wallet_api_port = int(config["wallet_port_override"]) print(f"Overriding wallet api port: {wallet_api_port}") read_json_api_wallet = read_json_api_wallet_auth else: read_json_api_wallet = read_json_api try: process_offers(args, config, script_state) process_bids(args, config, script_state) now = int(time.time()) prune_state_delay = config["prune_state_delay"] if prune_state_delay > 0: if ( now - script_state.get("time_last_pruned_state", 0) > prune_state_delay ): prune_script_state(now, args, config, script_state) except Exception as e: print(f"Error: {e}.") if args.debug: traceback.print_exc() if args.oneshot: break print("Looping indefinitely, ctrl+c to exit.") delay_event.wait(config["main_loop_delay"]) print("Done.") if __name__ == "__main__": main()