mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-05 18:38:09 +01:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12bae95e7d | ||
|
|
6b063d0582 | ||
|
|
a5b192b931 | ||
|
|
d1e015962c | ||
|
|
cf797afae9 | ||
|
|
843379325f | ||
|
|
d6f6a73324 | ||
|
|
67518efcad | ||
|
|
0fbdd32908 | ||
|
|
f90a96d9ca | ||
|
|
1c09a8b79e | ||
|
|
ea347093c2 | ||
|
|
76c7a281bb | ||
|
|
1658a6fc54 | ||
|
|
ce5aa0a13b | ||
|
|
3e5c3e1e6a |
@@ -1,3 +1,3 @@
|
||||
name = "basicswap"
|
||||
|
||||
__version__ = "0.0.31"
|
||||
__version__ = "0.0.32"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from .basicswap_util import (
|
||||
from .chainparams import (
|
||||
Coins,
|
||||
)
|
||||
from .ui import (
|
||||
from .ui.util import (
|
||||
PAGE_LIMIT,
|
||||
getCoinType,
|
||||
inputAmount,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
15
basicswap/templates/tor.html
Normal file
15
basicswap/templates/tor.html
Normal 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>
|
||||
102
basicswap/templates/wallet.html
Normal file
102
basicswap/templates/wallet.html
Normal 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>
|
||||
@@ -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
0
basicswap/ui/__init__.py
Normal file
46
basicswap/ui/page_tor.py
Normal file
46
basicswap/ui/page_tor.py
Normal 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')
|
||||
@@ -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
|
||||
|
||||
@@ -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
187
basicswap/util/__init__.py
Normal 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
128
basicswap/util/address.py
Normal 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))
|
||||
@@ -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
31
basicswap/util/rfc2440.py
Normal 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
71
basicswap/util/script.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
==============
|
||||
|
||||
|
||||
0.0.32
|
||||
==============
|
||||
|
||||
- Experimental tor integration
|
||||
|
||||
|
||||
0.0.31
|
||||
==============
|
||||
|
||||
|
||||
54
doc/tor.md
Normal file
54
doc/tor.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
51
docker/docker-compose_with_tor.yml
Normal file
51
docker/docker-compose_with_tor.yml
Normal 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"
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
8
docker/tor/Dockerfile
Normal 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
|
||||
@@ -6,3 +6,4 @@ python-gnupg
|
||||
Jinja2
|
||||
requests
|
||||
pycryptodome
|
||||
PySocks
|
||||
|
||||
1
setup.py
1
setup.py
@@ -39,6 +39,7 @@ setuptools.setup(
|
||||
"Jinja2",
|
||||
"requests",
|
||||
"pycryptodome",
|
||||
"PySocks",
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -33,6 +33,8 @@ from basicswap.basicswap import (
|
||||
)
|
||||
from basicswap.util import (
|
||||
COIN,
|
||||
)
|
||||
from basicswap.util.address import (
|
||||
toWIF,
|
||||
)
|
||||
from basicswap.rpc import (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
48
tests/basicswap/selenium/test_wallets.py
Normal file
48
tests/basicswap/selenium/test_wallets.py
Normal 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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user