mirror of
https://github.com/basicswap/basicswap.git
synced 2024-11-16 15:58:17 +00:00
Add to Github
This commit is contained in:
commit
e242f50b2b
26 changed files with 5035 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
old/
|
||||
*.pyc
|
||||
__pycache__
|
||||
/dist/
|
||||
/*.egg-info
|
||||
/*.egg
|
46
.travis.yml
Normal file
46
.travis.yml
Normal file
|
@ -0,0 +1,46 @@
|
|||
dist: xenial
|
||||
os: linux
|
||||
language: python
|
||||
python: '3.6'
|
||||
cache:
|
||||
directories:
|
||||
- /opt/binaries
|
||||
stages:
|
||||
- lint
|
||||
env:
|
||||
global:
|
||||
- PARTICL_BINDIR=/opt/binaries/particl-0.18.0.12/bin/
|
||||
- BITCOIN_BINDIR=/opt/binaries/bitcoin-0.18.0/bin/
|
||||
- LITECOIN_BINDIR=/opt/binaries/litecoin-0.17.1/bin/
|
||||
before_script:
|
||||
- if [ ! -d "/opt/binaries" ]; then mkdir -p "/opt/binaries" ; fi
|
||||
- if [ ! -d "$BITCOIN_BINDIR" ]; then cd "/opt/binaries" && wget https://bitcoincore.org/bin/bitcoin-core-0.18.0/bitcoin-0.18.0-x86_64-linux-gnu.tar.gz && tar xvf bitcoin-0.18.0-x86_64-linux-gnu.tar.gz ; fi
|
||||
- if [ ! -d "$LITECOIN_BINDIR" ]; then cd "/opt/binaries" && wget https://download.litecoin.org/litecoin-0.17.1/linux/litecoin-0.17.1-x86_64-linux-gnu.tar.gz && tar xvf litecoin-0.17.1-x86_64-linux-gnu.tar.gz ; fi
|
||||
- if [ ! -d "$PARTICL_BINDIR" ]; then cd "/opt/binaries" && wget https://github.com/particl/particl-core/releases/download/v0.18.0.12/particl-0.18.0.12-x86_64-linux-gnu_nousb.tar.gz && tar xvf particl-0.18.0.12-x86_64-linux-gnu_nousb.tar.gz ; fi
|
||||
- cd
|
||||
script:
|
||||
- cd $TRAVIS_BUILD_DIR
|
||||
- export PARTICL_BINDIR=/opt/binaries/particl-0.18.0.12/bin/
|
||||
- export BITCOIN_BINDIR=/opt/binaries/bitcoin-0.18.0/bin/
|
||||
- export LITECOIN_BINDIR=/opt/binaries/litecoin-0.17.1/bin/
|
||||
- python setup.py test
|
||||
after_success:
|
||||
- echo "End test"
|
||||
jobs:
|
||||
include:
|
||||
- stage: lint
|
||||
env:
|
||||
cache: false
|
||||
language: python
|
||||
python: '3.6'
|
||||
before_install:
|
||||
- sudo apt-get install -y wget gnupg
|
||||
install:
|
||||
- travis_retry pip install flake8==3.5.0
|
||||
- travis_retry pip install codespell==1.15.0
|
||||
before_script:
|
||||
script:
|
||||
- PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841 --exclude=key.py,messages_pb2.py
|
||||
- codespell --check-filenames --disable-colors --quiet-level=7 -S .git
|
||||
after_success:
|
||||
- echo "End lint"
|
45
Dockerfile
Normal file
45
Dockerfile
Normal file
|
@ -0,0 +1,45 @@
|
|||
FROM ubuntu:18.10
|
||||
|
||||
ENV PARTICL_DATADIR="/coindata/particl" \
|
||||
PARTICL_BINDIR="/opt/particl" \
|
||||
LITECOIN_BINDIR="/opt/litecoin" \
|
||||
DATADIRS="/coindata"
|
||||
|
||||
RUN apt-get update; \
|
||||
apt-get install -y wget python3-pip curl gnupg unzip protobuf-compiler;
|
||||
|
||||
RUN cd ~; \
|
||||
wget https://github.com/particl/coldstakepool/archive/master.zip; \
|
||||
unzip master.zip; \
|
||||
cd coldstakepool-master; \
|
||||
pip3 install .; \
|
||||
pip3 install pyzmq plyvel protobuf;
|
||||
|
||||
RUN PARTICL_VERSION=0.18.0.12 PARTICL_VERSION_TAG= PARTICL_ARCH=x86_64-linux-gnu_nousb.tar.gz coldstakepool-prepare --update_core; \
|
||||
wget https://download.litecoin.org/litecoin-0.17.1/linux/litecoin-0.17.1-x86_64-linux-gnu.tar.gz; \
|
||||
mkdir -p ${LITECOIN_BINDIR}; \
|
||||
tar -xvf litecoin-0.17.1-x86_64-linux-gnu.tar.gz -C ${LITECOIN_BINDIR} --strip-components 2 litecoin-0.17.1/bin/litecoind litecoin-0.17.1/bin/litecoin-cli
|
||||
|
||||
# Change to git clone
|
||||
COPY . /opt/basicswap
|
||||
|
||||
RUN ls /opt/basicswap; \
|
||||
cd /opt/basicswap; \
|
||||
protoc -I=basicswap --python_out=basicswap basicswap/messages.proto; \
|
||||
pip3 install .;
|
||||
|
||||
RUN useradd -ms /bin/bash user; \
|
||||
mkdir /coindata && chown user /coindata
|
||||
|
||||
USER user
|
||||
WORKDIR /home/user
|
||||
|
||||
# Expose html port
|
||||
EXPOSE 12700
|
||||
|
||||
ENV LANG C.UTF-8
|
||||
|
||||
VOLUME /coindata
|
||||
|
||||
ENTRYPOINT ["basicswap-run", "-datadir=/coindata/basicswap"]
|
||||
CMD
|
20
LICENSE.txt
Normal file
20
LICENSE.txt
Normal file
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
Copyright (c) 2019 tecnovert
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
7
MANIFEST.in
Normal file
7
MANIFEST.in
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Include the README
|
||||
include *.md
|
||||
|
||||
# Include the license file
|
||||
include LICENSE.txt
|
||||
|
||||
recursive-include doc *
|
69
README.md
Normal file
69
README.md
Normal file
|
@ -0,0 +1,69 @@
|
|||
|
||||
# Particl Atomic Swap - Proof of concept
|
||||
|
||||
## Overview
|
||||
|
||||
Simple atomic swap experiment, doesn't have many interesting features yet.
|
||||
Not ready for real world use.
|
||||
|
||||
Uses Particl secure messaging and Decred style atomic swaps.
|
||||
|
||||
The Particl node is used to hold the keys and sign for the swap transactions.
|
||||
Other nodes can be run in pruned mode.
|
||||
A node must be run for each coin type traded.
|
||||
In the future it should be possible to use data from explorers instead of running a node.
|
||||
|
||||
## Currently a work in progress
|
||||
|
||||
Not ready for real-world use.
|
||||
|
||||
Features still required (of many):
|
||||
- Cached addresses must be regenerated after use.
|
||||
- Option to lookup data from public explorers / nodes.
|
||||
- Load active bids from db at startup
|
||||
- Ability to swap coin-types without running nodes for all coin-types
|
||||
- More swap protocols
|
||||
- Method to load mnemonic into Particl.
|
||||
|
||||
|
||||
## Seller first protocol:
|
||||
|
||||
Seller sends the 1st transaction.
|
||||
|
||||
1. Seller posts offer.
|
||||
- smsg from seller to network
|
||||
coin-from
|
||||
coin-to
|
||||
amount-from
|
||||
rate
|
||||
min-amount
|
||||
time-valid
|
||||
|
||||
2. Buyer posts bid:
|
||||
- smsg from buyer to seller
|
||||
offerid
|
||||
amount
|
||||
proof-of-funds
|
||||
address_to_buyer
|
||||
time-valid
|
||||
|
||||
3. Seller accepts bid:
|
||||
- verifies proof-of-funds
|
||||
- generates secret
|
||||
- submits initiate tx to coin-from network
|
||||
- smsg from seller to buyer
|
||||
txid
|
||||
initiatescript (includes pkhash_to_seller as the pkhash_refund)
|
||||
|
||||
4. Buyer participates:
|
||||
- inspects initiate tx in coin-from network
|
||||
- submits participate tx in coin-to network
|
||||
|
||||
5. Seller redeems:
|
||||
- constructs participatescript
|
||||
- inspects participate tx in coin-to network
|
||||
- redeems from participate tx revealing secret
|
||||
|
||||
6. Buyer redeems:
|
||||
- scans coin-to network for seller-redeem tx
|
||||
- redeems from initiate tx with revealed secret
|
3
basicswap/__init__.py
Normal file
3
basicswap/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
name = "basicswap"
|
||||
|
||||
__version__ = "0.0.1"
|
2151
basicswap/basicswap.py
Normal file
2151
basicswap/basicswap.py
Normal file
File diff suppressed because it is too large
Load diff
120
basicswap/chainparams.py
Normal file
120
basicswap/chainparams.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
from enum import IntEnum
|
||||
from .util import (
|
||||
COIN,
|
||||
)
|
||||
|
||||
|
||||
class Coins(IntEnum):
|
||||
PART = 1
|
||||
BTC = 2
|
||||
LTC = 3
|
||||
# DCR = 4
|
||||
|
||||
|
||||
chainparams = {
|
||||
Coins.PART: {
|
||||
'name': 'particl',
|
||||
'ticker': 'PART',
|
||||
'message_magic': 'Bitcoin Signed Message:\n',
|
||||
'mainnet': {
|
||||
'rpcport': 51735,
|
||||
'pubkey_address': 0x38,
|
||||
'script_address': 0x3c,
|
||||
'key_prefix': 0x6c,
|
||||
'hrp': 'pw',
|
||||
'bip44': 44,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 51935,
|
||||
'pubkey_address': 0x76,
|
||||
'script_address': 0x7a,
|
||||
'key_prefix': 0x2e,
|
||||
'hrp': 'tpw',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
},
|
||||
'regtest': {
|
||||
'rpcport': 51936,
|
||||
'pubkey_address': 0x76,
|
||||
'script_address': 0x7a,
|
||||
'key_prefix': 0x2e,
|
||||
'hrp': 'rtpw',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
}
|
||||
},
|
||||
Coins.BTC: {
|
||||
'name': 'bitcoin',
|
||||
'ticker': 'BTC',
|
||||
'message_magic': 'Bitcoin Signed Message:\n',
|
||||
'mainnet': {
|
||||
'rpcport': 8332,
|
||||
'pubkey_address': 0,
|
||||
'script_address': 5,
|
||||
'hrp': 'bc',
|
||||
'bip44': 0,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 18332,
|
||||
'pubkey_address': 111,
|
||||
'script_address': 196,
|
||||
'hrp': 'tb',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
},
|
||||
'regtest': {
|
||||
'rpcport': 18443,
|
||||
'pubkey_address': 111,
|
||||
'script_address': 196,
|
||||
'hrp': 'bcrt',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
}
|
||||
},
|
||||
Coins.LTC: {
|
||||
'name': 'litecoin',
|
||||
'ticker': 'LTC',
|
||||
'message_magic': 'Litecoin Signed Message:\n',
|
||||
'mainnet': {
|
||||
'rpcport': 9332,
|
||||
'pubkey_address': 48,
|
||||
'script_address': 50,
|
||||
'hrp': 'ltc',
|
||||
'bip44': 2,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
},
|
||||
'testnet': {
|
||||
'rpcport': 19332,
|
||||
'pubkey_address': 111,
|
||||
'script_address': 58,
|
||||
'hrp': 'tltc',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
},
|
||||
'regtest': {
|
||||
'rpcport': 19443,
|
||||
'pubkey_address': 111,
|
||||
'script_address': 58,
|
||||
'hrp': 'rltc',
|
||||
'bip44': 1,
|
||||
'min_amount': 1000,
|
||||
'max_amount': 100000 * COIN,
|
||||
}
|
||||
}
|
||||
}
|
24
basicswap/config.py
Normal file
24
basicswap/config.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
|
||||
DATADIRS = os.path.expanduser(os.getenv('DATADIRS', '/tmp/basicswap'))
|
||||
|
||||
PARTICL_BINDIR = os.path.expanduser(os.getenv('PARTICL_BINDIR', ''))
|
||||
PARTICLD = os.getenv('PARTICLD', 'particld')
|
||||
PARTICL_CLI = os.getenv('PARTICL_CLI', 'particl-cli')
|
||||
PARTICL_TX = os.getenv('PARTICL_TX', 'particl-tx')
|
||||
|
||||
BITCOIN_BINDIR = os.path.expanduser(os.getenv('BITCOIN_BINDIR', ''))
|
||||
BITCOIND = os.getenv('BITCOIND', 'bitcoind')
|
||||
BITCOIN_CLI = os.getenv('BITCOIN_CLI', 'bitcoin-cli')
|
||||
BITCOIN_TX = os.getenv('BITCOIN_TX', 'bitcoin-tx')
|
||||
|
||||
LITECOIN_BINDIR = os.path.expanduser(os.getenv('LITECOIN_BINDIR', ''))
|
||||
LITECOIND = os.getenv('LITECOIND', 'litecoind')
|
||||
LITECOIN_CLI = os.getenv('LITECOIN_CLI', 'litecoin-cli')
|
||||
LITECOIN_TX = os.getenv('LITECOIN_TX', 'litecoin-tx')
|
542
basicswap/http_server.py
Normal file
542
basicswap/http_server.py
Normal file
|
@ -0,0 +1,542 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import struct
|
||||
import traceback
|
||||
import threading
|
||||
import http.client
|
||||
import urllib.parse
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from .util import (
|
||||
COIN,
|
||||
format8,
|
||||
)
|
||||
from .chainparams import (
|
||||
chainparams,
|
||||
Coins,
|
||||
)
|
||||
from .basicswap import (
|
||||
SwapTypes,
|
||||
getOfferState,
|
||||
getBidState,
|
||||
getTxState,
|
||||
getLockName,
|
||||
)
|
||||
|
||||
|
||||
def getCoinName(c):
|
||||
return chainparams[c]['name'].capitalize()
|
||||
|
||||
|
||||
def html_content_start(title, h2=None):
|
||||
content = '<!DOCTYPE html><html lang="en">\n<head>' \
|
||||
+ '<meta charset="UTF-8">' \
|
||||
+ '<title>' + title + '</title></head>' \
|
||||
+ '<body>'
|
||||
if h2 is not None:
|
||||
content += '<h2>' + h2 + '</h2>'
|
||||
return content
|
||||
|
||||
|
||||
class HttpHandler(BaseHTTPRequestHandler):
|
||||
def page_error(self, error_str):
|
||||
content = html_content_start('BasicSwap Error') \
|
||||
+ '<p>Error: ' + error_str + '</p>' \
|
||||
+ '<p><a href=\'/\'>home</a></p></body></html>'
|
||||
return bytes(content, 'UTF-8')
|
||||
|
||||
def js_error(self, error_str):
|
||||
error_str_json = json.dumps({'error': error_str})
|
||||
return bytes(error_str_json, 'UTF-8')
|
||||
|
||||
def js_wallets(self, url_split):
|
||||
return bytes(json.dumps(self.server.swap_client.getWalletsInfo()), 'UTF-8')
|
||||
|
||||
def js_offers(self, url_split):
|
||||
assert(False), 'TODO'
|
||||
return bytes(json.dumps(self.server.swap_client.listOffers()), 'UTF-8')
|
||||
|
||||
def js_sentoffers(self, url_split):
|
||||
assert(False), 'TODO'
|
||||
return bytes(json.dumps(self.server.swap_client.listOffers(sent=True)), 'UTF-8')
|
||||
|
||||
def js_bids(self, url_split):
|
||||
if len(url_split) > 3:
|
||||
bid_id = bytes.fromhex(url_split[3])
|
||||
assert(len(bid_id) == 28)
|
||||
return bytes(json.dumps(self.server.swap_client.viewBid(bid_id)), 'UTF-8')
|
||||
return bytes(json.dumps(self.server.swap_client.listBids()), 'UTF-8')
|
||||
|
||||
def js_sentbids(self, url_split):
|
||||
return bytes(json.dumps(self.server.swap_client.listBids(sent=True)), 'UTF-8')
|
||||
|
||||
def js_index(self, url_split):
|
||||
return bytes(json.dumps(self.server.swap_client.getSummary()), 'UTF-8')
|
||||
|
||||
def page_active(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
|
||||
content = html_content_start(self.server.title, self.server.title) \
|
||||
+ '<h3>Active Swaps</h3>'
|
||||
|
||||
active_swaps = swap_client.listSwapsInProgress()
|
||||
|
||||
content += '<table>'
|
||||
content += '<tr><th>Bid ID</th><th>Offer ID</th><th>Bid Status</th></tr>'
|
||||
for s in active_swaps:
|
||||
content += '<tr><td><a href=/bid/{0}>{0}</a></td><td><a href=/offer/{1}>{1}</a></td><td>{2}</td></tr>'.format(s[0].hex(), s[1], getBidState(s[2]))
|
||||
content += '</table>'
|
||||
|
||||
content += '<p><a href="/">home</a></p></body></html>'
|
||||
return bytes(content, 'UTF-8')
|
||||
|
||||
def page_wallets(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
|
||||
content = html_content_start(self.server.title, self.server.title) \
|
||||
+ '<h3>Wallets</h3>'
|
||||
|
||||
if post_string != '':
|
||||
form_data = urllib.parse.parse_qs(post_string)
|
||||
form_id = form_data[b'formid'][0].decode('utf-8')
|
||||
if self.server.last_form_id.get('wallets', None) == form_id:
|
||||
content += '<p>Prevented double submit for form {}.</p>'.format(form_id)
|
||||
else:
|
||||
self.server.last_form_id['wallets'] = form_id
|
||||
|
||||
for c in Coins:
|
||||
cid = str(int(c))
|
||||
|
||||
if bytes('newaddr_' + cid, 'utf-8') in form_data:
|
||||
swap_client.cacheNewAddressForCoin(c)
|
||||
|
||||
if bytes('withdraw_' + cid, 'utf-8') in form_data:
|
||||
value = form_data[bytes('amt_' + cid, 'utf-8')][0].decode('utf-8')
|
||||
address = form_data[bytes('to_' + cid, 'utf-8')][0].decode('utf-8')
|
||||
txid = swap_client.withdrawCoin(c, value, address)
|
||||
ticker = swap_client.getTicker(c)
|
||||
content += '<p>Withdrew {} {} to address {}<br/>In txid: {}</p>'.format(value, ticker, address, txid)
|
||||
|
||||
wallets = swap_client.getWalletsInfo()
|
||||
|
||||
content += '<form method="post">'
|
||||
for k, w in wallets.items():
|
||||
cid = str(int(k))
|
||||
content += '<h4>' + w['name'] + '</h4>' \
|
||||
+ '<table>' \
|
||||
+ '<tr><td>Balance:</td><td>' + str(w['balance']) + '</td></tr>' \
|
||||
+ '<tr><td>Blocks:</td><td>' + str(w['blocks']) + '</td></tr>' \
|
||||
+ '<tr><td>Synced:</td><td>' + str(w['synced']) + '</td></tr>' \
|
||||
+ '<tr><td><input type="submit" name="newaddr_' + cid + '" value="Deposit Address"></td><td>' + str(w['deposit_address']) + '</td></tr>' \
|
||||
+ '<tr><td><input type="submit" name="withdraw_' + cid + '" value="Withdraw"></td><td>Amount: <input type="text" name="amt_' + cid + '"></td><td>Address: <input type="text" name="to_' + cid + '"></td></tr>' \
|
||||
+ '</table>'
|
||||
|
||||
content += '<input type="hidden" name="formid" value="' + os.urandom(8).hex() + '"></form>'
|
||||
content += '<p><a href="/">home</a></p></body></html>'
|
||||
return bytes(content, 'UTF-8')
|
||||
|
||||
def make_coin_select(self, name, coins):
|
||||
s = '<select name="' + name + '"><option value="-1">-- Select Coin --</option>'
|
||||
for c in coins:
|
||||
s += '<option value="{}">{}</option>'.format(*c)
|
||||
s += '</select>'
|
||||
return s
|
||||
|
||||
def page_newoffer(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
|
||||
content = html_content_start(self.server.title, self.server.title) \
|
||||
+ '<h3>New Offer</h3>'
|
||||
|
||||
if post_string != '':
|
||||
form_data = urllib.parse.parse_qs(post_string)
|
||||
form_id = form_data[b'formid'][0].decode('utf-8')
|
||||
if self.server.last_form_id.get('newoffer', None) == form_id:
|
||||
content += '<p>Prevented double submit for form {}.</p>'.format(form_id)
|
||||
else:
|
||||
self.server.last_form_id['newoffer'] = form_id
|
||||
|
||||
try:
|
||||
coin_from = Coins(int(form_data[b'coin_from'][0]))
|
||||
except Exception:
|
||||
raise ValueError('Unknown Coin From')
|
||||
try:
|
||||
coin_to = Coins(int(form_data[b'coin_to'][0]))
|
||||
except Exception:
|
||||
raise ValueError('Unknown Coin From')
|
||||
|
||||
value_from = int(float(form_data[b'amt_from'][0]) * COIN)
|
||||
value_to = int(float(form_data[b'amt_to'][0]) * COIN)
|
||||
min_bid = int(value_from)
|
||||
rate = int((value_to / value_from) * COIN)
|
||||
autoaccept = True if b'autoaccept' in form_data else False
|
||||
# TODO: More accurate rate
|
||||
# assert(value_to == (value_from * rate) // COIN)
|
||||
offer_id = swap_client.postOffer(coin_from, coin_to, value_from, rate, min_bid, SwapTypes.SELLER_FIRST, auto_accept_bids=autoaccept)
|
||||
content += '<p><a href="/offer/' + offer_id.hex() + '">Sent Offer ' + offer_id.hex() + '</a><br/>Rate: ' + format8(rate) + '</p>'
|
||||
|
||||
coins = []
|
||||
|
||||
for k, v in swap_client.coin_clients.items():
|
||||
if v['connection_type'] == 'rpc':
|
||||
coins.append((int(k), getCoinName(k)))
|
||||
|
||||
content += '<form method="post">'
|
||||
|
||||
content += '<table>'
|
||||
content += '<tr><td>Coin From</td><td>' + self.make_coin_select('coin_from', coins) + '</td><td>Amount From</td><td><input type="text" name="amt_from"></td></tr>'
|
||||
content += '<tr><td>Coin To</td><td>' + self.make_coin_select('coin_to', coins) + '</td><td>Amount To</td><td><input type="text" name="amt_to"></td></tr>'
|
||||
content += '<tr><td>Auto Accept Bids</td><td><input type="checkbox" name="autoaccept" value="aa" checked></td></tr>'
|
||||
content += '</table>'
|
||||
|
||||
content += '<input type="submit" value="Submit">'
|
||||
content += '<input type="hidden" name="formid" value="' + os.urandom(8).hex() + '"></form>'
|
||||
content += '<p><a href="/">home</a></p></body></html>'
|
||||
return bytes(content, 'UTF-8')
|
||||
|
||||
def page_offer(self, url_split, post_string):
|
||||
assert(len(url_split) > 2), 'Offer ID not specified'
|
||||
try:
|
||||
offer_id = bytes.fromhex(url_split[2])
|
||||
assert(len(offer_id) == 28)
|
||||
except Exception:
|
||||
raise ValueError('Bad offer ID')
|
||||
swap_client = self.server.swap_client
|
||||
offer = swap_client.getOffer(offer_id)
|
||||
assert(offer), 'Unknown offer ID'
|
||||
|
||||
content = html_content_start(self.server.title, self.server.title) \
|
||||
+ '<h3>Offer: ' + offer_id.hex() + '</h3>'
|
||||
|
||||
if post_string != '':
|
||||
form_data = urllib.parse.parse_qs(post_string)
|
||||
form_id = form_data[b'formid'][0].decode('utf-8')
|
||||
if self.server.last_form_id.get('offer', None) == form_id:
|
||||
content += '<p>Prevented double submit for form {}.</p>'.format(form_id)
|
||||
else:
|
||||
self.server.last_form_id['offer'] = form_id
|
||||
bid_id = swap_client.postBid(offer_id, offer.amount_from)
|
||||
content += '<p><a href="/bid/' + bid_id.hex() + '">Sent Bid ' + bid_id.hex() + '</a></p>'
|
||||
|
||||
coin_from = Coins(offer.coin_from)
|
||||
coin_to = Coins(offer.coin_to)
|
||||
ticker_from = swap_client.getTicker(coin_from)
|
||||
ticker_to = swap_client.getTicker(coin_to)
|
||||
|
||||
tr = '<tr><td>{}</td><td>{}</td></tr>'
|
||||
content += '<table>'
|
||||
content += tr.format('Offer State', getOfferState(offer.state))
|
||||
content += tr.format('Coin From', getCoinName(coin_from))
|
||||
content += tr.format('Coin To', getCoinName(coin_to))
|
||||
content += tr.format('Amount From', format8(offer.amount_from) + ' ' + ticker_from)
|
||||
content += tr.format('Amount To', format8((offer.amount_from * offer.rate) // COIN) + ' ' + ticker_to)
|
||||
content += tr.format('Rate', format8(offer.rate) + ' ' + ticker_from + '/' + ticker_to)
|
||||
content += tr.format('Script Lock Type', getLockName(offer.lock_type))
|
||||
content += tr.format('Script Lock Value', offer.lock_value)
|
||||
content += tr.format('Address From', offer.addr_from)
|
||||
content += tr.format('Created At', time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(offer.created_at)))
|
||||
content += tr.format('Expired At', time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(offer.expire_at)))
|
||||
content += tr.format('Sent', 'True' if offer.was_sent else 'False')
|
||||
|
||||
if offer.was_sent:
|
||||
content += tr.format('Auto Accept Bids', 'True' if offer.auto_accept_bids else 'False')
|
||||
content += '</table>'
|
||||
|
||||
bids = swap_client.listBids(offer_id=offer_id)
|
||||
|
||||
content += '<h4>Bids</h4><table>'
|
||||
content += '<tr><th>Bid ID</th><th>Bid Amount</th><th>Bid Status</th><th>ITX Status</th><th>PTX Status</th></tr>'
|
||||
for b in bids:
|
||||
content += '<tr><td><a href=/bid/{0}>{0}</a></td><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td></tr>'.format(b.bid_id.hex(), format8(b.amount), getBidState(b.state), getTxState(b.initiate_txn_state), getTxState(b.participate_txn_state))
|
||||
content += '</table>'
|
||||
|
||||
content += '<form method="post">'
|
||||
content += '<input type="submit" value="Send Bid">'
|
||||
content += '<input type="hidden" name="formid" value="' + os.urandom(8).hex() + '"></form>'
|
||||
content += '<p><a href="/">home</a></p></body></html>'
|
||||
return bytes(content, 'UTF-8')
|
||||
|
||||
def page_offers(self, url_split, sent=False):
|
||||
swap_client = self.server.swap_client
|
||||
offers = swap_client.listOffers(sent)
|
||||
|
||||
content = html_content_start(self.server.title, self.server.title) \
|
||||
+ '<h3>' + ('Sent ' if sent else '') + 'Offers</h3>'
|
||||
|
||||
content += '<table>'
|
||||
content += '<tr><th>Offer ID</th><th>Coin From</th><th>Coin To</th><th>Amount From</th><th>Amount To</th><th>Rate</th></tr>'
|
||||
for o in offers:
|
||||
coin_from_name = getCoinName(Coins(o.coin_from))
|
||||
coin_to_name = getCoinName(Coins(o.coin_to))
|
||||
amount_to = (o.amount_from * o.rate) // COIN
|
||||
content += '<tr><td><a href=/offer/{0}>{0}</a></td><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td><td>{5}</td></tr>'.format(o.offer_id.hex(), coin_from_name, coin_to_name, format8(o.amount_from), format8(amount_to), format8(o.rate))
|
||||
|
||||
content += '</table>'
|
||||
content += '<p><a href="/">home</a></p></body></html>'
|
||||
return bytes(content, 'UTF-8')
|
||||
|
||||
def page_bid(self, url_split, post_string):
|
||||
assert(len(url_split) > 2), 'Bid ID not specified'
|
||||
try:
|
||||
bid_id = bytes.fromhex(url_split[2])
|
||||
assert(len(bid_id) == 28)
|
||||
except Exception:
|
||||
raise ValueError('Bad bid ID')
|
||||
swap_client = self.server.swap_client
|
||||
|
||||
content = html_content_start(self.server.title, self.server.title) \
|
||||
+ '<h3>Bid: ' + bid_id.hex() + '</h3>'
|
||||
|
||||
show_txns = False
|
||||
if post_string != '':
|
||||
form_data = urllib.parse.parse_qs(post_string)
|
||||
form_id = form_data[b'formid'][0].decode('utf-8')
|
||||
if self.server.last_form_id.get('bid', None) == form_id:
|
||||
content += '<p>Prevented double submit for form {}.</p>'.format(form_id)
|
||||
else:
|
||||
self.server.last_form_id['bid'] = form_id
|
||||
if b'abandon_bid' in form_data:
|
||||
try:
|
||||
swap_client.abandonBid(bid_id)
|
||||
content += '<p>Bid abandoned</p>'
|
||||
except Exception as e:
|
||||
content += '<p>Error' + str(e) + '</p>'
|
||||
if b'accept_bid' in form_data:
|
||||
try:
|
||||
swap_client.acceptBid(bid_id)
|
||||
content += '<p>Bid accepted</p>'
|
||||
except Exception as e:
|
||||
content += '<p>Error' + str(e) + '</p>'
|
||||
if b'show_txns' in form_data:
|
||||
show_txns = True
|
||||
|
||||
bid, offer = swap_client.getBidAndOffer(bid_id)
|
||||
assert(bid), 'Unknown bid ID'
|
||||
|
||||
coin_from = Coins(offer.coin_from)
|
||||
coin_to = Coins(offer.coin_to)
|
||||
ticker_from = swap_client.getTicker(coin_from)
|
||||
ticker_to = swap_client.getTicker(coin_to)
|
||||
|
||||
tr = '<tr><td>{}</td><td>{}</td></tr>'
|
||||
content += '<table>'
|
||||
|
||||
content += tr.format('Swap', format8(bid.amount) + ' ' + ticker_from + ' for ' + format8((bid.amount * offer.rate) // COIN) + ' ' + ticker_to)
|
||||
content += tr.format('Bid State', getBidState(bid.state))
|
||||
content += tr.format('ITX State', getTxState(bid.initiate_txn_state))
|
||||
content += tr.format('PTX State', getTxState(bid.participate_txn_state))
|
||||
content += tr.format('Offer', '<a href="/offer/' + bid.offer_id.hex() + '">' + bid.offer_id.hex() + '</a>')
|
||||
content += tr.format('Address From', bid.bid_addr)
|
||||
content += tr.format('Proof of Funds', bid.proof_address)
|
||||
content += tr.format('Created At', time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(bid.created_at)))
|
||||
content += tr.format('Expired At', time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(bid.expire_at)))
|
||||
content += tr.format('Sent', 'True' if bid.was_sent else 'False')
|
||||
content += tr.format('Received', 'True' if bid.was_received else 'False')
|
||||
content += tr.format('Initiate Tx', 'None' if not bid.initiate_txid else bid.initiate_txid.hex())
|
||||
content += tr.format('Initiate Conf', 'None' if not bid.initiate_txn_conf else bid.initiate_txn_conf)
|
||||
content += tr.format('Participate Tx', 'None' if not bid.participate_txid else bid.participate_txid.hex())
|
||||
content += tr.format('Participate Conf', 'None' if not bid.participate_txn_conf else bid.participate_txn_conf)
|
||||
if show_txns:
|
||||
content += tr.format('Initiate Tx Refund', 'None' if not bid.initiate_txn_refund else bid.initiate_txn_refund.hex())
|
||||
content += tr.format('Participate Tx Refund', 'None' if not bid.participate_txn_refund else bid.participate_txn_refund.hex())
|
||||
content += tr.format('Initiate Spend Tx', 'None' if not bid.initiate_spend_txid else (bid.initiate_spend_txid.hex() + ' {}'.format(bid.initiate_spend_n)))
|
||||
content += tr.format('Participate Spend Tx', 'None' if not bid.participate_spend_txid else (bid.participate_spend_txid.hex() + ' {}'.format(bid.participate_spend_n)))
|
||||
content += '</table>'
|
||||
|
||||
content += '<form method="post">'
|
||||
if bid.was_received:
|
||||
content += '<input name="accept_bid" type="submit" value="Accept Bid"><br/>'
|
||||
content += '<input name="abandon_bid" type="submit" value="Abandon Bid">'
|
||||
content += '<input name="show_txns" type="submit" value="Show More Info">'
|
||||
content += '<input type="hidden" name="formid" value="' + os.urandom(8).hex() + '"></form>'
|
||||
|
||||
content += '<h4>Old States</h4><table><tr><th>State</th><th>Set At</th></tr>'
|
||||
num_states = len(bid.states) // 12
|
||||
for i in range(num_states):
|
||||
up = struct.unpack_from('<iq', bid.states[i * 12:(i + 1) * 12])
|
||||
content += '<tr><td>Bid {}</td><td>{}</td></tr>'.format(getBidState(up[0]), time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(up[1])))
|
||||
if bid.initiate_txn_states is not None:
|
||||
num_states = len(bid.initiate_txn_states) // 12
|
||||
for i in range(num_states):
|
||||
up = struct.unpack_from('<iq', bid.initiate_txn_states[i * 12:(i + 1) * 12])
|
||||
content += '<tr><td>ITX {}</td><td>{}</td></tr>'.format(getTxState(up[0]), time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(up[1])))
|
||||
if bid.participate_txn_states is not None:
|
||||
num_states = len(bid.participate_txn_states) // 12
|
||||
for i in range(num_states):
|
||||
up = struct.unpack_from('<iq', bid.participate_txn_states[i * 12:(i + 1) * 12])
|
||||
content += '<tr><td>PTX {}</td><td>{}</td></tr>'.format(getTxState(up[0]), time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(up[1])))
|
||||
content += '</table>'
|
||||
|
||||
content += '<p><a href="/">home</a></p></body></html>'
|
||||
return bytes(content, 'UTF-8')
|
||||
|
||||
def page_bids(self, url_split, post_string, sent=False):
|
||||
swap_client = self.server.swap_client
|
||||
bids = swap_client.listBids(sent=sent)
|
||||
|
||||
content = html_content_start(self.server.title, self.server.title) \
|
||||
+ '<h3>' + ('Sent ' if sent else '') + 'Bids</h3>'
|
||||
|
||||
content += '<table>'
|
||||
content += '<tr><th>Bid ID</th><th>Offer ID</th><th>Bid Status</th><th>ITX Status</th><th>PTX Status</th></tr>'
|
||||
for b in bids:
|
||||
content += '<tr><td><a href=/bid/{0}>{0}</a></td><td><a href=/offer/{1}>{1}</a></td><td>{2}</td><td>{3}</td><td>{4}</td></tr>'.format(b.bid_id.hex(), b.offer_id.hex(), getBidState(b.state), getTxState(b.initiate_txn_state), getTxState(b.participate_txn_state))
|
||||
content += '</table>'
|
||||
|
||||
content += '<p><a href="/">home</a></p></body></html>'
|
||||
return bytes(content, 'UTF-8')
|
||||
|
||||
def page_watched(self, url_split, post_string):
|
||||
swap_client = self.server.swap_client
|
||||
watched_outputs, last_scanned = swap_client.listWatchedOutputs()
|
||||
|
||||
content = html_content_start(self.server.title, self.server.title) \
|
||||
+ '<h3>Watched Outputs</h3>'
|
||||
|
||||
for c in last_scanned:
|
||||
content += '<p>' + getCoinName(c[0]) + ' Scanned Height: ' + str(c[1]) + '</p>'
|
||||
|
||||
content += '<table>'
|
||||
content += '<tr><th>Bid ID</th><th>Chain</th><th>Txid</th><th>Index</th><th>Type</th></tr>'
|
||||
for o in watched_outputs:
|
||||
content += '<tr><td><a href=/bid/{0}>{0}</a></td><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td></tr>'.format(o[1].hex(), getCoinName(o[0]), o[2], o[3], int(o[4]))
|
||||
content += '</table>'
|
||||
|
||||
content += '<p><a href="/">home</a></p></body></html>'
|
||||
return bytes(content, 'UTF-8')
|
||||
|
||||
def page_index(self, url_split):
|
||||
swap_client = self.server.swap_client
|
||||
summary = swap_client.getSummary()
|
||||
|
||||
content = html_content_start(self.server.title, self.server.title) \
|
||||
+ '<p><a href="/wallets">View Wallets</a></p>' \
|
||||
+ '<p>' \
|
||||
+ 'Network: ' + str(summary['network']) + '<br/>' \
|
||||
+ '<a href="/active">Swaps in progress: ' + str(summary['num_swapping']) + '</a><br/>' \
|
||||
+ '<a href="/offers">Network Offers: ' + str(summary['num_network_offers']) + '</a><br/>' \
|
||||
+ '<a href="/sentoffers">Sent Offers: ' + str(summary['num_sent_offers']) + '</a><br/>' \
|
||||
+ '<a href="/bids">Received Bids: ' + str(summary['num_recv_bids']) + '</a><br/>' \
|
||||
+ '<a href="/sentbids">Sent Bids: ' + str(summary['num_sent_bids']) + '</a><br/>' \
|
||||
+ '<a href="/watched">Watched Outputs: ' + str(summary['num_watched_outputs']) + '</a><br/>' \
|
||||
+ '</p>' \
|
||||
+ '<p>' \
|
||||
+ '<a href="/newoffer">New Offer</a><br/>' \
|
||||
+ '</p>'
|
||||
content += '</body></html>'
|
||||
return bytes(content, 'UTF-8')
|
||||
|
||||
def putHeaders(self, status_code, content_type):
|
||||
self.send_response(status_code)
|
||||
if self.server.allow_cors:
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Content-type', content_type)
|
||||
self.end_headers()
|
||||
|
||||
def handle_http(self, status_code, path, post_string=''):
|
||||
url_split = self.path.split('/')
|
||||
if len(url_split) > 1 and url_split[1] == 'json':
|
||||
try:
|
||||
self.putHeaders(status_code, 'text/plain')
|
||||
func = self.js_index
|
||||
if len(url_split) > 2:
|
||||
func = {'wallets': self.js_wallets,
|
||||
'offers': self.js_offers,
|
||||
'sentoffers': self.js_sentoffers,
|
||||
'bids': self.js_bids,
|
||||
'sentbids': self.js_sentbids,
|
||||
}.get(url_split[2], self.js_index)
|
||||
return func(url_split)
|
||||
except Exception as e:
|
||||
return self.js_error(str(e))
|
||||
try:
|
||||
self.putHeaders(status_code, 'text/html')
|
||||
if len(url_split) > 1:
|
||||
if url_split[1] == 'active':
|
||||
return self.page_active(url_split, post_string)
|
||||
if url_split[1] == 'wallets':
|
||||
return self.page_wallets(url_split, post_string)
|
||||
if url_split[1] == 'offer':
|
||||
return self.page_offer(url_split, post_string)
|
||||
if url_split[1] == 'offers':
|
||||
return self.page_offers(url_split)
|
||||
if url_split[1] == 'newoffer':
|
||||
return self.page_newoffer(url_split, post_string)
|
||||
if url_split[1] == 'sentoffers':
|
||||
return self.page_offers(url_split, sent=True)
|
||||
if url_split[1] == 'bid':
|
||||
return self.page_bid(url_split, post_string)
|
||||
if url_split[1] == 'bids':
|
||||
return self.page_bids(url_split, post_string)
|
||||
if url_split[1] == 'sentbids':
|
||||
return self.page_bids(url_split, post_string, sent=True)
|
||||
if url_split[1] == 'watched':
|
||||
return self.page_watched(url_split, post_string)
|
||||
return self.page_index(url_split)
|
||||
except Exception as e:
|
||||
traceback.print_exc() # TODO: Remove
|
||||
return self.page_error(str(e))
|
||||
|
||||
def do_GET(self):
|
||||
response = self.handle_http(200, self.path)
|
||||
self.wfile.write(response)
|
||||
|
||||
def do_POST(self):
|
||||
post_string = self.rfile.read(int(self.headers['Content-Length']))
|
||||
response = self.handle_http(200, self.path, post_string)
|
||||
self.wfile.write(response)
|
||||
|
||||
def do_HEAD(self):
|
||||
self.putHeaders(200, 'text/html')
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(200, 'ok')
|
||||
if self.server.allow_cors:
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Headers', '*')
|
||||
self.end_headers()
|
||||
|
||||
|
||||
class HttpThread(threading.Thread, HTTPServer):
|
||||
def __init__(self, fp, host_name, port_no, allow_cors, swap_client):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.stop_event = threading.Event()
|
||||
self.fp = fp
|
||||
self.host_name = host_name
|
||||
self.port_no = port_no
|
||||
self.allow_cors = allow_cors
|
||||
self.swap_client = swap_client
|
||||
self.title = 'Simple Atomic Swap Demo'
|
||||
self.last_form_id = dict()
|
||||
|
||||
self.timeout = 60
|
||||
HTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)
|
||||
|
||||
def stop(self):
|
||||
self.stop_event.set()
|
||||
|
||||
# Send fake request
|
||||
conn = http.client.HTTPConnection(self.host_name, self.port_no)
|
||||
conn.connect()
|
||||
conn.request('GET', '/none')
|
||||
response = conn.getresponse()
|
||||
data = response.read()
|
||||
conn.close()
|
||||
|
||||
def stopped(self):
|
||||
return self.stop_event.is_set()
|
||||
|
||||
def serve_forever(self):
|
||||
while not self.stopped():
|
||||
self.handle_request()
|
||||
self.socket.close()
|
||||
|
||||
def run(self):
|
||||
self.serve_forever()
|
386
basicswap/key.py
Normal file
386
basicswap/key.py
Normal file
|
@ -0,0 +1,386 @@
|
|||
# Copyright (c) 2019 Pieter Wuille
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test-only secp256k1 elliptic curve implementation
|
||||
|
||||
WARNING: This code is slow, uses bad randomness, does not properly protect
|
||||
keys, and is trivially vulnerable to side channel attacks. Do not use for
|
||||
anything but tests."""
|
||||
import random
|
||||
|
||||
def modinv(a, n):
|
||||
"""Compute the modular inverse of a modulo n
|
||||
|
||||
See https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Modular_integers.
|
||||
"""
|
||||
t1, t2 = 0, 1
|
||||
r1, r2 = n, a
|
||||
while r2 != 0:
|
||||
q = r1 // r2
|
||||
t1, t2 = t2, t1 - q * t2
|
||||
r1, r2 = r2, r1 - q * r2
|
||||
if r1 > 1:
|
||||
return None
|
||||
if t1 < 0:
|
||||
t1 += n
|
||||
return t1
|
||||
|
||||
def jacobi_symbol(n, k):
|
||||
"""Compute the Jacobi symbol of n modulo k
|
||||
|
||||
See http://en.wikipedia.org/wiki/Jacobi_symbol
|
||||
|
||||
For our application k is always prime, so this is the same as the Legendre symbol."""
|
||||
assert k > 0 and k & 1, "jacobi symbol is only defined for positive odd k"
|
||||
n %= k
|
||||
t = 0
|
||||
while n != 0:
|
||||
while n & 1 == 0:
|
||||
n >>= 1
|
||||
r = k & 7
|
||||
t ^= (r == 3 or r == 5)
|
||||
n, k = k, n
|
||||
t ^= (n & k & 3 == 3)
|
||||
n = n % k
|
||||
if k == 1:
|
||||
return -1 if t else 1
|
||||
return 0
|
||||
|
||||
def modsqrt(a, p):
|
||||
"""Compute the square root of a modulo p when p % 4 = 3.
|
||||
|
||||
The Tonelli-Shanks algorithm can be used. See https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm
|
||||
|
||||
Limiting this function to only work for p % 4 = 3 means we don't need to
|
||||
iterate through the loop. The highest n such that p - 1 = 2^n Q with Q odd
|
||||
is n = 1. Therefore Q = (p-1)/2 and sqrt = a^((Q+1)/2) = a^((p+1)/4)
|
||||
|
||||
secp256k1's is defined over field of size 2**256 - 2**32 - 977, which is 3 mod 4.
|
||||
"""
|
||||
if p % 4 != 3:
|
||||
raise NotImplementedError("modsqrt only implemented for p % 4 = 3")
|
||||
sqrt = pow(a, (p + 1)//4, p)
|
||||
if pow(sqrt, 2, p) == a % p:
|
||||
return sqrt
|
||||
return None
|
||||
|
||||
class EllipticCurve:
|
||||
def __init__(self, p, a, b):
|
||||
"""Initialize elliptic curve y^2 = x^3 + a*x + b over GF(p)."""
|
||||
self.p = p
|
||||
self.a = a % p
|
||||
self.b = b % p
|
||||
|
||||
def affine(self, p1):
|
||||
"""Convert a Jacobian point tuple p1 to affine form, or None if at infinity.
|
||||
|
||||
An affine point is represented as the Jacobian (x, y, 1)"""
|
||||
x1, y1, z1 = p1
|
||||
if z1 == 0:
|
||||
return None
|
||||
inv = modinv(z1, self.p)
|
||||
inv_2 = (inv**2) % self.p
|
||||
inv_3 = (inv_2 * inv) % self.p
|
||||
return ((inv_2 * x1) % self.p, (inv_3 * y1) % self.p, 1)
|
||||
|
||||
def negate(self, p1):
|
||||
"""Negate a Jacobian point tuple p1."""
|
||||
x1, y1, z1 = p1
|
||||
return (x1, (self.p - y1) % self.p, z1)
|
||||
|
||||
def on_curve(self, p1):
|
||||
"""Determine whether a Jacobian tuple p is on the curve (and not infinity)"""
|
||||
x1, y1, z1 = p1
|
||||
z2 = pow(z1, 2, self.p)
|
||||
z4 = pow(z2, 2, self.p)
|
||||
return z1 != 0 and (pow(x1, 3, self.p) + self.a * x1 * z4 + self.b * z2 * z4 - pow(y1, 2, self.p)) % self.p == 0
|
||||
|
||||
def is_x_coord(self, x):
|
||||
"""Test whether x is a valid X coordinate on the curve."""
|
||||
x_3 = pow(x, 3, self.p)
|
||||
return jacobi_symbol(x_3 + self.a * x + self.b, self.p) != -1
|
||||
|
||||
def lift_x(self, x):
|
||||
"""Given an X coordinate on the curve, return a corresponding affine point."""
|
||||
x_3 = pow(x, 3, self.p)
|
||||
v = x_3 + self.a * x + self.b
|
||||
y = modsqrt(v, self.p)
|
||||
if y is None:
|
||||
return None
|
||||
return (x, y, 1)
|
||||
|
||||
def double(self, p1):
|
||||
"""Double a Jacobian tuple p1
|
||||
|
||||
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Doubling"""
|
||||
x1, y1, z1 = p1
|
||||
if z1 == 0:
|
||||
return (0, 1, 0)
|
||||
y1_2 = (y1**2) % self.p
|
||||
y1_4 = (y1_2**2) % self.p
|
||||
x1_2 = (x1**2) % self.p
|
||||
s = (4*x1*y1_2) % self.p
|
||||
m = 3*x1_2
|
||||
if self.a:
|
||||
m += self.a * pow(z1, 4, self.p)
|
||||
m = m % self.p
|
||||
x2 = (m**2 - 2*s) % self.p
|
||||
y2 = (m*(s - x2) - 8*y1_4) % self.p
|
||||
z2 = (2*y1*z1) % self.p
|
||||
return (x2, y2, z2)
|
||||
|
||||
def add_mixed(self, p1, p2):
|
||||
"""Add a Jacobian tuple p1 and an affine tuple p2
|
||||
|
||||
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition (with affine point)"""
|
||||
x1, y1, z1 = p1
|
||||
x2, y2, z2 = p2
|
||||
assert(z2 == 1)
|
||||
# Adding to the point at infinity is a no-op
|
||||
if z1 == 0:
|
||||
return p2
|
||||
z1_2 = (z1**2) % self.p
|
||||
z1_3 = (z1_2 * z1) % self.p
|
||||
u2 = (x2 * z1_2) % self.p
|
||||
s2 = (y2 * z1_3) % self.p
|
||||
if x1 == u2:
|
||||
if (y1 != s2):
|
||||
# p1 and p2 are inverses. Return the point at infinity.
|
||||
return (0, 1, 0)
|
||||
# p1 == p2. The formulas below fail when the two points are equal.
|
||||
return self.double(p1)
|
||||
h = u2 - x1
|
||||
r = s2 - y1
|
||||
h_2 = (h**2) % self.p
|
||||
h_3 = (h_2 * h) % self.p
|
||||
u1_h_2 = (x1 * h_2) % self.p
|
||||
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
|
||||
y3 = (r*(u1_h_2 - x3) - y1*h_3) % self.p
|
||||
z3 = (h*z1) % self.p
|
||||
return (x3, y3, z3)
|
||||
|
||||
def add(self, p1, p2):
|
||||
"""Add two Jacobian tuples p1 and p2
|
||||
|
||||
See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition"""
|
||||
x1, y1, z1 = p1
|
||||
x2, y2, z2 = p2
|
||||
# Adding the point at infinity is a no-op
|
||||
if z1 == 0:
|
||||
return p2
|
||||
if z2 == 0:
|
||||
return p1
|
||||
# Adding an Affine to a Jacobian is more efficient since we save field multiplications and squarings when z = 1
|
||||
if z1 == 1:
|
||||
return self.add_mixed(p2, p1)
|
||||
if z2 == 1:
|
||||
return self.add_mixed(p1, p2)
|
||||
z1_2 = (z1**2) % self.p
|
||||
z1_3 = (z1_2 * z1) % self.p
|
||||
z2_2 = (z2**2) % self.p
|
||||
z2_3 = (z2_2 * z2) % self.p
|
||||
u1 = (x1 * z2_2) % self.p
|
||||
u2 = (x2 * z1_2) % self.p
|
||||
s1 = (y1 * z2_3) % self.p
|
||||
s2 = (y2 * z1_3) % self.p
|
||||
if u1 == u2:
|
||||
if (s1 != s2):
|
||||
# p1 and p2 are inverses. Return the point at infinity.
|
||||
return (0, 1, 0)
|
||||
# p1 == p2. The formulas below fail when the two points are equal.
|
||||
return self.double(p1)
|
||||
h = u2 - u1
|
||||
r = s2 - s1
|
||||
h_2 = (h**2) % self.p
|
||||
h_3 = (h_2 * h) % self.p
|
||||
u1_h_2 = (u1 * h_2) % self.p
|
||||
x3 = (r**2 - h_3 - 2*u1_h_2) % self.p
|
||||
y3 = (r*(u1_h_2 - x3) - s1*h_3) % self.p
|
||||
z3 = (h*z1*z2) % self.p
|
||||
return (x3, y3, z3)
|
||||
|
||||
def mul(self, ps):
|
||||
"""Compute a (multi) point multiplication
|
||||
|
||||
ps is a list of (Jacobian tuple, scalar) pairs.
|
||||
"""
|
||||
r = (0, 1, 0)
|
||||
for i in range(255, -1, -1):
|
||||
r = self.double(r)
|
||||
for (p, n) in ps:
|
||||
if ((n >> i) & 1):
|
||||
r = self.add(r, p)
|
||||
return r
|
||||
|
||||
SECP256K1 = EllipticCurve(2**256 - 2**32 - 977, 0, 7)
|
||||
SECP256K1_G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, 1)
|
||||
SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
||||
SECP256K1_ORDER_HALF = SECP256K1_ORDER // 2
|
||||
|
||||
class ECPubKey():
|
||||
"""A secp256k1 public key"""
|
||||
|
||||
def __init__(self):
|
||||
"""Construct an uninitialized public key"""
|
||||
self.valid = False
|
||||
|
||||
def set(self, data):
|
||||
"""Construct a public key from a serialization in compressed or uncompressed format"""
|
||||
if (len(data) == 65 and data[0] == 0x04):
|
||||
p = (int.from_bytes(data[1:33], 'big'), int.from_bytes(data[33:65], 'big'), 1)
|
||||
self.valid = SECP256K1.on_curve(p)
|
||||
if self.valid:
|
||||
self.p = p
|
||||
self.compressed = False
|
||||
elif (len(data) == 33 and (data[0] == 0x02 or data[0] == 0x03)):
|
||||
x = int.from_bytes(data[1:33], 'big')
|
||||
if SECP256K1.is_x_coord(x):
|
||||
p = SECP256K1.lift_x(x)
|
||||
# if the oddness of the y co-ord isn't correct, find the other
|
||||
# valid y
|
||||
if (p[1] & 1) != (data[0] & 1):
|
||||
p = SECP256K1.negate(p)
|
||||
self.p = p
|
||||
self.valid = True
|
||||
self.compressed = True
|
||||
else:
|
||||
self.valid = False
|
||||
else:
|
||||
self.valid = False
|
||||
|
||||
@property
|
||||
def is_compressed(self):
|
||||
return self.compressed
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return self.valid
|
||||
|
||||
def get_bytes(self):
|
||||
assert(self.valid)
|
||||
p = SECP256K1.affine(self.p)
|
||||
if p is None:
|
||||
return None
|
||||
if self.compressed:
|
||||
return bytes([0x02 + (p[1] & 1)]) + p[0].to_bytes(32, 'big')
|
||||
else:
|
||||
return bytes([0x04]) + p[0].to_bytes(32, 'big') + p[1].to_bytes(32, 'big')
|
||||
|
||||
def verify_ecdsa(self, sig, msg, low_s=True):
|
||||
"""Verify a strictly DER-encoded ECDSA signature against this pubkey.
|
||||
|
||||
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
|
||||
ECDSA verifier algorithm"""
|
||||
assert(self.valid)
|
||||
|
||||
# Extract r and s from the DER formatted signature. Return false for
|
||||
# any DER encoding errors.
|
||||
if (sig[1] + 2 != len(sig)):
|
||||
return False
|
||||
if (len(sig) < 4):
|
||||
return False
|
||||
if (sig[0] != 0x30):
|
||||
return False
|
||||
if (sig[2] != 0x02):
|
||||
return False
|
||||
rlen = sig[3]
|
||||
if (len(sig) < 6 + rlen):
|
||||
return False
|
||||
if rlen < 1 or rlen > 33:
|
||||
return False
|
||||
if sig[4] >= 0x80:
|
||||
return False
|
||||
if (rlen > 1 and (sig[4] == 0) and not (sig[5] & 0x80)):
|
||||
return False
|
||||
r = int.from_bytes(sig[4:4+rlen], 'big')
|
||||
if (sig[4+rlen] != 0x02):
|
||||
return False
|
||||
slen = sig[5+rlen]
|
||||
if slen < 1 or slen > 33:
|
||||
return False
|
||||
if (len(sig) != 6 + rlen + slen):
|
||||
return False
|
||||
if sig[6+rlen] >= 0x80:
|
||||
return False
|
||||
if (slen > 1 and (sig[6+rlen] == 0) and not (sig[7+rlen] & 0x80)):
|
||||
return False
|
||||
s = int.from_bytes(sig[6+rlen:6+rlen+slen], 'big')
|
||||
|
||||
# Verify that r and s are within the group order
|
||||
if r < 1 or s < 1 or r >= SECP256K1_ORDER or s >= SECP256K1_ORDER:
|
||||
return False
|
||||
if low_s and s >= SECP256K1_ORDER_HALF:
|
||||
return False
|
||||
z = int.from_bytes(msg, 'big')
|
||||
|
||||
# Run verifier algorithm on r, s
|
||||
w = modinv(s, SECP256K1_ORDER)
|
||||
u1 = z*w % SECP256K1_ORDER
|
||||
u2 = r*w % SECP256K1_ORDER
|
||||
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, u1), (self.p, u2)]))
|
||||
if R is None or R[0] != r:
|
||||
return False
|
||||
return True
|
||||
|
||||
class ECKey():
|
||||
"""A secp256k1 private key"""
|
||||
|
||||
def __init__(self):
|
||||
self.valid = False
|
||||
|
||||
def set(self, secret, compressed):
|
||||
"""Construct a private key object with given 32-byte secret and compressed flag."""
|
||||
assert(len(secret) == 32)
|
||||
secret = int.from_bytes(secret, 'big')
|
||||
self.valid = (secret > 0 and secret < SECP256K1_ORDER)
|
||||
if self.valid:
|
||||
self.secret = secret
|
||||
self.compressed = compressed
|
||||
|
||||
def generate(self, compressed=True):
|
||||
"""Generate a random private key (compressed or uncompressed)."""
|
||||
self.set(random.randrange(1, SECP256K1_ORDER).to_bytes(32, 'big'), compressed)
|
||||
|
||||
def get_bytes(self):
|
||||
"""Retrieve the 32-byte representation of this key."""
|
||||
assert(self.valid)
|
||||
return self.secret.to_bytes(32, 'big')
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return self.valid
|
||||
|
||||
@property
|
||||
def is_compressed(self):
|
||||
return self.compressed
|
||||
|
||||
def get_pubkey(self):
|
||||
"""Compute an ECPubKey object for this secret key."""
|
||||
assert(self.valid)
|
||||
ret = ECPubKey()
|
||||
p = SECP256K1.mul([(SECP256K1_G, self.secret)])
|
||||
ret.p = p
|
||||
ret.valid = True
|
||||
ret.compressed = self.compressed
|
||||
return ret
|
||||
|
||||
def sign_ecdsa(self, msg, low_s=True):
|
||||
"""Construct a DER-encoded ECDSA signature with this key.
|
||||
|
||||
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
|
||||
ECDSA signer algorithm."""
|
||||
assert(self.valid)
|
||||
z = int.from_bytes(msg, 'big')
|
||||
# Note: no RFC6979, but a simple random nonce (some tests rely on distinct transactions for the same operation)
|
||||
k = random.randrange(1, SECP256K1_ORDER)
|
||||
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, k)]))
|
||||
r = R[0] % SECP256K1_ORDER
|
||||
s = (modinv(k, SECP256K1_ORDER) * (z + self.secret * r)) % SECP256K1_ORDER
|
||||
if low_s and s > SECP256K1_ORDER_HALF:
|
||||
s = SECP256K1_ORDER - s
|
||||
# Represent in DER format. The byte representations of r and s have
|
||||
# length rounded up (255 bits becomes 32 bytes and 256 bits becomes 33
|
||||
# bytes).
|
||||
rb = r.to_bytes((r.bit_length() + 8) // 8, 'big')
|
||||
sb = s.to_bytes((s.bit_length() + 8) // 8, 'big')
|
||||
return b'\x30' + bytes([4 + len(rb) + len(sb), 2, len(rb)]) + rb + bytes([2, len(sb)]) + sb
|
46
basicswap/messages.proto
Normal file
46
basicswap/messages.proto
Normal file
|
@ -0,0 +1,46 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package basicswap;
|
||||
|
||||
/* Step 1, seller -> network */
|
||||
message OfferMessage {
|
||||
uint32 coin_from = 1;
|
||||
uint32 coin_to = 2;
|
||||
uint64 amount_from = 3;
|
||||
uint64 rate = 4;
|
||||
uint64 min_bid_amount = 5;
|
||||
uint64 time_valid = 6;
|
||||
enum LockType {
|
||||
NOT_SET = 0;
|
||||
SEQUENCE_LOCK_BLOCKS = 1;
|
||||
SEQUENCE_LOCK_TIME = 2;
|
||||
}
|
||||
LockType lock_type = 7;
|
||||
uint32 lock_value = 8;
|
||||
uint32 swap_type = 9;
|
||||
|
||||
/* optional */
|
||||
string proof_address = 10;
|
||||
string proof_signature = 11;
|
||||
bytes pkhash_seller = 12;
|
||||
bytes secret_hash = 13;
|
||||
}
|
||||
|
||||
/* Step 2, buyer -> seller */
|
||||
message BidMessage {
|
||||
bytes offer_msg_id = 1;
|
||||
uint64 time_valid = 2; /* seconds bid is valid for */
|
||||
uint64 amount = 3; /* amount of amount_from bid is for */
|
||||
|
||||
/* optional */
|
||||
bytes pkhash_buyer = 4; /* buyer's address to receive amount_from */
|
||||
string proof_address = 5;
|
||||
string proof_signature = 6;
|
||||
}
|
||||
|
||||
/* Step 3, seller -> buyer */
|
||||
message BidAcceptMessage {
|
||||
bytes bid_msg_id = 1;
|
||||
bytes initiate_txid = 2;
|
||||
bytes contract_script = 3;
|
||||
}
|
310
basicswap/messages_pb2.py
Normal file
310
basicswap/messages_pb2.py
Normal file
|
@ -0,0 +1,310 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: messages.proto
|
||||
|
||||
import sys
|
||||
_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor.FileDescriptor(
|
||||
name='messages.proto',
|
||||
package='basicswap',
|
||||
syntax='proto3',
|
||||
serialized_options=None,
|
||||
serialized_pb=_b('\n\x0emessages.proto\x12\tbasicswap\"\x84\x03\n\x0cOfferMessage\x12\x11\n\tcoin_from\x18\x01 \x01(\r\x12\x0f\n\x07\x63oin_to\x18\x02 \x01(\r\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x16\n\x0emin_bid_amount\x18\x05 \x01(\x04\x12\x12\n\ntime_valid\x18\x06 \x01(\x04\x12\x33\n\tlock_type\x18\x07 \x01(\x0e\x32 .basicswap.OfferMessage.LockType\x12\x12\n\nlock_value\x18\x08 \x01(\r\x12\x11\n\tswap_type\x18\t \x01(\r\x12\x15\n\rproof_address\x18\n \x01(\t\x12\x17\n\x0fproof_signature\x18\x0b \x01(\t\x12\x15\n\rpkhash_seller\x18\x0c \x01(\x0c\x12\x13\n\x0bsecret_hash\x18\r \x01(\x0c\"I\n\x08LockType\x12\x0b\n\x07NOT_SET\x10\x00\x12\x18\n\x14SEQUENCE_LOCK_BLOCKS\x10\x01\x12\x16\n\x12SEQUENCE_LOCK_TIME\x10\x02\"\x8c\x01\n\nBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x04 \x01(\x0c\x12\x15\n\rproof_address\x18\x05 \x01(\t\x12\x17\n\x0fproof_signature\x18\x06 \x01(\t\"V\n\x10\x42idAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x15\n\rinitiate_txid\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63ontract_script\x18\x03 \x01(\x0c\x62\x06proto3')
|
||||
)
|
||||
|
||||
|
||||
|
||||
_OFFERMESSAGE_LOCKTYPE = _descriptor.EnumDescriptor(
|
||||
name='LockType',
|
||||
full_name='basicswap.OfferMessage.LockType',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
values=[
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='NOT_SET', index=0, number=0,
|
||||
serialized_options=None,
|
||||
type=None),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='SEQUENCE_LOCK_BLOCKS', index=1, number=1,
|
||||
serialized_options=None,
|
||||
type=None),
|
||||
_descriptor.EnumValueDescriptor(
|
||||
name='SEQUENCE_LOCK_TIME', index=2, number=2,
|
||||
serialized_options=None,
|
||||
type=None),
|
||||
],
|
||||
containing_type=None,
|
||||
serialized_options=None,
|
||||
serialized_start=345,
|
||||
serialized_end=418,
|
||||
)
|
||||
_sym_db.RegisterEnumDescriptor(_OFFERMESSAGE_LOCKTYPE)
|
||||
|
||||
|
||||
_OFFERMESSAGE = _descriptor.Descriptor(
|
||||
name='OfferMessage',
|
||||
full_name='basicswap.OfferMessage',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='coin_from', full_name='basicswap.OfferMessage.coin_from', index=0,
|
||||
number=1, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='coin_to', full_name='basicswap.OfferMessage.coin_to', index=1,
|
||||
number=2, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='amount_from', full_name='basicswap.OfferMessage.amount_from', index=2,
|
||||
number=3, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='rate', full_name='basicswap.OfferMessage.rate', index=3,
|
||||
number=4, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='min_bid_amount', full_name='basicswap.OfferMessage.min_bid_amount', index=4,
|
||||
number=5, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='time_valid', full_name='basicswap.OfferMessage.time_valid', index=5,
|
||||
number=6, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='lock_type', full_name='basicswap.OfferMessage.lock_type', index=6,
|
||||
number=7, type=14, cpp_type=8, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='lock_value', full_name='basicswap.OfferMessage.lock_value', index=7,
|
||||
number=8, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='swap_type', full_name='basicswap.OfferMessage.swap_type', index=8,
|
||||
number=9, type=13, cpp_type=3, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='proof_address', full_name='basicswap.OfferMessage.proof_address', index=9,
|
||||
number=10, type=9, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=_b("").decode('utf-8'),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='proof_signature', full_name='basicswap.OfferMessage.proof_signature', index=10,
|
||||
number=11, type=9, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=_b("").decode('utf-8'),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='pkhash_seller', full_name='basicswap.OfferMessage.pkhash_seller', index=11,
|
||||
number=12, type=12, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=_b(""),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='secret_hash', full_name='basicswap.OfferMessage.secret_hash', index=12,
|
||||
number=13, type=12, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=_b(""),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
_OFFERMESSAGE_LOCKTYPE,
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=30,
|
||||
serialized_end=418,
|
||||
)
|
||||
|
||||
|
||||
_BIDMESSAGE = _descriptor.Descriptor(
|
||||
name='BidMessage',
|
||||
full_name='basicswap.BidMessage',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='offer_msg_id', full_name='basicswap.BidMessage.offer_msg_id', index=0,
|
||||
number=1, type=12, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=_b(""),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='time_valid', full_name='basicswap.BidMessage.time_valid', index=1,
|
||||
number=2, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='amount', full_name='basicswap.BidMessage.amount', index=2,
|
||||
number=3, type=4, cpp_type=4, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='pkhash_buyer', full_name='basicswap.BidMessage.pkhash_buyer', index=3,
|
||||
number=4, type=12, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=_b(""),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='proof_address', full_name='basicswap.BidMessage.proof_address', index=4,
|
||||
number=5, type=9, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=_b("").decode('utf-8'),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='proof_signature', full_name='basicswap.BidMessage.proof_signature', index=5,
|
||||
number=6, type=9, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=_b("").decode('utf-8'),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=421,
|
||||
serialized_end=561,
|
||||
)
|
||||
|
||||
|
||||
_BIDACCEPTMESSAGE = _descriptor.Descriptor(
|
||||
name='BidAcceptMessage',
|
||||
full_name='basicswap.BidAcceptMessage',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='bid_msg_id', full_name='basicswap.BidAcceptMessage.bid_msg_id', index=0,
|
||||
number=1, type=12, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=_b(""),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='initiate_txid', full_name='basicswap.BidAcceptMessage.initiate_txid', index=1,
|
||||
number=2, type=12, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=_b(""),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='contract_script', full_name='basicswap.BidAcceptMessage.contract_script', index=2,
|
||||
number=3, type=12, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=_b(""),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
serialized_options=None, file=DESCRIPTOR),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
serialized_options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=563,
|
||||
serialized_end=649,
|
||||
)
|
||||
|
||||
_OFFERMESSAGE.fields_by_name['lock_type'].enum_type = _OFFERMESSAGE_LOCKTYPE
|
||||
_OFFERMESSAGE_LOCKTYPE.containing_type = _OFFERMESSAGE
|
||||
DESCRIPTOR.message_types_by_name['OfferMessage'] = _OFFERMESSAGE
|
||||
DESCRIPTOR.message_types_by_name['BidMessage'] = _BIDMESSAGE
|
||||
DESCRIPTOR.message_types_by_name['BidAcceptMessage'] = _BIDACCEPTMESSAGE
|
||||
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
||||
|
||||
OfferMessage = _reflection.GeneratedProtocolMessageType('OfferMessage', (_message.Message,), dict(
|
||||
DESCRIPTOR = _OFFERMESSAGE,
|
||||
__module__ = 'messages_pb2'
|
||||
# @@protoc_insertion_point(class_scope:basicswap.OfferMessage)
|
||||
))
|
||||
_sym_db.RegisterMessage(OfferMessage)
|
||||
|
||||
BidMessage = _reflection.GeneratedProtocolMessageType('BidMessage', (_message.Message,), dict(
|
||||
DESCRIPTOR = _BIDMESSAGE,
|
||||
__module__ = 'messages_pb2'
|
||||
# @@protoc_insertion_point(class_scope:basicswap.BidMessage)
|
||||
))
|
||||
_sym_db.RegisterMessage(BidMessage)
|
||||
|
||||
BidAcceptMessage = _reflection.GeneratedProtocolMessageType('BidAcceptMessage', (_message.Message,), dict(
|
||||
DESCRIPTOR = _BIDACCEPTMESSAGE,
|
||||
__module__ = 'messages_pb2'
|
||||
# @@protoc_insertion_point(class_scope:basicswap.BidAcceptMessage)
|
||||
))
|
||||
_sym_db.RegisterMessage(BidAcceptMessage)
|
||||
|
||||
|
||||
# @@protoc_insertion_point(module_scope)
|
123
basicswap/segwit_addr.py
Normal file
123
basicswap/segwit_addr.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
# Copyright (c) 2017 Pieter Wuille
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
"""Reference implementation for Bech32 and segwit addresses."""
|
||||
|
||||
|
||||
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
|
||||
|
||||
def bech32_polymod(values):
|
||||
"""Internal function that computes the Bech32 checksum."""
|
||||
generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
|
||||
chk = 1
|
||||
for value in values:
|
||||
top = chk >> 25
|
||||
chk = (chk & 0x1ffffff) << 5 ^ value
|
||||
for i in range(5):
|
||||
chk ^= generator[i] if ((top >> i) & 1) else 0
|
||||
return chk
|
||||
|
||||
|
||||
def bech32_hrp_expand(hrp):
|
||||
"""Expand the HRP into values for checksum computation."""
|
||||
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
|
||||
|
||||
|
||||
def bech32_verify_checksum(hrp, data):
|
||||
"""Verify a checksum given HRP and converted data characters."""
|
||||
return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1
|
||||
|
||||
|
||||
def bech32_create_checksum(hrp, data):
|
||||
"""Compute the checksum values given HRP and data."""
|
||||
values = bech32_hrp_expand(hrp) + data
|
||||
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
|
||||
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
|
||||
|
||||
|
||||
def bech32_encode(hrp, data):
|
||||
"""Compute a Bech32 string given HRP and data values."""
|
||||
combined = data + bech32_create_checksum(hrp, data)
|
||||
return hrp + '1' + ''.join([CHARSET[d] for d in combined])
|
||||
|
||||
|
||||
def bech32_decode(bech):
|
||||
"""Validate a Bech32 string, and determine HRP and data."""
|
||||
if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
|
||||
(bech.lower() != bech and bech.upper() != bech)):
|
||||
return (None, None)
|
||||
bech = bech.lower()
|
||||
pos = bech.rfind('1')
|
||||
if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
|
||||
return (None, None)
|
||||
if not all(x in CHARSET for x in bech[pos + 1:]):
|
||||
return (None, None)
|
||||
hrp = bech[:pos]
|
||||
data = [CHARSET.find(x) for x in bech[pos + 1:]]
|
||||
if not bech32_verify_checksum(hrp, data):
|
||||
return (None, None)
|
||||
return (hrp, data[:-6])
|
||||
|
||||
|
||||
def convertbits(data, frombits, tobits, pad=True):
|
||||
"""General power-of-2 base conversion."""
|
||||
acc = 0
|
||||
bits = 0
|
||||
ret = []
|
||||
maxv = (1 << tobits) - 1
|
||||
max_acc = (1 << (frombits + tobits - 1)) - 1
|
||||
for value in data:
|
||||
if value < 0 or (value >> frombits):
|
||||
return None
|
||||
acc = ((acc << frombits) | value) & max_acc
|
||||
bits += frombits
|
||||
while bits >= tobits:
|
||||
bits -= tobits
|
||||
ret.append((acc >> bits) & maxv)
|
||||
if pad:
|
||||
if bits:
|
||||
ret.append((acc << (tobits - bits)) & maxv)
|
||||
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
|
||||
return None
|
||||
return ret
|
||||
|
||||
|
||||
def decode(hrp, addr):
|
||||
"""Decode a segwit address."""
|
||||
hrpgot, data = bech32_decode(addr)
|
||||
if hrpgot != hrp:
|
||||
return (None, None)
|
||||
decoded = convertbits(data[1:], 5, 8, False)
|
||||
if decoded is None or len(decoded) < 2 or len(decoded) > 40:
|
||||
return (None, None)
|
||||
if data[0] > 16:
|
||||
return (None, None)
|
||||
if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
|
||||
return (None, None)
|
||||
return (data[0], decoded)
|
||||
|
||||
|
||||
def encode(hrp, witver, witprog):
|
||||
"""Encode a segwit address."""
|
||||
ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5))
|
||||
if decode(hrp, ret) == (None, None):
|
||||
return None
|
||||
return ret
|
292
basicswap/util.py
Normal file
292
basicswap/util.py
Normal file
|
@ -0,0 +1,292 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2018-2019 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import decimal
|
||||
import subprocess
|
||||
import json
|
||||
import traceback
|
||||
import hashlib
|
||||
import urllib
|
||||
from xmlrpc.client import (
|
||||
Transport,
|
||||
Fault,
|
||||
)
|
||||
from .segwit_addr import bech32_decode, convertbits, bech32_encode
|
||||
|
||||
COIN = 100000000
|
||||
|
||||
|
||||
def format8(i):
|
||||
n = abs(i)
|
||||
quotient = n // COIN
|
||||
remainder = n % COIN
|
||||
rv = "%d.%08d" % (quotient, remainder)
|
||||
if i < 0:
|
||||
rv = '-' + rv
|
||||
return rv
|
||||
|
||||
|
||||
def toBool(s):
|
||||
return s.lower() in ["1", "true"]
|
||||
|
||||
|
||||
def dquantize(n, places=8):
|
||||
return n.quantize(decimal.Decimal(10) ** -places)
|
||||
|
||||
|
||||
def jsonDecimal(obj):
|
||||
if isinstance(obj, decimal.Decimal):
|
||||
return str(obj)
|
||||
raise TypeError
|
||||
|
||||
|
||||
def dumpj(jin, indent=4):
|
||||
return json.dumps(jin, indent=indent, default=jsonDecimal)
|
||||
|
||||
|
||||
def dumpje(jin):
|
||||
return json.dumps(jin, default=jsonDecimal).replace('"', '\\"')
|
||||
|
||||
|
||||
__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
||||
|
||||
|
||||
def b58decode(v, length=None):
|
||||
long_value = 0
|
||||
for (i, c) in enumerate(v[::-1]):
|
||||
ofs = __b58chars.find(c)
|
||||
if ofs < 0:
|
||||
return None
|
||||
long_value += ofs * (58**i)
|
||||
result = bytes()
|
||||
while long_value >= 256:
|
||||
div, mod = divmod(long_value, 256)
|
||||
result = bytes((mod,)) + result
|
||||
long_value = div
|
||||
result = bytes((long_value,)) + result
|
||||
nPad = 0
|
||||
for c in v:
|
||||
if c == __b58chars[0]:
|
||||
nPad += 1
|
||||
else:
|
||||
break
|
||||
pad = bytes((0,)) * nPad
|
||||
result = pad + result
|
||||
if length is not None and len(result) != length:
|
||||
return None
|
||||
return result
|
||||
|
||||
|
||||
def b58encode(v):
|
||||
long_value = 0
|
||||
for (i, c) in enumerate(v[::-1]):
|
||||
long_value += (256**i) * c
|
||||
|
||||
result = ''
|
||||
while long_value >= 58:
|
||||
div, mod = divmod(long_value, 58)
|
||||
result = __b58chars[mod] + result
|
||||
long_value = div
|
||||
result = __b58chars[long_value] + result
|
||||
|
||||
# leading 0-bytes in the input become leading-1s
|
||||
nPad = 0
|
||||
for c in v:
|
||||
if c == 0:
|
||||
nPad += 1
|
||||
else:
|
||||
break
|
||||
return (__b58chars[0] * nPad) + result
|
||||
|
||||
|
||||
def decodeWif(network_key):
|
||||
key = b58decode(network_key)[1:-4]
|
||||
if len(key) == 33:
|
||||
return key[:-1]
|
||||
return key
|
||||
|
||||
|
||||
def toWIF(prefix_byte, b, compressed=True):
|
||||
b = bytes((prefix_byte, )) + b
|
||||
if compressed:
|
||||
b += bytes((0x01, ))
|
||||
b += hashlib.sha256(hashlib.sha256(b).digest()).digest()[:4]
|
||||
return b58encode(b)
|
||||
|
||||
|
||||
def bech32Decode(hrp, addr):
|
||||
hrpgot, data = bech32_decode(addr)
|
||||
if hrpgot != hrp:
|
||||
return None
|
||||
decoded = convertbits(data, 5, 8, False)
|
||||
if decoded is None or len(decoded) < 2 or len(decoded) > 40:
|
||||
return None
|
||||
return bytes(decoded)
|
||||
|
||||
|
||||
def bech32Encode(hrp, data):
|
||||
ret = bech32_encode(hrp, convertbits(data, 8, 5))
|
||||
if bech32Decode(hrp, ret) is None:
|
||||
return None
|
||||
return ret
|
||||
|
||||
|
||||
def decodeAddress(address_str):
|
||||
b58_addr = b58decode(address_str)
|
||||
if b58_addr is not None:
|
||||
address = b58_addr[:-4]
|
||||
checksum = b58_addr[-4:]
|
||||
assert(hashlib.sha256(hashlib.sha256(address).digest()).digest()[:4] == checksum), 'Checksum mismatch'
|
||||
return b58_addr[:-4]
|
||||
return None
|
||||
|
||||
|
||||
def encodeAddress(address):
|
||||
checksum = hashlib.sha256(hashlib.sha256(address).digest()).digest()
|
||||
return b58encode(address + checksum[0:4])
|
||||
|
||||
|
||||
def getKeyID(bytes):
|
||||
data = hashlib.sha256(bytes).digest()
|
||||
return hashlib.new("ripemd160", data).digest()
|
||||
|
||||
|
||||
def pubkeyToAddress(prefix, pubkey):
|
||||
return encodeAddress(bytes((prefix,)) + getKeyID(pubkey))
|
||||
|
||||
|
||||
def SerialiseNum(n):
|
||||
if n == 0:
|
||||
return bytes([0x00])
|
||||
if n > 0 and n <= 16:
|
||||
return bytes([0x50 + n])
|
||||
rv = bytearray()
|
||||
neg = n < 0
|
||||
absvalue = -n if neg else n
|
||||
while(absvalue):
|
||||
rv.append(absvalue & 0xff)
|
||||
absvalue >>= 8
|
||||
if rv[-1] & 0x80:
|
||||
rv.append(0x80 if neg else 0)
|
||||
elif neg:
|
||||
rv[-1] |= 0x80
|
||||
return bytes([len(rv)]) + rv
|
||||
|
||||
|
||||
def DeserialiseNum(b, o=0):
|
||||
if b[o] == 0:
|
||||
return 0
|
||||
if b[o] > 0x50 and b[o] <= 0x50 + 16:
|
||||
return b[o] - 0x50
|
||||
v = 0
|
||||
nb = b[o]
|
||||
o += 1
|
||||
for i in range(0, nb):
|
||||
v |= b[o + i] << (8 * i)
|
||||
# If the input vector's most significant byte is 0x80, remove it from the result's msb and return a negative.
|
||||
if b[o + nb - 1] & 0x80:
|
||||
return -(v & ~(0x80 << (8 * (nb - 1))))
|
||||
return v
|
||||
|
||||
|
||||
class Jsonrpc():
|
||||
# __getattr__ complicates extending ServerProxy
|
||||
def __init__(self, uri, transport=None, encoding=None, verbose=False,
|
||||
allow_none=False, use_datetime=False, use_builtin_types=False,
|
||||
*, context=None):
|
||||
# establish a "logical" server connection
|
||||
|
||||
# get the url
|
||||
type, uri = urllib.parse.splittype(uri)
|
||||
if type not in ("http", "https"):
|
||||
raise OSError("unsupported XML-RPC protocol")
|
||||
self.__host, self.__handler = urllib.parse.splithost(uri)
|
||||
if not self.__handler:
|
||||
self.__handler = "/RPC2"
|
||||
|
||||
if transport is None:
|
||||
handler = Transport
|
||||
extra_kwargs = {}
|
||||
transport = handler(use_datetime=use_datetime,
|
||||
use_builtin_types=use_builtin_types,
|
||||
**extra_kwargs)
|
||||
self.__transport = transport
|
||||
|
||||
self.__encoding = encoding or 'utf-8'
|
||||
self.__verbose = verbose
|
||||
self.__allow_none = allow_none
|
||||
|
||||
def close(self):
|
||||
if self.__transport is not None:
|
||||
self.__transport.close()
|
||||
|
||||
def json_request(self, method, params):
|
||||
try:
|
||||
connection = self.__transport.make_connection(self.__host)
|
||||
headers = self.__transport._extra_headers[:]
|
||||
|
||||
request_body = {
|
||||
'method': method,
|
||||
'params': params,
|
||||
'id': 2
|
||||
}
|
||||
|
||||
connection.putrequest("POST", self.__handler)
|
||||
headers.append(("Content-Type", "application/json"))
|
||||
headers.append(("User-Agent", 'jsonrpc'))
|
||||
self.__transport.send_headers(connection, headers)
|
||||
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8'))
|
||||
|
||||
resp = connection.getresponse()
|
||||
return resp.read()
|
||||
|
||||
except Fault:
|
||||
raise
|
||||
except Exception:
|
||||
# All unexpected errors leave connection in
|
||||
# a strange state, so we clear it.
|
||||
self.__transport.close()
|
||||
raise
|
||||
|
||||
|
||||
def callrpc(rpc_port, auth, method, params=[], wallet=None):
|
||||
|
||||
try:
|
||||
url = 'http://%s@127.0.0.1:%d/' % (auth, rpc_port)
|
||||
if wallet:
|
||||
url += 'wallet/' + wallet
|
||||
x = Jsonrpc(url)
|
||||
|
||||
v = x.json_request(method, params)
|
||||
x.close()
|
||||
r = json.loads(v.decode('utf-8'))
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
raise ValueError('RPC Server Error')
|
||||
|
||||
if 'error' in r and r['error'] is not None:
|
||||
raise ValueError('RPC error ' + str(r['error']))
|
||||
|
||||
return r['result']
|
||||
|
||||
|
||||
def callrpc_cli(bindir, datadir, chain, cmd, cli_bin='particl-cli'):
|
||||
cli_bin = os.path.join(bindir, cli_bin)
|
||||
|
||||
args = cli_bin + ('' if chain == 'mainnet' else ' -' + chain) + ' -datadir=' + datadir + ' ' + cmd
|
||||
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
|
||||
out = p.communicate()
|
||||
|
||||
if len(out[1]) > 0:
|
||||
raise ValueError('RPC error ' + str(out[1]))
|
||||
|
||||
r = out[0].decode('utf-8').strip()
|
||||
try:
|
||||
r = json.loads(r)
|
||||
except Exception:
|
||||
pass
|
||||
return r
|
1
bin/__init__.py
Normal file
1
bin/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
name = "bin"
|
1
bin/basicswap-run.py
Symbolic link
1
bin/basicswap-run.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
basicswap_run.py
|
177
bin/basicswap_run.py
Normal file
177
bin/basicswap_run.py
Normal file
|
@ -0,0 +1,177 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
"""
|
||||
Particl Atomic Swap - Proof of Concept
|
||||
|
||||
Dependencies:
|
||||
$ pacman -S python-pyzmq python-plyvel protobuf
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
import traceback
|
||||
import signal
|
||||
import subprocess
|
||||
|
||||
import basicswap.config as cfg
|
||||
from basicswap import __version__
|
||||
from basicswap.basicswap import BasicSwap
|
||||
from basicswap.http_server import HttpThread
|
||||
|
||||
|
||||
ALLOW_CORS = False
|
||||
swap_client = None
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
print('signal %d detected, ending program.' % (sig))
|
||||
if swap_client is not None:
|
||||
swap_client.stopRunning()
|
||||
|
||||
|
||||
def startDaemon(node_dir, bin_dir, daemon_bin):
|
||||
daemon_bin = os.path.join(bin_dir, daemon_bin)
|
||||
|
||||
args = [daemon_bin, '-datadir=' + node_dir]
|
||||
print('Starting node ' + daemon_bin + ' ' + '-datadir=' + node_dir)
|
||||
return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
|
||||
def runClient(fp, dataDir, chain):
|
||||
global swap_client
|
||||
settings_path = os.path.join(dataDir, 'basicswap.json')
|
||||
|
||||
if not os.path.exists(settings_path):
|
||||
raise ValueError('Settings file not found: ' + str(settings_path))
|
||||
|
||||
with open(settings_path) as fs:
|
||||
settings = json.load(fs)
|
||||
|
||||
daemons = []
|
||||
|
||||
for c, v in settings['chainclients'].items():
|
||||
if v['manage_daemon'] is True:
|
||||
print('Starting {} daemon'.format(c.capitalize()))
|
||||
if c == 'particl':
|
||||
daemons.append(startDaemon(v['datadir'], cfg.PARTICL_BINDIR, cfg.PARTICLD))
|
||||
print('Started {} {}'.format(cfg.PARTICLD, daemons[-1].pid))
|
||||
elif c == 'bitcoin':
|
||||
daemons.append(startDaemon(v['datadir'], cfg.BITCOIN_BINDIR, cfg.BITCOIND))
|
||||
print('Started {} {}'.format(cfg.BITCOIND, daemons[-1].pid))
|
||||
elif c == 'litecoin':
|
||||
daemons.append(startDaemon(v['datadir'], cfg.LITECOIN_BINDIR, cfg.LITECOIND))
|
||||
print('Started {} {}'.format(cfg.LITECOIND, daemons[-1].pid))
|
||||
else:
|
||||
print('Unknown chain', c)
|
||||
|
||||
swap_client = BasicSwap(fp, dataDir, settings, chain)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
swap_client.start()
|
||||
|
||||
threads = []
|
||||
if 'htmlhost' in settings:
|
||||
swap_client.log.info('Starting server at %s:%d.' % (settings['htmlhost'], settings['htmlport']))
|
||||
allow_cors = settings['allowcors'] if 'allowcors' in settings else ALLOW_CORS
|
||||
tS1 = HttpThread(fp, settings['htmlhost'], settings['htmlport'], allow_cors, swap_client)
|
||||
threads.append(tS1)
|
||||
tS1.start()
|
||||
|
||||
try:
|
||||
print('Exit with Ctrl + c.')
|
||||
while swap_client.is_running:
|
||||
time.sleep(0.5)
|
||||
swap_client.update()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
swap_client.log.info('Stopping threads.')
|
||||
for t in threads:
|
||||
t.stop()
|
||||
t.join()
|
||||
|
||||
for d in daemons:
|
||||
print('Terminating {}'.format(d.pid))
|
||||
d.terminate()
|
||||
d.wait(timeout=120)
|
||||
if d.stdout:
|
||||
d.stdout.close()
|
||||
if d.stderr:
|
||||
d.stderr.close()
|
||||
if d.stdin:
|
||||
d.stdin.close()
|
||||
|
||||
|
||||
def printVersion():
|
||||
print('Basicswap version:', __version__)
|
||||
|
||||
|
||||
def printHelp():
|
||||
print('basicswap-run.py --datadir=path -testnet')
|
||||
|
||||
|
||||
def main():
|
||||
data_dir = None
|
||||
chain = 'mainnet'
|
||||
|
||||
for v in sys.argv[1:]:
|
||||
if len(v) < 2 or v[0] != '-':
|
||||
print('Unknown argument', v)
|
||||
continue
|
||||
|
||||
s = v.split('=')
|
||||
name = s[0].strip()
|
||||
|
||||
for i in range(2):
|
||||
if name[0] == '-':
|
||||
name = name[1:]
|
||||
|
||||
if name == 'v' or name == 'version':
|
||||
printVersion()
|
||||
return 0
|
||||
if name == 'h' or name == 'help':
|
||||
printHelp()
|
||||
return 0
|
||||
if name == 'testnet':
|
||||
chain = 'testnet'
|
||||
continue
|
||||
if name == 'regtest':
|
||||
chain = 'regtest'
|
||||
continue
|
||||
|
||||
if len(s) == 2:
|
||||
if name == 'datadir':
|
||||
data_dir = os.path.expanduser(s[1])
|
||||
continue
|
||||
|
||||
print('Unknown argument', v)
|
||||
|
||||
if data_dir is None:
|
||||
data_dir = os.path.join(os.path.expanduser(os.path.join(cfg.DATADIRS)), 'particl', ('' if chain == 'mainnet' else chain), 'basicswap')
|
||||
|
||||
print('data_dir:', data_dir)
|
||||
if chain != 'mainnet':
|
||||
print('chain:', chain)
|
||||
|
||||
if not os.path.exists(data_dir):
|
||||
os.makedirs(data_dir)
|
||||
|
||||
with open(os.path.join(data_dir, 'basicswap.log'), 'w') as fp:
|
||||
print(os.path.basename(sys.argv[0]) + ', version: ' + __version__ + '\n\n')
|
||||
runClient(fp, data_dir, chain)
|
||||
|
||||
print('Done.')
|
||||
return swap_client.fail_code if swap_client is not None else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
0
bin/start_docker.bat
Normal file
0
bin/start_docker.bat
Normal file
5
docker/coindata/.gitignore
vendored
Normal file
5
docker/coindata/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
*
|
||||
!.gitignore
|
||||
!basicswap/basicswap.json
|
||||
!particl/particl.conf
|
||||
!litecoin/litecoin.conf
|
15
docker/docker-compose.yml
Normal file
15
docker/docker-compose.yml
Normal file
|
@ -0,0 +1,15 @@
|
|||
version: '3'
|
||||
services:
|
||||
|
||||
swapclient:
|
||||
build:
|
||||
context: ../
|
||||
volumes:
|
||||
- ./coindata:/coindata
|
||||
ports:
|
||||
- "127.0.0.1:12700:12700" # Expose only to localhost
|
||||
|
||||
volumes:
|
||||
coindata:
|
||||
driver: local
|
||||
|
37
setup.py
Normal file
37
setup.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
import setuptools
|
||||
import re
|
||||
import io
|
||||
|
||||
__version__ = re.search(
|
||||
r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]',
|
||||
io.open('basicswap/__init__.py', encoding='utf_8_sig').read()
|
||||
).group(1)
|
||||
|
||||
setuptools.setup(
|
||||
name="basicswap",
|
||||
version=__version__,
|
||||
author="tecnovert",
|
||||
author_email="hello@particl.io",
|
||||
description="Particl atomic swap demo",
|
||||
long_description=open("README.md").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/tecnovert/basicswap",
|
||||
packages=setuptools.find_packages(),
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: Linux",
|
||||
],
|
||||
install_requires=[
|
||||
"pyzmq",
|
||||
"plyvel",
|
||||
"protobuf",
|
||||
"sqlalchemy",
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"basicswap-run=bin.basicswap_run:main",
|
||||
]
|
||||
},
|
||||
test_suite="tests.test_suite"
|
||||
)
|
11
tests/__init__.py
Normal file
11
tests/__init__.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
import unittest
|
||||
|
||||
import tests.test_run
|
||||
import tests.test_other
|
||||
|
||||
|
||||
def test_suite():
|
||||
loader = unittest.TestLoader()
|
||||
suite = loader.loadTestsFromModule(tests.test_run)
|
||||
suite.addTests(loader.loadTestsFromModule(tests.test_other))
|
||||
return suite
|
62
tests/test_other.py
Normal file
62
tests/test_other.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import unittest
|
||||
from basicswap.util import (
|
||||
SerialiseNum,
|
||||
DeserialiseNum,
|
||||
)
|
||||
from basicswap.basicswap import (
|
||||
Coins,
|
||||
getExpectedSequence,
|
||||
decodeSequence,
|
||||
SEQUENCE_LOCK_BLOCKS,
|
||||
SEQUENCE_LOCK_TIME,
|
||||
)
|
||||
|
||||
|
||||
def test_case(v, nb=None):
|
||||
b = SerialiseNum(v)
|
||||
if nb is not None:
|
||||
assert(len(b) == nb)
|
||||
assert(v == DeserialiseNum(b))
|
||||
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
def test_serialise_num(self):
|
||||
test_case(0, 1)
|
||||
test_case(1, 1)
|
||||
test_case(16, 1)
|
||||
|
||||
test_case(-1, 2)
|
||||
test_case(17, 2)
|
||||
|
||||
test_case(500)
|
||||
test_case(-500)
|
||||
test_case(4194642)
|
||||
|
||||
def test_sequence(self):
|
||||
time_val = 48 * 60 * 60
|
||||
encoded = getExpectedSequence(SEQUENCE_LOCK_TIME, time_val, Coins.PART)
|
||||
decoded = decodeSequence(encoded)
|
||||
assert(decoded >= time_val)
|
||||
assert(decoded <= time_val + 512)
|
||||
|
||||
time_val = 24 * 60
|
||||
encoded = getExpectedSequence(SEQUENCE_LOCK_TIME, time_val, Coins.PART)
|
||||
decoded = decodeSequence(encoded)
|
||||
assert(decoded >= time_val)
|
||||
assert(decoded <= time_val + 512)
|
||||
|
||||
blocks_val = 123
|
||||
encoded = getExpectedSequence(SEQUENCE_LOCK_BLOCKS, blocks_val, Coins.PART)
|
||||
decoded = decodeSequence(encoded)
|
||||
assert(decoded == blocks_val)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
536
tests/test_run.py
Normal file
536
tests/test_run.py
Normal file
|
@ -0,0 +1,536 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2019 tecnovert
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
"""
|
||||
basicswap]$ python setup.py test
|
||||
|
||||
Run one test:
|
||||
$ python setup.py test -s tests.test_run.Test.test_04_ltc_btc
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
import json
|
||||
import logging
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import signal
|
||||
import threading
|
||||
from urllib.request import urlopen
|
||||
|
||||
from basicswap.basicswap import (
|
||||
BasicSwap,
|
||||
Coins,
|
||||
SwapTypes,
|
||||
BidStates,
|
||||
TxStates,
|
||||
SEQUENCE_LOCK_BLOCKS,
|
||||
)
|
||||
from basicswap.util import (
|
||||
COIN,
|
||||
toWIF,
|
||||
callrpc_cli,
|
||||
dumpje,
|
||||
)
|
||||
from basicswap.key import (
|
||||
ECKey,
|
||||
)
|
||||
from basicswap.http_server import (
|
||||
HttpThread,
|
||||
)
|
||||
|
||||
import basicswap.config as cfg
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.level = logging.DEBUG
|
||||
logger.addHandler(logging.StreamHandler(sys.stdout))
|
||||
|
||||
NUM_NODES = 3
|
||||
BASE_PORT = 14792
|
||||
BASE_RPC_PORT = 19792
|
||||
BASE_ZMQ_PORT = 20792
|
||||
PREFIX_SECRET_KEY_REGTEST = 0x2e
|
||||
TEST_HTML_PORT = 1800
|
||||
LTC_NODE = 3
|
||||
BTC_NODE = 4
|
||||
stop_test = False
|
||||
|
||||
|
||||
def prepareOtherDir(datadir, nodeId, conf_file='litecoin.conf'):
|
||||
node_dir = os.path.join(datadir, str(nodeId))
|
||||
if not os.path.exists(node_dir):
|
||||
os.makedirs(node_dir)
|
||||
filePath = os.path.join(node_dir, conf_file)
|
||||
|
||||
with open(filePath, 'w+') as fp:
|
||||
fp.write('regtest=1\n')
|
||||
fp.write('[regtest]\n')
|
||||
fp.write('port=' + str(BASE_PORT + nodeId) + '\n')
|
||||
fp.write('rpcport=' + str(BASE_RPC_PORT + nodeId) + '\n')
|
||||
|
||||
fp.write('daemon=0\n')
|
||||
fp.write('printtoconsole=0\n')
|
||||
fp.write('server=1\n')
|
||||
fp.write('discover=0\n')
|
||||
fp.write('listenonion=0\n')
|
||||
fp.write('bind=127.0.0.1\n')
|
||||
fp.write('findpeers=0\n')
|
||||
fp.write('debug=1\n')
|
||||
fp.write('debugexclude=libevent\n')
|
||||
|
||||
fp.write('acceptnonstdtxn=0\n')
|
||||
|
||||
|
||||
def prepareDir(datadir, nodeId, network_key, network_pubkey):
|
||||
node_dir = os.path.join(datadir, str(nodeId))
|
||||
if not os.path.exists(node_dir):
|
||||
os.makedirs(node_dir)
|
||||
filePath = os.path.join(node_dir, 'particl.conf')
|
||||
|
||||
with open(filePath, 'w+') as fp:
|
||||
fp.write('regtest=1\n')
|
||||
fp.write('[regtest]\n')
|
||||
fp.write('port=' + str(BASE_PORT + nodeId) + '\n')
|
||||
fp.write('rpcport=' + str(BASE_RPC_PORT + nodeId) + '\n')
|
||||
|
||||
fp.write('daemon=0\n')
|
||||
fp.write('printtoconsole=0\n')
|
||||
fp.write('server=1\n')
|
||||
fp.write('discover=0\n')
|
||||
fp.write('listenonion=0\n')
|
||||
fp.write('bind=127.0.0.1\n')
|
||||
fp.write('findpeers=0\n')
|
||||
fp.write('debug=1\n')
|
||||
fp.write('debugexclude=libevent\n')
|
||||
fp.write('zmqpubsmsg=tcp://127.0.0.1:' + str(BASE_ZMQ_PORT + nodeId) + '\n')
|
||||
|
||||
fp.write('acceptnonstdtxn=0\n')
|
||||
fp.write('minstakeinterval=5\n')
|
||||
|
||||
for i in range(0, NUM_NODES):
|
||||
if nodeId == i:
|
||||
continue
|
||||
fp.write('addnode=127.0.0.1:%d\n' % (BASE_PORT + i))
|
||||
|
||||
if nodeId < 2:
|
||||
fp.write('spentindex=1\n')
|
||||
fp.write('txindex=1\n')
|
||||
|
||||
basicswap_dir = os.path.join(datadir, str(nodeId), 'basicswap')
|
||||
if not os.path.exists(basicswap_dir):
|
||||
os.makedirs(basicswap_dir)
|
||||
|
||||
ltcdatadir = os.path.join(datadir, str(LTC_NODE))
|
||||
btcdatadir = os.path.join(datadir, str(BTC_NODE))
|
||||
settings_path = os.path.join(basicswap_dir, 'basicswap.json')
|
||||
settings = {
|
||||
'zmqhost': 'tcp://127.0.0.1',
|
||||
'zmqport': BASE_ZMQ_PORT + nodeId,
|
||||
'htmlhost': 'localhost',
|
||||
'htmlport': 12700 + nodeId,
|
||||
'network_key': network_key,
|
||||
'network_pubkey': network_pubkey,
|
||||
'chainclients': {
|
||||
'particl': {
|
||||
'connection_type': 'rpc',
|
||||
'manage_daemon': False,
|
||||
'rpcport': BASE_RPC_PORT + nodeId,
|
||||
'datadir': node_dir,
|
||||
'bindir': cfg.PARTICL_BINDIR,
|
||||
'blocks_confirmed': 2, # Faster testing
|
||||
},
|
||||
'litecoin': {
|
||||
'connection_type': 'rpc',
|
||||
'manage_daemon': False,
|
||||
'rpcport': BASE_RPC_PORT + LTC_NODE,
|
||||
'datadir': ltcdatadir,
|
||||
'bindir': cfg.LITECOIN_BINDIR,
|
||||
# 'use_segwit': True,
|
||||
},
|
||||
'bitcoin': {
|
||||
'connection_type': 'rpc',
|
||||
'manage_daemon': False,
|
||||
'rpcport': BASE_RPC_PORT + BTC_NODE,
|
||||
'datadir': btcdatadir,
|
||||
'bindir': cfg.BITCOIN_BINDIR,
|
||||
'use_segwit': True,
|
||||
}
|
||||
},
|
||||
'check_progress_seconds': 2,
|
||||
'check_watched_seconds': 4,
|
||||
'check_expired_seconds': 60
|
||||
}
|
||||
with open(settings_path, 'w') as fp:
|
||||
json.dump(settings, fp, indent=4)
|
||||
|
||||
|
||||
def startDaemon(nodeId, bin_dir=cfg.PARTICL_BINDIR, daemon_bin=cfg.PARTICLD):
|
||||
node_dir = os.path.join(cfg.DATADIRS, str(nodeId))
|
||||
daemon_bin = os.path.join(bin_dir, daemon_bin)
|
||||
|
||||
args = [daemon_bin, '-datadir=' + node_dir]
|
||||
logging.info('Starting node ' + str(nodeId) + ' ' + daemon_bin + ' ' + '-datadir=' + node_dir)
|
||||
return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
|
||||
def partRpc(cmd, node_id=0):
|
||||
return callrpc_cli(cfg.PARTICL_BINDIR, os.path.join(cfg.DATADIRS, str(node_id)), 'regtest', cmd, cfg.PARTICL_CLI)
|
||||
|
||||
|
||||
def btcRpc(cmd):
|
||||
return callrpc_cli(cfg.BITCOIN_BINDIR, os.path.join(cfg.DATADIRS, str(BTC_NODE)), 'regtest', cmd, cfg.BITCOIN_CLI)
|
||||
|
||||
|
||||
def ltcRpc(cmd):
|
||||
return callrpc_cli(cfg.LITECOIN_BINDIR, os.path.join(cfg.DATADIRS, str(LTC_NODE)), 'regtest', cmd, cfg.LITECOIN_CLI)
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
global stop_test
|
||||
print('signal {} detected.'.format(sig))
|
||||
stop_test = True
|
||||
|
||||
|
||||
def run_loop(self):
|
||||
while not stop_test:
|
||||
time.sleep(1)
|
||||
for c in self.swap_clients:
|
||||
c.update()
|
||||
ltcRpc('generatetoaddress 1 {}'.format(self.ltc_addr))
|
||||
btcRpc('generatetoaddress 1 {}'.format(self.btc_addr))
|
||||
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(Test, cls).setUpClass()
|
||||
|
||||
eckey = ECKey()
|
||||
eckey.generate()
|
||||
cls.network_key = toWIF(PREFIX_SECRET_KEY_REGTEST, eckey.get_bytes())
|
||||
cls.network_pubkey = eckey.get_pubkey().get_bytes().hex()
|
||||
|
||||
if os.path.isdir(cfg.DATADIRS):
|
||||
logging.info('Removing ' + cfg.DATADIRS)
|
||||
shutil.rmtree(cfg.DATADIRS)
|
||||
|
||||
for i in range(NUM_NODES):
|
||||
prepareDir(cfg.DATADIRS, i, cls.network_key, cls.network_pubkey)
|
||||
|
||||
prepareOtherDir(cfg.DATADIRS, LTC_NODE)
|
||||
prepareOtherDir(cfg.DATADIRS, BTC_NODE, 'bitcoin.conf')
|
||||
|
||||
cls.daemons = []
|
||||
cls.swap_clients = []
|
||||
|
||||
cls.daemons.append(startDaemon(BTC_NODE, cfg.BITCOIN_BINDIR, cfg.BITCOIND))
|
||||
logging.info('Started %s %d', cfg.BITCOIND, cls.daemons[-1].pid)
|
||||
cls.daemons.append(startDaemon(LTC_NODE, cfg.LITECOIN_BINDIR, cfg.LITECOIND))
|
||||
logging.info('Started %s %d', cfg.LITECOIND, cls.daemons[-1].pid)
|
||||
|
||||
for i in range(NUM_NODES):
|
||||
cls.daemons.append(startDaemon(i))
|
||||
logging.info('Started %s %d', cfg.PARTICLD, cls.daemons[-1].pid)
|
||||
time.sleep(1)
|
||||
for i in range(NUM_NODES):
|
||||
basicswap_dir = os.path.join(os.path.join(cfg.DATADIRS, str(i)), 'basicswap')
|
||||
settings_path = os.path.join(basicswap_dir, 'basicswap.json')
|
||||
with open(settings_path) as fs:
|
||||
settings = json.load(fs)
|
||||
fp = open(os.path.join(basicswap_dir, 'basicswap.log'), 'w')
|
||||
cls.swap_clients.append(BasicSwap(fp, basicswap_dir, settings, 'regtest', log_name='BasicSwap{}'.format(i)))
|
||||
cls.swap_clients[-1].start()
|
||||
cls.swap_clients[0].callrpc('extkeyimportmaster', ['abandon baby cabbage dad eager fabric gadget habit ice kangaroo lab absorb'])
|
||||
cls.swap_clients[1].callrpc('extkeyimportmaster', ['pact mammal barrel matrix local final lecture chunk wasp survey bid various book strong spread fall ozone daring like topple door fatigue limb olympic', '', 'true'])
|
||||
cls.swap_clients[1].callrpc('getnewextaddress', ['lblExtTest'])
|
||||
cls.swap_clients[1].callrpc('rescanblockchain')
|
||||
|
||||
num_blocks = 500
|
||||
logging.info('Mining %d litecoin blocks', num_blocks)
|
||||
cls.ltc_addr = ltcRpc('getnewaddress mining_addr legacy')
|
||||
ltcRpc('generatetoaddress {} {}'.format(num_blocks, cls.ltc_addr))
|
||||
|
||||
ro = ltcRpc('getblockchaininfo')
|
||||
assert(ro['bip9_softforks']['csv']['status'] == 'active')
|
||||
assert(ro['bip9_softforks']['segwit']['status'] == 'active')
|
||||
|
||||
cls.btc_addr = btcRpc('getnewaddress mining_addr bech32')
|
||||
logging.info('Mining %d bitcoin blocks to %s', num_blocks, cls.btc_addr)
|
||||
btcRpc('generatetoaddress {} {}'.format(num_blocks, cls.btc_addr))
|
||||
|
||||
ro = btcRpc('getblockchaininfo')
|
||||
assert(ro['bip9_softforks']['csv']['status'] == 'active')
|
||||
assert(ro['bip9_softforks']['segwit']['status'] == 'active')
|
||||
|
||||
ro = ltcRpc('getwalletinfo')
|
||||
print('ltcRpc', ro)
|
||||
|
||||
cls.http_threads = []
|
||||
host = '0.0.0.0' # All interfaces (docker)
|
||||
for i in range(3):
|
||||
t = HttpThread(cls.swap_clients[i].fp, host, TEST_HTML_PORT + i, False, cls.swap_clients[i])
|
||||
cls.http_threads.append(t)
|
||||
t.start()
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
cls.update_thread = threading.Thread(target=run_loop, args=(cls,))
|
||||
cls.update_thread.start()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
global stop_test
|
||||
logging.info('Finalising')
|
||||
stop_test = True
|
||||
cls.update_thread.join()
|
||||
for t in cls.http_threads:
|
||||
t.stop()
|
||||
t.join()
|
||||
for c in cls.swap_clients:
|
||||
c.fp.close()
|
||||
for d in cls.daemons:
|
||||
logging.info('Terminating %d', d.pid)
|
||||
d.terminate()
|
||||
d.wait(timeout=10)
|
||||
if d.stdout:
|
||||
d.stdout.close()
|
||||
if d.stderr:
|
||||
d.stderr.close()
|
||||
if d.stdin:
|
||||
d.stdin.close()
|
||||
|
||||
super(Test, cls).tearDownClass()
|
||||
|
||||
def wait_for_offer(self, swap_client, offer_id):
|
||||
logging.info('wait_for_offer %s', offer_id.hex())
|
||||
for i in range(20):
|
||||
time.sleep(1)
|
||||
offers = swap_client.listOffers()
|
||||
for offer in offers:
|
||||
if offer.offer_id == offer_id:
|
||||
return
|
||||
raise ValueError('wait_for_offer timed out.')
|
||||
|
||||
def wait_for_bid(self, swap_client, bid_id):
|
||||
logging.info('wait_for_bid %s', bid_id.hex())
|
||||
for i in range(20):
|
||||
time.sleep(1)
|
||||
bids = swap_client.listBids()
|
||||
for bid in bids:
|
||||
if bid.bid_id == bid_id and bid.was_received:
|
||||
return
|
||||
raise ValueError('wait_for_bid timed out.')
|
||||
|
||||
def wait_for_in_progress(self, swap_client, bid_id, sent=False):
|
||||
logging.info('wait_for_in_progress %s', bid_id.hex())
|
||||
for i in range(20):
|
||||
time.sleep(1)
|
||||
swaps = swap_client.listSwapsInProgress()
|
||||
for b in swaps:
|
||||
if b[0] == bid_id:
|
||||
return
|
||||
raise ValueError('wait_for_in_progress timed out.')
|
||||
|
||||
def wait_for_bid_state(self, swap_client, bid_id, state, sent=False, seconds_for=30):
|
||||
logging.info('wait_for_bid_state %s %s', bid_id.hex(), str(state))
|
||||
for i in range(seconds_for):
|
||||
time.sleep(1)
|
||||
bid = swap_client.getBid(bid_id)
|
||||
if bid.state >= state:
|
||||
return
|
||||
raise ValueError('wait_for_bid_state timed out.')
|
||||
|
||||
def wait_for_bid_tx_state(self, swap_client, bid_id, initiate_state, participate_state, seconds_for=30):
|
||||
logging.info('wait_for_bid_tx_state %s %s %s', bid_id.hex(), str(initiate_state), str(participate_state))
|
||||
for i in range(seconds_for):
|
||||
time.sleep(1)
|
||||
bid = swap_client.getBid(bid_id)
|
||||
if (initiate_state is None or bid.initiate_txn_state == initiate_state) \
|
||||
and (participate_state is None or bid.participate_txn_state == participate_state):
|
||||
return
|
||||
raise ValueError('wait_for_bid_tx_state timed out.')
|
||||
|
||||
def test_01_verifyrawtransaction(self):
|
||||
txn = '0200000001eb6e5c4ebba4efa32f40c7314cad456a64008e91ee30b2dd0235ab9bb67fbdbb01000000ee47304402200956933242dde94f6cf8f195a470f8d02aef21ec5c9b66c5d3871594bdb74c9d02201d7e1b440de8f4da672d689f9e37e98815fb63dbc1706353290887eb6e8f7235012103dc1b24feb32841bc2f4375da91fa97834e5983668c2a39a6b7eadb60e7033f9d205a803b28fe2f86c17db91fa99d7ed2598f79b5677ffe869de2e478c0d1c02cc7514c606382012088a8201fe90717abb84b481c2a59112414ae56ec8acc72273642ca26cc7a5812fdc8f68876a914225fbfa4cb725b75e511810ac4d6f74069bdded26703520140b27576a914207eb66b2fd6ed9924d6217efc7fa7b38dfabe666888acffffffff01e0167118020000001976a9140044e188928710cecba8311f1cf412135b98145c88ac00000000'
|
||||
prevout = {
|
||||
'txid': 'bbbd7fb69bab3502ddb230ee918e00646a45ad4c31c7402fa3efa4bb4e5c6eeb',
|
||||
'vout': 1,
|
||||
'scriptPubKey': 'a9143d37191e8b864222d14952a14c85504677a0581d87',
|
||||
'redeemScript': '6382012088a8201fe90717abb84b481c2a59112414ae56ec8acc72273642ca26cc7a5812fdc8f68876a914225fbfa4cb725b75e511810ac4d6f74069bdded26703520140b27576a914207eb66b2fd6ed9924d6217efc7fa7b38dfabe666888ac',
|
||||
'amount': 1.0}
|
||||
ro = partRpc('verifyrawtransaction {} "{}"'.format(txn, dumpje([prevout, ])))
|
||||
assert(ro['inputs_valid'] is False)
|
||||
assert(ro['validscripts'] == 1)
|
||||
|
||||
prevout['amount'] = 100.0
|
||||
ro = partRpc('verifyrawtransaction {} "{}"'.format(txn, dumpje([prevout, ])))
|
||||
assert(ro['inputs_valid'] is True)
|
||||
assert(ro['validscripts'] == 1)
|
||||
|
||||
txn = 'a000000000000128e8ba6a28673f2ebb5fd983b27a791fd1888447a47638b3cd8bfdd3f54a6f1e0100000000a90040000101e0c69a3b000000001976a9146c0f1ea47ca2bf84ed87bf3aa284e18748051f5788ac04473044022026b01f3a90e46883949404141467b741cd871722a4aaae8ddc8c4d6ab6fb1c77022047a2f3be2dcbe4c51837d2d5e0329aaa8a13a8186b03186b127cc51185e4f3ab012103dc1b24feb32841bc2f4375da91fa97834e5983668c2a39a6b7eadb60e7033f9d0100606382012088a8201fe90717abb84b481c2a59112414ae56ec8acc72273642ca26cc7a5812fdc8f68876a914207eb66b2fd6ed9924d6217efc7fa7b38dfabe666703a90040b27576a914225fbfa4cb725b75e511810ac4d6f74069bdded26888ac'
|
||||
prevout = {
|
||||
'txid': '1e6f4af5d3fd8bcdb33876a4478488d11f797ab283d95fbb2e3f67286abae828',
|
||||
'vout': 1,
|
||||
'scriptPubKey': 'a914129aee070317bbbd57062288849e85cf57d15c2687',
|
||||
'redeemScript': '6382012088a8201fe90717abb84b481c2a59112414ae56ec8acc72273642ca26cc7a5812fdc8f68876a914207eb66b2fd6ed9924d6217efc7fa7b38dfabe666703a90040b27576a914225fbfa4cb725b75e511810ac4d6f74069bdded26888ac',
|
||||
'amount': 1.0}
|
||||
ro = partRpc('verifyrawtransaction {} "{}"'.format(txn, dumpje([prevout, ])))
|
||||
assert(ro['inputs_valid'] is False)
|
||||
assert(ro['validscripts'] == 0) # Amount covered by signature
|
||||
|
||||
prevout['amount'] = 90.0
|
||||
ro = partRpc('verifyrawtransaction {} "{}"'.format(txn, dumpje([prevout, ])))
|
||||
assert(ro['inputs_valid'] is True)
|
||||
assert(ro['validscripts'] == 1)
|
||||
|
||||
def test_02_part_ltc(self):
|
||||
swap_clients = self.swap_clients
|
||||
|
||||
logging.info('---------- Test PART to LTC')
|
||||
offer_id = swap_clients[0].postOffer(Coins.PART, Coins.LTC, 100 * COIN, 0.1 * COIN, 100 * COIN, SwapTypes.SELLER_FIRST)
|
||||
|
||||
self.wait_for_offer(swap_clients[1], offer_id)
|
||||
offers = swap_clients[1].listOffers()
|
||||
assert(len(offers) == 1)
|
||||
for offer in offers:
|
||||
if offer.offer_id == offer_id:
|
||||
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
|
||||
|
||||
self.wait_for_bid(swap_clients[0], bid_id)
|
||||
|
||||
swap_clients[0].acceptBid(bid_id)
|
||||
|
||||
self.wait_for_in_progress(swap_clients[1], bid_id, sent=True)
|
||||
|
||||
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
|
||||
self.wait_for_bid_state(swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, seconds_for=60)
|
||||
|
||||
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
|
||||
js_1 = json.loads(urlopen('http://localhost:1801/json').read())
|
||||
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
|
||||
assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
|
||||
|
||||
def test_03_ltc_part(self):
|
||||
swap_clients = self.swap_clients
|
||||
|
||||
logging.info('---------- Test LTC to PART')
|
||||
offer_id = swap_clients[1].postOffer(Coins.LTC, Coins.PART, 10 * COIN, 9.0 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST)
|
||||
|
||||
self.wait_for_offer(swap_clients[0], offer_id)
|
||||
offers = swap_clients[0].listOffers()
|
||||
for offer in offers:
|
||||
if offer.offer_id == offer_id:
|
||||
bid_id = swap_clients[0].postBid(offer_id, offer.amount_from)
|
||||
|
||||
self.wait_for_bid(swap_clients[1], bid_id)
|
||||
swap_clients[1].acceptBid(bid_id)
|
||||
|
||||
self.wait_for_in_progress(swap_clients[0], bid_id, sent=True)
|
||||
|
||||
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, sent=True, seconds_for=60)
|
||||
self.wait_for_bid_state(swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
|
||||
|
||||
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
|
||||
js_1 = json.loads(urlopen('http://localhost:1801/json').read())
|
||||
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
|
||||
assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
|
||||
|
||||
def test_04_ltc_btc(self):
|
||||
swap_clients = self.swap_clients
|
||||
|
||||
logging.info('---------- Test LTC to BTC')
|
||||
offer_id = swap_clients[0].postOffer(Coins.LTC, Coins.BTC, 10 * COIN, 0.1 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST)
|
||||
|
||||
self.wait_for_offer(swap_clients[1], offer_id)
|
||||
offers = swap_clients[1].listOffers()
|
||||
for offer in offers:
|
||||
if offer.offer_id == offer_id:
|
||||
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
|
||||
|
||||
self.wait_for_bid(swap_clients[0], bid_id)
|
||||
swap_clients[0].acceptBid(bid_id)
|
||||
|
||||
self.wait_for_in_progress(swap_clients[1], bid_id, sent=True)
|
||||
|
||||
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
|
||||
self.wait_for_bid_state(swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, seconds_for=60)
|
||||
|
||||
js_0bid = json.loads(urlopen('http://localhost:1800/json/bids/{}'.format(bid_id.hex())).read())
|
||||
|
||||
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
|
||||
js_1 = json.loads(urlopen('http://localhost:1801/json').read())
|
||||
|
||||
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
|
||||
assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
|
||||
|
||||
def test_05_refund(self):
|
||||
# Seller submits initiate txn, buyer doesn't respond
|
||||
swap_clients = self.swap_clients
|
||||
|
||||
logging.info('---------- Test refund, LTC to BTC')
|
||||
offer_id = swap_clients[0].postOffer(Coins.LTC, Coins.BTC, 10 * COIN, 0.1 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST,
|
||||
SEQUENCE_LOCK_BLOCKS, 10)
|
||||
|
||||
self.wait_for_offer(swap_clients[1], offer_id)
|
||||
offers = swap_clients[1].listOffers()
|
||||
for offer in offers:
|
||||
if offer.offer_id == offer_id:
|
||||
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
|
||||
|
||||
self.wait_for_bid(swap_clients[0], bid_id)
|
||||
swap_clients[1].abandonBid(bid_id)
|
||||
swap_clients[0].acceptBid(bid_id)
|
||||
|
||||
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
|
||||
self.wait_for_bid_state(swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, seconds_for=60)
|
||||
|
||||
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
|
||||
js_1 = json.loads(urlopen('http://localhost:1801/json').read())
|
||||
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
|
||||
assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
|
||||
|
||||
def test_06_self_bid(self):
|
||||
swap_clients = self.swap_clients
|
||||
|
||||
logging.info('---------- Test same client, BTC to LTC')
|
||||
|
||||
js_0_before = json.loads(urlopen('http://localhost:1800/json').read())
|
||||
|
||||
offer_id = swap_clients[0].postOffer(Coins.LTC, Coins.BTC, 10 * COIN, 10 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST)
|
||||
|
||||
self.wait_for_offer(swap_clients[0], offer_id)
|
||||
offers = swap_clients[0].listOffers()
|
||||
for offer in offers:
|
||||
if offer.offer_id == offer_id:
|
||||
bid_id = swap_clients[0].postBid(offer_id, offer.amount_from)
|
||||
|
||||
self.wait_for_bid(swap_clients[0], bid_id)
|
||||
swap_clients[0].acceptBid(bid_id)
|
||||
|
||||
self.wait_for_bid_tx_state(swap_clients[0], bid_id, TxStates.TX_REDEEMED, TxStates.TX_REDEEMED, seconds_for=60)
|
||||
self.wait_for_bid_state(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, seconds_for=60)
|
||||
|
||||
js_0 = json.loads(urlopen('http://localhost:1800/json').read())
|
||||
assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
|
||||
assert(js_0['num_recv_bids'] == js_0_before['num_recv_bids'] + 1 and js_0['num_sent_bids'] == js_0_before['num_sent_bids'] + 1)
|
||||
|
||||
def pass_99_delay(self):
|
||||
global stop_test
|
||||
logging.info('Delay')
|
||||
for i in range(60 * 5):
|
||||
if stop_test:
|
||||
break
|
||||
time.sleep(1)
|
||||
print('delay', i)
|
||||
stop_test = True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Loading…
Reference in a new issue