diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15871d4..392e1a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,7 +92,7 @@ jobs: export PARTICL_BINDIR="$BIN_DIR/particl" export BITCOIN_BINDIR="$BIN_DIR/bitcoin" export XMR_BINDIR="$BIN_DIR/monero" - pytest tests/basicswap/test_btc_xmr.py::TestBTC -k "test_003_api or test_02_a_leader_recover_a_lock_tx or test_11_fee_validation" + pytest tests/basicswap/test_btc_xmr.py::TestBTC -k "test_003_api or test_02_a_leader_recover_a_lock_tx or test_03_a_follower_recover_a_lock_tx or test_11_fee_validation" - name: Run test_encrypted_xmr_reload id: test_encrypted_xmr_reload run: | diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index d648ec0..003f6e0 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -5552,8 +5552,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): use_cursor = self.openDB(cursor) bid, offer = self.getBidAndOffer(bid_id, use_cursor) - ensure(bid, "Bid not found") - ensure(offer, "Offer not found") + ensure(bid, f"Bid not found: {self.log.id(bid_id)}.") + ensure(offer, f"Offer not found: {self.log.id(bid.offer_id)}.") # Ensure bid is still valid now: int = self.getTime() @@ -6828,8 +6828,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): try: use_cursor = self.openDB(cursor) bid, offer = self.getBidAndOffer(bid_id, use_cursor, with_txns=False) - ensure(bid, "Bid not found") - ensure(offer, "Offer not found") + ensure(bid, f"Bid not found: {self.log.id(bid_id)}.") + ensure(offer, f"Offer not found: {self.log.id(bid.offer_id)}.") bid.setState(new_state) self.deactivateBid(use_cursor, offer, bid) @@ -7747,7 +7747,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): self.logBidEvent( bid.bid_id, EventLogTypes.DEBUG_TWEAK_APPLIED, - "ind {}".format(bid.debug_ind), + f"ind {bid.debug_ind}", cursor, ) self.commitDB() @@ -7802,6 +7802,23 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): self.saveBidInSession(bid_id, bid, cursor, xmr_swap) self.commitDB() + if refund_tx.block_height is None: + self.log.debug( + f"A_LOCK_REFUND tx: {self.logIDT(refund_tx.txid)} block height not known, bid: {self.log.id(bid_id)}" + ) + refund_tx_info = ci_from.getTxOutInfo( + refund_tx.txid, refund_tx.vout + ) + if refund_tx_info: + refund_tx.block_hash = refund_tx_info["block_hash"] + refund_tx.block_height = refund_tx_info["block_height"] + refund_tx.block_time = refund_tx_info["block_time"] + self.log.debug( + f"Found A_LOCK_REFUND tx block height: {refund_tx.block_height}, time: {refund_tx.block_time}" + ) + self.add(refund_tx, cursor, upsert=True) + self.commitDB() + if ( TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE not in bid.txns and refund_tx.block_height is not None @@ -7947,10 +7964,14 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): "", cursor, ) + refund_vout: int = ci_from.getLockRefundVout( + xmr_swap.a_lock_refund_tx, xmr_swap.vkbv + ) bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx( bid_id=bid_id, tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, txid=bytes.fromhex(txid), + vout=refund_vout, ) self.saveBidInSession(bid_id, bid, cursor, xmr_swap) self.commitDB() @@ -7962,10 +7983,14 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): ) txid = ci_from.getTxid(xmr_swap.a_lock_refund_tx) if TxTypes.XMR_SWAP_A_LOCK_REFUND not in bid.txns: + refund_vout: int = ci_from.getLockRefundVout( + xmr_swap.a_lock_refund_tx, xmr_swap.vkbv + ) bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx( bid_id=bid_id, tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, txid=txid, + vout=refund_vout, ) self.saveBidInSession(bid_id, bid, cursor, xmr_swap) self.commitDB() @@ -8346,14 +8371,15 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): return rv def _isScriptRefundMature(self, ci, offer, refund_tx_bytes, parent_tx) -> bool: - refund_tx = ci.loadTx(refund_tx_bytes) if offer.lock_type in (TxLockTypes.ABS_LOCK_BLOCKS, TxLockTypes.ABS_LOCK_TIME): - return ci.isAbsLockTimeMature(refund_tx.nLockTime) + tx_locktime: int = ci.getTxLocktime(refund_tx_bytes) + return ci.isAbsLockTimeMature(tx_locktime) if parent_tx is None or parent_tx.block_height is None: return False + txi_sequence: int = ci.getTxInSequence(refund_tx_bytes, 0) return ci.isCsvLockMature( offer.lock_type, - refund_tx.vin[0].nSequence, + txi_sequence, parent_tx.block_height, parent_tx.block_time, ) @@ -8641,12 +8667,33 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): f"Error trying to submit initiate refund txn: {ex}" ) - if ( + should_try_refund_ptx: bool = ( bid.getPTxState() in (TxStates.TX_SENT, TxStates.TX_CONFIRMED) and bid.participate_txn_refund is not None - and self._isScriptRefundMature( - ci_to, offer, bid.participate_txn_refund, bid.participate_tx + ) + if ( + should_try_refund_ptx + and bid.participate_tx is not None + and bid.participate_tx.block_height is None + ): + self.log.debug( + f"PTX: {self.logIDT(bid.participate_tx.txid)} block height not known, bid: {self.log.id(bid_id)}" ) + # An invalid ptx, won't be confirmed, check block height here + ptx_info = ci_to.getTxOutInfo( + bid.participate_tx.txid, bid.participate_tx.vout + ) + if ptx_info: + bid.participate_tx.block_hash = ptx_info["block_hash"] + bid.participate_tx.block_height = ptx_info["block_height"] + bid.participate_tx.block_time = ptx_info["block_time"] + self.log.debug( + f"Found PTX block height: {bid.participate_tx.block_height}, time: {bid.participate_tx.block_time}" + ) + self.saveBid(bid_id, bid) + + if should_try_refund_ptx and self._isScriptRefundMature( + ci_to, offer, bid.participate_txn_refund, bid.participate_tx ): try: txid = ci_to.publishTx(bid.participate_txn_refund) @@ -9017,10 +9064,14 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): ) if TxTypes.XMR_SWAP_A_LOCK_REFUND not in bid.txns: + refund_vout: int = ci_from.getLockRefundVout( + bytes.fromhex(spend_txn_hex), xmr_swap.vkbv + ) bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx( bid_id=bid.bid_id, tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, txid=xmr_swap.a_lock_refund_tx_id, + vout=refund_vout, ) else: self.setBidError( @@ -9140,6 +9191,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): if was_received: if self.isBchXmrSwap(offer): # Mercy tx is sent separately + # Can't set XMR_SWAP_FAILED_SWIPED, as bid should continue looking for mercy tx pass else: # Look for a mercy output @@ -11218,7 +11270,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): refundExtraArgs = dict() lockExtraArgs = dict() if self.isBchXmrSwap(offer): - # perform check that both lock and refund transactions have their outs pointing to correct follower address + # Perform check that both lock and refund transactions have their outs pointing to correct follower address # and prepare extra args for validation bch_ci = self.ci(Coins.BCH) @@ -12975,8 +13027,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): self.log.info(f"Route established for bid {self.log.id(bid_id)}") bid, offer = self.getBidAndOffer(bid_id, cursor) - ensure(bid, "Bid not found") - ensure(offer, "Offer not found") + ensure(bid, f"Bid not found: {self.log.id(bid_id)}.") + ensure(offer, f"Offer not found: {self.log.id(bid.offer_id)}.") coin_from = Coins(offer.coin_from) coin_to = Coins(offer.coin_to) @@ -14082,9 +14134,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): walletinfo = ci.getWalletInfo() rv = { "deposit_address": self.getCachedAddressForCoin(coin), - "balance": ci.format_amount(walletinfo["balance"], conv_int=True), + "balance": ci.format_amount(walletinfo["balance"], conv_int=True, r=-1), "unconfirmed": ci.format_amount( - walletinfo["unconfirmed_balance"], conv_int=True + walletinfo["unconfirmed_balance"], conv_int=True, r=-1 ), "expected_seed": ci.knownWalletSeed(), "encrypted": walletinfo["encrypted"], @@ -14099,7 +14151,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): if "immature_balance" in walletinfo: rv["immature"] = ci.format_amount( - walletinfo["immature_balance"], conv_int=True + walletinfo["immature_balance"], conv_int=True, r=-1 ) if "locked_utxos" in walletinfo: @@ -15090,8 +15142,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): return bid = self.getBid(bid_id) - if bid is None: - raise ValueError("Bid not found.") + ensure(bid, f"Bid not found: {self.log.id(bid_id)}.") bid.debug_ind = debug_ind diff --git a/basicswap/interface/base.py b/basicswap/interface/base.py index 092d918..26747aa 100644 --- a/basicswap/interface/base.py +++ b/basicswap/interface/base.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2024 tecnovert @@ -193,6 +192,9 @@ class AdaptorSigInterface: def getScriptLockRefundSwipeTxDummyWitness(self, script: bytes) -> List[bytes]: return [bytes(72), b"", bytes(len(script))] + def getLockRefundVout(self, lock_refund_tx_data: bytes, vbkv: bytes): + return 0 + class Secp256k1Interface(CoinInterface, AdaptorSigInterface): def __init__(self, **kwargs): diff --git a/basicswap/interface/bch.py b/basicswap/interface/bch.py index 91f9beb..b81e1b1 100644 --- a/basicswap/interface/bch.py +++ b/basicswap/interface/bch.py @@ -147,7 +147,9 @@ class BCHInterface(BTCInterface): if not self.isAddressMine(address, or_watch_only=True): # Expects P2WSH nested in BIP16_P2SH - self.rpc("importaddress", [lock_tx_dest.hex(), "bid lock", False, True]) + self.rpc_wallet( + "importaddress", [lock_tx_dest.hex(), "bid lock", False, True] + ) return address @@ -156,15 +158,23 @@ class BCHInterface(BTCInterface): def createRawFundedTransaction( self, - addr_to: str, + addr_to: str | bytes, amount: int, sub_fee: bool = False, lock_unspents: bool = True, feerate: int = None, ) -> str: - txn = self.rpc( - "createrawtransaction", [[], {addr_to: self.format_amount(amount)}] - ) + + if isinstance(addr_to, bytes): + # addr_to is script_pubkey + tx = CTransaction() + tx.nVersion = self.txVersion() + tx.vout.append(self.txoType()(amount, addr_to)) + txn = tx.serialize_without_witness().hex() + else: + txn = self.rpc( + "createrawtransaction", [[], {addr_to: self.format_amount(amount)}] + ) if feerate: fee_rate = self.format_amount(feerate) @@ -228,6 +238,16 @@ class BCHInterface(BTCInterface): ) return pay_fee + def getBLockTxo( + self, + chain_b_lock_txid: bytes, + lock_tx_vout: int, + script_pk: bytes, + ) -> (int, int): + txout = self.rpc("gettxout", [chain_b_lock_txid.hex(), lock_tx_vout, True]) + actual_value = self.make_int(txout["value"]) + return lock_tx_vout, actual_value + def findTxnByHash(self, txid_hex: str): # Only works for wallet txns try: @@ -282,7 +302,7 @@ class BCHInterface(BTCInterface): found_vout = try_vout break except Exception as e: # noqa: F841 - # self._log.warning('gettxout {}'.format(e)) + # self._log.warning(f"gettxout {e}") return None if found_vout is None: @@ -295,7 +315,7 @@ class BCHInterface(BTCInterface): # TODO: Better way? if confirmations > 0: - block_height = self.getChainHeight() - confirmations + block_height = self.getChainHeight() - (confirmations - 1) rv = { "txid": txid.hex(), @@ -516,6 +536,7 @@ class BCHInterface(BTCInterface): tx_lock = self.loadTx(tx_lock_bytes) output_script = self.getScriptDest(script_lock) + locked_n = findOutput(tx_lock, output_script) ensure(locked_n is not None, "Output not found in tx") locked_coin = tx_lock.vout[locked_n].nValue @@ -1134,7 +1155,7 @@ class BCHInterface(BTCInterface): refund_output_value = refund_swipe_tx.vout[0].nValue refund_output_script = refund_swipe_tx.vout[0].scriptPubKey - # mercy transaction size consisting of one input of freshly received funds, + # Mercy transaction size consisting of one input of freshly received funds, # one op_return with mercy information, a dust output to the leader and change back to the follower tx_size = 275 dust_limit = 546 diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 8cf1ed8..da62769 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2020-2024 tecnovert @@ -508,6 +507,14 @@ class BTCInterface(FeeValidator, Secp256k1Interface): return height return self.rpc("getblockcount") + def getTxLocktime(self, tx_data: bytes) -> int: + tx_obj = self.loadTx(tx_data) + return tx_obj.nLockTime + + def getTxInSequence(self, tx_data: bytes, vout: int) -> int: + tx_obj = self.loadTx(tx_data) + return tx_obj.vin[vout].nSequence + def getChainMedianTime(self) -> int: if self.useBackend(): import struct @@ -601,7 +608,7 @@ class BTCInterface(FeeValidator, Secp256k1Interface): block_hash = sha256(sha256(header_bytes))[::-1].hex() return {"height": height, "hash": block_hash, "time": block_time} - def getBlockHeader(self, block_hash): + def getBlockHeader(self, block_hash: str) -> dict: if self._connection_type == "electrum": raise NotImplementedError( "getBlockHeader by hash not available in electrum mode" @@ -2785,6 +2792,47 @@ class BTCInterface(FeeValidator, Secp256k1Interface): ) return pay_fee + def getBLockTxo( + self, + chain_b_lock_txid: bytes, + lock_tx_vout: int, + script_pk: bytes, + ) -> (int, int): + if self.useBackend(): + backend = self.getBackend() + tx_hex = backend.getTransactionRaw(chain_b_lock_txid.hex()) + if tx_hex: + lock_tx = self.loadTx(bytes.fromhex(tx_hex)) + locked_n = findOutput(lock_tx, script_pk) + if locked_n is not None: + actual_value = lock_tx.vout[locked_n].nValue + else: + self._log.error( + f"spendBLockTx: Output not found in tx {self._log.id(chain_b_lock_txid)}, " + f"script_pk={script_pk.hex()}, num_outputs={len(lock_tx.vout)}" + ) + for i, out in enumerate(lock_tx.vout): + self._log.debug( + f" vout[{i}]: value={out.nValue}, scriptPubKey={out.scriptPubKey.hex()}" + ) + else: + self._log.warning( + f"spendBLockTx: Failed to fetch tx {self._log.id(chain_b_lock_txid)} from backend" + ) + locked_n = lock_tx_vout + return locked_n, actual_value + wtx = self.rpc_wallet_watch( + "gettransaction", + [ + chain_b_lock_txid.hex(), + ], + ) + lock_tx = self.loadTx(bytes.fromhex(wtx["hex"])) + locked_n = findOutput(lock_tx, script_pk) + if locked_n is not None: + actual_value = lock_tx.vout[locked_n].nValue + return locked_n, actual_value + def spendBLockTx( self, chain_b_lock_txid: bytes, @@ -2798,48 +2846,14 @@ class BTCInterface(FeeValidator, Secp256k1Interface): lock_tx_vout=None, ) -> bytes: self._log.info( - "spendBLockTx: {} {}\n".format( - self._log.id(chain_b_lock_txid), lock_tx_vout - ) + f"spendBLockTx: {self._log.id(chain_b_lock_txid)} {lock_tx_vout}\n" ) Kbs = self.getPubkey(kbs) script_pk = self.getPkDest(Kbs) - locked_n = None - actual_value = None - if self.useBackend(): - backend = self.getBackend() - tx_hex = backend.getTransactionRaw(chain_b_lock_txid.hex()) - if tx_hex: - lock_tx = self.loadTx(bytes.fromhex(tx_hex)) - locked_n = findOutput(lock_tx, script_pk) - if locked_n is not None: - actual_value = lock_tx.vout[locked_n].nValue - else: - self._log.error( - f"spendBLockTx: Output not found in tx {chain_b_lock_txid.hex()}, " - f"script_pk={script_pk.hex()}, num_outputs={len(lock_tx.vout)}" - ) - for i, out in enumerate(lock_tx.vout): - self._log.debug( - f" vout[{i}]: value={out.nValue}, scriptPubKey={out.scriptPubKey.hex()}" - ) - else: - self._log.warning( - f"spendBLockTx: Failed to fetch tx {chain_b_lock_txid.hex()} from backend" - ) - locked_n = lock_tx_vout - else: - wtx = self.rpc_wallet_watch( - "gettransaction", - [ - chain_b_lock_txid.hex(), - ], - ) - lock_tx = self.loadTx(bytes.fromhex(wtx["hex"])) - locked_n = findOutput(lock_tx, script_pk) - if locked_n is not None: - actual_value = lock_tx.vout[locked_n].nValue + locked_n, actual_value = self.getBLockTxo( + chain_b_lock_txid, lock_tx_vout, script_pk + ) if ( locked_n is not None @@ -2848,7 +2862,7 @@ class BTCInterface(FeeValidator, Secp256k1Interface): ): self._log.warning( f"spendBLockTx: Stored vout {lock_tx_vout} differs from actual vout {locked_n} " - f"for tx {chain_b_lock_txid.hex()}" + f"for tx {self._log.id(chain_b_lock_txid)}" ) ensure(locked_n is not None, "Output not found in tx") @@ -2938,13 +2952,9 @@ class BTCInterface(FeeValidator, Secp256k1Interface): # Add watchonly address and rescan if required if not self.isAddressMine(dest_address, or_watch_only=True): self.importWatchOnlyAddress(dest_address, "bid") + self._log.info(f"Imported watch-only addr: {self._log.addr(dest_address)}") self._log.info( - "Imported watch-only addr: {}".format(self._log.addr(dest_address)) - ) - self._log.info( - "Rescanning {} chain from height: {}".format( - self.coin_name(), rescan_from - ) + f"Rescanning {self.coin_name()} chain from height: {rescan_from}" ) self.rpc_wallet("rescanblockchain", [rescan_from]) @@ -3657,7 +3667,7 @@ class BTCInterface(FeeValidator, Secp256k1Interface): continue if "desc" in u: desc = u["desc"] - if self.using_segwit: + if self.using_segwit(): if self.use_p2shp2wsh(): if not desc.startswith("sh(wpkh"): continue @@ -3818,7 +3828,7 @@ class BTCInterface(FeeValidator, Secp256k1Interface): ensure( sign_for_addr is not None, - "Could not find address with enough funds for proof", + f"Could not find {self.ticker()} address with enough funds for proof", ) self._log.debug(f"sign_for_addr {sign_for_addr}") @@ -4457,6 +4467,8 @@ class BTCInterface(FeeValidator, Secp256k1Interface): return None def isTxExistsError(self, err_str: str) -> bool: + if self._connection_type == "electrum": + return "Transaction outputs already in utxo set" in err_str return "Transaction already in block chain" in err_str def isTxNonFinalError(self, err_str: str) -> bool: @@ -4511,7 +4523,7 @@ class BTCInterface(FeeValidator, Secp256k1Interface): self._log.id(bytes.fromhex(tx["txid"])) ) ) - self.publishTx(tx_signed) + self.publishTx(bytes.fromhex(tx_signed)) return tx["txid"] @@ -4549,10 +4561,65 @@ class BTCInterface(FeeValidator, Secp256k1Interface): return bytes.fromhex(txi_txid_hex), fee_rate + def _getTxOutInfoElectrum(self, txid: bytes, n: int, include_mempool: bool = False): + backend = self.getBackend() + if not backend: + return None -def testBTCInterface(): - print("TODO: testBTCInterface") + try: + tx_info = backend.getTransaction(txid.hex()) + if "blockhash" not in tx_info: + return None + confirmations: int = ( + 0 if "confirmations" not in tx_info else tx_info["confirmations"] + ) + if confirmations < 1: + return None + chain_tip_height = self.getChainHeight() + block_height: int = chain_tip_height - (confirmations - 1) + block_hash: bytes = bytes.fromhex(tx_info["blockhash"]) + return { + "block_hash": block_hash, + "block_height": block_height, + "block_time": tx_info["blocktime"], + } + except Exception as e: + self._log.debug(f"_findTxnByHashElectrum failed: {e}") + return None -if __name__ == "__main__": - testBTCInterface() + def getTxOutInfo( + self, txid: bytes, n: int, include_mempool: bool = False + ) -> dict(): + if self._connection_type == "electrum": + return self._getTxOutInfoElectrum(txid, n, include_mempool) + try: + txout = self.rpc("gettxout", [txid.hex(), n, include_mempool]) + confirmations: int = ( + 0 if "confirmations" not in txout else txout["confirmations"] + ) + if confirmations < 1: + return None + chain_tip_height: int = 0 + if "bestblock" in txout: + bestheader_info = self.getBlockHeader(txout["bestblock"]) + chain_tip_height = bestheader_info["height"] + else: + chain_tip_height = self.getChainHeight() + + if confirmations == 1: + header_info = bestheader_info + else: + block_height: int = chain_tip_height - (confirmations - 1) + header_info = self.getBlockHeaderFromHeight(block_height) + + block_hash: bytes = bytes.fromhex(header_info["hash"]) + return { + "block_hash": block_hash, + "block_height": header_info["height"], + "block_time": header_info["time"], + } + + except Exception as e: # noqa: F841 + # self._log.warning(f"gettxout {e}") + return None diff --git a/basicswap/interface/dash.py b/basicswap/interface/dash.py index 7c833c3..7fb067d 100644 --- a/basicswap/interface/dash.py +++ b/basicswap/interface/dash.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2022-2024 tecnovert diff --git a/basicswap/interface/dcr/dcr.py b/basicswap/interface/dcr/dcr.py index 38e77f0..ac677a4 100644 --- a/basicswap/interface/dcr/dcr.py +++ b/basicswap/interface/dcr/dcr.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2024 tecnovert @@ -13,7 +12,7 @@ import logging import random import traceback -from typing import List +from typing import List, Optional from basicswap.basicswap_util import getVoutByScriptPubKey, TxLockTypes from basicswap.chainparams import Coins @@ -419,8 +418,10 @@ class DCRInterface(FeeValidator, Secp256k1Interface): return bci + def getBlockHeader(self, block_hash: str) -> dict: + return self.rpc("getblockheader", [block_hash]) + def getWalletInfo(self): - rv = {} rv = self.rpc_wallet("getinfo") wi = self.rpc_wallet("walletinfo") balances = self.rpc_wallet("getbalance") @@ -595,7 +596,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface): override_feerate = chain_client_settings.get("override_feerate", None) if override_feerate: self._log.debug( - "Fee rate override used for %s: %f", self.coin_name(), override_feerate + f"Fee rate override used for {self.coin_name()}: {override_feerate}" ) return override_feerate, "override_feerate" @@ -919,7 +920,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface): found_vout = try_vout break except Exception as e: # noqa: F841 - # self._log.warning('gettxout {}'.format(e)) + # self._log.warning(f"gettxout {e}) return None if found_vout is None: @@ -932,7 +933,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface): # TODO: Better way? if confirmations > 0: - block_height = self.getChainHeight() - confirmations + block_height = self.getChainHeight() - (confirmations - 1) rv = { "txid": txid.hex(), @@ -999,6 +1000,10 @@ class DCRInterface(FeeValidator, Secp256k1Interface): tx.vout.append(self.txoType()(output_value, script)) return tx.serialize().hex() + def ensureFunds(self, amount: int) -> None: + if self.getSpendableBalance() < amount: + raise ValueError("Balance too low") + def verifyRawTransaction(self, tx_hex: str, prevouts): inputs_valid: bool = True validscripts: int = 0 @@ -1151,6 +1156,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface): dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) + size += 1 pay_fee = round(tx_fee_rate * size / 1000) tx.vout[0].value = locked_coin - pay_fee @@ -1202,6 +1208,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface): dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) + size += 1 pay_fee = round(tx_fee_rate * size / 1000) tx.vout[0].value = locked_coin - pay_fee @@ -1253,6 +1260,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface): script_lock_refund ) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) + size += 1 pay_fee = round(tx_fee_rate * size / 1000) tx.vout[0].value = locked_coin - pay_fee @@ -1337,6 +1345,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface): assert fee_paid > 0 size = len(tx.serialize()) + add_witness_bytes + size += 1 fee_rate_paid = fee_paid * 1000 // size self._log.info( @@ -1398,6 +1407,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface): dummy_witness_stack = self.getScriptLockTxDummyWitness(lock_tx_script) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) + size += 1 fee_rate_paid = fee_paid * 1000 // size self._log.info( @@ -1470,6 +1480,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface): dummy_witness_stack = self.getScriptLockTxDummyWitness(prevout_script) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) + size += 1 fee_rate_paid = fee_paid * 1000 // size self._log.info( @@ -1531,6 +1542,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface): prevout_script ) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) + size += 1 fee_rate_paid = fee_paid * 1000 // size self._log.info( @@ -1781,32 +1793,41 @@ class DCRInterface(FeeValidator, Secp256k1Interface): spend_actual_balance: bool = False, lock_tx_vout=None, ) -> bytes: - self._log.info("spendBLockTx %s:\n", chain_b_lock_txid.hex()) + self._log.info( + f"spendBLockTx: {self._log.id(chain_b_lock_txid)} {lock_tx_vout}\n" + ) Kbs = self.getPubkey(kbs) script_pk = self.getPkDest(Kbs) locked_n = None actual_value = None - wtx = self.rpc_wallet( - "gettransaction", - [ - chain_b_lock_txid.hex(), - ], - ) - lock_tx = self.loadTx(bytes.fromhex(wtx["hex"])) - locked_n = findOutput(lock_tx, script_pk) - if locked_n is not None: - actual_value = lock_tx.vout[locked_n].value - else: - self._log.error( - f"spendBLockTx: Output not found in tx {chain_b_lock_txid.hex()}, " - f"script_pk={script_pk.hex()}, num_outputs={len(lock_tx.vout)}" + try: + wtx = self.rpc_wallet( + "gettransaction", + [ + chain_b_lock_txid.hex(), + ], ) - for i, out in enumerate(lock_tx.vout): - self._log.debug( - f" vout[{i}]: value={out.value}, scriptPubKey={out.scriptPubKey.hex()}" + lock_tx = self.loadTx(bytes.fromhex(wtx["hex"])) + locked_n = findOutput(lock_tx, script_pk) + if locked_n is not None: + actual_value = lock_tx.vout[locked_n].value + else: + self._log.error( + f"spendBLockTx: Output not found in tx {self._log.id(chain_b_lock_txid)}, " + f"script_pk={script_pk.hex()}, num_outputs={len(lock_tx.vout)}" ) + for i, out in enumerate(lock_tx.vout): + self._log.debug( + f" vout[{i}]: value={out.value}, scriptPubKey={out.scriptPubKey.hex()}" + ) + except Exception as e: # noqa: F841 + txout = self.rpc( + "gettxout", [chain_b_lock_txid.hex(), lock_tx_vout, 0, True] + ) + actual_value = self.make_int(txout["value"]) + locked_n = lock_tx_vout if ( locked_n is not None @@ -1815,7 +1836,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface): ): self._log.warning( f"spendBLockTx: Stored vout {lock_tx_vout} differs from actual vout {locked_n} " - f"for tx {chain_b_lock_txid.hex()}" + f"for tx {self._log.id(chain_b_lock_txid)}" ) ensure(locked_n is not None, "Output not found in tx") @@ -1851,14 +1872,14 @@ class DCRInterface(FeeValidator, Secp256k1Interface): try: txout = self.rpc("gettxout", [txid_hex, 0, 0, True]) except Exception as e: # noqa: F841 - # self._log.warning('gettxout {}'.format(e)) + # self._log.warning(f"gettxout {e}")) return None confirmations: int = ( 0 if "confirmations" not in txout else txout["confirmations"] ) if confirmations >= self.blocks_confirmed: - block_height = self.getChainHeight() - confirmations # TODO: Better way? + block_height = self.getChainHeight() - (confirmations - 1) return {"txid": txid_hex, "amount": 0, "height": block_height} return None @@ -1873,3 +1894,101 @@ class DCRInterface(FeeValidator, Secp256k1Interface): def isTxNonFinalError(self, err_str: str) -> bool: return "locks on inputs not met" in err_str + + def getChainMedianTime(self) -> int: + bestblockhash = self.rpc("getbestblockhash") + bestblockheader = self.rpc( + "getblockheader", + [ + bestblockhash, + ], + ) + return bestblockheader["mediantime"] + + def getTxLocktime(self, tx_data: bytes) -> int: + tx_obj = self.loadTx(tx_data) + return tx_obj.locktime + + def getTxInSequence(self, tx_data: bytes, vout: int) -> int: + tx_obj = self.loadTx(tx_data) + return tx_obj.vin[vout].sequence + + def isCsvLockMature( + self, + lock_type: int, + encoded_sequence: int, + parent_block_height: Optional[int], + parent_block_time: Optional[int], + chain_height: Optional[int] = None, + chain_mtp: Optional[int] = None, + ) -> bool: + if parent_block_height is None or parent_block_height < 1: + return False + lock_value: int = self.decodeSequence(encoded_sequence) + if lock_type == TxLockTypes.SEQUENCE_LOCK_BLOCKS: + if chain_height is None: + chain_height = self.getChainHeight() + return chain_height + 1 >= parent_block_height + lock_value + if lock_type == TxLockTypes.SEQUENCE_LOCK_TIME: + if parent_block_time is None or parent_block_time < 1: + return False + if chain_mtp is None: + chain_mtp = self.getChainMedianTime() + return chain_mtp >= parent_block_time + lock_value + raise ValueError(f"Unknown lock type {lock_type}") + + def isAbsLockTimeMature( + self, + nlocktime: int, + chain_height: Optional[int] = None, + chain_mtp: Optional[int] = None, + ) -> bool: + if nlocktime == 0: + return True + if nlocktime < 500000000: + if chain_height is None: + chain_height = self.getChainHeight() + return chain_height + 1 >= nlocktime + if chain_mtp is None: + chain_mtp = self.getChainMedianTime() + return chain_mtp >= nlocktime + + def getTxOutInfo( + self, txid: bytes, n: int, include_mempool: bool = False + ) -> dict(): + try: + txout = self.rpc("gettxout", [txid.hex(), n, 0, include_mempool]) + confirmations: int = ( + 0 if "confirmations" not in txout else txout["confirmations"] + ) + if confirmations < 1: + return None + chain_tip_height: int = 0 + if "bestblock" in txout: + bestheader_info = self.getBlockHeader(txout["bestblock"]) + chain_tip_height = bestheader_info["height"] + else: + chain_tip_height = self.getChainHeight() + + if confirmations == 1: + header_info = bestheader_info + else: + block_height: int = chain_tip_height - (confirmations - 1) + header_info = self.getBlockHeaderFromHeight(block_height) + + block_hash: bytes = bytes.fromhex(header_info["hash"]) + return { + "block_hash": block_hash, + "block_height": header_info["height"], + "block_time": header_info["time"], + } + + except Exception as e: # noqa: F841 + # self._log.warning(f"gettxout {e}") + return None + + def is_transient_error(self, ex) -> bool: + str_error: str = str(ex).lower() + if "no information for transaction" in str_error: + return True + return super().is_transient_error(ex) diff --git a/basicswap/interface/dcr/messages.py b/basicswap/interface/dcr/messages.py index 0860d65..c7afdd0 100644 --- a/basicswap/interface/dcr/messages.py +++ b/basicswap/interface/dcr/messages.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2024 tecnovert diff --git a/basicswap/interface/dcr/rpc.py b/basicswap/interface/dcr/rpc.py index e4bfd8a..90b92e3 100644 --- a/basicswap/interface/dcr/rpc.py +++ b/basicswap/interface/dcr/rpc.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright (c) 2024 tecnovert +# Copyright (c) 2024-2026 The Basicswap developers # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -9,10 +10,10 @@ import traceback from basicswap.rpc import Jsonrpc -def callrpc(rpc_port, auth, method, params=[], host="127.0.0.1"): +def callrpc(rpc_port, auth, method, params=[], host="127.0.0.1", timeout=None): try: url = "http://{}@{}:{}/".format(auth, host, rpc_port) - x = Jsonrpc(url) + x = Jsonrpc(url, timeout=timeout if timeout else 10) x.__handler = None v = x.json_request(method, params) x.close() @@ -41,7 +42,7 @@ def make_rpc_func(port, auth, host="127.0.0.1"): auth = auth host = host - def rpc_func(method, params=None): - return callrpc(port, auth, method, params, host) + def rpc_func(method, params=None, timeout=None): + return callrpc(port, auth, method, params, host, timeout=timeout) return rpc_func diff --git a/basicswap/interface/doge.py b/basicswap/interface/doge.py index 51feb98..b61618b 100644 --- a/basicswap/interface/doge.py +++ b/basicswap/interface/doge.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2024 The BasicSwap developers diff --git a/basicswap/interface/electrumx.py b/basicswap/interface/electrumx.py index f7f83d3..3ed8025 100644 --- a/basicswap/interface/electrumx.py +++ b/basicswap/interface/electrumx.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2024-2026 The Basicswap developers diff --git a/basicswap/interface/firo.py b/basicswap/interface/firo.py index 13081e4..d799ab0 100644 --- a/basicswap/interface/firo.py +++ b/basicswap/interface/firo.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2022-2023 tecnovert diff --git a/basicswap/interface/ltc.py b/basicswap/interface/ltc.py index 7b02167..a60edae 100644 --- a/basicswap/interface/ltc.py +++ b/basicswap/interface/ltc.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2020-2023 tecnovert @@ -103,7 +102,7 @@ class LTCInterface(BTCInterface): continue if "desc" in u: desc = u["desc"] - if self.using_segwit: + if self.using_segwit(): if self.use_p2shp2wsh(): if not desc.startswith("sh(wpkh"): continue diff --git a/basicswap/interface/nav.py b/basicswap/interface/nav.py index 955fddc..920d426 100644 --- a/basicswap/interface/nav.py +++ b/basicswap/interface/nav.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2023 tecnovert diff --git a/basicswap/interface/nmc.py b/basicswap/interface/nmc.py index 0214857..22205fc 100644 --- a/basicswap/interface/nmc.py +++ b/basicswap/interface/nmc.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2020-2022 tecnovert diff --git a/basicswap/interface/part.py b/basicswap/interface/part.py index ed492cb..a5ba8f9 100644 --- a/basicswap/interface/part.py +++ b/basicswap/interface/part.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2020-2024 tecnovert @@ -1300,6 +1299,17 @@ class PARTInterfaceBlind(PARTInterface): "fundrawtransactionfrom", ["blind", tx_hex, {}, outputs_info, options] )["hex"] + def getLockRefundVout(self, lock_refund_tx_data: bytes, vkbv: bytes): + lock_refund_tx_obj = self.rpc( + "decoderawtransaction", [lock_refund_tx_data.hex()] + ) + # Nonce is derived from vkbv + nonce = self.getScriptLockRefundTxNonce(vkbv) + + # Find the output of the lock refund tx to spend + spend_n, input_blinded_info = self.findOutputByNonce(lock_refund_tx_obj, nonce) + return spend_n + class PARTInterfaceAnon(PARTInterface): diff --git a/basicswap/interface/passthrough_btc.py b/basicswap/interface/passthrough_btc.py index 79f964a..151d815 100644 --- a/basicswap/interface/passthrough_btc.py +++ b/basicswap/interface/passthrough_btc.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2021 tecnovert diff --git a/basicswap/interface/pivx.py b/basicswap/interface/pivx.py index 8e5cff5..689e2c0 100644 --- a/basicswap/interface/pivx.py +++ b/basicswap/interface/pivx.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2022 tecnovert @@ -171,3 +170,13 @@ class PIVXInterface(BTCInterface): block_height = self.getBlockHeader(rv["blockhash"])["height"] return {"txid": txid_hex, "amount": 0, "height": block_height} return None + + def getChainMedianTime(self) -> int: + bestblockhash = self.rpc("getbestblockhash") + bestblockheader = self.rpc( + "getblockheader", + [ + bestblockhash, + ], + ) + return bestblockheader["mediantime"] diff --git a/basicswap/interface/wow.py b/basicswap/interface/wow.py index b58c0c8..2fabdf8 100644 --- a/basicswap/interface/wow.py +++ b/basicswap/interface/wow.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2024 The Basicswap developers diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index cd02b67..ea94002 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2020-2024 tecnovert diff --git a/basicswap/protocols/xmr_swap_1.py b/basicswap/protocols/xmr_swap_1.py index d9b03b0..0b32762 100644 --- a/basicswap/protocols/xmr_swap_1.py +++ b/basicswap/protocols/xmr_swap_1.py @@ -50,11 +50,11 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None): try: use_cursor = self.openDB(cursor) bid, xmr_swap = self.getXmrBidFromSession(use_cursor, bid_id) - ensure(bid, "Bid not found: {}.".format(bid_id.hex())) - ensure(xmr_swap, "Adaptor-sig swap not found: {}.".format(bid_id.hex())) + ensure(bid, f"Bid not found: {self.log.id(bid_id)}.") + ensure(xmr_swap, f"Adaptor-sig swap not found: {self.log.id(bid_id)}.") offer, xmr_offer = self.getXmrOfferFromSession(use_cursor, bid.offer_id) - ensure(offer, "Offer not found: {}.".format(bid.offer_id.hex())) - ensure(xmr_offer, "Adaptor-sig offer not found: {}.".format(bid.offer_id.hex())) + ensure(offer, f"Offer not found: {self.log.id(bid.offer_id)}.") + ensure(xmr_offer, f"Adaptor-sig offer not found: {self.log.id(bid.offer_id)}.") # The no-script coin is always the follower reverse_bid: bool = self.is_reverse_ads_bid(offer.coin_from, offer.coin_to) @@ -106,7 +106,10 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None): address_to = self.getReceiveAddressFromPool( base_coin_to, bid_id, TxTypes.XMR_SWAP_B_LOCK_SPEND, use_cursor ) - amount = bid.amount_to + amount: int = bid.amount_to + chain_b_fee_rate: int = ( + xmr_offer.a_fee_rate if reverse_bid else xmr_offer.b_fee_rate + ) lock_tx_vout = bid.getLockTXBVout() txid = ci_follower.spendBLockTx( xmr_swap.b_lock_tx_id, @@ -114,7 +117,7 @@ def recoverNoScriptTxnWithKey(self, bid_id: bytes, encoded_key, cursor=None): xmr_swap.vkbv, vkbs, amount, - xmr_offer.b_fee_rate, + chain_b_fee_rate, bid.chain_b_height_start, spend_actual_balance=True, lock_tx_vout=lock_tx_vout, @@ -209,7 +212,7 @@ class XmrSwapInterface(ProtocolInterface): ) def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript: - # fallthrough to ci if genScriptLockTxScript is implemented there + # Fallthrough to ci if genScriptLockTxScript is implemented there if hasattr(ci, "genScriptLockTxScript") and callable(ci.genScriptLockTxScript): return ci.genScriptLockTxScript(ci, Kal, Kaf, **kwargs) @@ -221,7 +224,12 @@ class XmrSwapInterface(ProtocolInterface): def getFundedInitiateTxTemplate( self, ci, amount: int, sub_fee: bool, feerate: int = None ) -> bytes: - addr_to = self.getMockScriptAddr(ci) + if ci.coin_type() == Coins.BCH: + # Workaround, BCH getScriptDest() uses OP_HASH256 + script: bytes = self.getMockScript() + addr_to: bytes = ci.getScriptDest(script) + else: + addr_to = self.getMockScriptAddr(ci) funded_tx = ci.createRawFundedTransaction( addr_to, amount, sub_fee, lock_unspents=False, feerate=feerate ) @@ -247,8 +255,12 @@ class XmrSwapInterface(ProtocolInterface): return lock_vout def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray: - mock_txo_script = self.getMockScriptScriptPubkey(ci) - real_txo_script = ci.getScriptDest(script) + if ci.coin_type() == Coins.BCH: + mock_script: bytes = self.getMockScript() + mock_txo_script: bytes = ci.getScriptDest(mock_script) + else: + mock_txo_script: bytes = self.getMockScriptScriptPubkey(ci) + real_txo_script: bytes = ci.getScriptDest(script) found: int = 0 ctx = ci.loadTx(mock_tx, allow_witness=False) diff --git a/doc/release-notes.md b/doc/release-notes.md index 1463248..df4c3d3 100644 --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -22,6 +22,7 @@ - Fixed feerate from other chain displayed for reversed swaps. - Added warning text for fee above 1.2 x local estimate. - Added subfee bid option. +- Increase DCR fee estimate by 1 byte. 0.14.5 diff --git a/tests/basicswap/common.py b/tests/basicswap/common.py index 066549f..8d414d2 100644 --- a/tests/basicswap/common.py +++ b/tests/basicswap/common.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2020-2024 tecnovert @@ -163,7 +162,7 @@ def prepare_balance( post_json["type_to"] = type_to json_rv = read_json_api( port_take_from_node, - "wallets/{}/withdraw".format(coin_ticker.lower()), + f"wallets/{coin_ticker.lower()}/withdraw", post_json, ) assert len(json_rv["txid"]) == 64 @@ -236,11 +235,17 @@ def wait_for_bid( ) if isinstance(state, (list, tuple)): if bid[5] in state: + swap_client.log.debug( + f"TEST: wait_for_bid found {bid_id.hex()}: Bid state {bid[5]}, target {state}." + ) return else: continue elif state is not None and state != bid[5]: continue + swap_client.log.debug( + f"TEST: wait_for_bid found {bid_id.hex()}: Bid state {bid[5]}, target {state}." + ) return else: if i > 0 and i % 10 == 0: diff --git a/tests/basicswap/common_xmr.py b/tests/basicswap/common_xmr.py index f7e2c80..0eb689b 100644 --- a/tests/basicswap/common_xmr.py +++ b/tests/basicswap/common_xmr.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2020-2024 tecnovert diff --git a/tests/basicswap/extended/test_dash.py b/tests/basicswap/extended/test_dash.py index c052f78..7482cc1 100644 --- a/tests/basicswap/extended/test_dash.py +++ b/tests/basicswap/extended/test_dash.py @@ -175,6 +175,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey): "datadir": node_dir, "bindir": cfg.PARTICL_BINDIR, "blocks_confirmed": 2, # Faster testing + "wallet_name": "bsx_wallet", }, "dash": { "connection_type": "rpc", @@ -184,6 +185,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey): "bindir": DASH_BINDIR, "use_csv": True, "use_segwit": False, + "wallet_name": "bsx_wallet", }, "bitcoin": { "connection_type": "rpc", @@ -192,6 +194,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey): "datadir": btcdatadir, "bindir": cfg.BITCOIN_BINDIR, "use_segwit": True, + "wallet_name": "bsx_wallet", }, }, "check_progress_seconds": 2, @@ -285,7 +288,7 @@ class Test(unittest.TestCase): @classmethod def setUpClass(cls): - super(Test, cls).setUpClass() + super().setUpClass() k = PrivateKey() cls.network_key = toWIF(PREFIX_SECRET_KEY_REGTEST, k.secret) @@ -409,15 +412,15 @@ class Test(unittest.TestCase): waitForRPC(dashRpc, delay_event, rpc_command="getblockchaininfo") if len(dashRpc("listwallets")) < 1: - dashRpc("createwallet wbsx_wallet") + dashRpc("createwallet bsx_wallet") sc.start() waitForRPC(dashRpc, delay_event) num_blocks = 500 - logging.info("Mining %d dash blocks", num_blocks) + logging.info(f"Mining {num_blocks} dash blocks") cls.dash_addr = dashRpc("getnewaddress mining_addr") - dashRpc("generatetoaddress {} {}".format(num_blocks, cls.dash_addr)) + dashRpc(f"generatetoaddress {num_blocks} {cls.dash_addr}") ro = dashRpc("getblockchaininfo") try: @@ -431,8 +434,8 @@ class Test(unittest.TestCase): waitForRPC(btcRpc, delay_event) cls.btc_addr = btcRpc("getnewaddress mining_addr bech32") - logging.info("Mining %d Bitcoin blocks to %s", num_blocks, cls.btc_addr) - btcRpc("generatetoaddress {} {}".format(num_blocks, cls.btc_addr)) + logging.info(f"Mining {num_blocks} Bitcoin blocks to {cls.btc_addr}") + btcRpc(f"generatetoaddress {num_blocks} {cls.btc_addr}") ro = btcRpc("getblockchaininfo") checkForks(ro) @@ -449,7 +452,7 @@ class Test(unittest.TestCase): # Wait for height, or sequencelock is thrown off by genesis blocktime num_blocks = 3 - logging.info("Waiting for Particl chain height %d", num_blocks) + logging.info(f"Waiting for Particl chain height {num_blocks}") for i in range(60): particl_blocks = cls.swap_clients[0].callrpc("getblockcount") print("particl_blocks", particl_blocks) @@ -473,7 +476,7 @@ class Test(unittest.TestCase): cls.swap_clients.clear() cls.daemons.clear() - super(Test, cls).tearDownClass() + super().tearDownClass() def test_02_part_dash(self): logging.info("---------- Test PART to DASH") @@ -683,9 +686,9 @@ class Test(unittest.TestCase): offer_id = swap_clients[0].postOffer( Coins.DASH, Coins.BTC, - 0.001 * COIN, + 0.01 * COIN, 1.0 * COIN, - 0.001 * COIN, + 0.01 * COIN, SwapTypes.SELLER_FIRST, ) @@ -709,7 +712,7 @@ class Test(unittest.TestCase): del swap_clients[0].getChainClientSettings(Coins.DASH)["override_feerate"] def test_08_wallet(self): - logging.info("---------- Test {} wallet".format(self.test_coin_from.name)) + logging.info(f"---------- Test {self.test_coin_from.name} wallet") logging.info("Test withdrawal") addr = dashRpc('getnewaddress "Withdrawal test"') diff --git a/tests/basicswap/extended/test_dcr.py b/tests/basicswap/extended/test_dcr.py index 6630dd5..423baf3 100644 --- a/tests/basicswap/extended/test_dcr.py +++ b/tests/basicswap/extended/test_dcr.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # Copyright (c) 2024 tecnovert -# Copyright (c) 2024-2025 The Basicswap developers +# Copyright (c) 2024-2026 The Basicswap developers # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -75,24 +75,6 @@ def make_rpc_func(node_id, base_rpc_port): return rpc_func -def wait_for_dcr_height(http_port, num_blocks=3): - logging.info("Waiting for DCR chain height %d", num_blocks) - for i in range(60): - if test_delay_event.is_set(): - raise ValueError("Test stopped.") - try: - wallet = read_json_api(http_port, "wallets/dcr") - decred_blocks = wallet["blocks"] - print("decred_blocks", decred_blocks) - if decred_blocks >= num_blocks: - return - except Exception as e: - print("Error reading wallets", str(e)) - - test_delay_event.wait(1) - raise ValueError(f"wait_for_decred_blocks failed http_port: {http_port}") - - def run_test_success_path(self, coin_from: Coins, coin_to: Coins): logging.info(f"---------- Test {coin_from.name} to {coin_to.name}") @@ -765,14 +747,14 @@ class Test(BaseTest): @classmethod def tearDownClass(cls): logging.info("Finalising Decred Test") - super(Test, cls).tearDownClass() + super().tearDownClass() stopDaemons(cls.dcr_daemons) cls.dcr_daemons.clear() @classmethod def coins_loop(cls): - super(Test, cls).coins_loop() + super().coins_loop() ci0 = cls.swap_clients[0].ci(cls.test_coin) num_passed: int = 0 @@ -878,15 +860,16 @@ class Test(BaseTest): "use_csv": True, "use_segwit": True, "blocks_confirmed": 1, + "min_relay_fee": 0.00001, } def test_0001_decred_address(self): - logging.info("---------- Test {}".format(self.test_coin.name)) + logging.info(f"---------- Test {self.test_coin.name}") coin_settings = {"rpcport": 0, "rpcauth": "none"} coin_settings.update(REQUIRED_SETTINGS) - ci = DCRInterface(coin_settings, "mainnet") + ci = DCRInterface(coin_settings, "mainnet", self.swap_clients[0]) k = ci.getNewRandomKey() K = ci.getPubkey(k) @@ -914,7 +897,7 @@ class Test(BaseTest): assert hash160(masterpubkey_data) == seed_hash def test_001_segwit(self): - logging.info("---------- Test {} segwit".format(self.test_coin.name)) + logging.info(f"---------- Test {self.test_coin.name} segwit") swap_clients = self.swap_clients ci0 = swap_clients[0].ci(self.test_coin) @@ -972,7 +955,7 @@ class Test(BaseTest): assert f_decoded["txid"] == ctx.TxHash().hex() def test_003_signature_hash(self): - logging.info("---------- Test {} signature_hash".format(self.test_coin.name)) + logging.info(f"---------- Test {self.test_coin.name} signature_hash") # Test that signing a transaction manually produces the same result when signed with the wallet swap_clients = self.swap_clients @@ -1047,7 +1030,7 @@ class Test(BaseTest): assert len(sent_txid) == 64 def test_004_csv(self): - logging.info("---------- Test {} csv".format(self.test_coin.name)) + logging.info(f"---------- Test {self.test_coin.name} csv") swap_clients = self.swap_clients ci0 = swap_clients[0].ci(self.test_coin) @@ -1161,7 +1144,7 @@ class Test(BaseTest): assert sent_spend_txid is not None def test_005_watchonly(self): - logging.info("---------- Test {} watchonly".format(self.test_coin.name)) + logging.info(f"---------- Test {self.test_coin.name} watchonly") swap_clients = self.swap_clients ci0 = swap_clients[0].ci(self.test_coin) @@ -1261,7 +1244,7 @@ class Test(BaseTest): assert found_txid is not None def test_008_gettxout(self): - logging.info("---------- Test {} gettxout".format(self.test_coin.name)) + logging.info(f"---------- Test {self.test_coin.name} gettxout") ci0 = self.swap_clients[0].ci(self.test_coin) @@ -1373,7 +1356,7 @@ class Test(BaseTest): assert amount_proved >= require_amount def test_009_wallet_encryption(self): - logging.info("---------- Test {} wallet encryption".format(self.test_coin.name)) + logging.info(f"---------- Test {self.test_coin.name} wallet encryption") for coin in ("part", "dcr", "xmr"): jsw = read_json_api(1800, f"wallets/{coin}") @@ -1412,7 +1395,7 @@ class Test(BaseTest): assert jsw["locked"] is False def test_010_txn_size(self): - logging.info("---------- Test {} txn size".format(self.test_coin.name)) + logging.info(f"---------- Test {self.test_coin.name} txn size") swap_clients = self.swap_clients ci = swap_clients[0].ci(self.test_coin) diff --git a/tests/basicswap/extended/test_doge.py b/tests/basicswap/extended/test_doge.py index 63a6a1c..3373f1b 100644 --- a/tests/basicswap/extended/test_doge.py +++ b/tests/basicswap/extended/test_doge.py @@ -179,6 +179,7 @@ class Test(TestFunctions): @classmethod def prepareExtraCoins(cls): + super().prepareExtraCoins() if cls.restore_instance: void_block_rewards_pubkey = cls.getRandomPubkey() cls.doge_addr = ( @@ -232,7 +233,7 @@ class Test(TestFunctions): @classmethod def tearDownClass(cls): logging.info("Finalising DOGE Test") - super(Test, cls).tearDownClass() + super().tearDownClass() stopDaemons(cls.doge_daemons) cls.doge_daemons.clear() @@ -251,11 +252,12 @@ class Test(TestFunctions): "use_segwit": False, "blocks_confirmed": 1, "min_relay_fee": 0.01, # RECOMMENDED_MIN_TX_FEE + "wallet_name": "bsx_wallet", } @classmethod def coins_loop(cls): - super(Test, cls).coins_loop() + super().coins_loop() if cls.pause_chain: return ci0 = cls.swap_clients[0].ci(cls.test_coin) diff --git a/tests/basicswap/extended/test_doge_with_prepare.py b/tests/basicswap/extended/test_doge_with_prepare.py index 7402732..96f562a 100644 --- a/tests/basicswap/extended/test_doge_with_prepare.py +++ b/tests/basicswap/extended/test_doge_with_prepare.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2024 The Basicswap developers +# Copyright (c) 2024-2026 The Basicswap developers # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -12,7 +12,7 @@ mkdir -p ${TEST_PATH}/bin cp -r ~/tmp/basicswap_bin/* ${TEST_PATH}/bin export PYTHONPATH=$(pwd) export TEST_COINS_LIST='bitcoin,dogecoin' -python tests/basicswap/extended/test_doge.py +python tests/basicswap/extended/test_doge_with_prepare.py """ @@ -27,11 +27,9 @@ from tests.basicswap.extended.test_xmr_persistent import ( BaseTestWithPrepare, UI_PORT, ) -from tests.basicswap.extended.test_scripts import ( - wait_for_offers, -) from tests.basicswap.util import ( read_json_api, + wait_for_offers, ) logger = logging.getLogger() @@ -50,11 +48,11 @@ def wait_for_bid( bid = read_json_api(UI_PORT + node_id, f"bids/{bid_id}") - if "state" not in bid: + if "bid_state" not in bid: continue if state is None: return - if bid["state"].lower() == state.lower(): + if bid["bid_state"].lower() == state.lower(): return raise ValueError("wait_for_bid failed") @@ -101,8 +99,9 @@ def prepare_balance( class DOGETest(BaseTestWithPrepare): - def test_a(self): + __test__ = True + def test_a(self): amount_from = 10.0 offer_json = { "coin_from": "btc", @@ -114,10 +113,8 @@ class DOGETest(BaseTestWithPrepare): "automation_strat_id": 1, } offer_id = read_json_api(UI_PORT + 0, "offers/new", offer_json)["offer_id"] - logging.debug(f"offer_id {offer_id}") prepare_balance(self.delay_event, 1, 0, "DOGE", 1000.0) - wait_for_offers(self.delay_event, 1, 1, offer_id) post_json = {"offer_id": offer_id, "amount_from": amount_from} diff --git a/tests/basicswap/extended/test_firo.py b/tests/basicswap/extended/test_firo.py index fea55d2..7d533cd 100644 --- a/tests/basicswap/extended/test_firo.py +++ b/tests/basicswap/extended/test_firo.py @@ -104,7 +104,7 @@ def prepareDataDir( fp.write("debug=1\n") fp.write("debugexclude=libevent\n") - fp.write("fallbackfee=0.01\n") + fp.write("fallbackfee=0.0002\n") fp.write("acceptnonstdtxn=0\n") """ diff --git a/tests/basicswap/extended/test_key_paths.py b/tests/basicswap/extended/test_key_paths.py index 3c66b37..a2f63fb 100644 --- a/tests/basicswap/extended/test_key_paths.py +++ b/tests/basicswap/extended/test_key_paths.py @@ -29,6 +29,7 @@ import unittest from tests.basicswap.util import ( read_json_api, waitForServer, + UI_PORT, ) logger = logging.getLogger() @@ -37,9 +38,6 @@ if not len(logger.handlers): logger.addHandler(logging.StreamHandler(sys.stdout)) -PORT_OFS = int(os.getenv("PORT_OFS", 1)) -UI_PORT = 12700 + PORT_OFS - ELECTRUM_PATH = os.getenv("ELECTRUM_PATH") ELECTRUM_DATADIR = os.getenv("ELECTRUM_DATADIR") diff --git a/tests/basicswap/extended/test_pivx.py b/tests/basicswap/extended/test_pivx.py index e1aec6c..9212547 100644 --- a/tests/basicswap/extended/test_pivx.py +++ b/tests/basicswap/extended/test_pivx.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # Copyright (c) 2022-2023 tecnovert -# Copyright (c) 2024-2025 The Basicswap developers +# Copyright (c) 2024-2026 The Basicswap developers # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -11,22 +11,14 @@ basicswap]$ python tests/basicswap/extended/test_pivx.py """ -import json import logging import os import random -import shutil -import signal import sys -import threading -import time import unittest -from coincurve.keys import PrivateKey - import basicswap.config as cfg from basicswap.basicswap import ( - BasicSwap, Coins, SwapTypes, BidStates, @@ -39,30 +31,27 @@ from basicswap.util import ( from basicswap.basicswap_util import ( TxLockTypes, ) -from basicswap.util.address import ( - toWIF, -) from tests.basicswap.util import ( read_json_api, ) from tests.basicswap.common import ( callrpc_cli, - checkForks, stopDaemons, wait_for_bid, wait_for_offer, wait_for_balance, - wait_for_unspent, wait_for_in_progress, wait_for_bid_tx_state, - TEST_HTTP_HOST, TEST_HTTP_PORT, - BASE_PORT, - BASE_RPC_PORT, - BASE_ZMQ_PORT, - PREFIX_SECRET_KEY_REGTEST, waitForRPC, + make_rpc_func, ) +from tests.basicswap.test_xmr import ( + BaseTest, + test_delay_event as delay_event, + callnoderpc, +) +from basicswap.contrib.rpcauth import generate_salt, password_to_hmac from basicswap.bin.run import startDaemon from basicswap.bin.prepare import downloadPIVXParams @@ -72,11 +61,6 @@ if not len(logger.handlers): logger.addHandler(logging.StreamHandler(sys.stdout)) NUM_NODES = 3 -PIVX_NODE = 3 -BTC_NODE = 4 - -delay_event = threading.Event() -stop_test = False PIVX_BINDIR = os.path.expanduser( os.getenv("PIVX_BINDIR", os.path.join(cfg.DEFAULT_TEST_BINDIR, "pivx")) @@ -85,395 +69,173 @@ PIVXD = os.getenv("PIVXD", "pivxd" + cfg.bin_suffix) PIVX_CLI = os.getenv("PIVX_CLI", "pivx-cli" + cfg.bin_suffix) PIVX_TX = os.getenv("PIVX_TX", "pivx-tx" + cfg.bin_suffix) - -def prepareOtherDir(datadir, nodeId, conf_file="pivx.conf"): - node_dir = os.path.join(datadir, str(nodeId)) - if not os.path.exists(node_dir): - os.makedirs(node_dir) - filePath = os.path.join(node_dir, conf_file) - - with open(filePath, "w+") as fp: - fp.write("regtest=1\n") - fp.write("[regtest]\n") - fp.write("port=" + str(BASE_PORT + nodeId) + "\n") - fp.write("rpcport=" + str(BASE_RPC_PORT + nodeId) + "\n") - - fp.write("daemon=0\n") - fp.write("printtoconsole=0\n") - fp.write("server=1\n") - fp.write("discover=0\n") - fp.write("listenonion=0\n") - fp.write("bind=127.0.0.1\n") - fp.write("findpeers=0\n") - fp.write("debug=1\n") - fp.write("debugexclude=libevent\n") - - fp.write("fallbackfee=0.01\n") - fp.write("acceptnonstdtxn=0\n") - - if conf_file == "pivx.conf": - params_dir = os.path.join(datadir, "pivx-params") - downloadPIVXParams(params_dir) - fp.write(f"paramsdir={params_dir}\n") - - if conf_file == "bitcoin.conf": - fp.write("wallet=bsx_wallet\n") +PIVX_BASE_PORT = 34832 +PIVX_BASE_RPC_PORT = 35832 +PIVX_BASE_ZMQ_PORT = 36832 -def prepareDir(datadir, nodeId, network_key, network_pubkey): - node_dir = os.path.join(datadir, str(nodeId)) - if not os.path.exists(node_dir): - os.makedirs(node_dir) - filePath = os.path.join(node_dir, "particl.conf") - - with open(filePath, "w+") as fp: - fp.write("regtest=1\n") - fp.write("[regtest]\n") - fp.write("port=" + str(BASE_PORT + nodeId) + "\n") - fp.write("rpcport=" + str(BASE_RPC_PORT + nodeId) + "\n") - - fp.write("daemon=0\n") - fp.write("printtoconsole=0\n") - fp.write("server=1\n") - fp.write("discover=0\n") - fp.write("listenonion=0\n") - fp.write("bind=127.0.0.1\n") - fp.write("findpeers=0\n") - fp.write("debug=1\n") - fp.write("debugexclude=libevent\n") - fp.write("zmqpubsmsg=tcp://127.0.0.1:" + str(BASE_ZMQ_PORT + nodeId) + "\n") - fp.write("wallet=bsx_wallet\n") - fp.write("fallbackfee=0.01\n") - - fp.write("acceptnonstdtxn=0\n") - fp.write("minstakeinterval=5\n") - fp.write("smsgsregtestadjust=0\n") - - for i in range(0, NUM_NODES): - if nodeId == i: - continue - fp.write("addnode=127.0.0.1:%d\n" % (BASE_PORT + i)) - - if nodeId < 2: - fp.write("spentindex=1\n") - fp.write("txindex=1\n") - - basicswap_dir = os.path.join(datadir, str(nodeId), "basicswap") - if not os.path.exists(basicswap_dir): - os.makedirs(basicswap_dir) - - pivxdatadir = os.path.join(datadir, str(PIVX_NODE)) - btcdatadir = os.path.join(datadir, str(BTC_NODE)) - settings_path = os.path.join(basicswap_dir, cfg.CONFIG_FILENAME) - settings = { - "debug": True, - "zmqhost": "tcp://127.0.0.1", - "zmqport": BASE_ZMQ_PORT + nodeId, - "htmlhost": TEST_HTTP_HOST, - "htmlport": TEST_HTTP_PORT + nodeId, - "network_key": network_key, - "network_pubkey": network_pubkey, - "chainclients": { - "particl": { - "connection_type": "rpc", - "manage_daemon": False, - "rpcport": BASE_RPC_PORT + nodeId, - "datadir": node_dir, - "bindir": cfg.PARTICL_BINDIR, - "blocks_confirmed": 2, # Faster testing - "wallet_name": "bsx_wallet", - }, - "pivx": { - "connection_type": "rpc", - "manage_daemon": False, - "rpcport": BASE_RPC_PORT + PIVX_NODE, - "datadir": pivxdatadir, - "bindir": PIVX_BINDIR, - "use_csv": False, - "use_segwit": False, - "wallet_name": "", - }, - "bitcoin": { - "connection_type": "rpc", - "manage_daemon": False, - "rpcport": BASE_RPC_PORT + BTC_NODE, - "datadir": btcdatadir, - "bindir": cfg.BITCOIN_BINDIR, - "use_segwit": True, - "wallet_name": "bsx_wallet", - }, - }, - "check_progress_seconds": 2, - "check_watched_seconds": 4, - "check_expired_seconds": 60, - "check_events_seconds": 1, - "check_xmr_swaps_seconds": 1, - "min_delay_event": 1, - "max_delay_event": 3, - "min_delay_event_short": 1, - "max_delay_event_short": 3, - "min_delay_retry": 2, - "max_delay_retry": 10, - "restrict_unknown_seed_wallets": False, - "check_updates": False, - } - with open(settings_path, "w") as fp: - json.dump(settings, fp, indent=4) - - -def partRpc(cmd, node_id=0): - return callrpc_cli( - cfg.PARTICL_BINDIR, - os.path.join(cfg.TEST_DATADIRS, str(node_id)), - "regtest", - cmd, - cfg.PARTICL_CLI, - ) - - -def btcRpc(cmd): - return callrpc_cli( - cfg.BITCOIN_BINDIR, - os.path.join(cfg.TEST_DATADIRS, str(BTC_NODE)), - "regtest", - cmd, - cfg.BITCOIN_CLI, - ) - - -def pivxRpc(cmd): +def pivxCli(cmd, node_id=0): return callrpc_cli( PIVX_BINDIR, - os.path.join(cfg.TEST_DATADIRS, str(PIVX_NODE)), + os.path.join(cfg.TEST_DATADIRS, "pivx_" + str(node_id)), "regtest", cmd, PIVX_CLI, ) -def signal_handler(sig, frame): - global stop_test - os.write(sys.stdout.fileno(), f"Signal {sig} detected.\n".encode("utf-8")) - stop_test = True - delay_event.set() +def prepareDataDir( + datadir, node_id, conf_file, dir_prefix, base_p2p_port, base_rpc_port, num_nodes=3 +): + node_dir = os.path.join(datadir, dir_prefix + str(node_id)) + if not os.path.exists(node_dir): + os.makedirs(node_dir) + cfg_file_path = os.path.join(node_dir, conf_file) + if os.path.exists(cfg_file_path): + return + with open(cfg_file_path, "w+") as fp: + fp.write("regtest=1\n") + fp.write("[regtest]\n") + fp.write("port=" + str(base_p2p_port + node_id) + "\n") + fp.write("rpcport=" + str(base_rpc_port + node_id) + "\n") - -def run_coins_loop(cls): - while not stop_test: - try: - pivxRpc("generatetoaddress 1 {}".format(cls.pivx_addr)) - btcRpc("generatetoaddress 1 {}".format(cls.btc_addr)) - except Exception as e: - logging.warning("run_coins_loop " + str(e)) - time.sleep(1.0) - - -def run_loop(self): - while not stop_test: - for c in self.swap_clients: - c.update() - time.sleep(1) - - -def make_part_cli_rpc_func(node_id): - node_id = node_id - - def rpc_func(method, params=None, wallet=None): - cmd = method - if params: - for p in params: - cmd += ' "' + p + '"' - return partRpc(cmd, node_id) - - return rpc_func - - -class Test(unittest.TestCase): - test_coin_from = Coins.PIVX - - @classmethod - def setUpClass(cls): - super(Test, cls).setUpClass() - - k = PrivateKey() - cls.network_key = toWIF(PREFIX_SECRET_KEY_REGTEST, k.secret) - cls.network_pubkey = k.public_key.format().hex() - - if os.path.isdir(cfg.TEST_DATADIRS): - logging.info("Removing " + cfg.TEST_DATADIRS) - for name in os.listdir(cfg.TEST_DATADIRS): - if name == "pivx-params": - continue - fullpath = os.path.join(cfg.TEST_DATADIRS, name) - if os.path.isdir(fullpath): - shutil.rmtree(fullpath) - else: - os.remove(fullpath) - - for i in range(NUM_NODES): - prepareDir(cfg.TEST_DATADIRS, i, cls.network_key, cls.network_pubkey) - - prepareOtherDir(cfg.TEST_DATADIRS, PIVX_NODE) - prepareOtherDir(cfg.TEST_DATADIRS, BTC_NODE, "bitcoin.conf") - - cls.daemons = [] - cls.swap_clients = [] - - btc_data_dir = os.path.join(cfg.TEST_DATADIRS, str(BTC_NODE)) - if os.path.exists(os.path.join(cfg.BITCOIN_BINDIR, "bitcoin-wallet")): - try: - callrpc_cli( - cfg.BITCOIN_BINDIR, - btc_data_dir, - "regtest", - "-wallet=bsx_wallet -legacy create", - "bitcoin-wallet", - ) - except Exception: - callrpc_cli( - cfg.BITCOIN_BINDIR, - btc_data_dir, - "regtest", - "-wallet=bsx_wallet create", - "bitcoin-wallet", - ) - cls.daemons.append(startDaemon(btc_data_dir, cfg.BITCOIN_BINDIR, cfg.BITCOIND)) - logging.info("Started %s %d", cfg.BITCOIND, cls.daemons[-1].handle.pid) - cls.daemons.append( - startDaemon( - os.path.join(cfg.TEST_DATADIRS, str(PIVX_NODE)), PIVX_BINDIR, PIVXD + salt = generate_salt(16) + fp.write( + "rpcauth={}:{}${}\n".format( + "test" + str(node_id), + salt, + password_to_hmac(salt, "test_pass" + str(node_id)), ) ) - logging.info("Started %s %d", PIVXD, cls.daemons[-1].handle.pid) - for i in range(NUM_NODES): - data_dir = os.path.join(cfg.TEST_DATADIRS, str(i)) - if os.path.exists(os.path.join(cfg.PARTICL_BINDIR, "particl-wallet")): - try: - callrpc_cli( - cfg.PARTICL_BINDIR, - data_dir, - "regtest", - "-wallet=bsx_wallet -legacy create", - "particl-wallet", - ) - except Exception: - callrpc_cli( - cfg.PARTICL_BINDIR, - data_dir, - "regtest", - "-wallet=bsx_wallet create", - "particl-wallet", - ) - cls.daemons.append(startDaemon(data_dir, cfg.PARTICL_BINDIR, cfg.PARTICLD)) - logging.info("Started %s %d", cfg.PARTICLD, cls.daemons[-1].handle.pid) + fp.write("daemon=0\n") + fp.write("printtoconsole=0\n") + fp.write("server=1\n") + fp.write("discover=0\n") + fp.write("listenonion=0\n") + fp.write("bind=127.0.0.1\n") + fp.write("findpeers=0\n") + fp.write("debug=1\n") + fp.write("debugexclude=libevent\n") - for i in range(NUM_NODES): - rpc = make_part_cli_rpc_func(i) - waitForRPC(rpc, delay_event) - if i == 0: - rpc( - "extkeyimportmaster", - [ - "abandon baby cabbage dad eager fabric gadget habit ice kangaroo lab absorb" - ], - ) - elif i == 1: - rpc( - "extkeyimportmaster", - [ - "pact mammal barrel matrix local final lecture chunk wasp survey bid various book strong spread fall ozone daring like topple door fatigue limb olympic", - "", - "true", - ], - ) - rpc("getnewextaddress", ["lblExtTest"]) - rpc("rescanblockchain") - else: - rpc("extkeyimportmaster", [rpc("mnemonic", ["new"])["master"]]) - rpc( - "walletsettings", - [ - "stakingoptions", - json.dumps( - {"stakecombinethreshold": 100, "stakesplitthreshold": 200} - ).replace('"', '\\"'), - ], + fp.write("fallbackfee=0.01\n") + fp.write("acceptnonstdtxn=0\n") + + params_dir = os.path.join(datadir, "pivx-params") + downloadPIVXParams(params_dir) + fp.write(f"paramsdir={params_dir}\n") + + for i in range(0, num_nodes): + if node_id == i: + continue + fp.write("addnode=127.0.0.1:{}\n".format(base_p2p_port + i)) + + return node_dir + + +class Test(BaseTest): + __test__ = True + test_coin_from = Coins.PIVX + pivx_daemons = [] + pivx_addr = None + start_ltc_nodes = False + start_xmr_nodes = False + + @classmethod + def prepareExtraDataDir(cls, i): + extra_opts = [] + if not cls.restore_instance: + prepareDataDir( + cfg.TEST_DATADIRS, + i, + "pivx.conf", + "pivx_", + base_p2p_port=PIVX_BASE_PORT, + base_rpc_port=PIVX_BASE_RPC_PORT, ) - rpc("reservebalance", ["false"]) - - basicswap_dir = os.path.join( - os.path.join(cfg.TEST_DATADIRS, str(i)), "basicswap" + cls.pivx_daemons.append( + startDaemon( + os.path.join(cfg.TEST_DATADIRS, "pivx_" + str(i)), + PIVX_BINDIR, + PIVXD, + opts=extra_opts, ) - settings_path = os.path.join(basicswap_dir, cfg.CONFIG_FILENAME) - with open(settings_path) as fs: - settings = json.load(fs) - sc = BasicSwap( - basicswap_dir, settings, "regtest", log_name="BasicSwap{}".format(i) + ) + logging.info("Started %s %d", PIVXD, cls.pivx_daemons[-1].handle.pid) + + waitForRPC(make_rpc_func(i, base_rpc_port=PIVX_BASE_RPC_PORT), delay_event) + + @classmethod + def addPIDInfo(cls, sc, i): + sc.setDaemonPID(Coins.PIVX, cls.pivx_daemons[i].handle.pid) + + @classmethod + def prepareExtraCoins(cls): + + if cls.restore_instance: + void_block_rewards_pubkey = cls.getRandomPubkey() + cls.pivx_addr = ( + cls.swap_clients[0] + .ci(Coins.PIVX) + .pubkey_to_address(void_block_rewards_pubkey) ) - cls.swap_clients.append(sc) - sc.setDaemonPID(Coins.BTC, cls.daemons[0].handle.pid) - sc.setDaemonPID(Coins.PIVX, cls.daemons[1].handle.pid) - sc.setDaemonPID(Coins.PART, cls.daemons[2 + i].handle.pid) - sc.start() + else: + num_blocks = 1352 # CHECKLOCKTIMEVERIFY soft-fork activates at (regtest) block height 1351. + logging.info(f"Mining {num_blocks} pivx blocks") + cls.pivx_addr = pivxCli("getnewaddress mining_addr") + pivxCli(f"generatetoaddress {num_blocks} {cls.pivx_addr}") - waitForRPC(pivxRpc, delay_event) - num_blocks = 1352 # CHECKLOCKTIMEVERIFY soft-fork activates at (regtest) block height 1351. - logging.info("Mining %d pivx blocks", num_blocks) - cls.pivx_addr = pivxRpc("getnewaddress mining_addr") - pivxRpc("generatetoaddress {} {}".format(num_blocks, cls.pivx_addr)) - - ro = pivxRpc("getblockchaininfo") - try: - assert ro["bip9_softforks"]["csv"]["status"] == "active" - except Exception: - logging.info("pivx: csv is not active") - try: - assert ro["bip9_softforks"]["segwit"]["status"] == "active" - except Exception: - logging.info("pivx: segwit is not active") - - waitForRPC(btcRpc, delay_event) - cls.btc_addr = btcRpc("getnewaddress mining_addr bech32") - logging.info("Mining %d Bitcoin blocks to %s", num_blocks, cls.btc_addr) - btcRpc("generatetoaddress {} {}".format(num_blocks, cls.btc_addr)) - - ro = btcRpc("getblockchaininfo") - checkForks(ro) - - signal.signal(signal.SIGINT, signal_handler) - cls.update_thread = threading.Thread(target=run_loop, args=(cls,)) - cls.update_thread.start() - - cls.coins_update_thread = threading.Thread(target=run_coins_loop, args=(cls,)) - cls.coins_update_thread.start() - - # Wait for height, or sequencelock is thrown off by genesis blocktime - num_blocks = 3 - logging.info("Waiting for Particl chain height %d", num_blocks) - for i in range(60): - particl_blocks = cls.swap_clients[0].callrpc("getblockcount") - print("particl_blocks", particl_blocks) - if particl_blocks >= num_blocks: - break - delay_event.wait(1) - assert particl_blocks >= num_blocks + ro = pivxCli("getblockchaininfo") + try: + assert ro["bip9_softforks"]["csv"]["status"] == "active" + except Exception: + logging.info("pivx: csv is not active") + try: + assert ro["bip9_softforks"]["segwit"]["status"] == "active" + except Exception: + logging.info("pivx: segwit is not active") @classmethod def tearDownClass(cls): - global stop_test - logging.info("Finalising") - stop_test = True - cls.update_thread.join() - cls.coins_update_thread.join() - for c in cls.swap_clients: - c.finalise() + logging.info("Finalising PIVX Test") + super().tearDownClass() - stopDaemons(cls.daemons) - cls.swap_clients.clear() - cls.daemons.clear() + stopDaemons(cls.pivx_daemons) + cls.pivx_daemons.clear() - super(Test, cls).tearDownClass() + @classmethod + def addCoinSettings(cls, settings, datadir, node_id): + settings["chainclients"]["pivx"] = { + "connection_type": "rpc", + "manage_daemon": False, + "rpcport": PIVX_BASE_RPC_PORT + node_id, + "rpcuser": "test" + str(node_id), + "rpcpassword": "test_pass" + str(node_id), + "datadir": os.path.join(datadir, "pivx_" + str(node_id)), + "bindir": PIVX_BINDIR, + "use_csv": False, + "use_segwit": False, + "wallet_name": "", + } + + @classmethod + def coins_loop(cls): + super().coins_loop() + callnoderpc( + 0, "generatetoaddress", [1, cls.pivx_addr], base_rpc_port=PIVX_BASE_RPC_PORT + ) + + @classmethod + def prepareBalances(cls): + super().prepareBalances() + + cls.prepare_balance( + cls, + Coins.PIVX, + 10000.0, + 1801, + 1800, + ) def test_02_part_pivx(self): logging.info("---------- Test PART to PIVX") @@ -500,7 +262,7 @@ class Test(unittest.TestCase): wait_for_in_progress(delay_event, swap_clients[1], bid_id, sent=True) wait_for_bid( - delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60 + delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80 ) wait_for_bid( delay_event, @@ -508,7 +270,7 @@ class Test(unittest.TestCase): bid_id, BidStates.SWAP_COMPLETED, sent=True, - wait_for=60, + wait_for=80, ) js_0 = read_json_api(1800) @@ -548,7 +310,7 @@ class Test(unittest.TestCase): wait_for=60, ) wait_for_bid( - delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=60 + delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=80 ) js_0 = read_json_api(1800) @@ -580,7 +342,7 @@ class Test(unittest.TestCase): wait_for_in_progress(delay_event, swap_clients[1], bid_id, sent=True) wait_for_bid( - delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60 + delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=80 ) wait_for_bid( delay_event, @@ -717,7 +479,7 @@ class Test(unittest.TestCase): logging.info("---------- Test {} wallet".format(self.test_coin_from.name)) logging.info("Test withdrawal") - addr = pivxRpc('getnewaddress "Withdrawal test"') + addr = pivxCli('getnewaddress "Withdrawal test"') wallets = read_json_api(TEST_HTTP_PORT + 0, "wallets") assert float(wallets[self.test_coin_from.name]["balance"]) > 100 @@ -747,30 +509,30 @@ class Test(unittest.TestCase): def test_09_v3_tx(self): logging.info("---------- Test PIVX v3 txns") - generate_addr = pivxRpc('getnewaddress "generate test"') - pivx_addr = pivxRpc('getnewaddress "Sapling test"') - pivx_sapling_addr = pivxRpc('getnewshieldaddress "shield addr"') + generate_addr = pivxCli('getnewaddress "generate test"') + pivx_addr = pivxCli('getnewaddress "Sapling test"') + pivx_sapling_addr = pivxCli('getnewshieldaddress "shield addr"') - pivxRpc(f'sendtoaddress "{pivx_addr}" 6.0') - pivxRpc(f'generatetoaddress 1 "{generate_addr}"') + pivxCli(f'sendtoaddress "{pivx_addr}" 6.0') + pivxCli(f'generatetoaddress 1 "{generate_addr}"') - txid = pivxRpc( + txid = pivxCli( 'shieldsendmany "{}" "[{{\\"address\\": \\"{}\\", \\"amount\\": 1}}]"'.format( pivx_addr, pivx_sapling_addr ) ) - rtx = pivxRpc(f'getrawtransaction "{txid}" true') + rtx = pivxCli(f'getrawtransaction "{txid}" true') assert rtx["version"] == 3 block_hash = None for i in range(15): - rtx = pivxRpc(f'getrawtransaction "{txid}" true') + rtx = pivxCli(f'getrawtransaction "{txid}" true') if "blockhash" in rtx: block_hash = rtx["blockhash"] logging.info(f"Shielded tx confirmed in block {block_hash} after {i}s") break if i == 5: - pivxRpc(f'generatetoaddress 1 "{generate_addr}"') + pivxCli(f'generatetoaddress 1 "{generate_addr}"') delay_event.wait(1) assert block_hash is not None, "Shielded tx was not confirmed" @@ -860,7 +622,6 @@ class Test(unittest.TestCase): value_after_subfee = ci_from.make_int(itx_decoded["vout"][n]["value"]) assert value_after_subfee < swap_value swap_value = value_after_subfee - wait_for_unspent(delay_event, ci_from, swap_value) extra_options = {"prefunded_itx": itx} rate_swap = ci_to.make_int(random.uniform(0.2, 10.0), r=1) diff --git a/tests/basicswap/extended/test_scripts.py b/tests/basicswap/extended/test_scripts.py index d1b49ca..81f2561 100644 --- a/tests/basicswap/extended/test_scripts.py +++ b/tests/basicswap/extended/test_scripts.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # Copyright (c) 2023-2024 tecnovert -# Copyright (c) 2024-2025 The Basicswap developers +# Copyright (c) 2024-2026 The Basicswap developers # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -36,6 +36,8 @@ from tests.basicswap.common import ( from tests.basicswap.util import ( read_json_api, waitForServer, + wait_for_offers, + UI_PORT, ) logger = logging.getLogger() @@ -44,10 +46,6 @@ if not len(logger.handlers): logger.addHandler(logging.StreamHandler(sys.stdout)) -PORT_OFS = int(os.getenv("PORT_OFS", 1)) -UI_PORT = 12700 + PORT_OFS - - class HttpHandler(BaseHTTPRequestHandler): def js_response(self, url_split, post_string, is_json): @@ -131,18 +129,6 @@ def clear_offers(delay_event, node_id) -> None: raise ValueError("clear_offers failed") -def wait_for_offers(delay_event, node_id, num_offers, offer_id=None) -> None: - logging.info(f"Waiting for {num_offers} offers on node {node_id}") - for i in range(20): - delay_event.wait(1) - offers = read_json_api( - UI_PORT + node_id, "offers" if offer_id is None else f"offers/{offer_id}" - ) - if len(offers) >= num_offers: - return - raise ValueError("wait_for_offers failed") - - def wait_for_bids(delay_event, node_id, num_bids, offer_id=None) -> None: logging.info(f"Waiting for {num_bids} bids on node {node_id}") for i in range(20): diff --git a/tests/basicswap/extended/test_wow.py b/tests/basicswap/extended/test_wow.py index ca5729a..352b886 100644 --- a/tests/basicswap/extended/test_wow.py +++ b/tests/basicswap/extended/test_wow.py @@ -5,9 +5,9 @@ # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. -import time import logging import os +import time from basicswap.basicswap import ( Coins, @@ -120,14 +120,14 @@ class Test(BaseTest): @classmethod def tearDownClass(cls): logging.info("Finalising Wownero Test") - super(Test, cls).tearDownClass() + super().tearDownClass() stopDaemons(cls.wow_daemons) cls.wow_daemons.clear() @classmethod def coins_loop(cls): - super(Test, cls).coins_loop() + super().coins_loop() if cls.wow_addr is not None: callrpc_xmr( @@ -162,7 +162,7 @@ class Test(BaseTest): startXmrWalletDaemon(node_dir, WOW_BINDIR, WOW_WALLET_RPC, opts=opts) ) - cls.wow_wallet_auth.append(("test{0}".format(i), "test_pass{0}".format(i))) + cls.wow_wallet_auth.append((f"test{i}", f"test_pass{i}")) waitForWOWNode(i, auth=cls.wow_wallet_auth[i]) diff --git a/tests/basicswap/extended/test_xmr_persistent.py b/tests/basicswap/extended/test_xmr_persistent.py index 896562e..367eb9e 100644 --- a/tests/basicswap/extended/test_xmr_persistent.py +++ b/tests/basicswap/extended/test_xmr_persistent.py @@ -59,6 +59,8 @@ from tests.basicswap.util import ( make_boolean, read_json_api, waitForServer, + PORT_OFS, + UI_PORT, ) from tests.basicswap.common_xmr import ( prepare_nodes, @@ -73,9 +75,6 @@ import basicswap.bin.run as runSystem test_path = os.path.expanduser(os.getenv("TEST_PATH", "/tmp/test_persistent")) RESET_TEST = make_boolean(os.getenv("RESET_TEST", "true")) -PORT_OFS = int(os.getenv("PORT_OFS", 1)) -UI_PORT = 12700 + PORT_OFS - PARTICL_RPC_PORT_BASE = int(os.getenv("PARTICL_RPC_PORT_BASE", BASE_RPC_PORT)) BITCOIN_RPC_PORT_BASE = int(os.getenv("BITCOIN_RPC_PORT_BASE", BTC_BASE_RPC_PORT)) LITECOIN_RPC_PORT_BASE = int(os.getenv("LITECOIN_RPC_PORT_BASE", LTC_BASE_RPC_PORT)) diff --git a/tests/basicswap/mnemonics.py b/tests/basicswap/mnemonics.py index 8da6a6c..357ce9d 100644 --- a/tests/basicswap/mnemonics.py +++ b/tests/basicswap/mnemonics.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2020 tecnovert diff --git a/tests/basicswap/selenium/util.py b/tests/basicswap/selenium/util.py index 5bf0f07..8105414 100644 --- a/tests/basicswap/selenium/util.py +++ b/tests/basicswap/selenium/util.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2023 tecnovert diff --git a/tests/basicswap/test_bch_xmr.py b/tests/basicswap/test_bch_xmr.py index 4a3ed93..e03e8ee 100644 --- a/tests/basicswap/test_bch_xmr.py +++ b/tests/basicswap/test_bch_xmr.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2024 The Basicswap developers +# Copyright (c) 2024-2026 The Basicswap developers # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -167,6 +167,7 @@ class TestBCH(BasicSwapTest): @classmethod def prepareExtraCoins(cls): + super().prepareExtraCoins() cls.bch_addr = callnoderpc( 0, "getnewaddress", @@ -197,11 +198,12 @@ class TestBCH(BasicSwapTest): "datadir": os.path.join(datadir, "bch_" + str(node_id)), "bindir": BITCOINCASH_BINDIR, "use_segwit": False, + "wallet_name": "bsx_wallet", } @classmethod def coins_loop(cls): - super(TestBCH, cls).coins_loop() + super().coins_loop() ci0 = cls.swap_clients[0].ci(cls.test_coin) try: if cls.bch_addr is not None: @@ -212,7 +214,7 @@ class TestBCH(BasicSwapTest): @classmethod def tearDownClass(cls): logging.info("Finalising Bitcoincash Test") - super(TestBCH, cls).tearDownClass() + super().tearDownClass() stopDaemons(cls.bch_daemons) cls.bch_daemons.clear() @@ -224,19 +226,15 @@ class TestBCH(BasicSwapTest): return True def test_001_nested_segwit(self): - logging.info( - "---------- Test {} p2sh nested segwit".format(self.test_coin.name) - ) + logging.info(f"---------- Test {self.test_coin.name} p2sh nested segwit") logging.info("Skipped") def test_002_native_segwit(self): - logging.info( - "---------- Test {} p2sh native segwit".format(self.test_coin.name) - ) + logging.info(f"---------- Test {self.test_coin.name} p2sh native segwit") logging.info("Skipped") def test_003_cltv(self): - logging.info("---------- Test {} cltv".format(self.test_coin.name)) + logging.info(f"---------- Test {self.test_coin.name} cltv") ci = self.swap_clients[0].ci(self.test_coin) @@ -348,7 +346,7 @@ class TestBCH(BasicSwapTest): assert len(tx_wallet["blockhash"]) == 64 def test_004_csv(self): - logging.info("---------- Test {} csv".format(self.test_coin.name)) + logging.info(f"---------- Test {self.test_coin.name} csv") ci = self.swap_clients[0].ci(self.test_coin) @@ -451,7 +449,7 @@ class TestBCH(BasicSwapTest): assert len(tx_wallet["blockhash"]) == 64 def test_005_watchonly(self): - logging.info("---------- Test {} watchonly".format(self.test_coin.name)) + logging.info(f"---------- Test {self.test_coin.name} watchonly") ci = self.swap_clients[0].ci(self.test_coin) ci1 = self.swap_clients[1].ci(self.test_coin) @@ -482,7 +480,7 @@ class TestBCH(BasicSwapTest): super().test_006_getblock_verbosity() def test_007_hdwallet(self): - logging.info("---------- Test {} hdwallet".format(self.test_coin.name)) + logging.info(f"---------- Test {self.test_coin.name} hdwallet") test_seed = "8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b" test_wif = ( @@ -506,10 +504,10 @@ class TestBCH(BasicSwapTest): super().test_009_scantxoutset() def test_010_txn_size(self): - logging.info("---------- Test {} txn_size".format(Coins.BCH)) + logging.info(f"---------- Test {self.test_coin.name} txn_size") swap_clients = self.swap_clients - ci = swap_clients[0].ci(Coins.BCH) + ci = swap_clients[0].ci(self.test_coin) pi = swap_clients[0].pi(SwapTypes.XMR_SWAP) amount: int = ci.make_int(random.uniform(0.1, 2.0), r=1) @@ -627,7 +625,7 @@ class TestBCH(BasicSwapTest): def test_011_p2sh(self): # Not used in bsx for native-segwit coins - logging.info("---------- Test {} p2sh".format(self.test_coin.name)) + logging.info(f"---------- Test {self.test_coin.name} p2sh") ci = self.swap_clients[0].ci(self.test_coin) @@ -717,7 +715,7 @@ class TestBCH(BasicSwapTest): def test_011_p2sh32(self): # Not used in bsx for native-segwit coins - logging.info("---------- Test {} p2sh32".format(self.test_coin.name)) + logging.info(f"---------- Test {self.test_coin.name} p2sh32") ci = self.swap_clients[0].ci(self.test_coin) @@ -806,7 +804,7 @@ class TestBCH(BasicSwapTest): assert len(tx_wallet["blockhash"]) == 64 def test_012_p2sh_p2wsh(self): - logging.info("---------- Test {} p2sh-p2wsh".format(self.test_coin.name)) + logging.info(f"---------- Test {self.test_coin.name} p2sh-p2wsh") logging.info("Skipped") def test_01_a_full_swap(self): @@ -877,7 +875,7 @@ class TestBCH(BasicSwapTest): def test_06_preselect_inputs(self): tla_from = self.test_coin.name - logging.info("---------- Test {} Preselected inputs".format(tla_from)) + logging.info(f"---------- Test {tla_from} Preselected inputs") logging.info("Skipped") def test_07_expire_stuck_accepted(self): diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py index 08a9603..0db2e40 100644 --- a/tests/basicswap/test_btc_xmr.py +++ b/tests/basicswap/test_btc_xmr.py @@ -74,6 +74,7 @@ class TestFunctions(BaseTest): @classmethod def prepareExtraCoins(cls): + # Save sent messages so tests can count them for sc in cls.swap_clients: sc._smsg_add_to_outbox = True @@ -113,7 +114,7 @@ class TestFunctions(BaseTest): ) def do_test_01_full_swap(self, coin_from: Coins, coin_to: Coins) -> None: - logging.info("---------- Test {} to {}".format(coin_from.name, coin_to.name)) + logging.info(f"---------- Test {coin_from.name} to {coin_to.name}") # Offerer sends the offer # Bidder sends the bid @@ -306,9 +307,7 @@ class TestFunctions(BaseTest): self, coin_from: Coins, coin_to: Coins, lock_value: int = 32 ) -> None: logging.info( - "---------- Test {} to {} leader recovers coin a lock tx".format( - coin_from.name, coin_to.name - ) + f"---------- Test {coin_from.name} to {coin_to.name} leader recovers coin a lock tx" ) id_offerer: int = self.node_a_id @@ -459,6 +458,12 @@ class TestFunctions(BaseTest): if with_mercy else (BidStates.BID_STALLED_FOR_TEST, BidStates.XMR_SWAP_FAILED_SWIPED) ) + + chain_a_coin = coin_to if reverse_bid else coin_from + if with_mercy is False and chain_a_coin == Coins.BCH: + # When using BCH, can't set XMR_SWAP_FAILED_SWIPED as should wait for mercy tx + expect_state = expect_state + (BidStates.XMR_SWAP_SCRIPT_TX_PREREFUND,) + wait_for_bid( test_delay_event, swap_clients[id_leader], @@ -492,12 +497,12 @@ class TestFunctions(BaseTest): # Test manually redeeming the no-script lock tx offerer_key = read_json_api( 1800 + id_offerer, - "bids/{}".format(bid_id.hex()), + f"bids/{bid_id.hex()}", {"chainbkeysplit": True}, )["splitkey"] data = {"spendchainblocktx": True, "remote_key": offerer_key} redeemed_txid = read_json_api( - 1800 + id_bidder, "bids/{}".format(bid_id.hex()), data + 1800 + id_bidder, f"bids/{bid_id.hex()}", data )["txid"] assert len(redeemed_txid) == 64 @@ -505,9 +510,7 @@ class TestFunctions(BaseTest): self, coin_from, coin_to, lock_value: int = 32 ): logging.info( - "---------- Test {} to {} follower recovers coin b lock tx".format( - coin_from.name, coin_to.name - ) + f"---------- Test {coin_from.name} to {coin_to.name} follower recovers coin b lock tx" ) id_offerer: int = self.node_a_id @@ -920,7 +923,7 @@ class BasicSwapTest(TestFunctions): @classmethod def setUpClass(cls): - super(BasicSwapTest, cls).setUpClass() + super().setUpClass() @classmethod def addCoinSettings(cls, settings, datadir, node_id): @@ -2480,11 +2483,6 @@ class BasicSwapTest(TestFunctions): def test_09_expire_accepted_rev(self): self.do_test_09_expire_accepted(Coins.XMR, self.test_coin_from) - def test_10_presigned_txns(self): - raise RuntimeError( - "TODO" - ) # Build without xmr first for quicker test iterations - def test_11_fee_validation(self): coin_from, coin_to = (self.test_coin_from, Coins.XMR) logging.info( @@ -2822,7 +2820,7 @@ class TestBTC_PARTB(TestFunctions): @classmethod def setUpClass(cls): - super(TestBTC_PARTB, cls).setUpClass() + super().setUpClass() if False: for client in cls.swap_clients: client.log.safe_logs = True diff --git a/tests/basicswap/test_electrum.py b/tests/basicswap/test_electrum.py index f24b8d8..3356e9e 100644 --- a/tests/basicswap/test_electrum.py +++ b/tests/basicswap/test_electrum.py @@ -115,6 +115,10 @@ def modify_config(test_path, i): with open(config_path, "w") as fp: json.dump(settings, fp, indent=4) + btc_config_path = os.path.join(test_path, f"client{i}", "bitcoin", "bitcoin.conf") + with open(btc_config_path, "a") as fp: + fp.write("minrelaytxfee=0.00001\n") + def wait_for_bid_state( delay_event, node_port: int, bid_id: str, state=None, wait_for: int = 30 @@ -641,7 +645,7 @@ class Test(TestFunctions): @classmethod def setUpClass(cls): cls.addElectrumxDaemon("bitcoin", 32793, 50001) - super(Test, cls).setUpClass() + super().setUpClass() @classmethod def modifyConfig(cls, test_path, i): @@ -754,14 +758,6 @@ class Test(TestFunctions): self.delay_event, self.test_coin_b, 100, - self.port_node_1, - self.port_node_0, - True, - ) - prepare_balance( - self.delay_event, - self.test_coin_xmr, - 100, self.port_node_0, self.port_node_1, True, @@ -788,7 +784,7 @@ class Test(TestFunctions): True, ) self.do_test_03_follower_recover_a_lock_tx( - self.test_coin_b, self.test_coin_xmr, self.port_node_1, self.port_node_0 + self.test_coin_b, self.test_coin_xmr, self.port_node_0, self.port_node_1 ) def test_03_b_follower_recover_a_lock_tx_reverse(self): diff --git a/tests/basicswap/test_ltc_xmr.py b/tests/basicswap/test_ltc_xmr.py index 9ab9ff8..8190577 100644 --- a/tests/basicswap/test_ltc_xmr.py +++ b/tests/basicswap/test_ltc_xmr.py @@ -48,15 +48,11 @@ class TestLTC(BasicSwapTest): assert deploymentinfo["softforks"][feature_name]["active"] is True def test_001_nested_segwit(self): - logging.info( - "---------- Test {} p2sh nested segwit".format(self.test_coin_from.name) - ) + logging.info(f"---------- Test {self.test_coin_from.name} p2sh nested segwit") logging.info("Skipped") def test_002_native_segwit(self): - logging.info( - "---------- Test {} p2sh native segwit".format(self.test_coin_from.name) - ) + logging.info(f"---------- Test {self.test_coin_from.name} p2sh native segwit") ci = self.swap_clients[0].ci(self.test_coin_from) addr_segwit = ci.rpc_wallet("getnewaddress", ["segwit test", "bech32"]) @@ -120,7 +116,7 @@ class TestLTC(BasicSwapTest): assert tx_funded_decoded["txid"] == tx_signed_decoded["txid"] def test_007_hdwallet(self): - logging.info("---------- Test {} hdwallet".format(self.test_coin_from.name)) + logging.info(f"---------- Test {self.test_coin_from.name} hdwallet") test_seed = "8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b" test_wif = ( @@ -136,7 +132,7 @@ class TestLTC(BasicSwapTest): assert addr == "rltc1qps7hnjd866e9ynxadgseprkc2l56m00djr82la" def test_20_btc_coin(self): - logging.info("---------- Test BTC to {}".format(self.test_coin_from.name)) + logging.info(f"---------- Test BTC to {self.test_coin_from.name}") swap_clients = self.swap_clients offer_id = swap_clients[0].postOffer( @@ -178,7 +174,7 @@ class TestLTC(BasicSwapTest): assert js_1["num_swapping"] == 0 and js_1["num_watched_outputs"] == 0 def test_21_mweb(self): - logging.info("---------- Test MWEB {}".format(self.test_coin_from.name)) + logging.info(f"---------- Test MWEB {self.test_coin_from.name}") swap_clients = self.swap_clients ci0 = swap_clients[0].ci(self.test_coin_from) @@ -327,7 +323,7 @@ class TestLTC(BasicSwapTest): # TODO def test_22_mweb_balance(self): - logging.info("---------- Test MWEB balance {}".format(self.test_coin_from.name)) + logging.info(f"---------- Test MWEB balance {self.test_coin_from.name}") swap_clients = self.swap_clients ci_mweb = swap_clients[0].ci(Coins.LTC_MWEB) diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index 01e439d..01f6bd7 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -815,7 +815,7 @@ class BaseTest(unittest.TestCase): .pubkey_to_address(void_block_rewards_pubkey) ) logging.info( - "Mining %d Litecoin blocks to %s", num_blocks, cls.ltc_addr + f"Mining {num_blocks} Litecoin blocks to {cls.ltc_addr}" ) callnoderpc( 0, @@ -942,6 +942,7 @@ class BaseTest(unittest.TestCase): ) cls.coins_update_thread.start() + cls.prepareBalances() except Exception: traceback.print_exc() cls.tearDownClass() @@ -999,6 +1000,10 @@ class BaseTest(unittest.TestCase): def prepareExtraCoins(cls): pass + @classmethod + def prepareBalances(cls): + pass + @classmethod def coins_loop(cls): if cls.btc_addr is not None: diff --git a/tests/basicswap/util.py b/tests/basicswap/util.py index ee29187..d325ea1 100644 --- a/tests/basicswap/util.py +++ b/tests/basicswap/util.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (c) 2022-2024 tecnovert @@ -7,9 +6,14 @@ # file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php. import json +import logging +import os import urllib from urllib.request import urlopen +PORT_OFS = int(os.getenv("PORT_OFS", 1)) +UI_PORT = 12700 + PORT_OFS + REQUIRED_SETTINGS = { "blocks_confirmed": 1, "conf_target": 1, @@ -67,5 +71,17 @@ def waitForServer(delay_event, port, wait_for=40): _ = read_json_api(port) return except Exception as e: - print("waitForServer, error:", str(e)) + logging.error(f"waitForServer: {e}") raise ValueError("waitForServer failed") + + +def wait_for_offers(delay_event, node_id, num_offers, offer_id=None) -> None: + logging.info(f"Waiting for {num_offers} offers on node {node_id}") + for i in range(20): + delay_event.wait(1) + offers = read_json_api( + UI_PORT + node_id, "offers" if offer_id is None else f"offers/{offer_id}" + ) + if len(offers) >= num_offers: + return + raise ValueError("wait_for_offers failed")