Support xmr-protocol swaps to BTC and PART

This commit is contained in:
tecnovert 2022-12-08 03:22:18 +02:00
parent 1d0a3fbc12
commit 0e1cb6d03d
No known key found for this signature in database
GPG key ID: 8ED6D8750C4E3F93
9 changed files with 349 additions and 205 deletions

View file

@ -3227,11 +3227,11 @@ class BasicSwap(BaseApp):
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_SEEN, '', session) self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_SEEN, '', session)
if bid.xmr_b_lock_tx is None: if bid.xmr_b_lock_tx is None:
self.log.debug('Found {} lock tx in chain'.format(ci_to.coin_name())) self.log.debug('Found {} lock tx in chain'.format(ci_to.coin_name()))
b_lock_tx_id = bytes.fromhex(found_tx['txid']) xmr_swap.b_lock_tx_id = bytes.fromhex(found_tx['txid'])
bid.xmr_b_lock_tx = SwapTx( bid.xmr_b_lock_tx = SwapTx(
bid_id=bid_id, bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_B_LOCK, tx_type=TxTypes.XMR_SWAP_B_LOCK,
txid=b_lock_tx_id, txid=xmr_swap.b_lock_tx_id,
chain_height=found_tx['height'], chain_height=found_tx['height'],
) )
bid_changed = True bid_changed = True
@ -3930,7 +3930,7 @@ class BasicSwap(BaseApp):
raise ValueError('TODO') raise ValueError('TODO')
elif offer_data.swap_type == SwapTypes.XMR_SWAP: elif offer_data.swap_type == SwapTypes.XMR_SWAP:
ensure(coin_from not in non_script_type_coins, 'Invalid coin from type') ensure(coin_from not in non_script_type_coins, 'Invalid coin from type')
ensure(coin_to in non_script_type_coins, 'Invalid coin to type') ensure(ci_from.has_segwit(), 'Coin from must support segwit')
ensure(len(offer_data.proof_address) == 0, 'Unexpected data') ensure(len(offer_data.proof_address) == 0, 'Unexpected data')
ensure(len(offer_data.proof_signature) == 0, 'Unexpected data') ensure(len(offer_data.proof_signature) == 0, 'Unexpected data')
ensure(len(offer_data.pkhash_seller) == 0, 'Unexpected data') ensure(len(offer_data.pkhash_seller) == 0, 'Unexpected data')
@ -4728,6 +4728,8 @@ class BasicSwap(BaseApp):
try: try:
b_lock_tx_id = ci_to.publishBLockTx(xmr_swap.pkbv, xmr_swap.pkbs, bid.amount_to, xmr_offer.b_fee_rate, unlock_time=unlock_time) b_lock_tx_id = ci_to.publishBLockTx(xmr_swap.pkbv, xmr_swap.pkbs, bid.amount_to, xmr_offer.b_fee_rate, unlock_time=unlock_time)
except Exception as ex: except Exception as ex:
if self.debug:
self.log.error(traceback.format_exc())
error_msg = 'publishBLockTx failed for bid {} with error {}'.format(bid_id.hex(), str(ex)) error_msg = 'publishBLockTx failed for bid {} with error {}'.format(bid_id.hex(), str(ex))
num_retries = self.countBidEvents(bid, EventLogTypes.FAILED_TX_B_LOCK_PUBLISH, session) num_retries = self.countBidEvents(bid, EventLogTypes.FAILED_TX_B_LOCK_PUBLISH, session)
if num_retries > 0: if num_retries > 0:
@ -4751,6 +4753,7 @@ class BasicSwap(BaseApp):
tx_type=TxTypes.XMR_SWAP_B_LOCK, tx_type=TxTypes.XMR_SWAP_B_LOCK,
txid=b_lock_tx_id, txid=b_lock_tx_id,
) )
xmr_swap.b_lock_tx_id = b_lock_tx_id
bid.xmr_b_lock_tx.setState(TxStates.TX_NONE) bid.xmr_b_lock_tx.setState(TxStates.TX_NONE)
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_PUBLISHED, '', session) self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_PUBLISHED, '', session)
@ -4867,8 +4870,11 @@ class BasicSwap(BaseApp):
if coin_to == Coins.XMR: if coin_to == Coins.XMR:
address_to = self.getCachedMainWalletAddress(ci_to) address_to = self.getCachedMainWalletAddress(ci_to)
else: elif coin_to == Coins.PART_BLIND:
address_to = self.getCachedStealthAddressForCoin(coin_to) address_to = self.getCachedStealthAddressForCoin(coin_to)
else:
address_to = self.getReceiveAddressFromPool(coin_to, bid_id, TxTypes.XMR_SWAP_B_LOCK_SPEND)
txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, bid.chain_b_height_start) txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, bid.chain_b_height_start)
self.log.debug('Submitted lock B spend txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex()) 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, '', session) self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED, '', session)
@ -4924,8 +4930,10 @@ class BasicSwap(BaseApp):
try: try:
if offer.coin_to == Coins.XMR: if offer.coin_to == Coins.XMR:
address_to = self.getCachedMainWalletAddress(ci_to) address_to = self.getCachedMainWalletAddress(ci_to)
else: elif coin_to == Coins.PART_BLIND:
address_to = self.getCachedStealthAddressForCoin(coin_to) address_to = self.getCachedStealthAddressForCoin(coin_to)
else:
address_to = self.getReceiveAddressFromPool(coin_to, bid_id, TxTypes.XMR_SWAP_B_LOCK_REFUND)
txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, bid.chain_b_height_start) txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, bid.chain_b_height_start)
self.log.debug('Submitted lock B refund txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex()) self.log.debug('Submitted lock B refund txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex())
self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED, '', session) self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED, '', session)

View file

@ -122,6 +122,8 @@ class TxTypes(IntEnum):
XMR_SWAP_A_LOCK_REFUND_SPEND = auto() XMR_SWAP_A_LOCK_REFUND_SPEND = auto()
XMR_SWAP_A_LOCK_REFUND_SWIPE = auto() XMR_SWAP_A_LOCK_REFUND_SWIPE = auto()
XMR_SWAP_B_LOCK = auto() XMR_SWAP_B_LOCK = auto()
XMR_SWAP_B_LOCK_SPEND = auto()
XMR_SWAP_B_LOCK_REFUND = auto()
ITX_PRE_FUNDED = auto() ITX_PRE_FUNDED = auto()

View file

