Add to Github

This commit is contained in:
tecnovert 2019-07-17 17:12:06 +02:00
commit e242f50b2b
No known key found for this signature in database
GPG key ID: 13F13651C9CF0D6B
26 changed files with 5035 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
old/
*.pyc
__pycache__
/dist/
/*.egg-info
/*.egg

46
.travis.yml Normal file
View file

@ -0,0 +1,46 @@
dist: xenial
os: linux
language: python
python: '3.6'
cache:
directories:
- /opt/binaries
stages:
- lint
env:
global:
- PARTICL_BINDIR=/opt/binaries/particl-0.18.0.12/bin/
- BITCOIN_BINDIR=/opt/binaries/bitcoin-0.18.0/bin/
- LITECOIN_BINDIR=/opt/binaries/litecoin-0.17.1/bin/
before_script:
- if [ ! -d "/opt/binaries" ]; then mkdir -p "/opt/binaries" ; fi
- if [ ! -d "$BITCOIN_BINDIR" ]; then cd "/opt/binaries" && wget https://bitcoincore.org/bin/bitcoin-core-0.18.0/bitcoin-0.18.0-x86_64-linux-gnu.tar.gz && tar xvf bitcoin-0.18.0-x86_64-linux-gnu.tar.gz ; fi
- if [ ! -d "$LITECOIN_BINDIR" ]; then cd "/opt/binaries" && wget https://download.litecoin.org/litecoin-0.17.1/linux/litecoin-0.17.1-x86_64-linux-gnu.tar.gz && tar xvf litecoin-0.17.1-x86_64-linux-gnu.tar.gz ; fi
- if [ ! -d "$PARTICL_BINDIR" ]; then cd "/opt/binaries" && wget https://github.com/particl/particl-core/releases/download/v0.18.0.12/particl-0.18.0.12-x86_64-linux-gnu_nousb.tar.gz && tar xvf particl-0.18.0.12-x86_64-linux-gnu_nousb.tar.gz ; fi
- cd
script:
- cd $TRAVIS_BUILD_DIR
- export PARTICL_BINDIR=/opt/binaries/particl-0.18.0.12/bin/
- export BITCOIN_BINDIR=/opt/binaries/bitcoin-0.18.0/bin/
- export LITECOIN_BINDIR=/opt/binaries/litecoin-0.17.1/bin/
- python setup.py test
after_success:
- echo "End test"
jobs:
include:
- stage: lint
env:
cache: false
language: python
python: '3.6'
before_install:
- sudo apt-get install -y wget gnupg
install:
- travis_retry pip install flake8==3.5.0
- travis_retry pip install codespell==1.15.0
before_script:
script:
- PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841 --exclude=key.py,messages_pb2.py
- codespell --check-filenames --disable-colors --quiet-level=7 -S .git
after_success:
- echo "End lint"

45
Dockerfile Normal file
View file

@ -0,0 +1,45 @@
FROM ubuntu:18.10
ENV PARTICL_DATADIR="/coindata/particl" \
PARTICL_BINDIR="/opt/particl" \
LITECOIN_BINDIR="/opt/litecoin" \
DATADIRS="/coindata"
RUN apt-get update; \
apt-get install -y wget python3-pip curl gnupg unzip protobuf-compiler;
RUN cd ~; \
wget https://github.com/particl/coldstakepool/archive/master.zip; \
unzip master.zip; \
cd coldstakepool-master; \
pip3 install .; \
pip3 install pyzmq plyvel protobuf;
RUN PARTICL_VERSION=0.18.0.12 PARTICL_VERSION_TAG= PARTICL_ARCH=x86_64-linux-gnu_nousb.tar.gz coldstakepool-prepare --update_core; \
wget https://download.litecoin.org/litecoin-0.17.1/linux/litecoin-0.17.1-x86_64-linux-gnu.tar.gz; \
mkdir -p ${LITECOIN_BINDIR}; \
tar -xvf litecoin-0.17.1-x86_64-linux-gnu.tar.gz -C ${LITECOIN_BINDIR} --strip-components 2 litecoin-0.17.1/bin/litecoind litecoin-0.17.1/bin/litecoin-cli
# Change to git clone
COPY . /opt/basicswap
RUN ls /opt/basicswap; \
cd /opt/basicswap; \
protoc -I=basicswap --python_out=basicswap basicswap/messages.proto; \
pip3 install .;
RUN useradd -ms /bin/bash user; \
mkdir /coindata && chown user /coindata
USER user
WORKDIR /home/user
# Expose html port
EXPOSE 12700
ENV LANG C.UTF-8
VOLUME /coindata
ENTRYPOINT ["basicswap-run", "-datadir=/coindata/basicswap"]
CMD

20
LICENSE.txt Normal file
View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2019 tecnovert
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.

7
MANIFEST.in Normal file
View file

@ -0,0 +1,7 @@
# Include the README
include *.md
# Include the license file
include LICENSE.txt
recursive-include doc *

69
README.md Normal file
View file

@ -0,0 +1,69 @@
# Particl Atomic Swap - Proof of concept
## Overview
Simple atomic swap experiment, doesn't have many interesting features yet.
Not ready for real world use.
Uses Particl secure messaging and Decred style atomic swaps.
The Particl node is used to hold the keys and sign for the swap transactions.
Other nodes can be run in pruned mode.
A node must be run for each coin type traded.
In the future it should be possible to use data from explorers instead of running a node.
## Currently a work in progress
Not ready for real-world use.
Features still required (of many):
- Cached addresses must be regenerated after use.
- Option to lookup data from public explorers / nodes.
- Load active bids from db at startup
- Ability to swap coin-types without running nodes for all coin-types
- More swap protocols
- Method to load mnemonic into Particl.
## Seller first protocol:
Seller sends the 1st transaction.
1. Seller posts offer.
- smsg from seller to network
coin-from
coin-to
amount-from
rate
min-amount
time-valid
2. Buyer posts bid:
- smsg from buyer to seller
offerid
amount
proof-of-funds
address_to_buyer
time-valid
3. Seller accepts bid:
- verifies proof-of-funds
- generates secret
- submits initiate tx to coin-from network
- smsg from seller to buyer
txid
initiatescript (includes pkhash_to_seller as the pkhash_refund)
4. Buyer participates:
- inspects initiate tx in coin-from network
- submits participate tx in coin-to network
5. Seller redeems:
- constructs participatescript
- inspects participate tx in coin-to network
- redeems from participate tx revealing secret
6. Buyer redeems:
- scans coin-to network for seller-redeem tx
- redeems from initiate tx with revealed secret

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

1
bin/__init__.py Normal file
View file

@ -0,0 +1 @@
name = "bin"

1
bin/basicswap-run.py Symbolic link
View file

@ -0,0 +1 @@
basicswap_run.py

177
bin/basicswap_run.py Normal file
View file

@ -0,0 +1,177 @@
#!/usr/bin/env python3
# -*- 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.
"""
Particl Atomic Swap - Proof of Concept
Dependencies:
$ pacman -S python-pyzmq python-plyvel protobuf
"""
import sys
import os
import time
import json
import traceback
import signal
import subprocess
import basicswap.config as cfg
from basicswap import __version__
from basicswap.basicswap import BasicSwap
from basicswap.http_server import HttpThread
ALLOW_CORS = False
swap_client = None
def signal_handler(sig, frame):
print('signal %d detected, ending program.' % (sig))
if swap_client is not None:
swap_client.stopRunning()
def startDaemon(node_dir, bin_dir, daemon_bin):
daemon_bin = os.path.join(bin_dir, daemon_bin)
args = [daemon_bin, '-datadir=' + node_dir]
print('Starting node ' + daemon_bin + ' ' + '-datadir=' + node_dir)
return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def runClient(fp, dataDir, chain):
global swap_client
settings_path = os.path.join(dataDir, 'basicswap.json')
if not os.path.exists(settings_path):
raise ValueError('Settings file not found: ' + str(settings_path))
with open(settings_path) as fs:
settings = json.load(fs)
daemons = []
for c, v in settings['chainclients'].items():
if v['manage_daemon'] is True:
print('Starting {} daemon'.format(c.capitalize()))
if c == 'particl':
daemons.append(startDaemon(v['datadir'], cfg.PARTICL_BINDIR, cfg.PARTICLD))
print('Started {} {}'.format(cfg.PARTICLD, daemons[-1].pid))
elif c == 'bitcoin':
daemons.append(startDaemon(v['datadir'], cfg.BITCOIN_BINDIR, cfg.BITCOIND))
print('Started {} {}'.format(cfg.BITCOIND, daemons[-1].pid))
elif c == 'litecoin':
daemons.append(startDaemon(v['datadir'], cfg.LITECOIN_BINDIR, cfg.LITECOIND))
print('Started {} {}'.format(cfg.LITECOIND, daemons[-1].pid))
else:
print('Unknown chain', c)
swap_client = BasicSwap(fp, dataDir, settings, chain)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
swap_client.start()
threads = []
if 'htmlhost' in settings:
swap_client.log.info('Starting server at %s:%d.' % (settings['htmlhost'], settings['htmlport']))
allow_cors = settings['allowcors'] if 'allowcors' in settings else ALLOW_CORS
tS1 = HttpThread(fp, settings['htmlhost'], settings['htmlport'], allow_cors, swap_client)
threads.append(tS1)
tS1.start()
try:
print('Exit with Ctrl + c.')
while swap_client.is_running:
time.sleep(0.5)
swap_client.update()
except Exception:
traceback.print_exc()
swap_client.log.info('Stopping threads.')
for t in threads:
t.stop()
t.join()
for d in daemons:
print('Terminating {}'.format(d.pid))
d.terminate()
d.wait(timeout=120)
if d.stdout:
d.stdout.close()
if d.stderr:
d.stderr.close()
if d.stdin:
d.stdin.close()
def printVersion():
print('Basicswap version:', __version__)
def printHelp():
print('basicswap-run.py --datadir=path -testnet')
def main():
data_dir = None
chain = 'mainnet'
for v in sys.argv[1:]:
if len(v) < 2 or v[0] != '-':
print('Unknown argument', v)
continue
s = v.split('=')
name = s[0].strip()
for i in range(2):
if name[0] == '-':
name = name[1:]
if name == 'v' or name == 'version':
printVersion()
return 0
if name == 'h' or name == 'help':
printHelp()
return 0
if name == 'testnet':
chain = 'testnet'
continue
if name == 'regtest':
chain = 'regtest'
continue
if len(s) == 2:
if name == 'datadir':
data_dir = os.path.expanduser(s[1])
continue
print('Unknown argument', v)
if data_dir is None:
data_dir = os.path.join(os.path.expanduser(os.path.join(cfg.DATADIRS)), 'particl', ('' if chain == 'mainnet' else chain), 'basicswap')
print('data_dir:', data_dir)
if chain != 'mainnet':
print('chain:', chain)
if not os.path.exists(data_dir):
os.makedirs(data_dir)
with open(os.path.join(data_dir, 'basicswap.log'), 'w') as fp:
print(os.path.basename(sys.argv[0]) + ', version: ' + __version__ + '\n\n')
runClient(fp, data_dir, chain)
print('Done.')
return swap_client.fail_code if swap_client is not None else 0
if __name__ == '__main__':
main()

0
bin/start_docker.bat Normal file
View file

5
docker/coindata/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
*
!.gitignore
!basicswap/basicswap.json
!particl/particl.conf
!litecoin/litecoin.conf

15
docker/docker-compose.yml Normal file
View file

@ -0,0 +1,15 @@
version: '3'
services:
swapclient:
build:
context: ../
volumes:
- ./coindata:/coindata
ports:
- "127.0.0.1:12700:12700" # Expose only to localhost
volumes:
coindata:
driver: local

37
setup.py Normal file
View file

@ -0,0 +1,37 @@
import setuptools
import re
import io
__version__ = re.search(
r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]',
io.open('basicswap/__init__.py', encoding='utf_8_sig').read()
).group(1)
setuptools.setup(
name="basicswap",
version=__version__,
author="tecnovert",
author_email="hello@particl.io",
description="Particl atomic swap demo",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
url="https://github.com/tecnovert/basicswap",
packages=setuptools.find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: Linux",
],
install_requires=[
"pyzmq",
"plyvel",
"protobuf",
"sqlalchemy",
],
entry_points={
"console_scripts": [
"basicswap-run=bin.basicswap_run:main",
]
},
test_suite="tests.test_suite"
)

11
tests/__init__.py Normal file
View file

@ -0,0 +1,11 @@
import unittest
import tests.test_run
import tests.test_other
def test_suite():
loader = unittest.TestLoader()
suite = loader.loadTestsFromModule(tests.test_run)
suite.addTests(loader.loadTestsFromModule(tests.test_other))
return suite

62
tests/test_other.py Normal file
View file

@ -0,0 +1,62 @@
#!/usr/bin/env python3
# -*- 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 unittest
from basicswap.util import (
SerialiseNum,
DeserialiseNum,
)
from basicswap.basicswap import (
Coins,
getExpectedSequence,
decodeSequence,
SEQUENCE_LOCK_BLOCKS,
SEQUENCE_LOCK_TIME,
)
def test_case(v, nb=None):
b = SerialiseNum(v)
if nb is not None:
assert(len(b) == nb)
assert(v == DeserialiseNum(b))
class Test(unittest.TestCase):
def test_serialise_num(self):
test_case(0, 1)
test_case(1, 1)
test_case(16, 1)
test_case(-1, 2)
test_case(17, 2)
test_case(500)
test_case(-500)
test_case(4194642)
def test_sequence(self):
time_val = 48 * 60 * 60
encoded = getExpectedSequence(SEQUENCE_LOCK_TIME, time_val, Coins.PART)
decoded = decodeSequence(encoded)
assert(decoded >= time_val)
assert(decoded <= time_val + 512)
time_val = 24 * 60
encoded = getExpectedSequence(SEQUENCE_LOCK_TIME, time_val, Coins.PART)
decoded = decodeSequence(encoded)
assert(decoded >= time_val)
assert(decoded <= time_val + 512)
blocks_val = 123
encoded = getExpectedSequence(SEQUENCE_LOCK_BLOCKS, blocks_val, Coins.PART)
decoded = decodeSequence(encoded)
assert(decoded == blocks_val)
if __name__ == '__main__':
unittest.main()

536
tests/test_run.py Normal file
View file

@ -0,0 +1,536 @@
#!/usr/bin/env python3
# -*- 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.
"""
basicswap]$ python setup.py test
Run one test:
$ python setup.py test -s tests.test_run.Test.test_04_ltc_btc
"""
import os
import sys
import unittest
import json
import logging
import shutil
import subprocess
import time
import signal
import threading
from urllib.request import urlopen
from basicswap.basicswap import (
BasicSwap,
Coins,
SwapTypes,
BidStates,
TxStates,
SEQUENCE_LOCK_BLOCKS,
)
from basicswap.util import (
COIN,
toWIF,
callrpc_cli,
dumpje,
)
from basicswap.key import (
ECKey,
)
from basicswap.http_server import (
HttpThread,
)
import basicswap.config as cfg
logger = logging.getLogger()
logger.level = logging.DEBUG
logger.addHandler(logging.StreamHandler(sys.stdout))
NUM_NODES = 3
BASE_PORT = 14792
BASE_RPC_PORT = 19792
BASE_ZMQ_PORT = 20792
PREFIX_SECRET_KEY_REGTEST = 0x2e
TEST_HTML_PORT = 1800
LTC_NODE = 3
BTC_NODE = 4
stop_test = False
def prepareOtherDir(datadir, nodeId, conf_file='litecoin.conf'):
node_dir = os.path.join(datadir, str(nodeId))
if not os.path.exists(node_dir):
os.makedirs(node_dir)
filePath = os.path.join(node_dir, conf_file)
with open(filePath, 'w+') as fp:
fp.write('regtest=1\n')
fp.write('[regtest]\n')
fp.write('port=' + str(BASE_PORT + nodeId) + '\n')
fp.write('rpcport=' + str(BASE_RPC_PORT + nodeId) + '\n')
fp.write('daemon=0\n')
fp.write('printtoconsole=0\n')
fp.write('server=1\n')
fp.write('discover=0\n')
fp.write('listenonion=0\n')
fp.write('bind=127.0.0.1\n')
fp.write('findpeers=0\n')
fp.write('debug=1\n')
fp.write('debugexclude=libevent\n')
fp.write('acceptnonstdtxn=0\n')
def prepareDir(datadir, nodeId, network_key, network_pubkey):
node_dir = os.path.join(datadir, str(nodeId))
if not os.path.exists(node_dir):
os.makedirs(node_dir)
filePath = os.path.join(node_dir, 'particl.conf')
with open(filePath, 'w+') as fp:
fp.write('regtest=1\n')
fp.write('[regtest]\n')
fp.write('port=' + str(BASE_PORT + nodeId) + '\n')
fp.write('rpcport=' + str(BASE_RPC_PORT + nodeId) + '\n')
fp.write('daemon=0\n')
fp.write('printtoconsole=0\n')
fp.write('server=1\n')
fp.write('discover=0\n')
fp.write('listenonion=0\n')
fp.write('bind=127.0.0.1\n')
fp.write('findpeers=0\n')
fp.write('debug=1\n')
fp.write('debugexclude=libevent\n')
fp.write('zmqpubsmsg=tcp://127.0.0.1:' + str(BASE_ZMQ_PORT + nodeId) + '\n')
fp.write('acceptnonstdtxn=0\n')
fp.write('minstakeinterval=5\n')
for i in range(0, NUM_NODES):
if nodeId == i:
continue
fp.write('addnode=127.0.0.1:%d\n' % (BASE_PORT + i))
if nodeId < 2:
fp.write('spentindex=1\n')
fp.write('txindex=1\n')
basicswap_dir = os.path.join(datadir, str(nodeId), 'basicswap')
if not os.path.exists(basicswap_dir):
os.makedirs(basicswap_dir)
ltcdatadir = os.path.join(datadir, str(LTC_NODE))
btcdatadir = os.path.join(datadir, str(BTC_NODE))
settings_path = os.path.join(basicswap_dir, 'basicswap.json')
settings = {
'zmqhost': 'tcp://127.0.0.1',
'zmqport': BASE_ZMQ_PORT + nodeId,
'htmlhost': 'localhost',
'htmlport': 12700 + nodeId,
'network_key': network_key,
'network_pubkey': network_pubkey,
'chainclients': {
'particl': {
'connection_type': 'rpc',
'manage_daemon': False,
'rpcport': BASE_RPC_PORT + nodeId,
'datadir': node_dir,
'bindir': cfg.PARTICL_BINDIR,
'blocks_confirmed': 2, # Faster testing
},
'litecoin': {
'connection_type': 'rpc',
'manage_daemon': False,
'rpcport': BASE_RPC_PORT + LTC_NODE,
'datadir': ltcdatadir,
'bindir': cfg.LITECOIN_BINDIR,
# 'use_segwit': True,
},
'bitcoin': {
'connection_type': 'rpc',
'manage_daemon': False,
'rpcport': BASE_RPC_PORT + BTC_NODE,
'datadir': btcdatadir,
'bindir': cfg.BITCOIN_BINDIR,
'use_segwit': True,
}
},
'check_progress_seconds': 2,
'check_watched_seconds': 4,
'check_expired_seconds': 60
}
with open(settings_path, 'w') as fp:
json.dump(settings, fp, indent=4)
def startDaemon(nodeId, bin_dir=cfg.PARTICL_BINDIR, daemon_bin=cfg.PARTICLD):
node_dir = os.path.join(cfg.DATADIRS, str(nodeId))
daemon_bin = os.path.join(bin_dir, daemon_bin)
args = [daemon_bin, '-datadir=' + node_dir]
logging.info('Starting node ' + str(nodeId) + ' ' + daemon_bin + ' ' + '-datadir=' + node_dir)
return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def partRpc(cmd, node_id=0):
return callrpc_cli(cfg.PARTICL_BINDIR, os.path.join(cfg.DATADIRS, str(node_id)), 'regtest', cmd, cfg.PARTICL_CLI)
def btcRpc(cmd):
return callrpc_cli(cfg.BITCOIN_BINDIR, os.path.join(cfg.DATADIRS, str(BTC_NODE)), 'regtest', cmd, cfg.BITCOIN_CLI)
def ltcRpc(cmd):
return callrpc_cli(cfg.LITECOIN_BINDIR, os.path.join(cfg.DATADIRS, str(LTC_NODE)), 'regtest', cmd, cfg.LITECOIN_CLI)
def signal_handler(sig, frame):
global stop_test
print('signal {} detected.'.format(sig))
stop_test = True
def run_loop(self):
while not stop_test:
time.sleep(1)
for c in self.swap_clients:
c.update()
ltcRpc('generatetoaddress 1 {}'.format(self.ltc_addr))
btcRpc('generatetoaddress 1 {}'.format(self.btc_addr))
class Test(unittest.TestCase):
@classmethod
def setUpClass(cls):
super(Test, cls).setUpClass()
eckey = ECKey()
eckey.generate()
cls.network_key = toWIF(PREFIX_SECRET_KEY_REGTEST, eckey.get_bytes())
cls.network_pubkey = eckey.get_pubkey().get_bytes().hex()
if os.path.isdir(cfg.DATADIRS):
logging.info('Removing ' + cfg.DATADIRS)
shutil.rmtree(cfg.DATADIRS)
for i in range(NUM_NODES):
prepareDir(cfg.DATADIRS, i, cls.network_key, cls.network_pubkey)
prepareOtherDir(cfg.DATADIRS, LTC_NODE)
prepareOtherDir(cfg.DATADIRS, BTC_NODE, 'bitcoin.conf')
cls.daemons = []
cls.swap_clients = []
cls.daemons.append(startDaemon(BTC_NODE, cfg.BITCOIN_BINDIR, cfg.BITCOIND))
logging.info('Started %s %d', cfg.BITCOIND, cls.daemons[-1].pid)
cls.daemons.append(startDaemon(LTC_NODE, cfg.LITECOIN_BINDIR, cfg.LITECOIND))
logging.info('Started %s %d', cfg.LITECOIND, cls.daemons[-1].pid)
for i in range(NUM_NODES):
cls.daemons.append(startDaemon(i))
logging.info('Started %s %d', cfg.PARTICLD, cls.daemons[-1].pid)
time.sleep(1)
for i in range(NUM_NODES):
basicswap_dir = os.path.join(os.path.join(cfg.DATADIRS, str(i)), 'basicswap')
settings_path = os.path.join(basicswap_dir, 'basicswap.json')
with open(settings_path) as fs:
settings = json.load(fs)
fp = open(os.path.join(basicswap_dir, 'basicswap.log'), 'w')
cls.swap_clients.append(BasicSwap(fp, basicswap_dir, settings, 'regtest', log_name='BasicSwap{}'.format(i)))
cls.swap_clients[-1].start()
cls.swap_clients[0].callrpc('extkeyimportmaster', ['abandon baby cabbage dad eager fabric gadget habit ice kangaroo lab absorb'])
cls.swap_clients[1].callrpc('extkeyimportmaster', ['pact mammal barrel matrix local final lecture chunk wasp survey bid various book strong spread fall ozone daring like topple door fatigue limb olympic', '', 'true'])
cls.swap_clients[1].callrpc('getnewextaddress', ['lblExtTest'])
cls.swap_clients[1].callrpc('rescanblockchain')
num_blocks = 500
logging.info('Mining %d litecoin blocks', num_blocks)
cls.ltc_addr = ltcRpc('getnewaddress mining_addr legacy')
ltcRpc('generatetoaddress {} {}'.format(num_blocks, cls.ltc_addr))
ro = ltcRpc('getblockchaininfo')
assert(ro['bip9_softforks']['csv']['status'] == 'active')
assert(ro['bip9_softforks']['segwit']['status'] == 'active')
cls.btc_addr = btcRpc('getnewaddress mining_addr bech32')
logging.info('Mining %d bitcoin blocks to %s', num_blocks, cls.btc_addr)
btcRpc('generatetoaddress {} {}'.format(num_blocks, cls.btc_addr))
ro = btcRpc('getblockchaininfo')
assert(ro['bip9_softforks']['csv']['status'] == 'active')
assert(ro['bip9_softforks']['segwit']['status'] == 'active')
ro = ltcRpc('getwalletinfo')
print('ltcRpc', ro)
cls.http_threads = []
host = '0.0.0.0' # All interfaces (docker)
for i in range(3):
t = HttpThread(cls.swap_clients[i].fp, host, TEST_HTML_PORT + i, False, cls.swap_clients[i])
cls.http_threads.append(t)
t.start()
signal.signal(signal.SIGINT, signal_handler)
cls.update_thread = threading.Thread(target=run_loop, args=(cls,))
cls.update_thread.start()
@classmethod
def tearDownClass(cls):
global stop_test
logging.info('Finalising')
stop_test = True
cls.update_thread.join()
for t in cls.http_threads:
t.stop()
t.join()
for c in cls.swap_clients:
c.fp.close()
for d in cls.daemons:
logging.info('Terminating %d', d.pid)
d.terminate()
d.wait(timeout=10)
if d.stdout:
d.stdout.close()
if d.stderr:
d.stderr.close()
if d.stdin:
d.stdin.close()
super(Test, cls).tearDownClass()
def wait_for_offer(self, swap_client, offer_id):
logging.info('wait_for_offer %s', offer_id.hex())
for i in range(20):
time.sleep(1)
offers = swap_client.listOffers()
for offer in offers:
if offer.offer_id == offer_id:
return
raise ValueError('wait_for_offer timed out.')
def wait_for_bid(self, swap_client, bid_id):
logging.info('wait_for_bid %s', bid_id.hex())
for i in range(20):
time.sleep(1)
bids = swap_client.listBids()
for bid in bids:
if bid.bid_id == bid_id and bid.was_received:
return
raise ValueError('wait_for_bid timed out.')
def wait_for_in_progress(self, swap_client, bid_id, sent=False):
logging.info('wait_for_in_progress %s', bid_id.hex())
for i in range(20):
time.sleep(1)
swaps = swap_client.listSwapsInProgress()
for b in swaps:
if b[0] == bid_id:
return
raise ValueError('wait_for_in_progress timed out.')
def wait_for_bid_state(self, swap_client, bid_id, state, sent=False, seconds_for=30):
logging.info('wait_for_bid_state %s %s', bid_id.hex(), str(state))
for i in range(seconds_for):
time.sleep(1)
bid = swap_client.getBid(bid_id)
if bid.state >= state:
return
raise ValueError('wait_for_bid_state timed out.')
def wait_for_bid_tx_state(self, swap_client, bid_id, initiate_state, participate_state, seconds_for=30):
logging.info('wait_for_bid_tx_state %s %s %s', bid_id.hex(), str(initiate_state), str(participate_state))
for i in range(seconds_for):
time.sleep(1)
bid = swap_client.getBid(bid_id)
if (initiate_state is None or bid.initiate_txn_state == initiate_state) \
and (participate_state is None or bid.participate_txn_state == participate_state):
return
raise ValueError('wait_for_bid_tx_state timed out.')
def test_01_verifyrawtransaction(self):
txn = '0200000001eb6e5c4ebba4efa32f40c7314cad456a64008e91ee30b2dd0235ab9bb67fbdbb01000000ee47304402200956933242dde94f6cf8f195a470f8d02aef21ec5c9b66c5d3871594bdb74c9d02201d7e1b440de8f4da672d689f9e37e98815fb63dbc1706353290887eb6e8f7235012103dc1b24feb32841bc2f4375da91fa97834e5983668c2a39a6b7eadb60e7033f9d205a803b28fe2f86c17db91fa99d7ed2598f79b5677ffe869de2e478c0d1c02cc7514c606382012088a8201fe90717abb84b481c2a59112414ae56ec8acc72273642ca26cc7a5812fdc8f68876a914225fbfa4cb725b75e511810ac4d6f74069bdded26703520140b27576a914207eb66b2fd6ed9924d6217efc7fa7b38dfabe666888acffffffff01e0167118020000001976a9140044e188928710cecba8311f1cf412135b98145c88ac00000000'
prevout = {
'txid': 'bbbd7fb69bab3502ddb230ee918e00646a45ad4c31c7402fa3efa4bb4e5c6eeb',
'vout': 1,
'scriptPubKey': 'a9143d37191e8b864222d14952a14c85504677a0581d87',
'redeemScript': '6382012088a8201fe90717abb84b481c2a59112414ae56ec8acc72273642ca26cc7a5812fdc8f68876a914225fbfa4cb725b75e511810ac4d6f74069bdded26703520140b27576a914207eb66b2fd6ed9924d6217efc7fa7b38dfabe666888ac',
'amount': 1.0}
ro = partRpc('verifyrawtransaction {} "{}"'.format(txn, dumpje([prevout, ])))
assert(ro['inputs_valid'] is False)
assert(ro['validscripts'] == 1)
prevout['amount'] = 100.0
ro = partRpc('verifyrawtransaction {} "{}"'.format(txn, dumpje([prevout, ])))
assert(ro['inputs_valid'] is True)
assert(ro['validscripts'] == 1)
txn = 'a000000000000128e8ba6a28673f2ebb5fd983b27a791fd1888447a47638b3cd8bfdd3f54a6f1e0100000000a90040000101e0c69a3b000000001976a9146c0f1ea47ca2bf84ed87bf3aa284e18748051f5788ac04473044022026b01f3a90e46883949404141467b741cd871722a4aaae8ddc8c4d6ab6fb1c77022047a2f3be2dcbe4c51837d2d5e0329aaa8a13a8186b03186b127cc51185e4f3ab012103dc1b24feb32841bc2f4375da91fa97834e5983668c2a39a6b7eadb60e7033f9d0100606382012088a8201fe90717abb84b481c2a59112414ae56ec8acc72273642ca26cc7a5812fdc8f68876a914207eb66b2fd6ed9924d6217efc7fa7b38dfabe666703a90040b27576a914225fbfa4cb725b75e511810ac4d6f74069bdded26888ac'
prevout = {
'txid': '1e6f4af5d3fd8bcdb33876a4478488d11f797ab283d95fbb2e3f67286abae828',
'vout': 1,
'scriptPubKey': 'a914129aee070317bbbd57062288849e85cf57d15c2687',
'redeemScript': '6382012088a8201fe90717abb84b481c2a59112414ae56ec8acc72273642ca26cc7a5812fdc8f68876a914207eb66b2fd6ed9924d6217efc7fa7b38dfabe666703a90040b27576a914225fbfa4cb725b75e511810ac4d6f74069bdded26888ac',
'amount': 1.0}
ro = partRpc('verifyrawtransaction {} "{}"'.format(txn, dumpje([prevout, ])))
assert(ro['inputs_valid'] is False)
assert(ro['validscripts'] == 0) # Amount covered by signature
prevout['amount'] = 90.0
ro = partRpc('verifyrawtransaction {} "{}"'.format(txn, dumpje([prevout, ])))
assert(ro['inputs_valid'] is True)
assert(ro['validscripts'] == 1)
def test_02_part_ltc(self):
swap_clients = self.swap_clients
logging.info('---------- Test PART to LTC')
offer_id = swap_clients[0].postOffer(Coins.PART, Coins.LTC, 100 * COIN, 0.1 * COIN, 100 * COIN, SwapTypes.SELLER_FIRST)
self.wait_for_offer(swap_clients[1], offer_id)
offers = swap_clients[1].listOffers()
assert(len(offers) == 1)
for offer in offers:
if offer.offer_id == offer_id:
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
self.wait_for_bid(swap_clients[0], bid_id)
swap_clients[0].acceptBid(bid_id)
self.wait_for_in_progress(swap_clients[1], bid_id, sent=True)
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
self.wait_for_bid_state(swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, seconds_for=60)
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
js_1 = json.loads(urlopen('http://localhost:1801/json').read())
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
def test_03_ltc_part(self):
swap_clients = self.swap_clients
logging.info('---------- Test LTC to PART')
offer_id = swap_clients[1].postOffer(Coins.LTC, Coins.PART, 10 * COIN, 9.0 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST)
self.wait_for_offer(swap_clients[0], offer_id)
offers = swap_clients[0].listOffers()
for offer in offers:
if offer.offer_id == offer_id:
bid_id = swap_clients[0].postBid(offer_id, offer.amount_from)
self.wait_for_bid(swap_clients[1], bid_id)
swap_clients[1].acceptBid(bid_id)
self.wait_for_in_progress(swap_clients[0], bid_id, sent=True)
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, sent=True, seconds_for=60)
self.wait_for_bid_state(swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
js_1 = json.loads(urlopen('http://localhost:1801/json').read())
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
def test_04_ltc_btc(self):
swap_clients = self.swap_clients
logging.info('---------- Test LTC to BTC')
offer_id = swap_clients[0].postOffer(Coins.LTC, Coins.BTC, 10 * COIN, 0.1 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST)
self.wait_for_offer(swap_clients[1], offer_id)
offers = swap_clients[1].listOffers()
for offer in offers:
if offer.offer_id == offer_id:
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
self.wait_for_bid(swap_clients[0], bid_id)
swap_clients[0].acceptBid(bid_id)
self.wait_for_in_progress(swap_clients[1], bid_id, sent=True)
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
self.wait_for_bid_state(swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, seconds_for=60)
js_0bid = json.loads(urlopen('http://localhost:1800/json/bids/{}'.format(bid_id.hex())).read())
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
js_1 = json.loads(urlopen('http://localhost:1801/json').read())
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
def test_05_refund(self):
# Seller submits initiate txn, buyer doesn't respond
swap_clients = self.swap_clients
logging.info('---------- Test refund, LTC to BTC')
offer_id = swap_clients[0].postOffer(Coins.LTC, Coins.BTC, 10 * COIN, 0.1 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST,
SEQUENCE_LOCK_BLOCKS, 10)
self.wait_for_offer(swap_clients[1], offer_id)
offers = swap_clients[1].listOffers()
for offer in offers:
if offer.offer_id == offer_id:
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
self.wait_for_bid(swap_clients[0], bid_id)
swap_clients[1].abandonBid(bid_id)
swap_clients[0].acceptBid(bid_id)
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
self.wait_for_bid_state(swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, seconds_for=60)
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
js_1 = json.loads(urlopen('http://localhost:1801/json').read())
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
def test_06_self_bid(self):
swap_clients = self.swap_clients
logging.info('---------- Test same client, BTC to LTC')
js_0_before = json.loads(urlopen('http://localhost:1800/json').read())
offer_id = swap_clients[0].postOffer(Coins.LTC, Coins.BTC, 10 * COIN, 10 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST)
self.wait_for_offer(swap_clients[0], offer_id)
offers = swap_clients[0].listOffers()
for offer in offers:
if offer.offer_id == offer_id:
bid_id = swap_clients[0].postBid(offer_id, offer.amount_from)
self.wait_for_bid(swap_clients[0], bid_id)
swap_clients[0].acceptBid(bid_id)
self.wait_for_bid_tx_state(swap_clients[0], bid_id, TxStates.TX_REDEEMED, TxStates.TX_REDEEMED, seconds_for=60)
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert(js_0['num_recv_bids'] == js_0_before['num_recv_bids'] + 1 and js_0['num_sent_bids'] == js_0_before['num_sent_bids'] + 1)
def pass_99_delay(self):
global stop_test
logging.info('Delay')
for i in range(60 * 5):
if stop_test:
break
time.sleep(1)
print('delay', i)
stop_test = True
if __name__ == '__main__':
unittest.main()