Variable bid amount and rate.

This commit is contained in:
tecnovert 2021-11-22 22:24:48 +02:00
parent 8a9f4f9e38
commit 99534756de
No known key found for this signature in database
GPG key ID: 8ED6D8750C4E3F93
8 changed files with 215 additions and 52 deletions

View file

@ -984,6 +984,14 @@ class BasicSwap(BaseApp):
if valid_for_seconds > 24 * 60 * 60:
raise ValueError('Bid TTL too high')
def validateBidAmount(self, offer, bid_amount, bid_rate):
ensure(bid_amount >= offer.min_bid_amount, 'Bid amount below minimum')
ensure(bid_amount <= offer.amount_from, 'Bid amount above offer amount')
if not offer.amount_negotiable:
ensure(offer.amount_from == bid_amount, 'Bid amount must match offer amount.')
if not offer.rate_negotiable:
ensure(offer.rate == bid_rate, 'Bid rate must match offer rate.')
def getOfferAddressTo(self, extra_options):
if 'addr_send_to' in extra_options:
return extra_options['addr_send_to']
@ -1613,7 +1621,7 @@ class BasicSwap(BaseApp):
return q[0]
def postBid(self, offer_id, amount, addr_send_from=None, extra_options={}):
# Bid to send bid.amount * offer.rate of coin_to in exchange for bid.amount of coin_from
# Bid to send bid.amount * bid.rate of coin_to in exchange for bid.amount of coin_from
self.log.debug('postBid %s', offer_id.hex())
offer = self.getOffer(offer_id)
@ -1627,10 +1635,7 @@ class BasicSwap(BaseApp):
self.validateBidValidTime(offer.swap_type, offer.coin_from, offer.coin_to, valid_for_seconds)
bid_rate = extra_options.get('bid_rate', offer.rate)
if not offer.amount_negotiable:
ensure(offer.amount_from == int(amount), 'Bid amount must match offer amount.')
if not offer.rate_negotiable:
ensure(offer.rate == bid_rate, 'Bid rate must match offer rate.')
self.validateBidAmount(offer, amount, bid_rate)
self.mxDB.acquire()
try:
@ -1650,7 +1655,7 @@ class BasicSwap(BaseApp):
contract_count = self.getNewContractId()
amount_to = int((msg_buf.amount * offer.rate) // self.ci(coin_from).COIN())
amount_to = int((msg_buf.amount * bid_rate) // self.ci(coin_from).COIN())
now = int(time.time())
if offer.swap_type == SwapTypes.SELLER_FIRST:
@ -1958,7 +1963,7 @@ class BasicSwap(BaseApp):
self.swaps_in_progress[bid_id] = (bid, offer)
def postXmrBid(self, offer_id, amount, addr_send_from=None, extra_options={}):
# Bid to send bid.amount * offer.rate of coin_to in exchange for bid.amount of coin_from
# Bid to send bid.amount * bid.rate of coin_to in exchange for bid.amount of coin_from
# Send MSG1L F -> L
self.log.debug('postXmrBid %s', offer_id.hex())
@ -1979,10 +1984,7 @@ class BasicSwap(BaseApp):
ci_to = self.ci(coin_to)
bid_rate = extra_options.get('bid_rate', offer.rate)
if not offer.amount_negotiable:
ensure(offer.amount_from == int(amount), 'Bid amount must match offer amount.')
if not offer.rate_negotiable:
ensure(offer.rate == bid_rate, 'Bid rate must match offer rate.')
self.validateBidAmount(offer, amount, bid_rate)
self.checkSynced(coin_from, coin_to)
@ -2080,7 +2082,7 @@ class BasicSwap(BaseApp):
rate=msg_buf.rate,
created_at=bid_created_at,
contract_count=xmr_swap.contract_count,
amount_to=(msg_buf.amount * offer.rate) // ci_from.COIN(),
amount_to=(msg_buf.amount * msg_buf.rate) // ci_from.COIN(),
expire_at=bid_created_at + msg_buf.time_valid,
bid_addr=bid_addr,
was_sent=True,
@ -2397,7 +2399,7 @@ class BasicSwap(BaseApp):
amount_to = bid.amount_to
# Check required?
assert(amount_to == (bid.amount * offer.rate) // self.ci(offer.coin_from).COIN())
assert(amount_to == (bid.amount * bid.rate) // self.ci(offer.coin_from).COIN())
if bid.debug_ind == DebugTypes.MAKE_INVALID_PTX:
amount_to -= 1
@ -3773,15 +3775,9 @@ class BasicSwap(BaseApp):
ensure(offer.state == OfferStates.OFFER_RECEIVED, 'Bad offer state')
ensure(msg['to'] == offer.addr_from, 'Received on incorrect address')
ensure(now <= offer.expire_at, 'Offer expired')
ensure(bid_data.amount >= offer.min_bid_amount, 'Bid amount below minimum')
ensure(bid_data.amount <= offer.amount_from, 'Bid amount above offer amount')
self.validateBidValidTime(offer.swap_type, offer.coin_from, offer.coin_to, bid_data.time_valid)
ensure(now <= msg['sent'] + bid_data.time_valid, 'Bid expired')
if not offer.amount_negotiable:
ensure(offer.amount_from == bid_data.amount, 'Bid amount must match offer amount.')
if not offer.rate_negotiable:
ensure(offer.rate == bid_data.rate, 'Bid rate must match offer rate.')
self.validateBidAmount(offer, bid_data.amount, bid_data.rate)
# TODO: Allow higher bids
# assert(bid_data.rate != offer['data'].rate), 'Bid rate mismatch'
@ -3790,7 +3786,7 @@ class BasicSwap(BaseApp):
ci_from = self.ci(offer.coin_from)
ci_to = self.ci(coin_to)
amount_to = int((bid_data.amount * offer.rate) // ci_from.COIN())
amount_to = int((bid_data.amount * bid_data.rate) // ci_from.COIN())
swap_type = offer.swap_type
if swap_type == SwapTypes.SELLER_FIRST:
ensure(len(bid_data.pkhash_buyer) == 20, 'Bad pkhash_buyer length')
@ -4067,15 +4063,10 @@ class BasicSwap(BaseApp):
raise ValueError('Bad offer state')
ensure(msg['to'] == offer.addr_from, 'Received on incorrect address')
ensure(now <= offer.expire_at, 'Offer expired')
ensure(bid_data.amount >= offer.min_bid_amount, 'Bid amount below minimum')
ensure(bid_data.amount <= offer.amount_from, 'Bid amount above offer amount')
self.validateBidValidTime(offer.swap_type, offer.coin_from, offer.coin_to, bid_data.time_valid)
ensure(now <= msg['sent'] + bid_data.time_valid, 'Bid expired')
if not offer.amount_negotiable:
ensure(offer.amount_from == bid_data.amount, 'Bid amount must match offer amount.')
if not offer.rate_negotiable:
ensure(offer.rate == bid_data.rate, 'Bid rate must match offer rate.')
self.validateBidAmount(offer, bid_data.amount, bid_data.rate)
ensure(ci_to.verifyKey(bid_data.kbvf), 'Invalid chain B follower view key')
ensure(ci_from.verifyPubkey(bid_data.pkaf), 'Invalid chain A follower public key')
@ -4092,7 +4083,7 @@ class BasicSwap(BaseApp):
amount=bid_data.amount,
rate=bid_data.rate,
created_at=msg['sent'],
amount_to=(bid_data.amount * offer.rate) // ci_from.COIN(),
amount_to=(bid_data.amount * bid_data.rate) // ci_from.COIN(),
expire_at=msg['sent'] + bid_data.time_valid,
bid_addr=msg['from'],
was_received=True,
@ -5525,24 +5516,75 @@ class BasicSwap(BaseApp):
return self._network.get_info()
def lookupRates(self, coin_from, coin_to):
self.log.debug('lookupRates {}, {}'.format(coin_from, coin_to))
rv = {}
ci_from = self.ci(int(coin_from))
ci_to = self.ci(int(coin_to))
name_from = ci_from.coin_name().lower()
name_to = ci_to.coin_name().lower()
headers = {'Connection': 'close'}
name_from = ci_from.chainparams()['name']
name_to = ci_to.chainparams()['name']
url = 'https://api.coingecko.com/api/v3/simple/price?ids={},{}&vs_currencies=usd'.format(name_from, name_to)
headers = {'User-Agent': 'Mozilla/5.0'}
start = time.time()
req = urllib.request.Request(url, headers=headers)
js = json.loads(urllib.request.urlopen(req).read())
js = json.loads(urllib.request.urlopen(req, timeout=10).read())
js['time_taken'] = time.time() - start
rate = float(js[name_from]['usd']) / float(js[name_to]['usd'])
js['rate'] = ci_to.format_amount(rate, conv_int=True, r=1)
js['rate_inferred'] = ci_to.format_amount(rate, conv_int=True, r=1)
rv['coingecko'] = js
url = 'https://api.bittrex.com/api/v1.1/public/getticker?market={}-{}'.format(ci_from.ticker(), ci_to.ticker())
headers = {'User-Agent': 'Mozilla/5.0'}
req = urllib.request.Request(url, headers=headers)
js = json.loads(urllib.request.urlopen(req).read())
rv['bittrex'] = js
ticker_from = ci_from.chainparams()['ticker']
ticker_to = ci_to.chainparams()['ticker']
if ci_from.coin_type() == Coins.BTC:
pair = '{}-{}'.format(ticker_from, ticker_to)
url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair
start = time.time()
req = urllib.request.Request(url, headers=headers)
js = json.loads(urllib.request.urlopen(req, timeout=10).read())
js['time_taken'] = time.time() - start
js['pair'] = pair
try:
rate_inverted = ci_from.make_int(1.0 / float(js['result']['Last']), r=1)
js['rate_inferred'] = ci_to.format_amount(rate_inverted)
except Exception as e:
self.log.warning('lookupRates error: %s', str(e))
js['rate_inferred'] = 'error'
rv['bittrex'] = js
elif ci_to.coin_type() == Coins.BTC:
pair = '{}-{}'.format(ticker_to, ticker_from)
url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair
start = time.time()
req = urllib.request.Request(url, headers=headers)
js = json.loads(urllib.request.urlopen(req, timeout=10).read())
js['time_taken'] = time.time() - start
js['pair'] = pair
js['rate_last'] = js['result']['Last']
rv['bittrex'] = js
else:
pair = 'BTC-{}'.format(ticker_from)
url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair
start = time.time()
req = urllib.request.Request(url, headers=headers)
js_from = json.loads(urllib.request.urlopen(req, timeout=10).read())
js_from['time_taken'] = time.time() - start
js_from['pair'] = pair
pair = 'BTC-{}'.format(ticker_to)
url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair
start = time.time()
req = urllib.request.Request(url, headers=headers)
js_to = json.loads(urllib.request.urlopen(req, timeout=10).read())
js_to['time_taken'] = time.time() - start
js_to['pair'] = pair
try:
rate_inferred = float(js_from['result']['Last']) / float(js_to['result']['Last'])
rate_inferred = ci_to.format_amount(rate, conv_int=True, r=1)
except Exception as e:
rate_inferred = 'error'
rv['bittrex'] = {'from': js_from, 'to': js_to, 'rate_inferred': rate_inferred}
return rv

View file

@ -497,7 +497,9 @@ class HttpHandler(BaseHTTPRequestHandler):
try:
page_data['amt_from'] = get_data_entry(form_data, 'amt_from')
parsed_data['amt_from'] = inputAmount(page_data['amt_from'], ci_from)
parsed_data['min_bid'] = int(parsed_data['amt_from'])
# TODO: Add min_bid to the ui
parsed_data['min_bid'] = ci_from.chainparams_network()['min_amount']
except Exception:
errors.append('Amount From')
@ -516,6 +518,10 @@ class HttpHandler(BaseHTTPRequestHandler):
page_data['rate_var'] = True if have_data_entry(form_data, 'rate_var') else False
parsed_data['rate_var'] = page_data['rate_var']
# Change default autoaccept to false
if page_data['amt_var'] or page_data['rate_var']:
page_data['autoaccept'] = False
if b'step1' in form_data:
if len(errors) == 0 and b'continue' in form_data:
page_data['step2'] = True
@ -717,6 +723,14 @@ class HttpHandler(BaseHTTPRequestHandler):
sent_bid_id = None
show_bid_form = None
form_data = self.checkForm(post_string, 'offer', messages)
ci_from = swap_client.ci(Coins(offer.coin_from))
ci_to = swap_client.ci(Coins(offer.coin_to))
# Set defaults
bid_amount = ci_from.format_amount(offer.amount_from)
bid_rate = ci_to.format_amount(offer.rate)
if form_data:
if b'revoke_offer' in form_data:
try:
@ -739,20 +753,29 @@ class HttpHandler(BaseHTTPRequestHandler):
extra_options = {
'valid_for_seconds': minutes_valid * 60,
}
sent_bid_id = swap_client.postBid(offer_id, offer.amount_from, addr_send_from=addr_from, extra_options=extra_options).hex()
if have_data_entry(form_data, 'bid_rate'):
bid_rate = get_data_entry(form_data, 'bid_rate')
extra_options['bid_rate'] = ci_to.make_int(bid_rate, r=1)
if have_data_entry(form_data, 'bid_amount'):
bid_amount = get_data_entry(form_data, 'bid_amount')
amount_from = inputAmount(bid_amount, ci_from)
else:
amount_from = offer.amount_from
sent_bid_id = swap_client.postBid(offer_id, amount_from, addr_send_from=addr_from, extra_options=extra_options).hex()
except Exception as ex:
messages.append('Error: Send bid failed: ' + str(ex))
show_bid_form = True
ci_from = swap_client.ci(Coins(offer.coin_from))
ci_to = swap_client.ci(Coins(offer.coin_to))
data = {
'tla_from': ci_from.ticker(),
'tla_to': ci_to.ticker(),
'state': strOfferState(offer.state),
'coin_from': ci_from.coin_name(),
'coin_to': ci_to.coin_name(),
'coin_from_ind': int(ci_from.coin_type()),
'coin_to_ind': int(ci_to.coin_type()),
'amt_from': ci_from.format_amount(offer.amount_from),
'amt_to': ci_to.format_amount((offer.amount_from * offer.rate) // ci_from.COIN()),
'rate': ci_to.format_amount(offer.rate),
@ -767,6 +790,8 @@ class HttpHandler(BaseHTTPRequestHandler):
'show_bid_form': show_bid_form,
'amount_negotiable': offer.amount_negotiable,
'rate_negotiable': offer.rate_negotiable,
'bid_amount': bid_amount,
'bid_rate': bid_rate,
}
data.update(extend_data)
@ -1097,6 +1122,13 @@ class HttpHandler(BaseHTTPRequestHandler):
def page_shutdown(self, url_split, post_string):
swap_client = self.server.swap_client
if len(url_split) > 2:
token = url_split[2]
expect_token = self.server.session_tokens.get('shutdown', None)
if token != expect_token:
return self.page_info('Unexpected token, still running.')
swap_client.stopRunning()
return self.page_info('Shutting down')
@ -1105,13 +1137,17 @@ class HttpHandler(BaseHTTPRequestHandler):
swap_client = self.server.swap_client
summary = swap_client.getSummary()
shutdown_token = os.urandom(8).hex()
self.server.session_tokens['shutdown'] = shutdown_token
template = env.get_template('index.html')
return bytes(template.render(
title=self.server.title,
refresh=30,
h2=self.server.title,
version=__version__,
summary=summary
summary=summary,
shutdown_token=shutdown_token
), 'UTF-8')
def page_404(self, url_split):
@ -1249,6 +1285,7 @@ class HttpThread(threading.Thread, HTTPServer):
self.swap_client = swap_client
self.title = 'BasicSwap, ' + self.swap_client.chain
self.last_form_id = dict()
self.session_tokens = dict()
self.timeout = 60
HTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)

View file

@ -23,7 +23,7 @@ Version: {{ version }}
<p><a href="/newoffer">New Offer</a></p>
<p><a href="/shutdown">Shutdown</a></p>
<p><a href="/shutdown/{{ shutdown_token }}">Shutdown</a></p>
</body>
</html>

View file

@ -43,7 +43,7 @@
<form method="post">
{% if data.show_bid_form %}
<br/><h4>New Bid</h4>
<p>You will send {{ data.amt_to }} {{ data.tla_to }} and receive {{ data.amt_from }} {{ data.tla_from }}
<p>You will send <span id="bid_amt_to">{{ data.amt_to }}</span> {{ data.tla_to }} and receive <span id="bid_amt_from">{{ data.amt_from }}</span> {{ data.tla_from }}
{% if data.xmr_type == true %}
(excluding {{ data.amt_from_lock_spend_tx_fee }} {{ data.tla_from }} in tx fees).
{% else %}
@ -62,24 +62,37 @@
</td></tr>
{% if data.amount_negotiable == true %}
<tr><td>Amount</td><td><input type="text" name="bid_amount" value="{{ data.amt_from }}"></td></tr>
<tr><td>Amount</td><td><input type="text" id="bid_amount" name="bid_amount" value="{{ data.bid_amount }}" onchange="updateBidParams('amount');"></td></tr>
{% endif %}
{% if data.rate_negotiable == true %}
<tr><td>Rate</td><td><input type="text" name="bid_rate" value="{{ data.rate }}"></td></tr>
<tr><td>Rate</td><td><input type="text" id="bid_rate" name="bid_rate" value="{{ data.bid_rate }}" onchange="updateBidParams('rate');"></td></tr>
{% endif %}
<tr><td>Minutes valid</td><td><input type="number" name="validmins" min="10" max="1440" value="{{ data.nb_validmins }}"></td></tr>
<tr><td><input type="submit" name="sendbid" value="Send Bid"><input type="submit" name="cancel" value="Cancel"></td></tr>
<tr><td>
<input type="submit" name="sendbid" value="Send Bid">
<input type="submit" name="cancel" value="Cancel">
<input name="check_rates" type="button" value="Lookup Rates" onclick='lookup_rates();'>
</td></tr>
</table>
{% else %}
<input type="submit" name="newbid" value="New Bid">
{% if data.sent == 'True' and data.was_revoked != true %}
<input name="revoke_offer" type="submit" value="Revoke Offer" onclick="return confirmPopup();">
{% endif %}
<input name="check_rates" type="button" value="Lookup Rates" onclick='lookup_rates();'>
{% endif %}
<input type="hidden" id="coin_from" value="{{ data.coin_from_ind }}">
<input type="hidden" id="coin_to" value="{{ data.coin_to_ind }}">
<input type="hidden" id="amt_var" value="{{ data.amount_negotiable }}">
<input type="hidden" id="rate_var" value="{{ data.rate_negotiable }}">
<input type="hidden" id="amount_from" value="{{ data.amt_from }}">
<input type="hidden" id="offer_rate" value="{{ data.rate }}">
<input type="hidden" name="formid" value="{{ form_id }}">
</form>
<p id="rates_display"></p>
<h4>Bids</h4>
<table>
<tr><th>Bid ID</th><th>Bid Amount</th><th>Bid Rate</th><th>Bid Status</th><th>Identity From</th></tr>
@ -91,8 +104,72 @@
<p><a href="/">home</a></p>
<script>
const xhr_rates = new XMLHttpRequest();
xhr_rates.onload = () => {
if (xhr_rates.status == 200) {
const obj = JSON.parse(xhr_rates.response);
inner_html = '<h4>Rates</h4><pre><code>' + JSON.stringify(obj, null, ' ') + '</code></pre>';
document.getElementById('rates_display').innerHTML = inner_html;
}
}
function lookup_rates() {
const coin_from = document.getElementById('coin_from').value;
const coin_to = document.getElementById('coin_to').value;
if (coin_from == '-1' || coin_to == '-1') {
alert('Coins from and to must be set first.');
return;
}
inner_html = '<h4>Rates</h4><p>Updating...</p>';
document.getElementById('rates_display').innerHTML = inner_html;
xhr_rates.open('POST', '/json/rates');
xhr_rates.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr_rates.send('coin_from='+coin_from+'&coin_to='+coin_to);
}
const xhr_bid_params = new XMLHttpRequest();
xhr_bid_params.onload = () => {
if (xhr_bid_params.status == 200) {
const obj = JSON.parse(xhr_bid_params.response);
document.getElementById('bid_amt_to').innerHTML = obj['amount_to'];
}
}
function updateBidParams(value_changed) {
const coin_from = document.getElementById('coin_from').value;
const coin_to = document.getElementById('coin_to').value;
const amt_var = document.getElementById('amt_var').value;
const rate_var = document.getElementById('rate_var').value;
let amt_from = '';
let rate = '';
if (amt_var) {
amt_from = document.getElementById('bid_amount').value;
} else {
amt_from = document.getElementById('amount_from').value;
}
if (rate_var) {
rate = document.getElementById('bid_rate').value;
} else {
rate = document.getElementById('offer_rate').value;
}
if (value_changed == 'amount') {
document.getElementById('bid_amt_from').innerHTML = amt_from;
}
xhr_bid_params.open('POST', '/json/rate');
xhr_bid_params.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr_bid_params.send('coin_from='+coin_from+'&coin_to='+coin_to+'&rate='+rate+'&amt_from='+amt_from);
}
function confirmPopup() {
return confirm("Are you sure?");
}
</script>
</body></html>

View file

@ -57,7 +57,7 @@ xhr_rates.onload = () => {
if (xhr_rates.status == 200) {
const obj = JSON.parse(xhr_rates.response);
inner_html = '<pre><code>' + JSON.stringify(obj, null, ' ') + '</code></pre>';
inner_html = '<h4>Rates</h4><pre><code>' + JSON.stringify(obj, null, ' ') + '</code></pre>';
document.getElementById('rates_display').innerHTML = inner_html;
}
}
@ -88,6 +88,9 @@ function lookup_rates() {
return;
}
inner_html = '<h4>Rates</h4><p>Updating...</p>';
document.getElementById('rates_display').innerHTML = inner_html;
xhr_rates.open('POST', '/json/rates');
xhr_rates.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr_rates.send('coin_from='+coin_from+'&coin_to='+coin_to);
@ -106,7 +109,7 @@ function set_rate(value_changed) {
}
params = 'coin_from='+coin_from+'&coin_to='+coin_to;
if (value_changed == 'rate' || (lock_rate && value_changed == 'amt_from')) {
if (value_changed == 'rate' || (lock_rate && value_changed == 'amt_from') || (amt_to == '' && value_changed == 'amt_from')) {
if (amt_from == '' || rate == '') {
return;
}

View file

@ -206,7 +206,7 @@ def describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, bid_events, edit_b
'coin_from': ci_from.coin_name(),
'coin_to': ci_to.coin_name(),
'amt_from': ci_from.format_amount(bid.amount),
'amt_to': ci_to.format_amount((bid.amount * offer.rate) // ci_from.COIN()),
'amt_to': ci_to.format_amount((bid.amount * bid.rate) // ci_from.COIN()),
'bid_rate': ci_to.format_amount(bid.rate),
'ticker_from': ticker_from,
'ticker_to': ticker_to,

View file

@ -4,6 +4,7 @@
- Track failed and successful swaps by address.
- Added rate lookup helper when creating offer.
- Prevent old shutdown link from shutting down a new session.
0.0.26

View file

@ -497,6 +497,9 @@ class BaseTest(unittest.TestCase):
class Test(BaseTest):
__test__ = True
def notest_00_delay(self):
test_delay_event.wait(100000)
def test_01_part_xmr(self):
logging.info('---------- Test PART to XMR')
swap_clients = self.swap_clients