16 Commits

Author SHA1 Message Date
tecnovert
12bae95e7d coins: Raise Bitcoin version to 22.0 2022-03-27 13:57:32 +02:00
tecnovert
6b063d0582 docker: Use static ip for tor container. 2022-03-27 11:32:29 +02:00
tecnovert
a5b192b931 Tor working for Bitcoin forks. 2022-03-27 00:08:15 +02:00
tecnovert
d1e015962c basicswap-prepare can enable and disable tor config. 2022-03-24 00:00:35 +02:00
tecnovert
cf797afae9 Raise Particl version to 0.21.2.7 2022-03-02 17:03:00 +02:00
tecnovert
843379325f tests: Add use_segwit to required settings. 2022-02-05 01:16:04 +02:00
tecnovert
d6f6a73324 tests: Allow passing extra config to Particl nodes. 2022-02-01 21:05:34 +02:00
tecnovert
67518efcad ui: Add option to create sized utxo. 2022-01-24 23:32:48 +02:00
tecnovert
0fbdd32908 coins: Raise Monero version to 0.17.3.0 2022-01-23 14:12:27 +02:00
tecnovert
f90a96d9ca ui: Split wallet page 2022-01-23 14:01:01 +02:00
tecnovert
1c09a8b79e coins: Raise Particl version 2022-01-13 11:09:25 +02:00
tecnovert
ea347093c2 Always use subprocess without shell 2022-01-03 13:23:02 +02:00
tecnovert
76c7a281bb Fix getProofOfFunds failure. 2022-01-03 12:05:44 +02:00
tecnovert
1658a6fc54 Fix refunding Particl Anon lock tx. 2022-01-02 01:42:49 +02:00
tecnovert
ce5aa0a13b Fix load bid failure case. 2022-01-02 00:20:30 +02:00
tecnovert
3e5c3e1e6a Make errors in activateBid non fatal. 2022-01-02 00:08:09 +02:00
50 changed files with 1652 additions and 680 deletions

View File

@@ -1,3 +1,3 @@
name = "basicswap"
__version__ = "0.0.31"
__version__ = "0.0.32"

View File

@@ -1,10 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2021 tecnovert
# 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 shlex
import socks
import socket
import urllib
import logging
import threading
import subprocess
@@ -24,6 +28,10 @@ from .chainparams import (
)
def getaddrinfo_tor(*args):
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (args[0], args[1]))]
class BaseApp:
def __init__(self, fp, data_dir, settings, chain, log_name='BasicSwap'):
self.log_name = log_name
@@ -43,6 +51,15 @@ class BaseApp:
self.prepareLogging()
self.log.info('Network: {}'.format(self.chain))
self.use_tor_proxy = self.settings.get('use_tor', False)
self.tor_proxy_host = self.settings.get('tor_proxy_host', '127.0.0.1')
self.tor_proxy_port = self.settings.get('tor_proxy_port', 9050)
self.tor_control_password = self.settings.get('tor_control_password', None)
self.tor_control_port = self.settings.get('tor_control_port', 9051)
self.default_socket = socket.socket
self.default_socket_timeout = socket.getdefaulttimeout()
self.default_socket_getaddrinfo = socket.getaddrinfo
def stopRunning(self, with_code=0):
self.fail_code = with_code
with self.mxDB:
@@ -108,10 +125,11 @@ class BaseApp:
def calltx(self, cmd):
bindir = self.coin_clients[Coins.PART]['bindir']
command_tx = os.path.join(bindir, cfg.PARTICL_TX)
chainname = '' if self.chain == 'mainnet' else (' -' + self.chain)
args = command_tx + chainname + ' ' + cmd
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
args = [os.path.join(bindir, cfg.PARTICL_TX), ]
if self.chain != 'mainnet':
args.append('-' + self.chain)
args += shlex.split(cmd)
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out = p.communicate()
if len(out[1]) > 0:
raise ValueError('TX error ' + str(out[1]))
@@ -121,9 +139,12 @@ class BaseApp:
bindir = self.coin_clients[coin_type]['bindir']
datadir = self.coin_clients[coin_type]['datadir']
command_cli = os.path.join(bindir, chainparams[coin_type]['name'] + '-cli' + ('.exe' if os.name == 'nt' else ''))
chainname = '' if self.chain == 'mainnet' else (' -' + self.chain)
args = command_cli + chainname + ' ' + '-datadir=' + datadir + ' ' + params
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
args = [command_cli, ]
if self.chain != 'mainnet':
args.append('-' + self.chain)
args.append('-datadir=' + datadir)
args += shlex.split(params)
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out = p.communicate(timeout=timeout)
if len(out[1]) > 0:
raise ValueError('CLI error ' + str(out[1]))
@@ -134,3 +155,38 @@ class BaseApp:
return True
str_error = str(ex).lower()
return 'read timed out' in str_error or 'no connection to daemon' in str_error
def setConnectionParameters(self, timeout=120):
opener = urllib.request.build_opener()
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
urllib.request.install_opener(opener)
if self.use_tor_proxy:
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, self.tor_proxy_host, self.tor_proxy_port, rdns=True)
socket.socket = socks.socksocket
socket.getaddrinfo = getaddrinfo_tor # Without this accessing .onion links would fail
socket.setdefaulttimeout(timeout)
def popConnectionParameters(self):
if self.use_tor_proxy:
socket.socket = self.default_socket
socket.getaddrinfo = self.default_socket_getaddrinfo
socket.setdefaulttimeout(self.default_socket_timeout)
def torControl(self, query):
try:
command = 'AUTHENTICATE "{}"\r\n{}\r\nQUIT\r\n'.format(self.tor_control_password, query).encode('utf-8')
c = socket.create_connection((self.tor_proxy_host, self.tor_control_port))
c.send(command)
response = bytearray()
while True:
rv = c.recv(1024)
if not rv:
break
response += rv
c.close()
return response
except Exception as e:
self.log.error(f'torControl {e}')
return

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2021 tecnovert
# 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.
@@ -38,23 +38,27 @@ from . import __version__
from .rpc_xmr import make_xmr_rpc2_func
from .util import (
TemporaryError,
pubkeyToAddress,
format_amount,
format_timestamp,
encodeAddress,
decodeAddress,
DeserialiseNum,
decodeWif,
toWIF,
getKeyID,
make_int,
getP2SHScriptForHash,
getP2WSH,
ensure,
)
from .util.script import (
getP2WSH,
getP2SHScriptForHash,
)
from .util.address import (
toWIF,
getKeyID,
decodeWif,
decodeAddress,
encodeAddress,
pubkeyToAddress,
)
from .chainparams import (
chainparams,
Coins,
chainparams,
)
from .script import (
OpCodes,
@@ -326,6 +330,8 @@ class BasicSwap(BaseApp):
else:
self.thread_pool.shutdown()
self.zmqContext.destroy()
close_all_sessions()
self.engine.dispose()
@@ -899,7 +905,20 @@ class BasicSwap(BaseApp):
session = scoped_session(self.session_factory)
for bid in session.query(Bid):
if bid.in_progress == 1 or (bid.state and bid.state > BidStates.BID_RECEIVED and bid.state < BidStates.SWAP_COMPLETED):
self.activateBid(session, bid)
try:
self.activateBid(session, bid)
except Exception as ex:
self.log.error('Failed to activate bid! Error: %s', str(ex))
if self.debug:
self.log.error(traceback.format_exc())
try:
bid.setState(BidStates.BID_ERROR, 'Failed to activate')
offer = session.query(Offer).filter_by(offer_id=bid.offer_id).first()
self.deactivateBid(session, offer, bid)
except Exception as ex:
self.log.error('Further error deactivating: %s', str(ex))
if self.debug:
self.log.error(traceback.format_exc())
finally:
session.close()
session.remove()
@@ -1507,6 +1526,18 @@ class BasicSwap(BaseApp):
self.mxDB.release()
return self._contract_count
def getUnspentsByAddr(self, coin_type):
ci = self.ci(coin_type)
unspent_addr = dict()
unspent = self.callcoinrpc(coin_type, 'listunspent')
for u in unspent:
if u['spendable'] is not True:
continue
unspent_addr[u['address']] = unspent_addr.get(u['address'], 0) + ci.make_int(u['amount'], r=1)
return unspent_addr
def getProofOfFunds(self, coin_type, amount_for, extra_commit_bytes):
ci = self.ci(coin_type)
self.log.debug('getProofOfFunds %s %s', ci.coin_name(), ci.format_amount(amount_for))
@@ -1515,10 +1546,7 @@ class BasicSwap(BaseApp):
return (None, None)
# TODO: Lock unspent and use same output/s to fund bid
unspent_addr = dict()
unspent = self.callcoinrpc(coin_type, 'listunspent')
for u in unspent:
unspent_addr[u['address']] = unspent_addr.get(u['address'], 0) + ci.make_int(u['amount'], r=1)
unspent_addr = self.getUnspentsByAddr(coin_type)
sign_for_addr = None
for addr, value in unspent_addr.items():
@@ -4622,7 +4650,10 @@ class BasicSwap(BaseApp):
vkbs = ci_to.sumKeys(kbsl, kbsf)
try:
address_to = self.getCachedMainWalletAddress(ci_to)
if offer.coin_to == Coins.XMR:
address_to = self.getCachedMainWalletAddress(ci_to)
else:
address_to = self.getCachedStealthAddressForCoin(coin_to)
txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, bid.chain_b_height_start)
self.log.debug('Submitted lock B refund txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex())
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED, '', session)
@@ -5242,7 +5273,10 @@ class BasicSwap(BaseApp):
# Requires? self.mxDB.acquire()
try:
session = scoped_session(self.session_factory)
inner_str = 'SELECT coin_id, MAX(created_at) as max_created_at FROM wallets GROUP BY coin_id'
where_str = ''
if opts is not None and 'coin_id' in opts:
where_str = 'WHERE coin_id = {}'.format(opts['coin_id'])
inner_str = f'SELECT coin_id, MAX(created_at) as max_created_at FROM wallets {where_str} GROUP BY coin_id'
query_str = 'SELECT a.coin_id, wallet_data, created_at FROM wallets a, ({}) b WHERE a.coin_id = b.coin_id AND a.created_at = b.max_created_at'.format(inner_str)
q = session.execute(query_str)
@@ -5267,6 +5301,9 @@ class BasicSwap(BaseApp):
session.close()
session.remove()
if opts is not None and 'coin_id' in opts:
return rv
for c in Coins:
if c not in chainparams:
continue
@@ -5622,74 +5659,78 @@ class BasicSwap(BaseApp):
def lookupRates(self, coin_from, coin_to):
self.log.debug('lookupRates {}, {}'.format(coin_from, coin_to))
rv = {}
ci_from = self.ci(int(coin_from))
ci_to = self.ci(int(coin_to))
try:
self.setConnectionParameters()
rv = {}
ci_from = self.ci(int(coin_from))
ci_to = self.ci(int(coin_to))
headers = {'Connection': 'close'}
name_from = ci_from.chainparams()['name']
name_to = ci_to.chainparams()['name']
url = 'https://api.coingecko.com/api/v3/simple/price?ids={},{}&vs_currencies=usd'.format(name_from, name_to)
start = time.time()
req = urllib.request.Request(url, headers=headers)
js = json.loads(urllib.request.urlopen(req, timeout=10).read())
js['time_taken'] = time.time() - start
rate = float(js[name_from]['usd']) / float(js[name_to]['usd'])
js['rate_inferred'] = ci_to.format_amount(rate, conv_int=True, r=1)
rv['coingecko'] = js
ticker_from = ci_from.chainparams()['ticker']
ticker_to = ci_to.chainparams()['ticker']
if ci_from.coin_type() == Coins.BTC:
pair = '{}-{}'.format(ticker_from, ticker_to)
url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair
headers = {'Connection': 'close'}
name_from = ci_from.chainparams()['name']
name_to = ci_to.chainparams()['name']
url = 'https://api.coingecko.com/api/v3/simple/price?ids={},{}&vs_currencies=usd'.format(name_from, name_to)
start = time.time()
req = urllib.request.Request(url, headers=headers)
js = json.loads(urllib.request.urlopen(req, timeout=10).read())
js['time_taken'] = time.time() - start
js['pair'] = pair
rate = float(js[name_from]['usd']) / float(js[name_to]['usd'])
js['rate_inferred'] = ci_to.format_amount(rate, conv_int=True, r=1)
rv['coingecko'] = js
try:
rate_inverted = ci_from.make_int(1.0 / float(js['result']['Last']), r=1)
js['rate_inferred'] = ci_to.format_amount(rate_inverted)
except Exception as e:
self.log.warning('lookupRates error: %s', str(e))
js['rate_inferred'] = 'error'
ticker_from = ci_from.chainparams()['ticker']
ticker_to = ci_to.chainparams()['ticker']
if ci_from.coin_type() == Coins.BTC:
pair = '{}-{}'.format(ticker_from, ticker_to)
url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair
start = time.time()
req = urllib.request.Request(url, headers=headers)
js = json.loads(urllib.request.urlopen(req, timeout=10).read())
js['time_taken'] = time.time() - start
js['pair'] = pair
rv['bittrex'] = js
elif ci_to.coin_type() == Coins.BTC:
pair = '{}-{}'.format(ticker_to, ticker_from)
url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair
start = time.time()
req = urllib.request.Request(url, headers=headers)
js = json.loads(urllib.request.urlopen(req, timeout=10).read())
js['time_taken'] = time.time() - start
js['pair'] = pair
js['rate_last'] = js['result']['Last']
rv['bittrex'] = js
else:
pair = 'BTC-{}'.format(ticker_from)
url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair
start = time.time()
req = urllib.request.Request(url, headers=headers)
js_from = json.loads(urllib.request.urlopen(req, timeout=10).read())
js_from['time_taken'] = time.time() - start
js_from['pair'] = pair
try:
rate_inverted = ci_from.make_int(1.0 / float(js['result']['Last']), r=1)
js['rate_inferred'] = ci_to.format_amount(rate_inverted)
except Exception as e:
self.log.warning('lookupRates error: %s', str(e))
js['rate_inferred'] = 'error'
pair = 'BTC-{}'.format(ticker_to)
url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair
start = time.time()
req = urllib.request.Request(url, headers=headers)
js_to = json.loads(urllib.request.urlopen(req, timeout=10).read())
js_to['time_taken'] = time.time() - start
js_to['pair'] = pair
rv['bittrex'] = js
elif ci_to.coin_type() == Coins.BTC:
pair = '{}-{}'.format(ticker_to, ticker_from)
url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair
start = time.time()
req = urllib.request.Request(url, headers=headers)
js = json.loads(urllib.request.urlopen(req, timeout=10).read())
js['time_taken'] = time.time() - start
js['pair'] = pair
js['rate_last'] = js['result']['Last']
rv['bittrex'] = js
else:
pair = 'BTC-{}'.format(ticker_from)
url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair
start = time.time()
req = urllib.request.Request(url, headers=headers)
js_from = json.loads(urllib.request.urlopen(req, timeout=10).read())
js_from['time_taken'] = time.time() - start
js_from['pair'] = pair
try:
rate_inferred = float(js_from['result']['Last']) / float(js_to['result']['Last'])
rate_inferred = ci_to.format_amount(rate, conv_int=True, r=1)
except Exception as e:
rate_inferred = 'error'
pair = 'BTC-{}'.format(ticker_to)
url = 'https://api.bittrex.com/api/v1.1/public/getticker?market=' + pair
start = time.time()
req = urllib.request.Request(url, headers=headers)
js_to = json.loads(urllib.request.urlopen(req, timeout=10).read())
js_to['time_taken'] = time.time() - start
js_to['pair'] = pair
rv['bittrex'] = {'from': js_from, 'to': js_to, 'rate_inferred': rate_inferred}
try:
rate_inferred = float(js_from['result']['Last']) / float(js_to['result']['Last'])
rate_inferred = ci_to.format_amount(rate, conv_int=True, r=1)
except Exception as e:
rate_inferred = 'error'
return rv
rv['bittrex'] = {'from': js_from, 'to': js_to, 'rate_inferred': rate_inferred}
return rv
finally:
self.popConnectionParameters()