@ -296,7 +296,7 @@ chainparams = {
'blocks_target': 60 * 10, 'blocks_target': 60 * 10,
'decimal_places': 8, 'decimal_places': 8,
'has_csv': True, 'has_csv': True,
'has_segwit': True, 'has_segwit': False,
'mainnet': { 'mainnet': {
'rpcport': 8888, 'rpcport': 8888,
'pubkey_address': 82, 'pubkey_address': 82,
@ -353,20 +353,20 @@ class CoinInterface:
self._unknown_wallet_seed = True self._unknown_wallet_seed = True
self._restore_height = None self._restore_height = None
def make_int(self, amount_in, r=0): def make_int(self, amount_in: int, r: int = 0) -> int:
return make_int(amount_in, self.exp(), r=r) return make_int(amount_in, self.exp(), r=r)
def format_amount(self, amount_in, conv_int=False, r=0): def format_amount(self, amount_in, conv_int=False, r=0):
amount_int = make_int(amount_in, self.exp(), r=r) if conv_int else amount_in amount_int = make_int(amount_in, self.exp(), r=r) if conv_int else amount_in
return format_amount(amount_int, self.exp()) return format_amount(amount_int, self.exp())
def coin_name(self): def coin_name(self) -> str:
coin_chainparams = chainparams[self.coin_type()] coin_chainparams = chainparams[self.coin_type()]
if coin_chainparams.get('use_ticker_as_name', False): if coin_chainparams.get('use_ticker_as_name', False):
return coin_chainparams['ticker'] return coin_chainparams['ticker']
return coin_chainparams['name'].capitalize() return coin_chainparams['name'].capitalize()
def ticker(self): def ticker(self) -> str:
ticker = chainparams[self.coin_type()]['ticker'] ticker = chainparams[self.coin_type()]['ticker']
if self._network == 'testnet': if self._network == 'testnet':
ticker = 't' + ticker ticker = 't' + ticker
@ -405,6 +405,9 @@ class CoinInterface:
def chainparams_network(self): def chainparams_network(self):
return chainparams[self.coin_type()][self._network] return chainparams[self.coin_type()][self._network]
def has_segwit(self) -> bool:
return chainparams[self.coin_type()].get('has_segwit', True)
def is_transient_error(self, ex): def is_transient_error(self, ex):
if isinstance(ex, TemporaryError): if isinstance(ex, TemporaryError):
return True return True

View file

@ -6,7 +6,6 @@
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json import json
import time
import base64 import base64
import hashlib import hashlib
import logging import logging
@ -16,7 +15,6 @@ from io import BytesIO
from basicswap.contrib.test_framework import segwit_addr from basicswap.contrib.test_framework import segwit_addr
from basicswap.util import ( from basicswap.util import (
dumpj,
ensure, ensure,
make_int, make_int,
b2h, i2b, b2i, i2h) b2h, i2b, b2i, i2h)
@ -55,7 +53,8 @@ from basicswap.contrib.test_framework.messages import (
CTxIn, CTxIn,
CTxInWitness, CTxInWitness,
CTxOut, CTxOut,
FromHex) FromHex,
uint256_from_str)
from basicswap.contrib.test_framework.script import ( from basicswap.contrib.test_framework.script import (
CScript, CScriptOp, CScript, CScriptOp,
@ -86,14 +85,14 @@ def ensure_op(v, err_string='Bad opcode'):
ensure(v, err_string) ensure(v, err_string)
def findOutput(tx, script_pk): def findOutput(tx, script_pk: bytes):
for i in range(len(tx.vout)): for i in range(len(tx.vout)):
if tx.vout[i].scriptPubKey == script_pk: if tx.vout[i].scriptPubKey == script_pk:
return i return i
return None return None
def find_vout_for_address_from_txobj(tx_obj, addr): def find_vout_for_address_from_txobj(tx_obj, addr) -> int:
""" """
Locate the vout index of the given transaction sending to the Locate the vout index of the given transaction sending to the
given address. Raises runtime error exception if not found. given address. Raises runtime error exception if not found.
@ -139,7 +138,7 @@ class BTCInterface(CoinInterface):
return 2 return 2
@staticmethod @staticmethod
def getTxOutputValue(tx): def getTxOutputValue(tx) -> int:
rv = 0 rv = 0
for output in tx.vout: for output in tx.vout:
rv += output.nValue rv += output.nValue
@ -158,7 +157,7 @@ class BTCInterface(CoinInterface):
return CTxOut return CTxOut
@staticmethod @staticmethod
def getExpectedSequence(lockType, lockVal): def getExpectedSequence(lockType: int, lockVal: int) -> int:
assert (lockVal >= 1), 'Bad lockVal' assert (lockVal >= 1), 'Bad lockVal'
if lockType == TxLockTypes.SEQUENCE_LOCK_BLOCKS: if lockType == TxLockTypes.SEQUENCE_LOCK_BLOCKS:
return lockVal return lockVal
@ -172,12 +171,16 @@ class BTCInterface(CoinInterface):
raise ValueError('Unknown lock type') raise ValueError('Unknown lock type')
@staticmethod @staticmethod
def decodeSequence(lock_value): def decodeSequence(lock_value: int) -> int:
# Return the raw value # Return the raw value
if lock_value & SEQUENCE_LOCKTIME_TYPE_FLAG: if lock_value & SEQUENCE_LOCKTIME_TYPE_FLAG:
return (lock_value & SEQUENCE_LOCKTIME_MASK) << SEQUENCE_LOCKTIME_GRANULARITY return (lock_value & SEQUENCE_LOCKTIME_MASK) << SEQUENCE_LOCKTIME_GRANULARITY
return lock_value & SEQUENCE_LOCKTIME_MASK return lock_value & SEQUENCE_LOCKTIME_MASK
@staticmethod
def depth_spendable() -> int:
return 0
def __init__(self, coin_settings, network, swap_client=None): def __init__(self, coin_settings, network, swap_client=None):
super().__init__(network) super().__init__(network)
self._rpc_host = coin_settings.get('rpchost', '127.0.0.1') self._rpc_host = coin_settings.get('rpchost', '127.0.0.1')
@ -192,7 +195,8 @@ class BTCInterface(CoinInterface):
self._log = self._sc.log if self._sc and self._sc.log else logging self._log = self._sc.log if self._sc and self._sc.log else logging
self._expect_seedid_hex = None self._expect_seedid_hex = None
def using_segwit(self): def using_segwit(self) -> bool:
# Using btc native segwit
return self._use_segwit return self._use_segwit
def get_connection_type(self): def get_connection_type(self):
@ -217,15 +221,12 @@ class BTCInterface(CoinInterface):
def close_rpc(self, rpc_conn): def close_rpc(self, rpc_conn):
rpc_conn.close() rpc_conn.close()
def setConfTarget(self, new_conf_target): def setConfTarget(self, new_conf_target: int) -> None:
ensure(new_conf_target >= 1 and new_conf_target < 33, 'Invalid conf_target value') ensure(new_conf_target >= 1 and new_conf_target < 33, 'Invalid conf_target value')
self._conf_target = new_conf_target self._conf_target = new_conf_target
def testDaemonRPC(self, with_wallet=True): def testDaemonRPC(self, with_wallet=True) -> None:
if with_wallet: self.rpc_callback('getwalletinfo' if with_wallet else 'getblockchaininfo')
self.rpc_callback('getwalletinfo', [])
else:
self.rpc_callback('getblockchaininfo', [])
def getDaemonVersion(self): def getDaemonVersion(self):
return self.rpc_callback('getnetworkinfo')['version'] return self.rpc_callback('getnetworkinfo')['version']
@ -233,7 +234,7 @@ class BTCInterface(CoinInterface):
def getBlockchainInfo(self): def getBlockchainInfo(self):
return self.rpc_callback('getblockchaininfo') return self.rpc_callback('getblockchaininfo')
def getChainHeight(self): def getChainHeight(self) -> int:
return self.rpc_callback('getblockcount') return self.rpc_callback('getblockcount')
def getMempoolTx(self, txid): def getMempoolTx(self, txid):
@ -259,7 +260,7 @@ class BTCInterface(CoinInterface):
last_block_header = prev_block_header last_block_header = prev_block_header
raise ValueError(f'Block header not found at time: {time}') raise ValueError(f'Block header not found at time: {time}')
def initialiseWallet(self, key_bytes): def initialiseWallet(self, key_bytes: bytes):
key_wif = self.encodeKey(key_bytes) key_wif = self.encodeKey(key_bytes)
self.rpc_callback('sethdseed', [True, key_wif]) self.rpc_callback('sethdseed', [True, key_wif])
@ -270,10 +271,10 @@ class BTCInterface(CoinInterface):
rv['locked'] = rv.get('unlocked_until', 1) <= 0 rv['locked'] = rv.get('unlocked_until', 1) <= 0
return rv return rv
def walletRestoreHeight(self): def walletRestoreHeight(self) -> int:
return self._restore_height return self._restore_height
def getWalletRestoreHeight(self): def getWalletRestoreHeight(self) -> int:
start_time = self.rpc_callback('getwalletinfo')['keypoololdest'] start_time = self.rpc_callback('getwalletinfo')['keypoololdest']
blockchaininfo = self.rpc_callback('getblockchaininfo') blockchaininfo = self.rpc_callback('getblockchaininfo')
@ -295,6 +296,7 @@ class BTCInterface(CoinInterface):
block_hash = block_header['previousblockhash'] block_hash = block_header['previousblockhash']
finally: finally:
self.close_rpc(rpc_conn) self.close_rpc(rpc_conn)
raise ValueError('{} wallet restore height not found.'.format(self.coin_name()))
def getWalletSeedID(self) -> str: def getWalletSeedID(self) -> str:
return self.rpc_callback('getwalletinfo')['hdseedid'] return self.rpc_callback('getwalletinfo')['hdseedid']
@ -309,9 +311,11 @@ class BTCInterface(CoinInterface):
args.append('bech32') args.append('bech32')
return self.rpc_callback('getnewaddress', args) return self.rpc_callback('getnewaddress', args)
def isAddressMine(self, address: str) -> bool: def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
addr_info = self.rpc_callback('getaddressinfo', [address]) addr_info = self.rpc_callback('getaddressinfo', [address])
if not or_watch_only:
return addr_info['ismine'] return addr_info['ismine']
return addr_info['ismine'] or addr_info['iswatchonly']
def checkAddressMine(self, address: str) -> None: def checkAddressMine(self, address: str) -> None:
addr_info = self.rpc_callback('getaddressinfo', [address]) addr_info = self.rpc_callback('getaddressinfo', [address])
@ -331,22 +335,22 @@ class BTCInterface(CoinInterface):
except Exception: except Exception:
return self.rpc_callback('getnetworkinfo')['relayfee'], 'relayfee' return self.rpc_callback('getnetworkinfo')['relayfee'], 'relayfee'
def isSegwitAddress(self, address): def isSegwitAddress(self, address: str) -> bool:
return address.startswith(self.chainparams_network()['hrp'] + '1') return address.startswith(self.chainparams_network()['hrp'] + '1')
def decodeAddress(self, address): def decodeAddress(self, address: str) -> bytes:
bech32_prefix = self.chainparams_network()['hrp'] bech32_prefix = self.chainparams_network()['hrp']
if len(bech32_prefix) > 0 and address.startswith(bech32_prefix + '1'): if len(bech32_prefix) > 0 and address.startswith(bech32_prefix + '1'):
return bytes(segwit_addr.decode(bech32_prefix, address)[1]) return bytes(segwit_addr.decode(bech32_prefix, address)[1])
return decodeAddress(address)[1:] return decodeAddress(address)[1:]
def pubkey_to_segwit_address(self, pk): def pubkey_to_segwit_address(self, pk: bytes) -> str:
bech32_prefix = self.chainparams_network()['hrp'] bech32_prefix = self.chainparams_network()['hrp']
version = 0 version = 0
pkh = hash160(pk) pkh = hash160(pk)
return segwit_addr.encode(bech32_prefix, version, pkh) return segwit_addr.encode(bech32_prefix, version, pkh)
def pkh_to_address(self, pkh): def pkh_to_address(self, pkh: bytes) -> str:
# pkh is hash160(pk) # pkh is hash160(pk)
assert (len(pkh) == 20) assert (len(pkh) == 20)
prefix = self.chainparams_network()['pubkey_address'] prefix = self.chainparams_network()['pubkey_address']
@ -354,26 +358,26 @@ class BTCInterface(CoinInterface):
checksum = hashlib.sha256(hashlib.sha256(data).digest()).digest() checksum = hashlib.sha256(hashlib.sha256(data).digest()).digest()
return b58encode(data + checksum[0:4]) return b58encode(data + checksum[0:4])
def sh_to_address(self, sh): def sh_to_address(self, sh: bytes) -> str:
assert (len(sh) == 20) assert (len(sh) == 20)
prefix = self.chainparams_network()['script_address'] prefix = self.chainparams_network()['script_address']
data = bytes((prefix,)) + sh data = bytes((prefix,)) + sh
checksum = hashlib.sha256(hashlib.sha256(data).digest()).digest() checksum = hashlib.sha256(hashlib.sha256(data).digest()).digest()
return b58encode(data + checksum[0:4]) return b58encode(data + checksum[0:4])
def encode_p2wsh(self, script): def encode_p2wsh(self, script: bytes) -> str:
bech32_prefix = self.chainparams_network()['hrp'] bech32_prefix = self.chainparams_network()['hrp']
version = 0 version = 0
program = script[2:] # strip version and length program = script[2:] # strip version and length
return segwit_addr.encode(bech32_prefix, version, program) return segwit_addr.encode(bech32_prefix, version, program)
def encodeScriptDest(self, script): def encodeScriptDest(self, script: bytes) -> str:
return self.encode_p2wsh(script) return self.encode_p2wsh(script)
def encode_p2sh(self, script): def encode_p2sh(self, script: bytes) -> str:
return pubkeyToAddress(self.chainparams_network()['script_address'], script) return pubkeyToAddress(self.chainparams_network()['script_address'], script)
def pubkey_to_address(self, pk): def pubkey_to_address(self, pk: bytes) -> str:
assert (len(pk) == 33) assert (len(pk) == 33)
return self.pkh_to_address(hash160(pk)) return self.pkh_to_address(hash160(pk))
@ -383,31 +387,31 @@ class BTCInterface(CoinInterface):
def getPubkey(self, privkey): def getPubkey(self, privkey):
return PublicKey.from_secret(privkey).format() return PublicKey.from_secret(privkey).format()
def getAddressHashFromKey(self, key): def getAddressHashFromKey(self, key) -> bytes:
pk = self.getPubkey(key) pk = self.getPubkey(key)
return hash160(pk) return hash160(pk)
def getSeedHash(self, seed): def getSeedHash(self, seed):
return self.getAddressHashFromKey(seed)[::-1] return self.getAddressHashFromKey(seed)[::-1]
def verifyKey(self, k): def verifyKey(self, k: bytes) -> bool:
i = b2i(k) i = b2i(k)
return (i < ep.o and i > 0) return (i < ep.o and i > 0)
def verifyPubkey(self, pubkey_bytes): def verifyPubkey(self, pubkey_bytes: bytes) -> bool:
return verify_secp256k1_point(pubkey_bytes) return verify_secp256k1_point(pubkey_bytes)
def encodeKey(self, key_bytes): def encodeKey(self, key_bytes: bytes) -> str:
wif_prefix = self.chainparams_network()['key_prefix'] wif_prefix = self.chainparams_network()['key_prefix']
return toWIF(wif_prefix, key_bytes) return toWIF(wif_prefix, key_bytes)
def encodePubkey(self, pk): def encodePubkey(self, pk: bytes) -> bytes:
return pointToCPK(pk) return pointToCPK(pk)
def encodeSegwitAddress(self, key_hash): def encodeSegwitAddress(self, key_hash: bytes) -> str:
return segwit_addr.encode(self.chainparams_network()['hrp'], 0, key_hash) return segwit_addr.encode(self.chainparams_network()['hrp'], 0, key_hash)
def decodeSegwitAddress(self, addr): def decodeSegwitAddress(self, addr: str) -> bytes:
return bytes(segwit_addr.decode(self.chainparams_network()['hrp'], addr)[1]) return bytes(segwit_addr.decode(self.chainparams_network()['hrp'], addr)[1])
def decodePubkey(self, pke): def decodePubkey(self, pke):
@ -423,10 +427,10 @@ class BTCInterface(CoinInterface):
def sumPubkeys(self, Ka, Kb): def sumPubkeys(self, Ka, Kb):
return PublicKey.combine_keys([PublicKey(Ka), PublicKey(Kb)]).format() return PublicKey.combine_keys([PublicKey(Ka), PublicKey(Kb)]).format()
def getScriptForPubkeyHash(self, pkh): def getScriptForPubkeyHash(self, pkh: bytes) -> CScript:
return CScript([OP_0, pkh]) return CScript([OP_0, pkh])
def extractScriptLockScriptValues(self, script_bytes): def extractScriptLockScriptValues(self, script_bytes: bytes):
script_len = len(script_bytes) script_len = len(script_bytes)
ensure(script_len == 71, 'Bad script length') ensure(script_len == 71, 'Bad script length')
o = 0 o = 0
@ -444,7 +448,7 @@ class BTCInterface(CoinInterface):
return pk1, pk2 return pk1, pk2
def createSCLockTx(self, value: int, script: bytearray, vkbv=None) -> bytes: def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes = None) -> bytes:
tx = CTransaction() tx = CTransaction()
tx.nVersion = self.txVersion() tx.nVersion = self.txVersion()
tx.vout.append(self.txoType()(value, self.getScriptDest(script))) tx.vout.append(self.txoType()(value, self.getScriptDest(script)))
@ -898,23 +902,28 @@ class BTCInterface(CoinInterface):
inputs.append({'txid': i2h(pi.prevout.hash), 'vout': pi.prevout.n}) inputs.append({'txid': i2h(pi.prevout.hash), 'vout': pi.prevout.n})
self.rpc_callback('lockunspent', [True, inputs]) self.rpc_callback('lockunspent', [True, inputs])
def signTxWithWallet(self, tx): def signTxWithWallet(self, tx: bytes) -> bytes:
rv = self.rpc_callback('signrawtransactionwithwallet', [tx.hex()]) rv = self.rpc_callback('signrawtransactionwithwallet', [tx.hex()])
return bytes.fromhex(rv['hex']) return bytes.fromhex(rv['hex'])
def publishTx(self, tx): def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
key_wif = self.encodeKey(key)
rv = self.rpc_callback('signrawtransactionwithkey', [tx.hex(), [key_wif, ]])
return bytes.fromhex(rv['hex'])
def publishTx(self, tx: bytes):
return self.rpc_callback('sendrawtransaction', [tx.hex()]) return self.rpc_callback('sendrawtransaction', [tx.hex()])
def encodeTx(self, tx): def encodeTx(self, tx) -> bytes:
return tx.serialize() return tx.serialize()
def loadTx(self, tx_bytes) -> CTransaction: def loadTx(self, tx_bytes: bytes) -> CTransaction:
# Load tx from bytes to internal representation # Load tx from bytes to internal representation
tx = CTransaction() tx = CTransaction()
tx.deserialize(BytesIO(tx_bytes)) tx.deserialize(BytesIO(tx_bytes))
return tx return tx
def getTxid(self, tx): def getTxid(self, tx) -> bytes:
if isinstance(tx, str): if isinstance(tx, str):
tx = bytes.fromhex(tx) tx = bytes.fromhex(tx)
if isinstance(tx, bytes): if isinstance(tx, bytes):
@ -928,16 +937,16 @@ class BTCInterface(CoinInterface):
script_pk = self.getScriptDest(script) script_pk = self.getScriptDest(script)
return findOutput(tx, script_pk) return findOutput(tx, script_pk)
def getPubkeyHash(self, K): def getPubkeyHash(self, K: bytes) -> bytes:
return hash160(self.encodePubkey(K)) return hash160(K)
def getScriptDest(self, script): def getScriptDest(self, script):
return CScript([OP_0, hashlib.sha256(script).digest()]) return CScript([OP_0, hashlib.sha256(script).digest()])
def getScriptScriptSig(self, script): def getScriptScriptSig(self, script: bytes) -> bytes:
return bytes() return bytes()
def getPkDest(self, K): def getPkDest(self, K: bytes) -> bytearray:
return self.getScriptForPubkeyHash(self.getPubkeyHash(K)) return self.getScriptForPubkeyHash(self.getPubkeyHash(K))
def scanTxOutset(self, dest): def scanTxOutset(self, dest):
@ -980,26 +989,26 @@ class BTCInterface(CoinInterface):
def createBLockTx(self, Kbs, output_amount): def createBLockTx(self, Kbs, output_amount):
tx = CTransaction() tx = CTransaction()
tx.nVersion = self.txVersion() tx.nVersion = self.txVersion()
p2wpkh = self.getPkDest(Kbs) p2wpkh_script_pk = self.getPkDest(Kbs)
tx.vout.append(self.txoType()(output_amount, p2wpkh)) tx.vout.append(self.txoType()(output_amount, p2wpkh_script_pk))
return tx.serialize() return tx.serialize()
def encodeSharedAddress(self, Kbv, Kbs): def encodeSharedAddress(self, Kbv, Kbs):
return self.pubkey_to_segwit_address(Kbs) return self.pubkey_to_segwit_address(Kbs)
def publishBLockTx(self, Kbv, Kbs, output_amount, feerate, delay_for=10, unlock_time=0): def publishBLockTx(self, Kbv, Kbs, output_amount, feerate, delay_for: int = 10, unlock_time: int = 0) -> bytes:
b_lock_tx = self.createBLockTx(Kbs, output_amount) b_lock_tx = self.createBLockTx(Kbs, output_amount)
b_lock_tx = self.fundTx(b_lock_tx, feerate) b_lock_tx = self.fundTx(b_lock_tx, feerate)
b_lock_tx_id = self.getTxid(b_lock_tx) b_lock_tx_id = self.getTxid(b_lock_tx)
b_lock_tx = self.signTxWithWallet(b_lock_tx) b_lock_tx = self.signTxWithWallet(b_lock_tx)
return self.publishTx(b_lock_tx) return bytes.fromhex(self.publishTx(b_lock_tx))
def recoverEncKey(self, esig, sig, K): def recoverEncKey(self, esig, sig, K):
return ecdsaotves_rec_enc_key(K, esig, sig[:-1]) # Strip sighash type return ecdsaotves_rec_enc_key(K, esig, sig[:-1]) # Strip sighash type
def getTxVSize(self, tx, add_bytes=0, add_witness_bytes=0): def getTxVSize(self, tx, add_bytes: int = 0, add_witness_bytes: int = 0) -> int:
wsf = self.witnessScaleFactor() wsf = self.witnessScaleFactor()
len_full = len(tx.serialize_with_witness()) + add_bytes + add_witness_bytes len_full = len(tx.serialize_with_witness()) + add_bytes + add_witness_bytes
len_nwit = len(tx.serialize_without_witness()) + add_bytes len_nwit = len(tx.serialize_without_witness()) + add_bytes
@ -1007,10 +1016,13 @@ class BTCInterface(CoinInterface):
return (weight + wsf - 1) // wsf return (weight + wsf - 1) // wsf
def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender): def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender):
dest_address = self.pubkey_to_segwit_address(Kbs) if self.using_segwit() else self.pubkey_to_address(Kbs)
return self.getLockTxHeight(None, dest_address, cb_swap_value, restore_height)
'''
raw_dest = self.getPkDest(Kbs) raw_dest = self.getPkDest(Kbs)
rv = self.scanTxOutset(raw_dest) rv = self.scanTxOutset(raw_dest)
print('scanTxOutset', dumpj(rv))
for utxo in rv['unspents']: for utxo in rv['unspents']:
if 'height' in utxo and utxo['height'] > 0 and rv['height'] - utxo['height'] > cb_block_confirmed: if 'height' in utxo and utxo['height'] > 0 and rv['height'] - utxo['height'] > cb_block_confirmed:
@ -1019,31 +1031,45 @@ class BTCInterface(CoinInterface):
else: else:
return {'txid': utxo['txid'], 'vout': utxo['vout'], 'amount': utxo['amount'], 'height': utxo['height']} return {'txid': utxo['txid'], 'vout': utxo['vout'], 'amount': utxo['amount'], 'height': utxo['height']}
return None return None
'''
def waitForLockTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed): def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int) -> bytes:
raw_dest = self.getPkDest(Kbs) wtx = self.rpc_callback('gettransaction', [chain_b_lock_txid.hex(), ])
lock_tx = self.loadTx(bytes.fromhex(wtx['hex']))
for i in range(20): Kbs = self.getPubkey(kbs)
time.sleep(1) script_pk = self.getPkDest(Kbs)
rv = self.scanTxOutset(raw_dest) locked_n = findOutput(lock_tx, script_pk)
print('scanTxOutset', dumpj(rv)) ensure(locked_n is not None, 'Output not found in tx')
pkh_to = self.decodeAddress(address_to)
for utxo in rv['unspents']: tx = CTransaction()
if 'height' in utxo and utxo['height'] > 0 and rv['height'] - utxo['height'] > cb_block_confirmed: tx.nVersion = self.txVersion()
if self.make_int(utxo['amount']) != cb_swap_value: script_lock = self.getScriptForPubkeyHash(Kbs)
self._log.warning('Found output to lock tx pubkey of incorrect value: %s', str(utxo['amount'])) chain_b_lock_txid_int = uint256_from_str(chain_b_lock_txid[::-1])
else:
return True
return False
def spendBLockTx(self, chain_b_lock_txid, address_to, kbv, kbs, cb_swap_value, b_fee, restore_height): tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n),
raise ValueError('TODO') nSequence=0,
scriptSig=self.getScriptScriptSig(script_lock)))
tx.vout.append(self.txoType()(cb_swap_value, self.getScriptForPubkeyHash(pkh_to)))
def importWatchOnlyAddress(self, address, label): witness_bytes = 109
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
pay_fee = int(b_fee * vsize // 1000)
tx.vout[0].nValue = cb_swap_value - pay_fee
self._log.info('spendBLockTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
chain_b_lock_txid.hex(), b_fee, vsize, pay_fee)
b_lock_spend_tx = tx.serialize()
b_lock_spend_tx = self.signTxWithKey(b_lock_spend_tx, kbs)
return bytes.fromhex(self.publishTx(b_lock_spend_tx))
def importWatchOnlyAddress(self, address: str, label: str):
self.rpc_callback('importaddress', [address, label, False]) self.rpc_callback('importaddress', [address, label, False])
def isWatchOnlyAddress(self, address): def isWatchOnlyAddress(self, address: str):
addr_info = self.rpc_callback('getaddressinfo', [address]) addr_info = self.rpc_callback('getaddressinfo', [address])
return addr_info['iswatchonly'] return addr_info['iswatchonly']
@ -1051,10 +1077,10 @@ class BTCInterface(CoinInterface):
lock_tx_dest = self.getScriptDest(lock_script) lock_tx_dest = self.getScriptDest(lock_script)
return self.encodeScriptDest(lock_tx_dest) return self.encodeScriptDest(lock_tx_dest)
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index=False): def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False):
# Add watchonly address and rescan if required # Add watchonly address and rescan if required
if not self.isWatchOnlyAddress(dest_address): if not self.isAddressMine(dest_address, or_watch_only=True):
self.importWatchOnlyAddress(dest_address, 'bid') self.importWatchOnlyAddress(dest_address, 'bid')
self._log.info('Imported watch-only addr: {}'.format(dest_address)) self._log.info('Imported watch-only addr: {}'.format(dest_address))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from)) self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from))
@ -1263,7 +1289,6 @@ class BTCInterface(CoinInterface):
sign_for_addr = None sign_for_addr = None
for addr, value in unspent_addr.items(): for addr, value in unspent_addr.items():
print('[rm]', value, amount_for)
if value >= amount_for: if value >= amount_for:
sign_for_addr = addr sign_for_addr = addr
break break
@ -1272,7 +1297,7 @@ class BTCInterface(CoinInterface):
self._log.debug('sign_for_addr %s', sign_for_addr) self._log.debug('sign_for_addr %s', sign_for_addr)
if self._use_segwit: # TODO: Use isSegwitAddress when scantxoutset can use combo if self.using_segwit(): # TODO: Use isSegwitAddress when scantxoutset can use combo
# 'Address does not refer to key' for non p2pkh # 'Address does not refer to key' for non p2pkh
pkh = self.decodeAddress(sign_for_addr) pkh = self.decodeAddress(sign_for_addr)
sign_for_addr = self.pkh_to_address(pkh) sign_for_addr = self.pkh_to_address(pkh)
@ -1286,7 +1311,7 @@ class BTCInterface(CoinInterface):
passed = self.verifyMessage(address, address + '_swap_proof_' + extra_commit_bytes.hex(), signature) passed = self.verifyMessage(address, address + '_swap_proof_' + extra_commit_bytes.hex(), signature)
ensure(passed is True, 'Proof of funds signature invalid') ensure(passed is True, 'Proof of funds signature invalid')
if self._use_segwit: if self.using_segwit():
address = self.encodeSegwitAddress(decodeAddress(address)[1:]) address = self.encodeSegwitAddress(decodeAddress(address)[1:])
return self.getUTXOBalance(address) return self.getUTXOBalance(address)
@ -1334,6 +1359,19 @@ class BTCInterface(CoinInterface):
def get_p2wsh_script_pubkey(self, script: bytearray) -> bytearray: def get_p2wsh_script_pubkey(self, script: bytearray) -> bytearray:
return CScript([OP_0, hashlib.sha256(script).digest()]) return CScript([OP_0, hashlib.sha256(script).digest()])
def findTxnByHash(self, txid_hex: str):
# Only works for wallet txns
try:
rv = self.rpc_callback('gettransaction', [txid_hex])
except Exception as ex:
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
return None
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
return {'txid': txid_hex, 'amount': 0, 'height': rv['blockheight']}
return None
def testBTCInterface(): def testBTCInterface():
print('TODO: testBTCInterface') print('TODO: testBTCInterface')

