add Haven

This commit is contained in:
pubclown 2024-03-23 10:42:02 -04:00
parent 594845e312
commit 187ad9cb27
37 changed files with 6366 additions and 7 deletions

View file

@ -14,6 +14,7 @@ from .util import (
TemporaryError,
)
XHV_COIN = 10 ** 12
XMR_COIN = 10 ** 12
@ -33,6 +34,7 @@ class Coins(IntEnum):
FIRO = 13
NAV = 14
LTC_MWEB = 15
XHV = 16
chainparams = {
@ -370,6 +372,30 @@ chainparams = {
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
Coins.XHV: {
'name': 'haven',
'ticker': 'XHV',
'client': 'xhv',
'decimal_places': 12,
'mainnet': {
'rpcport': 17750,
'walletrpcport': 17751,
'min_amount': 100000,
'max_amount': 10000 * XHV_COIN,
},
'testnet': {
'rpcport': 27750,
'walletrpcport': 27751,
'min_amount': 100000,
'max_amount': 10000 * XHV_COIN,
},
'regtest': {
'rpcport': 17750,
'walletrpcport': 17751,
'min_amount': 100000,
'max_amount': 10000 * XHV_COIN,
}
}
}
ticker_map = {}

View file

@ -33,6 +33,10 @@ NAMECOIND = os.getenv('NAMECOIND', 'namecoind' + bin_suffix)
NAMECOIN_CLI = os.getenv('NAMECOIN_CLI', 'namecoin-cli' + bin_suffix)
NAMECOIN_TX = os.getenv('NAMECOIN_TX', 'namecoin-tx' + bin_suffix)
XHV_BINDIR = os.path.expanduser(os.getenv('XHV_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'haven')))
XHVD = os.getenv('XHVD', 'havend' + bin_suffix)
XHV_WALLET_RPC = os.getenv('XHV_WALLET_RPC', 'haven-wallet-rpc' + bin_suffix)
XMR_BINDIR = os.path.expanduser(os.getenv('XMR_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'monero')))
XMRD = os.getenv('XMRD', 'monerod' + bin_suffix)
XMR_WALLET_RPC = os.getenv('XMR_WALLET_RPC', 'monero-wallet-rpc' + bin_suffix)

View file

View file

@ -0,0 +1,168 @@
# HavenPy - A python toolbox for Haven
# Copyright (C) 2016 The MoneroPy Developers and The Haven Developers.
#
# MoneroPy is released under the BSD 3-Clause license. Use and redistribution of
# this software is subject to the license terms in the LICENSE file found in the
# top-level directory of this distribution.
__alphabet = [ord(s) for s in '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz']
__b58base = 58
__UINT64MAX = 2**64
__encodedBlockSizes = [0, 2, 3, 5, 6, 7, 9, 10, 11]
__fullBlockSize = 8
__fullEncodedBlockSize = 11
def _hexToBin(hex):
if len(hex) % 2 != 0:
return "Hex string has invalid length!"
return [int(hex[i*2:i*2+2], 16) for i in range(len(hex)//2)]
def _binToHex(bin):
return "".join([("0" + hex(int(bin[i])).split('x')[1])[-2:] for i in range(len(bin))])
def _strToBin(a):
return [ord(s) for s in a]
def _binToStr(bin):
return ''.join([chr(bin[i]) for i in range(len(bin))])
def _uint8be_to_64(data):
l_data = len(data)
if l_data < 1 or l_data > 8:
return "Invalid input length"
res = 0
switch = 9 - l_data
for i in range(l_data):
if switch == 1:
res = res << 8 | data[i]
elif switch == 2:
res = res << 8 | data[i]
elif switch == 3:
res = res << 8 | data[i]
elif switch == 4:
res = res << 8 | data[i]
elif switch == 5:
res = res << 8 | data[i]
elif switch == 6:
res = res << 8 | data[i]
elif switch == 7:
res = res << 8 | data[i]
elif switch == 8:
res = res << 8 | data[i]
else:
return "Impossible condition"
return res
def _uint64_to_8be(num, size):
res = [0] * size;
if size < 1 or size > 8:
return "Invalid input length"
twopow8 = 2**8
for i in range(size-1,-1,-1):
res[i] = num % twopow8
num = num // twopow8
return res
def encode_block(data, buf, index):
l_data = len(data)
if l_data < 1 or l_data > __fullEncodedBlockSize:
return "Invalid block length: " + str(l_data)
num = _uint8be_to_64(data)
i = __encodedBlockSizes[l_data] - 1
while num > 0:
remainder = num % __b58base
num = num // __b58base
buf[index+i] = __alphabet[remainder];
i -= 1
return buf
def encode(hex):
'''Encode hexadecimal string as base58 (ex: encoding a Monero address).'''
data = _hexToBin(hex)
l_data = len(data)
if l_data == 0:
return ""
full_block_count = l_data // __fullBlockSize
last_block_size = l_data % __fullBlockSize
res_size = full_block_count * __fullEncodedBlockSize + __encodedBlockSizes[last_block_size]
res = [0] * res_size
for i in range(res_size):
res[i] = __alphabet[0]
for i in range(full_block_count):
res = encode_block(data[(i*__fullBlockSize):(i*__fullBlockSize+__fullBlockSize)], res, i * __fullEncodedBlockSize)
if last_block_size > 0:
res = encode_block(data[(full_block_count*__fullBlockSize):(full_block_count*__fullBlockSize+last_block_size)], res, full_block_count * __fullEncodedBlockSize)
return _binToStr(res)
def decode_block(data, buf, index):
l_data = len(data)
if l_data < 1 or l_data > __fullEncodedBlockSize:
return "Invalid block length: " + l_data
res_size = __encodedBlockSizes.index(l_data)
if res_size <= 0:
return "Invalid block size"
res_num = 0
order = 1
for i in range(l_data-1, -1, -1):
digit = __alphabet.index(data[i])
if digit < 0:
return "Invalid symbol"
product = order * digit + res_num
if product > __UINT64MAX:
return "Overflow"
res_num = product
order = order * __b58base
if res_size < __fullBlockSize and 2**(8 * res_size) <= res_num:
return "Overflow 2"
tmp_buf = _uint64_to_8be(res_num, res_size)
for i in range(len(tmp_buf)):
buf[i+index] = tmp_buf[i]
return buf
def decode(enc):
'''Decode a base58 string (ex: a Monero address) into hexidecimal form.'''
enc = _strToBin(enc)
l_enc = len(enc)
if l_enc == 0:
return ""
full_block_count = l_enc // __fullEncodedBlockSize
last_block_size = l_enc % __fullEncodedBlockSize
last_block_decoded_size = __encodedBlockSizes.index(last_block_size)
if last_block_decoded_size < 0:
return "Invalid encoded length"
data_size = full_block_count * __fullBlockSize + last_block_decoded_size
data = [0] * data_size
for i in range(full_block_count):
data = decode_block(enc[(i*__fullEncodedBlockSize):(i*__fullEncodedBlockSize+__fullEncodedBlockSize)], data, i * __fullBlockSize)
if last_block_size > 0:
data = decode_block(enc[(full_block_count*__fullEncodedBlockSize):(full_block_count*__fullEncodedBlockSize+last_block_size)], data, full_block_count * __fullBlockSize)
return _binToHex(data)

View file

@ -329,6 +329,8 @@ class HttpHandler(BaseHTTPRequestHandler):
coins = listAvailableCoins(swap_client, with_variants=False)
with_xmr: bool = any(c[0] == Coins.XMR for c in coins)
coins = [c for c in coins if c[0] != Coins.XMR]
with_xhv: bool = any(c[0] == Coins.XHV for c in coins)
coins = [c for c in coins if c[0] != Coins.XHV]
if any(c[0] == Coins.LTC for c in coins):
coins.append((-5, 'Litecoin MWEB Wallet'))
@ -336,6 +338,10 @@ class HttpHandler(BaseHTTPRequestHandler):
coins.append((-2, 'Monero'))
coins.append((-3, 'Monero JSON'))
coins.append((-4, 'Monero Wallet'))
if with_xhv:
coins.append((-2, 'Haven'))
coins.append((-3, 'Haven JSON'))
coins.append((-4, 'Haven Wallet'))
return self.render_template(template, {
'messages': messages,

575
basicswap/interface/xhv.py Normal file
View file

@ -0,0 +1,575 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import logging
import basicswap.contrib.ed25519_fast as edf
import basicswap.ed25519_fast_util as edu
import basicswap.util_xhv as xhv_util
from coincurve.ed25519 import (
ed25519_add,
ed25519_get_pubkey,
ed25519_scalar_add,
)
from coincurve.keys import PrivateKey
from coincurve.dleag import (
dleag_prove,
dleag_verify,
dleag_proof_len,
verify_ed25519_point,
)
from basicswap.interface import (
Curves)
from basicswap.util import (
i2b, b2i, b2h,
dumpj,
ensure,
make_int,
TemporaryError)
from basicswap.util.network import (
is_private_ip_address)
from basicswap.rpc_xhv import (
make_xhv_rpc_func,
make_xhv_rpc2_func)
from basicswap.chainparams import XHV_COIN, CoinInterface, Coins
class XHVInterface(CoinInterface):
@staticmethod
def curve_type():
return Curves.ed25519
@staticmethod
def coin_type():
return Coins.XHV
@staticmethod
def COIN():
return XHV_COIN
@staticmethod
def exp() -> int:
return 12
@staticmethod
def nbk() -> int:
return 32
@staticmethod
def nbK() -> int: # No. of bytes requires to encode a public key
return 32
@staticmethod
def depth_spendable() -> int:
return 10
@staticmethod
def xhv_swap_a_lock_spend_tx_vsize() -> int:
raise ValueError('Not possible')
@staticmethod
def xhv_swap_b_lock_spend_tx_vsize() -> int:
# TODO: Estimate with ringsize
return 1604
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(network)
self.blocks_confirmed = coin_settings['blocks_confirmed']
self._restore_height = coin_settings.get('restore_height', 0)
self.setFeePriority(coin_settings.get('fee_priority', 2))
self._sc = swap_client
self._log = self._sc.log if self._sc and self._sc.log else logging
self._wallet_password = None
self._have_checked_seed = False
daemon_login = None
if coin_settings.get('rpcuser', '') != '':
daemon_login = (coin_settings.get('rpcuser', ''), coin_settings.get('rpcpassword', ''))
rpchost = coin_settings.get('rpchost', '127.0.0.1')
proxy_host = None
proxy_port = None
# Connect to the daemon over a proxy if not running locally
if swap_client:
chain_client_settings = swap_client.getChainClientSettings(self.coin_type())
manage_daemon: bool = chain_client_settings['manage_daemon']
if swap_client.use_tor_proxy:
if manage_daemon is False:
log_str: str = ''
have_cc_tor_opt = 'use_tor' in chain_client_settings
if have_cc_tor_opt and chain_client_settings['use_tor'] is False:
log_str = ' bypassing proxy (use_tor false for XHV)'
elif have_cc_tor_opt is False and is_private_ip_address(rpchost):
log_str = ' bypassing proxy (private ip address)'
else:
proxy_host = swap_client.tor_proxy_host
proxy_port = swap_client.tor_proxy_port
log_str = f' through proxy at {proxy_host}'
self._log.info(f'Connecting to remote {self.coin_name()} daemon at {rpchost}{log_str}.')
else:
self._log.info(f'Not connecting to local {self.coin_name()} daemon through proxy.')
elif manage_daemon is False:
self._log.info(f'Connecting to remote {self.coin_name()} daemon at {rpchost}.')
self._rpctimeout = coin_settings.get('rpctimeout', 60)
self._walletrpctimeout = coin_settings.get('walletrpctimeout', 120)
self._walletrpctimeoutlong = coin_settings.get('walletrpctimeoutlong', 600)
self.rpc = make_xhv_rpc_func(coin_settings['rpcport'], daemon_login, host=rpchost, proxy_host=proxy_host, proxy_port=proxy_port, default_timeout=self._rpctimeout, tag='Node(j) ')
self.rpc2 = make_xhv_rpc2_func(coin_settings['rpcport'], daemon_login, host=rpchost, proxy_host=proxy_host, proxy_port=proxy_port, default_timeout=self._rpctimeout, tag='Node ') # non-json endpoint
self.rpc_wallet = make_xhv_rpc_func(coin_settings['walletrpcport'], coin_settings['walletrpcauth'], host=coin_settings.get('walletrpchost', '127.0.0.1'), default_timeout=self._walletrpctimeout, tag='Wallet ')
def checkWallets(self) -> int:
return 1
def setFeePriority(self, new_priority):
ensure(new_priority >= 0 and new_priority < 4, 'Invalid fee_priority value')
self._fee_priority = new_priority
def setWalletFilename(self, wallet_filename):
self._wallet_filename = wallet_filename
def createWallet(self, params):
if self._wallet_password is not None:
params['password'] = self._wallet_password
rv = self.rpc_wallet('generate_from_keys', params)
self._log.info('generate_from_keys %s', dumpj(rv))
def openWallet(self, filename):
params = {'filename': filename}
if self._wallet_password is not None:
params['password'] = self._wallet_password
try:
# Can't reopen the same wallet in windows, !is_keys_file_locked()
self.rpc_wallet('close_wallet')
except Exception:
pass
self.rpc_wallet('open_wallet', params)
def initialiseWallet(self, key_view, key_spend, restore_height=None):
with self._mx_wallet:
try:
self.openWallet(self._wallet_filename)
# TODO: Check address
return # Wallet exists
except Exception as e:
pass
Kbv = self.getPubkey(key_view)
Kbs = self.getPubkey(key_spend)
address_b58 = xhv_util.encode_address(Kbv, Kbs)
params = {
'filename': self._wallet_filename,
'address': address_b58,
'viewkey': b2h(key_view[::-1]),
'spendkey': b2h(key_spend[::-1]),
'restore_height': self._restore_height,
}
self.createWallet(params)
self.openWallet(self._wallet_filename)
def ensureWalletExists(self) -> None:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
def testDaemonRPC(self, with_wallet=True) -> None:
self.rpc_wallet('get_languages')
def getDaemonVersion(self):
return self.rpc_wallet('get_version')['version']
def getBlockchainInfo(self):
get_height = self.rpc2('get_height', timeout=self._rpctimeout)
rv = {
'blocks': get_height['height'],
'verificationprogress': 0.0,
}
try:
# get_block_count.block_count is how many blocks are in the longest chain known to the node.
# get_block_count returns "Internal error" if bootstrap-daemon is active
if get_height['untrusted'] is True:
rv['bootstrapping'] = True
get_info = self.rpc2('get_info', timeout=self._rpctimeout)
if 'height_without_bootstrap' in get_info:
rv['blocks'] = get_info['height_without_bootstrap']
rv['known_block_count'] = get_info['height']
if rv['known_block_count'] > rv['blocks']:
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count']
else:
rv['known_block_count'] = self.rpc('get_block_count', timeout=self._rpctimeout)['count']
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count']
except Exception as e:
self._log.warning('XHV get_block_count failed with: %s', str(e))
rv['verificationprogress'] = 0.0
return rv
def getChainHeight(self):
return self.rpc2('get_height', timeout=self._rpctimeout)['height']
def getWalletInfo(self):
with self._mx_wallet:
try:
self.openWallet(self._wallet_filename)
except Exception as e:
if 'Failed to open wallet' in str(e):
rv = {'encrypted': True, 'locked': True, 'balance': 0, 'unconfirmed_balance': 0}
return rv
raise e
rv = {}
self.rpc_wallet('refresh')
balance_info = self.rpc_wallet('get_balance')
rv['balance'] = self.format_amount(balance_info['unlocked_balance'])
rv['unconfirmed_balance'] = self.format_amount(balance_info['balance'] - balance_info['unlocked_balance'])
rv['encrypted'] = False if self._wallet_password is None else True
rv['locked'] = False
return rv
def walletRestoreHeight(self):
return self._restore_height
def getMainWalletAddress(self) -> str:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
return self.rpc_wallet('get_address')['address']
def getNewAddress(self, placeholder) -> str:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
new_address = self.rpc_wallet('create_address', {'account_index': 0})['address']
self.rpc_wallet('store')
return new_address
def get_fee_rate(self, conf_target: int = 2):
# fees - array of unsigned int; Represents the base fees at different priorities [slow, normal, fast, fastest].
fee_est = self.rpc('get_fee_estimate')
if conf_target <= 1:
conf_target = 1 # normal
else:
conf_target = 0 # slow
fee_per_k_bytes = fee_est['fees'][conf_target] * 1000
return float(self.format_amount(fee_per_k_bytes)), 'get_fee_estimate'
def getNewSecretKey(self) -> bytes:
# Note: Returned bytes are in big endian order
return i2b(edu.get_secret())
def pubkey(self, key: bytes) -> bytes:
return edf.scalarmult_B(key)
def encodeKey(self, vk: bytes) -> str:
return vk[::-1].hex()
def decodeKey(self, k_hex: str) -> bytes:
return bytes.fromhex(k_hex)[::-1]
def encodePubkey(self, pk: bytes) -> str:
return edu.encodepoint(pk)
def decodePubkey(self, pke):
return edf.decodepoint(pke)
def getPubkey(self, privkey):
return ed25519_get_pubkey(privkey)
def getAddressFromKeys(self, key_view: bytes, key_spend: bytes) -> str:
pk_view = self.getPubkey(key_view)
pk_spend = self.getPubkey(key_spend)
return xhv_util.encode_address(pk_view, pk_spend)
def verifyKey(self, k: int) -> bool:
i = b2i(k)
return (i < edf.l and i > 8)
def verifyPubkey(self, pubkey_bytes):
# Calls ed25519_decode_check_point() in secp256k1
# Checks for small order
return verify_ed25519_point(pubkey_bytes)
def proveDLEAG(self, key: bytes) -> bytes:
privkey = PrivateKey(key)
return dleag_prove(privkey)
def verifyDLEAG(self, dleag_bytes: bytes) -> bool:
return dleag_verify(dleag_bytes)
def lengthDLEAG(self) -> int:
return dleag_proof_len()
def sumKeys(self, ka: bytes, kb: bytes) -> bytes:
return ed25519_scalar_add(ka, kb)
def sumPubkeys(self, Ka: bytes, Kb: bytes) -> bytes:
return ed25519_add(Ka, Kb)
def encodeSharedAddress(self, Kbv: bytes, Kbs: bytes) -> str:
return xhv_util.encode_address(Kbv, Kbs)
def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, unlock_time: int = 0) -> bytes:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet('refresh')
Kbv = self.getPubkey(kbv)
shared_addr = xhv_util.encode_address(Kbv, Kbs)
params = {'destinations': [{'amount': output_amount, 'address': shared_addr}], 'unlock_time': unlock_time}
if self._fee_priority > 0:
params['priority'] = self._fee_priority
rv = self.rpc_wallet('transfer', params)
self._log.info('publishBLockTx %s to address_b58 %s', rv['tx_hash'], shared_addr)
tx_hash = bytes.fromhex(rv['tx_hash'])
return tx_hash
def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender):
with self._mx_wallet:
Kbv = self.getPubkey(kbv)
address_b58 = xhv_util.encode_address(Kbv, Kbs)
kbv_le = kbv[::-1]
params = {
'restore_height': restore_height,
'filename': address_b58,
'address': address_b58,
'viewkey': b2h(kbv_le),
}
try:
self.openWallet(address_b58)
except Exception as e:
self.createWallet(params)
self.openWallet(address_b58)
self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong)
'''
# Debug
try:
current_height = self.rpc_wallet('get_height')['height']
self._log.info('findTxB XHV current_height %d\nAddress: %s', current_height, address_b58)
except Exception as e:
self._log.info('rpc failed %s', str(e))
current_height = None # If the transfer is available it will be deep enough
# and (current_height is None or current_height - transfer['block_height'] > cb_block_confirmed):
'''
params = {'transfer_type': 'available'}
transfers = self.rpc_wallet('incoming_transfers', params)
rv = None
if 'transfers' in transfers:
for transfer in transfers['transfers']:
# unlocked <- wallet->is_transfer_unlocked() checks unlock_time and CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE
if not transfer['unlocked']:
full_tx = self.rpc_wallet('get_transfer_by_txid', {'txid': transfer['tx_hash']})
unlock_time = full_tx['transfer']['unlock_time']
if unlock_time != 0:
self._log.warning('Coin b lock txn is locked: {}, unlock_time {}'.format(transfer['tx_hash'], unlock_time))
rv = -1
continue
if transfer['amount'] == cb_swap_value:
return {'txid': transfer['tx_hash'], 'amount': transfer['amount'], 'height': 0 if 'block_height' not in transfer else transfer['block_height']}
else:
self._log.warning('Incorrect amount detected for coin b lock txn: {}'.format(transfer['tx_hash']))
rv = -1
return rv
def findTxnByHash(self, txid):
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong)
try:
current_height = self.rpc2('get_height', timeout=self._rpctimeout)['height']
self._log.info('findTxnByHash XHV current_height %d\nhash: %s', current_height, txid)
except Exception as e:
self._log.info('rpc failed %s', str(e))
current_height = None # If the transfer is available it will be deep enough
params = {'transfer_type': 'available'}
rv = self.rpc_wallet('incoming_transfers', params)
if 'transfers' in rv:
for transfer in rv['transfers']:
if transfer['tx_hash'] == txid \
and (current_height is None or current_height - transfer['block_height'] > self.blocks_confirmed):
return {'txid': transfer['tx_hash'], 'amount': transfer['amount'], 'height': transfer['block_height']}
return None
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee_rate: int, restore_height: int, spend_actual_balance: bool = False) -> bytes:
'''
Notes:
"Error: No unlocked balance in the specified subaddress(es)" can mean not enough funds after tx fee.
'''
with self._mx_wallet:
Kbv = self.getPubkey(kbv)
Kbs = self.getPubkey(kbs)
address_b58 = xhv_util.encode_address(Kbv, Kbs)
wallet_filename = address_b58 + '_spend'
params = {
'filename': wallet_filename,
'address': address_b58,
'viewkey': b2h(kbv[::-1]),
'spendkey': b2h(kbs[::-1]),
'restore_height': restore_height,
}
try:
self.openWallet(wallet_filename)
except Exception as e:
self.createWallet(params)
self.openWallet(wallet_filename)
self.rpc_wallet('refresh')
rv = self.rpc_wallet('get_balance')
if rv['balance'] < cb_swap_value:
self._log.warning('Balance is too low, checking for existing spend.')
txns = self.rpc_wallet('get_transfers', {'out': True})
if 'out' in txns:
txns = txns['out']
if len(txns) > 0:
txid = txns[0]['txid']
self._log.warning(f'spendBLockTx detected spending tx: {txid}.')
if txns[0]['address'] == address_b58:
return bytes.fromhex(txid)
self._log.error('wallet {} balance {}, expected {}'.format(wallet_filename, rv['balance'], cb_swap_value))
if not spend_actual_balance:
raise TemporaryError('Invalid balance')
if spend_actual_balance and rv['balance'] != cb_swap_value:
self._log.warning('Spending actual balance {}, not swap value {}.'.format(rv['balance'], cb_swap_value))
cb_swap_value = rv['balance']
if rv['unlocked_balance'] < cb_swap_value:
self._log.error('wallet {} balance {}, expected {}, blocks_to_unlock {}'.format(wallet_filename, rv['unlocked_balance'], cb_swap_value, rv['blocks_to_unlock']))
raise TemporaryError('Invalid unlocked_balance')
params = {'address': address_to}
if self._fee_priority > 0:
params['priority'] = self._fee_priority
rv = self.rpc_wallet('sweep_all', params)
self._log.debug('sweep_all {}'.format(json.dumps(rv)))
return bytes.fromhex(rv['tx_hash_list'][0])
def withdrawCoin(self, value, addr_to: str, sweepall: bool, estimate_fee: bool = False) -> str:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet('refresh')
if sweepall:
balance = self.rpc_wallet('get_balance')
if balance['balance'] != balance['unlocked_balance']:
raise ValueError('Balance must be fully confirmed to use sweep all.')
self._log.info('XHV {} sweep_all.'.format('estimate fee' if estimate_fee else 'withdraw'))
self._log.debug('XHV balance: {}'.format(balance['balance']))
params = {'address': addr_to, 'do_not_relay': estimate_fee}
if self._fee_priority > 0:
params['priority'] = self._fee_priority
rv = self.rpc_wallet('sweep_all', params)
if estimate_fee:
return {'num_txns': len(rv['fee_list']), 'sum_amount': sum(rv['amount_list']), 'sum_fee': sum(rv['fee_list']), 'sum_weight': sum(rv['weight_list'])}
return rv['tx_hash_list'][0]
value_sats: int = make_int(value, self.exp())
params = {'destinations': [{'amount': value_sats, 'address': addr_to}], 'do_not_relay': estimate_fee}
if self._fee_priority > 0:
params['priority'] = self._fee_priority
rv = self.rpc_wallet('transfer', params)
if estimate_fee:
return {'num_txns': 1, 'sum_amount': rv['amount'], 'sum_fee': rv['fee'], 'sum_weight': rv['weight']}
return rv['tx_hash']
def estimateFee(self, value: int, addr_to: str, sweepall: bool) -> str:
return self.withdrawCoin(value, addr_to, sweepall, estimate_fee=True)
def showLockTransfers(self, kbv, Kbs, restore_height):
with self._mx_wallet:
try:
Kbv = self.getPubkey(kbv)
address_b58 = xhv_util.encode_address(Kbv, Kbs)
wallet_file = address_b58 + '_spend'
try:
self.openWallet(wallet_file)
except Exception:
wallet_file = address_b58
try:
self.openWallet(wallet_file)
except Exception:
self._log.info(f'showLockTransfers trying to create wallet for address {address_b58}.')
kbv_le = kbv[::-1]
params = {
'restore_height': restore_height,
'filename': address_b58,
'address': address_b58,
'viewkey': b2h(kbv_le),
}
self.createWallet(params)
self.openWallet(address_b58)
self.rpc_wallet('refresh')
rv = self.rpc_wallet('get_transfers', {'in': True, 'out': True, 'pending': True, 'failed': True})
rv['filename'] = wallet_file
return rv
except Exception as e:
return {'error': str(e)}
def getSpendableBalance(self) -> int:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet('refresh')
balance_info = self.rpc_wallet('get_balance')
return balance_info['unlocked_balance']
def changeWalletPassword(self, old_password, new_password):
self._log.info('changeWalletPassword - {}'.format(self.ticker()))
orig_password = self._wallet_password
if old_password != '':
self._wallet_password = old_password
try:
self.openWallet(self._wallet_filename)
self.rpc_wallet('change_wallet_password', {'old_password': old_password, 'new_password': new_password})
except Exception as e:
self._wallet_password = orig_password
raise e
def unlockWallet(self, password: str) -> None:
self._log.info('unlockWallet - {}'.format(self.ticker()))
self._wallet_password = password
if not self._have_checked_seed:
self._sc.checkWalletSeed(self.coin_type())
def lockWallet(self) -> None:
self._log.info('lockWallet - {}'.format(self.ticker()))
self._wallet_password = None
def isAddressMine(self, address):
# TODO
return True
def ensureFunds(self, amount: int) -> None:
if self.getSpendableBalance() < amount:
raise ValueError('Balance too low')
def getTransaction(self, txid: bytes):
return self.rpc2('get_transactions', {'txs_hashes': [txid.hex(), ]})

View file

@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from sqlalchemy.orm import scoped_session
from basicswap.util import (
ensure,
)
from basicswap.interface import Curves
from basicswap.chainparams import (
Coins,
)
from basicswap.basicswap_util import (
KeyTypes,
SwapTypes,
EventLogTypes,
)
from . import ProtocolInterface
from basicswap.contrib.test_framework.script import (
CScript, CScriptOp,
OP_CHECKMULTISIG)
def addLockRefundSigs(self, xhv_swap, ci):
self.log.debug('Setting lock refund tx sigs')
witness_stack = [
b'',
xhv_swap.al_lock_refund_tx_sig,
xhv_swap.af_lock_refund_tx_sig,
xhv_swap.a_lock_tx_script,
]
signed_tx = ci.setTxSignature(xhv_swap.a_lock_refund_tx, witness_stack)
ensure(signed_tx, 'setTxSignature failed')
xhv_swap.a_lock_refund_tx = signed_tx
def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key):
self.log.info('Manually recovering %s', bid_id.hex())
# Manually recover txn if other key is known
session = scoped_session(self.session_factory)
try:
bid, xhv_swap = self.getXhvBidFromSession(session, bid_id)
ensure(bid, 'Bid not found: {}.'.format(bid_id.hex()))
ensure(xhv_swap, 'Adaptor-sig swap not found: {}.'.format(bid_id.hex()))
offer, xhv_offer = self.getXhvOfferFromSession(session, bid.offer_id, sent=False)
ensure(offer, 'Offer not found: {}.'.format(bid.offer_id.hex()))
ensure(xhv_offer, 'Adaptor-sig offer not found: {}.'.format(bid.offer_id.hex()))
ci_to = self.ci(offer.coin_to)
for_ed25519 = True if Coins(offer.coin_to) == Coins.XHV else False
try:
decoded_key_half = ci_to.decodeKey(encoded_key)
except Exception as e:
raise ValueError('Failed to decode provided key-half: ', str(e))
if bid.was_sent:
kbsl = decoded_key_half
kbsf = self.getPathKey(offer.coin_from, offer.coin_to, bid.created_at, xhv_swap.contract_count, KeyTypes.KBSF, for_ed25519)
else:
kbsl = self.getPathKey(offer.coin_from, offer.coin_to, bid.created_at, xhv_swap.contract_count, KeyTypes.KBSL, for_ed25519)
kbsf = decoded_key_half
ensure(ci_to.verifyKey(kbsl), 'Invalid kbsl')
ensure(ci_to.verifyKey(kbsf), 'Invalid kbsf')
vkbs = ci_to.sumKeys(kbsl, kbsf)
if offer.coin_to == Coins.XHV:
address_to = self.getCachedMainWalletAddress(ci_to)
else:
address_to = self.getCachedStealthAddressForCoin(offer.coin_to)
amount = bid.amount_to
txid = ci_to.spendBLockTx(xhv_swap.b_lock_tx_id, address_to, xhv_swap.vkbv, vkbs, bid.amount_to, xhv_offer.b_fee_rate, bid.chain_b_height_start, spend_actual_balance=True)
self.log.debug('Submitted lock B spend 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_SPEND_TX_PUBLISHED, txid.hex(), session)
session.commit()
return txid
finally:
session.close()
session.remove()
def getChainBSplitKey(swap_client, bid, xhv_swap, offer):
reverse_bid: bool = offer.bid_reversed
ci_follower = swap_client.ci(offer.coin_from if reverse_bid else offer.coin_to)
key_type = KeyTypes.KBSF if bid.was_sent else KeyTypes.KBSL
return ci_follower.encodeKey(swap_client.getPathKey(offer.coin_from, offer.coin_to, bid.created_at, xhv_swap.contract_count, key_type, True if ci_follower.coin_type() == Coins.XHV else False))
def getChainBRemoteSplitKey(swap_client, bid, xhv_swap, offer):
reverse_bid: bool = offer.bid_reversed
ci_leader = swap_client.ci(offer.coin_to if reverse_bid else offer.coin_from)
ci_follower = swap_client.ci(offer.coin_from if reverse_bid else offer.coin_to)
if bid.was_sent:
if xhv_swap.a_lock_refund_spend_tx:
af_lock_refund_spend_tx_sig = ci_leader.extractFollowerSig(xhv_swap.a_lock_refund_spend_tx)
kbsl = ci_leader.recoverEncKey(xhv_swap.af_lock_refund_spend_tx_esig, af_lock_refund_spend_tx_sig, xhv_swap.pkasl)
return ci_follower.encodeKey(kbsl)
else:
if xhv_swap.a_lock_spend_tx:
al_lock_spend_tx_sig = ci_leader.extractLeaderSig(xhv_swap.a_lock_spend_tx)
kbsf = ci_leader.recoverEncKey(xhv_swap.al_lock_spend_tx_esig, al_lock_spend_tx_sig, xhv_swap.pkasf)
return ci_follower.encodeKey(kbsf)
return None
def setDLEAG(xhv_swap, ci_to, kbsf: bytes) -> None:
if ci_to.curve_type() == Curves.ed25519:
xhv_swap.kbsf_dleag = ci_to.proveDLEAG(kbsf)
xhv_swap.pkasf = xhv_swap.kbsf_dleag[0: 33]
elif ci_to.curve_type() == Curves.secp256k1:
for i in range(10):
xhv_swap.kbsf_dleag = ci_to.signRecoverable(kbsf, 'proof kbsf owned for swap')
pk_recovered: bytes = ci_to.verifySigAndRecover(xhv_swap.kbsf_dleag, 'proof kbsf owned for swap')
if pk_recovered == xhv_swap.pkbsf:
break
# self.log.debug('kbsl recovered pubkey mismatch, retrying.')
assert (pk_recovered == xhv_swap.pkbsf)
xhv_swap.pkasf = xhv_swap.pkbsf
else:
raise ValueError('Unknown curve')
class XhvSwapInterface(ProtocolInterface):
swap_type = SwapTypes.XHV_SWAP
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes) -> CScript:
Kal_enc = Kal if len(Kal) == 33 else ci.encodePubkey(Kal)
Kaf_enc = Kaf if len(Kaf) == 33 else ci.encodePubkey(Kaf)
return CScript([2, Kal_enc, Kaf_enc, 2, CScriptOp(OP_CHECKMULTISIG)])
def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes:
addr_to = self.getMockAddrTo(ci)
funded_tx = ci.createRawFundedTransaction(addr_to, amount, sub_fee, lock_unspents=False)
return bytes.fromhex(funded_tx)
def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray:
mock_txo_script = self.getMockScriptScriptPubkey(ci)
real_txo_script = ci.getScriptDest(script)
found: int = 0
ctx = ci.loadTx(mock_tx)
for txo in ctx.vout:
if txo.scriptPubKey == mock_txo_script:
txo.scriptPubKey = real_txo_script
found += 1
if found < 1:
raise ValueError('Mocked output not found')
if found > 1:
raise ValueError('Too many mocked outputs found')
ctx.nLockTime = 0
return ctx.serialize()

258
basicswap/rpc_xhv.py Normal file
View file

@ -0,0 +1,258 @@
# -*- coding: utf-8 -*-
import os
import json
import socks
import time
import urllib
import hashlib
from xmlrpc.client import (
Fault,
Transport,
SafeTransport,
)
from sockshandler import SocksiPyConnection
from .util import jsonDecimal
class SocksTransport(Transport):
def set_proxy(self, proxy_host, proxy_port):
self.proxy_host = proxy_host
self.proxy_port = proxy_port
self.proxy_type = socks.PROXY_TYPE_SOCKS5
self.proxy_rdns = True
self.proxy_username = None
self.proxy_password = None
def make_connection(self, host):
# return an existing connection if possible. This allows
# HTTP/1.1 keep-alive.
if self._connection and host == self._connection[0]:
return self._connection[1]
# create a HTTP connection object from a host descriptor
chost, self._extra_headers, x509 = self.get_host_info(host)
self._connection = host, SocksiPyConnection(self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, self.proxy_username, self.proxy_password, chost)
return self._connection[1]
class JsonrpcDigest():
# __getattr__ complicates extending ServerProxy
def __init__(self, uri, transport=None, encoding=None, verbose=False,
allow_none=False, use_datetime=False, use_builtin_types=False,
*, context=None):
parsed = urllib.parse.urlparse(uri)
if parsed.scheme not in ('http', 'https'):
raise OSError('unsupported XML-RPC protocol')
self.__host = parsed.netloc
self.__handler = parsed.path
if transport is None:
handler = SafeTransport if parsed.scheme == 'https' else Transport
extra_kwargs = {}
transport = handler(use_datetime=use_datetime,
use_builtin_types=use_builtin_types,
**extra_kwargs)
self.__transport = transport
self.__encoding = encoding or 'utf-8'
self.__verbose = verbose
self.__allow_none = allow_none
self.__request_id = 0
def close(self):
if self.__transport is not None:
self.__transport.close()
def request_id(self):
return self.__request_id
def post_request(self, method, params, timeout=None):
try:
connection = self.__transport.make_connection(self.__host)
if timeout:
connection.timeout = timeout
headers = self.__transport._extra_headers[:]
connection.putrequest('POST', self.__handler)
headers.append(('Content-Type', 'application/json'))
headers.append(('User-Agent', 'jsonrpc'))
self.__transport.send_headers(connection, headers)
self.__transport.send_content(connection, '' if params is None else json.dumps(params, default=jsonDecimal).encode('utf-8'))
self.__request_id += 1
resp = connection.getresponse()
return resp.read()
except Fault:
raise
except Exception:
self.__transport.close()
raise
def json_request(self, request_body, username='', password='', timeout=None):
try:
connection = self.__transport.make_connection(self.__host)
if timeout:
connection.timeout = timeout
headers = self.__transport._extra_headers[:]
connection.putrequest('POST', self.__handler)
headers.append(('Content-Type', 'application/json'))
headers.append(('Connection', 'keep-alive'))
self.__transport.send_headers(connection, headers)
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8') if request_body else '')
resp = connection.getresponse()
if resp.status == 401:
resp_headers = resp.getheaders()
v = resp.read()
algorithm = ''
realm = ''
nonce = ''
for h in resp_headers:
if h[0] != 'WWW-authenticate':
continue
fields = h[1].split(',')
for f in fields:
key, value = f.split('=', 1)
if key == 'algorithm' and value != 'MD5':
break
if key == 'realm':
realm = value.strip('"')
if key == 'nonce':
nonce = value.strip('"')
if realm != '' and nonce != '':
break
if realm == '' or nonce == '':
raise ValueError('Authenticate header not found.')
path = self.__handler
HA1 = hashlib.md5(f'{username}:{realm}:{password}'.encode('utf-8')).hexdigest()
http_method = 'POST'
HA2 = hashlib.md5(f'{http_method}:{path}'.encode('utf-8')).hexdigest()
ncvalue = '{:08x}'.format(1)
s = ncvalue.encode('utf-8')
s += nonce.encode('utf-8')
s += time.ctime().encode('utf-8')
s += os.urandom(8)
cnonce = (hashlib.sha1(s).hexdigest()[:16])
# MD5-SESS
HA1 = hashlib.md5(f'{HA1}:{nonce}:{cnonce}'.encode('utf-8')).hexdigest()
respdig = hashlib.md5(f'{HA1}:{nonce}:{ncvalue}:{cnonce}:auth:{HA2}'.encode('utf-8')).hexdigest()
header_value = f'Digest username="{username}", realm="{realm}", nonce="{nonce}", uri="{path}", response="{respdig}", algorithm="MD5-sess", qop="auth", nc={ncvalue}, cnonce="{cnonce}"'
headers = self.__transport._extra_headers[:]
headers.append(('Authorization', header_value))
connection.putrequest('POST', self.__handler)
headers.append(('Content-Type', 'application/json'))
headers.append(('Connection', 'keep-alive'))
self.__transport.send_headers(connection, headers)
self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8') if request_body else '')
resp = connection.getresponse()
self.__request_id += 1
return resp.read()
except Fault:
raise
except Exception:
self.__transport.close()
raise
def callrpc_xmr(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rpc', auth=None, timeout=120, transport=None, tag=''):
# auth is a tuple: (username, password)
try:
if rpc_host.count('://') > 0:
url = '{}:{}/{}'.format(rpc_host, rpc_port, path)
else:
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, path)
x = JsonrpcDigest(url, transport=transport)
request_body = {
'method': method,
'params': params,
'jsonrpc': '2.0',
'id': x.request_id()
}
if auth:
v = x.json_request(request_body, username=auth[0], password=auth[1], timeout=timeout)
else:
v = x.json_request(request_body, timeout=timeout)
x.close()
r = json.loads(v.decode('utf-8'))
except Exception as ex:
raise ValueError('{}RPC Server Error: {}'.format(tag, str(ex)))
if 'error' in r and r['error'] is not None:
raise ValueError(tag + 'RPC error ' + str(r['error']))
return r['result']
def callrpc_xmr2(rpc_port: int, method: str, params=None, auth=None, rpc_host='127.0.0.1', timeout=120, transport=None, tag=''):
try:
if rpc_host.count('://') > 0:
url = '{}:{}/{}'.format(rpc_host, rpc_port, method)
else:
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, method)
x = JsonrpcDigest(url, transport=transport)
if auth:
v = x.json_request(params, username=auth[0], password=auth[1], timeout=timeout)
else:
v = x.json_request(params, timeout=timeout)
x.close()
r = json.loads(v.decode('utf-8'))
except Exception as ex:
raise ValueError('{}RPC Server Error: {}'.format(tag, str(ex)))
return r
def make_xmr_rpc2_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=None, default_timeout=120, tag=''):
port = port
auth = auth
host = host
transport = None
default_timeout = default_timeout
tag = tag
if proxy_host:
transport = SocksTransport()
transport.set_proxy(proxy_host, proxy_port)
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
nonlocal port, auth, host, transport, tag
return callrpc_xmr2(port, method, params, auth=auth, rpc_host=host, timeout=timeout, transport=transport, tag=tag)
return rpc_func
def make_xmr_rpc_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=None, default_timeout=120, tag=''):
port = port
auth = auth
host = host
transport = None
default_timeout = default_timeout
tag = tag
if proxy_host:
transport = SocksTransport()
transport.set_proxy(proxy_host, proxy_port)
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
nonlocal port, auth, host, transport, tag
return callrpc_xmr(port, method, params, rpc_host=host, auth=auth, timeout=timeout, transport=transport, tag=tag)
return rpc_func

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

