Add to Github

This commit is contained in:
tecnovert
2019-07-17 17:12:06 +02:00
commit e242f50b2b
26 changed files with 5035 additions and 0 deletions

3
basicswap/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
name = "basicswap"
__version__ = "0.0.1"

2151
basicswap/basicswap.py Normal file

File diff suppressed because it is too large Load Diff

120
basicswap/chainparams.py Normal file
View File

@@ -0,0 +1,120 @@
# -*- 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.
from enum import IntEnum
from .util import (
COIN,
)
class Coins(IntEnum):
PART = 1
BTC = 2
LTC = 3
# DCR = 4
chainparams = {
Coins.PART: {
'name': 'particl',
'ticker': 'PART',
'message_magic': 'Bitcoin Signed Message:\n',
'mainnet': {
'rpcport': 51735,
'pubkey_address': 0x38,
'script_address': 0x3c,
'key_prefix': 0x6c,
'hrp': 'pw',
'bip44': 44,
'min_amount': 1000,
'max_amount': 100000 * COIN,
},
'testnet': {
'rpcport': 51935,
'pubkey_address': 0x76,
'script_address': 0x7a,
'key_prefix': 0x2e,
'hrp': 'tpw',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
},
'regtest': {
'rpcport': 51936,
'pubkey_address': 0x76,
'script_address': 0x7a,
'key_prefix': 0x2e,
'hrp': 'rtpw',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
Coins.BTC: {
'name': 'bitcoin',
'ticker': 'BTC',
'message_magic': 'Bitcoin Signed Message:\n',
'mainnet': {
'rpcport': 8332,
'pubkey_address': 0,
'script_address': 5,
'hrp': 'bc',
'bip44': 0,
'min_amount': 1000,
'max_amount': 100000 * COIN,
},
'testnet': {
'rpcport': 18332,
'pubkey_address': 111,
'script_address': 196,
'hrp': 'tb',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
},
'regtest': {
'rpcport': 18443,
'pubkey_address': 111,
'script_address': 196,
'hrp': 'bcrt',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
Coins.LTC: {
'name': 'litecoin',
'ticker': 'LTC',
'message_magic': 'Litecoin Signed Message:\n',
'mainnet': {
'rpcport': 9332,
'pubkey_address': 48,
'script_address': 50,
'hrp': 'ltc',
'bip44': 2,
'min_amount': 1000,
'max_amount': 100000 * COIN,
},
'testnet': {
'rpcport': 19332,
'pubkey_address': 111,
'script_address': 58,
'hrp': 'tltc',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
},
'regtest': {
'rpcport': 19443,
'pubkey_address': 111,
'script_address': 58,
'hrp': 'rltc',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
}
}

24
basicswap/config.py Normal file
View File

@@ -0,0 +1,24 @@
# -*- 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
DATADIRS = os.path.expanduser(os.getenv('DATADIRS', '/tmp/basicswap'))
PARTICL_BINDIR = os.path.expanduser(os.getenv('PARTICL_BINDIR', ''))
PARTICLD = os.getenv('PARTICLD', 'particld')
PARTICL_CLI = os.getenv('PARTICL_CLI', 'particl-cli')
PARTICL_TX = os.getenv('PARTICL_TX', 'particl-tx')
BITCOIN_BINDIR = os.path.expanduser(os.getenv('BITCOIN_BINDIR', ''))
BITCOIND = os.getenv('BITCOIND', 'bitcoind')
BITCOIN_CLI = os.getenv('BITCOIN_CLI', 'bitcoin-cli')
BITCOIN_TX = os.getenv('BITCOIN_TX', 'bitcoin-tx')
LITECOIN_BINDIR = os.path.expanduser(os.getenv('LITECOIN_BINDIR', ''))
LITECOIND = os.getenv('LITECOIND', 'litecoind')
LITECOIN_CLI = os.getenv('LITECOIN_CLI', 'litecoin-cli')
LITECOIN_TX = os.getenv('LITECOIN_TX', 'litecoin-tx')

542
basicswap/http_server.py Normal file
View File

@@ -0,0 +1,542 @@
# -*- 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,
getOfferState,
getBidState,
getTxState,
getLockName,
)
def getCoinName(c):
return chainparams[c]['name'].capitalize()
def html_content_start(title, h2=None):
content = '<!DOCTYPE html><html lang="en">\n<head>' \
+ '<meta charset="UTF-8">' \
+ '<title>' + title + '</title></head>' \
+ '<body>'
if h2 is not None:
content += '<h2>' + h2 + '</h2>'
return content
class HttpHandler(BaseHTTPRequestHandler):
def page_error(self, error_str):
content = html_content_start('BasicSwap Error') \
+ '<p>Error: ' + error_str + '</p>' \
+ '<p><a href=\'/\'>home</a></p></body></html>'
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) \
+ '<h3>Active Swaps</h3>'
active_swaps = swap_client.listSwapsInProgress()
content += '<table>'
content += '<tr><th>Bid ID</th><th>Offer ID</th><th>Bid Status</th></tr>'
for s in active_swaps:
content += '<tr><td><a href=/bid/{0}>{0}</a></td><td><a href=/offer/{1}>{1}</a></td><td>{2}</td></tr>'.format(s[0].hex(), s[1], getBidState(s[2]))
content += '</table>'
content += '<p><a href="/">home</a></p></body></html>'
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) \
+ '<h3>Wallets</h3>'
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 += '<p>Prevented double submit for form {}.</p>'.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')
txid = swap_client.withdrawCoin(c, value, address)
ticker = swap_client.getTicker(c)
content += '<p>Withdrew {} {} to address {}<br/>In txid: {}</p>'.format(value, ticker, address, txid)
wallets = swap_client.getWalletsInfo()
content += '<form method="post">'
for k, w in wallets.items():
cid = str(int(k))
content += '<h4>' + w['name'] + '</h4>' \
+ '<table>' \
+ '<tr><td>Balance:</td><td>' + str(w['balance']) + '</td></tr>' \
+ '<tr><td>Blocks:</td><td>' + str(w['blocks']) + '</td></tr>' \
+ '<tr><td>Synced:</td><td>' + str(w['synced']) + '</td></tr>' \
+ '<tr><td><input type="submit" name="newaddr_' + cid + '" value="Deposit Address"></td><td>' + str(w['deposit_address']) + '</td></tr>' \
+ '<tr><td><input type="submit" name="withdraw_' + cid + '" value="Withdraw"></td><td>Amount: <input type="text" name="amt_' + cid + '"></td><td>Address: <input type="text" name="to_' + cid + '"></td></tr>' \
+ '</table>'
content += '<input type="hidden" name="formid" value="' + os.urandom(8).hex() + '"></form>'
content += '<p><a href="/">home</a></p></body></html>'
return bytes(content, 'UTF-8')
def make_coin_select(self, name, coins):
s = '<select name="' + name + '"><option value="-1">-- Select Coin --</option>'
for c in coins:
s += '<option value="{}">{}</option>'.format(*c)
s += '</select>'
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) \
+ '<h3>New Offer</h3>'
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 += '<p>Prevented double submit for form {}.</p>'.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
# TODO: More accurate rate
# assert(value_to == (value_from * rate) // COIN)
offer_id = swap_client.postOffer(coin_from, coin_to, value_from, rate, min_bid, SwapTypes.SELLER_FIRST, auto_accept_bids=autoaccept)
content += '<p><a href="/offer/' + offer_id.hex() + '">Sent Offer ' + offer_id.hex() + '</a><br/>Rate: ' + format8(rate) + '</p>'
coins = []
for k, v in swap_client.coin_clients.items():
if v['connection_type'] == 'rpc':
coins.append((int(k), getCoinName(k)))
content += '<form method="post">'
content += '<table>'
content += '<tr><td>Coin From</td><td>' + self.make_coin_select('coin_from', coins) + '</td><td>Amount From</td><td><input type="text" name="amt_from"></td></tr>'
content += '<tr><td>Coin To</td><td>' + self.make_coin_select('coin_to', coins) + '</td><td>Amount To</td><td><input type="text" name="amt_to"></td></tr>'
content += '<tr><td>Auto Accept Bids</td><td><input type="checkbox" name="autoaccept" value="aa" checked></td></tr>'
content += '</table>'
content += '<input type="submit" value="Submit">'
content += '<input type="hidden" name="formid" value="' + os.urandom(8).hex() + '"></form>'
content += '<p><a href="/">home</a></p></body></html>'
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) \
+ '<h3>Offer: ' + offer_id.hex() + '</h3>'
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 += '<p>Prevented double submit for form {}.</p>'.format(form_id)
else:
self.server.last_form_id['offer'] = form_id
bid_id = swap_client.postBid(offer_id, offer.amount_from)
content += '<p><a href="/bid/' + bid_id.hex() + '">Sent Bid ' + bid_id.hex() + '</a></p>'
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 = '<tr><td>{}</td><td>{}</td></tr>'
content += '<table>'
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 += '</table>'
bids = swap_client.listBids(offer_id=offer_id)
content += '<h4>Bids</h4><table>'
content += '<tr><th>Bid ID</th><th>Bid Amount</th><th>Bid Status</th><th>ITX Status</th><th>PTX Status</th></tr>'
for b in bids:
content += '<tr><td><a href=/bid/{0}>{0}</a></td><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td></tr>'.format(b.bid_id.hex(), format8(b.amount), getBidState(b.state), getTxState(b.initiate_txn_state), getTxState(b.participate_txn_state))
content += '</table>'
content += '<form method="post">'
content += '<input type="submit" value="Send Bid">'
content += '<input type="hidden" name="formid" value="' + os.urandom(8).hex() + '"></form>'
content += '<p><a href="/">home</a></p></body></html>'
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) \
+ '<h3>' + ('Sent ' if sent else '') + 'Offers</h3>'
content += '<table>'
content += '<tr><th>Offer ID</th><th>Coin From</th><th>Coin To</th><th>Amount From</th><th>Amount To</th><th>Rate</th></tr>'
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 += '<tr><td><a href=/offer/{0}>{0}</a></td><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td><td>{5}</td></tr>'.format(o.offer_id.hex(), coin_from_name, coin_to_name, format8(o.amount_from), format8(amount_to), format8(o.rate))
content += '</table>'
content += '<p><a href="/">home</a></p></body></html>'
return bytes(content, 'UTF-8')
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) \
+ '<h3>Bid: ' + bid_id.hex() + '</h3>'
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 += '<p>Prevented double submit for form {}.</p>'.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 += '<p>Bid abandoned</p>'
except Exception as e:
content += '<p>Error' + str(e) + '</p>'
if b'accept_bid' in form_data:
try:
swap_client.acceptBid(bid_id)
content += '<p>Bid accepted</p>'
except Exception as e:
content += '<p>Error' + str(e) + '</p>'
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)
tr = '<tr><td>{}</td><td>{}</td></tr>'
content += '<table>'
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('ITX State', getTxState(bid.initiate_txn_state))
content += tr.format('PTX State', getTxState(bid.participate_txn_state))
content += tr.format('Offer', '<a href="/offer/' + bid.offer_id.hex() + '">' + bid.offer_id.hex() + '</a>')
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())
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())
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 += '</table>'
content += '<form method="post">'
if bid.was_received:
content += '<input name="accept_bid" type="submit" value="Accept Bid"><br/>'
content += '<input name="abandon_bid" type="submit" value="Abandon Bid">'
content += '<input name="show_txns" type="submit" value="Show More Info">'
content += '<input type="hidden" name="formid" value="' + os.urandom(8).hex() + '"></form>'
content += '<h4>Old States</h4><table><tr><th>State</th><th>Set At</th></tr>'
num_states = len(bid.states) // 12
for i in range(num_states):
up = struct.unpack_from('<iq', bid.states[i * 12:(i + 1) * 12])
content += '<tr><td>Bid {}</td><td>{}</td></tr>'.format(getBidState(up[0]), time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(up[1])))
if bid.initiate_txn_states is not None:
num_states = len(bid.initiate_txn_states) // 12
for i in range(num_states):
up = struct.unpack_from('<iq', bid.initiate_txn_states[i * 12:(i + 1) * 12])
content += '<tr><td>ITX {}</td><td>{}</td></tr>'.format(getTxState(up[0]), time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(up[1])))
if bid.participate_txn_states is not None:
num_states = len(bid.participate_txn_states) // 12
for i in range(num_states):
up = struct.unpack_from('<iq', bid.participate_txn_states[i * 12:(i + 1) * 12])
content += '<tr><td>PTX {}</td><td>{}</td></tr>'.format(getTxState(up[0]), time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(up[1])))
content += '</table>'
content += '<p><a href="/">home</a></p></body></html>'
return bytes(content, 'UTF-8')
def page_bids(self, url_split, post_string, sent=False):
swap_client = self.server.swap_client
bids = swap_client.listBids(sent=sent)
content = html_content_start(self.server.title, self.server.title) \
+ '<h3>' + ('Sent ' if sent else '') + 'Bids</h3>'
content += '<table>'
content += '<tr><th>Bid ID</th><th>Offer ID</th><th>Bid Status</th><th>ITX Status</th><th>PTX Status</th></tr>'
for b in bids:
content += '<tr><td><a href=/bid/{0}>{0}</a></td><td><a href=/offer/{1}>{1}</a></td><td>{2}</td><td>{3}</td><td>{4}</td></tr>'.format(b.bid_id.hex(), b.offer_id.hex(), getBidState(b.state), getTxState(b.initiate_txn_state), getTxState(b.participate_txn_state))
content += '</table>'
content += '<p><a href="/">home</a></p></body></html>'
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) \
+ '<h3>Watched Outputs</h3>'
for c in last_scanned:
content += '<p>' + getCoinName(c[0]) + ' Scanned Height: ' + str(c[1]) + '</p>'
content += '<table>'
content += '<tr><th>Bid ID</th><th>Chain</th><th>Txid</th><th>Index</th><th>Type</th></tr>'
for o in watched_outputs:
content += '<tr><td><a href=/bid/{0}>{0}</a></td><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td></tr>'.format(o[1].hex(), getCoinName(o[0]), o[2], o[3], int(o[4]))
content += '</table>'
content += '<p><a href="/">home</a></p></body></html>'
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) \
+ '<p><a href="/wallets">View Wallets</a></p>' \
+ '<p>' \
+ 'Network: ' + str(summary['network']) + '<br/>' \
+ '<a href="/active">Swaps in progress: ' + str(summary['num_swapping']) + '</a><br/>' \
+ '<a href="/offers">Network Offers: ' + str(summary['num_network_offers']) + '</a><br/>' \
+ '<a href="/sentoffers">Sent Offers: ' + str(summary['num_sent_offers']) + '</a><br/>' \
+ '<a href="/bids">Received Bids: ' + str(summary['num_recv_bids']) + '</a><br/>' \
+ '<a href="/sentbids">Sent Bids: ' + str(summary['num_sent_bids']) + '</a><br/>' \
+ '<a href="/watched">Watched Outputs: ' + str(summary['num_watched_outputs']) + '</a><br/>' \
+ '</p>' \
+ '<p>' \
+ '<a href="/newoffer">New Offer</a><br/>' \
+ '</p>'
content += '</body></html>'
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] == '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.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()