View file

@ -55,15 +55,17 @@ class FIROInterface(BTCInterface):
addr_info = self.rpc_callback('validateaddress', [address]) addr_info = self.rpc_callback('validateaddress', [address])
return addr_info['iswatchonly'] return addr_info['iswatchonly']
def isAddressMine(self, address): def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
addr_info = self.rpc_callback('validateaddress', [address]) addr_info = self.rpc_callback('validateaddress', [address])
if not or_watch_only:
return addr_info['ismine'] return addr_info['ismine']
return addr_info['ismine'] or addr_info['iswatchonly']
def getSCLockScriptAddress(self, lock_script): def getSCLockScriptAddress(self, lock_script):
lock_tx_dest = self.getScriptDest(lock_script) lock_tx_dest = self.getScriptDest(lock_script)
address = self.encodeScriptDest(lock_tx_dest) address = self.encodeScriptDest(lock_tx_dest)
if not self.isWatchOnlyAddress(address): if not self.isAddressMine(address, or_watch_only=True):
# Expects P2WSH nested in BIP16_P2SH # Expects P2WSH nested in BIP16_P2SH
ro = self.rpc_callback('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True]) ro = self.rpc_callback('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True])
addr_info = self.rpc_callback('validateaddress', [address]) addr_info = self.rpc_callback('validateaddress', [address])
@ -74,7 +76,7 @@ class FIROInterface(BTCInterface):
# Add watchonly address and rescan if required # Add watchonly address and rescan if required
lock_tx_dest = self.getScriptDest(lock_script) lock_tx_dest = self.getScriptDest(lock_script)
dest_address = self.encodeScriptDest(lock_tx_dest) dest_address = self.encodeScriptDest(lock_tx_dest)
if not self.isWatchOnlyAddress(dest_address): if not self.isAddressMine(dest_address, or_watch_only=True):
self.rpc_callback('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True]) self.rpc_callback('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True])
self._log.info('Imported watch-only addr: {}'.format(dest_address)) self._log.info('Imported watch-only addr: {}'.format(dest_address))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from)) self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from))

