diff --git a/.travis.yml b/.travis.yml index 03f8b87..e98ca0d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,7 +39,7 @@ jobs: - 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 + - PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841 --exclude=key.py,messages_pb2.py,.eggs + - codespell --check-filenames --disable-colors --quiet-level=7 -S .git,.eggs after_success: - echo "End lint" diff --git a/README.md b/README.md index 921ca81..e61885f 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Features still required (of many): - Ability to swap coin-types without running nodes for all coin-types - More swap protocols - Method to load mnemonic into Particl. + - COIN must be defined per coin. ## Seller first protocol: diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index a635241..657b9f4 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -351,6 +351,7 @@ class Bid(Base): initiate_spend_txid = sa.Column(sa.LargeBinary) initiate_spend_n = sa.Column(sa.Integer) + initiate_txn_height = sa.Column(sa.Integer) initiate_txn_state = sa.Column(sa.Integer) initiate_txn_states = sa.Column(sa.LargeBinary) # Packed states and times @@ -364,6 +365,7 @@ class Bid(Base): participate_spend_txid = sa.Column(sa.LargeBinary) participate_spend_n = sa.Column(sa.Integer) + participate_txn_height = sa.Column(sa.Integer) participate_txn_state = sa.Column(sa.Integer) participate_txn_states = sa.Column(sa.LargeBinary) # Packed states and times @@ -371,6 +373,8 @@ class Bid(Base): state_time = sa.Column(sa.BigInteger) # timestamp of last state change states = sa.Column(sa.LargeBinary) # Packed states and times + state_note = sa.Column(sa.String) + def setITXState(self, new_state): self.initiate_txn_state = new_state if self.initiate_txn_states is None: @@ -1438,6 +1442,8 @@ class BasicSwap(): self.coin_clients[coin_type]['last_height_checked'] = tx_height self.log.debug('Rewind checking of %s chain to height %d', chain_name, tx_height) + return tx_height + def addParticipateTxn(self, bid_id, bid, coin_type, txid_hex, vout, tx_height): bid.participate_txid = bytes.fromhex(txid_hex) bid.participate_txn_n = vout @@ -1447,7 +1453,7 @@ class BasicSwap(): self.log.debug('Watching %s chain for spend of output %s %d', chain_name, txid_hex, vout) # TODO: Check connection type - self.setLastHeightChecked(coin_type, tx_height) + bid.participate_txn_height = self.setLastHeightChecked(coin_type, tx_height) self.log.debug('Adding watched output %s bid %s tx %s type %s', coin_type, bid_id.hex(), txid_hex, BidStates.SWAP_PARTICIPATING) self.coin_clients[coin_type]['watched_outputs'].append((bid_id, txid_hex, vout, BidStates.SWAP_PARTICIPATING)) @@ -1556,12 +1562,12 @@ class BasicSwap(): if bid.initiate_txn_n is None: bid.initiate_txn_n = index # Start checking for spends of initiate_txn before fully confirmed - self.setLastHeightChecked(coin_from, tx_height) + bid.initiate_txn_height = self.setLastHeightChecked(coin_from, tx_height) self.log.debug('Adding watched output %s bid %s tx %s type %s', coin_from, bid_id.hex(), initiate_txnid_hex, BidStates.SWAP_INITIATED) self.coin_clients[coin_from]['watched_outputs'].append((bid_id, initiate_txnid_hex, bid.initiate_txn_n, BidStates.SWAP_INITIATED)) if bid.initiate_txn_state is None or bid.initiate_txn_state < TxStates.TX_SENT: bid.setITXState(TxStates.TX_SENT) - save_bid = True + save_bid = True if bid.initiate_txn_conf >= self.coin_clients[coin_from]['blocks_confirmed']: self.initiateTxnConfirmed(bid_id, bid, offer) @@ -2112,7 +2118,7 @@ class BasicSwap(): 'deposit_address': self.getCachedAddressForCoin(coin), 'name': chainparams[coin]['name'].capitalize(), 'blocks': blockchaininfo['blocks'], - 'balance': walletinfo.get('total_balance', walletinfo['balance']), + 'balance': format8(walletinfo.get('total_balance', walletinfo['balance']) * COIN), 'synced': '{0:.2f}'.format(round(blockchaininfo['verificationprogress'], 2)), } return rv diff --git a/basicswap/config.py b/basicswap/config.py index 78b88af..00b569f 100644 --- a/basicswap/config.py +++ b/basicswap/config.py @@ -9,16 +9,16 @@ 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') +PARTICLD = os.getenv('PARTICLD', 'particld' + ('.exe' if os.name == 'nt' else '')) +PARTICL_CLI = os.getenv('PARTICL_CLI', 'particl-cli' + ('.exe' if os.name == 'nt' else '')) +PARTICL_TX = os.getenv('PARTICL_TX', 'particl-tx' + ('.exe' if os.name == 'nt' else '')) 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') +BITCOIND = os.getenv('BITCOIND', 'bitcoind' + ('.exe' if os.name == 'nt' else '')) +BITCOIN_CLI = os.getenv('BITCOIN_CLI', 'bitcoin-cli' + ('.exe' if os.name == 'nt' else '')) +BITCOIN_TX = os.getenv('BITCOIN_TX', 'bitcoin-tx' + ('.exe' if os.name == 'nt' else '')) 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') +LITECOIND = os.getenv('LITECOIND', 'litecoind' + ('.exe' if os.name == 'nt' else '')) +LITECOIN_CLI = os.getenv('LITECOIN_CLI', 'litecoin-cli' + ('.exe' if os.name == 'nt' else '')) +LITECOIN_TX = os.getenv('LITECOIN_TX', 'litecoin-tx' + ('.exe' if os.name == 'nt' else '')) diff --git a/basicswap/http_server.py b/basicswap/http_server.py index aa4fba6..d73f567 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -130,7 +130,7 @@ class HttpHandler(BaseHTTPRequestHandler): cid = str(int(k)) content += '

