From 99534756ded93b1f5d3959d0b9d4acd570154b5b Mon Sep 17 00:00:00 2001 From: tecnovert Date: Mon, 22 Nov 2021 22:24:48 +0200 Subject: [PATCH] Variable bid amount and rate. --- basicswap/basicswap.py | 118 ++++++++++++++++++--------- basicswap/http_server.py | 49 +++++++++-- basicswap/templates/index.html | 2 +- basicswap/templates/offer.html | 85 ++++++++++++++++++- basicswap/templates/offer_new_1.html | 7 +- basicswap/ui.py | 2 +- doc/release-notes.md | 1 + tests/basicswap/test_xmr.py | 3 + 8 files changed, 215 insertions(+), 52 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index e65c095..3cf02a8 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -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 diff --git a/basicswap/http_server.py b/basicswap/http_server.py index c1c7e97..0a9e804 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -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) diff --git a/basicswap/templates/index.html b/basicswap/templates/index.html index 1604053..41c7b57 100644 --- a/basicswap/templates/index.html +++ b/basicswap/templates/index.html @@ -23,7 +23,7 @@ Version: {{ version }}

New Offer

-

Shutdown

+

Shutdown

diff --git a/basicswap/templates/offer.html b/basicswap/templates/offer.html index 764ea09..b7aa839 100644 --- a/basicswap/templates/offer.html +++ b/basicswap/templates/offer.html @@ -43,7 +43,7 @@
{% if data.show_bid_form %}

New Bid

-

You will send {{ data.amt_to }} {{ data.tla_to }} and receive {{ data.amt_from }} {{ data.tla_from }} +

You will send {{ data.amt_to }} {{ data.tla_to }} and receive {{ data.amt_from }} {{ 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 @@ {% if data.amount_negotiable == true %} -Amount +Amount {% endif %} {% if data.rate_negotiable == true %} -Rate +Rate {% endif %} Minutes valid - + + + + + {% else %} {% if data.sent == 'True' and data.was_revoked != true %} {% endif %} + {% endif %} + + + + + +

+

+

Bids

@@ -91,8 +104,72 @@

home

diff --git a/basicswap/templates/offer_new_1.html b/basicswap/templates/offer_new_1.html index 490d4b2..11be0d1 100644 --- a/basicswap/templates/offer_new_1.html +++ b/basicswap/templates/offer_new_1.html @@ -57,7 +57,7 @@ xhr_rates.onload = () => { if (xhr_rates.status == 200) { const obj = JSON.parse(xhr_rates.response); - inner_html = '
' + JSON.stringify(obj, null, '  ') + '
'; + inner_html = '

Rates

' + JSON.stringify(obj, null, '  ') + '
'; document.getElementById('rates_display').innerHTML = inner_html; } } @@ -88,6 +88,9 @@ function lookup_rates() { return; } + inner_html = '

Rates

Updating...

'; + 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; } diff --git a/basicswap/ui.py b/basicswap/ui.py index 6f9e36a..0bfcd74 100644 --- a/basicswap/ui.py +++ b/basicswap/ui.py @@ -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, diff --git a/doc/release-notes.md b/doc/release-notes.md index 01380ca..886111a 100644 --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -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 diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index ebe39aa..fd4f0e2 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -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
Bid IDBid AmountBid RateBid StatusIdentity From