# -*- coding: utf-8 -*- # Copyright (c) 2019 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php. import os import json import time import struct import traceback import threading import http.client import urllib.parse from http.server import BaseHTTPRequestHandler, HTTPServer from .util import ( COIN, format8, ) from .chainparams import ( chainparams, Coins, ) from .basicswap import ( SwapTypes, BidStates, TxStates, getOfferState, getBidState, getTxState, getLockName, SEQUENCE_LOCK_TIME, ABS_LOCK_TIME, ) def getCoinName(c): return chainparams[c]['name'].capitalize() def html_content_start(title, h2=None, refresh=None): content = '\n' \ + '' \ + ('' if not refresh else ''.format(refresh)) \ + '' + title + '\n' \ + '' if h2 is not None: content += '

' + h2 + '

' return content class HttpHandler(BaseHTTPRequestHandler): def page_error(self, error_str): content = html_content_start('BasicSwap Error') \ + '

Error: ' + error_str + '

' \ + '

home

' return bytes(content, 'UTF-8') def js_error(self, error_str): error_str_json = json.dumps({'error': error_str}) return bytes(error_str_json, 'UTF-8') def js_wallets(self, url_split): return bytes(json.dumps(self.server.swap_client.getWalletsInfo()), 'UTF-8') def js_offers(self, url_split): assert(False), 'TODO' return bytes(json.dumps(self.server.swap_client.listOffers()), 'UTF-8') def js_sentoffers(self, url_split): assert(False), 'TODO' return bytes(json.dumps(self.server.swap_client.listOffers(sent=True)), 'UTF-8') def js_bids(self, url_split): if len(url_split) > 3: bid_id = bytes.fromhex(url_split[3]) assert(len(bid_id) == 28) return bytes(json.dumps(self.server.swap_client.viewBid(bid_id)), 'UTF-8') return bytes(json.dumps(self.server.swap_client.listBids()), 'UTF-8') def js_sentbids(self, url_split): return bytes(json.dumps(self.server.swap_client.listBids(sent=True)), 'UTF-8') def js_index(self, url_split): return bytes(json.dumps(self.server.swap_client.getSummary()), 'UTF-8') def page_active(self, url_split, post_string): swap_client = self.server.swap_client content = html_content_start(self.server.title, self.server.title) \ + '

Active Swaps

' active_swaps = swap_client.listSwapsInProgress() content += '' content += '' for s in active_swaps: content += ''.format(s[0].hex(), s[1], getBidState(s[2])) content += '
Bid IDOffer IDBid Status
{0}{1}{2}
' content += '

home

' return bytes(content, 'UTF-8') def page_wallets(self, url_split, post_string): swap_client = self.server.swap_client content = html_content_start(self.server.title, self.server.title) \ + '

Wallets

' if post_string != '': form_data = urllib.parse.parse_qs(post_string) form_id = form_data[b'formid'][0].decode('utf-8') if self.server.last_form_id.get('wallets', None) == form_id: content += '

Prevented double submit for form {}.

'.format(form_id) else: self.server.last_form_id['wallets'] = form_id for c in Coins: cid = str(int(c)) if bytes('newaddr_' + cid, 'utf-8') in form_data: swap_client.cacheNewAddressForCoin(c) if bytes('withdraw_' + cid, 'utf-8') in form_data: value = form_data[bytes('amt_' + cid, 'utf-8')][0].decode('utf-8') address = form_data[bytes('to_' + cid, 'utf-8')][0].decode('utf-8') subfee = True if bytes('subfee_' + cid, 'utf-8') in form_data else False txid = swap_client.withdrawCoin(c, value, address, subfee) ticker = swap_client.getTicker(c) content += '

Withdrew {} {} to address {}
In txid: {}

'.format(value, ticker, address, txid) wallets = swap_client.getWalletsInfo() content += '
' for k, w in wallets.items(): cid = str(int(k)) content += '

' + w['name'] + '

' if 'error' in w: content += '

Error: {}