View File

@@ -8,9 +8,9 @@
import struct
import hashlib
from enum import IntEnum, auto
from .util import (
encodeAddress,
from .util.address import (
decodeAddress,
encodeAddress,
)
from .chainparams import (
chainparams,

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2021 tecnovert
# 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.
@@ -207,6 +207,20 @@ chainparams = {
}
ticker_map = {}
for c, params in chainparams.items():
ticker_map[params['ticker'].lower()] = c
def getCoinIdFromTicker(ticker):
try:
return ticker_map[ticker.lower()]
except Exception:
raise ValueError('Unknown coin')
class CoinInterface:
def __init__(self, network):
self.setDefaults()
@@ -235,6 +249,10 @@ class CoinInterface:
ticker = 'rt' + ticker
return ticker
def ticker_mainnet(self):
ticker = chainparams[self.coin_type()]['ticker']
return ticker
def min_amount(self):
return chainparams[self.coin_type()][self._network]['min_amount']

View File

@@ -614,6 +614,11 @@ def find_vout_for_address(node, txid, addr):
"""
tx = node.getrawtransaction(txid, True)
for i in range(len(tx["vout"])):
if any([addr == a for a in tx["vout"][i]["scriptPubKey"]["addresses"]]):
return i
scriptPubKey = tx["vout"][i]["scriptPubKey"]
if "addresses" in scriptPubKey:
if any([addr == a for a in scriptPubKey["addresses"]]):
return i
elif "address" in scriptPubKey:
if addr == scriptPubKey["address"]:
return i
raise RuntimeError("Vout not found for address: txid=%s, addr=%s" % (txid, addr))

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2021 tecnovert
# 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.
@@ -17,9 +17,12 @@ class Explorer():
def readURL(self, url):
self.log.debug('Explorer url: {}'.format(url))
headers = {'User-Agent': 'Mozilla/5.0'}
req = urllib.request.Request(url, headers=headers)
return urllib.request.urlopen(req).read()
try:
self.swapclient.setConnectionParameters()
req = urllib.request.Request(url)
return urllib.request.urlopen(req).read()
finally:
self.swapclient.popConnectionParameters()
class ExplorerInsight(Explorer):

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2021 tecnovert
# 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.
@@ -17,11 +17,13 @@ from jinja2 import Environment, PackageLoader
from . import __version__
from .util import (
dumpj,
ensure,
format_timestamp,
)
from .chainparams import (
chainparams,
Coins,
chainparams,
getCoinIdFromTicker,
)
from .basicswap_util import (
SwapTypes,
@@ -47,7 +49,7 @@ from .js_server import (
js_rate,
js_index,
)
from .ui import (
from .ui.util import (
PAGE_LIMIT,
inputAmount,
describeBid,
@@ -57,6 +59,7 @@ from .ui import (
have_data_entry,
get_data_entry_or,
)
from .ui.page_tor import page_tor
env = Environment(loader=PackageLoader('basicswap', 'templates'))
@@ -370,16 +373,12 @@ class HttpHandler(BaseHTTPRequestHandler):
continue
ci = swap_client.ci(k)
fee_rate, fee_src = swap_client.getFeeRateForCoin(k)
est_fee = swap_client.estimateWithdrawFee(k, fee_rate)
cid = str(int(k))
wf = {
'name': w['name'],
'version': w['version'],
'ticker': ci.ticker_mainnet(),
'cid': cid,
'fee_rate': ci.format_amount(int(fee_rate * ci.COIN())),
'fee_rate_src': fee_src,
'est_fee': 'Unknown' if est_fee is None else ci.format_amount(int(est_fee * ci.COIN())),
'balance': w['balance'],
'blocks': w['blocks'],
'synced': w['synced'],
@@ -402,21 +401,6 @@ class HttpHandler(BaseHTTPRequestHandler):
if float(w['anon_pending']) > 0.0:
wf['anon_pending'] = w['anon_pending']
elif k == Coins.XMR:
wf['main_address'] = w.get('main_address', 'Refresh necessary')
if 'wd_type_from_' + cid in page_data:
wf['wd_type_from'] = page_data['wd_type_from_' + cid]
if 'wd_type_to_' + cid in page_data:
wf['wd_type_to'] = page_data['wd_type_to_' + cid]
if 'wd_value_' + cid in page_data:
wf['wd_value'] = page_data['wd_value_' + cid]
if 'wd_address_' + cid in page_data:
wf['wd_address'] = page_data['wd_address_' + cid]
if 'wd_subfee_' + cid in page_data:
wf['wd_subfee'] = page_data['wd_subfee_' + cid]
wallets_formatted.append(wf)
template = env.get_template('wallets.html')
@@ -428,6 +412,179 @@ class HttpHandler(BaseHTTPRequestHandler):
form_id=os.urandom(8).hex(),
), 'UTF-8')
def page_wallet(self, url_split, post_string):
ensure(len(url_split) > 2, 'Wallet not specified')
wallet_ticker = url_split[2]
swap_client = self.server.swap_client
coin_id = getCoinIdFromTicker(wallet_ticker)
page_data = {}
messages = []
form_data = self.checkForm(post_string, 'settings', messages)
show_utxo_groups = False
if form_data:
cid = str(int(coin_id))
if bytes('newaddr_' + cid, 'utf-8') in form_data:
swap_client.cacheNewAddressForCoin(coin_id)
elif bytes('reseed_' + cid, 'utf-8') in form_data:
try:
swap_client.reseedWallet(coin_id)
messages.append('Reseed complete ' + str(coin_id))
except Exception as ex:
messages.append('Reseed failed ' + str(ex))
swap_client.updateWalletsInfo(True, coin_id)
elif bytes('withdraw_' + cid, 'utf-8') in form_data:
try:
value = form_data[bytes('amt_' + cid, 'utf-8')][0].decode('utf-8')
page_data['wd_value_' + cid] = value
except Exception as e:
messages.append('Error: Missing value')
try:
address = form_data[bytes('to_' + cid, 'utf-8')][0].decode('utf-8')
page_data['wd_address_' + cid] = address
except Exception as e:
messages.append('Error: Missing address')
subfee = True if bytes('subfee_' + cid, 'utf-8') in form_data else False
page_data['wd_subfee_' + cid] = subfee
if coin_id == Coins.PART:
try:
type_from = form_data[bytes('withdraw_type_from_' + cid, 'utf-8')][0].decode('utf-8')
type_to = form_data[bytes('withdraw_type_to_' + cid, 'utf-8')][0].decode('utf-8')
page_data['wd_type_from_' + cid] = type_from
page_data['wd_type_to_' + cid] = type_to
except Exception as e:
messages.append('Error: Missing type')
if len(messages) == 0:
ci = swap_client.ci(coin_id)
ticker = ci.ticker()
if coin_id == Coins.PART:
try:
txid = swap_client.withdrawParticl(type_from, type_to, value, address, subfee)
messages.append('Withdrew {} {} ({} to {}) to address {}<br/>In txid: {}'.format(value, ticker, type_from, type_to, address, txid))
except Exception as e:
messages.append('Error: {}'.format(str(e)))
else:
try:
txid = swap_client.withdrawCoin(coin_id, value, address, subfee)
messages.append('Withdrew {} {} to address {}<br/>In txid: {}'.format(value, ticker, address, txid))
except Exception as e:
messages.append('Error: {}'.format(str(e)))
swap_client.updateWalletsInfo(True, coin_id)
elif have_data_entry(form_data, 'showutxogroups'):
show_utxo_groups = True
elif have_data_entry(form_data, 'create_utxo'):
show_utxo_groups = True
try:
value = get_data_entry(form_data, 'utxo_value')
page_data['utxo_value'] = value
ci = swap_client.ci(coin_id)
value_sats = ci.make_int(value)
txid, address = ci.createUTXO(value_sats)
messages.append('Created new utxo of value {} and address {}<br/>In txid: {}'.format(value, address, txid))
except Exception as e:
messages.append('Error: {}'.format(str(e)))
if swap_client.debug is True:
swap_client.log.error(traceback.format_exc())
swap_client.updateWalletsInfo()
wallets = swap_client.getCachedWalletsInfo({'coin_id': coin_id})
for k in wallets.keys():
w = wallets[k]
if 'error' in w:
wallet_data = {
'cid': str(int(k)),
'error': w['error']
}
continue
if 'balance' not in w:
wallet_data = {
'name': w['name'],
'havedata': False,
'updating': w['updating'],
}
continue
ci = swap_client.ci(k)
fee_rate, fee_src = swap_client.getFeeRateForCoin(k)
est_fee = swap_client.estimateWithdrawFee(k, fee_rate)
cid = str(int(k))
wallet_data = {
'name': w['name'],
'version': w['version'],
'ticker': ci.ticker_mainnet(),
'cid': cid,
'fee_rate': ci.format_amount(int(fee_rate * ci.COIN())),
'fee_rate_src': fee_src,
'est_fee': 'Unknown' if est_fee is None else ci.format_amount(int(est_fee * ci.COIN())),
'balance': w['balance'],
'blocks': w['blocks'],
'synced': w['synced'],
'deposit_address': w['deposit_address'],
'expected_seed': w['expected_seed'],
'balance_all': float(w['balance']) + float(w['unconfirmed']),
'updating': w['updating'],
'lastupdated': format_timestamp(w['lastupdated']),
'havedata': True,
}
if float(w['unconfirmed']) > 0.0:
wallet_data['unconfirmed'] = w['unconfirmed']
if k == Coins.PART:
wallet_data['stealth_address'] = w['stealth_address']
wallet_data['blind_balance'] = w['blind_balance']
if float(w['blind_unconfirmed']) > 0.0:
wallet_data['blind_unconfirmed'] = w['blind_unconfirmed']
wallet_data['anon_balance'] = w['anon_balance']
if float(w['anon_pending']) > 0.0:
wallet_data['anon_pending'] = w['anon_pending']
elif k == Coins.XMR:
wallet_data['main_address'] = w.get('main_address', 'Refresh necessary')
if 'wd_type_from_' + cid in page_data:
wallet_data['wd_type_from'] = page_data['wd_type_from_' + cid]
if 'wd_type_to_' + cid in page_data:
wallet_data['wd_type_to'] = page_data['wd_type_to_' + cid]
if 'wd_value_' + cid in page_data:
wallet_data['wd_value'] = page_data['wd_value_' + cid]
if 'wd_address_' + cid in page_data:
wallet_data['wd_address'] = page_data['wd_address_' + cid]
if 'wd_subfee_' + cid in page_data:
wallet_data['wd_subfee'] = page_data['wd_subfee_' + cid]
if 'utxo_value' in page_data:
wallet_data['utxo_value'] = page_data['utxo_value']
if show_utxo_groups:
utxo_groups = ''
unspent_by_addr = swap_client.getUnspentsByAddr(k)
sorted_unspent_by_addr = sorted(unspent_by_addr.items(), key=lambda x: x[1], reverse=True)
for kv in sorted_unspent_by_addr:
utxo_groups += kv[0] + ' ' + ci.format_amount(kv[1]) + '\n'
wallet_data['show_utxo_groups'] = True
wallet_data['utxo_groups'] = utxo_groups
template = env.get_template('wallet.html')
return bytes(template.render(
title=self.server.title,
h2=self.server.title,
messages=messages,
w=wallet_data,
form_id=os.urandom(8).hex(),
), 'UTF-8')
def page_settings(self, url_split, post_string):
swap_client = self.server.swap_client
@@ -764,7 +921,7 @@ class HttpHandler(BaseHTTPRequestHandler):
), 'UTF-8')
def page_offer(self, url_split, post_string):
assert(len(url_split) > 2), 'Offer ID not specified'
ensure(len(url_split) > 2, 'Offer ID not specified')
try:
offer_id = bytes.fromhex(url_split[2])
assert(len(offer_id) == 28)
@@ -772,7 +929,7 @@ class HttpHandler(BaseHTTPRequestHandler):
raise ValueError('Bad offer ID')
swap_client = self.server.swap_client
offer, xmr_offer = swap_client.getXmrOffer(offer_id)
assert(offer), 'Unknown offer ID'
ensure(offer, 'Unknown offer ID')
extend_data = { # Defaults
'nb_validmins': 10,
@@ -928,11 +1085,11 @@ class HttpHandler(BaseHTTPRequestHandler):
if have_data_entry(form_data, 'sort_by'):
sort_by = get_data_entry(form_data, 'sort_by')
assert(sort_by in ['created_at', 'rate']), 'Invalid sort by'
ensure(sort_by in ['created_at', 'rate'], 'Invalid sort by')
filters['sort_by'] = sort_by
if have_data_entry(form_data, 'sort_dir'):
sort_dir = get_data_entry(form_data, 'sort_dir')
assert(sort_dir in ['asc', 'desc']), 'Invalid sort dir'
ensure(sort_dir in ['asc', 'desc'], 'Invalid sort dir')
filters['sort_dir'] = sort_dir
if form_data and have_data_entry(form_data, 'pageback'):
@@ -973,7 +1130,7 @@ class HttpHandler(BaseHTTPRequestHandler):
), 'UTF-8')
def page_bid(self, url_split, post_string):
assert(len(url_split) > 2), 'Bid ID not specified'
ensure(len(url_split) > 2, 'Bid ID not specified')
try:
bid_id = bytes.fromhex(url_split[2])
assert(len(bid_id) == 28)
@@ -1023,7 +1180,7 @@ class HttpHandler(BaseHTTPRequestHandler):
show_lock_transfers = True
bid, xmr_swap, offer, xmr_offer, events = swap_client.getXmrBidAndOffer(bid_id)
assert(bid), 'Unknown bid ID'
ensure(bid, 'Unknown bid ID')
data = describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, events, edit_bid, show_txns, view_tx_ind, show_lock_transfers=show_lock_transfers)
@@ -1077,11 +1234,11 @@ class HttpHandler(BaseHTTPRequestHandler):
if form_data and have_data_entry(form_data, 'applyfilters'):
if have_data_entry(form_data, 'sort_by'):
sort_by = get_data_entry(form_data, 'sort_by')
assert(sort_by in ['created_at', ]), 'Invalid sort by'
ensure(sort_by in ['created_at', ], 'Invalid sort by')
filters['sort_by'] = sort_by
if have_data_entry(form_data, 'sort_dir'):
sort_dir = get_data_entry(form_data, 'sort_dir')
assert(sort_dir in ['asc', 'desc']), 'Invalid sort dir'
ensure(sort_dir in ['asc', 'desc'], 'Invalid sort dir')
filters['sort_dir'] = sort_dir
if form_data and have_data_entry(form_data, 'pageback'):
@@ -1144,7 +1301,7 @@ class HttpHandler(BaseHTTPRequestHandler):
edit_address_id = int(form_data[b'edit_address_id'][0].decode('utf-8'))
edit_addr = form_data[b'edit_address'][0].decode('utf-8')
active_ind = int(form_data[b'active_ind'][0].decode('utf-8'))
assert(active_ind == 0 or active_ind == 1), 'Invalid sort by'
ensure(active_ind in (0, 1), 'Invalid sort by')
addressnote = '' if b'addressnote' not in form_data else form_data[b'addressnote'][0].decode('utf-8')
if not validateTextInput(addressnote, 'Address note', messages, max_length=30):
listaddresses = False
@@ -1196,7 +1353,7 @@ class HttpHandler(BaseHTTPRequestHandler):
), 'UTF-8')
def page_identity(self, url_split, post_string):
assert(len(url_split) > 2), 'Address not specified'
ensure(len(url_split) > 2, 'Address not specified')
identity_address = url_split[2]
swap_client = self.server.swap_client
@@ -1265,6 +1422,7 @@ class HttpHandler(BaseHTTPRequestHandler):
h2=self.server.title,
version=__version__,
summary=summary,
use_tor_proxy=swap_client.use_tor_proxy,
shutdown_token=shutdown_token
), 'UTF-8')
@@ -1333,6 +1491,8 @@ class HttpHandler(BaseHTTPRequestHandler):
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] == 'wallet':
return self.page_wallet(url_split, post_string)
if url_split[1] == 'settings':
return self.page_settings(url_split, post_string)
if url_split[1] == 'rpc':
@@ -1361,6 +1521,8 @@ class HttpHandler(BaseHTTPRequestHandler):
return self.page_smsgaddresses(url_split, post_string)
if url_split[1] == 'identity':
return self.page_identity(url_split, post_string)
if url_split[1] == 'tor':
return page_tor(self, url_split, post_string)
if url_split[1] == 'shutdown':
return self.page_shutdown(url_split, post_string)
return self.page_index(url_split)
@@ -1404,6 +1566,7 @@ class HttpThread(threading.Thread, HTTPServer):
self.title = 'BasicSwap, ' + self.swap_client.chain
self.last_form_id = dict()
self.session_tokens = dict()
self.env = env
self.timeout = 60
HTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2021 tecnovert
# Copyright (c) 2020-2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -16,17 +16,26 @@ from basicswap.contrib.test_framework import segwit_addr
from .util import (
dumpj,
toWIF,
ensure,
make_int,
b2h, i2b, b2i, i2h)
from .util.ecc import (
ep,
pointToCPK, CPKToPoint,
getSecretInt)
from .util.script import (
decodeScriptNum,
getCompactSizeLen,
SerialiseNumCompact,
getWitnessElementLen,
)
from .util.address import (
toWIF,
b58encode,
decodeWif,
decodeAddress,
decodeScriptNum,
pubkeyToAddress,
getCompactSizeLen,
SerialiseNumCompact,
getWitnessElementLen)
)
from coincurve.keys import (
PrivateKey,
PublicKey)
@@ -38,12 +47,6 @@ from coincurve.ecdsaotves import (
ecdsaotves_dec_sig,
ecdsaotves_rec_enc_key)
from .ecc_util import (
ep,
pointToCPK, CPKToPoint,
getSecretInt,
b2h, i2b, b2i, i2h)
from .contrib.test_framework.messages import (
COIN,
COutPoint,
@@ -94,8 +97,13 @@ def find_vout_for_address_from_txobj(tx_obj, addr):
given address. Raises runtime error exception if not found.
"""
for i in range(len(tx_obj["vout"])):
if any([addr == a for a in tx_obj["vout"][i]["scriptPubKey"]["addresses"]]):
return i
scriptPubKey = tx_obj["vout"][i]["scriptPubKey"]
if "addresses" in scriptPubKey:
if any([addr == a for a in scriptPubKey["addresses"]]):
return i
elif "address" in scriptPubKey:
if addr == scriptPubKey["address"]:
return i
raise RuntimeError("Vout not found for address: txid={}, addr={}".format(tx_obj['txid'], addr))
@@ -176,6 +184,7 @@ class BTCInterface(CoinInterface):
self.rpc_callback = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
self.blocks_confirmed = coin_settings['blocks_confirmed']
self.setConfTarget(coin_settings['conf_target'])
self._use_segwit = coin_settings['use_segwit']
self._sc = swap_client
self._log = self._sc.log if self._sc and self._sc.log else logging
@@ -265,8 +274,8 @@ class BTCInterface(CoinInterface):
def getWalletSeedID(self):
return self.rpc_callback('getwalletinfo')['hdseedid']
def getNewAddress(self, use_segwit):
args = ['swap_receive']
def getNewAddress(self, use_segwit, label='swap_receive'):
args = [label]
if use_segwit:
args.append('bech32')
return self.rpc_callback('getnewaddress', args)
@@ -1128,6 +1137,16 @@ class BTCInterface(CoinInterface):
def getSpendableBalance(self):
return self.make_int(self.rpc_callback('getbalances')['mine']['trusted'])
def createUTXO(self, value_sats):
# Create a new address and send value_sats to it
spendable_balance = self.getSpendableBalance()
if spendable_balance < value_sats:
raise ValueError('Balance too low')
address = self.getNewAddress(self._use_segwit, 'create_utxo')
return self.withdrawCoin(self.format_amount(value_sats), address, False), address
def testBTCInterface():
print('testBTCInterface')

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2021 tecnovert
# Copyright (c) 2020-2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -16,17 +16,20 @@ from .contrib.test_framework.script import (
OP_0,
OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG
)
from .ecc_util import i2b
from .util import (
toWIF,
i2b,
ensure,
make_int,
getP2WSH,
TemporaryError,
)
from .util.script import (
getP2WSH,
getCompactSizeLen,
encodeStealthAddress,
getWitnessElementLen)
getWitnessElementLen,
)
from .util.address import (
toWIF,
encodeStealthAddress)
from .chainparams import Coins, chainparams
from .interface_btc import BTCInterface
@@ -70,11 +73,11 @@ class PARTInterface(BTCInterface):
# TODO: Double check
return True
def getNewAddress(self, use_segwit):
return self.rpc_callback('getnewaddress', ['swap_receive'])
def getNewAddress(self, use_segwit, label='swap_receive'):
return self.rpc_callback('getnewaddress', [label])
def getNewStealthAddress(self):
return self.rpc_callback('getnewstealthaddress', ['swap_stealth'])
def getNewStealthAddress(self, label='swap_stealth'):
return self.rpc_callback('getnewstealthaddress', [label])
def haveSpentIndex(self):
version = self.getDaemonVersion()

