Add to Github
This commit is contained in:
commit
e242f50b2b
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 New Issue
Block a user