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