diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 53bb162..5bba5a1 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -3964,6 +3964,13 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): except Exception as e: raise ValueError(f"Invalid message networks: {e}") + def isValidSwapDest(self, ci, dest: bytes): + ensure(isinstance(dest, bytes), "Swap destination must be bytes") + if ci.coin_type() in (Coins.PART_BLIND,): + return ci.isValidPubkey(dest) + # TODO: allow p2wsh + return ci.isValidAddressHash(dest) + def ensureWalletCanSend( self, ci, swap_type, ensure_balance: int, estimated_fee: int, for_offer=True ) -> None: @@ -5365,12 +5372,13 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): self.loadBidTxns(bid, cursor) return bid, xmr_swap - def getXmrBid(self, bid_id: bytes): + def getXmrBid(self, bid_id: bytes, cursor=None): try: - cursor = self.openDB() - return self.getXmrBidFromSession(cursor, bid_id) + use_cursor = self.openDB(cursor) + return self.getXmrBidFromSession(use_cursor, bid_id) finally: - self.closeDB(cursor, commit=False) + if cursor is None: + self.closeDB(use_cursor, commit=False) def getXmrOfferFromSession(self, cursor, offer_id: bytes): offer = self.queryOne(Offer, cursor, {"offer_id": offer_id}) @@ -5875,7 +5883,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): ) if bid.bid_id and bid_msg_id != bid.bid_id: self.log.warning( - f"sendBidMessage: Mismatched bid ids: {bid.bid_id.hex()}, {bid_msg_id.hex()}." + f"sendBidMessage: Mismatched bid ids: {self.log.id(bid.bid_id)}, {self.log.id(bid_msg_id)}." ) return bid_msg_id @@ -7771,7 +7779,12 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): len(xmr_swap.al_lock_refund_tx_sig) > 0 and len(xmr_swap.af_lock_refund_tx_sig) > 0 ): + try: + if bid.xmr_b_lock_tx is None and self.haveDebugInd( + bid.bid_id, DebugTypes.WAIT_FOR_COIN_B_LOCK_BEFORE_PREREFUND + ): + raise TemporaryError("Debug: Waiting for Coin B Lock Tx") txid = ci_from.publishTx(xmr_swap.a_lock_refund_tx) # BCH txids change @@ -7821,6 +7834,10 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): self.saveBidInSession(bid_id, bid, cursor, xmr_swap) self.commitDB() return rv + else: + self.log.warning( + f"Trying to publish coin a lock refund tx: {ex}" + ) state = BidStates(bid.state) if state == BidStates.SWAP_COMPLETED: @@ -8067,6 +8084,15 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): self.log.warning( f"Not releasing ads script coin lock tx for bid {self.log.id(bid_id)}: Chain A lock refund tx already exists." ) + elif ( + bid.debug_ind == DebugTypes.DONT_RELEASE_COIN_A_LOCK + ): + self.logBidEvent( + bid_id, + EventLogTypes.DEBUG_TWEAK_APPLIED, + f"ind {DebugTypes.DONT_RELEASE_COIN_A_LOCK}", + None, + ) else: delay = self.get_delay_event_seconds() self.log.info( @@ -8513,9 +8539,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): ) watched = self.coin_clients[coin_type]["watched_transactions"] - - for wo in watched: - if wo.bid_id == bid_id and wo.txid_hex == txid_hex: + for wt in watched: + if wt.bid_id == bid_id and wt.txid_hex == txid_hex: self.log.debug("Transaction already being watched.") return @@ -8953,7 +8978,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): else: self.log.info( - f"Coin a lock refund spent by unknown tx, bid {self.log.id(bid_id)}." + f"Coin a lock refund spent by unknown tx, bid {self.log.id(bid_id)}, txid {self.logIDT(spending_txid)}." ) mercy_keyshare = None @@ -8980,6 +9005,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): bid.setState(BidStates.XMR_SWAP_FAILED_SWIPED) else: delay = self.get_delay_event_seconds() + self.log.info("Found mercy output.") self.log.info( f"Redeeming coin b lock tx for bid {self.log.id(bid_id)} in {delay} seconds." ) @@ -9298,6 +9324,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): self.processFoundTransaction( t, block_hash, block["height"], chain_blocks ) + for s in c["watched_scripts"]: for i, txo in enumerate(tx["vout"]): if "scriptPubKey" in txo and "hex" in txo["scriptPubKey"]: @@ -12174,7 +12201,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): xmr_swap.af_lock_refund_spend_tx_sig = ci_from.decryptOtVES( kbsl, xmr_swap.af_lock_refund_spend_tx_esig ) - prevout_amount = ci_from.getLockRefundTxSwapOutputValue(bid, xmr_swap) + prevout_amount: int = ci_from.getLockRefundTxSwapOutputValue( + bid, xmr_swap + ) al_lock_refund_spend_tx_sig = ci_from.signTx( kal, xmr_swap.a_lock_refund_spend_tx, @@ -12601,8 +12630,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): ci_from.verifyPubkey(msg_data.pkaf), "Invalid chain A follower public key" ) ensure( - ci_from.isValidAddressHash(msg_data.dest_af) - or ci_from.isValidPubkey(msg_data.dest_af), + self.isValidSwapDest(ci_from, msg_data.dest_af), "Invalid destination address", ) if ci_to.curve_type() == Curves.ed25519: diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index 5ee6cbd..abd407d 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -240,8 +240,11 @@ class DebugTypes(IntEnum): OFFER_LOCK_2_VALUE_INC = auto() BID_STOP_AFTER_COIN_B_LOCK = auto() BID_DONT_SPEND_COIN_B_LOCK = auto() + WAIT_FOR_COIN_B_LOCK_BEFORE_PREREFUND = auto() WAIT_FOR_COIN_B_LOCK_BEFORE_REFUND = auto() BID_DONT_SPEND_COIN_A_LOCK = auto() + DONT_SEND_COIN_B_LOCK = auto() + DONT_RELEASE_COIN_A_LOCK = auto() class NotificationTypes(IntEnum): diff --git a/basicswap/chainparams.py b/basicswap/chainparams.py index 49ee7a2..1e30cd6 100644 --- a/basicswap/chainparams.py +++ b/basicswap/chainparams.py @@ -566,7 +566,7 @@ def getCoinIdFromTicker(ticker: str) -> str: raise ValueError(f"Unknown coin {ticker}") -def getCoinIdFromName(name: str) -> str: +def getCoinIdFromName(name: str) -> Coins: try: return name_map[name.lower()] except Exception: diff --git a/basicswap/db.py b/basicswap/db.py index 9b49fae..f624207 100644 --- a/basicswap/db.py +++ b/basicswap/db.py @@ -1229,7 +1229,6 @@ class DBMethods: values = {} constraint_values = {} set_columns = [] - for mc in inspect.getmembers(obj.__class__): mc_name, mc_obj = mc if not hasattr(mc_obj, "__sqlite3_column__"): diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 44cb8f6..b77b596 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -7,6 +7,7 @@ # file LICENSE or http://www.opensource.org/licenses/mit-license.php. import base64 +import copy import hashlib import json import logging @@ -530,6 +531,16 @@ class BTCInterface(Secp256k1Interface): ) return self.rpc("getblockheader", [block_hash]) + def getPrevBlockInChain(self, block_header_in): + block_header = copy.deeocopy(block_header_in) + while True: + previousblockhash = block_header.get("previousblockhash", None) + if previousblockhash is None: + raise RuntimeError("previousblockhash not in block_header") + if block_header["confirmations"] > 0: + return block_header + block_header = self.rpc("getblockheader", [previousblockhash]) + def getBlockHeaderAt(self, time_target: int, block_after=False): blockchaininfo = self.rpc("getblockchaininfo") last_block_header = self.rpc( @@ -1310,7 +1321,7 @@ class BTCInterface(Secp256k1Interface): vkbv=None, kbsf=None, ): - # lock refund swipe tx + # Lock refund swipe tx # Sends the coinA locked coin to the follower tx_lock_refund = self.loadTx(tx_lock_refund_bytes) @@ -1584,7 +1595,7 @@ class BTCInterface(Secp256k1Interface): ) if not self.compareFeeRates(fee_rate_paid, feerate): - raise ValueError("Bad fee rate, expected: {}".format(feerate)) + raise ValueError(f"Bad fee rate, expected: {feerate}") return txid, locked_coin, locked_n @@ -1647,7 +1658,7 @@ class BTCInterface(Secp256k1Interface): ) if not self.compareFeeRates(fee_rate_paid, feerate): - raise ValueError("Bad fee rate, expected: {}".format(feerate)) + raise ValueError(f"Bad fee rate, expected: {feerate}") return True @@ -1706,7 +1717,7 @@ class BTCInterface(Secp256k1Interface): ) if not self.compareFeeRates(fee_rate_paid, feerate): - raise ValueError("Bad fee rate, expected: {}".format(feerate)) + raise ValueError(f"Bad fee rate, expected: {feerate}") return True diff --git a/basicswap/interface/dcr/dcr.py b/basicswap/interface/dcr/dcr.py index d20ac6c..ded8ad5 100644 --- a/basicswap/interface/dcr/dcr.py +++ b/basicswap/interface/dcr/dcr.py @@ -1403,7 +1403,7 @@ class DCRInterface(Secp256k1Interface): ) if not self.compareFeeRates(fee_rate_paid, feerate): - raise ValueError("Bad fee rate, expected: {}".format(feerate)) + raise ValueError(f"Bad fee rate, expected: {feerate}") return True @@ -1472,7 +1472,7 @@ class DCRInterface(Secp256k1Interface): ) if not self.compareFeeRates(fee_rate_paid, feerate): - raise ValueError("Bad fee rate, expected: {}".format(feerate)) + raise ValueError(f"Bad fee rate, expected: {feerate}") return txid, locked_coin, locked_n @@ -1533,7 +1533,7 @@ class DCRInterface(Secp256k1Interface): ) if not self.compareFeeRates(fee_rate_paid, feerate): - raise ValueError("Bad fee rate, expected: {}".format(feerate)) + raise ValueError(f"Bad fee rate, expected: {feerate}") return True diff --git a/basicswap/interface/part.py b/basicswap/interface/part.py index a40a99b..88ad3ae 100644 --- a/basicswap/interface/part.py +++ b/basicswap/interface/part.py @@ -658,7 +658,7 @@ class PARTInterfaceBlind(PARTInterface): ensure( self.compareFeeRates(fee_rate_paid, feerate), - "Bad fee rate, expected: {}".format(feerate), + f"Bad fee rate, expected: {feerate}", ) return ( @@ -734,7 +734,7 @@ class PARTInterfaceBlind(PARTInterface): fee_rate_paid = fee_paid * 1000 // vsize ensure( self.compareFeeRates(fee_rate_paid, feerate), - "Bad fee rate, expected: {}".format(feerate), + f"Bad fee rate, expected: {feerate}", ) return True @@ -958,7 +958,7 @@ class PARTInterfaceBlind(PARTInterface): fee_rate_paid = fee_paid * 1000 // vsize self._log.info("vsize, feerate: %ld, %ld", vsize, fee_rate_paid) if not self.compareFeeRates(fee_rate_paid, feerate): - raise ValueError("Bad fee rate, expected: {}".format(feerate)) + raise ValueError(f"Bad fee rate, expected: {feerate}") return True diff --git a/basicswap/util/logging.py b/basicswap/util/logging.py index 370f27c..21a1fe3 100644 --- a/basicswap/util/logging.py +++ b/basicswap/util/logging.py @@ -45,3 +45,21 @@ class BSXLogger(logging.Logger): def info_s(self, msg, *args, **kwargs): if self.safe_logs is False: self.info(msg, *args, **kwargs) + + +class BSXLogAdapter(logging.LoggerAdapter): + def __init__(self, logger, prefix): + super().__init__(logger, {}) + self.prefix = prefix + + def process(self, msg, kwargs): + return f"{self.prefix} {msg}", kwargs + + def addr(self, addr: str) -> str: + return self.logger.addr(addr) + + def id(self, concept_id: bytes, prefix: str = "") -> str: + return self.logger.id(concept_id, prefix) + + def info_s(self, msg, *args, **kwargs): + return self.logger.info_s(msg, *args, **kwargs) diff --git a/tests/basicswap/common.py b/tests/basicswap/common.py index e6c7254..a6c581f 100644 --- a/tests/basicswap/common.py +++ b/tests/basicswap/common.py @@ -290,7 +290,7 @@ def wait_for_event( def wait_for_offer(delay_event, swap_client, offer_id, wait_for=20): - logging.info("wait_for_offer %s", offer_id.hex()) + logging.info(f"wait_for_offer {offer_id.hex()}") for i in range(wait_for): if delay_event.is_set(): raise ValueError("Test stopped.") diff --git a/tests/basicswap/test_other.py b/tests/basicswap/test_other.py index 8ccf936..d8d7035 100644 --- a/tests/basicswap/test_other.py +++ b/tests/basicswap/test_other.py @@ -229,22 +229,24 @@ class Test(unittest.TestCase): == "0dde9df8660d3e0f28fe00d648b70e0323e9c192fe9b94f1cf7138515e877725" ) - sum_secp256k1 = ci_btc.sumPubkeys( + pk_secp256k1 = ci_btc.sumPubkeys( ci_btc.getPubkey(keys[0]), ci_btc.getPubkey(keys[1]) ) assert ( - sum_secp256k1.hex() + pk_secp256k1.hex() == "028c30392e35620af0787b363a03cf9a695336759664436e1f609481c869541a5c" ) - sum_ed25519 = ci_xmr.sumPubkeys( + pk_ed25519 = ci_xmr.sumPubkeys( ci_xmr.getPubkey(keys[0]), ci_xmr.getPubkey(keys[1]) ) assert ( - sum_ed25519.hex() + pk_ed25519.hex() == "4b2dd2dc9acc9be7efed4fdbfb96f0002aeb9e4c8638c5b24562a7158b283626" ) + assert pk_secp256k1 == ci_btc.getPubkey(sum_secp256k1) + def test_ecdsa_otves(self): ci = self.ci_btc() vk_sign = ci.getNewRandomKey() diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index b2a6ba5..aac16c4 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # Copyright (c) 2020-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. @@ -263,9 +263,7 @@ def waitForXMRNode(rpc_offset, max_tries=7): except Exception as ex: if i < max_tries: logging.warning( - "Can't connect to XMR RPC: %s. Retrying in %d second/s.", - str(ex), - (i + 1), + f"Can't connect to XMR RPC: {ex}. Retrying in {i + 1} second/s." ) time.sleep(i + 1) raise ValueError("waitForXMRNode failed") @@ -281,9 +279,7 @@ def waitForXMRWallet(rpc_offset, auth, max_tries=7): except Exception as ex: if i < max_tries: logging.warning( - "Can't connect to XMR wallet RPC: %s. Retrying in %d second/s.", - str(ex), - (i + 1), + f"Can't connect to XMR wallet RPC: {ex}. Retrying in {i + 1} second/s." ) time.sleep(i + 1) raise ValueError("waitForXMRWallet failed") @@ -304,7 +300,7 @@ def run_coins_loop(cls): cls.coins_loop() except Exception as e: logging.warning("run_coins_loop " + str(e)) - test_delay_event.wait(1.0) + test_delay_event.wait(cls.coins_loop_delay) def run_loop(cls): @@ -336,6 +332,7 @@ class BaseTest(unittest.TestCase): xmr_addr = None btc_addr = None ltc_addr = None + coins_loop_delay = 1.0 @classmethod def getRandomPubkey(cls): @@ -345,7 +342,7 @@ class BaseTest(unittest.TestCase): @classmethod def setUpClass(cls): if signal_event.is_set(): - raise ValueError("Test has been cancelled.") + raise ValueError("Test has been cancelled") test_delay_event.clear() random.seed(time.time()) @@ -400,7 +397,7 @@ class BaseTest(unittest.TestCase): cls.prepareTestDir() try: - logging.info("Preparing coin nodes.") + logging.info("Preparing coin nodes") part_wallet_bin = "particl-wallet" + (".exe" if os.name == "nt" else "") for i in range(NUM_NODES): if not cls.restore_instance: @@ -411,7 +408,7 @@ class BaseTest(unittest.TestCase): part_wallet_bin, ) ): - logging.warning(f"{part_wallet_bin} not found.") + logging.warning(f"{part_wallet_bin} not found") else: try: callrpc_cli( @@ -623,10 +620,8 @@ class BaseTest(unittest.TestCase): ) for i in range(NUM_XMR_NODES): - cls.xmr_wallet_auth.append( - ("test{0}".format(i), "test_pass{0}".format(i)) - ) - logging.info("Creating XMR wallet %i", i) + cls.xmr_wallet_auth.append((f"test{i}", f"test_pass{i}")) + logging.info(f"Creating XMR wallet {i}") waitForXMRWallet(i, cls.xmr_wallet_auth[i]) @@ -732,7 +727,7 @@ class BaseTest(unittest.TestCase): wallet="wallet.dat", ) num_blocks = 400 # Mine enough to activate segwit - logging.info("Mining %d Bitcoin blocks to %s", num_blocks, cls.btc_addr) + logging.info(f"Mining {num_blocks} Bitcoin blocks to {cls.btc_addr}") callnoderpc( 0, "generatetoaddress", @@ -763,7 +758,7 @@ class BaseTest(unittest.TestCase): .ci(Coins.BTC) .pubkey_to_segwit_address(void_block_rewards_pubkey) ) - logging.info("Mining %d Bitcoin blocks to %s", num_blocks, cls.btc_addr) + logging.info(f"Mining {num_blocks} Bitcoin blocks to {cls.btc_addr}") callnoderpc( 0, "generatetoaddress", @@ -801,7 +796,7 @@ class BaseTest(unittest.TestCase): wallet="wallet.dat", ) 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,