View File

@@ -31,7 +31,7 @@ from .rpc_xmr import (
make_xmr_rpc_func,
make_xmr_rpc2_func,
make_xmr_wallet_rpc_func)
from .ecc_util import (
from .util import (
b2i, b2h)
from .chainparams import CoinInterface, Coins

View File

@@ -17,7 +17,7 @@ from .basicswap_util import (
from .chainparams import (
Coins,
)
from .ui import (
from .ui.util import (
PAGE_LIMIT,
getCoinType,
inputAmount,

View File

@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020 tecnovert
# Copyright (c) 2020-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 time
import json
import shlex
import urllib
import logging
import traceback
@@ -129,8 +130,13 @@ def openrpc(rpc_port, auth, wallet=None, host='127.0.0.1'):
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)
args = [cli_bin, ]
if chain != 'mainnet':
args.append('-' + chain)
args.append('-datadir=' + datadir)
args += shlex.split(cmd)
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out = p.communicate()
if len(out[1]) > 0:

View File

@@ -13,12 +13,13 @@ Version: {{ version }}
<a href="/explorers">Explorers</a><br/>
<a href="/smsgaddresses">SMSG Addresses</a><br/>
<br/>
<a href="/active">Swaps in progress: {{ summary.num_swapping }}</a><br/>
<a href="/active">Swaps in Progress: {{ summary.num_swapping }}</a><br/>
<a href="/offers">Network Offers: {{ summary.num_network_offers }}</a><br/>
<a href="/sentoffers">Sent Offers: {{ summary.num_sent_offers }}</a><br/>
<a href="/bids">Received Bids: {{ summary.num_recv_bids }}</a><br/>
<a href="/sentbids">Sent Bids: {{ summary.num_sent_bids }}</a><br/>
<a href="/watched">Watched Outputs: {{ summary.num_watched_outputs }}</a><br/>
{% if use_tor_proxy %} <a href="/tor">TOR Information</a><br/> {% endif %}
</p>
<p><a href="/newoffer">New Offer</a></p>

View File

@@ -0,0 +1,15 @@
{% include 'header.html' %}
<h3>TOR Information</h3>
{% if refresh %}
<p>Page Refresh: {{ refresh }} seconds</p>
{% endif %}
<table>
<tr><td>Circuit Established</td><td>{{ data.circuit_established }}</td></tr>
<tr><td>Bytes Written</td><td>{{ data.bytes_written }}</td></tr>
<tr><td>Bytes Read</td><td>{{ data.bytes_read }}</td></tr>
</table>
<p><a href="/">home</a></p>
</body></html>

View File

@@ -0,0 +1,102 @@
{% include 'header.html' %}
<p><a id="refresh" href="/wallet/{{ w.ticker }}">refresh</a></p>
<p><a id="back" href="/wallets">back</a></p>
<h3>{{ w.name }} Wallet</h3>
{% if refresh %}
<p>Page Refresh: {{ refresh }} seconds</p>
{% endif %}
{% for m in messages %}
<p>{{ m }}</p>
{% endfor %}
<form method="post">
{% if w.updating %}
<h5>Updating</h5>
{% endif %}
{% if w.havedata %}
{% if w.error %}
<p>Error: {{ w.error }}</p>
{% else %}
<table>
<tr><td>Last updated:</td><td>{{ w.lastupdated }}</td></tr>
<tr><td>Version:</td><td>{{ w.version }}</td></tr>
<tr><td>Balance:</td><td>{{ w.balance }}</td>{% if w.unconfirmed %}<td>Unconfirmed:</td><td>{{ w.unconfirmed }}</td>{% endif %}</tr>
{% if w.cid == '1' %}
<tr><td>Blind Balance:</td><td>{{ w.blind_balance }}</td>{% if w.blind_unconfirmed %}<td>Blind Unconfirmed:</td><td>{{ w.blind_unconfirmed }}</td>{% endif %}</tr>
<tr><td>Anon Balance:</td><td>{{ w.anon_balance }}</td>{% if w.anon_pending %}<td>Anon Pending:</td><td>{{ w.anon_pending }}</td>{% endif %}</tr>
{% endif %}
<tr><td>Blocks:</td><td>{{ w.blocks }}</td></tr>
<tr><td>Synced:</td><td>{{ w.synced }}</td></tr>
<tr><td>Expected Seed:</td><td>{{ w.expected_seed }}</td>{% if w.expected_seed != true %}<td><input type="submit" name="reseed_{{ w.cid }}" value="Reseed wallet" onclick="return confirmReseed();"></td>{% endif %}</tr>
{% if w.cid == '1' %}
<tr><td>Stealth Address</td><td colspan=2>{{ w.stealth_address }}</td></tr>
{% endif %}
{% if w.cid == '6' %}
<tr><td>Main Address</td><td colspan=2>{{ w.main_address }}</td></tr>
<tr><td><input type="submit" name="newaddr_{{ w.cid }}" value="New Subaddress"></td><td colspan=2>{{ w.deposit_address }}</td></tr>
{% else %}
<tr><td><input type="submit" name="newaddr_{{ w.cid }}" value="New Deposit Address"></td><td colspan=2>{{ w.deposit_address }}</td></tr>
{% endif %}
<tr><td><input type="submit" name="withdraw_{{ w.cid }}" value="Withdraw" onclick="return confirmWithdrawal();"></td><td>Amount: <input type="text" name="amt_{{ w.cid }}" value="{{ w.wd_value }}"></td><td>Address: <input type="text" name="to_{{ w.cid }}" value="{{ w.wd_address }}"></td><td>Subtract fee: <input type="checkbox" name="subfee_{{ w.cid }}" {% if w.wd_subfee==true %} checked="true"{% endif %}></td></tr>
{% if w.cid == '1' %}
<tr><td>Type From, To</td><td>
<select name="withdraw_type_from_{{ w.cid }}">
<option value="plain"{% if w.wd_type_from == 'plain' %} selected{% endif %}>Plain</option>
<option value="blind"{% if w.wd_type_from == 'blind' %} selected{% endif %}>Blind</option>
<option value="anon"{% if w.wd_type_from == 'anon' %} selected{% endif %}>Anon</option>
</select>
<select name="withdraw_type_to_{{ w.cid }}">
<option value="plain"{% if w.wd_type_to == 'plain' %} selected{% endif %}>Plain</option>
<option value="blind"{% if w.wd_type_to == 'blind' %} selected{% endif %}>Blind</option>
<option value="anon"{% if w.wd_type_to == 'anon' %} selected{% endif %}>Anon</option>
</select></td></tr>
{% endif %}
<tr><td>Fee Rate:</td><td>{{ w.fee_rate }}</td><td>Est Fee:</td><td>{{ w.est_fee }}</td></tr>
{% if w.cid != '6' %}
{% if w.show_utxo_groups %}
<tr><td colspan=3>
<textarea class="monospace" id="tx_view" rows="10" cols="150" readonly>
{{ w.utxo_groups }}
</textarea>
</td></tr>
<tr><td><input type="submit" id="create_utxo" name="create_utxo" value="Create UTXO" onclick="return confirmUTXOResize();"></td><td>Amount: <input type="text" name="utxo_value" value="{{ w.utxo_value }}"></td></tr>
<tr><td>
<input type="submit" id="closeutxogroups" name="closeutxogroups" value="Close UTXO Groups">
</td></tr>
{% else %}
<tr><td>
<input type="submit" id="showutxogroups" name="showutxogroups" value="Show UTXO Groups">
</td></tr>
{% endif %}
</table>
{% endif %}
{% endif %}
{% endif %} <!-- havedata -->
<input type="hidden" name="formid" value="{{ form_id }}">
</form>
<p><a href="/">home</a></p>
<script>
function confirmReseed() {
return confirm("Are you sure?\nBackup your wallet before and after.\nWon't detect used keys.\nShould only be used for new wallets.");
}
function confirmWithdrawal() {
return confirm("Are you sure?");
}
function confirmUTXOResize() {
return confirm("Are you sure?");
}
</script>
</body></html>

View File

@@ -1,6 +1,6 @@
{% include 'header.html' %}
<p><a href="/wallets">refresh</a></p>
<p><a id="refresh" href="/wallets">refresh</a></p>
<h3>Wallets</h3>
{% if refresh %}
@@ -35,31 +35,8 @@
<tr><td>Blocks:</td><td>{{ w.blocks }}</td></tr>
<tr><td>Synced:</td><td>{{ w.synced }}</td></tr>
<tr><td>Expected Seed:</td><td>{{ w.expected_seed }}</td>{% if w.expected_seed != true %}<td><input type="submit" name="reseed_{{ w.cid }}" value="Reseed wallet" onclick="return confirmReseed();"></td>{% endif %}</tr>
{% if w.cid == '1' %}
<tr><td>Stealth Address</td><td colspan=2>{{ w.stealth_address }}</td></tr>
{% endif %}
{% if w.cid == '6' %}
<tr><td>Main Address</td><td colspan=2>{{ w.main_address }}</td></tr>
<tr><td><input type="submit" name="newaddr_{{ w.cid }}" value="New Subaddress"></td><td colspan=2>{{ w.deposit_address }}</td></tr>
{% else %}
<tr><td><input type="submit" name="newaddr_{{ w.cid }}" value="New Deposit Address"></td><td colspan=2>{{ w.deposit_address }}</td></tr>
{% endif %}
<tr><td><input type="submit" name="withdraw_{{ w.cid }}" value="Withdraw" onclick="return confirmWithdrawal();"></td><td>Amount: <input type="text" name="amt_{{ w.cid }}" value="{{ w.wd_value }}"></td><td>Address: <input type="text" name="to_{{ w.cid }}" value="{{ w.wd_address }}"></td><td>Subtract fee: <input type="checkbox" name="subfee_{{ w.cid }}" {% if w.wd_subfee==true %} checked="true"{% endif %}></td></tr>
{% if w.cid == '1' %}
<tr><td>Type From, To</td><td>
<select name="withdraw_type_from_{{ w.cid }}">
<option value="plain"{% if w.wd_type_from == 'plain' %} selected{% endif %}>Plain</option>
<option value="blind"{% if w.wd_type_from == 'blind' %} selected{% endif %}>Blind</option>
<option value="anon"{% if w.wd_type_from == 'anon' %} selected{% endif %}>Anon</option>
</select>
<select name="withdraw_type_to_{{ w.cid }}">
<option value="plain"{% if w.wd_type_to == 'plain' %} selected{% endif %}>Plain</option>
<option value="blind"{% if w.wd_type_to == 'blind' %} selected{% endif %}>Blind</option>
<option value="anon"{% if w.wd_type_to == 'anon' %} selected{% endif %}>Anon</option>
</select></td></tr>
{% endif %}
<tr><td>Fee Rate:</td><td>{{ w.fee_rate }}</td><td>Est Fee:</td><td>{{ w.est_fee }}</td></tr>
<tr><td>Expected Seed:</td><td>{{ w.expected_seed }}</td></tr>
<tr><td><a href="/wallet/{{ w.ticker }}">Manage</a></td></tr>
</table>
{% endif %}
{% endif %} <!-- havedata -->

0
basicswap/ui/__init__.py Normal file
View File

46
basicswap/ui/page_tor.py Normal file
View File

@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
def extract_data(bytes_in):
str_in = bytes_in.decode('utf-8')
start = str_in.find('=')
if start < 0:
return None
start += 1
end = str_in.find('\r', start)
if end < 0:
return None
return str_in[start: end]
def page_tor(self, url_split, post_string):
template = self.server.env.get_template('tor.html')
swap_client = self.server.swap_client
page_data = {}
rv = swap_client.torControl('GETINFO status/circuit-established')
page_data['circuit_established'] = extract_data(rv)
rv = swap_client.torControl('GETINFO traffic/read')
page_data['bytes_written'] = extract_data(rv)
rv = swap_client.torControl('GETINFO traffic/written')
page_data['bytes_read'] = extract_data(rv)
messages = []
return bytes(template.render(
title=self.server.title,
h2=self.server.title,
messages=messages,
data=page_data,
form_id=os.urandom(8).hex(),
), 'UTF-8')

View File

@@ -1,19 +1,19 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2021 tecnovert
# Copyright (c) 2020-2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import traceback
from .util import (
from basicswap.util import (
make_int,
format_timestamp,
)
from .chainparams import (
from basicswap.chainparams import (
Coins,
)
from .basicswap_util import (
from basicswap.basicswap_util import (
TxTypes,
TxStates,
BidStates,
@@ -26,7 +26,7 @@ from .basicswap_util import (
getLastBidState,
)
from .protocols.xmr_swap_1 import getChainBSplitKey
from basicswap.protocols.xmr_swap_1 import getChainBSplitKey
PAGE_LIMIT = 50

View File

@@ -1,350 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018-2021 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import time
import struct
import decimal
import hashlib
from .script import OpCodes
from .contrib.segwit_addr import bech32_decode, convertbits, bech32_encode
COIN = 100000000
decimal_ctx = decimal.Context()
decimal_ctx.prec = 20
class TemporaryError(ValueError):
pass
def ensure(v, err_string):
if not v:
raise ValueError(err_string)
def toBool(s) -> bool:
return s.lower() in ["1", "true"]
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(encoded_key):
key = b58decode(encoded_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) -> int:
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
def decodeScriptNum(script_bytes, o):
v = 0
num_len = script_bytes[o]
if num_len >= OpCodes.OP_1 and num_len <= OpCodes.OP_16:
return((num_len - OpCodes.OP_1) + 1, 1)
if num_len > 4:
raise ValueError('Bad scriptnum length') # Max 4 bytes
if num_len + o >= len(script_bytes):
raise ValueError('Bad script length')
o += 1
for i in range(num_len):
b = script_bytes[o + i]
# Negative flag set in last byte, if num is positive and > 0x80 an extra 0x00 byte will be appended
if i == num_len - 1 and b & 0x80:
b &= (~(0x80) & 0xFF)
v += int(b) << 8 * i
v *= -1
else:
v += int(b) << 8 * i
return(v, 1 + num_len)
def getCompactSizeLen(v):
# Compact Size
if v < 253:
return 1
if v <= 0xffff: # USHRT_MAX
return 3
if v <= 0xffffffff: # UINT_MAX
return 5
if v <= 0xffffffffffffffff: # UINT_MAX
return 9
raise ValueError('Value too large')
def getWitnessElementLen(v):
return getCompactSizeLen(v) + v
def SerialiseNumCompact(v):
if v < 253:
return bytes((v,))
if v <= 0xffff: # USHRT_MAX
return struct.pack("<BH", 253, v)
if v <= 0xffffffff: # UINT_MAX
return struct.pack("<BI", 254, v)
if v <= 0xffffffffffffffff: # UINT_MAX
return struct.pack("<BQ", 255, v)
raise ValueError('Value too large')
def float_to_str(f):
# stackoverflow.com/questions/38847690
d1 = decimal_ctx.create_decimal(repr(f))
return format(d1, 'f')
def make_int(v, scale=8, r=0): # r = 0, no rounding, fail, r > 0 round up, r < 0 floor
if type(v) == float:
v = float_to_str(v)
elif type(v) == int:
return v * 10 ** scale
sign = 1
if v[0] == '-':
v = v[1:]
sign = -1
ep = 10 ** scale
have_dp = False
rv = 0
for c in v:
if c == '.':
rv *= ep
have_dp = True
continue
if not c.isdigit():
raise ValueError('Invalid char: ' + c)
if have_dp:
ep //= 10
if ep <= 0:
if r == 0:
raise ValueError('Mantissa too long')
if r > 0:
# Round up
if int(c) > 4:
rv += 1
break
rv += ep * int(c)
else:
rv = rv * 10 + int(c)
if not have_dp:
rv *= ep
return rv * sign
def validate_amount(amount, scale=8) -> bool:
str_amount = float_to_str(amount) if type(amount) == float else str(amount)
has_decimal = False
for c in str_amount:
if c == '.' and not has_decimal:
has_decimal = True
continue
if not c.isdigit():
raise ValueError('Invalid amount')
ar = str_amount.split('.')
if len(ar) > 1 and len(ar[1]) > scale:
raise ValueError('Too many decimal places in amount {}'.format(str_amount))
return True
def format_amount(i, display_scale, scale=None):
if not isinstance(i, int):
raise ValueError('Amount must be an integer.') # Raise error instead of converting as amounts should always be integers
if scale is None:
scale = display_scale
ep = 10 ** scale
n = abs(i)
quotient = n // ep
remainder = n % ep
if display_scale != scale:
remainder %= (10 ** display_scale)
rv = '{}.{:0>{scale}}'.format(quotient, remainder, scale=display_scale)
if i < 0:
rv = '-' + rv
return rv
def format_timestamp(value, with_seconds=False):
str_format = '%Y-%m-%d %H:%M'
if with_seconds:
str_format += ':%S'
str_format += ' %Z'
return time.strftime(str_format, time.localtime(value))
def getP2SHScriptForHash(p2sh):
return bytes((OpCodes.OP_HASH160, 0x14)) \
+ p2sh \
+ bytes((OpCodes.OP_EQUAL,))
def getP2WSH(script):
return bytes((OpCodes.OP_0, 0x20)) + hashlib.sha256(script).digest()
def encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey):
data = bytes((0x00,))
data += scan_pubkey
data += bytes((0x01,))
data += spend_pubkey
data += bytes((0x00,)) # number_signatures - unused
data += bytes((0x00,)) # num prefix bits
b = bytes((prefix_byte,)) + data
b += hashlib.sha256(hashlib.sha256(b).digest()).digest()[:4]
return b58encode(b)

187
basicswap/util/__init__.py Normal file
View File

@@ -0,0 +1,187 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018-2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import time
import decimal
COIN = 100000000
decimal_ctx = decimal.Context()
decimal_ctx.prec = 20
class TemporaryError(ValueError):
pass
def ensure(v, err_string):
if not v:
raise ValueError(err_string)
def toBool(s) -> bool:
return s.lower() in ['1', 'true']
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('"', '\\"')
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) -> int:
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
def float_to_str(f):
# stackoverflow.com/questions/38847690
d1 = decimal_ctx.create_decimal(repr(f))
return format(d1, 'f')
def make_int(v, scale=8, r=0): # r = 0, no rounding, fail, r > 0 round up, r < 0 floor
if type(v) == float:
v = float_to_str(v)
elif type(v) == int:
return v * 10 ** scale
sign = 1
if v[0] == '-':
v = v[1:]
sign = -1
ep = 10 ** scale
have_dp = False
rv = 0
for c in v:
if c == '.':
rv *= ep
have_dp = True
continue
if not c.isdigit():
raise ValueError('Invalid char: ' + c)
if have_dp:
ep //= 10
if ep <= 0:
if r == 0:
raise ValueError('Mantissa too long')
if r > 0:
# Round up
if int(c) > 4:
rv += 1
break
rv += ep * int(c)
else:
rv = rv * 10 + int(c)
if not have_dp:
rv *= ep
return rv * sign
def validate_amount(amount, scale=8) -> bool:
str_amount = float_to_str(amount) if type(amount) == float else str(amount)
has_decimal = False
for c in str_amount:
if c == '.' and not has_decimal:
has_decimal = True
continue
if not c.isdigit():
raise ValueError('Invalid amount')
ar = str_amount.split('.')
if len(ar) > 1 and len(ar[1]) > scale:
raise ValueError('Too many decimal places in amount {}'.format(str_amount))
return True
def format_amount(i, display_scale, scale=None):
if not isinstance(i, int):
raise ValueError('Amount must be an integer.') # Raise error instead of converting as amounts should always be integers
if scale is None:
scale = display_scale
ep = 10 ** scale
n = abs(i)
quotient = n // ep
remainder = n % ep
if display_scale != scale:
remainder %= (10 ** display_scale)
rv = '{}.{:0>{scale}}'.format(quotient, remainder, scale=display_scale)
if i < 0:
rv = '-' + rv
return rv
def format_timestamp(value, with_seconds=False):
str_format = '%Y-%m-%d %H:%M'
if with_seconds:
str_format += ':%S'
str_format += ' %Z'
return time.strftime(str_format, time.localtime(value))
def b2i(b) -> int:
# bytes32ToInt
return int.from_bytes(b, byteorder='big')
def i2b(i: int) -> bytes:
# intToBytes32
return i.to_bytes(32, byteorder='big')
def b2h(b: bytes) -> str:
return b.hex()
def h2b(h: str) -> bytes:
if h.startswith('0x'):
h = h[2:]
return bytes.fromhex(h)
def i2h(x):
return b2h(i2b(x))

128
basicswap/util/address.py Normal file
View File

@@ -0,0 +1,128 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import hashlib
from basicswap.contrib.segwit_addr import bech32_decode, convertbits, bech32_encode
__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 encodeStealthAddress(prefix_byte, scan_pubkey, spend_pubkey):
data = bytes((0x00,))
data += scan_pubkey
data += bytes((0x01,))
data += spend_pubkey
data += bytes((0x00,)) # number_signatures - unused
data += bytes((0x00,)) # num prefix bits
b = bytes((prefix_byte,)) + data
b += hashlib.sha256(hashlib.sha256(b).digest()).digest()[:4]
return b58encode(b)
def decodeWif(encoded_key):
key = b58decode(encoded_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 getKeyID(bytes):
data = hashlib.sha256(bytes).digest()
return hashlib.new('ripemd160', data).digest()
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 pubkeyToAddress(prefix, pubkey):
return encodeAddress(bytes((prefix,)) + getKeyID(pubkey))

View File

@@ -2,11 +2,11 @@
# -*- coding: utf-8 -*-
import os
import codecs
import hashlib
import secrets
from .contrib.ellipticcurve import CurveFp, Point, INFINITY, jacobi_symbol
from basicswap.contrib.ellipticcurve import CurveFp, Point, INFINITY, jacobi_symbol
from . import i2b
class ECCParameters():
@@ -37,31 +37,9 @@ def ToDER(P) -> bytes:
return bytes((4, )) + int(P.x()).to_bytes(32, byteorder='big') + int(P.y()).to_bytes(32, byteorder='big')
def bytes32ToInt(b) -> int:
return int.from_bytes(b, byteorder='big')
def intToBytes32(i: int) -> bytes:
return i.to_bytes(32, byteorder='big')
def intToBytes32_le(i: int) -> bytes:
return i.to_bytes(32, byteorder='little')
def bytesToHexStr(b: bytes) -> str:
return codecs.encode(b, 'hex').decode('utf-8')
def hexStrToBytes(h: str) -> bytes:
if h.startswith('0x'):
h = h[2:]
return bytes.fromhex(h)
def getSecretBytes() -> bytes:
i = 1 + secrets.randbelow(ep.o - 1)
return intToBytes32(i)
return i2b(i)
def getSecretInt() -> int:
@@ -189,16 +167,6 @@ def hash256(inb):
return hashlib.sha256(inb).digest()
i2b = intToBytes32
b2i = bytes32ToInt
b2h = bytesToHexStr
h2b = hexStrToBytes
def i2h(x):
return b2h(i2b(x))
def testEccUtils():
print('testEccUtils()')

31
basicswap/util/rfc2440.py Normal file
View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
import hashlib
import secrets
def rfc2440_hash_password(password, salt=None):
# Match tor --hash-password
# secret_to_key_rfc2440
EXPBIAS = 6
c = 96
count = (16 + (c & 15)) << ((c >> 4) + EXPBIAS)
if salt is None:
salt = secrets.token_bytes(8)
assert(len(salt) == 8)
hashbytes = salt + password.encode('utf-8')
len_hashbytes = len(hashbytes)
h = hashlib.sha1()
while count > 0:
if count >= len_hashbytes:
h.update(hashbytes)
count -= len_hashbytes
continue
h.update(hashbytes[:count])
break
rv = '16:' + salt.hex() + '60' + h.hexdigest()
return rv.upper()

71
basicswap/util/script.py Normal file
View File

@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import struct
import hashlib
from basicswap.script import OpCodes
def decodeScriptNum(script_bytes, o):
v = 0
num_len = script_bytes[o]
if num_len >= OpCodes.OP_1 and num_len <= OpCodes.OP_16:
return((num_len - OpCodes.OP_1) + 1, 1)
if num_len > 4:
raise ValueError('Bad scriptnum length') # Max 4 bytes
if num_len + o >= len(script_bytes):
raise ValueError('Bad script length')
o += 1
for i in range(num_len):
b = script_bytes[o + i]
# Negative flag set in last byte, if num is positive and > 0x80 an extra 0x00 byte will be appended
if i == num_len - 1 and b & 0x80:
b &= (~(0x80) & 0xFF)
v += int(b) << 8 * i
v *= -1
else:
v += int(b) << 8 * i
return(v, 1 + num_len)
def getP2SHScriptForHash(p2sh):
return bytes((OpCodes.OP_HASH160, 0x14)) \
+ p2sh \
+ bytes((OpCodes.OP_EQUAL,))
def getP2WSH(script):
return bytes((OpCodes.OP_0, 0x20)) + hashlib.sha256(script).digest()
def SerialiseNumCompact(v):
if v < 253:
return bytes((v,))
if v <= 0xffff: # USHRT_MAX
return struct.pack("<BH", 253, v)
if v <= 0xffffffff: # UINT_MAX
return struct.pack("<BI", 254, v)
if v <= 0xffffffffffffffff: # UINT_MAX
return struct.pack("<BQ", 255, v)
raise ValueError('Value too large')
def getCompactSizeLen(v):
# Compact Size
if v < 253:
return 1
if v <= 0xffff: # USHRT_MAX
return 3
if v <= 0xffffffff: # UINT_MAX
return 5
if v <= 0xffffffffffffffff: # UINT_MAX
return 9
raise ValueError('Value too large')
def getWitnessElementLen(v):
return getCompactSizeLen(v) + v

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2021 tecnovert
# 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.
@@ -11,7 +11,10 @@ import json
import mmap
import stat
import gnupg
import socks
import shutil
import signal
import socket
import hashlib
import tarfile
import zipfile
@@ -25,12 +28,24 @@ 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
# version, version tag eg. "rc1", signers
known_coins = {
'particl': ('0.21.2.7', '', ('tecnovert',)),
'litecoin': ('0.18.1', '', ('thrasher',)),
'bitcoin': ('22.0', '', ('laanwj',)),
'namecoin': ('0.18.0', '', ('JeremyRand',)),
'monero': ('0.17.3.0', '', ('',)),
}
if platform.system() == 'Darwin':
BIN_ARCH = 'osx64'
FILE_EXT = 'tar.gz'
@@ -41,14 +56,6 @@ else:
BIN_ARCH = 'x86_64-linux-gnu'
FILE_EXT = 'tar.gz'
known_coins = {
'particl': ('0.21.2.5', ''),
'litecoin': ('0.18.1', ''),
'bitcoin': ('0.21.1', ''),
'namecoin': ('0.18.0', ''),
'monero': ('0.17.2.3', ''),
}
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
@@ -61,7 +68,7 @@ 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 = 'ba7fcd947349416bedc0eb2e864416c3779af820' # Lock hashes.txt to monero version
XMR_SITE_COMMIT = 'be137696c3a1d93bfaddf619da0d05a3e6cee4f2' # Lock hashes.txt to monero version
DEFAULT_XMR_RESTORE_HEIGHT = 2245107
@@ -78,6 +85,10 @@ 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', '')
@@ -85,9 +96,21 @@ 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_BIND_IP = os.getenv('COINS_BIND_IP', '127.0.0.1')
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'))
extract_core_overwrite = True
use_tor_proxy = False
default_socket = socket.socket
default_socket_timeout = socket.getdefaulttimeout()
default_socket_getaddrinfo = socket.getaddrinfo
def make_reporthook():
@@ -108,17 +131,64 @@ def make_reporthook():
return reporthook
def downloadFile(url, path):
logger.info('Downloading file %s', url)
logger.info('To %s', path)
def setConnectionParameters():
opener = urllib.request.build_opener()
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
urllib.request.install_opener(opener)
urlretrieve(url, path, make_reporthook())
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 extractCore(coin, version_pair, settings, bin_dir, release_path):
version, version_tag = version_pair
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 extractCore(coin, version_data, settings, bin_dir, release_path):
version, version_tag, signers = version_data
logger.info('extractCore %s v%s%s', coin, version, version_tag)
if coin == 'monero':
@@ -151,7 +221,7 @@ def extractCore(coin, version_pair, settings, bin_dir, release_path):
bins = [coin + 'd', coin + '-cli', coin + '-tx']
versions = version.split('.')
if int(versions[1]) >= 19:
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:
@@ -178,8 +248,8 @@ def extractCore(coin, version_pair, settings, bin_dir, release_path):
logging.warning('Unable to set file permissions: %s, for %s', str(e), out_path)
def prepareCore(coin, version_pair, settings, data_dir):
version, version_tag = version_pair
def prepareCore(coin, version_data, settings, data_dir):
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'])
@@ -213,24 +283,25 @@ def prepareCore(coin, version_pair, settings, data_dir):
if not os.path.exists(assert_path):
downloadFile(assert_url, assert_path)
else:
major_version = int(version.split('.')[0])
signing_key_name = signers[0]
release_filename = '{}-{}-{}.{}'.format(coin, version + version_tag, BIN_ARCH, FILE_EXT)
if coin == 'particl':
signing_key_name = 'tecnovert'
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':
signing_key_name = 'thrasher'
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':
signing_key_name = 'laanwj'
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]))
assert_url = 'https://raw.githubusercontent.com/bitcoin-core/gitian.sigs/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename)
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':
signing_key_name = 'JeremyRand'
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)
@@ -238,19 +309,19 @@ def prepareCore(coin, version_pair, settings, data_dir):
raise ValueError('Unknown coin')
assert_sig_filename = assert_filename + '.sig'
assert_sig_url = assert_url + '.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)
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)
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)
@@ -283,7 +354,7 @@ def prepareCore(coin, version_pair, settings, data_dir):
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(urllib.request.urlopen(pubkeyurl).read())
rv = gpg.import_keys(downloadBytes(pubkeyurl))
print('import_keys', rv)
assert('F0AF4D462A0BDF92' in rv.fingerprints[0])
gpg.trust_keys(rv.fingerprints[0], 'TRUST_FULLY')
@@ -298,7 +369,8 @@ def prepareCore(coin, version_pair, settings, data_dir):
pubkeyurl = 'https://raw.githubusercontent.com/tecnovert/basicswap/master/gitianpubkeys/{}_{}.pgp'.format(coin, signing_key_name)
logger.info('Importing public key from url: ' + pubkeyurl)
rv = gpg.import_keys(urllib.request.urlopen(pubkeyurl).read())
rv = gpg.import_keys(downloadBytes(pubkeyurl))
for key in rv.fingerprints:
gpg.trust_keys(key, 'TRUST_FULLY')
@@ -310,10 +382,26 @@ def prepareCore(coin, version_pair, settings, data_dir):
and not (verified.status == 'signature valid' and verified.key_status == 'signing key has expired'):
raise ValueError('Signature verification failed.')
extractCore(coin, version_pair, settings, bin_dir, release_path)
extractCore(coin, version_data, settings, bin_dir, release_path)
def prepareDataDir(coin, settings, chain, particl_mnemonic, use_containers=False):
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, use_containers=False, tor_control_password=None):
core_settings = settings['chainclients'][coin]
bin_dir = core_settings['bindir']
data_dir = core_settings['datadir']
@@ -337,11 +425,16 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, use_containers=False
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_BIND_IP))
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_BIND_IP))
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)
@@ -355,11 +448,15 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, use_containers=False
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_BIND_IP))
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):
@@ -374,20 +471,23 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, use_containers=False
else:
logger.warning('Unknown chain %s', chain)
if COINS_BIND_IP != '127.0.0.1':
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_BIND_IP))
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_BIND_IP, settings['zmqport']))
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')
@@ -415,6 +515,133 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, use_containers=False
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__)
@@ -446,31 +673,14 @@ def printHelp():
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. Note that some download links may be inaccessible over TOR.')
logger.info('\n' + 'Known coins: %s', ', '.join(known_coins.keys()))
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 main():
global extract_core_overwrite
global use_tor_proxy
data_dir = None
bin_dir = None
port_offset = None
@@ -484,6 +694,9 @@ def main():
disable_coin = ''
htmlhost = '127.0.0.1'
xmr_restore_height = DEFAULT_XMR_RESTORE_HEIGHT
enable_tor = False
disable_tor = False
tor_control_password = None
for v in sys.argv[1:]:
if len(v) < 2 or v[0] != '-':
@@ -522,6 +735,15 @@ def main():
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 len(s) == 2:
if name == 'datadir':
data_dir = os.path.expanduser(s[1].strip('"'))
@@ -569,6 +791,14 @@ def main():
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:
@@ -592,6 +822,7 @@ def main():
'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,
@@ -605,6 +836,7 @@ def main():
'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,
@@ -618,6 +850,7 @@ def main():
'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,
@@ -671,6 +904,48 @@ def main():
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):
@@ -709,6 +984,7 @@ def main():
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)
@@ -748,6 +1024,10 @@ def main():
'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)
@@ -757,7 +1037,7 @@ def main():
return 0
for c in with_coins:
prepareDataDir(c, settings, chain, particl_wallet_mnemonic, use_containers=use_containers)
prepareDataDir(c, settings, chain, particl_wallet_mnemonic, use_containers=use_containers, tor_control_password=tor_control_password)
with open(config_path, 'w') as fp:
json.dump(settings, fp, indent=4)
@@ -772,7 +1052,11 @@ def main():
partRpc = make_rpc_func(particl_settings['bindir'], particl_settings['datadir'], chain)
daemons = []
daemons.append(startDaemon(particl_settings['datadir'], particl_settings['bindir'], cfg.PARTICLD, ['-noconnect', '-nofindpeers', '-nostaking', '-nodnsseed', '-nolisten']))
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)
@@ -805,7 +1089,7 @@ def main():
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, ['-noconnect', '-nodnsseed', '-nolisten']))
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)

