|
|
|
#!/usr/bin/env python3
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
# Copyright (c) 2019-2022 tecnovert
|
|
|
|
# Distributed under the MIT software license, see the accompanying
|
|
|
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
|
|
|
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import json
|
|
|
|
import mmap
|
|
|
|
import stat
|
|
|
|
import gnupg
|
|
|
|
import socks
|
|
|
|
import shutil
|
|
|
|
import signal
|
|
|
|
import socket
|
|
|
|
import hashlib
|
|
|
|
import tarfile
|
|
|
|
import zipfile
|
|
|
|
import logging
|
|
|
|
import platform
|
|
|
|
import urllib.parse
|
|
|
|
from urllib.request import urlretrieve
|
|
|
|
|
|
|
|
import basicswap.config as cfg
|
|
|
|
from basicswap.rpc import (
|
|
|
|
callrpc_cli,
|
|
|
|
waitForRPC,
|
|
|
|
)
|
|
|
|
from basicswap.base import getaddrinfo_tor
|
|
|
|
from basicswap.basicswap import BasicSwap
|
|
|
|
from basicswap.chainparams import Coins
|
|
|
|
from basicswap.util import toBool
|
|
|
|
from basicswap.util.rfc2440 import rfc2440_hash_password
|
|
|
|
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
|
|
|
|
from bin.basicswap_run import startDaemon, startXmrWalletDaemon
|
|
|
|
|
|
|
|
PARTICL_VERSION = os.getenv('PARTICL_VERSION', '0.21.2.9')
|
|
|
|
PARTICL_VERSION_TAG = os.getenv('PARTICL_VERSION_TAG', '')
|
|
|
|
PARTICL_LINUX_EXTRA = os.getenv('PARTICL_LINUX_EXTRA', '_nousb')
|
|
|
|
|
|
|
|
LITECOIN_VERSION = os.getenv('LITECOIN_VERSION', '0.18.1')
|
|
|
|
LITECOIN_VERSION_TAG = os.getenv('LITECOIN_VERSION_TAG', '')
|
|
|
|
|
|
|
|
BITCOIN_VERSION = os.getenv('BITCOIN_VERSION', '22.0')
|
|
|
|
BITCOIN_VERSION_TAG = os.getenv('BITCOIN_VERSION_TAG', '')
|
|
|
|
|
|
|
|
MONERO_VERSION = os.getenv('MONERO_VERSION', '0.17.3.0')
|
|
|
|
MONERO_VERSION_TAG = os.getenv('MONERO_VERSION_TAG', '')
|
|
|
|
|
|
|
|
# version, version tag eg. "rc1", signers
|
|
|
|
known_coins = {
|
|
|
|
'particl': (PARTICL_VERSION, PARTICL_VERSION_TAG, ('tecnovert',)),
|
|
|
|
'litecoin': (LITECOIN_VERSION, LITECOIN_VERSION_TAG, ('thrasher',)),
|
|
|
|
'bitcoin': (BITCOIN_VERSION, BITCOIN_VERSION_TAG, ('laanwj',)),
|
|
|
|
'namecoin': ('0.18.0', '', ('JeremyRand',)),
|
|
|
|
'monero': (MONERO_VERSION, MONERO_VERSION_TAG, ('binaryfate',)),
|
|
|
|
}
|
|
|
|
|
|
|
|
expected_key_ids = {
|
|
|
|
'tecnovert': ('13F13651C9CF0D6B',),
|
|
|
|
'thrasher': ('FE3348877809386C',),
|
|
|
|
'laanwj': ('1E4AED62986CD25D',),
|
|
|
|
'JeremyRand': ('2DBE339E29F6294C',),
|
|
|
|
'binaryfate': ('F0AF4D462A0BDF92',),
|
|
|
|
}
|
|
|
|
|
|
|
|
if platform.system() == 'Darwin':
|
|
|
|
BIN_ARCH = 'osx64'
|
|
|
|
FILE_EXT = 'tar.gz'
|
|
|
|
elif platform.system() == 'Windows':
|
|
|
|
BIN_ARCH = 'win64'
|
|
|
|
FILE_EXT = 'zip'
|
|
|
|
else:
|
|
|
|
BIN_ARCH = 'x86_64-linux-gnu'
|
|
|
|
FILE_EXT = 'tar.gz'
|
|
|
|
|
|
|
|
logger = logging.getLogger()
|
|
|
|
logger.level = logging.DEBUG
|
|
|
|
if not len(logger.handlers):
|
|
|
|
logger.addHandler(logging.StreamHandler(sys.stdout))
|
|
|
|
|
|
|
|
XMR_RPC_HOST = os.getenv('XMR_RPC_HOST', '127.0.0.1')
|
|
|
|
BASE_XMR_RPC_PORT = int(os.getenv('BASE_XMR_RPC_PORT', 29798))
|
|
|
|
BASE_XMR_ZMQ_PORT = int(os.getenv('BASE_XMR_ZMQ_PORT', 30898))
|
|
|
|
BASE_XMR_WALLET_PORT = int(os.getenv('BASE_XMR_WALLET_PORT', 29998))
|
|
|
|
XMR_WALLET_RPC_HOST = os.getenv('XMR_WALLET_RPC_HOST', '127.0.0.1')
|
|
|
|
XMR_WALLET_RPC_USER = os.getenv('XMR_WALLET_RPC_USER', 'xmr_wallet_user')
|
|
|
|
XMR_WALLET_RPC_PWD = os.getenv('XMR_WALLET_RPC_PWD', 'xmr_wallet_pwd')
|
|
|
|
XMR_SITE_COMMIT = 'be137696c3a1d93bfaddf619da0d05a3e6cee4f2' # Lock hashes.txt to monero version
|
|
|
|
|
|
|
|
DEFAULT_XMR_RESTORE_HEIGHT = 2245107
|
|
|
|
|
|
|
|
UI_HTML_PORT = int(os.getenv('UI_HTML_PORT', 12700))
|
|
|
|
PART_ZMQ_PORT = int(os.getenv('PART_ZMQ_PORT', 20792))
|
|
|
|
|
|
|
|
PART_RPC_HOST = os.getenv('PART_RPC_HOST', '127.0.0.1')
|
|
|
|
LTC_RPC_HOST = os.getenv('LTC_RPC_HOST', '127.0.0.1')
|
|
|
|
BTC_RPC_HOST = os.getenv('BTC_RPC_HOST', '127.0.0.1')
|
|
|
|
NMC_RPC_HOST = os.getenv('NMC_RPC_HOST', '127.0.0.1')
|
|
|
|
|
|
|
|
PART_RPC_PORT = int(os.getenv('PART_RPC_PORT', 19792))
|
|
|
|
LTC_RPC_PORT = int(os.getenv('LTC_RPC_PORT', 19795))
|
|
|
|
BTC_RPC_PORT = int(os.getenv('BTC_RPC_PORT', 19796))
|
|
|
|
NMC_RPC_PORT = int(os.getenv('NMC_RPC_PORT', 19798))
|
|
|
|
|
|
|
|
PART_ONION_PORT = int(os.getenv('PART_ONION_PORT', 51734))
|
|
|
|
LTC_ONION_PORT = int(os.getenv('LTC_ONION_PORT', 9333)) # Still on 0.18 codebase, same port
|
|
|
|
BTC_ONION_PORT = int(os.getenv('BTC_ONION_PORT', 8334))
|
|
|
|
|
|
|
|
PART_RPC_USER = os.getenv('PART_RPC_USER', '')
|
|
|
|
PART_RPC_PWD = os.getenv('PART_RPC_PWD', '')
|
|
|
|
BTC_RPC_USER = os.getenv('BTC_RPC_USER', '')
|
|
|
|
BTC_RPC_PWD = os.getenv('BTC_RPC_PWD', '')
|
|
|
|
LTC_RPC_USER = os.getenv('LTC_RPC_USER', '')
|
|
|
|
LTC_RPC_PWD = os.getenv('LTC_RPC_PWD', '')
|
|
|
|
|
|
|
|
COINS_RPCBIND_IP = os.getenv('COINS_RPCBIND_IP', '127.0.0.1')
|
|
|
|
|
|
|
|
TOR_PROXY_HOST = os.getenv('TOR_PROXY_HOST', '127.0.0.1')
|
|
|
|
TOR_PROXY_PORT = int(os.getenv('TOR_PROXY_PORT', 9050))
|
|
|
|
TOR_CONTROL_PORT = int(os.getenv('TOR_CONTROL_PORT', 9051))
|
|
|
|
TOR_DNS_PORT = int(os.getenv('TOR_DNS_PORT', 5353))
|
|
|
|
TEST_TOR_PROXY = toBool(os.getenv('TEST_TOR_PROXY', 'true')) # Expects a known exit node
|
|
|
|
TEST_ONION_LINK = toBool(os.getenv('TEST_ONION_LINK', 'false'))
|
|
|
|
|
|
|
|
BITCOIN_FASTSYNC_URL = os.getenv('BITCOIN_FASTSYNC_URL', 'http://utxosets.blob.core.windows.net/public/')
|
|
|
|
BITCOIN_FASTSYNC_FILE = os.getenv('BITCOIN_FASTSYNC_FILE', 'utxo-snapshot-bitcoin-mainnet-720179.tar')
|
|
|
|
|
|
|
|
use_tor_proxy = False
|
|
|
|
|
|
|
|
default_socket = socket.socket
|
|
|
|
default_socket_timeout = socket.getdefaulttimeout()
|
|
|
|
default_socket_getaddrinfo = socket.getaddrinfo
|
|
|
|
|
|
|
|
|
|
|
|
def make_reporthook():
|
|
|
|
read = 0 # Number of bytes read so far
|
|
|
|
last_percent_str = ''
|
|
|
|
|
|
|
|
def reporthook(blocknum, blocksize, totalsize):
|
|
|
|
nonlocal read
|
|
|
|
nonlocal last_percent_str
|
|
|
|
read += blocksize
|
|
|
|
if totalsize > 0:
|
|
|
|
percent_str = '%5.0f%%' % (read * 1e2 / totalsize)
|
|
|
|
if percent_str != last_percent_str:
|
|
|
|
logger.info(percent_str)
|
|
|
|
last_percent_str = percent_str
|
|
|
|
else:
|
|
|
|
logger.info('read %d' % (read,))
|
|
|
|
return reporthook
|
|
|
|
|
|
|
|
|
|
|
|
def setConnectionParameters():
|
|
|
|
opener = urllib.request.build_opener()
|
|
|
|
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
|
|
|
|
urllib.request.install_opener(opener)
|
|
|
|
|
|
|
|
if use_tor_proxy:
|
|
|
|
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, TOR_PROXY_HOST, TOR_PROXY_PORT, rdns=True)
|
|
|
|
socket.socket = socks.socksocket
|
|
|
|
socket.getaddrinfo = getaddrinfo_tor # Without this accessing .onion links would fail
|
|
|
|
|
|
|
|
# Set low timeout for urlretrieve connections
|
|
|
|
socket.setdefaulttimeout(5)
|
|
|
|
|
|
|
|
|
|
|
|
def popConnectionParameters():
|
|
|
|
if use_tor_proxy:
|
|
|
|
socket.socket = default_socket
|
|
|
|
socket.getaddrinfo = default_socket_getaddrinfo
|
|
|
|
socket.setdefaulttimeout(default_socket_timeout)
|
|
|
|
|
|
|
|
|
|
|
|
def downloadFile(url, path):
|
|
|
|
logger.info('Downloading file %s', url)
|
|
|
|
logger.info('To %s', path)
|
|
|
|
try:
|
|
|
|
setConnectionParameters()
|
|
|
|
urlretrieve(url, path, make_reporthook())
|
|
|
|
finally:
|
|
|
|
popConnectionParameters()
|
|
|
|
|
|
|
|
|
|
|
|
def downloadBytes(url):
|
|
|
|
try:
|
|
|
|
setConnectionParameters()
|
|
|
|
return urllib.request.urlopen(url).read()
|
|
|
|
finally:
|
|
|
|
popConnectionParameters()
|
|
|
|
|
|
|
|
|
|
|
|
def testTorConnection():
|
|
|
|
test_url = 'https://check.torproject.org/'
|
|
|
|
logger.info('Testing TOR connection at: ' + test_url)
|
|
|
|
|
|
|
|
test_response = downloadBytes(test_url).decode('utf-8')
|
|
|
|
assert('Congratulations. This browser is configured to use Tor.' in test_response)
|
|
|
|
logger.info('TOR is working.')
|
|
|
|
|
|
|
|
|
|
|
|
def testOnionLink():
|
|
|
|
test_url = 'http://jqyzxhjk6psc6ul5jnfwloamhtyh7si74b4743k2qgpskwwxrzhsxmad.onion'
|
|
|
|
logger.info('Testing onion site: ' + test_url)
|
|
|
|
test_response = downloadBytes(test_url).decode('utf-8')
|
|
|
|
assert('The Tor Project\'s free software protects your privacy online.' in test_response)
|
|
|
|
logger.info('Onion links work.')
|
|
|
|
|
|
|
|
|
|
|
|
def ensureValidSignatureBy(result, signing_key_name):
|
|
|
|
if result.valid is False \
|
|
|
|
and not (result.status == 'signature valid' and result.key_status == 'signing key has expired'):
|
|
|
|
raise ValueError('Signature verification failed.')
|
|
|
|
|
|
|
|
if result.key_id not in expected_key_ids[signing_key_name]:
|
|
|
|
raise ValueError('Signature made by unexpected keyid: ' + result.key_id)
|
|
|
|
|
|
|
|
|
|
|
|
def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts={}):
|
|
|
|
version, version_tag, signers = version_data
|
|
|
|
logger.info('extractCore %s v%s%s', coin, version, version_tag)
|
|
|
|
extract_core_overwrite = extra_opts.get('extract_core_overwrite', True)
|
|
|
|
|
|
|
|
if coin == 'monero':
|
|
|
|
bins = ['monerod', 'monero-wallet-rpc']
|
|
|
|
num_exist = 0
|
|
|
|
for b in bins:
|
|
|
|
out_path = os.path.join(bin_dir, b)
|
|
|
|
if os.path.exists(out_path):
|
|
|
|
num_exist += 1
|
|
|
|
if not extract_core_overwrite and num_exist == len(bins):
|
|
|
|
logger.info('Skipping extract, files exist.')
|
|
|
|
return
|
|
|
|
|
|
|
|
with tarfile.open(release_path) as ft:
|
|
|
|
for member in ft.getmembers():
|
|
|
|
if member.isdir():
|
|
|
|
continue
|
|
|
|
bin_name = os.path.basename(member.name)
|
|
|
|
if bin_name not in bins:
|
|
|
|
continue
|
|
|
|
out_path = os.path.join(bin_dir, bin_name)
|
|
|
|
if (not os.path.exists(out_path)) or extract_core_overwrite:
|
|
|
|
with open(out_path, 'wb') as fout, ft.extractfile(member) as fi:
|
|
|
|
fout.write(fi.read())
|
|
|
|
try:
|
|
|
|
os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH)
|
|
|
|
except Exception as e:
|
|
|
|
logging.warning('Unable to set file permissions: %s, for %s', str(e), out_path)
|
|
|
|
return
|
|
|
|
|
|
|
|
bins = [coin + 'd', coin + '-cli', coin + '-tx']
|
|
|
|
versions = version.split('.')
|
|
|
|
if int(versions[0]) >= 22 or int(versions[1]) >= 19:
|
|
|
|
bins.append(coin + '-wallet')
|
|
|
|
if 'win32' in BIN_ARCH or 'win64' in BIN_ARCH:
|
|
|
|
with zipfile.ZipFile(release_path) as fz:
|
|
|
|
for b in bins:
|
|
|
|
b += '.exe'
|
|
|
|
out_path = os.path.join(bin_dir, b)
|
|
|
|
if (not os.path.exists(out_path)) or extract_core_overwrite:
|
|
|
|
with open(out_path, 'wb') as fout:
|
|
|
|
fout.write(fz.read('{}-{}/bin/{}'.format(coin, version, b)))
|
|
|
|
try:
|
|
|
|
os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH)
|
|
|
|
except Exception as e:
|
|
|
|
logging.warning('Unable to set file permissions: %s, for %s', str(e), out_path)
|
|
|
|
else:
|
|
|
|
with tarfile.open(release_path) as ft:
|
|
|
|
for b in bins:
|
|
|
|
out_path = os.path.join(bin_dir, b)
|
|
|
|
if not os.path.exists(out_path) or extract_core_overwrite:
|
|
|
|
with open(out_path, 'wb') as fout, ft.extractfile('{}-{}/bin/{}'.format(coin, version + version_tag, b)) as fi:
|
|
|
|
fout.write(fi.read())
|
|
|
|
try:
|
|
|
|
os.chmod(out_path, stat.S_IRWXU | stat.S_IXGRP | stat.S_IXOTH)
|
|
|
|
except Exception as e:
|
|
|
|
logging.warning('Unable to set file permissions: %s, for %s', str(e), out_path)
|
|
|
|
|
|
|
|
|
|
|
|
def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
|
|
|
|
version, version_tag, signers = version_data
|
|
|
|
logger.info('prepareCore %s v%s%s', coin, version, version_tag)
|
|
|
|
|
|
|
|
bin_dir = os.path.expanduser(settings['chainclients'][coin]['bindir'])
|
|
|
|
if not os.path.exists(bin_dir):
|
|
|
|
os.makedirs(bin_dir)
|
|
|
|
|
|
|
|
filename_extra = ''
|
|
|
|
if 'osx' in BIN_ARCH:
|
|
|
|
os_dir_name = 'osx-unsigned'
|
|
|
|
os_name = 'osx'
|
|
|
|
elif 'win32' in BIN_ARCH or 'win64' in BIN_ARCH:
|
|
|
|
os_dir_name = 'win-unsigned'
|
|
|
|
os_name = 'win'
|
|
|
|
else:
|
|
|
|
os_dir_name = 'linux'
|
|
|
|
os_name = 'linux'
|
|
|
|
if coin == 'particl':
|
|
|
|
filename_extra = PARTICL_LINUX_EXTRA
|
|
|
|
|
|
|
|
signing_key_name = signers[0]
|
|
|
|
if coin == 'monero':
|
|
|
|
use_file_ext = 'tar.bz2' if FILE_EXT == 'tar.gz' else FILE_EXT
|
|
|
|
release_filename = '{}-{}-{}.{}'.format(coin, version, BIN_ARCH, use_file_ext)
|
|
|
|
if os_name == 'osx':
|
|
|
|
os_name = 'mac'
|
|
|
|
release_url = 'https://downloads.getmonero.org/cli/monero-{}-x64-v{}.{}'.format(os_name, version, use_file_ext)
|
|
|
|
release_path = os.path.join(bin_dir, release_filename)
|
|
|
|
if not os.path.exists(release_path):
|
|
|
|
downloadFile(release_url, release_path)
|
|
|
|
|
|
|
|
assert_filename = 'monero-{}-hashes.txt'.format(version)
|
|
|
|
# assert_url = 'https://www.getmonero.org/downloads/hashes.txt'
|
|
|
|
assert_url = 'https://raw.githubusercontent.com/monero-project/monero-site/{}/downloads/hashes.txt'.format(XMR_SITE_COMMIT)
|
|
|
|
assert_path = os.path.join(bin_dir, assert_filename)
|
|
|
|
if not os.path.exists(assert_path):
|
|
|
|
downloadFile(assert_url, assert_path)
|
|
|
|
else:
|
|
|
|
major_version = int(version.split('.')[0])
|
|
|
|
release_filename = '{}-{}-{}{}.{}'.format(coin, version + version_tag, BIN_ARCH, filename_extra, FILE_EXT)
|
|
|
|
if coin == 'particl':
|
|
|
|
release_url = 'https://github.com/particl/particl-core/releases/download/v{}/{}'.format(version + version_tag, release_filename)
|
|
|
|
assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, version)
|
|
|
|
assert_url = 'https://raw.githubusercontent.com/particl/gitian.sigs/master/%s-%s/%s/%s' % (version + version_tag, os_dir_name, signing_key_name, assert_filename)
|
|
|
|
elif coin == 'litecoin':
|
|
|
|
release_url = 'https://download.litecoin.org/litecoin-{}/{}/{}'.format(version, os_name, release_filename)
|
|
|
|
assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, version.rsplit('.', 1)[0])
|
|
|
|
assert_url = 'https://raw.githubusercontent.com/litecoin-project/gitian.sigs.ltc/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename)
|
|
|
|
elif coin == 'bitcoin':
|
|
|
|
release_url = 'https://bitcoincore.org/bin/bitcoin-core-{}/{}'.format(version, release_filename)
|
|
|
|
assert_filename = '{}-core-{}-{}-build.assert'.format(coin, os_name, '.'.join(version.split('.')[:2]))
|
|
|
|
if major_version >= 22:
|
|
|
|
assert_url = f'https://raw.githubusercontent.com/bitcoin-core/guix.sigs/main/{version}/{signing_key_name}/all.SHA256SUMS'
|
|
|
|
else:
|
|
|
|
assert_url = 'https://raw.githubusercontent.com/bitcoin-core/gitian.sigs/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename)
|
|
|
|
elif coin == 'namecoin':
|
|
|
|
release_url = 'https://beta.namecoin.org/files/namecoin-core/namecoin-core-{}/{}'.format(version, release_filename)
|
|
|
|
assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, version.rsplit('.', 1)[0])
|
|
|
|
assert_url = 'https://raw.githubusercontent.com/namecoin/gitian.sigs/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename)
|
|
|
|
else:
|
|
|
|
raise ValueError('Unknown coin')
|
|
|
|
|
|
|
|
assert_sig_filename = assert_filename + '.sig'
|
|
|
|
assert_sig_url = assert_url + ('.asc' if major_version >= 22 else '.sig')
|
|
|
|
|
|
|
|
release_path = os.path.join(bin_dir, release_filename)
|
|
|
|
if not os.path.exists(release_path):
|
|
|
|
downloadFile(release_url, release_path)
|
|
|
|
|
|
|
|
# Rename assert files with full version
|
|
|
|
assert_filename = '{}-{}-{}-build-{}.assert'.format(coin, os_name, version, signing_key_name)
|
|
|
|
assert_path = os.path.join(bin_dir, assert_filename)
|
|
|
|
if not os.path.exists(assert_path):
|
|
|
|
downloadFile(assert_url, assert_path)
|
|
|
|
|
|
|
|
assert_sig_filename = '{}-{}-{}-build-{}.assert.sig'.format(coin, os_name, version, signing_key_name)
|
|
|
|
assert_sig_path = os.path.join(bin_dir, assert_sig_filename)
|
|
|
|
if not os.path.exists(assert_sig_path):
|
|
|
|
downloadFile(assert_sig_url, assert_sig_path)
|
|
|
|
|
|
|
|
hasher = hashlib.sha256()
|
|
|
|
with open(release_path, 'rb') as fp:
|
|
|
|
hasher.update(fp.read())
|
|
|
|
release_hash = hasher.digest()
|
|
|
|
|
|
|
|
logger.info('%s hash: %s', release_filename, release_hash.hex())
|
|
|
|
with open(assert_path, 'rb', 0) as fp, mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) as s:
|
|
|
|
if s.find(bytes(release_hash.hex(), 'utf-8')) == -1:
|
|
|
|
raise ValueError('Error: release hash %s not found in assert file.' % (release_hash.hex()))
|
|
|
|
else:
|
|
|
|
logger.info('Found release hash in assert file.')
|
|
|
|
|
|
|
|
"""
|
|
|
|
gnupghome = os.path.join(data_dir, 'gpg')
|
|
|
|
if not os.path.exists(gnupghome):
|
|
|
|
os.makedirs(gnupghome)
|
|
|
|
"""
|
|
|
|
gpg = gnupg.GPG()
|
|
|
|
|
|
|
|
if coin == 'monero':
|
|
|
|
with open(assert_path, 'rb') as fp:
|
|
|
|
verified = gpg.verify_file(fp)
|
|
|
|
|
|
|
|
if verified.username is None:
|
|
|
|
logger.warning('Signature made by unknown key.')
|
|
|
|
|
|
|
|
pubkeyurl = 'https://raw.githubusercontent.com/monero-project/monero/master/utils/gpg_keys/binaryfate.asc'
|
|
|
|
logger.info('Importing public key from url: ' + pubkeyurl)
|
|
|
|
rv = gpg.import_keys(downloadBytes(pubkeyurl))
|
|
|
|
assert('F0AF4D462A0BDF92' in rv.fingerprints[0])
|
|
|
|
gpg.trust_keys(rv.fingerprints[0], 'TRUST_FULLY')
|
|
|
|
with open(assert_path, 'rb') as fp:
|
|
|
|
verified = gpg.verify_file(fp)
|
|
|
|
else:
|
|
|
|
with open(assert_sig_path, 'rb') as fp:
|
|
|
|
verified = gpg.verify_file(fp, assert_path)
|
|
|
|
|
|
|
|
if verified.username is None:
|
|
|
|
logger.warning('Signature made by unknown key.')
|
|
|
|
|
|
|
|
filename = '{}_{}.pgp'.format(coin, signing_key_name)
|
|
|
|
pubkeyurls = (
|
|
|
|
'https://raw.githubusercontent.com/tecnovert/basicswap/master/pgp/keys/' + filename,
|
|
|
|
'https://gitlab.com/particl/basicswap/-/raw/master/pgp/keys/' + filename,
|
|
|
|
)
|
|
|
|
for url in pubkeyurls:
|
|
|
|
try:
|
|
|
|
logger.info('Importing public key from url: ' + url)
|
|
|
|
rv = gpg.import_keys(downloadBytes(url))
|
|
|
|
break
|
|
|
|
except Exception as e:
|
|
|
|
logging.warning('Import from url failed: %s', str(e))
|
|
|
|
|
|
|
|
for key in rv.fingerprints:
|
|
|
|
gpg.trust_keys(key, 'TRUST_FULLY')
|
|
|
|
|
|
|
|
with open(assert_sig_path, 'rb') as fp:
|
|
|
|
verified = gpg.verify_file(fp, assert_path)
|
|
|
|
|
|
|
|
ensureValidSignatureBy(verified, signing_key_name)
|
|
|
|
|
|
|
|
extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts)
|
|
|
|
|
|
|
|
|
|
|
|
def writeTorSettings(fp, coin, coin_settings, tor_control_password):
|
|
|
|
onionport = coin_settings['onionport']
|
|
|
|
'''
|
|
|
|
TOR_PROXY_HOST must be an ip address.
|
|
|
|
BTC versions >21 and Particl with lookuptorcontrolhost=any can accept hostnames, XMR and LTC cannot
|
|
|
|
'''
|
|
|
|
fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n')
|
|
|
|
fp.write(f'torpassword={tor_control_password}\n')
|
|
|
|
fp.write(f'torcontrol={TOR_PROXY_HOST}:{TOR_CONTROL_PORT}\n')
|
|
|
|
|
|
|
|
if coin == 'litecoin':
|
|
|
|
fp.write(f'bind=0.0.0.0:{onionport}\n')
|
|
|
|
else:
|
|
|
|
fp.write(f'bind=0.0.0.0:{onionport}=onion\n')
|
|
|
|
|
|
|
|
|
|
|
|
def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
|
|
|
|
core_settings = settings['chainclients'][coin]
|
|
|
|
bin_dir = core_settings['bindir']
|
|
|
|
data_dir = core_settings['datadir']
|
|
|
|
tor_control_password = extra_opts.get('tor_control_password', None)
|
|
|
|
|
|
|
|
if not os.path.exists(data_dir):
|
|
|
|
os.makedirs(data_dir)
|
|
|
|
|
|
|
|
if coin == 'monero':
|
|
|
|
core_conf_path = os.path.join(data_dir, coin + 'd.conf')
|
|
|
|
if os.path.exists(core_conf_path):
|
|
|
|
exitWithError('{} exists'.format(core_conf_path))
|
|
|
|
with open(core_conf_path, 'w') as fp:
|
|
|
|
if chain == 'regtest':
|
|
|
|
fp.write('regtest=1\n')
|
|
|
|
fp.write('keep-fakechain=1\n')
|
|
|
|
fp.write('fixed-difficulty=1\n')
|
|
|
|
else:
|
|
|
|
fp.write('bootstrap-daemon-address=auto\n')
|
|
|
|
fp.write('restricted-rpc=1\n')
|
|
|
|
if chain == 'testnet':
|
|
|
|
fp.write('testnet=1\n')
|
|
|
|
fp.write('data-dir={}\n'.format(data_dir))
|
|
|
|
fp.write('rpc-bind-port={}\n'.format(core_settings['rpcport']))
|
|
|
|
fp.write('rpc-bind-ip={}\n'.format(COINS_RPCBIND_IP))
|
|
|
|
fp.write('zmq-rpc-bind-port={}\n'.format(core_settings['zmqport']))
|
|
|
|
fp.write('zmq-rpc-bind-ip={}\n'.format(COINS_RPCBIND_IP))
|
|
|
|
fp.write('prune-blockchain=1\n')
|
|
|
|
|
|
|
|
if tor_control_password is not None:
|
|
|
|
fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n')
|
|
|
|
fp.write('proxy-allow-dns-leaks=0\n')
|
|
|
|
fp.write('no-igd=1\n')
|
|
|
|
|
|
|
|
wallets_dir = core_settings.get('walletsdir', data_dir)
|
|
|
|
if not os.path.exists(wallets_dir):
|
|
|
|
os.makedirs(wallets_dir)
|
|
|
|
|
|
|
|
wallet_conf_path = os.path.join(wallets_dir, coin + '_wallet.conf')
|
|
|
|
if os.path.exists(wallet_conf_path):
|
|
|
|
exitWithError('{} exists'.format(wallet_conf_path))
|
|
|
|
with open(wallet_conf_path, 'w') as fp:
|
|
|
|
if extra_opts.get('use_containers', False) is True:
|
|
|
|
fp.write('daemon-address={}:{}\n'.format(core_settings['rpchost'], core_settings['rpcport']))
|
|
|
|
fp.write('untrusted-daemon=1\n')
|
|
|
|
fp.write('no-dns=1\n')
|
|
|
|
fp.write('rpc-bind-port={}\n'.format(core_settings['walletrpcport']))
|
|
|
|
fp.write('rpc-bind-ip={}\n'.format(COINS_RPCBIND_IP))
|
|
|
|
fp.write('wallet-dir={}\n'.format(os.path.join(data_dir, 'wallets')))
|
|
|
|
fp.write('log-file={}\n'.format(os.path.join(data_dir, 'wallet.log')))
|
|
|
|
fp.write('shared-ringdb-dir={}\n'.format(os.path.join(data_dir, 'shared-ringdb')))
|
|
|
|
fp.write('rpc-login={}:{}\n'.format(core_settings['walletrpcuser'], core_settings['walletrpcpassword']))
|
|
|
|
|
|
|
|
if tor_control_password is not None:
|
|
|
|
if not core_settings['manage_daemon']:
|
|
|
|
fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n')
|
|
|
|
return
|
|
|
|
|
|
|
|
core_conf_path = os.path.join(data_dir, coin + '.conf')
|
|
|
|
if os.path.exists(core_conf_path):
|
|
|
|
exitWithError('{} exists'.format(core_conf_path))
|
|
|
|
with open(core_conf_path, 'w') as fp:
|
|
|
|
if chain != 'mainnet':
|
|
|
|
fp.write(chain + '=1\n')
|
|
|
|
if chain == 'testnet':
|
|
|
|
fp.write('[test]\n\n')
|
|
|
|
if chain == 'regtest':
|
|
|
|
fp.write('[regtest]\n\n')
|
|
|
|
else:
|
|
|
|
logger.warning('Unknown chain %s', chain)
|
|
|
|
|
|
|
|
if COINS_RPCBIND_IP != '127.0.0.1':
|
|
|
|
fp.write('rpcallowip=127.0.0.1\n')
|
|
|
|
fp.write('rpcallowip=172.0.0.0/8\n') # Allow 172.x.x.x, range used by docker
|
|
|
|
fp.write('rpcbind={}\n'.format(COINS_RPCBIND_IP))
|
|
|
|
|
|
|
|
fp.write('rpcport={}\n'.format(core_settings['rpcport']))
|
|
|
|
fp.write('printtoconsole=0\n')
|
|
|
|
fp.write('daemon=0\n')
|
|
|
|
fp.write('wallet=wallet.dat\n')
|
|
|
|
|
|
|
|
if tor_control_password is not None:
|
|
|
|
writeTorSettings(fp, coin, core_settings, tor_control_password)
|
|
|
|
|
|
|
|
salt = generate_salt(16)
|
|
|
|
if coin == 'particl':
|
|
|
|
fp.write('debugexclude=libevent\n')
|
|
|
|
fp.write('zmqpubsmsg=tcp://{}:{}\n'.format(COINS_RPCBIND_IP, settings['zmqport']))
|
|
|
|
fp.write('spentindex=1\n')
|
|
|
|
fp.write('txindex=1\n')
|
|
|
|
fp.write('staking=0\n')
|
|
|
|
if PART_RPC_USER != '':
|
|
|
|
fp.write('rpcauth={}:{}${}\n'.format(PART_RPC_USER, salt, password_to_hmac(salt, PART_RPC_PWD)))
|
|
|
|
if particl_mnemonic == 'none':
|
|
|
|
fp.write('createdefaultmasterkey=1')
|
|
|
|
elif coin == 'litecoin':
|
|
|
|
fp.write('prune=2000\n')
|
|
|
|
if LTC_RPC_USER != '':
|
|
|
|
fp.write('rpcauth={}:{}${}\n'.format(LTC_RPC_USER, salt, password_to_hmac(salt, LTC_RPC_PWD)))
|
|
|
|
elif coin == 'bitcoin':
|
|
|
|
fp.write('prune=2000\n')
|
|
|
|
fp.write('fallbackfee=0.0002\n')
|
|
|
|
if BTC_RPC_USER != '':
|
|
|
|
fp.write('rpcauth={}:{}${}\n'.format(BTC_RPC_USER, salt, password_to_hmac(salt, BTC_RPC_PWD)))
|
|
|
|
elif coin == 'namecoin':
|
|
|
|
fp.write('prune=2000\n')
|
|
|
|
else:
|
|
|
|
logger.warning('Unknown coin %s', coin)
|
|
|
|
|
|
|
|
if coin == 'bitcoin' and extra_opts.get('use_btc_fastsync', False) is True:
|
|
|
|
logger.info('Initialising BTC chain with fastsync %s', BITCOIN_FASTSYNC_FILE)
|
|
|
|
base_dir = extra_opts['data_dir']
|
|
|
|
|
|
|
|
for dirname in ('blocks', 'chainstate'):
|
|
|
|
if os.path.exists(os.path.join(data_dir, dirname)):
|
|
|
|
raise ValueError(f'{dirname} directory already exists, not overwriting.')
|
|
|
|
|
|
|
|
sync_file_path = os.path.join(base_dir, BITCOIN_FASTSYNC_FILE)
|
|
|
|
if not os.path.exists(sync_file_path):
|
|
|
|
sync_file_url = os.path.join(BITCOIN_FASTSYNC_URL, BITCOIN_FASTSYNC_FILE)
|
|
|
|
downloadFile(sync_file_url, sync_file_path)
|
|
|
|
|
|
|
|
asc_filename = BITCOIN_FASTSYNC_FILE + '.asc'
|
|
|
|
asc_file_path = os.path.join(base_dir, asc_filename)
|
|
|
|
if not os.path.exists(asc_file_path):
|
|
|
|
asc_file_urls = (
|
|
|
|
'https://raw.githubusercontent.com/tecnovert/basicswap/master/pgp/sigs/' + asc_filename,
|
|
|
|
'https://gitlab.com/particl/basicswap/-/raw/master/pgp/sigs/' + asc_filename,
|
|
|
|
)
|
|
|
|
for url in asc_file_urls:
|
|
|
|
try:
|
|
|
|
downloadFile(url, asc_file_path)
|
|
|
|
break
|
|
|
|
except Exception as e:
|
|
|
|
logging.warning('Download failed: %s', str(e))
|
|
|
|
gpg = gnupg.GPG()
|
|
|
|
with open(asc_file_path, 'rb') as fp:
|
|
|
|
verified = gpg.verify_file(fp, sync_file_path)
|
|
|
|
|
|
|
|
ensureValidSignatureBy(verified, 'tecnovert')
|
|
|
|
|
|
|
|
with tarfile.open(sync_file_path) as ft:
|
|
|
|
ft.extractall(path=data_dir)
|
|
|
|
# Create the wallet later, no option to set bestblock through wallet_util
|
|
|
|
else:
|
|
|
|
wallet_util = coin + '-wallet'
|
|
|
|
if os.path.exists(os.path.join(bin_dir, wallet_util)):
|
|
|
|
logger.info('Creating wallet.dat for {}.'.format(wallet_util.capitalize()))
|
|
|
|
callrpc_cli(bin_dir, data_dir, chain, '-wallet=wallet.dat create', wallet_util)
|
|
|
|
|
|
|
|
|
|
|
|
def write_torrc(data_dir, tor_control_password):
|
|
|
|
tor_dir = os.path.join(data_dir, 'tor')
|
|
|
|
if not os.path.exists(tor_dir):
|
|
|
|
os.makedirs(tor_dir)
|
|
|
|
torrc_path = os.path.join(tor_dir, 'torrc')
|
|
|
|
|
|
|
|
tor_control_hash = rfc2440_hash_password(tor_control_password)
|
|
|
|
with open(torrc_path, 'w') as fp:
|
|
|
|
fp.write(f'SocksPort 0.0.0.0:{TOR_PROXY_PORT}\n')
|
|
|
|
fp.write(f'ControlPort 0.0.0.0:{TOR_CONTROL_PORT}\n')
|
|
|
|
fp.write(f'DNSPort 0.0.0.0:{TOR_DNS_PORT}\n')
|
|
|
|
fp.write(f'HashedControlPassword {tor_control_hash}\n')
|
|
|
|
|
|
|
|
|
|
|
|
def addTorSettings(settings, tor_control_password):
|
|
|
|
settings['use_tor'] = True
|
|
|
|
settings['tor_proxy_host'] = TOR_PROXY_HOST
|
|
|
|
settings['tor_proxy_port'] = TOR_PROXY_PORT
|
|
|
|
settings['tor_control_password'] = tor_control_password
|
|
|
|
settings['tor_control_port'] = TOR_CONTROL_PORT
|
|
|
|
|
|
|
|
|
|
|
|
def modify_tor_config(settings, coin, tor_control_password=None, enable=False):
|
|
|
|
coin_settings = settings['chainclients'][coin]
|
|
|
|
data_dir = coin_settings['datadir']
|
|
|
|
|
|
|
|
if coin == 'monero':
|
|
|
|
core_conf_path = os.path.join(data_dir, coin + 'd.conf')
|
|
|
|
if not os.path.exists(core_conf_path):
|
|
|
|
exitWithError('{} does not exist'.format(core_conf_path))
|
|
|
|
wallets_dir = coin_settings.get('walletsdir', data_dir)
|
|
|
|
wallet_conf_path = os.path.join(wallets_dir, coin + '_wallet.conf')
|
|
|
|
if not os.path.exists(wallet_conf_path):
|
|
|
|
exitWithError('{} does not exist'.format(wallet_conf_path))
|
|
|
|
|
|
|
|
# Backup
|
|
|
|
shutil.copyfile(core_conf_path, core_conf_path + '.last')
|
|
|
|
shutil.copyfile(wallet_conf_path, wallet_conf_path + '.last')
|
|
|
|
|
|
|
|
daemon_tor_settings = ('proxy=', 'proxy-allow-dns-leaks=', 'no-igd=')
|
|
|
|
with open(core_conf_path, 'w') as fp:
|
|
|
|
with open(core_conf_path + '.last') as fp_in:
|
|
|
|
# Disable tor first
|
|
|
|
for line in fp_in:
|
|
|
|
skip_line = False
|
|
|
|
for setting in daemon_tor_settings:
|
|
|
|
if line.startswith(setting):
|
|
|
|
skip_line = True
|
|
|
|
break
|
|
|
|
if not skip_line:
|
|
|
|
fp.write(line)
|
|
|
|
if enable:
|
|
|
|
fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n')
|
|
|
|
fp.write('proxy-allow-dns-leaks=0\n')
|
|
|
|
fp.write('no-igd=1\n')
|
|
|
|
|
|
|
|
wallet_tor_settings = ('proxy=',)
|
|
|
|
with open(wallet_conf_path, 'w') as fp:
|
|
|
|
with open(wallet_conf_path + '.last') as fp_in:
|
|
|
|
# Disable tor first
|
|
|
|
for line in fp_in:
|
|
|
|
skip_line = False
|
|
|
|
for setting in wallet_tor_settings:
|
|
|
|
if line.startswith(setting):
|
|
|
|
skip_line = True
|
|
|
|
break
|
|
|
|
if not skip_line:
|
|
|
|
fp.write(line)
|
|
|
|
if enable:
|
|
|
|
if not coin_settings['manage_daemon']:
|
|
|
|
fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n')
|
|
|
|
return
|
|
|
|
|
|
|
|
config_path = os.path.join(data_dir, coin + '.conf')
|
|
|
|
if not os.path.exists(config_path):
|
|
|
|
exitWithError('{} does not exist'.format(config_path))
|
|
|
|
|
|
|
|
if 'onionport' not in coin_settings:
|
|
|
|
default_onionport = 0
|
|
|
|
if coin == 'bitcoin':
|
|
|
|
default_onionport = BTC_ONION_PORT
|
|
|
|
elif coin == 'particl':
|
|
|
|
default_onionport = PART_ONION_PORT
|
|
|
|
elif coin == 'litecoin':
|
|
|
|
default_onionport = LTC_ONION_PORT
|
|
|
|
else:
|
|
|
|
exitWithError('Unknown default onion listening port for {}'.format(coin))
|
|
|
|
coin_settings['onionport'] = default_onionport
|
|
|
|
|
|
|
|
# Backup
|
|
|
|
shutil.copyfile(config_path, config_path + '.last')
|
|
|
|
|
|
|
|
tor_settings = ('proxy=', 'torpassword=', 'torcontrol=', 'bind=')
|
|
|
|
with open(config_path, 'w') as fp:
|
|
|
|
with open(config_path + '.last') as fp_in:
|
|
|
|
# Disable tor first
|
|
|
|
for line in fp_in:
|
|
|
|
skip_line = False
|
|
|
|
for setting in tor_settings:
|
|
|
|
if line.startswith(setting):
|
|
|
|
skip_line = True
|
|
|
|
break
|
|
|
|
if not skip_line:
|
|
|
|
fp.write(line)
|
|
|
|
if enable:
|
|
|
|
writeTorSettings(fp, coin, coin_settings, tor_control_password)
|
|
|
|
|
|
|
|
|
|
|
|
def make_rpc_func(bin_dir, data_dir, chain):
|
|
|
|
bin_dir = bin_dir
|
|
|
|
data_dir = data_dir
|
|
|
|
chain = chain
|
|
|
|
|
|
|
|
def rpc_func(cmd):
|
|
|
|
nonlocal bin_dir
|
|
|
|
nonlocal data_dir
|
|
|
|
nonlocal chain
|
|
|
|
|
|
|
|
return callrpc_cli(bin_dir, data_dir, chain, cmd, cfg.PARTICL_CLI)
|
|
|
|
return rpc_func
|
|
|
|
|
|
|
|
|
|
|
|
def exitWithError(error_msg):
|
|
|
|
sys.stderr.write('Error: {}, exiting.\n'.format(error_msg))
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
def printVersion():
|
|
|
|
from basicswap import __version__
|
|
|
|
logger.info('Basicswap version: %s', __version__)
|
|
|
|
|
|
|
|
logger.info('Core versions:')
|
|
|
|
for coin, version in known_coins.items():
|
|
|
|
logger.info('\t%s: %s%s', coin, version[0], version[1])
|
|
|
|
|
|
|
|
|
|
|
|
def printHelp():
|
|
|
|
logger.info('Usage: basicswap-prepare ')
|
|
|
|
logger.info('\n--help, -h Print help.')
|
|
|
|
logger.info('--version, -v Print version.')
|
|
|
|
logger.info('--datadir=PATH Path to basicswap data directory, default:{}.'.format(cfg.DEFAULT_DATADIR))
|
|
|
|
logger.info('--bindir=PATH Path to cores directory, default:datadir/bin.')
|
|
|
|
logger.info('--mainnet Run in mainnet mode.')
|
|
|
|
logger.info('--testnet Run in testnet mode.')
|
|
|
|
logger.info('--regtest Run in regtest mode.')
|
|
|
|
logger.info('--particl_mnemonic= Recovery phrase to use for the Particl wallet, default is randomly generated,\n'
|
|
|
|
+ ' "none" to set autogenerate account mode.')
|
|
|
|
logger.info('--withcoin= Prepare system to run daemon for coin.')
|
|
|
|
logger.info('--withoutcoin= Do not prepare system to run daemon for coin.')
|
|
|
|
logger.info('--addcoin= Add coin to existing setup.')
|
|
|
|
logger.info('--disablecoin= Make coin inactive.')
|
|
|
|
logger.info('--preparebinonly Don\'t prepare settings or datadirs.')
|
|
|
|
logger.info('--nocores Don\'t download and extract any coin clients.')
|
|
|
|
logger.info('--usecontainers Expect each core to run in a unique container.')
|
|
|
|
logger.info('--portoffset=n Raise all ports by n.')
|
|
|
|
logger.info('--htmlhost= Interface to host on, default:127.0.0.1.')
|
|
|
|
logger.info('--xmrrestoreheight=n Block height to restore Monero wallet from, default:{}.'.format(DEFAULT_XMR_RESTORE_HEIGHT))
|
|
|
|
logger.info('--noextractover Prevent extracting cores if files exist. Speeds up tests')
|
|
|
|
logger.info('--usetorproxy Use TOR proxy during setup. Note that some download links may be inaccessible over TOR.')
|
|
|
|
logger.info('--enabletor Setup Basicswap instance to use TOR.')
|
|
|
|
logger.info('--disabletor Setup Basicswap instance to not use TOR.')
|
|
|
|
logger.info('--usebtcfastsync Initialise the BTC chain with a snapshot from btcpayserver FastSync.\n'
|
|
|
|
+ ' See https://github.com/btcpayserver/btcpayserver-docker/blob/master/contrib/FastSync/README.md')
|
|
|
|
|
|
|
|
logger.info('\n' + 'Known coins: %s', ', '.join(known_coins.keys()))
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
global use_tor_proxy
|
|
|
|
data_dir = None
|
|
|
|
bin_dir = None
|
|
|
|
port_offset = None
|
|
|
|
chain = 'mainnet'
|
|
|
|
particl_wallet_mnemonic = None
|
|
|
|
prepare_bin_only = False
|
|
|
|
no_cores = False
|
|
|
|
use_containers = False
|
|
|
|
with_coins = {'particl', }
|
|
|
|
add_coin = ''
|
|
|
|
disable_coin = ''
|
|
|
|
htmlhost = '127.0.0.1'
|
|
|
|
xmr_restore_height = DEFAULT_XMR_RESTORE_HEIGHT
|
|
|
|
enable_tor = False
|
|
|
|
disable_tor = False
|
|
|
|
tor_control_password = None
|
|
|
|
use_btc_fastsync = False
|
|
|
|
extract_core_overwrite = True
|
|
|
|
|
|
|
|
for v in sys.argv[1:]:
|
|
|
|
if len(v) < 2 or v[0] != '-':
|
|
|
|
exitWithError('Unknown argument {}'.format(v))
|
|
|
|
|
|
|
|
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 name == 'preparebinonly':
|
|
|
|
prepare_bin_only = True
|
|
|
|
continue
|
|
|
|
if name == 'nocores':
|
|
|
|
no_cores = True
|
|
|
|
continue
|
|
|
|
if name == 'usecontainers':
|
|
|
|
use_containers = True
|
|
|
|
continue
|
|
|
|
if name == 'noextractover':
|
|
|
|
extract_core_overwrite = False
|
|
|
|
continue
|
|
|
|
if name == 'usetorproxy':
|
|
|
|
use_tor_proxy = True
|
|
|
|
continue
|
|
|
|
if name == 'enabletor':
|
|
|
|
enable_tor = True
|
|
|
|
continue
|
|
|
|
if name == 'disabletor':
|
|
|
|
disable_tor = True
|
|
|
|
continue
|
|
|
|
if name == 'usebtcfastsync':
|
|
|
|
use_btc_fastsync = True
|
|
|
|
continue
|
|
|
|
if len(s) == 2:
|
|
|
|
if name == 'datadir':
|
|
|
|
data_dir = os.path.expanduser(s[1].strip('"'))
|
|
|
|
continue
|
|
|
|
if name == 'bindir':
|
|
|
|
bin_dir = os.path.expanduser(s[1].strip('"'))
|
|
|
|
continue
|
|
|
|
if name == 'portoffset':
|
|
|
|
port_offset = int(s[1])
|
|
|
|
continue
|
|
|
|
if name == 'particl_mnemonic':
|
|
|
|
particl_wallet_mnemonic = s[1].strip('"')
|
|
|
|
continue
|
|
|
|
if name == 'withcoin' or name == 'withcoins':
|
|
|
|
coins = s[1].split(',')
|
|
|
|
for coin in coins:
|
|
|
|
if coin not in known_coins:
|
|
|
|
exitWithError('Unknown coin {}'.format(coin))
|
|
|
|
with_coins.add(coin)
|
|
|
|
continue
|
|
|
|
if name == 'withoutcoin' or name == 'withoutcoins':
|
|
|
|
coins = s[1].split(',')
|
|
|
|
for coin in coins:
|
|
|
|
if coin not in known_coins:
|
|
|
|
exitWithError('Unknown coin {}'.format(coin))
|
|
|
|
with_coins.discard(coin)
|
|
|
|
continue
|
|
|
|
if name == 'addcoin':
|
|
|
|
if s[1] not in known_coins:
|
|
|
|
exitWithError('Unknown coin {}'.format(s[1]))
|
|
|
|
add_coin = s[1]
|
|
|
|
with_coins = [add_coin, ]
|
|
|
|
continue
|
|
|
|
if name == 'disablecoin':
|
|
|
|
if s[1] not in known_coins:
|
|
|
|
exitWithError('Unknown coin {}'.format(s[1]))
|
|
|
|
disable_coin = s[1]
|
|
|
|
continue
|
|
|
|
if name == 'htmlhost':
|
|
|
|
htmlhost = s[1].strip('"')
|
|
|
|
continue
|
|
|
|
if name == 'xmrrestoreheight':
|
|
|
|
xmr_restore_height = int(s[1])
|
|
|
|
continue
|
|
|
|
|
|
|
|
exitWithError('Unknown argument {}'.format(v))
|
|
|
|
|
|
|
|
setConnectionParameters()
|
|
|
|
|
|
|
|
if use_tor_proxy and TEST_TOR_PROXY:
|
|
|
|
testTorConnection()
|
|
|
|
|
|
|
|
if use_tor_proxy and TEST_ONION_LINK:
|
|
|
|
testOnionLink()
|
|
|
|
|
|
|
|
if data_dir is None:
|
|
|
|
data_dir = os.path.join(os.path.expanduser(cfg.DEFAULT_DATADIR))
|
|
|
|
if bin_dir is None:
|
|
|
|
bin_dir = os.path.join(data_dir, 'bin')
|
|
|
|
|
|
|
|
logger.info('datadir: %s', data_dir)
|
|
|
|
logger.info('bindir: %s', bin_dir)
|
|
|
|
logger.info('Chain: %s', chain)
|
|
|
|
|
|
|
|
if port_offset is None:
|
|
|
|
port_offset = 300 if chain == 'testnet' else 0
|
|
|
|
|
|
|
|
if not os.path.exists(data_dir):
|
|
|
|
os.makedirs(data_dir)
|
|
|
|
config_path = os.path.join(data_dir, cfg.CONFIG_FILENAME)
|
|
|
|
|
|
|
|
withchainclients = {}
|
|
|
|
chainclients = {
|
|
|
|
'particl': {
|
|
|
|
'connection_type': 'rpc',
|
|
|
|
'manage_daemon': True if ('particl' in with_coins and PART_RPC_HOST == '127.0.0.1') else False,
|
|
|
|
'rpchost': PART_RPC_HOST,
|
|
|
|
'rpcport': PART_RPC_PORT + port_offset,
|
|
|
|
'onionport': PART_ONION_PORT + port_offset,
|
|
|
|
'datadir': os.getenv('PART_DATA_DIR', os.path.join(data_dir, 'particl')),
|
|
|
|
'bindir': os.path.join(bin_dir, 'particl'),
|
|
|
|
'blocks_confirmed': 2,
|
|
|
|
'override_feerate': 0.002,
|
|
|
|
'conf_target': 2,
|
|
|
|
'core_version_group': 18,
|
|
|
|
'chain_lookups': 'local',
|
|
|
|
},
|
|
|
|
'litecoin': {
|
|
|
|
'connection_type': 'rpc' if 'litecoin' in with_coins else 'none',
|
|
|
|
'manage_daemon': True if ('litecoin' in with_coins and LTC_RPC_HOST == '127.0.0.1') else False,
|
|
|
|
'rpchost': LTC_RPC_HOST,
|
|
|
|
'rpcport': LTC_RPC_PORT + port_offset,
|
|
|
|
'onionport': LTC_ONION_PORT + port_offset,
|
|
|
|
'datadir': os.getenv('LTC_DATA_DIR', os.path.join(data_dir, 'litecoin')),
|
|
|
|
'bindir': os.path.join(bin_dir, 'litecoin'),
|
|
|
|
'use_segwit': True,
|
|
|
|
'blocks_confirmed': 2,
|
|
|
|
'conf_target': 2,
|
|
|
|
'core_version_group': 18,
|
|
|
|
'chain_lookups': 'local',
|
|
|
|
},
|
|
|
|
'bitcoin': {
|
|
|
|
'connection_type': 'rpc' if 'bitcoin' in with_coins else 'none',
|
|
|
|
'manage_daemon': True if ('bitcoin' in with_coins and BTC_RPC_HOST == '127.0.0.1') else False,
|
|
|
|
'rpchost': BTC_RPC_HOST,
|
|
|
|
'rpcport': BTC_RPC_PORT + port_offset,
|
|
|
|
'onionport': BTC_ONION_PORT + port_offset,
|
|
|
|
'datadir': os.getenv('BTC_DATA_DIR', os.path.join(data_dir, 'bitcoin')),
|
|
|
|
'bindir': os.path.join(bin_dir, 'bitcoin'),
|
|
|
|
'use_segwit': True,
|
|
|
|
'blocks_confirmed': 1,
|
|
|
|
'conf_target': 2,
|
|
|
|
'core_version_group': 18,
|
|
|
|
'chain_lookups': 'local',
|
|
|
|
},
|
|
|
|
'namecoin': {
|
|
|
|
'connection_type': 'rpc' if 'namecoin' in with_coins else 'none',
|
|
|
|
'manage_daemon': True if ('namecoin' in with_coins and NMC_RPC_HOST == '127.0.0.1') else False,
|
|
|
|
'rpchost': NMC_RPC_HOST,
|
|
|
|
'rpcport': NMC_RPC_PORT + port_offset,
|
|
|
|
'datadir': os.getenv('NMC_DATA_DIR', os.path.join(data_dir, 'namecoin')),
|
|
|
|
'bindir': os.path.join(bin_dir, 'namecoin'),
|
|
|
|
'use_segwit': False,
|
|
|
|
'use_csv': False,
|
|
|
|
'blocks_confirmed': 1,
|
|
|
|
'conf_target': 2,
|
|
|
|
'core_version_group': 18,
|
|
|
|
'chain_lookups': 'local',
|
|
|
|
},
|
|
|
|
'monero': {
|
|
|
|
'connection_type': 'rpc' if 'monero' in with_coins else 'none',
|
|
|
|
'manage_daemon': True if ('monero' in with_coins and XMR_RPC_HOST == '127.0.0.1') else False,
|
|
|
|
'manage_wallet_daemon': True if ('monero' in with_coins and XMR_WALLET_RPC_HOST == '127.0.0.1') else False,
|
|
|
|
'rpcport': BASE_XMR_RPC_PORT + port_offset,
|
|
|
|
'zmqport': BASE_XMR_ZMQ_PORT + port_offset,
|
|
|
|
'walletrpcport': BASE_XMR_WALLET_PORT + port_offset,
|
|
|
|
'rpchost': XMR_RPC_HOST,
|
|
|
|
'walletrpchost': XMR_WALLET_RPC_HOST,
|
|
|
|
'walletrpcuser': XMR_WALLET_RPC_USER,
|
|
|
|
'walletrpcpassword': XMR_WALLET_RPC_PWD,
|
|
|
|
'walletfile': 'swap_wallet',
|
|
|
|
'datadir': os.getenv('XMR_DATA_DIR', os.path.join(data_dir, 'monero')),
|
|
|
|
'bindir': os.path.join(bin_dir, 'monero'),
|
|
|
|
'restore_height': xmr_restore_height,
|
|
|
|
'blocks_confirmed': 7, # TODO: 10?
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if PART_RPC_USER != '':
|
|
|
|
chainclients['particl']['rpcuser'] = PART_RPC_USER
|
|
|
|
chainclients['particl']['rpcpassword'] = PART_RPC_PWD
|
|
|
|
if LTC_RPC_USER != '':
|
|
|
|
chainclients['litecoin']['rpcuser'] = LTC_RPC_USER
|
|
|
|
chainclients['litecoin']['rpcpassword'] = LTC_RPC_PWD
|
|
|
|
if BTC_RPC_USER != '':
|
|
|
|
chainclients['bitcoin']['rpcuser'] = BTC_RPC_USER
|
|
|
|
chainclients['bitcoin']['rpcpassword'] = BTC_RPC_PWD
|
|
|
|
|
|
|
|
chainclients['monero']['walletsdir'] = os.getenv('XMR_WALLETS_DIR', chainclients['monero']['datadir'])
|
|
|
|
|
|
|
|
if enable_tor:
|
|
|
|
logger.info('Enabling TOR')
|
|
|
|
|
|
|
|
if not os.path.exists(config_path):
|
|
|
|
exitWithError('{} does not exist'.format(config_path))
|
|
|
|
with open(config_path) as fs:
|
|
|
|
settings = json.load(fs)
|
|
|
|
|
|
|
|
tor_control_password = settings.get('tor_control_password', None)
|
|
|
|
if tor_control_password is None:
|
|
|
|
tor_control_password = generate_salt(24)
|
|
|
|
settings['tor_control_password'] = tor_control_password
|
|
|
|
write_torrc(data_dir, tor_control_password)
|
|
|
|
|
|
|
|
addTorSettings(settings, tor_control_password)
|
|
|
|
for coin in settings['chainclients']:
|
|
|
|
modify_tor_config(settings, coin, tor_control_password, enable=True)
|
|
|
|
|
|
|
|
with open(config_path, 'w') as fp:
|
|
|
|
json.dump(settings, fp, indent=4)
|
|
|
|
|
|
|
|
logger.info('Done.')
|
|
|
|
return 0
|
|
|
|
|
|
|
|
if disable_tor:
|
|
|
|
logger.info('Disabling TOR')
|
|
|
|
|
|
|
|
if not os.path.exists(config_path):
|
|
|
|
exitWithError('{} does not exist'.format(config_path))
|
|
|
|
with open(config_path) as fs:
|
|
|
|
settings = json.load(fs)
|
|
|
|
|
|
|
|
settings['use_tor'] = False
|
|
|
|
for coin in settings['chainclients']:
|
|
|
|
modify_tor_config(settings, coin, tor_control_password=None, enable=False)
|
|
|
|
|
|
|
|
with open(config_path, 'w') as fp:
|
|
|
|
json.dump(settings, fp, indent=4)
|
|
|
|
|
|
|
|
logger.info('Done.')
|
|
|
|
return 0
|
|
|
|
|
|
|
|
if disable_coin != '':
|
|
|
|
logger.info('Disabling coin: %s', disable_coin)
|
|
|
|
if not os.path.exists(config_path):
|
|
|
|
exitWithError('{} does not exist'.format(config_path))
|
|
|
|
with open(config_path) as fs:
|
|
|
|
settings = json.load(fs)
|
|
|
|
|
|
|
|
if disable_coin not in settings['chainclients']:
|
|
|
|
exitWithError('{} has not been prepared'.format(disable_coin))
|
|
|
|
settings['chainclients'][disable_coin]['connection_type'] = 'none'
|
|
|
|
settings['chainclients'][disable_coin]['manage_daemon'] = False
|
|
|
|
|
|
|
|
with open(config_path, 'w') as fp:
|
|
|
|
json.dump(settings, fp, indent=4)
|
|
|
|
|
|
|
|
logger.info('Done.')
|
|
|
|
return 0
|
|
|
|
|
|
|
|
extra_opts = {
|
|
|
|
'use_btc_fastsync': use_btc_fastsync,
|
|
|
|
'extract_core_overwrite': extract_core_overwrite,
|
|
|
|
'data_dir': data_dir,
|
|
|
|
'use_containers': use_containers,
|
|
|
|
'tor_control_password': tor_control_password,
|
|
|
|
}
|
|
|
|
|
|
|
|
if add_coin != '':
|
|
|
|
logger.info('Adding coin: %s', add_coin)
|
|
|
|
if not os.path.exists(config_path):
|
|
|
|
exitWithError('{} does not exist'.format(config_path))
|
|
|
|
with open(config_path) as fs:
|
|
|
|
settings = json.load(fs)
|
|
|
|
|
|
|
|
if add_coin in settings['chainclients']:
|
|
|
|
coin_settings = settings['chainclients'][add_coin]
|
|
|
|
if coin_settings['connection_type'] == 'none' and coin_settings['manage_daemon'] is False:
|
|
|
|
logger.info('Enabling coin: %s', add_coin)
|
|
|
|
coin_settings['connection_type'] = 'rpc'
|
|
|
|
coin_settings['manage_daemon'] = True
|
|
|
|
with open(config_path, 'w') as fp:
|
|
|
|
json.dump(settings, fp, indent=4)
|
|
|
|
logger.info('Done.')
|
|
|
|
return 0
|
|
|
|
exitWithError('{} is already in the settings file'.format(add_coin))
|
|
|
|
|
|
|
|
settings['chainclients'][add_coin] = chainclients[add_coin]
|
|
|
|
settings['use_tor_proxy'] = use_tor_proxy
|
|
|
|
|
|
|
|
if not no_cores:
|
|
|
|
prepareCore(add_coin, known_coins[add_coin], settings, data_dir, extra_opts)
|
|
|
|
|
|
|
|
if not prepare_bin_only:
|
|
|
|
prepareDataDir(add_coin, settings, chain, particl_wallet_mnemonic, extra_opts)
|
|
|
|
with open(config_path, 'w') as fp:
|
|
|
|
json.dump(settings, fp, indent=4)
|
|
|
|
|
|
|
|
logger.info('Done.')
|
|
|
|
return 0
|
|
|
|
|
|
|
|
logger.info('With coins: %s', ', '.join(with_coins))
|
|
|
|
if os.path.exists(config_path):
|
|
|
|
if not prepare_bin_only:
|
|
|
|
exitWithError('{} exists'.format(config_path))
|
|
|
|
else:
|
|
|
|
with open(config_path) as fs:
|
|
|
|
settings = json.load(fs)
|
|
|
|
else:
|
|
|
|
for c in with_coins:
|
|
|
|
withchainclients[c] = chainclients[c]
|
|
|
|
|
|
|
|
settings = {
|
|
|
|
'debug': True,
|
|
|
|
'zmqhost': 'tcp://127.0.0.1',
|
|
|
|
'zmqport': PART_ZMQ_PORT + port_offset,
|
|
|
|
'htmlhost': htmlhost,
|
|
|
|
'htmlport': UI_HTML_PORT + port_offset,
|
|
|
|
'network_key': '7sW2UEcHXvuqEjkpE5mD584zRaQYs6WXYohue4jLFZPTvMSxwvgs',
|
|
|
|
'network_pubkey': '035758c4a22d7dd59165db02a56156e790224361eb3191f02197addcb3bde903d2',
|
|
|
|
'chainclients': withchainclients,
|
|
|
|
'min_delay_event': 5, # Min delay in seconds before reacting to an event
|
|
|
|
'max_delay_event': 50, # Max delay in seconds before reacting to an event
|
|
|
|
'check_progress_seconds': 60,
|
|
|
|
'check_watched_seconds': 60,
|
|
|
|
'check_expired_seconds': 60
|
|
|
|
}
|
|
|
|
|
|
|
|
if use_tor_proxy:
|
|
|
|
tor_control_password = generate_salt(24)
|
|
|
|
addTorSettings(settings, tor_control_password)
|
|
|
|
|
|
|
|
if not no_cores:
|
|
|
|
for c in with_coins:
|
|
|
|
prepareCore(c, known_coins[c], settings, data_dir, extra_opts)
|
|
|
|
|
|
|
|
if prepare_bin_only:
|
|
|
|
logger.info('Done.')
|
|
|
|
return 0
|
|
|
|
|
|
|
|
for c in with_coins:
|
|
|
|
prepareDataDir(c, settings, chain, particl_wallet_mnemonic, extra_opts)
|
|
|
|
|
|
|
|
with open(config_path, 'w') as fp:
|
|
|
|
json.dump(settings, fp, indent=4)
|
|
|
|
|
|
|
|
if particl_wallet_mnemonic == 'none':
|
|
|
|
logger.info('Done.')
|
|
|
|
return 0
|
|
|
|
|
|
|
|
logger.info('Loading Particl mnemonic')
|
|
|
|
|
|
|
|
particl_settings = settings['chainclients']['particl']
|
|
|
|
partRpc = make_rpc_func(particl_settings['bindir'], particl_settings['datadir'], chain)
|
|
|
|
|
|
|
|
daemons = []
|
|
|
|
daemon_args = ['-noconnect', '-nodnsseed']
|
|
|
|
if not use_tor_proxy:
|
|
|
|
# Cannot set -bind or -whitebind together with -listen=0
|
|
|
|
daemon_args.append('-nolisten')
|
|
|
|
daemons.append(startDaemon(particl_settings['datadir'], particl_settings['bindir'], cfg.PARTICLD, daemon_args + ['-nofindpeers', '-nostaking']))
|
|
|
|
try:
|
|
|
|
waitForRPC(partRpc)
|
|
|
|
|
|
|
|
if particl_wallet_mnemonic is None:
|
|
|
|
particl_wallet_mnemonic = partRpc('mnemonic new')['mnemonic']
|
|
|
|
partRpc('extkeyimportmaster "{}"'.format(particl_wallet_mnemonic))
|
|
|
|
|
|
|
|
# Initialise wallets
|
|
|
|
with open(os.path.join(data_dir, 'basicswap.log'), 'a') as fp:
|
|
|
|
swap_client = BasicSwap(fp, data_dir, settings, chain)
|
|
|
|
|
|
|
|
swap_client.setCoinConnectParams(Coins.PART)
|
|
|
|
swap_client.setDaemonPID(Coins.PART, daemons[-1].pid)
|
|
|
|
swap_client.setCoinRunParams(Coins.PART)
|
|
|
|
swap_client.createCoinInterface(Coins.PART)
|
|
|
|
|
|
|
|
for coin_name in with_coins:
|
|
|
|
coin_settings = settings['chainclients'][coin_name]
|
|
|
|
c = swap_client.getCoinIdFromName(coin_name)
|
|
|
|
if c == Coins.PART:
|
|
|
|
continue
|
|
|
|
|
|
|
|
swap_client.setCoinConnectParams(c)
|
|
|
|
|
|
|
|
if c == Coins.XMR:
|
|
|
|
if not coin_settings['manage_wallet_daemon']:
|
|
|
|
continue
|
|
|
|
daemons.append(startXmrWalletDaemon(coin_settings['datadir'], coin_settings['bindir'], 'monero-wallet-rpc'))
|
|
|
|
else:
|
|
|
|
if not coin_settings['manage_daemon']:
|
|
|
|
continue
|
|
|
|
filename = coin_name + 'd' + ('.exe' if os.name == 'nt' else '')
|
|
|
|
daemons.append(startDaemon(coin_settings['datadir'], coin_settings['bindir'], filename, daemon_args))
|
|
|
|
swap_client.setDaemonPID(c, daemons[-1].pid)
|
|
|
|
swap_client.setCoinRunParams(c)
|
|
|
|
swap_client.createCoinInterface(c)
|
|
|
|
|
|
|
|
# Create wallet if it doesn't exist yet
|
|
|
|
if c == Coins.BTC:
|
|
|
|
swap_client.waitForDaemonRPC(c, with_wallet=False)
|
|
|
|
wallets = swap_client.callcoinrpc(c, 'listwallets')
|
|
|
|
if 'wallet.dat' not in wallets:
|
|
|
|
swap_client.callcoinrpc(c, 'createwallet', ['wallet.dat'])
|
|
|
|
|
|
|
|
swap_client.waitForDaemonRPC(c)
|
|
|
|
swap_client.initialiseWallet(c)
|
|
|
|
swap_client.finalise()
|
|
|
|
del swap_client
|
|
|
|
finally:
|
|
|
|
for d in daemons:
|
|
|
|
logging.info('Interrupting {}'.format(d.pid))
|
|
|
|
d.send_signal(signal.SIGINT)
|
|
|
|
d.wait(timeout=120)
|
|
|
|
for fp in (d.stdout, d.stderr, d.stdin):
|
|
|
|
if fp:
|
|
|
|
fp.close()
|
|
|
|
|
|
|
|
logger.info('IMPORTANT - Save your particl wallet recovery phrase:\n{}\n'.format(particl_wallet_mnemonic))
|
|
|
|
logger.info('Done.')
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|