From 8c6ea947baeeaa885998e1180886b219f4f34e86 Mon Sep 17 00:00:00 2001 From: tecnovert Date: Tue, 8 Oct 2024 00:52:27 +0200 Subject: [PATCH] script: Add min_amount offer setting. If min_amount is set offers will be created for amounts between "min_coin_from_amt" and "amount" in increments of "min_amount". --- scripts/createoffers.py | 72 ++++++++++++++++++++++-- tests/basicswap/extended/test_scripts.py | 62 +++++++++++++++++++- 2 files changed, 127 insertions(+), 7 deletions(-) diff --git a/scripts/createoffers.py b/scripts/createoffers.py index 5f7e531..8250109 100755 --- a/scripts/createoffers.py +++ b/scripts/createoffers.py @@ -7,6 +7,54 @@ """ 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. + "offers": [ + { + "name": Offer tenplate 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 valid bid when offer amount is variable. + "min_amount": If set offers will be created for amounts between "min_coin_from_amt" and "amount" in increments of "min_amount". + }, + ... + ], + "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.2' @@ -120,9 +168,10 @@ def readConfig(args, known_coins): num_changes += 1 offer_templates_map[offer_template['name']] = offer_template - if offer_template.get('min_coin_from_amt', 0) < offer_template['amount']: + min_offer_amount: float = float(offer_template.get('min_amount', offer_template['amount'])) + if float(offer_template.get('min_coin_from_amt', 0)) < min_offer_amount: print('Setting min_coin_from_amt for', offer_template['name']) - offer_template['min_coin_from_amt'] = offer_template['amount'] + offer_template['min_coin_from_amt'] = min_offer_amount num_changes += 1 if 'address' not in offer_template: @@ -280,10 +329,21 @@ def main(): if offers_found > 0: continue - if float(wallet_from['balance']) <= float(offer_template['min_coin_from_amt']): + max_offer_amount: float = offer_template['amount'] + min_offer_amount: float = offer_template.get('min_amount', max_offer_amount) + wallet_balance: float = float(wallet_from['balance']) + 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)) @@ -316,7 +376,7 @@ def main(): 'addr_from': -1 if template_from_addr == 'auto' else template_from_addr, 'coin_from': coin_from_data['ticker'], 'coin_to': coin_to_data['ticker'], - 'amt_from': offer_template['amount'], + '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, @@ -339,10 +399,10 @@ def main(): 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) - else: - time_between_offers = min_seconds_between_offers + script_state['delay_next_offer_before'] = int(time.time()) + time_between_offers write_state(args.statefile, script_state) diff --git a/tests/basicswap/extended/test_scripts.py b/tests/basicswap/extended/test_scripts.py index 8aad9fe..3b23228 100644 --- a/tests/basicswap/extended/test_scripts.py +++ b/tests/basicswap/extended/test_scripts.py @@ -28,6 +28,9 @@ import http.client from http.server import BaseHTTPRequestHandler, HTTPServer from urllib import parse +from tests.basicswap.common import ( + wait_for_balance, +) from tests.basicswap.util import ( read_json_api, waitForServer, @@ -557,6 +560,63 @@ class Test(unittest.TestCase): rv_stdout = result.stdout.decode().split('\n') ''' + def test_offer_min_amount(self): + waitForServer(self.delay_event, UI_PORT + 0) + waitForServer(self.delay_event, UI_PORT + 1) + + # 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) + + offer_amount = 200000000 + offer_min_amount = 20 + min_coin_from_amt = 10 + + xmr_wallet = read_json_api(UI_PORT + 0, 'wallets/xmr') + xmr_wallet_balance = float(xmr_wallet['balance']) + + expect_balance = offer_min_amount * 2 + min_coin_from_amt + 1 + if xmr_wallet_balance < expect_balance: + + post_json = { + 'value': expect_balance, + 'address': 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 + 1}/json/wallets/xmr', 'balance', expect_balance) + + xmr_wallet_balance = read_json_api(UI_PORT + 0, 'wallets/xmr')['balance'] + + assert (xmr_wallet_balance > offer_min_amount) + assert (xmr_wallet_balance < offer_amount) + + node0_test_config = { + 'offers': [ + { + 'name': 'test min amount', + 'coin_from': 'XMR', + 'coin_to': 'Particl', + 'amount': offer_amount, + 'min_amount': offer_min_amount, + 'minrate': 0.05, + 'amount_variable': True, + 'address': -1, + 'min_coin_from_amt': min_coin_from_amt, + 'max_coin_to_amt': -1 + } + ], + } + with open(self.node0_configfile, 'w') as fp: + json.dump(node0_test_config, fp, indent=4) + + result = subprocess.run(self.node0_args, stdout=subprocess.PIPE) + rv_stdout = result.stdout.decode().split('\n') + assert (len(get_created_offers(rv_stdout)) == 1) + def test_error_messages(self): waitForServer(self.delay_event, UI_PORT + 0) waitForServer(self.delay_event, UI_PORT + 1) @@ -586,7 +646,7 @@ class Test(unittest.TestCase): with open(self.node0_configfile, 'w') as fp: json.dump(node0_test1_config, fp, indent=4) - logging.info('Test that an offer is created') + logging.info('Test that an offer is not created') result = subprocess.run(self.node0_args, stdout=subprocess.PIPE) rv_stdout = result.stdout.decode().split('\n') assert (count_lines_with(rv_stdout, 'Error: Server failed to create offer: To amount above max') == 1)