' + w['name'] + '

' \ + '' \ - + '' \ + + '' \ + '' \ + '' \ + '' \ diff --git a/bin/basicswap-prepare.py b/bin/basicswap-prepare.py new file mode 100644 index 0000000..ed0fb6e --- /dev/null +++ b/bin/basicswap-prepare.py @@ -0,0 +1,117 @@ +#!/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 + +sudo pip install python-gnupg + +""" + +import sys +import os +import subprocess +import time +import json +import hashlib +import mmap +import tarfile +import urllib.request +import urllib.parse +import logging + +import gnupg + + +logger = logging.getLogger() +logger.level = logging.DEBUG + + +def printVersion(): + from basicswap import __version__ + logger.info('Basicswap version:', __version__) + + +def printHelp(): + logger.info('Usage: basicswap-prepare ') + logger.info('\n--help, -h Print help.') + logger.info('\n--version, -v Print version.') + logger.info('\n--datadir=PATH Path to basicswap data directory, default:~/.basicswap.') + logger.info('\n--mainnet Run in mainnet mode.') + logger.info('\n--testnet Run in testnet mode.') + logger.info('\n--regtest Run in regtest mode.') + logger.info('\n--particl_mnemonic= Recovery phrase to use for the Particl wallet, default is randomly generated.') + + +def main(): + print('main') + data_dir = None + chain = 'mainnet' + particl_wallet_mnemonic = None + + for v in sys.argv[1:]: + if len(v) < 2 or v[0] != '-': + logger.warning('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 == 'mainnet': + continue + 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 + if name == 'particl_mnemonic': + particl_wallet_mnemonic = s[1] + continue + + logger.warning('Unknown argument', v) + + if data_dir is None: + default_datadir = '~/.basicswap' + data_dir = os.path.join(os.path.expanduser(default_datadir)) + logger.info('Using datadir: %s', data_dir) + logger.info('Chain: %s', chain) + + if not os.path.exists(data_dir): + os.makedirs(data_dir) + + config_path = os.path.join(data_dir, 'basicswap.json') + if os.path.exists(config_path): + sys.stderr.write('Error: {} exists, exiting.'.format(config_path)) + exit(1) + + settings = { + 'debug': True, + } + + with open(config_path, 'w') as fp: + json.dump(settings, fp, indent=4) + + logger.info('Done.') + +if __name__ == '__main__': + main() diff --git a/bin/basicswap-run.py b/bin/basicswap-run.py deleted file mode 120000 index 0bf5b17..0000000 --- a/bin/basicswap-run.py +++ /dev/null @@ -1 +0,0 @@ -basicswap_run.py \ No newline at end of file diff --git a/bin/basicswap-run.py b/bin/basicswap-run.py new file mode 100644 index 0000000..cccf364 --- /dev/null +++ b/bin/basicswap-run.py @@ -0,0 +1,181 @@ +#!/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 logging + +import basicswap.config as cfg +from basicswap import __version__ +from basicswap.basicswap import BasicSwap +from basicswap.http_server import HttpThread + + +logger = logging.getLogger() +logger.level = logging.DEBUG +logger.addHandler(logging.StreamHandler(sys.stdout)) + +ALLOW_CORS = False +swap_client = None + + +def signal_handler(sig, frame): + logger.info('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] + logger.info('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: + logger.info('Starting {} daemon'.format(c.capitalize())) + if c == 'particl': + daemons.append(startDaemon(v['datadir'], cfg.PARTICL_BINDIR, cfg.PARTICLD)) + logger.info('Started {} {}'.format(cfg.PARTICLD, daemons[-1].pid)) + elif c == 'bitcoin': + daemons.append(startDaemon(v['datadir'], cfg.BITCOIN_BINDIR, cfg.BITCOIND)) + logger.info('Started {} {}'.format(cfg.BITCOIND, daemons[-1].pid)) + elif c == 'litecoin': + daemons.append(startDaemon(v['datadir'], cfg.LITECOIN_BINDIR, cfg.LITECOIND)) + logger.info('Started {} {}'.format(cfg.LITECOIND, daemons[-1].pid)) + else: + logger.warning('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: + logger.info('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: + logger.info('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(): + logger.info('Basicswap version:', __version__) + + +def printHelp(): + logger.info('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] != '-': + logger.warning('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 + + logger.warning('Unknown argument', v) + + if data_dir is None: + default_datadir = '~/.basicswap' + data_dir = os.path.join(os.path.expanduser(default_datadir)) + logger.info('Using datadir:', data_dir) + logger.info('Chain:', chain) + + if not os.path.exists(data_dir): + os.makedirs(data_dir) + + with open(os.path.join(data_dir, 'basicswap.log'), 'a') as fp: + logger.info(os.path.basename(sys.argv[0]) + ', version: ' + __version__ + '\n\n') + runClient(fp, data_dir, chain) + + logger.info('Done.') + return swap_client.fail_code if swap_client is not None else 0 + + +if __name__ == '__main__': + main() diff --git a/bin/basicswap_run.py b/bin/basicswap_run.py deleted file mode 100644 index b79a670..0000000 --- a/bin/basicswap_run.py +++ /dev/null @@ -1,182 +0,0 @@ -#!/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 logging - -import basicswap.config as cfg -from basicswap import __version__ -from basicswap.basicswap import BasicSwap -from basicswap.http_server import HttpThread - - -logger = logging.getLogger() -logger.level = logging.DEBUG -logger.addHandler(logging.StreamHandler(sys.stdout)) - -ALLOW_CORS = False -swap_client = None - - -def signal_handler(sig, frame): - logger.info('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] - logger.info('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: - logger.info('Starting {} daemon'.format(c.capitalize())) - if c == 'particl': - daemons.append(startDaemon(v['datadir'], cfg.PARTICL_BINDIR, cfg.PARTICLD)) - logger.info('Started {} {}'.format(cfg.PARTICLD, daemons[-1].pid)) - elif c == 'bitcoin': - daemons.append(startDaemon(v['datadir'], cfg.BITCOIN_BINDIR, cfg.BITCOIND)) - logger.info('Started {} {}'.format(cfg.BITCOIND, daemons[-1].pid)) - elif c == 'litecoin': - daemons.append(startDaemon(v['datadir'], cfg.LITECOIN_BINDIR, cfg.LITECOIND)) - logger.info('Started {} {}'.format(cfg.LITECOIND, daemons[-1].pid)) - else: - logger.warning('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: - logger.info('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: - logger.info('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(): - logger.info('Basicswap version:', __version__) - - -def printHelp(): - logger.info('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] != '-': - logger.warning('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 - - logger.warning('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': - logger.info('chain:', chain) - - if not os.path.exists(data_dir): - os.makedirs(data_dir) - - with open(os.path.join(data_dir, 'basicswap.log'), 'a') as fp: - logger.info(os.path.basename(sys.argv[0]) + ', version: ' + __version__ + '\n\n') - runClient(fp, data_dir, chain) - - logger.info('Done.') - return swap_client.fail_code if swap_client is not None else 0 - - -if __name__ == '__main__': - main() diff --git a/bin/start_docker.bat b/bin/start_docker.bat deleted file mode 100644 index e69de29..0000000 diff --git a/setup.py b/setup.py index 2d87b73..28ce73f 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ setuptools.setup( "pyzmq", "protobuf", "sqlalchemy", + "python-gnupg", ], entry_points={ "console_scripts": [ diff --git a/tests/__init__.py b/tests/__init__.py index 84e7855..48ce776 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,11 +1,14 @@ import unittest -import tests.test_run import tests.test_other +import tests.test_prepare +import tests.test_run def test_suite(): loader = unittest.TestLoader() - suite = loader.loadTestsFromModule(tests.test_run) suite.addTests(loader.loadTestsFromModule(tests.test_other)) + suite.addTests(loader.loadTestsFromModule(tests.test_prepare)) + suite = loader.loadTestsFromModule(tests.test_run) + return suite diff --git a/tests/test_prepare.py b/tests/test_prepare.py new file mode 100644 index 0000000..9c59f31 --- /dev/null +++ b/tests/test_prepare.py @@ -0,0 +1,51 @@ +#!/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 os +import sys +import unittest +from unittest.mock import patch +from io import StringIO +import logging +import shutil +import importlib + +prepareSystem = importlib.import_module('bin.basicswap-prepare') +test_path = os.path.expanduser('~/test_basicswap') + +logger = logging.getLogger() +logger.level = logging.DEBUG + + +class Test(unittest.TestCase): + @classmethod + def tearDownClass(self): + try: + shutil.rmtree(test_path) + except Exception as e: + logger.warning('tearDownClass %s', str(e)) + + def test_no_overwrite(self): + testargs = ['basicswap-prepare', '-datadir=' + test_path] + with patch.object(sys, 'argv', testargs): + prepareSystem.main() + + self.assertTrue(os.path.exists(os.path.join(test_path, 'basicswap.json'))) + + testargs = ['basicswap-prepare', '-datadir=' + test_path] + with patch('sys.stderr', new=StringIO()) as fake_stderr: + with patch.object(sys, 'argv', testargs): + with self.assertRaises(SystemExit) as cm: + prepareSystem.main() + + self.assertEqual(cm.exception.code, 1) + logger.info('fake_stderr.getvalue() %s', fake_stderr.getvalue()) + self.assertTrue('exists, exiting' in fake_stderr.getvalue()) + + +if __name__ == '__main__': + unittest.main()
Balance:' + str(w['balance']) + '
Balance:' + w['balance'] + '
Blocks:' + str(w['blocks']) + '
Synced:' + str(w['synced']) + '
' + str(w['deposit_address']) + '