'.format(w['error']) fee_rate = swap_client.getFeeRateForCoin(k) tx_vsize = swap_client.getContractSpendTxVSize(k) est_fee = (fee_rate * tx_vsize) / 1000 content += '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '
Balance:' + w['balance'] + '
Blocks:' + str(w['blocks']) + '
Synced:' + str(w['synced']) + '
' + str(w['deposit_address']) + '
Amount: Address: Subtract fee:
Fee Rate:' + format8(fee_rate * COIN) + 'Est Fee:' + format8(est_fee * COIN) + '
' content += '
' content += '

home

' return bytes(content, 'UTF-8') def make_coin_select(self, name, coins): s = '' return s def page_newoffer(self, url_split, post_string): swap_client = self.server.swap_client content = html_content_start(self.server.title, self.server.title) \ + '

New Offer

' if post_string != '': form_data = urllib.parse.parse_qs(post_string) form_id = form_data[b'formid'][0].decode('utf-8') if self.server.last_form_id.get('newoffer', None) == form_id: content += '

Prevented double submit for form {}.

'.format(form_id) else: self.server.last_form_id['newoffer'] = form_id try: coin_from = Coins(int(form_data[b'coin_from'][0])) except Exception: raise ValueError('Unknown Coin From') try: coin_to = Coins(int(form_data[b'coin_to'][0])) except Exception: raise ValueError('Unknown Coin From') value_from = int(float(form_data[b'amt_from'][0]) * COIN) value_to = int(float(form_data[b'amt_to'][0]) * COIN) min_bid = int(value_from) rate = int((value_to / value_from) * COIN) autoaccept = True if b'autoaccept' in form_data else False lock_seconds = int(form_data[b'lockhrs'][0]) * 60 * 60 # TODO: More accurate rate # assert(value_to == (value_from * rate) // COIN) if swap_client.coin_clients[coin_from]['use_csv'] and swap_client.coin_clients[coin_to]['use_csv']: lock_type = SEQUENCE_LOCK_TIME else: lock_type = ABS_LOCK_TIME offer_id = swap_client.postOffer(coin_from, coin_to, value_from, rate, min_bid, SwapTypes.SELLER_FIRST, auto_accept_bids=autoaccept, lock_type=lock_type, lock_value=lock_seconds) content += '

Sent Offer ' + offer_id.hex() + '
Rate: ' + format8(rate) + '

' coins = [] for k, v in swap_client.coin_clients.items(): if v['connection_type'] == 'rpc': coins.append((int(k), getCoinName(k))) content += '
' content += '' content += '' content += '' content += '' content += '' content += '
Coin From' + self.make_coin_select('coin_from', coins) + 'Amount From
Coin To' + self.make_coin_select('coin_to', coins) + 'Amount To
Contract locked (hrs)Participate txn will be locked for half the time.
Auto Accept Bids
' content += '' content += '
' content += '

home

' return bytes(content, 'UTF-8') def page_offer(self, url_split, post_string): assert(len(url_split) > 2), 'Offer ID not specified' try: offer_id = bytes.fromhex(url_split[2]) assert(len(offer_id) == 28) except Exception: raise ValueError('Bad offer ID') swap_client = self.server.swap_client offer = swap_client.getOffer(offer_id) assert(offer), 'Unknown offer ID' content = html_content_start(self.server.title, self.server.title) \ + '

Offer: ' + offer_id.hex() + '

' if post_string != '': form_data = urllib.parse.parse_qs(post_string) form_id = form_data[b'formid'][0].decode('utf-8') if self.server.last_form_id.get('offer', None) == form_id: content += '

Prevented double submit for form {}.

'.format(form_id) else: self.server.last_form_id['offer'] = form_id bid_id = swap_client.postBid(offer_id, offer.amount_from) content += '

Sent Bid ' + bid_id.hex() + '