View file

@ -0,0 +1,864 @@
{% include 'header.html' %}
{% from 'style.html' import input_arrow_down_svg %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li>
<svg width="6" height="15" viewBox="0 0 6 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.34 0.671999L2.076 14.1H0.732L3.984 0.671999H5.34Z" fill="#BBC3CF"></path>
</svg>
</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="#">Bids</a>
</li>
<li>
<svg width="6" height="15" viewBox="0 0 6 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.34 0.671999L2.076 14.1H0.732L3.984 0.671999H5.34Z" fill="#BBC3CF"></path>
</svg>
</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="{{ bid_id }}">BID ID: {{ bid_id }}</a>
</li>
</ul>
</div>
</div>
</section>
<section class="py-3">
<div class="container px-4 mx-auto">
<div class="relative py-11 px-16 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="mb-6 text-4xl font-bold text-white tracking-tighter">Bid {% if debug_mode == true %} (Debug: bid_xhv template) {% endif %}</h2>
<p class="font-normal text-coolGray-200 dark:text-white">Bid ID: {{ bid_id }}</p>
</div>
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto"> {% if refresh %} <a id="refresh" href="/bid/{{ bid_id }}" class="flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
<svg class="text-gray-500 w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<g fill="#ffffff">
<path fill="#ffffff" d="M12,3c1.989,0,3.873,0.65,5.43,1.833l-3.604,3.393l9.167,0.983L22.562,0l-3.655,3.442 C16.957,1.862,14.545,1,12,1C5.935,1,1,5.935,1,12h2C3,7.037,7.037,3,12,3z"></path>
<path data-color="color-2" d="M12,21c-1.989,0-3.873-0.65-5.43-1.833l3.604-3.393l-9.167-0.983L1.438,24l3.655-3.442 C7.043,22.138,9.455,23,12,23c6.065,0,11-4.935,11-11h-2C21,16.963,16.963,21,12,21z"></path>
</g>
</svg>
<span>Refresh {{ refresh }} seconds</span>
</a>
{% else %}
<a id="refresh" href="/bid/{{ bid_id }}" class="flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
<svg class="text-gray-500 w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<g fill="#ffffff">
<path fill="#ffffff" d="M12,3c1.989,0,3.873,0.65,5.43,1.833l-3.604,3.393l9.167,0.983L22.562,0l-3.655,3.442 C16.957,1.862,14.545,1,12,1C5.935,1,1,5.935,1,12h2C3,7.037,7.037,3,12,3z"></path>
<path data-color="color-2" d="M12,21c-1.989,0-3.873-0.65-5.43-1.833l3.604-3.393l-9.167-0.983L1.438,24l3.655-3.442 C7.043,22.138,9.455,23,12,23c6.065,0,11-4.935,11-11h-2C21,16.963,16.963,21,12,21z"></path>
</g>
</svg>
<span>Refresh</span>
</a>
{% endif %}
</div>
</div>
</div>
</div>
</section>
{% include 'inc_messages.html' %}
<section>
<div class="pl-6 pr-6 pt-0 pb-0 mt-5 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<div class="w-full mt-6 pb-6 overflow-x-auto">
<table class="w-full min-w-max text-sm">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Options</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Details</span>
</div>
</th>
</tr>
</thead>
{% if data.was_sent == 'True' %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Swap</td>
<td class="py-3 px-6">
<div class="content flex py-2">
<span class="bold">{{ data.amt_to }} {{ data.ticker_to }}</span>
<svg aria-hidden="true " class="w-5 h-5 ml-3 mr-3" fill="currentColor " viewBox="0 0 20 20 " xmlns="http://www.w3.org/2000/svg ">
<path fill-rule="evenodd " d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z " clip-rule="evenodd "></path>
</svg>
<span class="text-xs bold">{{ data.amt_from }} {{ data.ticker_from }}</span>
</div>
</td>
</tr>
{% else %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Swap</td>
<td class="py-3 px-6">
<div class="content flex py-2">
<span class="bold">{{ data.amt_from }} {{ data.ticker_from }}</span>
<svg aria-hidden="true " class="w-5 h-5 ml-3 mr-3" fill="currentColor " viewBox="0 0 20 20 " xmlns="http://www.w3.org/2000/svg ">
<path fill-rule="evenodd " d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z " clip-rule="evenodd "></path>
</svg>
<span class="bold">{{ data.amt_to }} {{ data.ticker_to }}</span>
</div>
</td>
</tr>
{% endif %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Bid Rate</td>
<td class="py-3 px-6">{{ data.bid_rate }}</td>
</tr>
{% if data.was_sent == 'True' %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">You Send</td>
<td class="py-3 px-6">
<span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded">
<img class="h-7" src="/static/images/coins/{{ data.coin_to }}.png" alt="{{ data.coin_to }}">
</span>{{ data.coin_to }}
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">You Get</td>
<td class="py-3 px-6">
<span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded">
<img class="h-7" src="/static/images/coins/{{ data.coin_from }}.png" alt="{{ data.coin_from }}">
</span>{{ data.coin_from }}
</td>
</tr>{% else %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">You Send</td>
<td class="py-3 px-6">
<span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded">
<img class="h-7" src="/static/images/coins/{{ data.coin_from }}.png" alt="{{ data.coin_from }}">
</span>
{{ data.coin_from }}
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">You Get</td>
<td class="py-3 px-6">
<span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded">
<img class="h-7" src="/static/images/coins/{{ data.coin_to }}.png" alt="{{ data.coin_to }}">
</span>
{{ data.coin_to }}
</td>
</tr>
{% endif %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Bid State</td>
<td class="py-3 px-6">{{ data.bid_state }}</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">State Description </td>
<td class="py-3 px-6">{{ data.state_description }}</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Offer</td>
<td class="py-3 px-6">
<a class="monospace bold select-all" href="/offer/{{ data.offer_id }}">{{ data.offer_id }}</a>
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Address From</td>
<td class="py-3 px-6">
<a class="monospace bold select-all" href="/identity/{{ data.addr_from }}">{{ data.addr_from }}</a> {{ data.addr_from_label }}
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="flex items-center px-46 whitespace-nowrap">
<svg alt="" class="w-5 h-5 rounded-full ml-5" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#3B82F6" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points=" 12,6 12,12 18,12 " stroke="#3B82F6"></polyline>
</g>
</svg>
<div class="py-3 pl-2 bold">
<div>Created At</div>
</div>
</td>
<td class="py-3 px-6">{{ data.created_at }}</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="flex items-center px-46 whitespace-nowrap">
<svg alt="" class="w-5 h-5 rounded-full ml-5" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#6b7280" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points=" 12,6 12,12 18,12 " stroke="#6b7280"></polyline>
</g>
</svg>
<div class="py-3 pl-2 bold">
<div>Expired At</div>
</div>
</td>
<td class="py-3 px-6">{{ data.expired_at }}</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Sent</td>
<td class="py-3 px-6">{{ data.was_sent }}</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Received</td>
<td class="py-3 px-6">{{ data.was_received }}</td>
</tr>
{% if data.coin_a_lock_refund_tx_est_final != 'None' %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">{{ data.ticker_from }} lock refund tx valid at</td>
<td class="py-3 px-6">{{ data.coin_a_lock_refund_tx_est_final | formatts }}</td>
</tr>
{% if data.coin_a_lock_refund_swipe_tx_est_final != 'None' %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">{{ data.ticker_from }} lock refund tx swipeable at</td>
<td class="py-3 px-6">{{ data.coin_a_lock_refund_swipe_tx_est_final | formatts }}</td>
</tr>
{% endif %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">{{ data.ticker_from }} chain median time</td>
<td class="py-3 px-6">{{ data.coin_a_last_median_time | formatts }}</td>
</tr>
{% endif %}
</table>
</div>
</div>
</div>
</section>
<section class="p-6">
<div class="flex flex-wrap items-center">
<div class="w-full">
<h4 class="font-semibold text-black dark:text-white text-2xl">Old states</h4>
</div>
</div>
</section>
<section>
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<div class="w-full mt-6 pb-6 overflow-x-auto">
<table class="w-full min-w-max text-sm">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Set at time</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Old states</span>
</div>
</th>
</tr>
</thead>
{% for s in old_states %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="flex items-center whitespace-nowrap">
<svg alt="" class="w-5 h-5 rounded-full ml-5" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#6b7280" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points=" 12,6 12,12 18,12 " stroke="#6b7280"></polyline>
</g>
</svg>
<div class="py-3 pl-2 bold">
<div>{{ s[0] | formatts }}</div>
</div>
</td>
<td class="py-3 px-6">{{ s[1] }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section> {% if data.events %} <section class="p-6">
<div class="flex flex-wrap items-center">
<div class="w-full">
<h4 class="font-semibold text-black dark:text-white text-2xl">Events</h4>
</div>
</div>
</section>
<section>
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<div class="w-full mt-6 pb-6 overflow-x-auto">
<table class="w-full min-w-max text-sm">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Time</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Events</span>
</div>
</th>
</tr>
</thead> {% for e in data.events %} <tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="flex items-center px-46 whitespace-nowrap">
<svg alt="" class="w-5 h-5 rounded-full ml-5" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#6b7280" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle>
<polyline points=" 12,6 12,12 18,12 " stroke="#6b7280"></polyline>
</g>
</svg>
<div class="py-3 pl-2 bold">
<div>{{ e.at | formatts }}</div>
</div>
</td>
<td class="py-3 px-6">{{ e.desc }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% else %}
{% endif %}
<form method="post">
{% if data.show_txns %}
{% if data.xhv_b_shared_address or data.xhv_b_shared_viewkey or data.xhv_b_half_privatekey %}
<section class="p-6">
<div class="flex flex-wrap items-center">
<div class="w-full">
<h4 class="font-semibold text-black dark:text-white text-2xl">View keys/Shared Address</h4>
</div>
</div>
</section>
<section>
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<div class="w-full mt-6 pb-6 overflow-x-auto">
<table class="table w-full min-w-max text-sm">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Type</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Output</span>
</div>
</th>
</tr>
</thead>
{% if data.xhv_b_shared_address %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Shared Address:</td>
<td class="py-3 px-6 monospace">{{ data.xhv_b_shared_address }}</td>
</tr>
{% endif %}
{% if data.xhv_b_shared_viewkey %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Shared View Key:</td>
<td class="py-3 px-6 monospace">{{ data.xhv_b_shared_viewkey }}</td>
</tr>
{% endif %}
{% if data.xhv_b_half_privatekey %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Key Half (WARNING key data!):</td>
<td class="py-3 px-6 monospace">{{ data.xhv_b_half_privatekey }}</td>
</tr>
{% endif %}
{% if data.xhv_b_half_privatekey_remote %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Remote Key Half:</td>
<td class="py-3 px-6 monospace">{{ data.xhv_b_half_privatekey_remote }}</td>
</tr>
{% endif %}
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endif %}
<section class="p-6">
<div class="flex flex-wrap items-center">
<div class="w-full">
<h4 class="font-semibold text-black dark:text-white text-2xl">Transactions</h4>
</div>
</div>
</section>
<section>
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<div class="w-full mt-6 pb-6 overflow-x-auto">
<table class="w-full min-w-max text-sm">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Tx Type</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Tx ID</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Blocks Deep</span>
</div>
</th>
</tr>
</thead>
{% for tx in data.txns %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">{{ tx.type }}</td>
<td class="py-3 px-6 monospace">{{ tx.txid }}</td>
<td class="py-3 px-6">{{ tx.confirms }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="p-6">
<div class="flex flex-wrap items-center">
<div class="w-full">
<h4 class="font-semibold text-black dark:text-white text-2xl">More Information</h4>
</div>
</div>
</section>
<section>
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<div class="w-full mt-6 pb-6 overflow-x-auto">
<table class="w-full min-w-max text-sm">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Setting</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Options</span>
</div>
</th>
</tr>
</thead>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">View Transaction:</td>
<td class="py-3 px-6 bold">
<div class="relative">{{ input_arrow_down_svg | safe }}
<select class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="view_tx">
{% if data.txns|length %} {% for tx in data.txns %}
<option value="{{ tx.txid }}" {% if data.view_tx_ind==tx.txid %} selected{% endif %}>{{ tx.type }} {{ tx.txid }}</option>
{% endfor %}
{% else %}
<option value="0">None exist yet</option>
{% endif %}
</select>
</div>
</td>
</tr>
</table>
<div class="w-full md:w-0/12 mt-5">
<div class="flex flex-wrap justify-end">
<div class="w-full md:w-auto p-1.5 ml-2">
<button name="view_tx_submit" value="View Tx" type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
<span>View Tx</span>
</button>
</div>
<!-- Info: Unimplemented
<div class="w-full md:w-auto p-1.5 ml-2"><button name="view_lock_transfers" value="View Lock Wallet Transfers" type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" ><span>View Lock Wallet Transfers</span></button></div>
-->
</div>
</div>
{% if data.view_tx_hex %}
<table class="w-full min-w-max text-sm mt-10">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 px-6 rounded-tl-xl rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">TX Hex:</span>
</div>
</th>
</tr>
</thead>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6">
<textarea rows="5" class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0 monospace" readonly>{{ data.view_tx_hex }}</textarea>
<textarea rows="20" class="mt-5 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0 monospace" id="tx_view" readonly>{{ data.view_tx_desc }}</textarea>
</td>
</tr>
{% endif %}
{% if data.lock_transfers %}
<!--<label for="transfers_view" class="bold block mb-2 text-sm font-medium text-gray-900">Lock wallet transfers:</label>-->
<textarea rows="20" class="mt-5 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0 monospace" id="transfers_view" readonly>{{ data.lock_transfers }}</textarea>
{% endif %}
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endif %}
{% if data.chain_a_lock_tx_inputs %}
<section class="p-6">
<div class="flex flex-wrap items-center">
<div class="w-full">
<h4 class="font-semibold text-black dark:text-white text-2xl">Chain A Lock TX Inputs</h4>
</div>
</div>
</section>
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<div class="w-full mt-6 pb-6 overflow-x-auto">
<table class="w-full min-w-max text-sm">
<thead class="uppercase">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">TXID</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Out</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Locked</span>
</div>
</th>
</tr>
</thead>
{% for txi in data.chain_a_lock_tx_inputs %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 monospace">{{ txi.txid }}</td>
<td class="py-3 px-6 monospace">{{ txi.vout }}</td>
<td class="py-3 px-6 bold">{% if txi.islocked %} true {% else %} false {% endif %}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endif %}
{% if data.show_bidder_seq_diagram %}
<section class="p-6">
<div class="flex flex-wrap items-center">
<div class="w-full">
<h4 class="font-semibold text-black dark:text-white text-2xl">Bidder Sequence Diagram</h4>
</div>
</div>
</section>
<section>
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<div class="w-full mt-6 pb-6 overflow-x-auto">
<div class="overflow-x-auto items-center justify-center relative">
<div class="flex items-center justify-center min-h-screen">
<div class="flex items-center justify-between text-white">
{% if data.reverse_bid %}
<img class="h-full py-2 pr-4 ml-8" src="/static/sequence_diagrams/ads.rev.bidder.xu.min.svg">
{% else %}
<img class="h-full py-2 pr-4 ml-8" src="/static/sequence_diagrams/ads.bidder.alt.xu.min.svg">
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endif %}
{% if data.show_offerer_seq_diagram %}
<section class="p-6">
<div class="flex flex-wrap items-center">
<div class="w-full">
<h4 class="font-semibold text-black dark:text-white text-2xl">Offerer Sequence Diagram</h4>
</div>
</div>
</section>
<section>
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<div class="w-full mt-6 pb-6 overflow-x-auto">
<div class="overflow-x-auto items-center justify-center relative">
<div class="flex items-center justify-center min-h-screen">
<div class="flex items-center justify-between text-white">
{% if data.reverse_bid %}
<img class="h-full py-2 pr-4 ml-8" src="/static/sequence_diagrams/ads.rev.offerer.xu.min.svg">
{% else %}
<img class="h-full py-2 pr-4 ml-8" src="/static/sequence_diagrams/ads.offerer.alt.xu.min.svg">
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endif %}
{% if edit_bid %}
<section>
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<div class="w-full mt-6 pb-6 overflow-x-auto">
<table class="w-full min-w-max text-sm">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Option</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Settings</span>
</div>
</th>
</tr>
</thead>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Change Bid State:</td>
<td class="py-3 px-6">
<div class="relative">{{ input_arrow_down_svg | safe }}
<select class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="new_state">
{% for s in data.bid_states %}
<option value="{{ s[0] }}" {% if data.bid_state_ind==s[0] %} selected{% endif %}>{{ s[1] }}</option>
{% endfor %}
</select>
</div>
</td>
</tr>
{% if data.debug_ui == true %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Add Bid Action:</td>
<td class="py-3 px-6">
<div class="relative">{{ input_arrow_down_svg | safe }}
<select class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="new_action">
{% for a in data.bid_actions %}
<option value="{{ a[0] }}">{{ a[1] }}</option>
{% endfor %}
</select>
</div>
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Debug Option</td>
<td class="py-3 px-6">
<div class="relative">{{ input_arrow_down_svg | safe }}
<select class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="debugind">
<option{% if data.debug_ind=="-1" %} selected{% endif %} value="-1">None</option>
{% for a in data.debug_options %}
<option{% if data.debug_ind==a[0] %} selected{% endif %} value="{{ a[0] }}">{{ a[1] }}</option>
{% endfor %}
</select>
</div>
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Sweep No-Script TX</td>
<td class="py-3 px-6">
<input class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="text" id="kbs_other" name="kbs_other">
</td>
</tr>
{% endif %}
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section>
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<div class="flex flex-wrap justify-end">
<div class="w-full md:w-auto p-1.5">
<button name="edit_bid_cancel" value="Cancel" type="submit" class="lex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md shadow-button focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Cancel</button>
</div>
<div class="w-full md:w-auto p-1.5">
<button name="edit_bid_submit" value="Submit" type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Submit Edit</button>
</div>
{% else %}
<section>
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<div class="flex flex-wrap justify-end">
{% if data.show_bidder_seq_diagram %}
<div class="w-full md:w-auto p-1.5">
<button name="hide_bidder_seq_diagram" type="submit" value="Hide Bidder Sequence Diagram" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Hide Bidder Sequence Diagram</button>
</div> {% else %} <div class="w-full md:w-auto p-1.5">
<button name="show_bidder_seq_diagram" type="submit" value="Show Bidder Sequence Diagram" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Show Bidder Sequence Diagram</button>
</div>
{% endif %}
{% if data.show_offerer_seq_diagram %}
<div class="w-full md:w-auto p-1.5">
<button name="hide_offerer_seq_diagram" type="submit" value="Hide Offerer Sequence Diagram" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Hide Offerer Sequence Diagram</button>
</div>
{% else %}
<div class="w-full md:w-auto p-1.5">
<button name="show_offerer_seq_diagram" type="submit" value="Show Offerer Sequence Diagram" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Show Offerer Sequence Diagram</button>
</div>
{% endif %}
{% if data.show_txns %}
<div class="w-full md:w-auto p-1.5">
<button name="hide_txns" type="submit" value="Hide Info" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Hide More info</button>
</div>
{% else %}
<div class="w-full md:w-auto p-1.5">
<button name="show_txns" type="submit" value="Show More Info" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Show More Info </button>
</div>
{% endif %}
<div class="w-full md:w-auto p-1.5">
<button name="edit_bid" type="submit" value="Edit Bid" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Edit Bid</button>
</div>
{% endif %}
{% if data.can_abandon == true %}
<div class="w-full md:w-auto p-1.5">
<button name="abandon_bid" type="submit" value="Abandon Bid" onclick="return confirmPopup();" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md focus:ring-0 focus:outline-none">Abandon Bid</button>
</div>
{% endif %}
{% if data.was_received == 'True' and not edit_bid and data.can_accept_bid %}
<div class="w-full md:w-auto p-1.5">
<button name="accept_bid" value="Accept Bid" type="submit" onclick='return confirmPopup("Accept");' class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md focus:ring-0 focus:outline-none">Accept Bid</button>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<input type="hidden" name="formid" value="{{ form_id }}">
</div>
</div>
</div>
</div>
</form>
<script>
function confirmPopup(name) {
return confirm(name + " Bid - Are you sure?");
}
</script>
</div>
{% include 'footer.html' %}
</body>
</html>

View file

@ -128,6 +128,40 @@
</div>
</div>
<div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="xhv-container">
<div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white price-container">
<div class="flex items-center">
<img src="/static/images/coins/Haven.png" class="rounded-xl" style="width: 35px; height: 35px; object-fit: contain;" alt="Haven">
<p class="ml-2 text-black text-sm dark:text-white">
Haven (XHV)
</p>
</div>
<div class="flex flex-col justify-start">
<p class="my-2 text-xl font-bold text-left text-gray-700 dark:text-gray-100" id="xhv-price-usd">
<span class="text-sm">
<span id="xhv-price-usd-value"></span>
</p>
<div class="flex items-center text-sm">
<div class="w-auto">
<div id="xhv-price-change-container" class="w-auto p-1"></div>
</div>
</div>
<div class="flex items-center text-xs text-gray-600 dark:text-gray-300 mt-2">
<span class="bold mr-2">VOL:</span>
<div id="xhv-volume-24h">
</div>
</div>
<div class="flex items-center text-xs text-gray-600 dark:text-gray-300 mt-2">
<span class="bold mr-2">BTC:</span>
<span id="xhv-price-btc">
</span>
</div>
</div>
</div>
</div>
<div class="w-full sm:w-1/2 lg:w-1/6 p-3" id="part-container">
<div class="px-5 py-3 h-full bg-coolGray-100 dark:bg-gray-500 rounded-2xl dark:text-white">
<div class="flex items-center">
@ -366,7 +400,7 @@
<script>
window.addEventListener('load', function() {
const api_key = '{{chart_api_key}}';
const coins = ['BTC', 'PART', 'XMR', 'LTC', 'FIRO', 'DASH', 'PIVX', 'DOGE', 'ETH'];
const coins = ['BTC', 'PART', 'XMR', 'XHV', 'LTC', 'FIRO', 'DASH', 'PIVX', 'DOGE', 'ETH'];
coins.forEach(coin => {
fetch(`https://min-api.cryptocompare.com/data/pricemultifull?fsyms=${coin}&tsyms=USD,BTC&api_key=${api_key}`)
.then(response => {
@ -451,7 +485,7 @@ function negativePriceChangeHTML(value) {
}
function setActiveContainer(containerId) {
const containerIds = ['btc-container', 'xmr-container', 'part-container', 'pivx-container', 'firo-container', 'dash-container', 'ltc-container', 'doge-container', 'eth-container'];
const containerIds = ['btc-container', 'xmr-container', 'xhv-container', 'part-container', 'pivx-container', 'firo-container', 'dash-container', 'ltc-container', 'doge-container', 'eth-container'];
const activeClass = 'active-container';
containerIds.forEach(id => {
const container = document.getElementById(id);
@ -472,6 +506,10 @@ document.getElementById('xmr-container').addEventListener('click', () => {
setActiveContainer('xmr-container');
updateChart('XMR');
});
document.getElementById('xhv-container').addEventListener('click', () => {
setActiveContainer('xhv-container');
updateChart('XHV');
});
document.getElementById('part-container').addEventListener('click', () => {
setActiveContainer('part-container');
updateChart('PART');
@ -1075,6 +1113,7 @@ const coinNameToSymbol = {
'Particl Blind': 'PART',
'Particl Anon': 'PART',
'Monero': 'XMR',
'Haven': 'XHV',
'Litecoin': 'LTC',
'Firo': 'FIRO',
'Dash': 'DASH',

View file

@ -541,6 +541,7 @@ const coinNameToSymbol = {
'Particl Blind': 'PART',
'Particl Anon': 'PART',
'Monero': 'XMR',
'Haven': 'XHV',
'Litecoin': 'LTC',
'Firo': 'FIRO',
'Dash': 'DASH',

View file

@ -200,6 +200,7 @@ const coinNameToSymbol = {
'Particl Blind': 'PART',
'Particl Anon': 'PART',
'Monero': 'XMR',
'Haven': 'XHV',
'Litecoin': 'LTC',
'Firo': 'FIRO',
'Dash': 'DASH',

View file

@ -55,7 +55,7 @@ def page_settings(self, url_split, post_string):
for name, c in swap_client.settings['chainclients'].items():
if have_data_entry(form_data, 'apply_' + name):
data = {'lookups': get_data_entry(form_data, 'lookups_' + name)}
if name == 'monero':
if name in ['haven', 'monero']:
data['fee_priority'] = int(get_data_entry(form_data, 'fee_priority_' + name))
data['manage_daemon'] = True if get_data_entry(form_data, 'managedaemon_' + name) == 'true' else False
data['rpchost'] = get_data_entry(form_data, 'rpchost_' + name)
@ -102,7 +102,7 @@ def page_settings(self, url_split, post_string):
'manage_daemon': c.get('manage_daemon', 'Unknown'),
'connection_type': c.get('connection_type', 'Unknown'),
})
if name == 'monero':
if name in ['haven', 'monero']:
chains_formatted[-1]['fee_priority'] = c.get('fee_priority', 0)
chains_formatted[-1]['manage_wallet_daemon'] = c.get('manage_wallet_daemon', 'Unknown')
chains_formatted[-1]['rpchost'] = c.get('rpchost', 'localhost')

17
basicswap/util_xhv.py Normal file
View file

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
import basicswap.contrib.Keccak as Keccak
from .contrib.HavenPy.base58 import encode as xhv_b58encode
def cn_fast_hash(s):
k = Keccak.Keccak()
return k.Keccak((len(s) * 8, s.hex()), 1088, 512, 32 * 8, False).lower() # r = bitrate = 1088, c = capacity, n = output length in bits
def encode_address(view_point, spend_point, version=18):
buf = bytes((version,)) + spend_point + view_point
h = cn_fast_hash(buf)
buf = buf + bytes.fromhex(h[0: 8])
return xhv_b58encode(buf.hex())

View file

@ -0,0 +1,16 @@
haven_wallet:
image: i_haven_wallet
build:
context: haven_wallet
dockerfile: Dockerfile
container_name: haven_wallet
volumes:
- ${DATA_PATH}/haven_wallet:/data
expose:
- ${BASE_XHV_WALLET_PORT}
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
restart: unless-stopped

View file

@ -0,0 +1,16 @@
haven_daemon:
image: i_haven_daemon
build:
context: haven_daemon
dockerfile: Dockerfile
container_name: haven_daemon
volumes:
- ${DATA_PATH}/haven_daemon:/data
expose:
- ${BASE_XHV_RPC_PORT}
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
restart: unless-stopped

View file

@ -8,6 +8,8 @@
- ${DATA_PATH}/swapclient:/data/swapclient
- ${DATA_PATH}/monero_daemon:/data/monero_daemon
- ${DATA_PATH}/monero_wallet:/data/monero_wallet
- ${DATA_PATH}/haven_daemon:/data/haven_daemon
- ${DATA_PATH}/haven_wallet:/data/haven_wallet
- ${DATA_PATH}/particl:/data/particl
- ${DATA_PATH}/bitcoin:/data/bitcoin
- ${DATA_PATH}/litecoin:/data/litecoin
@ -45,6 +47,16 @@
- XMR_WALLET_RPC_USER
- XMR_WALLET_RPC_PWD
- DEFAULT_XMR_RESTORE_HEIGHT
- XHV_DATA_DIR
- XHV_RPC_HOST
- BASE_XHV_RPC_PORT
- BASE_XHV_ZMQ_PORT
- XHV_WALLETS_DIR
- XHV_WALLET_RPC_HOST
- BASE_XHV_WALLET_PORT
- XHV_WALLET_RPC_USER
- XHV_WALLET_RPC_PWD
- DEFAULT_XHV_RESTORE_HEIGHT
- PIVX_DATA_DIR
- PIVX_RPC_HOST
- PIVX_RPC_PORT

View file

@ -30,12 +30,22 @@ XMR_DATA_DIR=/data/monero_daemon
XMR_RPC_HOST=monero_daemon
BASE_XMR_RPC_PORT=29798
XMR_WALLETS_DIR=/data/monero_wallet
XMR_WALLET_RPC_HOST=monero_wallet
BASE_XMR_WALLET_PORT=29998
XMR_WALLETS_DIR=/data/haven_wallet
XMR_WALLET_RPC_HOST=haven_wallet
BASE_XMR_WALLET_PORT=29958
XMR_WALLET_RPC_USER=xmr_wallet_user
XMR_WALLET_RPC_PWD=xmr_wallet_pwd
XHV_DATA_DIR=/data/haven_daemon
XHV_RPC_HOST=haven_daemon
BASE_XHV_RPC_PORT=29758
XHV_WALLETS_DIR=/data/haven_wallet
XHV_WALLET_RPC_HOST=haven_wallet
BASE_XHV_WALLET_PORT=29958
XHV_WALLET_RPC_USER=xhv_wallet_user
XHV_WALLET_RPC_PWD=xhv_wallet_pwd
PIVX_DATA_DIR=/data/pivx
PIVX_RPC_HOST=pivx_core
PIVX_RPC_PORT=51473

View file

@ -0,0 +1,24 @@
FROM i_swapclient as install_stage
RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=haven --withoutcoins=particl
FROM debian:bullseye-slim
COPY --from=install_stage /coin_bin .
ENV HAVEN_DATA /data
RUN groupadd -r haven && useradd -r -m -g haven haven \
&& apt-get update \
&& apt-get install -qq --no-install-recommends gosu \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p "$HAVEN_DATA" \
&& chown -R haven:haven "$HAVEN_DATA" \
&& ln -sfn "$HAVEN_DATA" /home/haven/.haven \
&& chown -h haven:haven /home/haven/.haven
VOLUME $HAVEN_DATA
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/haven/havend", "--non-interactive", "--config-file=/home/haven/.haven/havend.conf", "--confirm-external-bind"]

View file

@ -0,0 +1,11 @@
#!/bin/bash
set -e
if [[ "$1" == "havend" ]]; then
mkdir -p "$HAVEN_DATA"
chown -h haven:haven /home/haven/.haven
exec gosu haven "$@"
else
exec "$@"
fi

View file

@ -0,0 +1,16 @@
FROM i_haven_daemon
ENV HAVEN_DATA /data
RUN groupadd -r haven_wallet && useradd -r -m -g haven_wallet haven_wallet \
&& apt-get update \
&& apt-get install -qq --no-install-recommends gosu \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p "$HAVEN_DATA" \
&& chown -R haven_wallet:haven_wallet "$HAVEN_DATA"
VOLUME $HAVEN_DATA
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/haven/haven-wallet-rpc", "--non-interactive", "--config-file=/data/haven_wallet.conf", "--confirm-external-bind"]

View file

@ -0,0 +1,11 @@
#!/bin/bash
set -e
if [[ "$1" == "haven-wallet-rpc" ]]; then
mkdir -p "$HAVEN_DATA"
chown -h haven_wallet:haven_wallet /data
exec gosu haven_wallet "$@"
else
exec "$@"
fi

View file

@ -66,6 +66,15 @@ def main():
for line in fp_in:
fp.write(line)
continue
if coin_name == 'haven':
with open(os.path.join(fragments_dir, '1_haven-wallet.yml'), 'rb') as fp_in:
for line in fp_in:
fp.write(line)
fpp.write(line)
with open(os.path.join(fragments_dir, '8_haven-daemon.yml'), 'rb') as fp_in:
for line in fp_in:
fp.write(line)
continue
with open(os.path.join(fragments_dir, f'1_{coin_name}.yml'), 'rb') as fp_in:
for line in fp_in:
fp.write(line)

View file

@ -0,0 +1,419 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 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 sys
import json
import shutil
import signal
import logging
import unittest
import threading
import multiprocessing
from io import StringIO
from urllib.request import urlopen
from unittest.mock import patch
from basicswap.rpc_xhv import (
callrpc_xhv,
)
from tests.basicswap.mnemonics import mnemonics
from tests.basicswap.util import (
waitForServer,
)
from tests.basicswap.common import (
BASE_PORT, BASE_RPC_PORT,
BTC_BASE_PORT, BTC_BASE_RPC_PORT, BTC_BASE_TOR_PORT,
LTC_BASE_PORT, LTC_BASE_RPC_PORT,
PIVX_BASE_PORT,
)
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
import basicswap.config as cfg
import bin.basicswap_run as runSystem
TEST_PATH = os.path.expanduser(os.getenv('TEST_PATH', '~/test_basicswap1'))
PARTICL_PORT_BASE = int(os.getenv('PARTICL_PORT_BASE', BASE_PORT))
PARTICL_RPC_PORT_BASE = int(os.getenv('PARTICL_RPC_PORT_BASE', BASE_RPC_PORT))
BITCOIN_PORT_BASE = int(os.getenv('BITCOIN_PORT_BASE', BTC_BASE_PORT))
BITCOIN_RPC_PORT_BASE = int(os.getenv('BITCOIN_RPC_PORT_BASE', BTC_BASE_RPC_PORT))
BITCOIN_TOR_PORT_BASE = int(os.getenv('BITCOIN_TOR_PORT_BASE', BTC_BASE_TOR_PORT))
LITECOIN_RPC_PORT_BASE = int(os.getenv('LITECOIN_RPC_PORT_BASE', LTC_BASE_RPC_PORT))
FIRO_BASE_PORT = 34832
FIRO_BASE_RPC_PORT = 35832
FIRO_RPC_PORT_BASE = int(os.getenv('FIRO_RPC_PORT_BASE', FIRO_BASE_RPC_PORT))
XHV_BASE_P2P_PORT = 17752
XHV_BASE_RPC_PORT = 29758
XHV_BASE_WALLET_RPC_PORT = 29958
EXTRA_CONFIG_JSON = json.loads(os.getenv('EXTRA_CONFIG_JSON', '{}'))
def waitForBidState(delay_event, port, bid_id, state_str, wait_for=60):
for i in range(wait_for):
if delay_event.is_set():
raise ValueError('Test stopped.')
bid = json.loads(urlopen('http://127.0.0.1:12700/json/bids/{}'.format(bid_id)).read())
if bid['bid_state'] == state_str:
return
delay_event.wait(1)
raise ValueError('waitForBidState failed')
def updateThread(xhv_addr, delay_event, xhv_auth):
while not delay_event.is_set():
try:
callrpc_xhv(XHV_BASE_RPC_PORT + 1, 'generateblocks', {'wallet_address': xhv_addr, 'amount_of_blocks': 1}, auth=xhv_auth)
except Exception as e:
print('updateThread error', str(e))
delay_event.wait(2)
def recursive_update_dict(base, new_vals):
for key, value in new_vals.items():
if key in base and isinstance(value, dict):
recursive_update_dict(base[key], value)
else:
base[key] = value
def run_prepare(node_id, datadir_path, bins_path, with_coins, mnemonic_in=None, num_nodes=3, use_rpcauth=False, extra_settings={}, port_ofs=0):
config_path = os.path.join(datadir_path, cfg.CONFIG_FILENAME)
os.environ['PART_RPC_PORT'] = str(PARTICL_RPC_PORT_BASE)
os.environ['BTC_RPC_PORT'] = str(BITCOIN_RPC_PORT_BASE)
os.environ['LTC_RPC_PORT'] = str(LITECOIN_RPC_PORT_BASE)
os.environ['FIRO_RPC_PORT'] = str(FIRO_RPC_PORT_BASE)
os.environ['XHV_RPC_USER'] = 'xhv_user'
os.environ['XHV_RPC_PWD'] = 'xhv_pwd'
import bin.basicswap_prepare as prepareSystem
# Hack: Reload module to set env vars as the basicswap_prepare module is initialised if imported from elsewhere earlier
from importlib import reload
prepareSystem = reload(prepareSystem)
testargs = [
'basicswap-prepare',
f'-datadir="{datadir_path}"',
f'-bindir="{bins_path}"',
f'-portoffset={(node_id + port_ofs)}',
'-regtest',
f'-withcoins={with_coins}',
'-noextractover',
'-xhvrestoreheight=0']
if mnemonic_in:
testargs.append(f'-particl_mnemonic="{mnemonic_in}"')
keysdirpath = os.getenv('PGP_KEYS_DIR_PATH', None)
if keysdirpath is not None:
testargs.append('-keysdirpath="' + os.path.expanduser(keysdirpath) + '"')
with patch.object(sys, 'argv', testargs), patch('sys.stdout', new=StringIO()) as mocked_stdout:
prepareSystem.main()
lines = mocked_stdout.getvalue().split('\n')
mnemonic_out = lines[-4]
with open(config_path) as fs:
settings = json.load(fs)
with open(os.path.join(datadir_path, 'particl', 'particl.conf'), 'r') as fp:
lines = fp.readlines()
with open(os.path.join(datadir_path, 'particl', 'particl.conf'), 'w') as fp:
for line in lines:
if not line.startswith('staking'):
fp.write(line)
fp.write('port={}\n'.format(PARTICL_PORT_BASE + node_id + port_ofs))
fp.write('bind=127.0.0.1\n')
fp.write('dnsseed=0\n')
fp.write('discover=0\n')
fp.write('listenonion=0\n')
fp.write('upnp=0\n')
fp.write('minstakeinterval=5\n')
fp.write('stakethreadconddelayms=2000\n')
fp.write('smsgsregtestadjust=0\n')
if use_rpcauth:
salt = generate_salt(16)
rpc_user = 'test_part_' + str(node_id)
rpc_pass = 'test_part_pwd_' + str(node_id)
fp.write('rpcauth={}:{}${}\n'.format(rpc_user, salt, password_to_hmac(salt, rpc_pass)))
settings['chainclients']['particl']['rpcuser'] = rpc_user
settings['chainclients']['particl']['rpcpassword'] = rpc_pass
for ip in range(num_nodes):
if ip != node_id:
fp.write('connect=127.0.0.1:{}\n'.format(PARTICL_PORT_BASE + ip + port_ofs))
for opt in EXTRA_CONFIG_JSON.get('part{}'.format(node_id), []):
fp.write(opt + '\n')
coins_array = with_coins.split(',')
if 'bitcoin' in coins_array:
# Pruned nodes don't provide blocks
with open(os.path.join(datadir_path, 'bitcoin', 'bitcoin.conf'), 'r') as fp:
lines = fp.readlines()
with open(os.path.join(datadir_path, 'bitcoin', 'bitcoin.conf'), 'w') as fp:
for line in lines:
if not line.startswith('prune'):
fp.write(line)
fp.write('port={}\n'.format(BITCOIN_PORT_BASE + node_id + port_ofs))
fp.write('bind=127.0.0.1\n')
# listenonion=0 does not stop the node from trying to bind to the tor port
# https://github.com/bitcoin/bitcoin/issues/22726
fp.write('bind=127.0.0.1:{}=onion\n'.format(BITCOIN_TOR_PORT_BASE + node_id + port_ofs))
fp.write('dnsseed=0\n')
fp.write('discover=0\n')
fp.write('listenonion=0\n')
fp.write('upnp=0\n')
if use_rpcauth:
salt = generate_salt(16)
rpc_user = 'test_btc_' + str(node_id)
rpc_pass = 'test_btc_pwd_' + str(node_id)
fp.write('rpcauth={}:{}${}\n'.format(rpc_user, salt, password_to_hmac(salt, rpc_pass)))
settings['chainclients']['bitcoin']['rpcuser'] = rpc_user
settings['chainclients']['bitcoin']['rpcpassword'] = rpc_pass
for ip in range(num_nodes):
if ip != node_id:
fp.write('connect=127.0.0.1:{}\n'.format(BITCOIN_PORT_BASE + ip + port_ofs))
for opt in EXTRA_CONFIG_JSON.get('btc{}'.format(node_id), []):
fp.write(opt + '\n')
if 'litecoin' in coins_array:
# Pruned nodes don't provide blocks
with open(os.path.join(datadir_path, 'litecoin', 'litecoin.conf'), 'r') as fp:
lines = fp.readlines()
with open(os.path.join(datadir_path, 'litecoin', 'litecoin.conf'), 'w') as fp:
for line in lines:
if not line.startswith('prune'):
fp.write(line)
fp.write('port={}\n'.format(LTC_BASE_PORT + node_id + port_ofs))
fp.write('bind=127.0.0.1\n')
fp.write('dnsseed=0\n')
fp.write('discover=0\n')
fp.write('listenonion=0\n')
fp.write('upnp=0\n')
if use_rpcauth:
salt = generate_salt(16)
rpc_user = 'test_ltc_' + str(node_id)
rpc_pass = 'test_ltc_pwd_' + str(node_id)
fp.write('rpcauth={}:{}${}\n'.format(rpc_user, salt, password_to_hmac(salt, rpc_pass)))
settings['chainclients']['litecoin']['rpcuser'] = rpc_user
settings['chainclients']['litecoin']['rpcpassword'] = rpc_pass
for ip in range(num_nodes):
if ip != node_id:
fp.write('connect=127.0.0.1:{}\n'.format(LTC_BASE_PORT + ip + port_ofs))
for opt in EXTRA_CONFIG_JSON.get('ltc{}'.format(node_id), []):
fp.write(opt + '\n')
if 'pivx' in coins_array:
# Pruned nodes don't provide blocks
with open(os.path.join(datadir_path, 'pivx', 'pivx.conf'), 'r') as fp:
lines = fp.readlines()
with open(os.path.join(datadir_path, 'pivx', 'pivx.conf'), 'w') as fp:
for line in lines:
if not line.startswith('prune'):
fp.write(line)
fp.write('port={}\n'.format(PIVX_BASE_PORT + node_id + port_ofs))
fp.write('bind=127.0.0.1\n')
fp.write('dnsseed=0\n')
fp.write('discover=0\n')
fp.write('listenonion=0\n')
fp.write('upnp=0\n')
if use_rpcauth:
salt = generate_salt(16)
rpc_user = 'test_pivx_' + str(node_id)
rpc_pass = 'test_pivx_pwd_' + str(node_id)
fp.write('rpcauth={}:{}${}\n'.format(rpc_user, salt, password_to_hmac(salt, rpc_pass)))
settings['chainclients']['pivx']['rpcuser'] = rpc_user
settings['chainclients']['pivx']['rpcpassword'] = rpc_pass
for ip in range(num_nodes):
if ip != node_id:
fp.write('connect=127.0.0.1:{}\n'.format(PIVX_BASE_PORT + ip + port_ofs))
for opt in EXTRA_CONFIG_JSON.get('pivx{}'.format(node_id), []):
fp.write(opt + '\n')
if 'firo' in coins_array:
# Pruned nodes don't provide blocks
with open(os.path.join(datadir_path, 'firo', 'firo.conf'), 'r') as fp:
lines = fp.readlines()
with open(os.path.join(datadir_path, 'firo', 'firo.conf'), 'w') as fp:
for line in lines:
if not line.startswith('prune'):
fp.write(line)
fp.write('port={}\n'.format(FIRO_BASE_PORT + node_id + port_ofs))
fp.write('bind=127.0.0.1\n')
fp.write('dnsseed=0\n')
fp.write('discover=0\n')
fp.write('listenonion=0\n')
fp.write('upnp=0\n')
if use_rpcauth:
salt = generate_salt(16)
rpc_user = 'test_firo_' + str(node_id)
rpc_pass = 'test_firo_pwd_' + str(node_id)
fp.write('rpcauth={}:{}${}\n'.format(rpc_user, salt, password_to_hmac(salt, rpc_pass)))
settings['chainclients']['firo']['rpcuser'] = rpc_user
settings['chainclients']['firo']['rpcpassword'] = rpc_pass
for ip in range(num_nodes):
if ip != node_id:
fp.write('connect=127.0.0.1:{}\n'.format(FIRO_BASE_PORT + ip + port_ofs))
for opt in EXTRA_CONFIG_JSON.get('firo{}'.format(node_id), []):
fp.write(opt + '\n')
if 'haven' in coins_array:
with open(os.path.join(datadir_path, 'haven', 'havend.conf'), 'a') as fp:
fp.write('p2p-bind-ip=127.0.0.1\n')
fp.write('p2p-bind-port={}\n'.format(XHV_BASE_P2P_PORT + node_id + port_ofs))
for ip in range(num_nodes):
if ip != node_id:
fp.write('add-exclusive-node=127.0.0.1:{}\n'.format(XHV_BASE_P2P_PORT + ip + port_ofs))
with open(config_path) as fs:
settings = json.load(fs)
settings['min_delay_event'] = 1
settings['max_delay_event'] = 4
settings['min_delay_event_short'] = 1
settings['max_delay_event_short'] = 4
settings['min_delay_retry'] = 10
settings['max_delay_retry'] = 20
settings['check_progress_seconds'] = 5
settings['check_watched_seconds'] = 5
settings['check_expired_seconds'] = 60
settings['check_events_seconds'] = 5
settings['check_xhv_swaps_seconds'] = 5
recursive_update_dict(settings, extra_settings)
extra_config = EXTRA_CONFIG_JSON.get('sc{}'.format(node_id), {})
recursive_update_dict(settings, extra_config)
with open(config_path, 'w') as fp:
json.dump(settings, fp, indent=4)
return mnemonic_out
def prepare_nodes(num_nodes, extra_coins, use_rpcauth=False, extra_settings={}, port_ofs=0):
bins_path = os.path.join(TEST_PATH, 'bin')
for i in range(num_nodes):
logging.info('Preparing node: %d.', i)
client_path = os.path.join(TEST_PATH, 'client{}'.format(i))
try:
shutil.rmtree(client_path)
except Exception as ex:
logging.warning('setUpClass %s', str(ex))
run_prepare(i, client_path, bins_path, extra_coins, mnemonics[i] if i < len(mnemonics) else None,
num_nodes=num_nodes, use_rpcauth=use_rpcauth, extra_settings=extra_settings, port_ofs=port_ofs)
class TestBase(unittest.TestCase):
def setUpClass(cls):
super(TestBase, cls).setUpClass()
cls.delay_event = threading.Event()
signal.signal(signal.SIGINT, lambda signal, frame: cls.signal_handler(cls, signal, frame))
def signal_handler(self, sig, frame):
logging.info('signal {} detected.'.format(sig))
self.delay_event.set()
def wait_seconds(self, seconds):
self.delay_event.wait(seconds)
if self.delay_event.is_set():
raise ValueError('Test stopped.')
def wait_for_particl_height(self, http_port, num_blocks=3):
# Wait for height, or sequencelock is thrown off by genesis blocktime
logging.info('Waiting for Particl chain height %d', num_blocks)
for i in range(60):
if self.delay_event.is_set():
raise ValueError('Test stopped.')
try:
wallets = json.loads(urlopen(f'http://127.0.0.1:{http_port}/json/wallets').read())
particl_blocks = wallets['PART']['blocks']
print('particl_blocks', particl_blocks)
if particl_blocks >= num_blocks:
return
except Exception as e:
print('Error reading wallets', str(e))
self.delay_event.wait(1)
raise ValueError(f'wait_for_particl_height failed http_port: {http_port}')
class XhvTestBase(TestBase):
@classmethod
def setUpClass(cls):
super(XhvTestBase, cls).setUpClass(cls)
cls.update_thread = None
cls.processes = []
prepare_nodes(3, 'haven')
def run_thread(self, client_id):
client_path = os.path.join(TEST_PATH, 'client{}'.format(client_id))
testargs = ['basicswap-run', '-datadir=' + client_path, '-regtest']
with patch.object(sys, 'argv', testargs):
runSystem.main()
def start_processes(self):
self.delay_event.clear()
for i in range(3):
self.processes.append(multiprocessing.Process(target=self.run_thread, args=(i,)))
self.processes[-1].start()
waitForServer(self.delay_event, 12701)
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['XHV']['main_address']
except Exception as e:
print('Waiting for main address {}'.format(str(e)))
self.delay_event.wait(1)
raise ValueError('waitForMainAddress timedout')
xhv_addr1 = waitForMainAddress()
num_blocks = 100
xhv_auth = None
if os.getenv('XHV_RPC_USER', '') != '':
xhv_auth = (os.getenv('XHV_RPC_USER', ''), os.getenv('XHV_RPC_PWD', ''))
if callrpc_xhv(XHV_BASE_RPC_PORT + 1, 'get_block_count', auth=xhv_auth)['count'] < num_blocks:
logging.info('Mining {} Haven blocks to {}.'.format(num_blocks, xhv_addr1))
callrpc_xhv(XHV_BASE_RPC_PORT + 1, 'generateblocks', {'wallet_address': xhv_addr1, 'amount_of_blocks': num_blocks}, auth=xhv_auth)
logging.info('XHV blocks: %d', callrpc_xhv(XHV_BASE_RPC_PORT + 1, 'get_block_count', auth=xhv_auth)['count'])
self.update_thread = threading.Thread(target=updateThread, args=(xhv_addr1, self.delay_event, xhv_auth))
self.update_thread.start()
self.wait_for_particl_height(12701, num_blocks=3)
@classmethod
def tearDownClass(cls):
logging.info('Stopping test')
cls.delay_event.set()
if cls.update_thread:
cls.update_thread.join()
for p in cls.processes:
p.terminate()
for p in cls.processes:
p.join()
cls.update_thread = None
cls.processes = []

View file

@ -0,0 +1,252 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2021-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
"""
export RESET_TEST=true
export TEST_PATH=/tmp/test_persistent
mkdir -p ${TEST_PATH}/bin
cp -r ~/tmp/basicswap_bin/* ${TEST_PATH}/bin
export PYTHONPATH=$(pwd)
export XHV_RPC_USER=xhv_user
export XHV_RPC_PWD=xhv_pwd
python tests/basicswap/extended/test_xhv_persistent.py
"""
import os
import sys
import json
import time
import random
import signal
import logging
import unittest
import threading
import multiprocessing
from unittest.mock import patch
from basicswap.rpc_xhv import (
callrpc_xhv,
)
from basicswap.rpc import (
callrpc,
)
from tests.basicswap.common import (
BASE_RPC_PORT,
BTC_BASE_RPC_PORT,
LTC_BASE_RPC_PORT,
)
from tests.basicswap.util import (
make_boolean,
read_json_api,
waitForServer,
)
from tests.basicswap.common_xhv import (
prepare_nodes,
XHV_BASE_RPC_PORT,
)
import bin.basicswap_run as runSystem
test_path = os.path.expanduser(os.getenv('TEST_PATH', '/tmp/test_persistent'))
RESET_TEST = make_boolean(os.getenv('RESET_TEST', 'false'))
PORT_OFS = int(os.getenv('PORT_OFS', 1))
UI_PORT = 12700 + PORT_OFS
PARTICL_RPC_PORT_BASE = int(os.getenv('PARTICL_RPC_PORT_BASE', BASE_RPC_PORT))
BITCOIN_RPC_PORT_BASE = int(os.getenv('BITCOIN_RPC_PORT_BASE', BTC_BASE_RPC_PORT))
LITECOIN_RPC_PORT_BASE = int(os.getenv('LITECOIN_RPC_PORT_BASE', LTC_BASE_RPC_PORT))
XHV_BASE_RPC_PORT = int(os.getenv('XHV_BASE_RPC_PORT', XHV_BASE_RPC_PORT))
TEST_COINS_LIST = os.getenv('TEST_COINS_LIST', 'bitcoin,monero')
NUM_NODES = int(os.getenv('NUM_NODES', 3))
EXTRA_CONFIG_JSON = json.loads(os.getenv('EXTRA_CONFIG_JSON', '{}'))
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
def callpartrpc(node_id, method, params=[], wallet=None, base_rpc_port=PARTICL_RPC_PORT_BASE + PORT_OFS):
auth = 'test_part_{0}:test_part_pwd_{0}'.format(node_id)
return callrpc(base_rpc_port + node_id, auth, method, params, wallet)
def callbtcrpc(node_id, method, params=[], wallet=None, base_rpc_port=BITCOIN_RPC_PORT_BASE + PORT_OFS):
auth = 'test_btc_{0}:test_btc_pwd_{0}'.format(node_id)
return callrpc(base_rpc_port + node_id, auth, method, params, wallet)
def callltcrpc(node_id, method, params=[], wallet=None, base_rpc_port=LITECOIN_RPC_PORT_BASE + PORT_OFS):
auth = 'test_ltc_{0}:test_ltc_pwd_{0}'.format(node_id)
return callrpc(base_rpc_port + node_id, auth, method, params, wallet)
def updateThread(cls):
while not cls.delay_event.is_set():
try:
if cls.btc_addr is not None:
callbtcrpc(0, 'generatetoaddress', [1, cls.btc_addr])
except Exception as e:
print('updateThread error', str(e))
cls.delay_event.wait(random.randrange(cls.update_min, cls.update_max))
def updateThreadXhv(cls):
xhv_auth = None
if os.getenv('XHV_RPC_USER', '') != '':
xhv_auth = (os.getenv('XHV_RPC_USER', ''), os.getenv('XHV_RPC_PWD', ''))
while not cls.delay_event.is_set():
try:
if cls.xhv_addr is not None:
callrpc_xhv(XHV_BASE_RPC_PORT + 1, 'generateblocks', {'wallet_address': cls.xhv_addr, 'amount_of_blocks': 1}, auth=xhv_auth)
except Exception as e:
print('updateThreadXhv error', str(e))
cls.delay_event.wait(random.randrange(cls.xhv_update_min, cls.xhv_update_max))
class Test(unittest.TestCase):
@classmethod
def setUpClass(cls):
super(Test, cls).setUpClass()
cls.update_min = int(os.getenv('UPDATE_THREAD_MIN_WAIT', '1'))
cls.update_max = cls.update_min * 4
cls.xhv_update_min = int(os.getenv('XHV_UPDATE_THREAD_MIN_WAIT', '1'))
cls.xhv_update_max = cls.xhv_update_min * 4
cls.delay_event = threading.Event()
cls.update_thread = None
cls.update_thread_xhv = None
cls.processes = []
cls.btc_addr = None
cls.xhv_addr = None
random.seed(time.time())
if os.path.exists(test_path) and not RESET_TEST:
logging.info(f'Continuing with existing directory: {test_path}')
else:
logging.info('Preparing %d nodes.', NUM_NODES)
prepare_nodes(NUM_NODES, TEST_COINS_LIST, True, {'min_sequence_lock_seconds': 60}, PORT_OFS)
signal.signal(signal.SIGINT, lambda signal, frame: cls.signal_handler(cls, signal, frame))
def signal_handler(self, sig, frame):
logging.info('signal {} detected.'.format(sig))
self.delay_event.set()
def run_thread(self, client_id):
client_path = os.path.join(test_path, 'client{}'.format(client_id))
testargs = ['basicswap-run', '-datadir=' + client_path, '-regtest']
with patch.object(sys, 'argv', testargs):
runSystem.main()
def start_processes(self):
self.delay_event.clear()
for i in range(NUM_NODES):
self.processes.append(multiprocessing.Process(target=self.run_thread, args=(i,)))
self.processes[-1].start()
for i in range(NUM_NODES):
waitForServer(self.delay_event, UI_PORT + i)
wallets = read_json_api(UI_PORT + 1, 'wallets')
xhv_auth = None
if os.getenv('XHV_RPC_USER', '') != '':
xhv_auth = (os.getenv('XHV_RPC_USER', ''), os.getenv('XHV_RPC_PWD', ''))
self.xhv_addr = wallets['XHV']['main_address']
num_blocks = 100
if callrpc_xhv(XHV_BASE_RPC_PORT + 1, 'get_block_count', auth=xhv_auth)['count'] < num_blocks:
logging.info('Mining {} Monero blocks to {}.'.format(num_blocks, self.xhv_addr))
callrpc_xhv(XHV_BASE_RPC_PORT + 1, 'generateblocks', {'wallet_address': self.xhv_addr, 'amount_of_blocks': num_blocks}, auth=xhv_auth)
logging.info('XHV blocks: %d', callrpc_xhv(XHV_BASE_RPC_PORT + 1, 'get_block_count', auth=xhv_auth)['count'])
self.btc_addr = callbtcrpc(0, 'getnewaddress', ['mining_addr', 'bech32'])
num_blocks: int = 500 # Mine enough to activate segwit
if callbtcrpc(0, 'getblockcount') < num_blocks:
logging.info('Mining %d Bitcoin blocks to %s', num_blocks, self.btc_addr)
callbtcrpc(0, 'generatetoaddress', [num_blocks, self.btc_addr])
logging.info('BTC blocks: %d', callbtcrpc(0, 'getblockcount'))
if 'litecoin' in TEST_COINS_LIST:
self.ltc_addr = callltcrpc(0, 'getnewaddress', ['mining_addr'], wallet='wallet.dat')
num_blocks: int = 431
have_blocks: int = callltcrpc(0, 'getblockcount')
if have_blocks < 500:
logging.info('Mining %d Litecoin blocks to %s', num_blocks, self.ltc_addr)
callltcrpc(0, 'generatetoaddress', [num_blocks - have_blocks, self.ltc_addr], wallet='wallet.dat')
# https://github.com/litecoin-project/litecoin/issues/807
# Block 432 is when MWEB activates. It requires a peg-in. You'll need to generate an mweb address and send some coins to it. Then it will allow you to mine the next block.
mweb_addr = callltcrpc(0, 'getnewaddress', ['mweb_addr', 'mweb'], wallet='mweb')
callltcrpc(0, 'sendtoaddress', [mweb_addr, 1.0], wallet='wallet.dat')
num_blocks = 69
have_blocks: int = callltcrpc(0, 'getblockcount')
callltcrpc(0, 'generatetoaddress', [500 - have_blocks, self.ltc_addr], wallet='wallet.dat')
# Lower output split threshold for more stakeable outputs
for i in range(NUM_NODES):
callpartrpc(i, 'walletsettings', ['stakingoptions', {'stakecombinethreshold': 100, 'stakesplitthreshold': 200}])
self.update_thread = threading.Thread(target=updateThread, args=(self,))
self.update_thread.start()
self.update_thread_xhv = threading.Thread(target=updateThreadXhv, args=(self,))
self.update_thread_xhv.start()
# Wait for height, or sequencelock is thrown off by genesis blocktime
num_blocks = 3
logging.info('Waiting for Particl chain height %d', num_blocks)
for i in range(60):
if self.delay_event.is_set():
raise ValueError('Test stopped.')
particl_blocks = callpartrpc(0, 'getblockcount')
print('particl_blocks', particl_blocks)
if particl_blocks >= num_blocks:
break
self.delay_event.wait(1)
logging.info('PART blocks: %d', callpartrpc(0, 'getblockcount'))
assert particl_blocks >= num_blocks
@classmethod
def tearDownClass(cls):
logging.info('Stopping test')
cls.delay_event.set()
if cls.update_thread:
cls.update_thread.join()
if cls.update_thread_xhv:
cls.update_thread_xhv.join()
for p in cls.processes:
p.terminate()
for p in cls.processes:
p.join()
cls.update_thread = None
cls.update_thread_xhv = None
cls.processes = []
def test_persistent(self):
self.start_processes()
waitForServer(self.delay_event, UI_PORT + 0)
waitForServer(self.delay_event, UI_PORT + 1)
while not self.delay_event.is_set():
logging.info('Looping indefinitely, ctrl+c to exit.')
self.delay_event.wait(10)
if __name__ == '__main__':
unittest.main()

View file

@ -1,2 +1,3 @@
python tests/basicswap/extended/test_xmr_persistent.py
python tests/basicswap/extended/test_xhv_persistent.py
python tests/basicswap/selenium/test_wallets.py

View file

@ -88,6 +88,7 @@ def test_offer(driver):
print('Test Passed!')
# didn't do test_offer_for_xhv
def run_tests():
driver = get_driver()

View file

@ -136,6 +136,40 @@ def test_settings(driver):
settings = json.load(fs)
assert (len(settings['chainclients']['monero']['remote_daemon_urls']) == 0)
# Apply XHV settings with blank nodes list
driver.find_element(By.ID, 'coins-tab').click()
btn_apply_haven = wait.until(EC.element_to_be_clickable((By.NAME, 'apply_haven')))
el = driver.find_element(By.NAME, 'remotedaemonurls_haven')
el.clear()
btn_apply_haven.click()
time.sleep(1)
with open(settings_path_0) as fs:
settings = json.load(fs)
assert (len(settings['chainclients']['haven']['remote_daemon_urls']) == 0)
btn_apply_haven = wait.until(EC.element_to_be_clickable((By.NAME, 'apply_haven')))
el = driver.find_element(By.NAME, 'remotedaemonurls_haven')
el.clear()
el.send_keys('remote.haven.miner.rocks:17750\nsg.haven.miner.rocks:30031')
btn_apply_haven.click()
time.sleep(1)
with open(settings_path_0) as fs:
settings = json.load(fs)
remotedaemonurls = settings['chainclients']['haven']['remote_daemon_urls']
assert (len(remotedaemonurls) == 2)
btn_apply_haven = wait.until(EC.element_to_be_clickable((By.NAME, 'apply_haven')))
el = driver.find_element(By.NAME, 'remotedaemonurls_haven')
el.clear()
btn_apply_haven.click()
time.sleep(1)
with open(settings_path_0) as fs:
settings = json.load(fs)
assert (len(settings['chainclients']['haven']['remote_daemon_urls']) == 0)
print('Test Passed!')

View file

@ -59,6 +59,7 @@ def test_swap_dir(driver):
print('Test Passed!')
# didn't do test_swap_dir_for_xhv
def run_tests():
driver = get_driver()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,246 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2021-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import random
import logging
import unittest
from basicswap.basicswap import (
Coins,
SwapTypes,
BidStates,
)
from basicswap.util import (
COIN,
)
from tests.basicswap.util import (
read_json_api,
)
from tests.basicswap.common import (
wait_for_bid,
wait_for_offer,
wait_for_in_progress,
TEST_HTTP_PORT,
LTC_BASE_RPC_PORT,
)
from .test_btc_xhv import BasicSwapTest, test_delay_event
from .test_xhv import pause_event
logger = logging.getLogger()
class TestLTC(BasicSwapTest):
__test__ = True
test_coin_from = Coins.LTC
start_ltc_nodes = True
base_rpc_port = LTC_BASE_RPC_PORT
def mineBlock(self, num_blocks=1):
self.callnoderpc('generatetoaddress', [num_blocks, self.ltc_addr])
def check_softfork_active(self, feature_name):
deploymentinfo = self.callnoderpc('getblockchaininfo')
assert (deploymentinfo['softforks'][feature_name]['active'] is True)
def test_001_nested_segwit(self):
logging.info('---------- Test {} p2sh nested segwit'.format(self.test_coin_from.name))
logging.info('Skipped')
def test_002_native_segwit(self):
logging.info('---------- Test {} p2sh native segwit'.format(self.test_coin_from.name))
ci = self.swap_clients[0].ci(self.test_coin_from)
addr_segwit = ci.rpc_wallet('getnewaddress', ['segwit test', 'bech32'])
addr_info = ci.rpc_wallet('getaddressinfo', [addr_segwit, ])
assert addr_info['iswitness'] is True
txid = ci.rpc_wallet('sendtoaddress', [addr_segwit, 1.0])
assert len(txid) == 64
tx_wallet = ci.rpc_wallet('gettransaction', [txid, ])['hex']
tx = ci.rpc('decoderawtransaction', [tx_wallet, ])
self.mineBlock()
ro = ci.rpc('scantxoutset', ['start', ['addr({})'.format(addr_segwit)]])
assert (len(ro['unspents']) == 1)
assert (ro['unspents'][0]['txid'] == txid)
prevout_n = -1
for txo in tx['vout']:
if addr_segwit in txo['scriptPubKey']['addresses']:
prevout_n = txo['n']
break
assert prevout_n > -1
tx_funded = ci.rpc('createrawtransaction', [[{'txid': txid, 'vout': prevout_n}], {addr_segwit: 0.99}])
tx_signed = ci.rpc_wallet('signrawtransactionwithwallet', [tx_funded, ])['hex']
tx_funded_decoded = ci.rpc('decoderawtransaction', [tx_funded, ])
tx_signed_decoded = ci.rpc('decoderawtransaction', [tx_signed, ])
assert tx_funded_decoded['txid'] == tx_signed_decoded['txid']
def test_007_hdwallet(self):
logging.info('---------- Test {} hdwallet'.format(self.test_coin_from.name))
test_seed = '8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b'
test_wif = self.swap_clients[0].ci(self.test_coin_from).encodeKey(bytes.fromhex(test_seed))
new_wallet_name = random.randbytes(10).hex()
self.callnoderpc('createwallet', [new_wallet_name])
self.callnoderpc('sethdseed', [True, test_wif], wallet=new_wallet_name)
addr = self.callnoderpc('getnewaddress', wallet=new_wallet_name)
self.callnoderpc('unloadwallet', [new_wallet_name])
assert (addr == 'rltc1qps7hnjd866e9ynxadgseprkc2l56m00djr82la')
def test_20_btc_coin(self):
logging.info('---------- Test BTC to {}'.format(self.test_coin_from.name))
swap_clients = self.swap_clients
offer_id = swap_clients[0].postOffer(Coins.BTC, self.test_coin_from, 100 * COIN, 0.1 * COIN, 100 * COIN, SwapTypes.SELLER_FIRST)
wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offer = swap_clients[1].getOffer(offer_id)
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
wait_for_bid(test_delay_event, swap_clients[0], bid_id)
swap_clients[0].acceptBid(bid_id)
wait_for_in_progress(test_delay_event, swap_clients[1], bid_id, sent=True)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60)
js_0 = read_json_api(1800)
js_1 = read_json_api(1801)
assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert (js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
def test_21_mweb(self):
logging.info('---------- Test MWEB {}'.format(self.test_coin_from.name))
swap_clients = self.swap_clients
ci0 = swap_clients[0].ci(self.test_coin_from)
ci1 = swap_clients[1].ci(self.test_coin_from)
mweb_addr_0 = ci0.rpc_wallet('getnewaddress', ['mweb addr test 0', 'mweb'])
mweb_addr_1 = ci1.rpc_wallet('getnewaddress', ['mweb addr test 1', 'mweb'])
addr_info0 = ci0.rpc_wallet('getaddressinfo', [mweb_addr_0,])
assert (addr_info0['ismweb'] is True)
addr_info1 = ci1.rpc_wallet('getaddressinfo', [mweb_addr_1,])
assert (addr_info1['ismweb'] is True)
trusted_before = ci0.rpc_wallet('getbalances')['mine']['trusted']
ci0.rpc_wallet('sendtoaddress', [mweb_addr_0, 10.0])
assert (trusted_before - float(ci0.rpc_wallet('getbalances')['mine']['trusted']) < 0.1)
try:
pause_event.clear() # Stop mining
ci0.rpc_wallet('sendtoaddress', [mweb_addr_1, 10.0])
found_unconfirmed: bool = False
for i in range(20):
test_delay_event.wait(1)
ltc_wallet = read_json_api(TEST_HTTP_PORT + 1, 'wallets/ltc')
if float(ltc_wallet['unconfirmed']) == 10.0:
found_unconfirmed = True
break
finally:
pause_event.set()
assert (found_unconfirmed)
self.mineBlock()
txns = ci0.rpc_wallet('listtransactions')
utxos = ci0.rpc_wallet('listunspent')
balances = ci0.rpc_wallet('getbalances')
wi = ci0.rpc_wallet('getwalletinfo')
txid = ci0.rpc_wallet('sendtoaddress', [mweb_addr_1, 10.0])
self.mineBlock()
txns = ci1.rpc_wallet('listtransactions')
utxos = ci1.rpc_wallet('listunspent')
balances = ci1.rpc_wallet('getbalances')
wi = ci1.rpc_wallet('getwalletinfo')
mweb_tx = None
for utxo in utxos:
if utxo.get('address', '') == mweb_addr_1:
mweb_tx = utxo
assert (mweb_tx is not None)
tx = ci1.rpc_wallet('gettransaction', [mweb_tx['txid'],])
blockhash = tx['blockhash']
block = ci1.rpc('getblock', [blockhash, 3])
block = ci1.rpc('getblock', [blockhash, 0])
# TODO
def test_22_mweb_balance(self):
logging.info('---------- Test MWEB balance {}'.format(self.test_coin_from.name))
swap_clients = self.swap_clients
ci_mweb = swap_clients[0].ci(Coins.LTC_MWEB)
mweb_addr_0 = ci_mweb.getNewAddress()
addr_info0 = ci_mweb.rpc_wallet('getaddressinfo', [mweb_addr_0,])
assert (addr_info0['ismweb'] is True)
ltc_addr = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc/nextdepositaddr')
ltc_mweb_addr = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc_mweb/nextdepositaddr')
ltc_mweb_addr2 = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc/newmwebaddress')
assert (ci_mweb.rpc_wallet('getaddressinfo', [ltc_addr,])['ismweb'] is False)
assert (ci_mweb.rpc_wallet('getaddressinfo', [ltc_mweb_addr,])['ismweb'] is True)
assert (ci_mweb.rpc_wallet('getaddressinfo', [ltc_mweb_addr2,])['ismweb'] is True)
post_json = {
'value': 10,
'address': ltc_mweb_addr,
'subfee': False,
}
json_rv = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc/withdraw', post_json)
assert (len(json_rv['txid']) == 64)
self.mineBlock()
json_rv = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc', post_json)
assert (json_rv['mweb_balance'] == 10.0)
mweb_address = json_rv['mweb_address']
post_json = {
'value': 11,
'address': mweb_address,
'subfee': False,
}
json_rv = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc/withdraw', post_json)
assert (len(json_rv['txid']) == 64)
self.mineBlock()
json_rv = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc_mweb', post_json)
assert (json_rv['mweb_balance'] == 21.0)
assert (json_rv['mweb_address'] == mweb_address)
ltc_address = json_rv['deposit_address']
# Check that spending the mweb balance takes from the correct wallet
post_json = {
'value': 1,
'address': ltc_address,
'subfee': False,
'type_from': 'mweb',
}
json_rv = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc/withdraw', post_json)
assert (len(json_rv['txid']) == 64)
json_rv = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc', post_json)
assert (json_rv['mweb_balance'] <= 20.0)
if __name__ == '__main__':
unittest.main()

1588
tests/basicswap/test_xhv.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,155 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 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.
"""
export TEST_PATH=/tmp/test_basicswap
mkdir -p ${TEST_PATH}/bin
cp -r ~/tmp/basicswap_bin/* ${TEST_PATH}/bin
export PYTHONPATH=$(pwd)
python tests/basicswap/test_xhv_bids_offline.py
"""
import sys
import json
import logging
import unittest
import multiprocessing
from urllib import parse
from urllib.request import urlopen
from tests.basicswap.util import (
read_json_api,
waitForServer,
)
from tests.basicswap.common import (
waitForNumOffers,
waitForNumBids,
)
from tests.basicswap.common_xhv import (
XhvTestBase,
waitForBidState,
)
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
class Test(XhvTestBase):
def test_bids_offline(self):
# Start multiple bids while offering node is offline
self.start_processes()
waitForServer(self.delay_event, 12700)
waitForServer(self.delay_event, 12701)
wallets1 = read_json_api(12701, 'wallets')
assert (float(wallets1['XHV']['balance']) > 0.0)
offer_data = {
'addr_from': -1,
'coin_from': 'PART',
'coin_to': 'XHV',
'amt_from': 1,
'amt_to': 1,
'lockhrs': 24,
'automation_strat_id': 1}
rv = json.loads(urlopen('http://127.0.0.1:12700/json/offers/new', data=parse.urlencode(offer_data).encode()).read())
offer0_id = rv['offer_id']
offer_data['amt_from'] = '2'
rv = json.loads(urlopen('http://127.0.0.1:12700/json/offers/new', data=parse.urlencode(offer_data).encode()).read())
offer1_id = rv['offer_id']
summary = read_json_api(12700)
assert (summary['num_sent_offers'] > 1)
logger.info('Waiting for offer')
waitForNumOffers(self.delay_event, 12701, 2)
logger.info('Stopping node 0')
c0 = self.processes[0]
c0.terminate()
c0.join()
offers = json.loads(urlopen('http://127.0.0.1:12701/json/offers/{}'.format(offer0_id)).read())
assert (len(offers) == 1)
offer0 = offers[0]
post_data = {
'coin_from': 'PART'
}
test_post_offers = json.loads(urlopen('http://127.0.0.1:12701/json/offers', data=parse.urlencode(post_data).encode()).read())
assert (len(test_post_offers) == 2)
post_data['coin_from'] = '2'
test_post_offers = json.loads(urlopen('http://127.0.0.1:12701/json/offers', data=parse.urlencode(post_data).encode()).read())
assert (len(test_post_offers) == 0)
bid_data = {
'offer_id': offer0_id,
'amount_from': offer0['amount_from']}
bid0_id = json.loads(urlopen('http://127.0.0.1:12701/json/bids/new', data=parse.urlencode(bid_data).encode()).read())['bid_id']
offers = json.loads(urlopen('http://127.0.0.1:12701/json/offers/{}'.format(offer1_id)).read())
assert (len(offers) == 1)
offer1 = offers[0]
bid_data = {
'offer_id': offer1_id,
'amount_from': offer1['amount_from']}
bid1_id = json.loads(urlopen('http://127.0.0.1:12701/json/bids/new', data=parse.urlencode(bid_data).encode()).read())['bid_id']
logger.info('Delaying for 5 seconds.')
self.delay_event.wait(5)
logger.info('Starting node 0')
self.processes[0] = multiprocessing.Process(target=self.run_thread, args=(0,))
self.processes[0].start()
waitForServer(self.delay_event, 12700)
waitForNumBids(self.delay_event, 12700, 2)
waitForBidState(self.delay_event, 12700, bid0_id, 'Received')
waitForBidState(self.delay_event, 12700, bid1_id, 'Received')
# Manually accept on top of auto-accept for extra chaos
data = parse.urlencode({
'accept': True
}).encode()
try:
rv = json.loads(urlopen('http://127.0.0.1:12700/json/bids/{}'.format(bid0_id), data=data).read())
assert rv['bid_state'] == 'Accepted'
except Exception as e:
print('Accept bid failed', str(e), rv)
try:
rv = json.loads(urlopen('http://127.0.0.1:12700/json/bids/{}'.format(bid1_id), data=data).read())
assert (rv['bid_state'] == 'Accepted')
except Exception as e:
print('Accept bid failed', str(e), rv)
logger.info('Completing swap')
for i in range(240):
if self.delay_event.is_set():
raise ValueError('Test stopped.')
self.delay_event.wait(4)
rv0 = read_json_api(12700, 'bids/{}'.format(bid0_id))
rv1 = read_json_api(12700, 'bids/{}'.format(bid1_id))
if rv0['bid_state'] == 'Completed' and rv1['bid_state'] == 'Completed':
break
assert rv0['bid_state'] == 'Completed'
assert rv1['bid_state'] == 'Completed'
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,147 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 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.
"""
export TEST_PATH=/tmp/test_basicswap
mkdir -p ${TEST_PATH}/bin
cp -r ~/tmp/basicswap_bin/* ${TEST_PATH}/bin
export PYTHONPATH=$(pwd)
python tests/basicswap/test_xhv_reload.py
"""
import sys
import logging
import unittest
import multiprocessing
from tests.basicswap.util import (
read_json_api,
post_json_api,
waitForServer,
)
from tests.basicswap.common import (
waitForNumOffers,
waitForNumBids,
waitForNumSwapping,
)
from tests.basicswap.common_xhv import (
XhvTestBase,
)
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
class Test(XhvTestBase):
def test_reload(self):
self.start_processes()
waitForServer(self.delay_event, 12700)
waitForServer(self.delay_event, 12701)
wallets1 = read_json_api(12701, 'wallets')
assert (float(wallets1['XHV']['balance']) > 0.0)
data = {
'addr_from': '-1',
'coin_from': 'part',
'coin_to': 'xhv',
'amt_from': '1',
'amt_to': '1',
'lockhrs': '24'}
offer_id = post_json_api(12700, 'offers/new', data)['offer_id']
summary = read_json_api(12700)
assert (summary['num_sent_offers'] == 1)
logger.info('Waiting for offer')
waitForNumOffers(self.delay_event, 12701, 1)
offers = read_json_api(12701, 'offers')
offer = offers[0]
data = {
'offer_id': offer['offer_id'],
'amount_from': offer['amount_from']}
data['valid_for_seconds'] = 24 * 60 * 60 + 1
bid = post_json_api(12701, 'bids/new', data)
assert (bid['error'] == 'Bid TTL too high')
del data['valid_for_seconds']
data['validmins'] = 24 * 60 + 1
bid = post_json_api(12701, 'bids/new', data)
assert (bid['error'] == 'Bid TTL too high')
del data['validmins']
data['valid_for_seconds'] = 10
bid = post_json_api(12701, 'bids/new', data)
assert (bid['error'] == 'Bid TTL too low')
del data['valid_for_seconds']
data['validmins'] = 1
bid = post_json_api(12701, 'bids/new', data)
assert (bid['error'] == 'Bid TTL too low')
data['validmins'] = 60
bid_id = post_json_api(12701, 'bids/new', data)
waitForNumBids(self.delay_event, 12700, 1)
for i in range(10):
bids = read_json_api(12700, 'bids')
bid = bids[0]
if bid['bid_state'] == 'Received':
break
self.delay_event.wait(1)
assert (bid['expire_at'] == bid['created_at'] + data['validmins'] * 60)
data = {
'accept': True
}
rv = post_json_api(12700, 'bids/{}'.format(bid['bid_id']), data)
assert (rv['bid_state'] == 'Accepted')
waitForNumSwapping(self.delay_event, 12701, 1)
logger.info('Restarting client')
c1 = self.processes[1]
c1.terminate()
c1.join()
self.processes[1] = multiprocessing.Process(target=self.run_thread, args=(1,))
self.processes[1].start()
waitForServer(self.delay_event, 12701)
rv = read_json_api(12701)
assert (rv['num_swapping'] == 1)
rv = read_json_api(12700, 'revokeoffer/{}'.format(offer_id))
assert (rv['revoked_offer'] == offer_id)
logger.info('Completing swap')
for i in range(240):
if self.delay_event.is_set():
raise ValueError('Test stopped.')
self.delay_event.wait(4)
rv = read_json_api(12700, 'bids/{}'.format(bid['bid_id']))
if rv['bid_state'] == 'Completed':
break
assert (rv['bid_state'] == 'Completed')
# Ensure offer was revoked
summary = read_json_api(12700)
assert (summary['num_network_offers'] == 0)
# Wait for bid to be removed from in-progress
waitForNumBids(self.delay_event, 12700, 0)
if __name__ == '__main__':
unittest.main()

View file

@ -10,6 +10,7 @@ passenv =
PARTICL_BINDIR
BITCOIN_BINDIR
LITECOIN_BINDIR
XHV_BINDIR
XMR_BINDIR
TEST_PREPARE_PATH
TEST_RELOAD_PATH