mirror of
https://github.com/basicswap/basicswap.git
synced 2024-12-22 11:39:34 +00:00
tests: Add script test
This commit is contained in:
parent
9117e2b723
commit
dc0bd147b8
11 changed files with 1379 additions and 106 deletions
|
@ -1,6 +1,6 @@
|
|||
# -*- 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.
|
||||
|
||||
|
@ -867,7 +867,7 @@ class BasicSwap(BaseApp):
|
|||
def updateIdentityBidState(self, session, address: str, bid) -> None:
|
||||
identity_stats = session.query(KnownIdentity).filter_by(address=address).first()
|
||||
if not identity_stats:
|
||||
identity_stats = KnownIdentity(address=address, created_at=int(time.time()))
|
||||
identity_stats = KnownIdentity(active_ind=1, address=address, created_at=int(time.time()))
|
||||
|
||||
if bid.state == BidStates.SWAP_COMPLETED:
|
||||
if bid.was_sent:
|
||||
|
@ -1171,7 +1171,7 @@ class BasicSwap(BaseApp):
|
|||
|
||||
def buildNotificationsCache(self, session):
|
||||
self._notifications_cache.clear()
|
||||
q = session.execute(f'SELECT created_at, event_type, event_data FROM notifications WHERE active_ind=1 ORDER BY created_at ASC LIMIT {self._show_notifications}')
|
||||
q = session.execute(f'SELECT created_at, event_type, event_data FROM notifications WHERE active_ind = 1 ORDER BY created_at ASC LIMIT {self._show_notifications}')
|
||||
for entry in q:
|
||||
self._notifications_cache[entry[0]] = (entry[1], json.loads(entry[2].decode('UTF-8')))
|
||||
|
||||
|
@ -1181,6 +1181,71 @@ class BasicSwap(BaseApp):
|
|||
rv.append((time.strftime('%d-%m-%y %H:%M:%S', time.localtime(k)), int(v[0]), v[1]))
|
||||
return rv
|
||||
|
||||
def setIdentityData(self, filters, data):
|
||||
address = filters['address']
|
||||
ci = self.ci(Coins.PART)
|
||||
ensure(ci.isValidAddress(address), 'Invalid identity address')
|
||||
|
||||
try:
|
||||
now = int(time.time())
|
||||
session = self.openSession()
|
||||
q = session.execute(f'SELECT COUNT(*) FROM knownidentities WHERE address = "{address}"').first()
|
||||
if q[0] < 1:
|
||||
q = session.execute(f'INSERT INTO knownidentities (active_ind, address, created_at) VALUES (1, "{address}", {now})')
|
||||
|
||||
values = []
|
||||
pattern = ''
|
||||
if 'label' in data:
|
||||
pattern += (', ' if pattern != '' else '')
|
||||
pattern += 'label = "{}"'.format(data['label'])
|
||||
values.append(address)
|
||||
q = session.execute(f'UPDATE knownidentities SET {pattern} WHERE address = "{address}"')
|
||||
|
||||
finally:
|
||||
self.closeSession(session)
|
||||
|
||||
def listIdentities(self, filters):
|
||||
try:
|
||||
session = self.openSession()
|
||||
|
||||
query_str = 'SELECT address, label, num_sent_bids_successful, num_recv_bids_successful, ' + \
|
||||
' num_sent_bids_rejected, num_recv_bids_rejected, num_sent_bids_failed, num_recv_bids_failed ' + \
|
||||
' FROM knownidentities ' + \
|
||||
' WHERE active_ind = 1 '
|
||||
|
||||
address = filters.get('address', None)
|
||||
if address is not None:
|
||||
query_str += f' AND address = "{address}" '
|
||||
|
||||
sort_dir = filters.get('sort_dir', 'DESC').upper()
|
||||
sort_by = filters.get('sort_by', 'created_at')
|
||||
query_str += f' ORDER BY {sort_by} {sort_dir}'
|
||||
|
||||
limit = filters.get('limit', None)
|
||||
if limit is not None:
|
||||
query_str += f' LIMIT {limit}'
|
||||
offset = filters.get('offset', None)
|
||||
if offset is not None:
|
||||
query_str += f' OFFSET {offset}'
|
||||
|
||||
q = session.execute(query_str)
|
||||
rv = []
|
||||
for row in q:
|
||||
identity = {
|
||||
'address': row[0],
|
||||
'label': row[1],
|
||||
'num_sent_bids_successful': zeroIfNone(row[2]),
|
||||
'num_recv_bids_successful': zeroIfNone(row[3]),
|
||||
'num_sent_bids_rejected': zeroIfNone(row[4]),
|
||||
'num_recv_bids_rejected': zeroIfNone(row[5]),
|
||||
'num_sent_bids_failed': zeroIfNone(row[6]),
|
||||
'num_recv_bids_failed': zeroIfNone(row[7]),
|
||||
}
|
||||
rv.append(identity)
|
||||
return rv
|
||||
finally:
|
||||
self.closeSession(session)
|
||||
|
||||
def vacuumDB(self):
|
||||
try:
|
||||
session = self.openSession()
|
||||
|
@ -2127,8 +2192,9 @@ class BasicSwap(BaseApp):
|
|||
session = scoped_session(self.session_factory)
|
||||
identity = session.query(KnownIdentity).filter_by(address=address).first()
|
||||
if identity is None:
|
||||
identity = KnownIdentity(address=address)
|
||||
identity = KnownIdentity(active_ind=1, address=address)
|
||||
identity.label = label
|
||||
identity.updated_at = int(time.time())
|
||||
session.add(identity)
|
||||
session.commit()
|
||||
finally:
|
||||
|
@ -5434,7 +5500,6 @@ class BasicSwap(BaseApp):
|
|||
settings_changed = True
|
||||
|
||||
if settings_changed:
|
||||
|
||||
settings_path = os.path.join(self.data_dir, cfg.CONFIG_FILENAME)
|
||||
settings_path_new = settings_path + '.new'
|
||||
shutil.copyfile(settings_path, settings_path + '.last')
|
||||
|
@ -5838,8 +5903,9 @@ class BasicSwap(BaseApp):
|
|||
filter_coin_to = filters.get('coin_to', None)
|
||||
if filter_coin_to and filter_coin_to > -1:
|
||||
q = q.filter(Offer.coin_to == int(filter_coin_to))
|
||||
|
||||
filter_include_sent = filters.get('include_sent', None)
|
||||
if filter_include_sent and filter_include_sent is not True:
|
||||
if filter_include_sent is not None and filter_include_sent is not True:
|
||||
q = q.filter(Offer.was_sent == False) # noqa: E712
|
||||
|
||||
order_dir = filters.get('sort_dir', 'desc')
|
||||
|
@ -5874,15 +5940,14 @@ class BasicSwap(BaseApp):
|
|||
session.remove()
|
||||
self.mxDB.release()
|
||||
|
||||
def listBids(self, sent=False, offer_id=None, for_html=False, filters={}, with_identity_info=False):
|
||||
def listBids(self, sent=False, offer_id=None, for_html=False, filters={}):
|
||||
self.mxDB.acquire()
|
||||
try:
|
||||
rv = []
|
||||
now = int(time.time())
|
||||
session = scoped_session(self.session_factory)
|
||||
|
||||
identity_fields = ''
|
||||
query_str = 'SELECT bids.created_at, bids.expire_at, bids.bid_id, bids.offer_id, bids.amount, bids.state, bids.was_received, tx1.state, tx2.state, offers.coin_from, bids.rate, bids.bid_addr {} FROM bids '.format(identity_fields) + \
|
||||
query_str = 'SELECT bids.created_at, bids.expire_at, bids.bid_id, bids.offer_id, bids.amount, bids.state, bids.was_received, tx1.state, tx2.state, offers.coin_from, bids.rate, bids.bid_addr FROM bids ' + \
|
||||
'LEFT JOIN offers ON offers.offer_id = bids.offer_id ' + \
|
||||
'LEFT JOIN transactions AS tx1 ON tx1.bid_id = bids.bid_id AND tx1.tx_type = {} '.format(TxTypes.ITX) + \
|
||||
'LEFT JOIN transactions AS tx2 ON tx2.bid_id = bids.bid_id AND tx2.tx_type = {} '.format(TxTypes.PTX)
|
||||
|
@ -5901,7 +5966,12 @@ class BasicSwap(BaseApp):
|
|||
bid_state_ind = filters.get('bid_state_ind', -1)
|
||||
if bid_state_ind != -1:
|
||||
query_str += 'AND bids.state = {} '.format(bid_state_ind)
|
||||
|
||||
with_available_or_active = filters.get('with_available_or_active', False)
|
||||
with_expired = filters.get('with_expired', True)
|
||||
if with_available_or_active:
|
||||
query_str += 'AND (bids.state NOT IN ({}, {}, {}, {}, {}) AND (bids.state > {} OR bids.expire_at > {})) '.format(BidStates.SWAP_COMPLETED, BidStates.BID_ERROR, BidStates.BID_REJECTED, BidStates.SWAP_TIMEDOUT, BidStates.BID_ABANDONED, BidStates.BID_RECEIVED, now)
|
||||
else:
|
||||
if with_expired is not True:
|
||||
query_str += 'AND bids.expire_at > {} '.format(now)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -*- 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.
|
||||
|
||||
|
@ -697,11 +697,8 @@ class HttpThread(threading.Thread, HTTPServer):
|
|||
data = response.read()
|
||||
conn.close()
|
||||
|
||||
def stopped(self):
|
||||
return self.stop_event.is_set()
|
||||
|
||||
def serve_forever(self):
|
||||
while not self.stopped():
|
||||
while not self.stop_event.is_set():
|
||||
self.handle_request()
|
||||
self.socket.close()
|
||||
|
||||
|
|
|
@ -1237,7 +1237,7 @@ class BTCInterface(CoinInterface):
|
|||
def describeTx(self, tx_hex: str):
|
||||
return self.rpc_callback('decoderawtransaction', [tx_hex])
|
||||
|
||||
def getSpendableBalance(self):
|
||||
def getSpendableBalance(self) -> int:
|
||||
return self.make_int(self.rpc_callback('getbalances')['mine']['trusted'])
|
||||
|
||||
def createUTXO(self, value_sats: int):
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- 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.
|
||||
|
||||
|
@ -77,10 +77,10 @@ class PARTInterface(BTCInterface):
|
|||
# TODO: Double check
|
||||
return True
|
||||
|
||||
def getNewAddress(self, use_segwit, label='swap_receive'):
|
||||
def getNewAddress(self, use_segwit, label='swap_receive') -> str:
|
||||
return self.rpc_callback('getnewaddress', [label])
|
||||
|
||||
def getNewStealthAddress(self, label='swap_stealth'):
|
||||
def getNewStealthAddress(self, label='swap_stealth') -> str:
|
||||
return self.rpc_callback('getnewstealthaddress', [label])
|
||||
|
||||
def haveSpentIndex(self):
|
||||
|
@ -105,7 +105,7 @@ class PARTInterface(BTCInterface):
|
|||
def getScriptForPubkeyHash(self, pkh):
|
||||
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
|
||||
|
||||
def formatStealthAddress(self, scan_pubkey, spend_pubkey):
|
||||
def formatStealthAddress(self, scan_pubkey, spend_pubkey) -> str:
|
||||
prefix_byte = chainparams[self.coin_type()][self._network]['stealth_key_prefix']
|
||||
|
||||
return encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey)
|
||||
|
@ -116,7 +116,7 @@ class PARTInterface(BTCInterface):
|
|||
length += getWitnessElementLen(len(e) // 2) # hex -> bytes
|
||||
return length
|
||||
|
||||
def getWalletRestoreHeight(self):
|
||||
def getWalletRestoreHeight(self) -> int:
|
||||
start_time = self.rpc_callback('getwalletinfo')['keypoololdest']
|
||||
|
||||
blockchaininfo = self.rpc_callback('getblockchaininfo')
|
||||
|
@ -131,6 +131,15 @@ class PARTInterface(BTCInterface):
|
|||
block_header = self.rpc_callback('getblockheader', [block_hash])
|
||||
return block_header['height']
|
||||
|
||||
def isValidAddress(self, address: str) -> bool:
|
||||
try:
|
||||
rv = self.rpc_callback('validateaddress', [address])
|
||||
if rv['isvalid'] is True:
|
||||
return True
|
||||
except Exception as ex:
|
||||
self._log.debug('validateaddress failed: {}'.format(address))
|
||||
return False
|
||||
|
||||
|
||||
class PARTInterfaceBlind(PARTInterface):
|
||||
@staticmethod
|
||||
|
@ -622,7 +631,7 @@ class PARTInterfaceBlind(PARTInterface):
|
|||
|
||||
return bytes.fromhex(lock_refund_swipe_tx_hex)
|
||||
|
||||
def getSpendableBalance(self):
|
||||
def getSpendableBalance(self) -> int:
|
||||
return self.make_int(self.rpc_callback('getbalances')['mine']['blind_trusted'])
|
||||
|
||||
def publishBLockTx(self, vkbv, Kbs, output_amount, feerate, delay_for: int = 10, unlock_time: int = 0) -> bytes:
|
||||
|
@ -840,7 +849,7 @@ class PARTInterfaceAnon(PARTInterface):
|
|||
rv = self.rpc_callback('sendtypeto', params)
|
||||
return bytes.fromhex(rv['txid'])
|
||||
|
||||
def findTxnByHash(self, txid_hex):
|
||||
def findTxnByHash(self, txid_hex: str):
|
||||
# txindex is enabled for Particl
|
||||
|
||||
try:
|
||||
|
@ -854,5 +863,5 @@ class PARTInterfaceAnon(PARTInterface):
|
|||
|
||||
return None
|
||||
|
||||
def getSpendableBalance(self):
|
||||
def getSpendableBalance(self) -> int:
|
||||
return self.make_int(self.rpc_callback('getbalances')['mine']['anon_trusted'])
|
||||
|
|
|
@ -9,6 +9,7 @@ import random
|
|||
import urllib.parse
|
||||
|
||||
from .util import (
|
||||
ensure,
|
||||
toBool,
|
||||
)
|
||||
from .basicswap_util import (
|
||||
|
@ -38,7 +39,7 @@ from .ui.page_offers import postNewOffer
|
|||
from .protocols.xmr_swap_1 import recoverNoScriptTxnWithKey, getChainBSplitKey
|
||||
|
||||
|
||||
def getFormData(post_string, is_json):
|
||||
def getFormData(post_string: str, is_json: bool):
|
||||
if post_string == '':
|
||||
raise ValueError('No post data')
|
||||
if is_json:
|
||||
|
@ -138,6 +139,7 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
|||
return bytes(json.dumps(rv), 'UTF-8')
|
||||
offer_id = bytes.fromhex(url_split[3])
|
||||
|
||||
with_extra_info = False
|
||||
filters = {
|
||||
'coin_from': -1,
|
||||
'coin_to': -1,
|
||||
|
@ -174,12 +176,15 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
|||
if have_data_entry(post_data, 'include_sent'):
|
||||
filters['include_sent'] = toBool(get_data_entry(post_data, 'include_sent'))
|
||||
|
||||
if have_data_entry(post_data, 'with_extra_info'):
|
||||
with_extra_info = toBool(get_data_entry(post_data, 'with_extra_info'))
|
||||
|
||||
offers = swap_client.listOffers(sent, filters)
|
||||
rv = []
|
||||
for o in offers:
|
||||
ci_from = swap_client.ci(o.coin_from)
|
||||
ci_to = swap_client.ci(o.coin_to)
|
||||
rv.append({
|
||||
offer_data = {
|
||||
'swap_type': o.swap_type,
|
||||
'addr_from': o.addr_from,
|
||||
'addr_to': o.addr_to,
|
||||
|
@ -191,8 +196,11 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
|
|||
'amount_from': ci_from.format_amount(o.amount_from),
|
||||
'amount_to': ci_to.format_amount((o.amount_from * o.rate) // ci_from.COIN()),
|
||||
'rate': ci_to.format_amount(o.rate),
|
||||
})
|
||||
|
||||
}
|
||||
if with_extra_info:
|
||||
offer_data['amount_negotiable'] = o.amount_negotiable
|
||||
offer_data['rate_negotiable'] = o.rate_negotiable
|
||||
rv.append(offer_data)
|
||||
return bytes(json.dumps(rv), 'UTF-8')
|
||||
|
||||
|
||||
|
@ -200,7 +208,60 @@ 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) -> bytes:
|
||||
def parseBidFilters(post_data):
|
||||
offer_id = None
|
||||
filters = {}
|
||||
|
||||
if have_data_entry(post_data, 'offer_id'):
|
||||
offer_id = bytes.fromhex(get_data_entry(post_data, 'offer_id'))
|
||||
assert (len(offer_id) == 28)
|
||||
|
||||
if have_data_entry(post_data, 'sort_by'):
|
||||
sort_by = get_data_entry(post_data, 'sort_by')
|
||||
assert (sort_by in ['created_at', ]), 'Invalid sort by'
|
||||
filters['sort_by'] = sort_by
|
||||
if have_data_entry(post_data, 'sort_dir'):
|
||||
sort_dir = get_data_entry(post_data, 'sort_dir')
|
||||
assert (sort_dir in ['asc', 'desc']), 'Invalid sort dir'
|
||||
filters['sort_dir'] = sort_dir
|
||||
|
||||
if have_data_entry(post_data, 'offset'):
|
||||
filters['offset'] = int(get_data_entry(post_data, 'offset'))
|
||||
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, 'with_available_or_active'):
|
||||
filters['with_available_or_active'] = toBool(get_data_entry(post_data, 'with_available_or_active'))
|
||||
elif have_data_entry(post_data, 'with_expired'):
|
||||
filters['with_expired'] = toBool(get_data_entry(post_data, 'with_expired'))
|
||||
|
||||
if have_data_entry(post_data, 'with_extra_info'):
|
||||
filters['with_extra_info'] = toBool(get_data_entry(post_data, 'with_extra_info'))
|
||||
|
||||
return offer_id, filters
|
||||
|
||||
|
||||
def formatBids(swap_client, bids, filters) -> bytes:
|
||||
with_extra_info = filters.get('with_extra_info', False)
|
||||
rv = []
|
||||
for b in bids:
|
||||
bid_data = {
|
||||
'bid_id': b[2].hex(),
|
||||
'offer_id': b[3].hex(),
|
||||
'created_at': b[0],
|
||||
'expire_at': b[1],
|
||||
'coin_from': b[9],
|
||||
'amount_from': swap_client.ci(b[9]).format_amount(b[4]),
|
||||
'bid_state': strBidState(b[5])
|
||||
}
|
||||
if with_extra_info:
|
||||
bid_data['addr_from'] = b[11]
|
||||
rv.append(bid_data)
|
||||
return bytes(json.dumps(rv), 'UTF-8')
|
||||
|
||||
|
||||
def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
if len(url_split) > 3:
|
||||
|
@ -281,22 +342,21 @@ def js_bids(self, url_split, post_string, is_json) -> bytes:
|
|||
data = describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, events, edit_bid, show_txns, for_api=True)
|
||||
return bytes(json.dumps(data), 'UTF-8')
|
||||
|
||||
bids = swap_client.listBids()
|
||||
return bytes(json.dumps([{
|
||||
'bid_id': b[2].hex(),
|
||||
'offer_id': b[3].hex(),
|
||||
'created_at': b[0],
|
||||
'expire_at': b[1],
|
||||
'coin_from': b[9],
|
||||
'amount_from': swap_client.ci(b[9]).format_amount(b[4]),
|
||||
'bid_state': strBidState(b[5])
|
||||
} for b in bids]), 'UTF-8')
|
||||
post_data = getFormData(post_string, is_json)
|
||||
offer_id, filters = parseBidFilters(post_data)
|
||||
|
||||
bids = swap_client.listBids(offer_id=offer_id, filters=filters)
|
||||
return formatBids(swap_client, bids, filters)
|
||||
|
||||
|
||||
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')
|
||||
post_data = getFormData(post_string, is_json)
|
||||
offer_id, filters = parseBidFilters(post_data)
|
||||
|
||||
bids = swap_client.listBids(sent=True, offer_id=offer_id, filters=filters)
|
||||
return formatBids(swap_client, bids, filters)
|
||||
|
||||
|
||||
def js_network(self, url_split, post_string, is_json) -> bytes:
|
||||
|
@ -318,7 +378,7 @@ 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:
|
||||
post_data = getFormData(post_string, is_json)
|
||||
post_data = {} if post_string == '' else getFormData(post_string, is_json)
|
||||
if url_split[3] == 'new':
|
||||
addressnote = get_data_entry_or(post_data, 'addressnote', '')
|
||||
new_addr, pubkey = swap_client.newSMSGAddress(addressnote=addressnote)
|
||||
|
@ -417,11 +477,54 @@ def js_generatenotification(self, url_split, post_string, is_json) -> bytes:
|
|||
def js_notifications(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
swap_client.getNotifications()
|
||||
|
||||
return bytes(json.dumps(swap_client.getNotifications()), 'UTF-8')
|
||||
|
||||
|
||||
def js_identities(self, url_split, post_string: str, is_json: bool) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
|
||||
filters = {
|
||||
'page_no': 1,
|
||||
'limit': PAGE_LIMIT,
|
||||
'sort_by': 'created_at',
|
||||
'sort_dir': 'desc',
|
||||
}
|
||||
|
||||
if len(url_split) > 3:
|
||||
address = url_split[3]
|
||||
filters['address'] = address
|
||||
|
||||
if post_string != '':
|
||||
post_data = getFormData(post_string, is_json)
|
||||
|
||||
if have_data_entry(post_data, 'sort_by'):
|
||||
sort_by = get_data_entry(post_data, 'sort_by')
|
||||
assert (sort_by in ['created_at', 'rate']), 'Invalid sort by'
|
||||
filters['sort_by'] = sort_by
|
||||
if have_data_entry(post_data, 'sort_dir'):
|
||||
sort_dir = get_data_entry(post_data, 'sort_dir')
|
||||
assert (sort_dir in ['asc', 'desc']), 'Invalid sort dir'
|
||||
filters['sort_dir'] = sort_dir
|
||||
|
||||
if have_data_entry(post_data, 'offset'):
|
||||
filters['offset'] = int(get_data_entry(post_data, 'offset'))
|
||||
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'
|
||||
|
||||
set_data = {}
|
||||
if have_data_entry(post_data, 'set_label'):
|
||||
set_data['label'] = get_data_entry(post_data, 'set_label')
|
||||
|
||||
if set_data:
|
||||
ensure('address' in filters, 'Must provide an address to modify data')
|
||||
swap_client.setIdentityData(filters, set_data)
|
||||
|
||||
return bytes(json.dumps(swap_client.listIdentities(filters)), 'UTF-8')
|
||||
|
||||
|
||||
def js_vacuumdb(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
|
@ -528,6 +631,7 @@ pages = {
|
|||
'rateslist': js_rates_list,
|
||||
'generatenotification': js_generatenotification,
|
||||
'notifications': js_notifications,
|
||||
'identities': js_identities,
|
||||
'vacuumdb': js_vacuumdb,
|
||||
'getcoinseed': js_getcoinseed,
|
||||
'setpassword': js_setpassword,
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
|
||||
Rendered files can be found in:
|
||||
|
||||
basicswap/static/sequence_diagrams
|
||||
|
||||
|
||||
To render:
|
||||
|
||||
nvm use 14
|
||||
npm install -g mscgenjs-cli
|
||||
|
||||
|
|
3
scripts/.gitignore
vendored
3
scripts/.gitignore
vendored
|
@ -1 +1,4 @@
|
|||
*.csv
|
||||
*.json
|
||||
*.last
|
||||
*.sqlite
|
||||
|
|
|
@ -9,10 +9,13 @@
|
|||
Create offers
|
||||
"""
|
||||
|
||||
__version__ = '0.1'
|
||||
__version__ = '0.2'
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import shutil
|
||||
import signal
|
||||
import urllib
|
||||
import logging
|
||||
|
@ -22,23 +25,35 @@ from urllib.request import urlopen
|
|||
|
||||
delay_event = threading.Event()
|
||||
|
||||
DEFAULT_CONFIG_FILE: str = 'createoffers.json'
|
||||
DEFAULT_STATE_FILE: str = 'createoffers_state.json'
|
||||
|
||||
def post_json_req(url, json_data):
|
||||
req = urllib.request.Request(url)
|
||||
|
||||
def post_req(url: str, json_data=None):
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
||||
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))
|
||||
return urlopen(req, post_bytes, timeout=300).read()
|
||||
else:
|
||||
post_bytes = None
|
||||
return urlopen(req, data=post_bytes, timeout=300).read()
|
||||
|
||||
|
||||
def read_json_api(port, path=None, json_data=None):
|
||||
url = f'http://127.0.0.1:{port}/json'
|
||||
def make_json_api_func(host: str, port: int):
|
||||
host = host
|
||||
port = port
|
||||
|
||||
def api_func(path=None, json_data=None, timeout=300):
|
||||
nonlocal host, port
|
||||
url = f'http://{host}:{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())
|
||||
return json.loads(post_req(url, json_data))
|
||||
response = urlopen(url, timeout=300).read()
|
||||
return json.loads(response)
|
||||
return api_func
|
||||
|
||||
|
||||
def signal_handler(sig, frame) -> None:
|
||||
|
@ -55,74 +70,208 @@ def findCoin(coin: str, known_coins) -> str:
|
|||
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:
|
||||
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 not 'offers' in config:
|
||||
config['offers'] = []
|
||||
if not 'bids' in config:
|
||||
config['bids'] = []
|
||||
if not 'stealthex' in config:
|
||||
config['stealthex'] = []
|
||||
|
||||
if not 'min_seconds_between_offers' in config:
|
||||
config['min_seconds_between_offers'] = 60
|
||||
print('Set min_seconds_between_offers', config['min_seconds_between_offers'])
|
||||
num_changes += 1
|
||||
if not 'max_seconds_between_offers' 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 not 'min_seconds_between_bids' in config:
|
||||
config['min_seconds_between_bids'] = 60
|
||||
print('Set min_seconds_between_bids', config['min_seconds_between_bids'])
|
||||
num_changes += 1
|
||||
if not 'max_seconds_between_bids' 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.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
|
||||
|
||||
if offer_template.get('enabled', True) is False:
|
||||
continue
|
||||
line = line.strip()
|
||||
if line[0] == '#':
|
||||
offer_template['coin_from'] = findCoin(offer_template['coin_from'], known_coins)
|
||||
offer_template['coin_to'] = findCoin(offer_template['coin_to'], known_coins)
|
||||
|
||||
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
|
||||
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.get('enabled', True) is False:
|
||||
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_template['address'] = row_data[6]
|
||||
offer_templates.append(offer_template)
|
||||
except Exception as e:
|
||||
print(f'Warning: Skipping row {i}, {e}')
|
||||
continue
|
||||
return offer_templates
|
||||
|
||||
if bid_template.get('min_swap_amount', 0.0) < 0.00001:
|
||||
print('Setting min_swap_amount for bid template', bid_template['name'])
|
||||
bid_template['min_swap_amount'] = 0.00001
|
||||
|
||||
bid_template['coin_from'] = findCoin(bid_template['coin_from'], known_coins)
|
||||
bid_template['coin_to'] = findCoin(bid_template['coin_to'], known_coins)
|
||||
|
||||
if bid_template['name'] in bid_templates_map:
|
||||
print('renaming bid template', offer_templates_map_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
|
||||
config['num_enabled_bids'] = num_enabled
|
||||
|
||||
num_enabled = 0
|
||||
stealthex_swaps = config['stealthex']
|
||||
for i, swap in enumerate(stealthex_swaps):
|
||||
num_enabled += 1 if swap.get('enabled', True) else 0
|
||||
swap['coin_from'] = findCoin(swap['coin_from'], known_coins)
|
||||
#bid_template['coin_to'] = findCoin(bid_template['coin_to'], known_coins)
|
||||
config['num_enabled_swaps'] = num_enabled
|
||||
|
||||
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 main():
|
||||
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('offer_rules.csv'):
|
||||
with open('offer_rules.csv', 'w') as fp:
|
||||
# Set address to -1 to use new addresses
|
||||
fp.write('coin from,coin to,offer value,min rate,rate tweak percent,amount variable,address')
|
||||
read_json_api = make_json_api_func(args.host, args.port)
|
||||
|
||||
known_coins = read_json_api(args.port, 'coins')
|
||||
if not os.path.exists(args.configfile):
|
||||
raise ValueError(f'Config file "{args.configfile}" not found.')
|
||||
|
||||
known_coins = read_json_api('coins')
|
||||
coins_map = {}
|
||||
for known_coin in known_coins:
|
||||
coins_map[known_coin['name']] = known_coin
|
||||
|
||||
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 templates each iteration so they can be modified without restarting
|
||||
offer_templates = readTemplates(known_coins)
|
||||
# Read config each iteration so they can be modified without restarting
|
||||
config = readConfig(args, known_coins)
|
||||
offer_templates = config['offers']
|
||||
random.shuffle(offer_templates)
|
||||
|
||||
bid_templates = config['bids']
|
||||
random.shuffle(bid_templates)
|
||||
|
||||
stealthex_swaps = config['stealthex']
|
||||
random.shuffle(bid_templates)
|
||||
|
||||
# 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 = make_json_api_func(args.host, wallet_api_port)
|
||||
else:
|
||||
read_json_api_wallet = read_json_api
|
||||
|
||||
num_state_changes: int = 0
|
||||
try:
|
||||
recieved_offers = read_json_api(args.port, 'offers', {'active': 'active', 'include_sent': False})
|
||||
print('recieved_offers', recieved_offers)
|
||||
|
||||
sent_offers = read_json_api(args.port, 'sentoffers', {'active': 'active'})
|
||||
sent_offers = read_json_api('sentoffers', {'active': 'active'})
|
||||
|
||||
if args.debug and len(offer_templates) > 0:
|
||||
print('Processing {} offer templates'.format(config['num_enabled_offers']))
|
||||
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']})
|
||||
wallet_from = read_json_api_wallet('wallets/{}'.format(coin_from_data['ticker']))
|
||||
|
||||
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 float(wallet_from['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
|
||||
|
||||
if float(wallet_from['balance']) <= float(offer_template['min_coin_from_amt']):
|
||||
print('Skipping template {}, wallet from balance below minimum'.format(offer_template['name']))
|
||||
continue
|
||||
|
||||
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
|
||||
|
||||
"""
|
||||
recieved_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('recieved_offers', recieved_offers)
|
||||
|
||||
TODO - adjust rates based on extisting 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
|
||||
|
@ -137,21 +286,282 @@ def main():
|
|||
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': offer_template['address'],
|
||||
'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_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': '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.')
|
||||
if args.debug:
|
||||
print('offer data {}'.format(offer_data))
|
||||
new_offer = read_json_api('offers/new', offer_data)
|
||||
print('New offer: {}'.format(new_offer['offer_id']))
|
||||
num_state_changes += 1
|
||||
if not 'offers' in script_state:
|
||||
script_state['offers'] = {}
|
||||
template_name = offer_template['name']
|
||||
if not template_name 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']
|
||||
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
|
||||
|
||||
if args.debug and len(bid_templates) > 0:
|
||||
print('Processing {} bid templates'.format(config['num_enabled_bids']))
|
||||
for bid_template in bid_templates:
|
||||
|
||||
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 not 'bids' in script_state:
|
||||
script_state['bids'] = {}
|
||||
template_name = bid_template['name']
|
||||
if not template_name 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
|
||||
num_state_changes += 1
|
||||
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
|
||||
num_state_changes += 1
|
||||
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']]
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
recieved_offers = read_json_api('offers', offers_options)
|
||||
print('recieved_offers', recieved_offers)
|
||||
|
||||
for offer in recieved_offers:
|
||||
offer_id = offer['offer_id']
|
||||
offer_amount = float(offer['amount_from'])
|
||||
offer_rate = float(offer['rate'])
|
||||
bid_amount = offer_amount
|
||||
|
||||
min_swap_amount = bid_template.get('min_swap_amount', 0.01) # TODO: Make default vary per coin
|
||||
can_adjust_amount: bool = offer['amount_negotiable'] and bid_template.get('amount_variable', True)
|
||||
if can_adjust_amount is False and offer_amount > bid_template['amount']:
|
||||
if args.debug:
|
||||
print(f'Bid amount too low for offer {offer_id}')
|
||||
continue
|
||||
if (can_adjust_amount is False and offer_amount < bid_template['amount']) or offer_amount < min_swap_amount:
|
||||
if args.debug:
|
||||
print(f'Offer amount too low for bid {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 len(offer_identity) > 0:
|
||||
successful_sent_bids = offer_identity[0]['num_sent_bids_successful']
|
||||
failed_sent_bids = offer_identity[0]['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
|
||||
|
||||
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
|
||||
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:
|
||||
print(f'Reduced bid amount to {bid_amount}')
|
||||
bid_amount = adjusted_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
|
||||
|
||||
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)
|
||||
print('New bid: {} on offer {}'.format(new_bid['bid_id'], offer['offer_id']))
|
||||
bid_id = new_bid['bid_id']
|
||||
|
||||
num_state_changes += 1
|
||||
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
|
||||
break # Create max one bid per iteration
|
||||
|
||||
if args.debug and len(stealthex_swaps) > 0:
|
||||
print('Processing {} stealthex templates'.format(config['num_enabled_swaps']))
|
||||
for stealthex_swap in stealthex_swaps:
|
||||
if stealthex_swap.get('enabled', True) is False:
|
||||
continue
|
||||
coin_from_data = coins_map[stealthex_swap['coin_from']]
|
||||
|
||||
wallet_from = read_json_api_wallet('wallets/{}'.format(coin_from_data['ticker']))
|
||||
|
||||
current_balance = float(wallet_from['balance'])
|
||||
|
||||
min_balance_from = float(stealthex_swap['min_balance_from'])
|
||||
min_swap_amount = float(stealthex_swap['min_amount_tx'])
|
||||
max_swap_amount = float(stealthex_swap['max_amount_tx'])
|
||||
|
||||
# TODO: Check range limits
|
||||
|
||||
if current_balance >= min_balance_from + min_swap_amount:
|
||||
swap_amount = max_swap_amount
|
||||
if current_balance - swap_amount < min_balance_from:
|
||||
swap_amount = max(min_swap_amount, current_balance - min_balance_from)
|
||||
|
||||
estimate_url = 'https://api.stealthex.io/api/v2/estimate/{}/{}?amount={}&api_key={}&fixed=true'.format(coin_from_data['ticker'].lower(), stealthex_swap['coin_to'].lower(), swap_amount, stealthex_swap['api_key'])
|
||||
if args.debug:
|
||||
print(f'Estimate URL: {estimate_url}')
|
||||
estimate_response = json.loads(post_req(estimate_url))
|
||||
|
||||
amount_to = float(estimate_response['estimated_amount'])
|
||||
rate = swap_amount / amount_to
|
||||
min_rate = float(stealthex_swap['min_rate'])
|
||||
if rate < min_rate:
|
||||
if args.debug:
|
||||
print('Stealthex rate {} below minimum {} for {} to {}'.format(rate, min_rate, coin_from_data['ticker'], stealthex_swap['coin_to']))
|
||||
continue
|
||||
|
||||
exchange_url = 'https://api.stealthex.io/api/v2/exchange?api_key={}'.format(stealthex_swap['api_key'])
|
||||
|
||||
address_to = stealthex_swap.get('receive_address', 'auto')
|
||||
if address_to == 'auto':
|
||||
address_to = read_json_api('wallets/{}/nextdepositaddr'.format(stealthex_swap['coin_to']))
|
||||
|
||||
address_refund = stealthex_swap.get('refund_address', 'auto')
|
||||
if address_refund == 'auto':
|
||||
address_refund = read_json_api('wallets/{}/nextdepositaddr'.format(coin_from_data['ticker']))
|
||||
|
||||
exchange_data = {
|
||||
'currency_from': coin_from_data['ticker'].lower(),
|
||||
'currency_to': stealthex_swap['coin_to'].lower(),
|
||||
'address_to': address_to,
|
||||
'amount_from': swap_amount,
|
||||
'fixed': True,
|
||||
#'extra_id_to':
|
||||
#'referral':
|
||||
'refund_address': address_refund,
|
||||
#'refund_extra_id':
|
||||
'rate_id': estimate_response['rate_id'],
|
||||
}
|
||||
|
||||
if args.debug:
|
||||
print(f'Exchange URL: {estimate_url}')
|
||||
print(f'Exchange data: {exchange_data}')
|
||||
|
||||
exchange_response = json.loads(post_req(exchange_url, exchange_data))
|
||||
|
||||
if 'Error' in estimate_response:
|
||||
raise ValueError('Exchange error ' + estimate_response)
|
||||
|
||||
raise ValueError('TODO')
|
||||
|
||||
if num_state_changes > 0:
|
||||
if os.path.exists(args.statefile):
|
||||
shutil.copyfile(args.statefile, args.statefile + '.last')
|
||||
with open(args.statefile, 'w') as fp:
|
||||
json.dump(script_state, fp, indent=4)
|
||||
|
||||
except Exception as e:
|
||||
print(f'Error: {e}.')
|
||||
|
||||
if args.oneshot:
|
||||
break
|
||||
print('Looping indefinitely, ctrl+c to exit.')
|
||||
delay_event.wait(60)
|
||||
|
||||
|
|
654
tests/basicswap/extended/test_scripts.py
Normal file
654
tests/basicswap/extended/test_scripts.py
Normal file
|
@ -0,0 +1,654 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- 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.
|
||||
|
||||
"""
|
||||
Start test_xmr_persistent.py
|
||||
|
||||
python tests/basicswap/extended/test_scripts.py
|
||||
|
||||
pytest -v -s tests/basicswap/extended/test_scripts.py::Test::test_bid_tracking
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import math
|
||||
import logging
|
||||
import sqlite3
|
||||
import unittest
|
||||
import threading
|
||||
import subprocess
|
||||
import http.client
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from urllib import parse
|
||||
|
||||
from tests.basicswap.util import (
|
||||
read_json_api,
|
||||
waitForServer,
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.level = logging.DEBUG
|
||||
if not len(logger.handlers):
|
||||
logger.addHandler(logging.StreamHandler(sys.stdout))
|
||||
|
||||
|
||||
PORT_OFS = int(os.getenv('PORT_OFS', 1))
|
||||
UI_PORT = 12700 + PORT_OFS
|
||||
|
||||
|
||||
class HttpHandler(BaseHTTPRequestHandler):
|
||||
|
||||
def js_response(self, url_split, post_string, is_json):
|
||||
return bytes(json.dumps(self.server.return_data[url_split[3]]), 'UTF-8')
|
||||
|
||||
def putHeaders(self, status_code, content_type):
|
||||
self.send_response(status_code)
|
||||
self.send_header('Content-Type', content_type)
|
||||
self.end_headers()
|
||||
|
||||
def handle_http(self, status_code, path, post_string='', is_json=False):
|
||||
parsed = parse.urlparse(self.path)
|
||||
url_split = parsed.path.split('/')
|
||||
if post_string == '' and len(parsed.query) > 0:
|
||||
post_string = parsed.query
|
||||
if len(url_split) > 1 and url_split[1] == 'json':
|
||||
self.putHeaders(status_code, 'text/plain')
|
||||
return self.js_response(url_split, post_string, is_json)
|
||||
|
||||
self.putHeaders(status_code, 'text/plain')
|
||||
return bytes('No response', 'UTF-8')
|
||||
|
||||
def do_GET(self):
|
||||
response = self.handle_http(200, self.path)
|
||||
self.wfile.write(response)
|
||||
|
||||
def do_POST(self):
|
||||
post_string = self.rfile.read(int(self.headers.get('Content-Length')))
|
||||
|
||||
is_json = True if 'json' in self.headers.get('Content-Type', '') else False
|
||||
response = self.handle_http(200, self.path, post_string, is_json)
|
||||
self.wfile.write(response)
|
||||
|
||||
def do_HEAD(self):
|
||||
self.putHeaders(200, 'text/html')
|
||||
|
||||
|
||||
class HttpThread(threading.Thread, HTTPServer):
|
||||
host = '127.0.0.1'
|
||||
port_no = 12699
|
||||
stop_event = threading.Event()
|
||||
return_data = {'test': 1}
|
||||
|
||||
def __init__(self):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
HTTPServer.__init__(self, (self.host, self.port_no), HttpHandler)
|
||||
|
||||
def stop(self):
|
||||
self.stop_event.set()
|
||||
|
||||
# Send fake request
|
||||
conn = http.client.HTTPConnection(self.host, self.port_no)
|
||||
conn.connect()
|
||||
conn.request('GET', '/none')
|
||||
response = conn.getresponse()
|
||||
data = response.read()
|
||||
conn.close()
|
||||
|
||||
def serve_forever(self):
|
||||
while not self.stop_event.is_set():
|
||||
self.handle_request()
|
||||
self.socket.close()
|
||||
|
||||
def run(self):
|
||||
self.serve_forever()
|
||||
|
||||
|
||||
def clear_offers(delay_event, node_id) -> None:
|
||||
logging.info(f'clear_offers node {node_id}')
|
||||
offers = read_json_api(UI_PORT + node_id, 'offers')
|
||||
|
||||
for offer in offers:
|
||||
read_json_api(UI_PORT + node_id, 'revokeoffer/{}'.format(offer['offer_id']))
|
||||
|
||||
for i in range(20):
|
||||
delay_event.wait(1)
|
||||
offers = read_json_api(UI_PORT + node_id, 'offers')
|
||||
if len(offers) == 0:
|
||||
return
|
||||
raise ValueError('clear_offers failed')
|
||||
|
||||
|
||||
def wait_for_offers(delay_event, node_id, num_offers) -> 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')
|
||||
if len(offers) >= num_offers:
|
||||
return
|
||||
raise ValueError('wait_for_offers failed')
|
||||
|
||||
|
||||
def delete_file(filepath: str) -> None:
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
|
||||
|
||||
def get_created_offers(rv_stdout):
|
||||
offers = []
|
||||
for line in rv_stdout:
|
||||
if line.startswith('New offer'):
|
||||
offers.append(line.split(':')[1].strip())
|
||||
return offers
|
||||
|
||||
|
||||
def count_lines_with(rv_stdout, str_needle):
|
||||
lines_found = 0
|
||||
for line in rv_stdout:
|
||||
if str_needle in line:
|
||||
lines_found += 1
|
||||
return lines_found
|
||||
|
||||
|
||||
def get_created_bids(rv_stdout):
|
||||
bids = []
|
||||
for line in rv_stdout:
|
||||
if line.startswith('New bid'):
|
||||
bids.append(line.split(':')[1].strip())
|
||||
return bids
|
||||
|
||||
|
||||
def get_possible_bids(rv_stdout):
|
||||
bids = []
|
||||
tag = 'Would create bid: '
|
||||
for line in rv_stdout:
|
||||
if line.startswith(tag):
|
||||
bids.append(json.loads(line[len(tag):].replace("'", '"')))
|
||||
return bids
|
||||
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
delay_event = threading.Event()
|
||||
thread_http = HttpThread()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(Test, cls).setUpClass()
|
||||
cls.thread_http.start()
|
||||
|
||||
script_path = 'scripts/createoffers.py'
|
||||
datadir = '/tmp/bsx_scripts'
|
||||
if not os.path.isdir(datadir):
|
||||
os.makedirs(datadir)
|
||||
|
||||
cls.node0_configfile = os.path.join(datadir, 'node0.json')
|
||||
cls.node0_statefile = os.path.join(datadir, 'node0_state.json')
|
||||
cls.node0_args = [script_path, '--port', str(UI_PORT), '--configfile', cls.node0_configfile, '--statefile', cls.node0_statefile, '--oneshot', '--debug']
|
||||
|
||||
cls.node1_configfile = os.path.join(datadir, 'node1.json')
|
||||
cls.node1_statefile = os.path.join(datadir, 'node1_state.json')
|
||||
cls.node1_args = [script_path, '--port', str(UI_PORT + 1), '--configfile', cls.node1_configfile, '--statefile', cls.node1_statefile, '--oneshot', '--debug']
|
||||
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
logging.info('Stopping test')
|
||||
cls.thread_http.stop()
|
||||
|
||||
def test_offers(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)
|
||||
|
||||
node0_test1_config = {
|
||||
'offers': [
|
||||
{
|
||||
'name': 'offer example 1',
|
||||
'coin_from': 'Particl',
|
||||
'coin_to': 'Monero',
|
||||
'amount': 20,
|
||||
'minrate': 0.05,
|
||||
'ratetweakpercent': 5,
|
||||
'amount_variable': True,
|
||||
'address': -1,
|
||||
'min_coin_from_amt': 20,
|
||||
'max_coin_to_amt': -1
|
||||
},
|
||||
{
|
||||
'name': 'offer example 1_2',
|
||||
'coin_from': 'Particl',
|
||||
'coin_to': 'Monero',
|
||||
'amount': 21,
|
||||
'minrate': 0.07,
|
||||
'ratetweakpercent': 5,
|
||||
'amount_variable': True,
|
||||
'address': -1,
|
||||
'min_coin_from_amt': 21,
|
||||
'max_coin_to_amt': -1
|
||||
}
|
||||
],
|
||||
}
|
||||
with open(self.node0_configfile, 'w') as fp:
|
||||
json.dump(node0_test1_config, fp, indent=4)
|
||||
|
||||
logging.info('Test that an offer is created')
|
||||
result = subprocess.run(self.node0_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
assert (len(get_created_offers(rv_stdout)) == 1)
|
||||
|
||||
offers = read_json_api(UI_PORT, 'offers')
|
||||
assert (len(offers) == 1)
|
||||
|
||||
logging.info('Test that an offer is not created while delaying')
|
||||
result = subprocess.run(self.node0_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
assert (len(get_created_offers(rv_stdout)) == 0)
|
||||
|
||||
with open(self.node0_statefile) as fs:
|
||||
node0_state = json.load(fs)
|
||||
node0_state['delay_next_offer_before'] = 0
|
||||
with open(self.node0_statefile, 'w') as fp:
|
||||
json.dump(node0_state, fp, indent=4)
|
||||
|
||||
logging.info('Test that the second offer is created when not delaying')
|
||||
result = subprocess.run(self.node0_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
assert (len(get_created_offers(rv_stdout)) == 1)
|
||||
|
||||
with open(self.node0_statefile) as fs:
|
||||
node0_state = json.load(fs)
|
||||
assert (len(node0_state['offers']['offer example 1']) == 1)
|
||||
assert (len(node0_state['offers']['offer example 1_2']) == 1)
|
||||
|
||||
offers = read_json_api(UI_PORT, 'offers')
|
||||
assert (len(offers) == 2)
|
||||
|
||||
addr_bid_from = read_json_api(UI_PORT + 1, 'smsgaddresses/new')['new_address']
|
||||
node1_test1_config = {
|
||||
'bids': [
|
||||
{
|
||||
'name': 'bid example 1',
|
||||
'coin_from': 'PART',
|
||||
'coin_to': 'XMR',
|
||||
'amount': 10,
|
||||
'maxrate': 0.06,
|
||||
'amount_variable': True,
|
||||
'address': addr_bid_from,
|
||||
'min_swap_amount': 0.1,
|
||||
'max_coin_from_balance': -1,
|
||||
'min_coin_to_balance': -1,
|
||||
'max_concurrent': 4,
|
||||
},
|
||||
{
|
||||
'coin_from': 'PART',
|
||||
'coin_to': 'XMR',
|
||||
'amount': 10,
|
||||
'maxrate': 0.04,
|
||||
'amount_variable': True,
|
||||
'address': -1,
|
||||
'min_swap_amount': 0.1,
|
||||
'max_coin_from_balance': -1,
|
||||
'min_coin_to_balance': -1,
|
||||
}
|
||||
],
|
||||
}
|
||||
with open(self.node1_configfile, 'w') as fp:
|
||||
json.dump(node1_test1_config, fp, indent=4)
|
||||
|
||||
wait_for_offers(self.delay_event, 1, 2)
|
||||
|
||||
logging.info('Test that a bid is created')
|
||||
result = subprocess.run(self.node1_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
assert (len(get_created_bids(rv_stdout)) == 1)
|
||||
|
||||
logging.info('Test no bids are created while delaying')
|
||||
result = subprocess.run(self.node1_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
assert (count_lines_with(rv_stdout, 'Delaying bids until') == 1)
|
||||
|
||||
with open(self.node1_statefile) as fs:
|
||||
node1_state = json.load(fs)
|
||||
node1_state['delay_next_bid_before'] = 0
|
||||
with open(self.node1_statefile, 'w') as fp:
|
||||
json.dump(node1_state, fp, indent=4)
|
||||
|
||||
logging.info('Test that a bid is not created if one already exists on that offer')
|
||||
result = subprocess.run(self.node1_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
assert (count_lines_with(rv_stdout, 'Bid rate too low for offer') == 3)
|
||||
assert (count_lines_with(rv_stdout, 'Already bidding on offer') == 1)
|
||||
|
||||
logging.info(f'Modifying node1 config')
|
||||
node1_test1_config['bids'][0]['maxrate'] = 0.07
|
||||
node1_test1_config['bids'][0]['max_coin_from_balance'] = 100
|
||||
node1_test1_config['bids'][0]['min_coin_to_balance'] = 100
|
||||
node1_test1_config['bids'][0]['min_swap_amount'] = 9
|
||||
node1_test1_config['wallet_port_override'] = 12699
|
||||
node1_test1_config['test_mode'] = True
|
||||
with open(self.node1_configfile, 'w') as fp:
|
||||
json.dump(node1_test1_config, fp, indent=4)
|
||||
|
||||
self.thread_http.return_data = {
|
||||
'PART': {
|
||||
'balance': '0.0',
|
||||
'unconfirmed': '0.0',
|
||||
'expected_seed': True,
|
||||
'encrypted': False,
|
||||
'locked': False,
|
||||
'anon_balance': 0.0,
|
||||
'anon_pending': 0.0,
|
||||
'blind_balance': 0.0,
|
||||
'blind_unconfirmed': 0.0,
|
||||
'version': 23000300,
|
||||
'name': 'Particl',
|
||||
'blocks': 3556,
|
||||
'synced': '100.00'
|
||||
},
|
||||
'XMR': {
|
||||
'balance': '362299.12',
|
||||
'unconfirmed': '0.0',
|
||||
'expected_seed': True,
|
||||
'encrypted': False,
|
||||
'locked': False,
|
||||
'main_address': '',
|
||||
'version': 65562,
|
||||
'name': 'Monero',
|
||||
'blocks': 10470,
|
||||
'synced': '100.00',
|
||||
'known_block_count': 10470
|
||||
}
|
||||
}
|
||||
|
||||
# Check max_coin_from_balance (bids increase coin_from)
|
||||
result = subprocess.run(self.node1_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
possible_bids = get_possible_bids(rv_stdout)
|
||||
assert (len(possible_bids) == 1)
|
||||
assert (float(possible_bids[0]['amount_from']) == 21.0)
|
||||
|
||||
# Test multiple bids are delayed
|
||||
result = subprocess.run(self.node1_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
assert (count_lines_with(rv_stdout, 'Delaying bids until') == 1)
|
||||
|
||||
delete_file(self.node1_statefile)
|
||||
self.thread_http.return_data['PART']['balance'] = 100.0
|
||||
result = subprocess.run(self.node1_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
assert (count_lines_with(rv_stdout, 'Bid amount would exceed maximum wallet total') == 1)
|
||||
|
||||
self.thread_http.return_data['PART']['balance'] = 90.0
|
||||
result = subprocess.run(self.node1_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
possible_bids = get_possible_bids(rv_stdout)
|
||||
assert (len(possible_bids) == 1)
|
||||
assert (math.isclose(float(possible_bids[0]['amount_from']), 10.0))
|
||||
|
||||
# Check min_swap_amount
|
||||
delete_file(self.node1_statefile)
|
||||
self.thread_http.return_data['PART']['balance'] = 95.0
|
||||
result = subprocess.run(self.node1_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
possible_bids = get_possible_bids(rv_stdout)
|
||||
assert (count_lines_with(rv_stdout, 'Bid amount would exceed maximum wallet total') == 1)
|
||||
|
||||
# Check min_coin_to_balance (bids decrease coin_to)
|
||||
self.thread_http.return_data['PART']['balance'] = 0.0
|
||||
self.thread_http.return_data['XMR']['balance'] = 101.0
|
||||
|
||||
result = subprocess.run(self.node1_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
possible_bids = get_possible_bids(rv_stdout)
|
||||
possible_bids = get_possible_bids(rv_stdout)
|
||||
assert (len(possible_bids) == 1)
|
||||
assert (float(possible_bids[0]['amount_from'] < 20.0))
|
||||
|
||||
logging.info(f'Adding mock data to node1 db for tests')
|
||||
rows = []
|
||||
offers = read_json_api(UI_PORT, 'offers')
|
||||
|
||||
now = int(time.time())
|
||||
for offer in offers:
|
||||
rows.append((1, offer['addr_from'], 5, 5, now, now))
|
||||
db_path = '/tmp/test_persistent/client1/db_regtest.sqlite'
|
||||
with sqlite3.connect(db_path) as dbc:
|
||||
c = dbc.cursor()
|
||||
c.executemany('INSERT INTO knownidentities (active_ind, address, num_sent_bids_failed, num_recv_bids_failed, updated_at, created_at) VALUES (?,?,?,?,?,?)', rows)
|
||||
dbc.commit()
|
||||
|
||||
delete_file(self.node1_statefile)
|
||||
self.thread_http.return_data['XMR']['balance'] = 10000.0
|
||||
result = subprocess.run(self.node1_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
assert (len(get_possible_bids(get_possible_bids(rv_stdout))) == 0)
|
||||
assert (count_lines_with(rv_stdout, 'too many failed bids') == 1)
|
||||
|
||||
'''
|
||||
TODO
|
||||
node0_test1_config['stealthex'] = [
|
||||
{
|
||||
'coin_from': 'XMR',
|
||||
'coin_to': 'BTC',
|
||||
'min_balance_from': 1,
|
||||
'min_amount_tx': 1,
|
||||
'max_amount_tx': 5,
|
||||
'min_rate': 0.01,
|
||||
'refund_address': 'auto',
|
||||
'receive_address': 'auto',
|
||||
'api_key': 'API_KEY_HERE'
|
||||
}
|
||||
]
|
||||
node0_test1_config['wallet_port_override'] = 12699
|
||||
node0_test1_config['test_mode'] = True
|
||||
with open(self.node0_configfile, 'w') as fp:
|
||||
json.dump(node0_test1_config, fp, indent=4)
|
||||
|
||||
result = subprocess.run(self.node0_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
'''
|
||||
|
||||
def test_bid_tracking(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)
|
||||
|
||||
addrs = []
|
||||
for i in range(2):
|
||||
addrs.append(read_json_api(UI_PORT, 'smsgaddresses/new')['new_address'])
|
||||
|
||||
node0_test2_config = {
|
||||
'offers': [
|
||||
{
|
||||
'name': 'offer example 1',
|
||||
'coin_from': 'Particl',
|
||||
'coin_to': 'Monero',
|
||||
'amount': 20,
|
||||
'minrate': 0.04,
|
||||
'ratetweakpercent': 5,
|
||||
'amount_variable': True,
|
||||
'address': addrs[0],
|
||||
'min_coin_from_amt': 20,
|
||||
'max_coin_to_amt': -1
|
||||
},
|
||||
{
|
||||
'name': 'offer example 1_2',
|
||||
'coin_from': 'Particl',
|
||||
'coin_to': 'Monero',
|
||||
'amount': 21,
|
||||
'minrate': 0.05,
|
||||
'ratetweakpercent': 5,
|
||||
'amount_variable': True,
|
||||
'address': addrs[1],
|
||||
'min_coin_from_amt': 21,
|
||||
'max_coin_to_amt': -1
|
||||
},
|
||||
{
|
||||
'name': 'offer example 1_3',
|
||||
'coin_from': 'Particl',
|
||||
'coin_to': 'Monero',
|
||||
'amount': 22,
|
||||
'minrate': 0.06,
|
||||
'ratetweakpercent': 5,
|
||||
'amount_variable': True,
|
||||
'address': 'auto',
|
||||
'min_coin_from_amt': 22,
|
||||
'max_coin_to_amt': -1
|
||||
}
|
||||
],
|
||||
}
|
||||
with open(self.node0_configfile, 'w') as fp:
|
||||
json.dump(node0_test2_config, fp, indent=4)
|
||||
|
||||
offer_ids = []
|
||||
logging.info('Create three offers')
|
||||
|
||||
|
||||
for i in range(3):
|
||||
if i > 0:
|
||||
with open(self.node0_statefile) as fs:
|
||||
node0_state = json.load(fs)
|
||||
node0_state['delay_next_offer_before'] = 0
|
||||
with open(self.node0_statefile, 'w') as fp:
|
||||
json.dump(node0_state, fp, indent=4)
|
||||
|
||||
result = subprocess.run(self.node0_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
created_offers = get_created_offers(rv_stdout)
|
||||
assert (len(get_created_offers(rv_stdout)) == 1)
|
||||
offer_ids.append(created_offers[0])
|
||||
|
||||
found_addrs = {}
|
||||
for offer_id in offer_ids:
|
||||
offer = read_json_api(UI_PORT, f'offers/{offer_id}')[0]
|
||||
found_addrs[offer['addr_from']] = found_addrs.get(offer['addr_from'], 0) + 1
|
||||
|
||||
for addr in addrs:
|
||||
assert (found_addrs[addr] == 1)
|
||||
|
||||
addr_bid_from = read_json_api(UI_PORT + 1, 'smsgaddresses/new')['new_address']
|
||||
node1_test1_config = {
|
||||
'bids': [
|
||||
{
|
||||
'name': 'bid example 1',
|
||||
'coin_from': 'PART',
|
||||
'coin_to': 'XMR',
|
||||
'amount': 50,
|
||||
'maxrate': 0.08,
|
||||
'amount_variable': False,
|
||||
'address': addr_bid_from,
|
||||
'min_swap_amount': 1,
|
||||
'max_coin_from_balance': -1,
|
||||
'min_coin_to_balance': -1
|
||||
}
|
||||
],
|
||||
}
|
||||
with open(self.node1_configfile, 'w') as fp:
|
||||
json.dump(node1_test1_config, fp, indent=4)
|
||||
|
||||
wait_for_offers(self.delay_event, 1, 3)
|
||||
|
||||
logging.info('Check that no bids are created (offer values too low)')
|
||||
result = subprocess.run(self.node1_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
assert (len(get_created_bids(rv_stdout)) == 0)
|
||||
assert (count_lines_with(rv_stdout, 'Offer amount too low for bid') == 3)
|
||||
|
||||
node1_test1_config['bids'][0]['amount_variable'] = True
|
||||
with open(self.node1_configfile, 'w') as fp:
|
||||
json.dump(node1_test1_config, fp, indent=4)
|
||||
|
||||
logging.info('Check that one bid is created at the best rate')
|
||||
result = subprocess.run(self.node1_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
created_bids = get_created_bids(rv_stdout)
|
||||
assert (len(created_bids) == 1)
|
||||
|
||||
bid_id = created_bids[0].split(' ')[0]
|
||||
bid = read_json_api(UI_PORT + 1, f'bids/{bid_id}')
|
||||
assert (math.isclose(float(bid['bid_rate']), 0.04))
|
||||
assert (math.isclose(float(bid['amt_from']), 20.0))
|
||||
assert (bid['addr_from'] == addr_bid_from)
|
||||
|
||||
logging.info('Check that bids are delayed')
|
||||
result = subprocess.run(self.node1_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
assert (count_lines_with(rv_stdout, 'Delaying bids until') == 1)
|
||||
assert (len(get_created_bids(rv_stdout)) == 0)
|
||||
|
||||
with open(self.node1_statefile) as fs:
|
||||
node1_state = json.load(fs)
|
||||
node1_state['delay_next_bid_before'] = 0
|
||||
with open(self.node1_statefile, 'w') as fp:
|
||||
json.dump(node1_state, fp, indent=4)
|
||||
|
||||
logging.info('Test that a bid is not created while one is active')
|
||||
result = subprocess.run(self.node1_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
assert (len(get_created_bids(rv_stdout)) == 0)
|
||||
assert (count_lines_with(rv_stdout, 'Max concurrent bids') == 1)
|
||||
|
||||
logging.info('Waiting for bid to complete')
|
||||
bid_complete: bool = False
|
||||
for i in range(60):
|
||||
self.delay_event.wait(5)
|
||||
bid = read_json_api(UI_PORT + 1, f'bids/{bid_id}')
|
||||
print('bid_state', bid['bid_state'])
|
||||
if bid['bid_state'] == 'Completed':
|
||||
bid_complete = True
|
||||
break
|
||||
|
||||
assert bid_complete
|
||||
|
||||
logging.info('Test that a bid is created after one expires')
|
||||
result = subprocess.run(self.node1_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
created_bids = get_created_bids(rv_stdout)
|
||||
assert (len(created_bids) == 1)
|
||||
assert (count_lines_with(rv_stdout, 'Marking bid inactive') == 1)
|
||||
|
||||
logging.info('Test that two bids are created if max concurrent is raised')
|
||||
node1_test1_config['bids'][0]['max_concurrent'] = 2
|
||||
with open(self.node1_configfile, 'w') as fp:
|
||||
json.dump(node1_test1_config, fp, indent=4)
|
||||
|
||||
with open(self.node1_statefile) as fs:
|
||||
node1_state = json.load(fs)
|
||||
node1_state['delay_next_bid_before'] = 0
|
||||
with open(self.node1_statefile, 'w') as fp:
|
||||
json.dump(node1_state, fp, indent=4)
|
||||
|
||||
result = subprocess.run(self.node1_args, stdout=subprocess.PIPE)
|
||||
rv_stdout = result.stdout.decode().split('\n')
|
||||
created_bids = get_created_bids(rv_stdout)
|
||||
assert (len(created_bids) == 1)
|
||||
|
||||
bid_id = created_bids[0].split(' ')[0]
|
||||
bid = read_json_api(UI_PORT + 1, f'bids/{bid_id}')
|
||||
assert (math.isclose(float(bid['bid_rate']), 0.05))
|
||||
assert (math.isclose(float(bid['amt_from']), 21.0))
|
||||
assert (bid['addr_from'] == addr_bid_from)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -254,6 +254,12 @@ class BasicSwapTest(BaseTest):
|
|||
self.callnoderpc('unloadwallet', [new_wallet_name])
|
||||
assert (addr == 'bcrt1qps7hnjd866e9ynxadgseprkc2l56m00dvwargr')
|
||||
|
||||
self.swap_clients[0].initialiseWallet(Coins.BTC, raise_errors=True)
|
||||
assert self.swap_clients[0].checkWalletSeed(Coins.BTC) is True
|
||||
for i in range(1500):
|
||||
self.callnoderpc('getnewaddress')
|
||||
assert self.swap_clients[0].checkWalletSeed(Coins.BTC) is True
|
||||
|
||||
def do_test_01_full_swap(self, coin_from, coin_to):
|
||||
logging.info('---------- Test {} to {}'.format(coin_from.name, coin_to.name))
|
||||
|
||||
|
|
|
@ -133,6 +133,18 @@ class Test(BaseTest):
|
|||
rv = read_json_api(1800, 'getcoinseed', {'coin': 'BTC'})
|
||||
assert (rv['seed'] == '8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b')
|
||||
|
||||
rv = read_json_api(1800, 'identities/ppCsRro5po7Yu6kyu5XjSyr3A1PPdk9j1F', {'set_label': 'test 1'})
|
||||
assert (len(rv) == 1)
|
||||
assert (rv[0]['address'] == 'ppCsRro5po7Yu6kyu5XjSyr3A1PPdk9j1F')
|
||||
assert (rv[0]['label'] == 'test 1')
|
||||
rv = read_json_api(1800, 'identities/ppCsRro5po7Yu6kyu5XjSyr3A1PPdk9j1F', {'set_label': 'test 2'})
|
||||
assert (len(rv) == 1)
|
||||
assert (rv[0]['address'] == 'ppCsRro5po7Yu6kyu5XjSyr3A1PPdk9j1F')
|
||||
assert (rv[0]['label'] == 'test 2')
|
||||
|
||||
rv = read_json_api(1800, 'identities/pPCsRro5po7Yu6kyu5XjSyr3A1PPdk9j1F', {'set_label': 'test 3'})
|
||||
assert (rv['error'] == 'Invalid identity address')
|
||||
|
||||
def test_01_verifyrawtransaction(self):
|
||||
txn = '0200000001eb6e5c4ebba4efa32f40c7314cad456a64008e91ee30b2dd0235ab9bb67fbdbb01000000ee47304402200956933242dde94f6cf8f195a470f8d02aef21ec5c9b66c5d3871594bdb74c9d02201d7e1b440de8f4da672d689f9e37e98815fb63dbc1706353290887eb6e8f7235012103dc1b24feb32841bc2f4375da91fa97834e5983668c2a39a6b7eadb60e7033f9d205a803b28fe2f86c17db91fa99d7ed2598f79b5677ffe869de2e478c0d1c02cc7514c606382012088a8201fe90717abb84b481c2a59112414ae56ec8acc72273642ca26cc7a5812fdc8f68876a914225fbfa4cb725b75e511810ac4d6f74069bdded26703520140b27576a914207eb66b2fd6ed9924d6217efc7fa7b38dfabe666888acffffffff01e0167118020000001976a9140044e188928710cecba8311f1cf412135b98145c88ac00000000'
|
||||
prevout = {
|
||||
|
|
Loading…
Reference in a new issue