View File

@@ -158,7 +158,7 @@ def runClient(fp, data_dir, chain):
swap_client.start()
if 'htmlhost' in settings:
swap_client.log.info('Starting server at %s:%d.' % (settings['htmlhost'], settings['htmlport']))
swap_client.log.info('Starting server at http://%s:%d.' % (settings['htmlhost'], settings['htmlport']))
allow_cors = settings['allowcors'] if 'allowcors' in settings else cfg.DEFAULT_ALLOW_CORS
tS1 = HttpThread(fp, settings['htmlhost'], settings['htmlport'], allow_cors, swap_client)
threads.append(tS1)

View File

@@ -3,6 +3,12 @@
==============
0.0.32
==============
- Experimental tor integration
0.0.31
==============

54
doc/tor.md Normal file
View File

@@ -0,0 +1,54 @@
## Tor
Basicswap can be configured to route all traffic through a tor proxy.
Note that TOR integration is experimental and should not yet be relied upon.
### basicswap-prepare
basicswap-prepare can be configured to download all binaries through tor and to enable or disable tor in all active coin config files.
#### Create initial files
Docker will create directories instead of files if these don't exist.
touch $COINDATA_PATH/tor/torrc
#### For a new install
Note that some download links, notably for Litecoin, are unreachable when using tor.
If running through docker start the tor container with the following command as the torrc configuration file won't exist yet.
docker compose -f docker-compose_with_tor.yml run --name tor --rm tor \
tor --allow-missing-torrc --SocksPort 0.0.0.0:9050
docker compose -f docker-compose_with_tor.yml run -e TOR_PROXY_HOST=172.16.238.200 --rm swapclient \
basicswap-prepare --usetorproxy --datadir=/coindata --withcoins=monero,particl
Start Basicswap with:
docker compose -f docker-compose_with_tor.yml up
#### Enable tor on an existing datadir
docker compose -f docker-compose_with_tor.yml run -e TOR_PROXY_HOST=172.16.238.200 --rm swapclient \
basicswap-prepare --datadir=/coindata --enabletor
#### Disable tor on an existing datadir
docker compose -f docker-compose_with_tor.yml run --rm swapclient \
basicswap-prepare --datadir=/coindata --disabletor
#### Update coin release
docker compose -f docker-compose_with_tor.yml up -d tor
docker compose -f docker-compose_with_tor.yml run -e TOR_PROXY_HOST=172.16.238.200 --rm swapclient \
basicswap-prepare --usetorproxy --datadir=/coindata --preparebinonly --withcoins=bitcoin
docker compose -f docker-compose_with_tor.yml stop