' coin_from = Coins(offer.coin_from) coin_to = Coins(offer.coin_to) ticker_from = swap_client.getTicker(coin_from) ticker_to = swap_client.getTicker(coin_to) tr = '{}{}' content += '' content += tr.format('Offer State', getOfferState(offer.state)) content += tr.format('Coin From', getCoinName(coin_from)) content += tr.format('Coin To', getCoinName(coin_to)) content += tr.format('Amount From', format8(offer.amount_from) + ' ' + ticker_from) content += tr.format('Amount To', format8((offer.amount_from * offer.rate) // COIN) + ' ' + ticker_to) content += tr.format('Rate', format8(offer.rate) + ' ' + ticker_from + '/' + ticker_to) content += tr.format('Script Lock Type', getLockName(offer.lock_type)) content += tr.format('Script Lock Value', offer.lock_value) content += tr.format('Address From', offer.addr_from) content += tr.format('Created At', time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(offer.created_at))) content += tr.format('Expired At', time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(offer.expire_at))) content += tr.format('Sent', 'True' if offer.was_sent else 'False') if offer.was_sent: content += tr.format('Auto Accept Bids', 'True' if offer.auto_accept_bids else 'False') content += '
' bids = swap_client.listBids(offer_id=offer_id) content += '

Bids

' content += '' for b in bids: content += ''.format(b.bid_id.hex(), format8(b.amount), getBidState(b.state), getTxState(b.initiate_txn_state), getTxState(b.participate_txn_state)) content += '
Bid IDBid AmountBid StatusITX StatusPTX Status
{0}{1}{2}{3}{4}
' content += '
' content += '' content += '
' content += '

home

' return bytes(content, 'UTF-8') def page_offers(self, url_split, sent=False): swap_client = self.server.swap_client offers = swap_client.listOffers(sent) content = html_content_start(self.server.title, self.server.title) \ + '

' + ('Sent ' if sent else '') + 'Offers

' content += '' content += '' for o in offers: coin_from_name = getCoinName(Coins(o.coin_from)) coin_to_name = getCoinName(Coins(o.coin_to)) amount_to = (o.amount_from * o.rate) // COIN content += ''.format( time.strftime('%Y-%m-%d %H:%M', time.localtime(o.created_at)), o.offer_id.hex(), coin_from_name, coin_to_name, format8(o.amount_from), format8(amount_to), format8(o.rate)) content += '
AtOffer IDCoin FromCoin ToAmount FromAmount ToRate
{0}{1}{2}{3}{4}{5}{6}
' content += '

home

' return bytes(content, 'UTF-8') def page_advance(self, url_split, post_string): assert(len(url_split) > 2), 'Bid ID not specified' try: bid_id = bytes.fromhex(url_split[2]) assert(len(bid_id) == 28) except Exception: raise ValueError('Bad bid ID') swap_client = self.server.swap_client content = html_content_start(self.server.title, self.server.title) \ + '

Advance: ' + bid_id.hex() + '

' def page_bid(self, url_split, post_string): assert(len(url_split) > 2), 'Bid ID not specified' try: bid_id = bytes.fromhex(url_split[2]) assert(len(bid_id) == 28) except Exception: raise ValueError('Bad bid ID') swap_client = self.server.swap_client content = html_content_start(self.server.title, self.server.title, 30) \ + '

Bid: ' + bid_id.hex() + '

' \ + '

Page Refresh: 30 seconds

' show_txns = False if post_string != '': form_data = urllib.parse.parse_qs(post_string) form_id = form_data[b'formid'][0].decode('utf-8') if self.server.last_form_id.get('bid', None) == form_id: content += '

Prevented double submit for form {}.

'.format(form_id) else: self.server.last_form_id['bid'] = form_id if b'abandon_bid' in form_data: try: swap_client.abandonBid(bid_id) content += '

Bid abandoned

' except Exception as e: content += '

Error' + str(e) + '

' if b'accept_bid' in form_data: try: swap_client.acceptBid(bid_id) content += '

Bid accepted

' except Exception as e: content += '

Error' + str(e) + '