386
basicswap/key.py Normal file
View File

@@ -0,0 +1,386 @@
# Copyright (c) 2019 Pieter Wuille
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test-only secp256k1 elliptic curve implementation
WARNING: This code is slow, uses bad randomness, does not properly protect
keys, and is trivially vulnerable to side channel attacks. Do not use for
anything but tests."""
import random
def modinv(a, n):
"""Compute the modular inverse of a modulo n
See https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Modular_integers.
"""
t1, t2 = 0, 1
r1, r2 = n, a
while r2 != 0:
q = r1 // r2
t1, t2 = t2, t1 - q * t2
r1, r2 = r2, r1 - q * r2
if r1 > 1:
return None
if t1 < 0:
t1 += n
return t1
def jacobi_symbol(n, k):
"""Compute the Jacobi symbol of n modulo k
See http://en.wikipedia.org/wiki/Jacobi_symbol
For our application k is always prime, so this is the same as the Legendre symbol."""
assert k > 0 and k & 1, "jacobi symbol is only defined for positive odd k"
n %= k
t = 0
while n != 0:
while n & 1 == 0:
n >>= 1
r = k & 7
t ^= (r == 3 or r == 5)
n, k = k, n
t ^= (n & k & 3 == 3)
n = n % k
if k == 1:
return -1 if t else 1
return 0
def modsqrt(a, p):
"""Compute the square root of a modulo p when p % 4 = 3.
The Tonelli-Shanks algorithm can be used. See https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm
Limiting this function to only work for p % 4 = 3 means we don't need to
iterate through the loop. The highest n such that p - 1 = 2^n Q with Q odd
is n = 1. Therefore Q = (p-1)/2 and sqrt = a^((Q+1)/2) = a^((p+1)/4)
secp256k1's is defined over field of size 2**256 - 2**32 - 977, which is 3 mod 4.
"""
if p % 4 != 3:
raise NotImplementedError("modsqrt only implemented for p % 4 = 3")
sqrt = pow(a, (p + 1)//4, p)
if pow(sqrt, 2, p) == a % p:
return sqrt
return None
class EllipticCurve:
def __init__(self, p, a, b):
"""Initialize elliptic curve y^2 = x^3 + a*x + b over GF(p)."""
self.p = p
self.a = a % p
self.b = b % p
def affine(self, p1):
"""Convert a Jacobian point tuple p1 to affine form, or None if at infinity.
An affine point is represented as the Jacobian (x, y, 1)"""
x1, y1, z1 = p1
if z1 == 0:
return None
inv = modinv(z1, self.p)
inv_2 = (inv**2) % self.p
inv_3 = (inv_2 * inv) % self.p
return ((inv_2 * x1) % self.p, (inv_3 * y1) % self.p, 1)
def negate(self, p1):
"""Negate a Jacobian point tuple p1."""
x1, y1, z1 = p1
return (x1, (self.p - y1) % self.p, z1)
def on_curve(self, p1):
"""Determine whether a Jacobian tuple p is on the curve (and not infinity)"""
x1, y1, z1 = p1
z2 = pow(z1, 2, self.p)
z4 = pow(z2, 2, self.p)
return z1 != 0 and (pow(x1, 3, self.p) + self.a * x1 * z4 + self.b * z2 * z4 - pow(y1, 2, self.p)) % self.p == 0
def is_x_coord(self, x):
"""Test whether x is a valid X coordinate on the curve."""
x_3 = pow(x, 3, self.p)
return jacobi_symbol(x_3 + self.a * x + self.b, self.p) != -1
def lift_x(self, x):
"""Given an X coordinate on the curve, return a corresponding affine point."""
x_3 = pow(x, 3, self.p)
v = x_3 + self.a * x + self.b
y = modsqrt(v, self.p)
if y is None:
return None
return (x, y, 1)
def double(self, p1):
"""Double a Jacobian tuple p1
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Doubling"""
x1, y1, z1 = p1
if z1 == 0:
return (0, 1, 0)
y1_2 = (y1**2) % self.p
y1_4 = (y1_2**2) % self.p
x1_2 = (x1**2) % self.p
s = (4*x1*y1_2) % self.p
m = 3*x1_2
if self.a:
m += self.a * pow(z1, 4, self.p)
m = m % self.p
x2 = (m**2 - 2*s) % self.p
y2 = (m*(s - x2) - 8*y1_4) % self.p
z2 = (2*y1*z1) % self.p
return (x2, y2, z2)
def add_mixed(self, p1, p2):
"""Add a Jacobian tuple p1 and an affine tuple p2
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition (with affine point)"""
x1, y1, z1 = p1
x2, y2, z2 = p2
assert(z2 == 1)
# Adding to the point at infinity is a no-op
if z1 == 0:
return p2
z1_2 = (z1**2) % self.p
z1_3 = (z1_2 * z1) % self.p
u2 = (x2 * z1_2) % self.p
s2 = (y2 * z1_3) % self.p
if x1 == u2:
if (y1 != s2):
# p1 and p2 are inverses. Return the point at infinity.
return (0, 1, 0)
# p1 == p2. The formulas below fail when the two points are equal.
return self.double(p1)
h = u2 - x1
r = s2 - y1
h_2 = (h**2) % self.p
h_3 = (h_2 * h) % self.p
u1_h_2 = (x1 * h_2) % self.p
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
y3 = (r*(u1_h_2 - x3) - y1*h_3) % self.p
z3 = (h*z1) % self.p
return (x3, y3, z3)
def add(self, p1, p2):
"""Add two Jacobian tuples p1 and p2
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition"""
x1, y1, z1 = p1
x2, y2, z2 = p2
# Adding the point at infinity is a no-op
if z1 == 0:
return p2
if z2 == 0:
return p1
# Adding an Affine to a Jacobian is more efficient since we save field multiplications and squarings when z = 1
if z1 == 1:
return self.add_mixed(p2, p1)
if z2 == 1:
return self.add_mixed(p1, p2)
z1_2 = (z1**2) % self.p
z1_3 = (z1_2 * z1) % self.p
z2_2 = (z2**2) % self.p
z2_3 = (z2_2 * z2) % self.p
u1 = (x1 * z2_2) % self.p
u2 = (x2 * z1_2) % self.p
s1 = (y1 * z2_3) % self.p
s2 = (y2 * z1_3) % self.p
if u1 == u2:
if (s1 != s2):
# p1 and p2 are inverses. Return the point at infinity.
return (0, 1, 0)
# p1 == p2. The formulas below fail when the two points are equal.
return self.double(p1)
h = u2 - u1
r = s2 - s1
h_2 = (h**2) % self.p
h_3 = (h_2 * h) % self.p
u1_h_2 = (u1 * h_2) % self.p
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
y3 = (r*(u1_h_2 - x3) - s1*h_3) % self.p
z3 = (h*z1*z2) % self.p
return (x3, y3, z3)
def mul(self, ps):
"""Compute a (multi) point multiplication
ps is a list of (Jacobian tuple, scalar) pairs.
"""
r = (0, 1, 0)
for i in range(255, -1, -1):
r = self.double(r)
for (p, n) in ps:
if ((n >> i) & 1):
r = self.add(r, p)
return r
SECP256K1 = EllipticCurve(2**256 - 2**32 - 977, 0, 7)
SECP256K1_G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, 1)
SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
SECP256K1_ORDER_HALF = SECP256K1_ORDER // 2
class ECPubKey():
"""A secp256k1 public key"""
def __init__(self):
"""Construct an uninitialized public key"""
self.valid = False
def set(self, data):
"""Construct a public key from a serialization in compressed or uncompressed format"""
if (len(data) == 65 and data[0] == 0x04):
p = (int.from_bytes(data[1:33], 'big'), int.from_bytes(data[33:65], 'big'), 1)
self.valid = SECP256K1.on_curve(p)
if self.valid:
self.p = p
self.compressed = False
elif (len(data) == 33 and (data[0] == 0x02 or data[0] == 0x03)):
x = int.from_bytes(data[1:33], 'big')
if SECP256K1.is_x_coord(x):
p = SECP256K1.lift_x(x)
# if the oddness of the y co-ord isn't correct, find the other
# valid y
if (p[1] & 1) != (data[0] & 1):
p = SECP256K1.negate(p)
self.p = p
self.valid = True
self.compressed = True
else:
self.valid = False
else:
self.valid = False
@property
def is_compressed(self):
return self.compressed
@property
def is_valid(self):
return self.valid
def get_bytes(self):
assert(self.valid)
p = SECP256K1.affine(self.p)
if p is None:
return None
if self.compressed:
return bytes([0x02 + (p[1] & 1)]) + p[0].to_bytes(32, 'big')
else:
return bytes([0x04]) + p[0].to_bytes(32, 'big') + p[1].to_bytes(32, 'big')
def verify_ecdsa(self, sig, msg, low_s=True):
"""Verify a strictly DER-encoded ECDSA signature against this pubkey.
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
ECDSA verifier algorithm"""
assert(self.valid)
# Extract r and s from the DER formatted signature. Return false for
# any DER encoding errors.
if (sig[1] + 2 != len(sig)):
return False
if (len(sig) < 4):
return False
if (sig[0] != 0x30):
return False
if (sig[2] != 0x02):
return False
rlen = sig[3]
if (len(sig) < 6 + rlen):
return False
if rlen < 1 or rlen > 33:
return False
if sig[4] >= 0x80:
return False
if (rlen > 1 and (sig[4] == 0) and not (sig[5] & 0x80)):
return False
r = int.from_bytes(sig[4:4+rlen], 'big')
if (sig[4+rlen] != 0x02):
return False
slen = sig[5+rlen]
if slen < 1 or slen > 33:
return False
if (len(sig) != 6 + rlen + slen):
return False
if sig[6+rlen] >= 0x80:
return False
if (slen > 1 and (sig[6+rlen] == 0) and not (sig[7+rlen] & 0x80)):
return False
s = int.from_bytes(sig[6+rlen:6+rlen+slen], 'big')
# Verify that r and s are within the group order
if r < 1 or s < 1 or r >= SECP256K1_ORDER or s >= SECP256K1_ORDER:
return False
if low_s and s >= SECP256K1_ORDER_HALF:
return False
z = int.from_bytes(msg, 'big')
# Run verifier algorithm on r, s
w = modinv(s, SECP256K1_ORDER)
u1 = z*w % SECP256K1_ORDER
u2 = r*w % SECP256K1_ORDER
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, u1), (self.p, u2)]))
if R is None or R[0] != r:
return False
return True
class ECKey():
"""A secp256k1 private key"""
def __init__(self):
self.valid = False
def set(self, secret, compressed):
"""Construct a private key object with given 32-byte secret and compressed flag."""
assert(len(secret) == 32)
secret = int.from_bytes(secret, 'big')
self.valid = (secret > 0 and secret < SECP256K1_ORDER)
if self.valid:
self.secret = secret
self.compressed = compressed
def generate(self, compressed=True):
"""Generate a random private key (compressed or uncompressed)."""
self.set(random.randrange(1, SECP256K1_ORDER).to_bytes(32, 'big'), compressed)
def get_bytes(self):
"""Retrieve the 32-byte representation of this key."""
assert(self.valid)
return self.secret.to_bytes(32, 'big')
@property
def is_valid(self):
return self.valid
@property
def is_compressed(self):
return self.compressed
def get_pubkey(self):
"""Compute an ECPubKey object for this secret key."""
assert(self.valid)
ret = ECPubKey()
p = SECP256K1.mul([(SECP256K1_G, self.secret)])
ret.p = p
ret.valid = True
ret.compressed = self.compressed
return ret
def sign_ecdsa(self, msg, low_s=True):
"""Construct a DER-encoded ECDSA signature with this key.
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
ECDSA signer algorithm."""
assert(self.valid)
z = int.from_bytes(msg, 'big')
# Note: no RFC6979, but a simple random nonce (some tests rely on distinct transactions for the same operation)
k = random.randrange(1, SECP256K1_ORDER)
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, k)]))
r = R[0] % SECP256K1_ORDER
s = (modinv(k, SECP256K1_ORDER) * (z + self.secret * r)) % SECP256K1_ORDER
if low_s and s > SECP256K1_ORDER_HALF:
s = SECP256K1_ORDER - s
# Represent in DER format. The byte representations of r and s have
# length rounded up (255 bits becomes 32 bytes and 256 bits becomes 33
# bytes).
rb = r.to_bytes((r.bit_length() + 8) // 8, 'big')
sb = s.to_bytes((s.bit_length() + 8) // 8, 'big')
return b'\x30' + bytes([4 + len(rb) + len(sb), 2, len(rb)]) + rb + bytes([2, len(sb)]) + sb

46
basicswap/messages.proto Normal file
View File

@@ -0,0 +1,46 @@
syntax = "proto3";
package basicswap;
/* Step 1, seller -> network */
message OfferMessage {
uint32 coin_from = 1;
uint32 coin_to = 2;
uint64 amount_from = 3;
uint64 rate = 4;
uint64 min_bid_amount = 5;
uint64 time_valid = 6;
enum LockType {
NOT_SET = 0;
SEQUENCE_LOCK_BLOCKS = 1;
SEQUENCE_LOCK_TIME = 2;
}
LockType lock_type = 7;
uint32 lock_value = 8;
uint32 swap_type = 9;
/* optional */
string proof_address = 10;
string proof_signature = 11;
bytes pkhash_seller = 12;
bytes secret_hash = 13;
}
/* Step 2, buyer -> seller */
message BidMessage {
bytes offer_msg_id = 1;
uint64 time_valid = 2; /* seconds bid is valid for */
uint64 amount = 3; /* amount of amount_from bid is for */
/* optional */
bytes pkhash_buyer = 4; /* buyer's address to receive amount_from */
string proof_address = 5;
string proof_signature = 6;
}
/* Step 3, seller -> buyer */
message BidAcceptMessage {
bytes bid_msg_id = 1;
bytes initiate_txid = 2;
bytes contract_script = 3;
}

310
basicswap/messages_pb2.py Normal file
View File

@@ -0,0 +1,310 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: messages.proto
import sys
_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor.FileDescriptor(
name='messages.proto',
package='basicswap',
syntax='proto3',
serialized_options=None,
serialized_pb=_b('\n\x0emessages.proto\x12\tbasicswap\"\x84\x03\n\x0cOfferMessage\x12\x11\n\tcoin_from\x18\x01 \x01(\r\x12\x0f\n\x07\x63oin_to\x18\x02 \x01(\r\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x16\n\x0emin_bid_amount\x18\x05 \x01(\x04\x12\x12\n\ntime_valid\x18\x06 \x01(\x04\x12\x33\n\tlock_type\x18\x07 \x01(\x0e\x32 .basicswap.OfferMessage.LockType\x12\x12\n\nlock_value\x18\x08 \x01(\r\x12\x11\n\tswap_type\x18\t \x01(\r\x12\x15\n\rproof_address\x18\n \x01(\t\x12\x17\n\x0fproof_signature\x18\x0b \x01(\t\x12\x15\n\rpkhash_seller\x18\x0c \x01(\x0c\x12\x13\n\x0bsecret_hash\x18\r \x01(\x0c\"I\n\x08LockType\x12\x0b\n\x07NOT_SET\x10\x00\x12\x18\n\x14SEQUENCE_LOCK_BLOCKS\x10\x01\x12\x16\n\x12SEQUENCE_LOCK_TIME\x10\x02\"\x8c\x01\n\nBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x04 \x01(\x0c\x12\x15\n\rproof_address\x18\x05 \x01(\t\x12\x17\n\x0fproof_signature\x18\x06 \x01(\t\"V\n\x10\x42idAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x15\n\rinitiate_txid\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63ontract_script\x18\x03 \x01(\x0c\x62\x06proto3')
)
_OFFERMESSAGE_LOCKTYPE = _descriptor.EnumDescriptor(
name='LockType',
full_name='basicswap.OfferMessage.LockType',
filename=None,
file=DESCRIPTOR,
values=[
_descriptor.EnumValueDescriptor(
name='NOT_SET', index=0, number=0,
serialized_options=None,
type=None),
_descriptor.EnumValueDescriptor(
name='SEQUENCE_LOCK_BLOCKS', index=1, number=1,
serialized_options=None,
type=None),
_descriptor.EnumValueDescriptor(
name='SEQUENCE_LOCK_TIME', index=2, number=2,
serialized_options=None,
type=None),
],
containing_type=None,
serialized_options=None,
serialized_start=345,
serialized_end=418,
)
_sym_db.RegisterEnumDescriptor(_OFFERMESSAGE_LOCKTYPE)
_OFFERMESSAGE = _descriptor.Descriptor(
name='OfferMessage',
full_name='basicswap.OfferMessage',
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
_descriptor.FieldDescriptor(
name='coin_from', full_name='basicswap.OfferMessage.coin_from', index=0,
number=1, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='coin_to', full_name='basicswap.OfferMessage.coin_to', index=1,
number=2, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='amount_from', full_name='basicswap.OfferMessage.amount_from', index=2,
number=3, type=4, cpp_type=4, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='rate', full_name='basicswap.OfferMessage.rate', index=3,
number=4, type=4, cpp_type=4, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='min_bid_amount', full_name='basicswap.OfferMessage.min_bid_amount', index=4,
number=5, type=4, cpp_type=4, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='time_valid', full_name='basicswap.OfferMessage.time_valid', index=5,
number=6, type=4, cpp_type=4, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='lock_type', full_name='basicswap.OfferMessage.lock_type', index=6,
number=7, type=14, cpp_type=8, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='lock_value', full_name='basicswap.OfferMessage.lock_value', index=7,
number=8, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='swap_type', full_name='basicswap.OfferMessage.swap_type', index=8,
number=9, type=13, cpp_type=3, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='proof_address', full_name='basicswap.OfferMessage.proof_address', index=9,
number=10, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=_b("").decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='proof_signature', full_name='basicswap.OfferMessage.proof_signature', index=10,
number=11, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=_b("").decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='pkhash_seller', full_name='basicswap.OfferMessage.pkhash_seller', index=11,
number=12, type=12, cpp_type=9, label=1,
has_default_value=False, default_value=_b(""),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='secret_hash', full_name='basicswap.OfferMessage.secret_hash', index=12,
number=13, type=12, cpp_type=9, label=1,
has_default_value=False, default_value=_b(""),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
],
extensions=[
],
nested_types=[],
enum_types=[
_OFFERMESSAGE_LOCKTYPE,
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=30,
serialized_end=418,
)
_BIDMESSAGE = _descriptor.Descriptor(
name='BidMessage',
full_name='basicswap.BidMessage',
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
_descriptor.FieldDescriptor(
name='offer_msg_id', full_name='basicswap.BidMessage.offer_msg_id', index=0,
number=1, type=12, cpp_type=9, label=1,
has_default_value=False, default_value=_b(""),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='time_valid', full_name='basicswap.BidMessage.time_valid', index=1,
number=2, type=4, cpp_type=4, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='amount', full_name='basicswap.BidMessage.amount', index=2,
number=3, type=4, cpp_type=4, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='pkhash_buyer', full_name='basicswap.BidMessage.pkhash_buyer', index=3,
number=4, type=12, cpp_type=9, label=1,
has_default_value=False, default_value=_b(""),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='proof_address', full_name='basicswap.BidMessage.proof_address', index=4,
number=5, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=_b("").decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='proof_signature', full_name='basicswap.BidMessage.proof_signature', index=5,
number=6, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=_b("").decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=421,
serialized_end=561,
)
_BIDACCEPTMESSAGE = _descriptor.Descriptor(
name='BidAcceptMessage',
full_name='basicswap.BidAcceptMessage',
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
_descriptor.FieldDescriptor(
name='bid_msg_id', full_name='basicswap.BidAcceptMessage.bid_msg_id', index=0,
number=1, type=12, cpp_type=9, label=1,
has_default_value=False, default_value=_b(""),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='initiate_txid', full_name='basicswap.BidAcceptMessage.initiate_txid', index=1,
number=2, type=12, cpp_type=9, label=1,
has_default_value=False, default_value=_b(""),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='contract_script', full_name='basicswap.BidAcceptMessage.contract_script', index=2,
number=3, type=12, cpp_type=9, label=1,
has_default_value=False, default_value=_b(""),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=563,
serialized_end=649,
)
_OFFERMESSAGE.fields_by_name['lock_type'].enum_type = _OFFERMESSAGE_LOCKTYPE
_OFFERMESSAGE_LOCKTYPE.containing_type = _OFFERMESSAGE
DESCRIPTOR.message_types_by_name['OfferMessage'] = _OFFERMESSAGE
DESCRIPTOR.message_types_by_name['BidMessage'] = _BIDMESSAGE
DESCRIPTOR.message_types_by_name['BidAcceptMessage'] = _BIDACCEPTMESSAGE
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
OfferMessage = _reflection.GeneratedProtocolMessageType('OfferMessage', (_message.Message,), dict(
DESCRIPTOR = _OFFERMESSAGE,
__module__ = 'messages_pb2'
# @@protoc_insertion_point(class_scope:basicswap.OfferMessage)
))
_sym_db.RegisterMessage(OfferMessage)
BidMessage = _reflection.GeneratedProtocolMessageType('BidMessage', (_message.Message,), dict(
DESCRIPTOR = _BIDMESSAGE,
__module__ = 'messages_pb2'
# @@protoc_insertion_point(class_scope:basicswap.BidMessage)
))
_sym_db.RegisterMessage(BidMessage)
BidAcceptMessage = _reflection.GeneratedProtocolMessageType('BidAcceptMessage', (_message.Message,), dict(
DESCRIPTOR = _BIDACCEPTMESSAGE,
__module__ = 'messages_pb2'
# @@protoc_insertion_point(class_scope:basicswap.BidAcceptMessage)
))
_sym_db.RegisterMessage(BidAcceptMessage)
# @@protoc_insertion_point(module_scope)

123
basicswap/segwit_addr.py Normal file
View File

@@ -0,0 +1,123 @@
# Copyright (c) 2017 Pieter Wuille
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""Reference implementation for Bech32 and segwit addresses."""
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
def bech32_polymod(values):
"""Internal function that computes the Bech32 checksum."""
generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
chk = 1
for value in values:
top = chk >> 25
chk = (chk & 0x1ffffff) << 5 ^ value
for i in range(5):
chk ^= generator[i] if ((top >> i) & 1) else 0
return chk
def bech32_hrp_expand(hrp):
"""Expand the HRP into values for checksum computation."""
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
def bech32_verify_checksum(hrp, data):
"""Verify a checksum given HRP and converted data characters."""
return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1
def bech32_create_checksum(hrp, data):
"""Compute the checksum values given HRP and data."""
values = bech32_hrp_expand(hrp) + data
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
def bech32_encode(hrp, data):
"""Compute a Bech32 string given HRP and data values."""
combined = data + bech32_create_checksum(hrp, data)
return hrp + '1' + ''.join([CHARSET[d] for d in combined])
def bech32_decode(bech):
"""Validate a Bech32 string, and determine HRP and data."""
if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
(bech.lower() != bech and bech.upper() != bech)):
return (None, None)
bech = bech.lower()
pos = bech.rfind('1')
if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
return (None, None)
if not all(x in CHARSET for x in bech[pos + 1:]):
return (None, None)
hrp = bech[:pos]
data = [CHARSET.find(x) for x in bech[pos + 1:]]
if not bech32_verify_checksum(hrp, data):
return (None, None)
return (hrp, data[:-6])
def convertbits(data, frombits, tobits, pad=True):
"""General power-of-2 base conversion."""
acc = 0
bits = 0
ret = []
maxv = (1 << tobits) - 1
max_acc = (1 << (frombits + tobits - 1)) - 1
for value in data:
if value < 0 or (value >> frombits):
return None
acc = ((acc << frombits) | value) & max_acc
bits += frombits
while bits >= tobits:
bits -= tobits
ret.append((acc >> bits) & maxv)
if pad:
if bits:
ret.append((acc << (tobits - bits)) & maxv)
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
return None
return ret
def decode(hrp, addr):
"""Decode a segwit address."""
hrpgot, data = bech32_decode(addr)
if hrpgot != hrp:
return (None, None)
decoded = convertbits(data[1:], 5, 8, False)
if decoded is None or len(decoded) < 2 or len(decoded) > 40:
return (None, None)
if data[0] > 16:
return (None, None)
if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
return (None, None)
return (data[0], decoded)
def encode(hrp, witver, witprog):
"""Encode a segwit address."""
ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5))
if decode(hrp, ret) == (None, None):
return None
return ret