View File

@@ -1,6 +1,5 @@
version: '3'
version: '3.4'
services:
swapclient:
image: i_swapclient
stop_grace_period: 5m
@@ -19,6 +18,6 @@ services:
max-file: "5"
volumes:
coindata:
driver: local
coindata:
driver: local

View File

@@ -0,0 +1,51 @@
version: '3.4'
services:
swapclient:
image: i_swapclient
container_name: swapclient
stop_grace_period: 5m
build:
context: ../
volumes:
- ${COINDATA_PATH}:/coindata
ports:
- "${HTML_PORT}" # Expose only to localhost, see .env
environment:
- TZ
- TOR_PROXY_HOST
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
networks:
- tor_net
tor:
image: i_tor
container_name: tor
build:
context: ./tor
volumes:
- ${COINDATA_PATH}/tor/data:/var/lib/tor/
- ${COINDATA_PATH}/tor/torrc:/etc/tor/torrc
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
networks:
tor_net:
ipv4_address: 172.16.238.200
volumes:
coindata:
driver: local
networks:
tor_net:
ipam:
driver: default
config:
- subnet: "172.16.238.0/24"

View File

@@ -19,4 +19,4 @@ BTC_DATA_DIR=/data/bitcoin
XMR_DATA_DIR=/data/monero_daemon
XMR_WALLETS_DIR=/data/monero_wallet
COINS_BIND_IP=0.0.0.0
COINS_RPCBIND_IP=0.0.0.0