View file

@ -638,7 +638,7 @@ class PARTInterfaceAnon(PARTInterface):
def coin_name(self): def coin_name(self):
return super().coin_name() + ' Anon' return super().coin_name() + ' Anon'
def publishBLockTx(self, Kbv, Kbs, output_amount, feerate, delay_for=10, unlock_time=0): def publishBLockTx(self, Kbv, Kbs, output_amount, feerate, delay_for: int = 10, unlock_time: int = 0) -> bytes:
sx_addr = self.formatStealthAddress(Kbv, Kbs) sx_addr = self.formatStealthAddress(Kbv, Kbs)
self._log.debug('sx_addr: {}'.format(sx_addr)) self._log.debug('sx_addr: {}'.format(sx_addr))

View file

@ -260,7 +260,7 @@ class XMRInterface(CoinInterface):
def encodeSharedAddress(self, Kbv, Kbs): def encodeSharedAddress(self, Kbv, Kbs):
return xmr_util.encode_address(Kbv, Kbs) return xmr_util.encode_address(Kbv, Kbs)
def publishBLockTx(self, Kbv, Kbs, output_amount, feerate, delay_for=10, unlock_time=0): def publishBLockTx(self, Kbv, Kbs, output_amount, feerate, delay_for: int = 10, unlock_time: int = 0) -> bytes:
with self._mx_wallet: with self._mx_wallet:
self.openWallet(self._wallet_filename) self.openWallet(self._wallet_filename)
@ -339,67 +339,6 @@ class XMRInterface(CoinInterface):
rv = -1 rv = -1
return rv return rv
def waitForLockTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height):
with self._mx_wallet:
Kbv_enc = self.encodePubkey(self.pubkey(kbv))
address_b58 = xmr_util.encode_address(Kbv_enc, self.encodePubkey(Kbs))
try:
self.rpc_wallet_cb('close_wallet')
except Exception as e:
self._log.warning('close_wallet failed %s', str(e))
params = {
'filename': address_b58,
'address': address_b58,
'viewkey': b2h(kbv[::-1]),
'restore_height': restore_height,
}
self.createWallet(params)
self.openWallet(address_b58)
# For a while after opening the wallet rpc cmds return empty data
num_tries = 40
for i in range(num_tries + 1):
try:
current_height = self.rpc_cb2('get_height')['height']
print('current_height', current_height)
except Exception as e:
self._log.warning('rpc_cb failed %s', str(e))
current_height = None # If the transfer is available it will be deep enough
# TODO: Make accepting current_height == None a user selectable option
# Or look for all transfers and check height
params = {'transfer_type': 'available'}
rv = self.rpc_wallet_cb('incoming_transfers', params)
print('rv', rv)
if 'transfers' in rv:
for transfer in rv['transfers']:
if transfer['amount'] == cb_swap_value \
and (current_height is None or current_height - transfer['block_height'] > cb_block_confirmed):
return True
# TODO: Is it necessary to check the address?
'''
rv = self.rpc_wallet_cb('get_balance')
print('get_balance', rv)
if 'per_subaddress' in rv:
for sub_addr in rv['per_subaddress']:
if sub_addr['address'] == address_b58:
'''
if i >= num_tries:
raise ValueError('Balance not confirming on node')
self._sc.delay_event.wait(1.0)
if self._sc.delay_event.is_set():
raise ValueError('Stopped')
return False
def findTxnByHash(self, txid): def findTxnByHash(self, txid):
with self._mx_wallet: with self._mx_wallet:
self.openWallet(self._wallet_filename) self.openWallet(self._wallet_filename)