' if b'show_txns' in form_data: show_txns = True bid, offer = swap_client.getBidAndOffer(bid_id) assert(bid), 'Unknown bid ID' coin_from = Coins(offer.coin_from) coin_to = Coins(offer.coin_to) ticker_from = swap_client.getTicker(coin_from) ticker_to = swap_client.getTicker(coin_to) if bid.state == BidStates.BID_SENT: state_description = 'Waiting for seller to accept.' elif bid.state == BidStates.BID_RECEIVED: state_description = 'Waiting for seller to accept.' elif bid.state == BidStates.BID_ACCEPTED: if not bid.initiate_txid: state_description = 'Waiting for seller to send initiate tx.' else: state_description = 'Waiting for initiate tx to confirm.' elif bid.state == BidStates.SWAP_INITIATED: state_description = 'Waiting for participate txn to be confirmed in {} chain'.format(ticker_to) elif bid.state == BidStates.SWAP_PARTICIPATING: state_description = 'Waiting for initiate txn to be spent in {} chain'.format(ticker_from) elif bid.state == BidStates.SWAP_COMPLETED: state_description = 'Swap completed' if bid.initiate_txn_state == TxStates.TX_REDEEMED and bid.participate_txn_state == TxStates.TX_REDEEMED: state_description += ' successfully' else: state_description += ', ITX ' + getTxState(bid.initiate_txn_state + ', PTX ' + getTxState(bid.participate_txn_state)) elif bid.state == BidStates.SWAP_TIMEDOUT: state_description = 'Timed out waiting for initiate txn' elif bid.state == BidStates.BID_ABANDONED: state_description = 'Bid abandoned' elif bid.state == BidStates.BID_ERROR: state_description = bid.state_note else: state_description = '' tr = '{}{}' content += '' content += tr.format('Swap', format8(bid.amount) + ' ' + ticker_from + ' for ' + format8((bid.amount * offer.rate) // COIN) + ' ' + ticker_to) content += tr.format('Bid State', getBidState(bid.state)) content += tr.format('State Description', state_description) content += tr.format('ITX State', getTxState(bid.initiate_txn_state)) content += tr.format('PTX State', getTxState(bid.participate_txn_state)) content += tr.format('Offer', '' + bid.offer_id.hex() + '') content += tr.format('Address From', bid.bid_addr) content += tr.format('Proof of Funds', bid.proof_address) content += tr.format('Created At', time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(bid.created_at))) content += tr.format('Expired At', time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(bid.expire_at))) content += tr.format('Sent', 'True' if bid.was_sent else 'False') content += tr.format('Received', 'True' if bid.was_received else 'False') content += tr.format('Initiate Tx', 'None' if not bid.initiate_txid else (bid.initiate_txid.hex() + ' ' + ticker_from)) content += tr.format('Initiate Conf', 'None' if not bid.initiate_txn_conf else bid.initiate_txn_conf) content += tr.format('Participate Tx', 'None' if not bid.participate_txid else (bid.participate_txid.hex() + ' ' + ticker_to)) content += tr.format('Participate Conf', 'None' if not bid.participate_txn_conf else bid.participate_txn_conf) if show_txns: content += tr.format('Initiate Tx Refund', 'None' if not bid.initiate_txn_refund else bid.initiate_txn_refund.hex()) content += tr.format('Participate Tx Refund', 'None' if not bid.participate_txn_refund else bid.participate_txn_refund.hex()) content += tr.format('Initiate Spend Tx', 'None' if not bid.initiate_spend_txid else (bid.initiate_spend_txid.hex() + ' {}'.format(bid.initiate_spend_n))) content += tr.format('Participate Spend Tx', 'None' if not bid.participate_spend_txid else (bid.participate_spend_txid.hex() + ' {}'.format(bid.participate_spend_n))) content += '
' content += '
' if bid.was_received: content += '
' content += '' content += '' content += '
' content += '

Old States

' num_states = len(bid.states) // 12 for i in range(num_states): up = struct.unpack_from('' + ('Sent ' if sent else '') + 'Bids' content += '
StateSet At
' content += '' for b in bids: content += ''.format( time.strftime('%Y-%m-%d %H:%M', time.localtime(b.created_at)), b.bid_id.hex(), b.offer_id.hex(), getBidState(b.state), getTxState(b.initiate_txn_state), getTxState(b.participate_txn_state)) content += '
AtBid IDOffer IDBid StatusITX StatusPTX Status
{0}{1}{2}{3}{4}{5}
' content += '

home

' return bytes(content, 'UTF-8') def page_watched(self, url_split, post_string): swap_client = self.server.swap_client watched_outputs, last_scanned = swap_client.listWatchedOutputs() content = html_content_start(self.server.title, self.server.title) \ + '

Watched Outputs

' for c in last_scanned: content += '

