diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 7be311e..c1d54bb 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -3227,11 +3227,11 @@ class BasicSwap(BaseApp): self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_SEEN, '', session) if bid.xmr_b_lock_tx is None: 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_id=bid_id, tx_type=TxTypes.XMR_SWAP_B_LOCK, - txid=b_lock_tx_id, + txid=xmr_swap.b_lock_tx_id, chain_height=found_tx['height'], ) bid_changed = True @@ -3930,7 +3930,7 @@ class BasicSwap(BaseApp): raise ValueError('TODO') elif offer_data.swap_type == SwapTypes.XMR_SWAP: 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_signature) == 0, 'Unexpected data') ensure(len(offer_data.pkhash_seller) == 0, 'Unexpected data') @@ -4728,6 +4728,8 @@ class BasicSwap(BaseApp): 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) 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)) num_retries = self.countBidEvents(bid, EventLogTypes.FAILED_TX_B_LOCK_PUBLISH, session) if num_retries > 0: @@ -4751,6 +4753,7 @@ class BasicSwap(BaseApp): tx_type=TxTypes.XMR_SWAP_B_LOCK, txid=b_lock_tx_id, ) + xmr_swap.b_lock_tx_id = b_lock_tx_id bid.xmr_b_lock_tx.setState(TxStates.TX_NONE) self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_PUBLISHED, '', session) @@ -4867,8 +4870,11 @@ class BasicSwap(BaseApp): if coin_to == Coins.XMR: address_to = self.getCachedMainWalletAddress(ci_to) - else: + elif coin_to == Coins.PART_BLIND: 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) 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) @@ -4924,8 +4930,10 @@ class BasicSwap(BaseApp): try: if offer.coin_to == Coins.XMR: address_to = self.getCachedMainWalletAddress(ci_to) - else: + elif coin_to == Coins.PART_BLIND: 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) 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) diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index 85f1fed..7cc1e94 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -122,6 +122,8 @@ class TxTypes(IntEnum): XMR_SWAP_A_LOCK_REFUND_SPEND = auto() XMR_SWAP_A_LOCK_REFUND_SWIPE = auto() XMR_SWAP_B_LOCK = auto() + XMR_SWAP_B_LOCK_SPEND = auto() + XMR_SWAP_B_LOCK_REFUND = auto() ITX_PRE_FUNDED = auto() diff --git a/basicswap/chainparams.py b/basicswap/chainparams.py index 0d20bb7..4b495d9 100644 --- a/basicswap/chainparams.py +++ b/basicswap/chainparams.py @@ -296,7 +296,7 @@ chainparams = { 'blocks_target': 60 * 10, 'decimal_places': 8, 'has_csv': True, - 'has_segwit': True, + 'has_segwit': False, 'mainnet': { 'rpcport': 8888, 'pubkey_address': 82, @@ -353,20 +353,20 @@ class CoinInterface: self._unknown_wallet_seed = True 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) 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 return format_amount(amount_int, self.exp()) - def coin_name(self): + def coin_name(self) -> str: coin_chainparams = chainparams[self.coin_type()] if coin_chainparams.get('use_ticker_as_name', False): return coin_chainparams['ticker'] return coin_chainparams['name'].capitalize() - def ticker(self): + def ticker(self) -> str: ticker = chainparams[self.coin_type()]['ticker'] if self._network == 'testnet': ticker = 't' + ticker @@ -405,6 +405,9 @@ class CoinInterface: def chainparams_network(self): 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): if isinstance(ex, TemporaryError): return True diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index a96e246..2d80ef9 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -6,7 +6,6 @@ # file LICENSE or http://www.opensource.org/licenses/mit-license.php. import json -import time import base64 import hashlib import logging @@ -16,7 +15,6 @@ from io import BytesIO from basicswap.contrib.test_framework import segwit_addr from basicswap.util import ( - dumpj, ensure, make_int, b2h, i2b, b2i, i2h) @@ -55,7 +53,8 @@ from basicswap.contrib.test_framework.messages import ( CTxIn, CTxInWitness, CTxOut, - FromHex) + FromHex, + uint256_from_str) from basicswap.contrib.test_framework.script import ( CScript, CScriptOp, @@ -86,14 +85,14 @@ def ensure_op(v, err_string='Bad opcode'): ensure(v, err_string) -def findOutput(tx, script_pk): +def findOutput(tx, script_pk: bytes): for i in range(len(tx.vout)): if tx.vout[i].scriptPubKey == script_pk: return i 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 given address. Raises runtime error exception if not found. @@ -139,7 +138,7 @@ class BTCInterface(CoinInterface): return 2 @staticmethod - def getTxOutputValue(tx): + def getTxOutputValue(tx) -> int: rv = 0 for output in tx.vout: rv += output.nValue @@ -158,7 +157,7 @@ class BTCInterface(CoinInterface): return CTxOut @staticmethod - def getExpectedSequence(lockType, lockVal): + def getExpectedSequence(lockType: int, lockVal: int) -> int: assert (lockVal >= 1), 'Bad lockVal' if lockType == TxLockTypes.SEQUENCE_LOCK_BLOCKS: return lockVal @@ -172,12 +171,16 @@ class BTCInterface(CoinInterface): raise ValueError('Unknown lock type') @staticmethod - def decodeSequence(lock_value): + def decodeSequence(lock_value: int) -> int: # Return the raw value if lock_value & SEQUENCE_LOCKTIME_TYPE_FLAG: return (lock_value & SEQUENCE_LOCKTIME_MASK) << SEQUENCE_LOCKTIME_GRANULARITY return lock_value & SEQUENCE_LOCKTIME_MASK + @staticmethod + def depth_spendable() -> int: + return 0 + def __init__(self, coin_settings, network, swap_client=None): super().__init__(network) 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._expect_seedid_hex = None - def using_segwit(self): + def using_segwit(self) -> bool: + # Using btc native segwit return self._use_segwit def get_connection_type(self): @@ -217,15 +221,12 @@ class BTCInterface(CoinInterface): def close_rpc(self, rpc_conn): 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') self._conf_target = new_conf_target - def testDaemonRPC(self, with_wallet=True): - if with_wallet: - self.rpc_callback('getwalletinfo', []) - else: - self.rpc_callback('getblockchaininfo', []) + def testDaemonRPC(self, with_wallet=True) -> None: + self.rpc_callback('getwalletinfo' if with_wallet else 'getblockchaininfo') def getDaemonVersion(self): return self.rpc_callback('getnetworkinfo')['version'] @@ -233,7 +234,7 @@ class BTCInterface(CoinInterface): def getBlockchainInfo(self): return self.rpc_callback('getblockchaininfo') - def getChainHeight(self): + def getChainHeight(self) -> int: return self.rpc_callback('getblockcount') def getMempoolTx(self, txid): @@ -259,7 +260,7 @@ class BTCInterface(CoinInterface): last_block_header = prev_block_header 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) self.rpc_callback('sethdseed', [True, key_wif]) @@ -270,10 +271,10 @@ class BTCInterface(CoinInterface): rv['locked'] = rv.get('unlocked_until', 1) <= 0 return rv - def walletRestoreHeight(self): + def walletRestoreHeight(self) -> int: return self._restore_height - def getWalletRestoreHeight(self): + def getWalletRestoreHeight(self) -> int: start_time = self.rpc_callback('getwalletinfo')['keypoololdest'] blockchaininfo = self.rpc_callback('getblockchaininfo') @@ -295,6 +296,7 @@ class BTCInterface(CoinInterface): block_hash = block_header['previousblockhash'] finally: self.close_rpc(rpc_conn) + raise ValueError('{} wallet restore height not found.'.format(self.coin_name())) def getWalletSeedID(self) -> str: return self.rpc_callback('getwalletinfo')['hdseedid'] @@ -309,9 +311,11 @@ class BTCInterface(CoinInterface): args.append('bech32') 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]) - return addr_info['ismine'] + if not or_watch_only: + return addr_info['ismine'] + return addr_info['ismine'] or addr_info['iswatchonly'] def checkAddressMine(self, address: str) -> None: addr_info = self.rpc_callback('getaddressinfo', [address]) @@ -331,22 +335,22 @@ class BTCInterface(CoinInterface): except Exception: 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') - def decodeAddress(self, address): + def decodeAddress(self, address: str) -> bytes: bech32_prefix = self.chainparams_network()['hrp'] if len(bech32_prefix) > 0 and address.startswith(bech32_prefix + '1'): return bytes(segwit_addr.decode(bech32_prefix, 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'] version = 0 pkh = hash160(pk) 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) assert (len(pkh) == 20) prefix = self.chainparams_network()['pubkey_address'] @@ -354,26 +358,26 @@ class BTCInterface(CoinInterface): checksum = hashlib.sha256(hashlib.sha256(data).digest()).digest() return b58encode(data + checksum[0:4]) - def sh_to_address(self, sh): + def sh_to_address(self, sh: bytes) -> str: assert (len(sh) == 20) prefix = self.chainparams_network()['script_address'] data = bytes((prefix,)) + sh checksum = hashlib.sha256(hashlib.sha256(data).digest()).digest() return b58encode(data + checksum[0:4]) - def encode_p2wsh(self, script): + def encode_p2wsh(self, script: bytes) -> str: bech32_prefix = self.chainparams_network()['hrp'] version = 0 program = script[2:] # strip version and length return segwit_addr.encode(bech32_prefix, version, program) - def encodeScriptDest(self, script): + def encodeScriptDest(self, script: bytes) -> str: 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) - def pubkey_to_address(self, pk): + def pubkey_to_address(self, pk: bytes) -> str: assert (len(pk) == 33) return self.pkh_to_address(hash160(pk)) @@ -383,31 +387,31 @@ class BTCInterface(CoinInterface): def getPubkey(self, privkey): return PublicKey.from_secret(privkey).format() - def getAddressHashFromKey(self, key): + def getAddressHashFromKey(self, key) -> bytes: pk = self.getPubkey(key) return hash160(pk) def getSeedHash(self, seed): return self.getAddressHashFromKey(seed)[::-1] - def verifyKey(self, k): + def verifyKey(self, k: bytes) -> bool: i = b2i(k) 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) - def encodeKey(self, key_bytes): + def encodeKey(self, key_bytes: bytes) -> str: wif_prefix = self.chainparams_network()['key_prefix'] return toWIF(wif_prefix, key_bytes) - def encodePubkey(self, pk): + def encodePubkey(self, pk: bytes) -> bytes: 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) - def decodeSegwitAddress(self, addr): + def decodeSegwitAddress(self, addr: str) -> bytes: return bytes(segwit_addr.decode(self.chainparams_network()['hrp'], addr)[1]) def decodePubkey(self, pke): @@ -423,10 +427,10 @@ class BTCInterface(CoinInterface): def sumPubkeys(self, Ka, Kb): return PublicKey.combine_keys([PublicKey(Ka), PublicKey(Kb)]).format() - def getScriptForPubkeyHash(self, pkh): + def getScriptForPubkeyHash(self, pkh: bytes) -> CScript: return CScript([OP_0, pkh]) - def extractScriptLockScriptValues(self, script_bytes): + def extractScriptLockScriptValues(self, script_bytes: bytes): script_len = len(script_bytes) ensure(script_len == 71, 'Bad script length') o = 0 @@ -444,7 +448,7 @@ class BTCInterface(CoinInterface): 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.nVersion = self.txVersion() 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}) self.rpc_callback('lockunspent', [True, inputs]) - def signTxWithWallet(self, tx): + def signTxWithWallet(self, tx: bytes) -> bytes: rv = self.rpc_callback('signrawtransactionwithwallet', [tx.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()]) - def encodeTx(self, tx): + def encodeTx(self, tx) -> bytes: return tx.serialize() - def loadTx(self, tx_bytes) -> CTransaction: + def loadTx(self, tx_bytes: bytes) -> CTransaction: # Load tx from bytes to internal representation tx = CTransaction() tx.deserialize(BytesIO(tx_bytes)) return tx - def getTxid(self, tx): + def getTxid(self, tx) -> bytes: if isinstance(tx, str): tx = bytes.fromhex(tx) if isinstance(tx, bytes): @@ -928,16 +937,16 @@ class BTCInterface(CoinInterface): script_pk = self.getScriptDest(script) return findOutput(tx, script_pk) - def getPubkeyHash(self, K): - return hash160(self.encodePubkey(K)) + def getPubkeyHash(self, K: bytes) -> bytes: + return hash160(K) def getScriptDest(self, script): return CScript([OP_0, hashlib.sha256(script).digest()]) - def getScriptScriptSig(self, script): + def getScriptScriptSig(self, script: bytes) -> bytes: return bytes() - def getPkDest(self, K): + def getPkDest(self, K: bytes) -> bytearray: return self.getScriptForPubkeyHash(self.getPubkeyHash(K)) def scanTxOutset(self, dest): @@ -980,26 +989,26 @@ class BTCInterface(CoinInterface): def createBLockTx(self, Kbs, output_amount): tx = CTransaction() tx.nVersion = self.txVersion() - p2wpkh = self.getPkDest(Kbs) - tx.vout.append(self.txoType()(output_amount, p2wpkh)) + p2wpkh_script_pk = self.getPkDest(Kbs) + tx.vout.append(self.txoType()(output_amount, p2wpkh_script_pk)) return tx.serialize() def encodeSharedAddress(self, Kbv, 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.fundTx(b_lock_tx, feerate) b_lock_tx_id = self.getTxid(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): 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() len_full = len(tx.serialize_with_witness()) + add_bytes + add_witness_bytes len_nwit = len(tx.serialize_without_witness()) + add_bytes @@ -1007,10 +1016,13 @@ class BTCInterface(CoinInterface): return (weight + wsf - 1) // wsf 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) rv = self.scanTxOutset(raw_dest) - print('scanTxOutset', dumpj(rv)) for utxo in rv['unspents']: if 'height' in utxo and utxo['height'] > 0 and rv['height'] - utxo['height'] > cb_block_confirmed: @@ -1019,31 +1031,45 @@ class BTCInterface(CoinInterface): else: return {'txid': utxo['txid'], 'vout': utxo['vout'], 'amount': utxo['amount'], 'height': utxo['height']} return None + ''' - def waitForLockTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed): - raw_dest = self.getPkDest(Kbs) + 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: + wtx = self.rpc_callback('gettransaction', [chain_b_lock_txid.hex(), ]) + lock_tx = self.loadTx(bytes.fromhex(wtx['hex'])) - for i in range(20): - time.sleep(1) - rv = self.scanTxOutset(raw_dest) - print('scanTxOutset', dumpj(rv)) + Kbs = self.getPubkey(kbs) + script_pk = self.getPkDest(Kbs) + locked_n = findOutput(lock_tx, script_pk) + ensure(locked_n is not None, 'Output not found in tx') + pkh_to = self.decodeAddress(address_to) - for utxo in rv['unspents']: - if 'height' in utxo and utxo['height'] > 0 and rv['height'] - utxo['height'] > cb_block_confirmed: + tx = CTransaction() + tx.nVersion = self.txVersion() - if self.make_int(utxo['amount']) != cb_swap_value: - self._log.warning('Found output to lock tx pubkey of incorrect value: %s', str(utxo['amount'])) - else: - return True - return False + script_lock = self.getScriptForPubkeyHash(Kbs) + chain_b_lock_txid_int = uint256_from_str(chain_b_lock_txid[::-1]) - def spendBLockTx(self, chain_b_lock_txid, address_to, kbv, kbs, cb_swap_value, b_fee, restore_height): - raise ValueError('TODO') + tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n), + 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]) - def isWatchOnlyAddress(self, address): + def isWatchOnlyAddress(self, address: str): addr_info = self.rpc_callback('getaddressinfo', [address]) return addr_info['iswatchonly'] @@ -1051,10 +1077,10 @@ class BTCInterface(CoinInterface): lock_tx_dest = self.getScriptDest(lock_script) 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 - if not self.isWatchOnlyAddress(dest_address): + if not self.isAddressMine(dest_address, or_watch_only=True): self.importWatchOnlyAddress(dest_address, 'bid') self._log.info('Imported watch-only addr: {}'.format(dest_address)) self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from)) @@ -1263,7 +1289,6 @@ class BTCInterface(CoinInterface): sign_for_addr = None for addr, value in unspent_addr.items(): - print('[rm]', value, amount_for) if value >= amount_for: sign_for_addr = addr break @@ -1272,7 +1297,7 @@ class BTCInterface(CoinInterface): 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 pkh = self.decodeAddress(sign_for_addr) 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) ensure(passed is True, 'Proof of funds signature invalid') - if self._use_segwit: + if self.using_segwit(): address = self.encodeSegwitAddress(decodeAddress(address)[1:]) return self.getUTXOBalance(address) @@ -1334,6 +1359,19 @@ class BTCInterface(CoinInterface): def get_p2wsh_script_pubkey(self, script: bytearray) -> bytearray: 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(): print('TODO: testBTCInterface') diff --git a/basicswap/interface/firo.py b/basicswap/interface/firo.py index e14bf48..24468c7 100644 --- a/basicswap/interface/firo.py +++ b/basicswap/interface/firo.py @@ -55,15 +55,17 @@ class FIROInterface(BTCInterface): addr_info = self.rpc_callback('validateaddress', [address]) 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]) - return addr_info['ismine'] + if not or_watch_only: + return addr_info['ismine'] + return addr_info['ismine'] or addr_info['iswatchonly'] def getSCLockScriptAddress(self, lock_script): lock_tx_dest = self.getScriptDest(lock_script) 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 ro = self.rpc_callback('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True]) addr_info = self.rpc_callback('validateaddress', [address]) @@ -74,7 +76,7 @@ class FIROInterface(BTCInterface): # Add watchonly address and rescan if required lock_tx_dest = self.getScriptDest(lock_script) 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._log.info('Imported watch-only addr: {}'.format(dest_address)) self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from)) diff --git a/basicswap/interface/part.py b/basicswap/interface/part.py index bc542e1..d389f09 100644 --- a/basicswap/interface/part.py +++ b/basicswap/interface/part.py @@ -638,7 +638,7 @@ class PARTInterfaceAnon(PARTInterface): def coin_name(self): 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) self._log.debug('sx_addr: {}'.format(sx_addr)) diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index f6fcc20..1a0da16 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -260,7 +260,7 @@ class XMRInterface(CoinInterface): def encodeSharedAddress(self, 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: self.openWallet(self._wallet_filename) @@ -339,67 +339,6 @@ class XMRInterface(CoinInterface): rv = -1 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): with self._mx_wallet: self.openWallet(self._wallet_filename) diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py index 666c223..8deb64c 100644 --- a/tests/basicswap/test_btc_xmr.py +++ b/tests/basicswap/test_btc_xmr.py @@ -28,6 +28,8 @@ from tests.basicswap.util import ( from tests.basicswap.common import ( wait_for_bid, wait_for_offer, + wait_for_balance, + wait_for_unspent, wait_for_none_active, BTC_BASE_RPC_PORT, ) @@ -52,8 +54,8 @@ logger = logging.getLogger() class BasicSwapTest(BaseTest): base_rpc_port = None - def getBalance(self, js_wallets): - return float(js_wallets[self.test_coin_from.name]['balance']) + float(js_wallets[self.test_coin_from.name]['unconfirmed']) + def getBalance(self, js_wallets, coin): + return float(js_wallets[coin.name]['balance']) + float(js_wallets[coin.name]['unconfirmed']) def callnoderpc(self, method, params=[], wallet=None, node_id=0): return callnoderpc(node_id, method, params, wallet, self.base_rpc_port) @@ -252,22 +254,25 @@ class BasicSwapTest(BaseTest): self.callnoderpc('unloadwallet', [new_wallet_name]) assert (addr == 'bcrt1qps7hnjd866e9ynxadgseprkc2l56m00dvwargr') - def test_01_full_swap(self): - logging.info('---------- Test {} to XMR'.format(self.test_coin_from.name)) + def do_test_01_full_swap(self, coin_from, coin_to): + logging.info('---------- Test {} to {}'.format(coin_from.name, coin_to.name)) + 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') - node0_from_before = self.getBalance(js_0) + node0_from_before = self.getBalance(js_0, coin_from) 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_1_xmr = read_json_api(1801, 'wallets/xmr') + js_0_to = read_json_api(1800, 'wallets/{}'.format(coin_to.name.lower())) + 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) - rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) - offer_id = swap_clients[0].postOffer(self.test_coin_from, Coins.XMR, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP) + amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1) + rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1) + 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) offers = swap_clients[0].listOffers(filters={'offer_id': offer_id}) 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[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') - node1_from_after = self.getBalance(js_1) - assert (node1_from_after > node1_from_before + (amount_from - 0.05)) + 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)) 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 # assert (node0_from_after < node0_from_before - amount_from) - js_0_xmr_after = read_json_api(1800, 'wallets/xmr') - js_1_xmr_after = read_json_api(1801, 'wallets/xmr') + js_0_to_after = read_json_api(1800, 'wallets/{}'.format(coin_to.name.lower())) + js_1_to_after = read_json_api(1801, 'wallets/{}'.format(coin_to.name.lower())) scale_from = 8 amount_to = int((amt_swap * rate_swap) // (10 ** scale_from)) - amount_to_float = float(format_amount(amount_to, 12)) - node1_xmr_after = float(js_1_xmr_after['unconfirmed']) + float(js_1_xmr_after['balance']) - node1_xmr_before = float(js_1_xmr['unconfirmed']) + float(js_1_xmr['balance']) - assert (node1_xmr_after > node1_xmr_before + (amount_to_float - 0.02)) + amount_to_float = float(ci_to.format_amount(amount_to)) + node1_to_after = float(js_1_to_after['unconfirmed']) + float(js_1_to_after['balance']) + node1_to_before = float(js_1_to['unconfirmed']) + float(js_1_to['balance']) + 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 + ci_from = swap_clients[0].ci(coin_from) + ci_to = swap_clients[0].ci(coin_to) 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) - rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, r=1) + amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), 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, + coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32) wait_for_offer(test_delay_event, swap_clients[1], 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) 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 # assert (node0_from_before - node0_from_after < 0.02) - def test_03_follower_recover_a_lock_tx(self): - logging.info('---------- Test {} to XMR follower recovers coin a lock tx'.format(self.test_coin_from.name)) + def test_02_leader_recover_a_lock_tx(self): + 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 + 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_w1_before = read_json_api(1801, 'wallets') - 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) + amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), 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, + coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32) wait_for_offer(test_delay_event, swap_clients[1], offer_id) offer = swap_clients[1].getOffer(offer_id) 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) bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) @@ -368,8 +406,8 @@ class BasicSwapTest(BaseTest): js_w1_after = read_json_api(1801, 'wallets') - node1_from_before = self.getBalance(js_w1_before) - node1_from_after = self.getBalance(js_w1_after) + node1_from_before = self.getBalance(js_w1_before, coin_from) + node1_from_after = self.getBalance(js_w1_after, coin_from) amount_from = float(format_amount(amt_swap, 8)) # TODO: Discard block rewards # 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, 1801) - def test_04_follower_recover_b_lock_tx(self): - logging.info('---------- Test {} to XMR follower recovers coin b lock tx'.format(self.test_coin_from.name)) + def test_03_follower_recover_a_lock_tx(self): + 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 + 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_w1_before = read_json_api(1801, 'wallets') - 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) + amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), 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, + coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32) wait_for_offer(test_delay_event, swap_clients[1], offer_id) offer = swap_clients[1].getOffer(offer_id) 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) 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_w1_after = read_json_api(1801, 'wallets') - node0_from_before = self.getBalance(js_w0_before) - node0_from_after = self.getBalance(js_w0_after) - + node0_from_before = self.getBalance(js_w0_before, coin_from) + 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 # assert (node0_from_before - node0_from_after < 0.02) - node1_xmr_before = self.getXmrBalance(js_w1_before) - node1_xmr_after = self.getXmrBalance(js_w1_after) - assert (node1_xmr_before - node1_xmr_after < 0.02) + node1_coin_to_before = self.getBalance(js_w1_before, coin_to) + node1_coin_to_after = self.getBalance(js_w1_after, coin_to) + 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 + ci_to = swap_clients[0].ci(coin_to) 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) 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): __test__ = True diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index e43c38c..d4af781 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -318,6 +318,7 @@ class BaseTest(unittest.TestCase): start_ltc_nodes = False start_xmr_nodes = True + has_segwit = True xmr_addr = None btc_addr = None