View file

@ -28,6 +28,8 @@ from tests.basicswap.util import (
from tests.basicswap.common import ( from tests.basicswap.common import (
wait_for_bid, wait_for_bid,
wait_for_offer, wait_for_offer,
wait_for_balance,
wait_for_unspent,
wait_for_none_active, wait_for_none_active,
BTC_BASE_RPC_PORT, BTC_BASE_RPC_PORT,
) )
@ -52,8 +54,8 @@ logger = logging.getLogger()
class BasicSwapTest(BaseTest): class BasicSwapTest(BaseTest):
base_rpc_port = None base_rpc_port = None
def getBalance(self, js_wallets): def getBalance(self, js_wallets, coin):
return float(js_wallets[self.test_coin_from.name]['balance']) + float(js_wallets[self.test_coin_from.name]['unconfirmed']) return float(js_wallets[coin.name]['balance']) + float(js_wallets[coin.name]['unconfirmed'])
def callnoderpc(self, method, params=[], wallet=None, node_id=0): def callnoderpc(self, method, params=[], wallet=None, node_id=0):
return callnoderpc(node_id, method, params, wallet, self.base_rpc_port) return callnoderpc(node_id, method, params, wallet, self.base_rpc_port)
@ -252,22 +254,25 @@ class BasicSwapTest(BaseTest):
self.callnoderpc('unloadwallet', [new_wallet_name]) self.callnoderpc('unloadwallet', [new_wallet_name])
assert (addr == 'bcrt1qps7hnjd866e9ynxadgseprkc2l56m00dvwargr') assert (addr == 'bcrt1qps7hnjd866e9ynxadgseprkc2l56m00dvwargr')
def test_01_full_swap(self): def do_test_01_full_swap(self, coin_from, coin_to):
logging.info('---------- Test {} to XMR'.format(self.test_coin_from.name)) logging.info('---------- Test {} to {}'.format(coin_from.name, coin_to.name))
swap_clients = self.swap_clients swap_clients = self.swap_clients
ci_from = swap_clients[0].ci(coin_from)
ci_to = swap_clients[1].ci(coin_to)
js_0 = read_json_api(1800, 'wallets') js_0 = read_json_api(1800, 'wallets')
node0_from_before = self.getBalance(js_0) node0_from_before = self.getBalance(js_0, coin_from)
js_1 = read_json_api(1801, 'wallets') js_1 = read_json_api(1801, 'wallets')
node1_from_before = self.getBalance(js_1) node1_from_before = self.getBalance(js_1, coin_from)
js_0_xmr = read_json_api(1800, 'wallets/xmr') js_0_to = read_json_api(1800, 'wallets/{}'.format(coin_to.name.lower()))
js_1_xmr = read_json_api(1801, 'wallets/xmr') js_1_to = read_json_api(1801, 'wallets/{}'.format(coin_to.name.lower()))
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
offer_id = swap_clients[0].postOffer(self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP) offer_id = swap_clients[0].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP)
wait_for_offer(test_delay_event, swap_clients[1], offer_id) wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offers = swap_clients[0].listOffers(filters={'offer_id': offer_id}) offers = swap_clients[0].listOffers(filters={'offer_id': offer_id})
offer = offers[0] offer = offers[0]
@ -281,37 +286,55 @@ class BasicSwapTest(BaseTest):
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=180)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True) wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True)
amount_from = float(format_amount(amt_swap, 8)) amount_from = float(ci_from.format_amount(amt_swap))
js_1 = read_json_api(1801, 'wallets') js_1 = read_json_api(1801, 'wallets')
node1_from_after = self.getBalance(js_1) node1_from_after = self.getBalance(js_1, coin_from)
if coin_from is not Coins.PART: # TODO: staking
assert (node1_from_after > node1_from_before + (amount_from - 0.05)) assert (node1_from_after > node1_from_before + (amount_from - 0.05))
js_0 = read_json_api(1800, 'wallets') js_0 = read_json_api(1800, 'wallets')
node0_from_after = self.getBalance(js_0) node0_from_after = self.getBalance(js_0, coin_from)
# TODO: Discard block rewards # TODO: Discard block rewards
# assert (node0_from_after < node0_from_before - amount_from) # assert (node0_from_after < node0_from_before - amount_from)
js_0_xmr_after = read_json_api(1800, 'wallets/xmr') js_0_to_after = read_json_api(1800, 'wallets/{}'.format(coin_to.name.lower()))
js_1_xmr_after = read_json_api(1801, 'wallets/xmr') js_1_to_after = read_json_api(1801, 'wallets/{}'.format(coin_to.name.lower()))
scale_from = 8 scale_from = 8
amount_to = int((amt_swap * rate_swap) // (10 ** scale_from)) amount_to = int((amt_swap * rate_swap) // (10 ** scale_from))
amount_to_float = float(format_amount(amount_to, 12)) amount_to_float = float(ci_to.format_amount(amount_to))
node1_xmr_after = float(js_1_xmr_after['unconfirmed']) + float(js_1_xmr_after['balance']) node1_to_after = float(js_1_to_after['unconfirmed']) + float(js_1_to_after['balance'])
node1_xmr_before = float(js_1_xmr['unconfirmed']) + float(js_1_xmr['balance']) node1_to_before = float(js_1_to['unconfirmed']) + float(js_1_to['balance'])
assert (node1_xmr_after > node1_xmr_before + (amount_to_float - 0.02)) if False: # TODO: set stakeaddress and xmr rewards to non wallet addresses
assert (node1_to_after < node1_to_before - amount_to_float)
def test_01_full_swap(self):
if not self.has_segwit:
return
self.do_test_01_full_swap(self.test_coin_from, Coins.XMR)
def test_01_full_swap_to_part(self):
if not self.has_segwit:
return
self.do_test_01_full_swap(self.test_coin_from, Coins.PART)
def test_01_full_swap_from_part(self):
self.do_test_01_full_swap(Coins.PART, self.test_coin_from)
def do_test_02_leader_recover_a_lock_tx(self, coin_from, coin_to):
logging.info('---------- Test {} to {} leader recovers coin a lock tx'.format(coin_from.name, coin_to.name))
def test_02_leader_recover_a_lock_tx(self):
logging.info('---------- Test {} to XMR leader recovers coin a lock tx'.format(self.test_coin_from.name))
swap_clients = self.swap_clients swap_clients = self.swap_clients
ci_from = swap_clients[0].ci(coin_from)
ci_to = swap_clients[0].ci(coin_to)
js_w0_before = read_json_api(1800, 'wallets') js_w0_before = read_json_api(1800, 'wallets')
node0_from_before = self.getBalance(js_w0_before) node0_from_before = self.getBalance(js_w0_before, coin_from)
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
offer_id = swap_clients[0].postOffer( offer_id = swap_clients[0].postOffer(
self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32) lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32)
wait_for_offer(test_delay_event, swap_clients[1], offer_id) wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offer = swap_clients[1].getOffer(offer_id) offer = swap_clients[1].getOffer(offer_id)
@ -331,28 +354,43 @@ class BasicSwapTest(BaseTest):
wait_for_bid(test_delay_event, swap_clients[1], bid_id, [BidStates.BID_STALLED_FOR_TEST, BidStates.XMR_SWAP_FAILED], sent=True) wait_for_bid(test_delay_event, swap_clients[1], bid_id, [BidStates.BID_STALLED_FOR_TEST, BidStates.XMR_SWAP_FAILED], sent=True)
js_w0_after = read_json_api(1800, 'wallets') js_w0_after = read_json_api(1800, 'wallets')
node0_from_after = self.getBalance(js_w0_after) node0_from_after = self.getBalance(js_w0_after, coin_from)
# TODO: Discard block rewards # TODO: Discard block rewards
# assert (node0_from_before - node0_from_after < 0.02) # assert (node0_from_before - node0_from_after < 0.02)
def test_03_follower_recover_a_lock_tx(self): def test_02_leader_recover_a_lock_tx(self):
logging.info('---------- Test {} to XMR follower recovers coin a lock tx'.format(self.test_coin_from.name)) if not self.has_segwit:
return
self.do_test_02_leader_recover_a_lock_tx(self.test_coin_from, Coins.XMR)
def test_02_leader_recover_a_lock_tx_to_part(self):
if not self.has_segwit:
return
self.do_test_02_leader_recover_a_lock_tx(self.test_coin_from, Coins.PART)
def test_02_leader_recover_a_lock_tx_from_part(self):
self.do_test_02_leader_recover_a_lock_tx(Coins.PART, self.test_coin_from)
def do_test_03_follower_recover_a_lock_tx(self, coin_from, coin_to):
logging.info('---------- Test {} to {} follower recovers coin a lock tx'.format(coin_from.name, coin_to.name))
swap_clients = self.swap_clients swap_clients = self.swap_clients
ci_from = swap_clients[0].ci(coin_from)
ci_to = swap_clients[0].ci(coin_to)
js_w0_before = read_json_api(1800, 'wallets') js_w0_before = read_json_api(1800, 'wallets')
js_w1_before = read_json_api(1801, 'wallets') js_w1_before = read_json_api(1801, 'wallets')
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
offer_id = swap_clients[0].postOffer( offer_id = swap_clients[0].postOffer(
self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32) lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32)
wait_for_offer(test_delay_event, swap_clients[1], offer_id) wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offer = swap_clients[1].getOffer(offer_id) offer = swap_clients[1].getOffer(offer_id)
bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED)
bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) bid, xmr_swap = swap_clients[0].getXmrBid(bid_id)
@ -368,8 +406,8 @@ class BasicSwapTest(BaseTest):
js_w1_after = read_json_api(1801, 'wallets') js_w1_after = read_json_api(1801, 'wallets')
node1_from_before = self.getBalance(js_w1_before) node1_from_before = self.getBalance(js_w1_before, coin_from)
node1_from_after = self.getBalance(js_w1_after) node1_from_after = self.getBalance(js_w1_after, coin_from)
amount_from = float(format_amount(amt_swap, 8)) amount_from = float(format_amount(amt_swap, 8))
# TODO: Discard block rewards # TODO: Discard block rewards
# assert (node1_from_after - node1_from_before > (amount_from - 0.02)) # assert (node1_from_after - node1_from_before > (amount_from - 0.02))
@ -379,24 +417,38 @@ class BasicSwapTest(BaseTest):
wait_for_none_active(test_delay_event, 1800) wait_for_none_active(test_delay_event, 1800)
wait_for_none_active(test_delay_event, 1801) wait_for_none_active(test_delay_event, 1801)
def test_04_follower_recover_b_lock_tx(self): def test_03_follower_recover_a_lock_tx(self):
logging.info('---------- Test {} to XMR follower recovers coin b lock tx'.format(self.test_coin_from.name)) if not self.has_segwit:
return
self.do_test_03_follower_recover_a_lock_tx(self.test_coin_from, Coins.XMR)
def test_03_follower_recover_a_lock_tx_to_part(self):
if not self.has_segwit:
return
self.do_test_03_follower_recover_a_lock_tx(self.test_coin_from, Coins.PART)
def test_03_follower_recover_a_lock_tx_from_part(self):
self.do_test_03_follower_recover_a_lock_tx(Coins.PART, self.test_coin_from)
def do_test_04_follower_recover_b_lock_tx(self, coin_from, coin_to):
logging.info('---------- Test {} to {} follower recovers coin b lock tx'.format(coin_from.name, coin_to.name))
swap_clients = self.swap_clients swap_clients = self.swap_clients
ci_from = swap_clients[0].ci(coin_from)
ci_to = swap_clients[0].ci(coin_to)
js_w0_before = read_json_api(1800, 'wallets') js_w0_before = read_json_api(1800, 'wallets')
js_w1_before = read_json_api(1801, 'wallets') js_w1_before = read_json_api(1801, 'wallets')
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1)
rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
offer_id = swap_clients[0].postOffer( offer_id = swap_clients[0].postOffer(
self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32) lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32)
wait_for_offer(test_delay_event, swap_clients[1], offer_id) wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offer = swap_clients[1].getOffer(offer_id) offer = swap_clients[1].getOffer(offer_id)
bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED)
bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) bid, xmr_swap = swap_clients[0].getXmrBid(bid_id)
@ -411,28 +463,127 @@ class BasicSwapTest(BaseTest):
js_w0_after = read_json_api(1800, 'wallets') js_w0_after = read_json_api(1800, 'wallets')
js_w1_after = read_json_api(1801, 'wallets') js_w1_after = read_json_api(1801, 'wallets')
node0_from_before = self.getBalance(js_w0_before) node0_from_before = self.getBalance(js_w0_before, coin_from)
node0_from_after = self.getBalance(js_w0_after) node0_from_after = self.getBalance(js_w0_after, coin_from)
logging.info('End coin_from balance {}, diff {}'.format(node0_from_after, node0_from_after - node0_from_before))
# TODO: Discard block rewards # TODO: Discard block rewards
# assert (node0_from_before - node0_from_after < 0.02) # assert (node0_from_before - node0_from_after < 0.02)
node1_xmr_before = self.getXmrBalance(js_w1_before) node1_coin_to_before = self.getBalance(js_w1_before, coin_to)
node1_xmr_after = self.getXmrBalance(js_w1_after) node1_coin_to_after = self.getBalance(js_w1_after, coin_to)
assert (node1_xmr_before - node1_xmr_after < 0.02) logging.info('End coin_to balance {}, diff {}'.format(node1_coin_to_after, node1_coin_to_after - node1_coin_to_before))
assert (node1_coin_to_before - node1_coin_to_after < 0.02)
def test_04_follower_recover_b_lock_tx(self):
if not self.has_segwit:
return
self.do_test_04_follower_recover_b_lock_tx(self.test_coin_from, Coins.XMR)
def test_04_follower_recover_b_lock_tx_to_part(self):
if not self.has_segwit:
return
self.do_test_04_follower_recover_b_lock_tx(self.test_coin_from, Coins.PART)
def test_04_follower_recover_b_lock_tx_from_part(self):
self.do_test_04_follower_recover_b_lock_tx(Coins.PART, self.test_coin_from)
def do_test_05_self_bid(self, coin_from, coin_to):
logging.info('---------- Test {} to {} same client'.format(coin_from.name, coin_to.name))
def test_05_self_bid(self):
logging.info('---------- Test {} to XMR same client'.format(self.test_coin_from.name))
swap_clients = self.swap_clients swap_clients = self.swap_clients
ci_to = swap_clients[0].ci(coin_to)
amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1)
rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1)
offer_id = swap_clients[1].postOffer(self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, auto_accept_bids=True) offer_id = swap_clients[1].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, auto_accept_bids=True)
bid_id = swap_clients[1].postXmrBid(offer_id, amt_swap) bid_id = swap_clients[1].postXmrBid(offer_id, amt_swap)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=180)
def test_05_self_bid(self):
if not self.has_segwit:
return
self.do_test_05_self_bid(self.test_coin_from, Coins.XMR)
def test_05_self_bid_to_part(self):
if not self.has_segwit:
return
self.do_test_05_self_bid(self.test_coin_from, Coins.PART)
def test_05_self_bid_from_part(self):
self.do_test_05_self_bid(Coins.PART, self.test_coin_from)
def test_06_preselect_inputs(self):
tla_from = self.test_coin_from.name
logging.info('---------- Test {} Preselected inputs'.format(tla_from))
swap_clients = self.swap_clients
# Prepare balance
js_w2 = read_json_api(1802, 'wallets')
if float(js_w2[tla_from]['balance']) < 100.0:
post_json = {
'value': 100,
'address': js_w2[tla_from]['deposit_address'],
'subfee': False,
}
json_rv = read_json_api(1800, 'wallets/{}/withdraw'.format(tla_from.lower()), post_json)
assert (len(json_rv['txid']) == 64)
wait_for_balance(test_delay_event, 'http://127.0.0.1:1802/json/wallets/{}'.format(tla_from.lower()), 'balance', 100.0)
js_w2 = read_json_api(1802, 'wallets')
assert (float(js_w2[tla_from]['balance']) >= 100.0)
js_w2 = read_json_api(1802, 'wallets')
post_json = {
'value': float(js_w2[tla_from]['balance']),
'address': read_json_api(1802, 'wallets/{}/nextdepositaddr'.format(tla_from.lower())),
'subfee': True,
}
json_rv = read_json_api(1802, 'wallets/{}/withdraw'.format(tla_from.lower()), post_json)
wait_for_balance(test_delay_event, 'http://127.0.0.1:1802/json/wallets/{}'.format(tla_from.lower()), 'balance', 10.0)
assert (len(json_rv['txid']) == 64)
# Create prefunded ITX
ci = swap_clients[2].ci(self.test_coin_from)
ci_to = swap_clients[2].ci(Coins.XMR)
pi = swap_clients[2].pi(SwapTypes.XMR_SWAP)
js_w2 = read_json_api(1802, 'wallets')
swap_value = ci.make_int(js_w2[tla_from]['balance'])
assert (swap_value > ci.make_int(95))
itx = pi.getFundedInitiateTxTemplate(ci, swap_value, True)
itx_decoded = ci.describeTx(itx.hex())
value_after_subfee = ci.make_int(itx_decoded['vout'][0]['value'])
assert (value_after_subfee < swap_value)
swap_value = value_after_subfee
wait_for_unspent(test_delay_event, ci, swap_value)
extra_options = {'prefunded_itx': itx}
rate_swap = ci_to.make_int(random.uniform(0.2, 20.0))
offer_id = swap_clients[2].postOffer(self.test_coin_from, Coins.XMR, swap_value, rate_swap, swap_value, SwapTypes.XMR_SWAP, extra_options=extra_options)
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[2], bid_id, BidStates.BID_RECEIVED)
swap_clients[2].acceptBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[2], bid_id, BidStates.SWAP_COMPLETED, wait_for=120)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=120)
# Verify expected inputs were used
bid, _, _, _, _ = swap_clients[2].getXmrBidAndOffer(bid_id)
assert (bid.xmr_a_lock_tx)
wtx = ci.rpc_callback('gettransaction', [bid.xmr_a_lock_tx.txid.hex(),])
itx_after = ci.describeTx(wtx['hex'])
assert (len(itx_after['vin']) == len(itx_decoded['vin']))
for i, txin in enumerate(itx_decoded['vin']):
txin_after = itx_after['vin'][i]
assert (txin['txid'] == txin_after['txid'])
assert (txin['vout'] == txin_after['vout'])
class TestBTC(BasicSwapTest): class TestBTC(BasicSwapTest):
__test__ = True __test__ = True

View file

@ -318,6 +318,7 @@ class BaseTest(unittest.TestCase):
start_ltc_nodes = False start_ltc_nodes = False
start_xmr_nodes = True start_xmr_nodes = True
has_segwit = True
xmr_addr = None xmr_addr = None
btc_addr = None btc_addr = None