' + getCoinName(c[0]) + ' Scanned Height: ' + str(c[1]) + '

' content += '' content += '' for o in watched_outputs: content += ''.format(o[1].hex(), getCoinName(o[0]), o[2], o[3], int(o[4])) content += '
Bid IDChainTxidIndexType
{0}{1}{2}{3}{4}
' content += '

home

' return bytes(content, 'UTF-8') def page_index(self, url_split): swap_client = self.server.swap_client summary = swap_client.getSummary() content = html_content_start(self.server.title, self.server.title, 30) \ + '

View Wallets

' \ + '

' \ + 'Page Refresh: 30 seconds
' \ + 'Network: ' + str(summary['network']) + '
' \ + 'Swaps in progress: ' + str(summary['num_swapping']) + '
' \ + 'Network Offers: ' + str(summary['num_network_offers']) + '
' \ + 'Sent Offers: ' + str(summary['num_sent_offers']) + '
' \ + 'Received Bids: ' + str(summary['num_recv_bids']) + '
' \ + 'Sent Bids: ' + str(summary['num_sent_bids']) + '
' \ + 'Watched Outputs: ' + str(summary['num_watched_outputs']) + '
' \ + '

' \ + '

' \ + 'New Offer
' \ + '

' content += '' return bytes(content, 'UTF-8') def putHeaders(self, status_code, content_type): self.send_response(status_code) if self.server.allow_cors: self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Content-type', content_type) self.end_headers() def handle_http(self, status_code, path, post_string=''): url_split = self.path.split('/') if len(url_split) > 1 and url_split[1] == 'json': try: self.putHeaders(status_code, 'text/plain') func = self.js_index if len(url_split) > 2: func = {'wallets': self.js_wallets, 'offers': self.js_offers, 'sentoffers': self.js_sentoffers, 'bids': self.js_bids, 'sentbids': self.js_sentbids, }.get(url_split[2], self.js_index) return func(url_split) except Exception as e: return self.js_error(str(e)) try: self.putHeaders(status_code, 'text/html') if len(url_split) > 1: if url_split[1] == 'active': return self.page_active(url_split, post_string) if url_split[1] == 'wallets': return self.page_wallets(url_split, post_string) if url_split[1] == 'offer': return self.page_offer(url_split, post_string) if url_split[1] == 'offers': return self.page_offers(url_split) if url_split[1] == 'newoffer': return self.page_newoffer(url_split, post_string) if url_split[1] == 'sentoffers': return self.page_offers(url_split, sent=True) if url_split[1] == 'advance': return self.page_advance(url_split, post_string) if url_split[1] == 'bid': return self.page_bid(url_split, post_string) if url_split[1] == 'bids': return self.page_bids(url_split, post_string) if url_split[1] == 'sentbids': return self.page_bids(url_split, post_string, sent=True) if url_split[1] == 'watched': return self.page_watched(url_split, post_string) return self.page_index(url_split) except Exception as e: traceback.print_exc() # TODO: Remove return self.page_error(str(e)) 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['Content-Length'])) response = self.handle_http(200, self.path, post_string) self.wfile.write(response) def do_HEAD(self): self.putHeaders(200, 'text/html') def do_OPTIONS(self): self.send_response(200, 'ok') if self.server.allow_cors: self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Access-Control-Allow-Headers', '*') self.end_headers() class HttpThread(threading.Thread, HTTPServer): def __init__(self, fp, host_name, port_no, allow_cors, swap_client): threading.Thread.__init__(self) self.stop_event = threading.Event() self.fp = fp self.host_name = host_name self.port_no = port_no self.allow_cors = allow_cors self.swap_client = swap_client self.title = 'Simple Atomic Swap Demo, ' + self.swap_client.chain self.last_form_id = dict() self.timeout = 60 HTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler) def stop(self): self.stop_event.set() # Send fake request conn = http.client.HTTPConnection(self.host_name, self.port_no) conn.connect() conn.request('GET', '/none') response = conn.getresponse() data = response.read() conn.close() def stopped(self): return self.stop_event.is_set() def serve_forever(self): while not self.stopped(): self.handle_request() self.socket.close() def run(self): self.serve_forever()