From ef71ca7ef415873ad2ef9dc98e1c97af5d2b2836 Mon Sep 17 00:00:00 2001 From: tecnovert Date: Mon, 9 Jan 2023 00:53:59 +0200 Subject: [PATCH] scripts: Start example offer script. --- basicswap/__init__.py | 2 +- basicswap/js_server.py | 66 +++++----- basicswap/templates/offer.html | 2 +- basicswap/ui/page_offers.py | 35 +++-- scripts/.gitignore | 1 + scripts/createoffers.py | 157 +++++++++++++++++++++++ tests/basicswap/test_partblind_xmr.py | 1 - tests/basicswap/test_reload.py | 7 +- tests/basicswap/test_xmr.py | 3 +- tests/basicswap/test_xmr_bids_offline.py | 2 +- 10 files changed, 224 insertions(+), 52 deletions(-) create mode 100644 scripts/.gitignore create mode 100755 scripts/createoffers.py diff --git a/basicswap/__init__.py b/basicswap/__init__.py index 002f394..d77e35a 100644 --- a/basicswap/__init__.py +++ b/basicswap/__init__.py @@ -1,3 +1,3 @@ name = "basicswap" -__version__ = "0.11.56" +__version__ = "0.11.57" diff --git a/basicswap/js_server.py b/basicswap/js_server.py index e3f3d25..8ed3815 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2020-2022 tecnovert +# Copyright (c) 2020-2023 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -49,11 +49,6 @@ def getFormData(post_string, is_json): return form_data -def js_error(self, error_str): - error_str_json = json.dumps({'error': error_str}) - return bytes(error_str_json, 'UTF-8') - - def withdraw_coin(swap_client, coin_type, post_string, is_json): post_data = getFormData(post_string, is_json) @@ -73,17 +68,24 @@ def withdraw_coin(swap_client, coin_type, post_string, is_json): return {'txid': txid_hex} -def js_coins(self, url_split, post_string, is_json): +def js_error(self, error_str) -> bytes: + error_str_json = json.dumps({'error': error_str}) + return bytes(error_str_json, 'UTF-8') + + +def js_coins(self, url_split, post_string, is_json) -> bytes: swap_client = self.server.swap_client coins = [] for coin in Coins: cc = swap_client.coin_clients[coin] + coin_chainparams = chainparams[cc['coin']] entry = { 'id': int(coin), - 'ticker': chainparams[cc['coin']]['ticker'], + 'ticker': coin_chainparams['ticker'], 'name': getCoinName(coin), 'active': False if cc['connection_type'] == 'none' else True, + 'decimal_places': coin_chainparams['decimal_places'], } if coin == Coins.PART_ANON: entry['variant'] = 'Anon' @@ -124,7 +126,7 @@ def js_wallets(self, url_split, post_string, is_json): return bytes(json.dumps(swap_client.getWalletsInfo({'ticker_key': True})), 'UTF-8') -def js_offers(self, url_split, post_string, is_json, sent=False): +def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes: swap_client = self.server.swap_client swap_client.checkSystemStatus() offer_id = None @@ -162,11 +164,13 @@ def js_offers(self, url_split, post_string, is_json, sent=False): assert (sort_dir in ['asc', 'desc']), 'Invalid sort dir' filters['sort_dir'] = sort_dir - if b'offset' in post_data: + if have_data_entry(post_data, 'offset'): filters['offset'] = int(get_data_entry(post_data, 'offset')) - if b'limit' in post_data: + if have_data_entry(post_data, 'limit'): filters['limit'] = int(get_data_entry(post_data, 'limit')) assert (filters['limit'] > 0 and filters['limit'] <= PAGE_LIMIT), 'Invalid limit' + if have_data_entry(post_data, 'active'): + filters['active'] = get_data_entry(post_data, 'active') offers = swap_client.listOffers(sent, filters) rv = [] @@ -190,11 +194,11 @@ def js_offers(self, url_split, post_string, is_json, sent=False): return bytes(json.dumps(rv), 'UTF-8') -def js_sentoffers(self, url_split, post_string, is_json): - return self.js_offers(url_split, post_string, is_json, True) +def js_sentoffers(self, url_split, post_string, is_json) -> bytes: + return js_offers(self, url_split, post_string, is_json, True) -def js_bids(self, url_split, post_string, is_json): +def js_bids(self, url_split, post_string, is_json) -> bytes: swap_client = self.server.swap_client swap_client.checkSystemStatus() if len(url_split) > 3: @@ -287,19 +291,19 @@ def js_bids(self, url_split, post_string, is_json): } for b in bids]), 'UTF-8') -def js_sentbids(self, url_split, post_string, is_json): +def js_sentbids(self, url_split, post_string, is_json) -> bytes: swap_client = self.server.swap_client swap_client.checkSystemStatus() return bytes(json.dumps(swap_client.listBids(sent=True)), 'UTF-8') -def js_network(self, url_split, post_string, is_json): +def js_network(self, url_split, post_string, is_json) -> bytes: swap_client = self.server.swap_client swap_client.checkSystemStatus() return bytes(json.dumps(swap_client.get_network_info()), 'UTF-8') -def js_revokeoffer(self, url_split, post_string, is_json): +def js_revokeoffer(self, url_split, post_string, is_json) -> bytes: swap_client = self.server.swap_client swap_client.checkSystemStatus() offer_id = bytes.fromhex(url_split[3]) @@ -308,7 +312,7 @@ def js_revokeoffer(self, url_split, post_string, is_json): return bytes(json.dumps({'revoked_offer': offer_id.hex()}), 'UTF-8') -def js_smsgaddresses(self, url_split, post_string, is_json): +def js_smsgaddresses(self, url_split, post_string, is_json) -> bytes: swap_client = self.server.swap_client swap_client.checkSystemStatus() if len(url_split) > 3: @@ -332,7 +336,7 @@ def js_smsgaddresses(self, url_split, post_string, is_json): return bytes(json.dumps(swap_client.listAllSMSGAddresses()), 'UTF-8') -def js_rates(self, url_split, post_string, is_json): +def js_rates(self, url_split, post_string, is_json) -> bytes: post_data = getFormData(post_string, is_json) sc = self.server.swap_client @@ -341,7 +345,7 @@ def js_rates(self, url_split, post_string, is_json): return bytes(json.dumps(sc.lookupRates(coin_from, coin_to)), 'UTF-8') -def js_rates_list(self, url_split, query_string, is_json): +def js_rates_list(self, url_split, query_string, is_json) -> bytes: get_data = urllib.parse.parse_qs(query_string) sc = self.server.swap_client @@ -350,7 +354,7 @@ def js_rates_list(self, url_split, query_string, is_json): return bytes(json.dumps(sc.lookupRates(coin_from, coin_to, True)), 'UTF-8') -def js_rate(self, url_split, post_string, is_json): +def js_rate(self, url_split, post_string, is_json) -> bytes: post_data = getFormData(post_string, is_json) sc = self.server.swap_client @@ -383,13 +387,13 @@ def js_rate(self, url_split, post_string, is_json): return bytes(json.dumps({'rate': rate}), 'UTF-8') -def js_index(self, url_split, post_string, is_json): +def js_index(self, url_split, post_string, is_json) -> bytes: swap_client = self.server.swap_client swap_client.checkSystemStatus() return bytes(json.dumps(swap_client.getSummary()), 'UTF-8') -def js_generatenotification(self, url_split, post_string, is_json): +def js_generatenotification(self, url_split, post_string, is_json) -> bytes: swap_client = self.server.swap_client if not swap_client.debug: @@ -408,7 +412,7 @@ def js_generatenotification(self, url_split, post_string, is_json): return bytes(json.dumps({'type': r}), 'UTF-8') -def js_notifications(self, url_split, post_string, is_json): +def js_notifications(self, url_split, post_string, is_json) -> bytes: swap_client = self.server.swap_client swap_client.checkSystemStatus() swap_client.getNotifications() @@ -416,7 +420,7 @@ def js_notifications(self, url_split, post_string, is_json): return bytes(json.dumps(swap_client.getNotifications()), 'UTF-8') -def js_vacuumdb(self, url_split, post_string, is_json): +def js_vacuumdb(self, url_split, post_string, is_json) -> bytes: swap_client = self.server.swap_client swap_client.checkSystemStatus() swap_client.vacuumDB() @@ -424,7 +428,7 @@ def js_vacuumdb(self, url_split, post_string, is_json): return bytes(json.dumps({'completed': True}), 'UTF-8') -def js_getcoinseed(self, url_split, post_string, is_json): +def js_getcoinseed(self, url_split, post_string, is_json) -> bytes: swap_client = self.server.swap_client swap_client.checkSystemStatus() post_data = getFormData(post_string, is_json) @@ -440,7 +444,7 @@ def js_getcoinseed(self, url_split, post_string, is_json): return bytes(json.dumps({'coin': ci.ticker(), 'seed': seed.hex()}), 'UTF-8') -def js_setpassword(self, url_split, post_string, is_json): +def js_setpassword(self, url_split, post_string, is_json) -> bytes: # Set or change wallet passwords # Only works with currently enabled coins # Will fail if any coin does not unlock on the old password @@ -463,7 +467,7 @@ def js_setpassword(self, url_split, post_string, is_json): return bytes(json.dumps({'success': True}), 'UTF-8') -def js_unlock(self, url_split, post_string, is_json): +def js_unlock(self, url_split, post_string, is_json) -> bytes: swap_client = self.server.swap_client post_data = getFormData(post_string, is_json) @@ -480,7 +484,7 @@ def js_unlock(self, url_split, post_string, is_json): return bytes(json.dumps({'success': True}), 'UTF-8') -def js_lock(self, url_split, post_string, is_json): +def js_lock(self, url_split, post_string, is_json) -> bytes: swap_client = self.server.swap_client post_data = {} if post_string == '' else getFormData(post_string, is_json) @@ -495,11 +499,11 @@ def js_lock(self, url_split, post_string, is_json): return bytes(json.dumps({'success': True}), 'UTF-8') -def js_404(self, url_split, post_string, is_json): +def js_404(self, url_split, post_string, is_json) -> bytes: return bytes(json.dumps({'Error': 'path unknown'}), 'UTF-8') -def js_help(self, url_split, post_string, is_json): +def js_help(self, url_split, post_string, is_json) -> bytes: # TODO: Add details and examples commands = [] for k in pages: diff --git a/basicswap/templates/offer.html b/basicswap/templates/offer.html index 74efcbc..874c0a7 100644 --- a/basicswap/templates/offer.html +++ b/basicswap/templates/offer.html @@ -127,7 +127,7 @@ {{ data.amt_from }} {{ data.tla_from }} - Amount Bidder Send + Amount Bidder Sends {{ data.amt_to }} {{ data.tla_to }} diff --git a/basicswap/ui/page_offers.py b/basicswap/ui/page_offers.py index a50fba3..8e64fb0 100644 --- a/basicswap/ui/page_offers.py +++ b/basicswap/ui/page_offers.py @@ -58,9 +58,9 @@ def decode_offer_id(v): def swap_type_from_string(str_swap_type: str) -> SwapTypes: - if str_swap_type == 'seller_first': + if str_swap_type == 'seller_first' or str_swap_type == 'secret_hash': return SwapTypes.SELLER_FIRST - elif str_swap_type == 'xmr_swap': + elif str_swap_type == 'xmr_swap' or str_swap_type == 'adaptor_sig': return SwapTypes.XMR_SWAP else: raise ValueError('Unknown swap type') @@ -123,11 +123,15 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}): except Exception: errors.append('Minimum Bid Amount') - try: - page_data['amt_to'] = get_data_entry(form_data, 'amt_to') - parsed_data['amt_to'] = inputAmount(page_data['amt_to'], ci_to) - except Exception: - errors.append('Amount To') + if (have_data_entry(form_data, 'rate')): + parsed_data['rate'] = ci_to.make_int(form_data['rate'], r=1) + page_data['rate'] = ci_to.format_amount(parsed_data['rate']) + else: + try: + page_data['amt_to'] = get_data_entry(form_data, 'amt_to') + parsed_data['amt_to'] = inputAmount(page_data['amt_to'], ci_to) + except Exception: + errors.append('Amount To') if 'amt_to' in parsed_data and 'amt_from' in parsed_data: parsed_data['rate'] = ci_from.make_int(parsed_data['amt_to'] / parsed_data['amt_from'], r=1) @@ -141,19 +145,24 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}): page_data['automation_strat_id'] = int(get_data_entry_or(form_data, 'automation_strat_id', -1)) parsed_data['automation_strat_id'] = page_data['automation_strat_id'] swap_type = -1 + + if have_data_entry(form_data, 'subfee'): + parsed_data['subfee'] = True if have_data_entry(form_data, 'swap_type'): page_data['swap_type'] = get_data_entry(form_data, 'swap_type') parsed_data['swap_type'] = page_data['swap_type'] swap_type = swap_type_from_string(parsed_data['swap_type']) - if have_data_entry(form_data, 'subfee'): - parsed_data['subfee'] = True - - if parsed_data['coin_to'] in (Coins.XMR, Coins.PART_ANON) or swap_type == SwapTypes.XMR_SWAP: - page_data['swap_style'] = 'xmr' + elif parsed_data['coin_to'] in (Coins.XMR, Coins.PART_ANON): parsed_data['swap_type'] = strSwapType(SwapTypes.XMR_SWAP) + swap_type = SwapTypes.XMR_SWAP + else: + parsed_data['swap_type'] = strSwapType(SwapTypes.SELLER_FIRST) + swap_type = SwapTypes.SELLER_FIRST + + if swap_type == SwapTypes.XMR_SWAP: + page_data['swap_style'] = 'xmr' else: page_data['swap_style'] = 'atomic' - parsed_data['swap_type'] = strSwapType(SwapTypes.SELLER_FIRST) if 'swap_type' in parsed_data: try: diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 0000000..afed073 --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1 @@ +*.csv diff --git a/scripts/createoffers.py b/scripts/createoffers.py new file mode 100755 index 0000000..46ab445 --- /dev/null +++ b/scripts/createoffers.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2023 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +""" +Create offers +""" + +__version__ = '0.1' + +import os +import json +import signal +import urllib +import logging +import argparse +import threading +from urllib.request import urlopen + +delay_event = threading.Event() + + +def post_json_req(url, json_data): + req = urllib.request.Request(url) + 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)) + return urlopen(req, post_bytes, timeout=300).read() + + +def read_json_api(port, path=None, json_data=None): + url = f'http://127.0.0.1:{port}/json' + if path is not None: + url += '/' + path + + if json_data is not None: + return json.loads(post_json_req(url, json_data)) + return json.loads(urlopen(url, timeout=300).read()) + + +def signal_handler(sig, frame) -> None: + logging.info('Signal {} detected.'.format(sig)) + 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 readTemplates(known_coins): + offer_templates = [] + with open('offer_rules.csv', 'r') as fp: + for i, line in enumerate(fp): + if i < 1: + continue + line = line.strip() + if line[0] == '#': + continue + row_data = line.split(',') + try: + if len(row_data) < 6: + raise ValueError('missing data') + offer_template = {} + offer_template['coin_from'] = findCoin(row_data[0], known_coins) + offer_template['coin_to'] = findCoin(row_data[1], known_coins) + offer_template['amount'] = row_data[2] + offer_template['minrate'] = float(row_data[3]) + offer_template['ratetweakpercent'] = float(row_data[4]) + offer_template['amount_variable'] = row_data[5].lower() in ('true', 1) + offer_templates.append(offer_template) + except Exception as e: + print(f'Warning: Skipping row {i}, {e}') + continue + return offer_templates + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('-v', '--version', action='version', + version='%(prog)s {version}'.format(version=__version__)) + parser.add_argument('--port', dest='port', help='RPC port (default=12700)', type=int, default=12700, required=False) + args = parser.parse_args() + + if not os.path.exists('offer_rules.csv'): + with open('offer_rules.csv', 'w') as fp: + fp.write('coin from,coin to,offer value,min rate,rate tweak percent,amount variable') + + known_coins = read_json_api(args.port, 'coins') + coins_map = {} + for known_coin in known_coins: + coins_map[known_coin['name']] = known_coin + + signal.signal(signal.SIGINT, signal_handler) + while not delay_event.is_set(): + # Read templates each iteration so they can be modified without restarting + offer_templates = readTemplates(known_coins) + + try: + sent_offers = read_json_api(args.port, 'sentoffers', {'active': 'active'}) + + for offer_template in offer_templates: + offers_found = 0 + for offer in sent_offers: + if offer['coin_from'] == offer_template['coin_from'] and offer['coin_to'] == offer_template['coin_to']: + offers_found += 1 + + if offers_found > 0: + continue + coin_from_data = coins_map[offer_template['coin_from']] + coin_to_data = coins_map[offer_template['coin_to']] + + rates = read_json_api(args.port, '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)) + offer_data = { + 'addr_from': '-1', + 'coin_from': coin_from_data['ticker'], + 'coin_to': coin_to_data['ticker'], + 'amt_from': offer_template['amount'], + 'amt_var': offer_template['amount_variable'], + 'rate': use_rate, + 'swap_type': 'adaptor_sig', + 'lockhrs': '24', + 'automation_strat_id': 1} + new_offer = read_json_api(args.port, 'offers/new', offer_data) + print('New offer: {}'.format(new_offer)) + except Exception as e: + print('Error: Clamping rate to minimum.') + + print('Looping indefinitely, ctrl+c to exit.') + delay_event.wait(60) + + print('Done.') + + +if __name__ == '__main__': + main() diff --git a/tests/basicswap/test_partblind_xmr.py b/tests/basicswap/test_partblind_xmr.py index c85f11a..9cd5a77 100644 --- a/tests/basicswap/test_partblind_xmr.py +++ b/tests/basicswap/test_partblind_xmr.py @@ -313,7 +313,6 @@ class Test(BaseTest): self.ensure_balance(self.test_coin_from, 2, 100.0) js_w2 = read_json_api(1802, 'wallets') - print('[rm] js_w2', js_w2) post_json = { 'value': float(js_w2['PART']['blind_balance']), 'type_from': 'blind', diff --git a/tests/basicswap/test_reload.py b/tests/basicswap/test_reload.py index 3cfce0d..25a0b30 100644 --- a/tests/basicswap/test_reload.py +++ b/tests/basicswap/test_reload.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2019-2022 tecnovert +# Copyright (c) 2019-2023 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -127,12 +127,15 @@ class Test(unittest.TestCase): 'amt_to': '1', 'lockhrs': '24'} - offer_id = post_json_api(12700, 'offers/new', data) + offer_id = post_json_api(12700, 'offers/new', data)['offer_id'] summary = read_json_api(12700) assert (summary['num_sent_offers'] == 1) except Exception: traceback.print_exc() + sentoffers = read_json_api(12700, 'sentoffers', {'active': True}) + assert sentoffers[0]['offer_id'] == offer_id + logger.info('Waiting for offer:') waitForNumOffers(delay_event, 12701, 1) diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index cf29c3a..043c21d 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -788,8 +788,7 @@ class Test(BaseTest): 'coin_to': 6, 'amt_from': 1, 'amt_to': 1, - 'lockhrs': 24, - 'autoaccept': True} + 'lockhrs': 24} rv = json.loads(post_json_req('http://127.0.0.1:1800/json/offers/new', post_json)) offer_id_hex = rv['offer_id'] diff --git a/tests/basicswap/test_xmr_bids_offline.py b/tests/basicswap/test_xmr_bids_offline.py index ad899bf..e52b4df 100644 --- a/tests/basicswap/test_xmr_bids_offline.py +++ b/tests/basicswap/test_xmr_bids_offline.py @@ -61,7 +61,7 @@ class Test(XmrTestBase): 'amt_from': 1, 'amt_to': 1, 'lockhrs': 24, - 'autoaccept': True} + 'automation_strat_id': 1} rv = json.loads(urlopen('http://127.0.0.1:12700/json/offers/new', data=parse.urlencode(offer_data).encode()).read()) offer0_id = rv['offer_id']