View File

@@ -18,7 +18,7 @@ services:
options:
max-size: "10m"
max-file: "3"
restart: unless-stopped
#restart: unless-stopped
#bitcoin_core:
#image: i_bitcoin
#build:
@@ -50,7 +50,7 @@ services:
options:
max-size: "10m"
max-file: "3"
restart: unless-stopped
#restart: unless-stopped
#monero_daemon:
#image: i_monero_daemon
#build:
@@ -147,7 +147,7 @@ services:
- BTC_DATA_DIR
- XMR_DATA_DIR
- XMR_WALLETS_DIR
- COINS_BIND_IP
- COINS_RPCBIND_IP
restart: "no"
networks:
default:

View File

@@ -18,4 +18,4 @@ BTC_DATA_DIR=/data/bitcoin
XMR_DATA_DIR=/data/monero_daemon
XMR_WALLETS_DIR=/data/monero_wallet
COINS_BIND_IP=0.0.0.0
COINS_RPCBIND_IP=0.0.0.0

8
docker/tor/Dockerfile Normal file
View File

@@ -0,0 +1,8 @@
FROM alpine:latest
# 9050 SOCKS port
# 9051 control port
# 5353 DNS port
EXPOSE 9050 9051 5353
RUN apk add --no-cache tor
CMD tor -f /etc/tor/torrc