292
basicswap/util.py Normal file
View File

@@ -0,0 +1,292 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018-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 decimal
import subprocess
import json
import traceback
import hashlib
import urllib
from xmlrpc.client import (
Transport,
Fault,
)
from .segwit_addr import bech32_decode, convertbits, bech32_encode
COIN = 100000000
def format8(i):
n = abs(i)
quotient = n // COIN
remainder = n % COIN
rv = "%d.%08d" % (quotient, remainder)
if i < 0:
rv = '-' + rv
return rv
def toBool(s):
return s.lower() in ["1", "true"]
def dquantize(n, places=8):
return n.quantize(decimal.Decimal(10) ** -places)
def jsonDecimal(obj):
if isinstance(obj, decimal.Decimal):
return str(obj)
raise TypeError
def dumpj(jin, indent=4):
return json.dumps(jin, indent=indent, default=jsonDecimal)
def dumpje(jin):
return json.dumps(jin, default=jsonDecimal).replace('"', '\\"')
__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
def b58decode(v, length=None):
long_value = 0
for (i, c) in enumerate(v[::-1]):
ofs = __b58chars.find(c)
if ofs < 0:
return None
long_value += ofs * (58**i)
result = bytes()
while long_value >= 256:
div, mod = divmod(long_value, 256)
result = bytes((mod,)) + result
long_value = div
result = bytes((long_value,)) + result
nPad = 0
for c in v:
if c == __b58chars[0]:
nPad += 1
else:
break
pad = bytes((0,)) * nPad
result = pad + result
if length is not None and len(result) != length:
return None
return result
def b58encode(v):
long_value = 0
for (i, c) in enumerate(v[::-1]):
long_value += (256**i) * c
result = ''
while long_value >= 58:
div, mod = divmod(long_value, 58)
result = __b58chars[mod] + result
long_value = div
result = __b58chars[long_value] + result
# leading 0-bytes in the input become leading-1s
nPad = 0
for c in v:
if c == 0:
nPad += 1
else:
break
return (__b58chars[0] * nPad) + result
def decodeWif(network_key):
key = b58decode(network_key)[1:-4]
if len(key) == 33:
return key[:-1]
return key
def toWIF(prefix_byte, b, compressed=True):
b = bytes((prefix_byte, )) + b
if compressed:
b += bytes((0x01, ))
b += hashlib.sha256(hashlib.sha256(b).digest()).digest()[:4]
return b58encode(b)
def bech32Decode(hrp, addr):
hrpgot, data = bech32_decode(addr)
if hrpgot != hrp:
return None
decoded = convertbits(data, 5, 8, False)
if decoded is None or len(decoded) < 2 or len(decoded) > 40:
return None
return bytes(decoded)
def bech32Encode(hrp, data):
ret = bech32_encode(hrp, convertbits(data, 8, 5))
if bech32Decode(hrp, ret) is None:
return None
return ret
def decodeAddress(address_str):
b58_addr = b58decode(address_str)
if b58_addr is not None:
address = b58_addr[:-4]
checksum = b58_addr[-4:]
assert(hashlib.sha256(hashlib.sha256(address).digest()).digest()[:4] == checksum), 'Checksum mismatch'
return b58_addr[:-4]
return None
def encodeAddress(address):
checksum = hashlib.sha256(hashlib.sha256(address).digest()).digest()
return b58encode(address + checksum[0:4])
def getKeyID(bytes):
data = hashlib.sha256(bytes).digest()
return hashlib.new("ripemd160", data).digest()
def pubkeyToAddress(prefix, pubkey):
return encodeAddress(bytes((prefix,)) + getKeyID(pubkey))
def SerialiseNum(n):
if n == 0:
return bytes([0x00])
if n > 0 and n <= 16:
return bytes([0x50 + n])
rv = bytearray()
neg = n < 0
absvalue = -n if neg else n
while(absvalue):
rv.append(absvalue & 0xff)
absvalue >>= 8
if rv[-1] & 0x80:
rv.append(0x80 if neg else 0)
elif neg:
rv[-1] |= 0x80
return bytes([len(rv)]) + rv
def DeserialiseNum(b, o=0):
if b[o] == 0:
return 0
if b[o] > 0x50 and b[o] <= 0x50 + 16:
return b[o] - 0x50
v = 0
nb = b[o]
o += 1
for i in range(0, nb):
v |= b[o + i] << (8 * i)
# If the input vector's most significant byte is 0x80, remove it from the result's msb and return a negative.
if b[o + nb - 1] & 0x80:
return -(v & ~(0x80 << (8 * (nb - 1))))
return v
class Jsonrpc():
# __getattr__ complicates extending ServerProxy
def __init__(self, uri, transport=None, encoding=None, verbose=False,
allow_none=False, use_datetime=False, use_builtin_types=False,
*, context=None):
# establish a "logical" server connection
# get the url
type, uri = urllib.parse.splittype(uri)
if type not in ("http", "https"):
raise OSError("unsupported XML-RPC protocol")
self.__host, self.__handler = urllib.parse.splithost(uri)
if not self.__handler:
self.__handler = "/RPC2"
if transport is None:
handler = Transport
extra_kwargs = {}
transport = handler(use_datetime=use_datetime,
use_builtin_types=use_builtin_types,
**extra_kwargs)
self.__transport = transport
self.__encoding = encoding or 'utf-8'
self.__verbose = verbose
self.__allow_none = allow_none
def close(self):
if self.__transport is not None:
self.__transport.close()
def json_request(self, method, params):
try:
connection = self.__transport.make_connection(self.__host)
headers = self.__transport._extra_headers[:]
request_body = {
'method': method,
'params': params,
'id': 2
}
connection.putrequest("POST", self.__handler)
headers.append(("Content-Type", "application/json"))
headers.append(("User-Agent", 'jsonrpc'))
self.__transport.send_headers(connection, headers)
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8'))
resp = connection.getresponse()
return resp.read()
except Fault:
raise
except Exception:
# All unexpected errors leave connection in
# a strange state, so we clear it.
self.__transport.close()
raise
def callrpc(rpc_port, auth, method, params=[], wallet=None):
try:
url = 'http://%s@127.0.0.1:%d/' % (auth, rpc_port)
if wallet:
url += 'wallet/' + wallet
x = Jsonrpc(url)
v = x.json_request(method, params)
x.close()
r = json.loads(v.decode('utf-8'))
except Exception as e:
traceback.print_exc()
raise ValueError('RPC Server Error')
if 'error' in r and r['error'] is not None:
raise ValueError('RPC error ' + str(r['error']))
return r['result']
def callrpc_cli(bindir, datadir, chain, cmd, cli_bin='particl-cli'):
cli_bin = os.path.join(bindir, cli_bin)
args = cli_bin + ('' if chain == 'mainnet' else ' -' + chain) + ' -datadir=' + datadir + ' ' + cmd
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
out = p.communicate()
if len(out[1]) > 0:
raise ValueError('RPC error ' + str(out[1]))
r = out[0].decode('utf-8').strip()
try:
r = json.loads(r)
except Exception:
pass
return r