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']