View File

@@ -6,3 +6,4 @@ python-gnupg
Jinja2
requests
pycryptodome
PySocks

View File

@@ -39,6 +39,7 @@ setuptools.setup(
"Jinja2",
"requests",
"pycryptodome",
"PySocks",
],
entry_points={
"console_scripts": [

View File

@@ -7,9 +7,9 @@
import os
import json
import urllib
import signal
import logging
import urllib
from urllib.request import urlopen
from basicswap.rpc import callrpc

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2021 tecnovert
# Copyright (c) 2020-2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -149,18 +149,18 @@ class XmrTestBase(unittest.TestCase):
waitForServer(self.delay_event, 12701)
def waitForDepositAddress():
def waitForMainAddress():
for i in range(20):
if self.delay_event.is_set():
raise ValueError('Test stopped.')
try:
wallets = json.loads(urlopen('http://127.0.0.1:12701/json/wallets').read())
return wallets['6']['deposit_address']
return wallets['6']['main_address']
except Exception as e:
print('Waiting for deposit address {}'.format(str(e)))
print('Waiting for main address {}'.format(str(e)))
self.delay_event.wait(1)
raise ValueError('waitForDepositAddress timedout')
xmr_addr1 = waitForDepositAddress()
raise ValueError('waitForMainAddress timedout')
xmr_addr1 = waitForMainAddress()
num_blocks = 100
@@ -178,11 +178,15 @@ class XmrTestBase(unittest.TestCase):
for i in range(60):
if self.delay_event.is_set():
raise ValueError('Test stopped.')
wallets = json.loads(urlopen('http://127.0.0.1:12701/json/wallets').read())
particl_blocks = wallets['1']['blocks']
print('particl_blocks', particl_blocks)
if particl_blocks >= num_blocks:
break
try:
wallets = json.loads(urlopen('http://127.0.0.1:12701/json/wallets').read())
particl_blocks = wallets['1']['blocks']
print('particl_blocks', particl_blocks)
if particl_blocks >= num_blocks:
break
except Exception as e:
print('Error reading wallets', str(e))
self.delay_event.wait(1)
assert(particl_blocks >= num_blocks)

View File

@@ -24,9 +24,11 @@ from basicswap.basicswap import (
)
from basicswap.util import (
COIN,
toWIF,
dumpj,
)
from basicswap.util.address import (
toWIF,
)
from basicswap.rpc import (
callrpc,
callrpc_cli,

View File

@@ -33,6 +33,8 @@ from basicswap.basicswap import (
)
from basicswap.util import (
COIN,
)
from basicswap.util.address import (
toWIF,
)
from basicswap.rpc import (

View File

@@ -8,8 +8,8 @@
"""
export TEST_PATH=/tmp/test_basicswap_wallet_init
mkdir -p ${TEST_PATH}/bin/{particl,monero,bitcoin}
cp ~/tmp/particl-0.21.2.5-x86_64-linux-gnu.tar.gz ${TEST_PATH}/bin/particl
cp ~/tmp/monero-linux-x64-v0.17.2.3.tar.bz2 ${TEST_PATH}/bin/monero/monero-0.17.2.3-x86_64-linux-gnu.tar.bz2
cp ~/tmp/particl-0.21.2.7-x86_64-linux-gnu.tar.gz ${TEST_PATH}/bin/particl
cp ~/tmp/monero-linux-x64-v0.17.3.0.tar.bz2 ${TEST_PATH}/bin/monero/monero-0.17.3.0-x86_64-linux-gnu.tar.bz2
cp ~/tmp/bitcoin-0.21.1-x86_64-linux-gnu.tar.gz ${TEST_PATH}/bin/bitcoin
export PYTHONPATH=$(pwd)
python tests/basicswap/extended/test_wallet_init.py

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2021 tecnovert
# Copyright (c) 2021-2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -9,9 +9,9 @@
export RESET_TEST=true
export TEST_PATH=/tmp/test_persistent
mkdir -p ${TEST_PATH}/bin/{particl,monero,bitcoin}
cp ~/tmp/particl-0.21.2.5-x86_64-linux-gnu.tar.gz ${TEST_PATH}/bin/particl
cp ~/tmp/particl-0.21.2.7-x86_64-linux-gnu.tar.gz ${TEST_PATH}/bin/particl
cp ~/tmp/bitcoin-0.21.1-x86_64-linux-gnu.tar.gz ${TEST_PATH}/bin/bitcoin
cp ~/tmp/monero-linux-x64-v0.17.2.3.tar.bz2 ${TEST_PATH}/bin/monero/monero-0.17.2.3-x86_64-linux-gnu.tar.bz2
cp ~/tmp/monero-linux-x64-v0.17.3.0.tar.bz2 ${TEST_PATH}/bin/monero/monero-0.17.3.0-x86_64-linux-gnu.tar.bz2
export PYTHONPATH=$(pwd)
python tests/basicswap/extended/test_xmr_persistent.py
@@ -186,6 +186,8 @@ class Test(unittest.TestCase):
for ip in range(NUM_NODES):
if ip != i:
fp.write('connect=127.0.0.1:{}\n'.format(PARTICL_PORT_BASE + ip + PORT_OFS))
for opt in EXTRA_CONFIG_JSON.get('part{}'.format(i), []):
fp.write(opt + '\n')
# Pruned nodes don't provide blocks
with open(os.path.join(client_path, 'bitcoin', 'bitcoin.conf'), 'r') as fp:
@@ -266,7 +268,7 @@ class Test(unittest.TestCase):
wallets = json.loads(urlopen('http://127.0.0.1:{}/json/wallets'.format(UI_PORT + 1)).read())
self.xmr_addr = wallets['6']['deposit_address']
self.xmr_addr = wallets['6']['main_address']
num_blocks = 100
if callrpc_xmr_na(XMR_BASE_RPC_PORT + 1, 'get_block_count')['count'] < num_blocks:
logging.info('Mining {} Monero blocks to {}.'.format(num_blocks, self.xmr_addr))

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
"""
cd /tmp
wget -4 https://chromedriver.storage.googleapis.com/96.0.4664.45/chromedriver_linux64.zip
7z x chromedriver_linux64.zip
sudo mv chromedriver /opt/chromedriver96
python tests/basicswap/extended/test_xmr_persistent.py
python tests/basicswap/selenium/test_wallets.py
html = driver.page_source
print('html', html)
"""
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
def test_html():
base_url = 'http://localhost:12701'
driver = webdriver.Chrome(service=Service('/opt/chromedriver96'))
url = base_url + '/wallets'
driver.get(url)
time.sleep(1)
driver.refresh()
driver.find_element(By.ID, "refresh").click()
time.sleep(1)
driver.refresh()
driver.close()
if __name__ == '__main__':
test_html()

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2021 tecnovert
# 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.
@@ -21,7 +21,8 @@ from coincurve.ecdsaotves import (
from coincurve.keys import (
PrivateKey)
from basicswap.ecc_util import i2b, h2b
from basicswap.util import i2b, h2b
from basicswap.util.rfc2440 import rfc2440_hash_password
from basicswap.interface_btc import BTCInterface
from basicswap.interface_xmr import XMRInterface
@@ -36,6 +37,8 @@ from basicswap.util import (
class Test(unittest.TestCase):
REQUIRED_SETTINGS = {'blocks_confirmed': 1, 'conf_target': 1, 'use_segwit': True}
def test_serialise_num(self):
def test_case(v, nb=None):
b = SerialiseNum(v)
@@ -54,7 +57,9 @@ class Test(unittest.TestCase):
test_case(4194642)
def test_sequence(self):
coin_settings = {'rpcport': 0, 'rpcauth': 'none', 'blocks_confirmed': 1, 'conf_target': 1}
coin_settings = {'rpcport': 0, 'rpcauth': 'none'}
coin_settings.update(self.REQUIRED_SETTINGS)
ci = BTCInterface(coin_settings, 'regtest')
time_val = 48 * 60 * 60
@@ -160,7 +165,8 @@ class Test(unittest.TestCase):
assert(pubkey == pubkey_test)
def test_ecdsa_otves(self):
coin_settings = {'rpcport': 0, 'rpcauth': 'none', 'blocks_confirmed': 1, 'conf_target': 1}
coin_settings = {'rpcport': 0, 'rpcauth': 'none'}
coin_settings.update(self.REQUIRED_SETTINGS)
ci = BTCInterface(coin_settings, 'regtest')
vk_sign = i2b(ci.getNewSecretKey())
vk_encrypt = i2b(ci.getNewSecretKey())
@@ -182,7 +188,8 @@ class Test(unittest.TestCase):
assert(vk_encrypt == recovered_key)
def test_sign(self):
coin_settings = {'rpcport': 0, 'rpcauth': 'none', 'blocks_confirmed': 1, 'conf_target': 1}
coin_settings = {'rpcport': 0, 'rpcauth': 'none'}
coin_settings.update(self.REQUIRED_SETTINGS)
ci = BTCInterface(coin_settings, 'regtest')
vk = i2b(ci.getNewSecretKey())
@@ -196,7 +203,8 @@ class Test(unittest.TestCase):
ci.verifySig(pk, message_hash, sig)
def test_sign_compact(self):
coin_settings = {'rpcport': 0, 'rpcauth': 'none', 'blocks_confirmed': 1, 'conf_target': 1}
coin_settings = {'rpcport': 0, 'rpcauth': 'none'}
coin_settings.update(self.REQUIRED_SETTINGS)
ci = BTCInterface(coin_settings, 'regtest')
vk = i2b(ci.getNewSecretKey())
@@ -206,14 +214,17 @@ class Test(unittest.TestCase):
ci.verifyCompact(pk, 'test signing message', sig)
def test_pubkey_to_address(self):
coin_settings = {'rpcport': 0, 'rpcauth': 'none', 'blocks_confirmed': 1, 'conf_target': 1}
coin_settings = {'rpcport': 0, 'rpcauth': 'none'}
coin_settings.update(self.REQUIRED_SETTINGS)
ci = BTCInterface(coin_settings, 'regtest')
pk = h2b('02c26a344e7d21bcc6f291532679559f2fd234c881271ff98714855edc753763a6')
addr = ci.pubkey_to_address(pk)
assert(addr == 'mj6SdSxmWRmdDqR5R3FfZmRiLmQfQAsLE8')
def test_dleag(self):
coin_settings = {'rpcport': 0, 'walletrpcport': 0, 'walletrpcauth': 'none', 'blocks_confirmed': 1, 'conf_target': 1}
coin_settings = {'rpcport': 0, 'walletrpcport': 0, 'walletrpcauth': 'none'}
coin_settings.update(self.REQUIRED_SETTINGS)
ci = XMRInterface(coin_settings, 'regtest')
key = i2b(ci.getNewSecretKey())
@@ -269,6 +280,13 @@ class Test(unittest.TestCase):
amount_to_recreate = int((amount_from * rate) // (10 ** scale_from))
assert('10.00000000' == format_amount(amount_to_recreate, scale_to))
def test_rfc2440(self):
password = 'test'
salt = bytes.fromhex('B7A94A7E4988630E')
password_hash = rfc2440_hash_password(password, salt=salt)
assert(password_hash == '16:B7A94A7E4988630E6095334BA67F06FBA509B2A7136A04C9C1B430F539')
if __name__ == '__main__':
unittest.main()

View File

@@ -8,7 +8,7 @@
"""
export TEST_RELOAD_PATH=/tmp/test_basicswap
mkdir -p ${TEST_RELOAD_PATH}/bin/{particl,bitcoin}
cp ~/tmp/particl-0.21.2.5-x86_64-linux-gnu.tar.gz ${TEST_RELOAD_PATH}/bin/particl
cp ~/tmp/particl-0.21.2.7-x86_64-linux-gnu.tar.gz ${TEST_RELOAD_PATH}/bin/particl
cp ~/tmp/bitcoin-0.21.1-x86_64-linux-gnu.tar.gz ${TEST_RELOAD_PATH}/bin/bitcoin
export PYTHONPATH=$(pwd)
python tests/basicswap/test_reload.py

View File

@@ -31,10 +31,12 @@ from basicswap.basicswap_util import (
)
from basicswap.util import (
COIN,
toWIF,
make_int,
format_amount,
)
from basicswap.util.address import (
toWIF,
)
from basicswap.rpc import (
callrpc,
callrpc_cli,

View File

@@ -8,8 +8,8 @@
"""
export TEST_RELOAD_PATH=/tmp/test_basicswap
mkdir -p ${TEST_RELOAD_PATH}/bin/{particl,monero}
cp ~/tmp/particl-0.21.2.5-x86_64-linux-gnu.tar.gz ${TEST_RELOAD_PATH}/bin/particl
cp ~/tmp/monero-linux-x64-v0.17.2.3.tar.bz2 ${TEST_RELOAD_PATH}/bin/monero/monero-0.17.2.3-x86_64-linux-gnu.tar.bz2
cp ~/tmp/particl-0.21.2.7-x86_64-linux-gnu.tar.gz ${TEST_RELOAD_PATH}/bin/particl
cp ~/tmp/monero-linux-x64-v0.17.3.0.tar.bz2 ${TEST_RELOAD_PATH}/bin/monero/monero-0.17.3.0-x86_64-linux-gnu.tar.bz2
export PYTHONPATH=$(pwd)
python tests/basicswap/test_xmr_bids_offline.py

View File

@@ -8,8 +8,8 @@
"""
export TEST_RELOAD_PATH=/tmp/test_basicswap
mkdir -p ${TEST_RELOAD_PATH}/bin/{particl,monero}
cp ~/tmp/particl-0.21.2.4-x86_64-linux-gnu.tar.gz ${TEST_RELOAD_PATH}/bin/particl
cp ~/tmp/monero-linux-x64-v0.17.2.3.tar.bz2 ${TEST_RELOAD_PATH}/bin/monero/monero-0.17.2.3-x86_64-linux-gnu.tar.bz2
cp ~/tmp/particl-0.21.2.7-x86_64-linux-gnu.tar.gz ${TEST_RELOAD_PATH}/bin/particl
cp ~/tmp/monero-linux-x64-v0.17.3.0.tar.bz2 ${TEST_RELOAD_PATH}/bin/monero/monero-0.17.3.0-x86_64-linux-gnu.tar.bz2
export PYTHONPATH=$(pwd)
python tests/basicswap/test_xmr_reload.py