commit
e242f50b2b
26 changed files with 5035 additions and 0 deletions
@ -0,0 +1,6 @@ |
||||
old/ |
||||
*.pyc |
||||
__pycache__ |
||||
/dist/ |
||||
/*.egg-info |
||||
/*.egg |
@ -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" |
@ -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 |
@ -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. |
@ -0,0 +1,7 @@ |
||||
# Include the README |
||||
include *.md |
||||
|
||||
# Include the license file |
||||
include LICENSE.txt |
||||
|
||||
recursive-include doc * |
@ -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 |
@ -0,0 +1,3 @@ |
||||
name = "basicswap" |
||||
|
||||
__version__ = "0.0.1" |
File diff suppressed because it is too large
Load Diff
@ -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, |
||||
} |
||||
} |
||||
} |
@ -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') |
@ -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() |
@ -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 |
@ -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; |
||||
} |
@ -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) |
@ -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 |
@ -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 |
@ -0,0 +1 @@ |
||||
name = "bin" |
@ -0,0 +1 @@ |
||||
basicswap_run.py |
@ -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,0 +1,5 @@ |
||||
* |
||||
!.gitignore |
||||
!basicswap/basicswap.json |
||||
!particl/particl.conf |
||||
!litecoin/litecoin.conf |
@ -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 |
||||
|
@ -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" |
||||
) |
@ -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 |
@ -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() |
@ -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() |
Loading…
Reference in new issue