From 7655f1ad81afcbfb0e467e8d952766b14fb8b568 Mon Sep 17 00:00:00 2001 From: tecnovert Date: Fri, 5 Jun 2026 22:56:26 +0200 Subject: [PATCH] fix: get refund tx block info for CSV check --- .github/workflows/ci.yml | 2 +- basicswap/basicswap.py | 65 ++++++++-- basicswap/interface/base.py | 1 - basicswap/interface/bch.py | 37 ++++-- basicswap/interface/btc.py | 171 ++++++++++++++++-------- basicswap/interface/dash.py | 1 - basicswap/interface/dcr/dcr.py | 173 +++++++++++++++++++++---- basicswap/interface/dcr/messages.py | 1 - basicswap/interface/dcr/rpc.py | 9 +- basicswap/interface/doge.py | 1 - basicswap/interface/electrumx.py | 1 - basicswap/interface/firo.py | 1 - basicswap/interface/ltc.py | 1 - basicswap/interface/nav.py | 1 - basicswap/interface/nmc.py | 1 - basicswap/interface/part.py | 1 - basicswap/interface/passthrough_btc.py | 1 - basicswap/interface/pivx.py | 1 - basicswap/interface/wow.py | 1 - basicswap/interface/xmr.py | 1 - basicswap/protocols/xmr_swap_1.py | 17 ++- doc/release-notes.md | 1 + tests/basicswap/common.py | 6 + tests/basicswap/extended/test_dcr.py | 39 ++---- tests/basicswap/test_bch_xmr.py | 36 +++-- tests/basicswap/test_btc_xmr.py | 18 +-- tests/basicswap/test_electrum.py | 16 +-- tests/basicswap/test_ltc_xmr.py | 16 +-- 28 files changed, 424 insertions(+), 196 deletions(-) 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..af86b42 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -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 @@ -7951,6 +7968,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): bid_id=bid_id, tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, txid=bytes.fromhex(txid), + vout=0, ) self.saveBidInSession(bid_id, bid, cursor, xmr_swap) self.commitDB() @@ -7966,6 +7984,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): bid_id=bid_id, tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, txid=txid, + vout=0, ) self.saveBidInSession(bid_id, bid, cursor, xmr_swap) self.commitDB() @@ -8346,14 +8365,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 +8661,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) @@ -9021,6 +9062,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): bid_id=bid.bid_id, tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, txid=xmr_swap.a_lock_refund_tx_id, + vout=0, ) else: self.setBidError( @@ -9140,6 +9182,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 +11261,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) @@ -14082,9 +14125,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 +14142,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: diff --git a/basicswap/interface/base.py b/basicswap/interface/base.py index 092d918..0879a9d 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 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..ce60c0a 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]) @@ -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..2faadfc 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 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..ea8c650 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 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..9c32696 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 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..575b162 100644 --- a/basicswap/protocols/xmr_swap_1.py +++ b/basicswap/protocols/xmr_swap_1.py @@ -209,7 +209,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 +221,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 +252,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..78616ea 100644 --- a/tests/basicswap/common.py +++ b/tests/basicswap/common.py @@ -236,11 +236,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/extended/test_dcr.py b/tests/basicswap/extended/test_dcr.py index 6630dd5..1f0ba0e 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}") @@ -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/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..bd76ec0 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 @@ -459,6 +460,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], @@ -920,7 +927,7 @@ class BasicSwapTest(TestFunctions): @classmethod def setUpClass(cls): - super(BasicSwapTest, cls).setUpClass() + super().setUpClass() @classmethod def addCoinSettings(cls, settings, datadir, node_id): @@ -2480,11 +2487,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 +2824,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)