fix: get refund tx block info for CSV check

This commit is contained in:
tecnovert
2026-06-05 22:56:26 +02:00
parent 3264a5845e
commit 7655f1ad81
28 changed files with 424 additions and 196 deletions
+1 -1
View File
@@ -92,7 +92,7 @@ jobs:
export PARTICL_BINDIR="$BIN_DIR/particl" export PARTICL_BINDIR="$BIN_DIR/particl"
export BITCOIN_BINDIR="$BIN_DIR/bitcoin" export BITCOIN_BINDIR="$BIN_DIR/bitcoin"
export XMR_BINDIR="$BIN_DIR/monero" 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 - name: Run test_encrypted_xmr_reload
id: test_encrypted_xmr_reload id: test_encrypted_xmr_reload
run: | run: |
+54 -11
View File
@@ -7747,7 +7747,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.logBidEvent( self.logBidEvent(
bid.bid_id, bid.bid_id,
EventLogTypes.DEBUG_TWEAK_APPLIED, EventLogTypes.DEBUG_TWEAK_APPLIED,
"ind {}".format(bid.debug_ind), f"ind {bid.debug_ind}",
cursor, cursor,
) )
self.commitDB() self.commitDB()
@@ -7802,6 +7802,23 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.saveBidInSession(bid_id, bid, cursor, xmr_swap) self.saveBidInSession(bid_id, bid, cursor, xmr_swap)
self.commitDB() 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 ( if (
TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE not in bid.txns TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE not in bid.txns
and refund_tx.block_height is not None and refund_tx.block_height is not None
@@ -7951,6 +7968,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
bid_id=bid_id, bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND,
txid=bytes.fromhex(txid), txid=bytes.fromhex(txid),
vout=0,
) )
self.saveBidInSession(bid_id, bid, cursor, xmr_swap) self.saveBidInSession(bid_id, bid, cursor, xmr_swap)
self.commitDB() self.commitDB()
@@ -7966,6 +7984,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
bid_id=bid_id, bid_id=bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND,
txid=txid, txid=txid,
vout=0,
) )
self.saveBidInSession(bid_id, bid, cursor, xmr_swap) self.saveBidInSession(bid_id, bid, cursor, xmr_swap)
self.commitDB() self.commitDB()
@@ -8346,14 +8365,15 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
return rv return rv
def _isScriptRefundMature(self, ci, offer, refund_tx_bytes, parent_tx) -> bool: 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): 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: if parent_tx is None or parent_tx.block_height is None:
return False return False
txi_sequence: int = ci.getTxInSequence(refund_tx_bytes, 0)
return ci.isCsvLockMature( return ci.isCsvLockMature(
offer.lock_type, offer.lock_type,
refund_tx.vin[0].nSequence, txi_sequence,
parent_tx.block_height, parent_tx.block_height,
parent_tx.block_time, parent_tx.block_time,
) )
@@ -8641,12 +8661,33 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
f"Error trying to submit initiate refund txn: {ex}" f"Error trying to submit initiate refund txn: {ex}"
) )
if ( should_try_refund_ptx: bool = (
bid.getPTxState() in (TxStates.TX_SENT, TxStates.TX_CONFIRMED) bid.getPTxState() in (TxStates.TX_SENT, TxStates.TX_CONFIRMED)
and bid.participate_txn_refund is not None 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: try:
txid = ci_to.publishTx(bid.participate_txn_refund) txid = ci_to.publishTx(bid.participate_txn_refund)
@@ -9021,6 +9062,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
bid_id=bid.bid_id, bid_id=bid.bid_id,
tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND,
txid=xmr_swap.a_lock_refund_tx_id, txid=xmr_swap.a_lock_refund_tx_id,
vout=0,
) )
else: else:
self.setBidError( self.setBidError(
@@ -9140,6 +9182,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if was_received: if was_received:
if self.isBchXmrSwap(offer): if self.isBchXmrSwap(offer):
# Mercy tx is sent separately # Mercy tx is sent separately
# Can't set XMR_SWAP_FAILED_SWIPED, as bid should continue looking for mercy tx
pass pass
else: else:
# Look for a mercy output # Look for a mercy output
@@ -11218,7 +11261,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
refundExtraArgs = dict() refundExtraArgs = dict()
lockExtraArgs = dict() lockExtraArgs = dict()
if self.isBchXmrSwap(offer): 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 # and prepare extra args for validation
bch_ci = self.ci(Coins.BCH) bch_ci = self.ci(Coins.BCH)
@@ -14082,9 +14125,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
walletinfo = ci.getWalletInfo() walletinfo = ci.getWalletInfo()
rv = { rv = {
"deposit_address": self.getCachedAddressForCoin(coin), "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( "unconfirmed": ci.format_amount(
walletinfo["unconfirmed_balance"], conv_int=True walletinfo["unconfirmed_balance"], conv_int=True, r=-1
), ),
"expected_seed": ci.knownWalletSeed(), "expected_seed": ci.knownWalletSeed(),
"encrypted": walletinfo["encrypted"], "encrypted": walletinfo["encrypted"],
@@ -14099,7 +14142,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if "immature_balance" in walletinfo: if "immature_balance" in walletinfo:
rv["immature"] = ci.format_amount( rv["immature"] = ci.format_amount(
walletinfo["immature_balance"], conv_int=True walletinfo["immature_balance"], conv_int=True, r=-1
) )
if "locked_utxos" in walletinfo: if "locked_utxos" in walletinfo:
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert # Copyright (c) 2024 tecnovert
+29 -8
View File
@@ -147,7 +147,9 @@ class BCHInterface(BTCInterface):
if not self.isAddressMine(address, or_watch_only=True): if not self.isAddressMine(address, or_watch_only=True):
# Expects P2WSH nested in BIP16_P2SH # 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 return address
@@ -156,15 +158,23 @@ class BCHInterface(BTCInterface):
def createRawFundedTransaction( def createRawFundedTransaction(
self, self,
addr_to: str, addr_to: str | bytes,
amount: int, amount: int,
sub_fee: bool = False, sub_fee: bool = False,
lock_unspents: bool = True, lock_unspents: bool = True,
feerate: int = None, feerate: int = None,
) -> str: ) -> 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: if feerate:
fee_rate = self.format_amount(feerate) fee_rate = self.format_amount(feerate)
@@ -228,6 +238,16 @@ class BCHInterface(BTCInterface):
) )
return pay_fee 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): def findTxnByHash(self, txid_hex: str):
# Only works for wallet txns # Only works for wallet txns
try: try:
@@ -282,7 +302,7 @@ class BCHInterface(BTCInterface):
found_vout = try_vout found_vout = try_vout
break break
except Exception as e: # noqa: F841 except Exception as e: # noqa: F841
# self._log.warning('gettxout {}'.format(e)) # self._log.warning(f"gettxout {e}")
return None return None
if found_vout is None: if found_vout is None:
@@ -295,7 +315,7 @@ class BCHInterface(BTCInterface):
# TODO: Better way? # TODO: Better way?
if confirmations > 0: if confirmations > 0:
block_height = self.getChainHeight() - confirmations block_height = self.getChainHeight() - (confirmations - 1)
rv = { rv = {
"txid": txid.hex(), "txid": txid.hex(),
@@ -516,6 +536,7 @@ class BCHInterface(BTCInterface):
tx_lock = self.loadTx(tx_lock_bytes) tx_lock = self.loadTx(tx_lock_bytes)
output_script = self.getScriptDest(script_lock) output_script = self.getScriptDest(script_lock)
locked_n = findOutput(tx_lock, output_script) locked_n = findOutput(tx_lock, output_script)
ensure(locked_n is not None, "Output not found in tx") ensure(locked_n is not None, "Output not found in tx")
locked_coin = tx_lock.vout[locked_n].nValue 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_value = refund_swipe_tx.vout[0].nValue
refund_output_script = refund_swipe_tx.vout[0].scriptPubKey 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 # one op_return with mercy information, a dust output to the leader and change back to the follower
tx_size = 275 tx_size = 275
dust_limit = 546 dust_limit = 546
+119 -52
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # Copyright (c) 2020-2024 tecnovert
@@ -508,6 +507,14 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
return height return height
return self.rpc("getblockcount") 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: def getChainMedianTime(self) -> int:
if self.useBackend(): if self.useBackend():
import struct import struct
@@ -601,7 +608,7 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
block_hash = sha256(sha256(header_bytes))[::-1].hex() block_hash = sha256(sha256(header_bytes))[::-1].hex()
return {"height": height, "hash": block_hash, "time": block_time} 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": if self._connection_type == "electrum":
raise NotImplementedError( raise NotImplementedError(
"getBlockHeader by hash not available in electrum mode" "getBlockHeader by hash not available in electrum mode"
@@ -2785,6 +2792,47 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
) )
return pay_fee 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( def spendBLockTx(
self, self,
chain_b_lock_txid: bytes, chain_b_lock_txid: bytes,
@@ -2798,48 +2846,14 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
lock_tx_vout=None, lock_tx_vout=None,
) -> bytes: ) -> bytes:
self._log.info( self._log.info(
"spendBLockTx: {} {}\n".format( f"spendBLockTx: {self._log.id(chain_b_lock_txid)} {lock_tx_vout}\n"
self._log.id(chain_b_lock_txid), lock_tx_vout
)
) )
Kbs = self.getPubkey(kbs) Kbs = self.getPubkey(kbs)
script_pk = self.getPkDest(Kbs) script_pk = self.getPkDest(Kbs)
locked_n = None locked_n, actual_value = self.getBLockTxo(
actual_value = None chain_b_lock_txid, lock_tx_vout, script_pk
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
if ( if (
locked_n is not None locked_n is not None
@@ -2848,7 +2862,7 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
): ):
self._log.warning( self._log.warning(
f"spendBLockTx: Stored vout {lock_tx_vout} differs from actual vout {locked_n} " 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") 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 # Add watchonly address and rescan if required
if not self.isAddressMine(dest_address, or_watch_only=True): if not self.isAddressMine(dest_address, or_watch_only=True):
self.importWatchOnlyAddress(dest_address, "bid") self.importWatchOnlyAddress(dest_address, "bid")
self._log.info(f"Imported watch-only addr: {self._log.addr(dest_address)}")
self._log.info( self._log.info(
"Imported watch-only addr: {}".format(self._log.addr(dest_address)) f"Rescanning {self.coin_name()} chain from height: {rescan_from}"
)
self._log.info(
"Rescanning {} chain from height: {}".format(
self.coin_name(), rescan_from
)
) )
self.rpc_wallet("rescanblockchain", [rescan_from]) self.rpc_wallet("rescanblockchain", [rescan_from])
@@ -4457,6 +4467,8 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
return None return None
def isTxExistsError(self, err_str: str) -> bool: 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 return "Transaction already in block chain" in err_str
def isTxNonFinalError(self, err_str: str) -> bool: def isTxNonFinalError(self, err_str: str) -> bool:
@@ -4511,7 +4523,7 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
self._log.id(bytes.fromhex(tx["txid"])) self._log.id(bytes.fromhex(tx["txid"]))
) )
) )
self.publishTx(tx_signed) self.publishTx(bytes.fromhex(tx_signed))
return tx["txid"] return tx["txid"]
@@ -4549,10 +4561,65 @@ class BTCInterface(FeeValidator, Secp256k1Interface):
return bytes.fromhex(txi_txid_hex), fee_rate 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(): try:
print("TODO: testBTCInterface") 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__": def getTxOutInfo(
testBTCInterface() 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
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2022-2024 tecnovert # Copyright (c) 2022-2024 tecnovert
+146 -27
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert # Copyright (c) 2024 tecnovert
@@ -13,7 +12,7 @@ import logging
import random import random
import traceback import traceback
from typing import List from typing import List, Optional
from basicswap.basicswap_util import getVoutByScriptPubKey, TxLockTypes from basicswap.basicswap_util import getVoutByScriptPubKey, TxLockTypes
from basicswap.chainparams import Coins from basicswap.chainparams import Coins
@@ -419,8 +418,10 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
return bci return bci
def getBlockHeader(self, block_hash: str) -> dict:
return self.rpc("getblockheader", [block_hash])
def getWalletInfo(self): def getWalletInfo(self):
rv = {}
rv = self.rpc_wallet("getinfo") rv = self.rpc_wallet("getinfo")
wi = self.rpc_wallet("walletinfo") wi = self.rpc_wallet("walletinfo")
balances = self.rpc_wallet("getbalance") balances = self.rpc_wallet("getbalance")
@@ -595,7 +596,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
override_feerate = chain_client_settings.get("override_feerate", None) override_feerate = chain_client_settings.get("override_feerate", None)
if override_feerate: if override_feerate:
self._log.debug( 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" return override_feerate, "override_feerate"
@@ -919,7 +920,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
found_vout = try_vout found_vout = try_vout
break break
except Exception as e: # noqa: F841 except Exception as e: # noqa: F841
# self._log.warning('gettxout {}'.format(e)) # self._log.warning(f"gettxout {e})
return None return None
if found_vout is None: if found_vout is None:
@@ -932,7 +933,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
# TODO: Better way? # TODO: Better way?
if confirmations > 0: if confirmations > 0:
block_height = self.getChainHeight() - confirmations block_height = self.getChainHeight() - (confirmations - 1)
rv = { rv = {
"txid": txid.hex(), "txid": txid.hex(),
@@ -999,6 +1000,10 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
tx.vout.append(self.txoType()(output_value, script)) tx.vout.append(self.txoType()(output_value, script))
return tx.serialize().hex() 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): def verifyRawTransaction(self, tx_hex: str, prevouts):
inputs_valid: bool = True inputs_valid: bool = True
validscripts: int = 0 validscripts: int = 0
@@ -1151,6 +1156,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock) dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
pay_fee = round(tx_fee_rate * size / 1000) pay_fee = round(tx_fee_rate * size / 1000)
tx.vout[0].value = locked_coin - pay_fee tx.vout[0].value = locked_coin - pay_fee
@@ -1202,6 +1208,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock) dummy_witness_stack = self.getScriptLockTxDummyWitness(script_lock)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
pay_fee = round(tx_fee_rate * size / 1000) pay_fee = round(tx_fee_rate * size / 1000)
tx.vout[0].value = locked_coin - pay_fee tx.vout[0].value = locked_coin - pay_fee
@@ -1253,6 +1260,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
script_lock_refund script_lock_refund
) )
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
pay_fee = round(tx_fee_rate * size / 1000) pay_fee = round(tx_fee_rate * size / 1000)
tx.vout[0].value = locked_coin - pay_fee tx.vout[0].value = locked_coin - pay_fee
@@ -1337,6 +1345,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
assert fee_paid > 0 assert fee_paid > 0
size = len(tx.serialize()) + add_witness_bytes size = len(tx.serialize()) + add_witness_bytes
size += 1
fee_rate_paid = fee_paid * 1000 // size fee_rate_paid = fee_paid * 1000 // size
self._log.info( self._log.info(
@@ -1398,6 +1407,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
dummy_witness_stack = self.getScriptLockTxDummyWitness(lock_tx_script) dummy_witness_stack = self.getScriptLockTxDummyWitness(lock_tx_script)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
fee_rate_paid = fee_paid * 1000 // size fee_rate_paid = fee_paid * 1000 // size
self._log.info( self._log.info(
@@ -1470,6 +1480,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
dummy_witness_stack = self.getScriptLockTxDummyWitness(prevout_script) dummy_witness_stack = self.getScriptLockTxDummyWitness(prevout_script)
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
fee_rate_paid = fee_paid * 1000 // size fee_rate_paid = fee_paid * 1000 // size
self._log.info( self._log.info(
@@ -1531,6 +1542,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
prevout_script prevout_script
) )
size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack)) size = len(self.setTxSignature(tx.serialize(), dummy_witness_stack))
size += 1
fee_rate_paid = fee_paid * 1000 // size fee_rate_paid = fee_paid * 1000 // size
self._log.info( self._log.info(
@@ -1781,32 +1793,41 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
spend_actual_balance: bool = False, spend_actual_balance: bool = False,
lock_tx_vout=None, lock_tx_vout=None,
) -> bytes: ) -> 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) Kbs = self.getPubkey(kbs)
script_pk = self.getPkDest(Kbs) script_pk = self.getPkDest(Kbs)
locked_n = None locked_n = None
actual_value = None actual_value = None
wtx = self.rpc_wallet( try:
"gettransaction", wtx = self.rpc_wallet(
[ "gettransaction",
chain_b_lock_txid.hex(), [
], 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)}"
) )
for i, out in enumerate(lock_tx.vout): lock_tx = self.loadTx(bytes.fromhex(wtx["hex"]))
self._log.debug( locked_n = findOutput(lock_tx, script_pk)
f" vout[{i}]: value={out.value}, scriptPubKey={out.scriptPubKey.hex()}" 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 ( if (
locked_n is not None locked_n is not None
@@ -1815,7 +1836,7 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
): ):
self._log.warning( self._log.warning(
f"spendBLockTx: Stored vout {lock_tx_vout} differs from actual vout {locked_n} " 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") ensure(locked_n is not None, "Output not found in tx")
@@ -1851,14 +1872,14 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
try: try:
txout = self.rpc("gettxout", [txid_hex, 0, 0, True]) txout = self.rpc("gettxout", [txid_hex, 0, 0, True])
except Exception as e: # noqa: F841 except Exception as e: # noqa: F841
# self._log.warning('gettxout {}'.format(e)) # self._log.warning(f"gettxout {e}"))
return None return None
confirmations: int = ( confirmations: int = (
0 if "confirmations" not in txout else txout["confirmations"] 0 if "confirmations" not in txout else txout["confirmations"]
) )
if confirmations >= self.blocks_confirmed: 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 {"txid": txid_hex, "amount": 0, "height": block_height}
return None return None
@@ -1873,3 +1894,101 @@ class DCRInterface(FeeValidator, Secp256k1Interface):
def isTxNonFinalError(self, err_str: str) -> bool: def isTxNonFinalError(self, err_str: str) -> bool:
return "locks on inputs not met" in err_str 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)
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert # Copyright (c) 2024 tecnovert
+5 -4
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert # Copyright (c) 2024 tecnovert
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -9,10 +10,10 @@ import traceback
from basicswap.rpc import Jsonrpc 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: try:
url = "http://{}@{}:{}/".format(auth, host, rpc_port) url = "http://{}@{}:{}/".format(auth, host, rpc_port)
x = Jsonrpc(url) x = Jsonrpc(url, timeout=timeout if timeout else 10)
x.__handler = None x.__handler = None
v = x.json_request(method, params) v = x.json_request(method, params)
x.close() x.close()
@@ -41,7 +42,7 @@ def make_rpc_func(port, auth, host="127.0.0.1"):
auth = auth auth = auth
host = host host = host
def rpc_func(method, params=None): def rpc_func(method, params=None, timeout=None):
return callrpc(port, auth, method, params, host) return callrpc(port, auth, method, params, host, timeout=timeout)
return rpc_func return rpc_func
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024 The BasicSwap developers # Copyright (c) 2024 The BasicSwap developers
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024-2026 The Basicswap developers # Copyright (c) 2024-2026 The Basicswap developers
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert # Copyright (c) 2022-2023 tecnovert
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert # Copyright (c) 2020-2023 tecnovert
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2023 tecnovert # Copyright (c) 2023 tecnovert
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2022 tecnovert # Copyright (c) 2020-2022 tecnovert
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # Copyright (c) 2020-2024 tecnovert
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2021 tecnovert # Copyright (c) 2021 tecnovert
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert # Copyright (c) 2022 tecnovert
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024 The Basicswap developers # Copyright (c) 2024 The Basicswap developers
-1
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # Copyright (c) 2020-2024 tecnovert
+13 -4
View File
@@ -209,7 +209,7 @@ class XmrSwapInterface(ProtocolInterface):
) )
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript: 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): if hasattr(ci, "genScriptLockTxScript") and callable(ci.genScriptLockTxScript):
return ci.genScriptLockTxScript(ci, Kal, Kaf, **kwargs) return ci.genScriptLockTxScript(ci, Kal, Kaf, **kwargs)
@@ -221,7 +221,12 @@ class XmrSwapInterface(ProtocolInterface):
def getFundedInitiateTxTemplate( def getFundedInitiateTxTemplate(
self, ci, amount: int, sub_fee: bool, feerate: int = None self, ci, amount: int, sub_fee: bool, feerate: int = None
) -> bytes: ) -> 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( funded_tx = ci.createRawFundedTransaction(
addr_to, amount, sub_fee, lock_unspents=False, feerate=feerate addr_to, amount, sub_fee, lock_unspents=False, feerate=feerate
) )
@@ -247,8 +252,12 @@ class XmrSwapInterface(ProtocolInterface):
return lock_vout return lock_vout
def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray: def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray:
mock_txo_script = self.getMockScriptScriptPubkey(ci) if ci.coin_type() == Coins.BCH:
real_txo_script = ci.getScriptDest(script) 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 found: int = 0
ctx = ci.loadTx(mock_tx, allow_witness=False) ctx = ci.loadTx(mock_tx, allow_witness=False)
+1
View File
@@ -22,6 +22,7 @@
- Fixed feerate from other chain displayed for reversed swaps. - Fixed feerate from other chain displayed for reversed swaps.
- Added warning text for fee above 1.2 x local estimate. - Added warning text for fee above 1.2 x local estimate.
- Added subfee bid option. - Added subfee bid option.
- Increase DCR fee estimate by 1 byte.
0.14.5 0.14.5
+6
View File
@@ -236,11 +236,17 @@ def wait_for_bid(
) )
if isinstance(state, (list, tuple)): if isinstance(state, (list, tuple)):
if bid[5] in state: 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 return
else: else:
continue continue
elif state is not None and state != bid[5]: elif state is not None and state != bid[5]:
continue continue
swap_client.log.debug(
f"TEST: wait_for_bid found {bid_id.hex()}: Bid state {bid[5]}, target {state}."
)
return return
else: else:
if i > 0 and i % 10 == 0: if i > 0 and i % 10 == 0:
+11 -28
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert # 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 # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # 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 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): def run_test_success_path(self, coin_from: Coins, coin_to: Coins):
logging.info(f"---------- Test {coin_from.name} to {coin_to.name}") logging.info(f"---------- Test {coin_from.name} to {coin_to.name}")
@@ -878,15 +860,16 @@ class Test(BaseTest):
"use_csv": True, "use_csv": True,
"use_segwit": True, "use_segwit": True,
"blocks_confirmed": 1, "blocks_confirmed": 1,
"min_relay_fee": 0.00001,
} }
def test_0001_decred_address(self): 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 = {"rpcport": 0, "rpcauth": "none"}
coin_settings.update(REQUIRED_SETTINGS) coin_settings.update(REQUIRED_SETTINGS)
ci = DCRInterface(coin_settings, "mainnet") ci = DCRInterface(coin_settings, "mainnet", self.swap_clients[0])
k = ci.getNewRandomKey() k = ci.getNewRandomKey()
K = ci.getPubkey(k) K = ci.getPubkey(k)
@@ -914,7 +897,7 @@ class Test(BaseTest):
assert hash160(masterpubkey_data) == seed_hash assert hash160(masterpubkey_data) == seed_hash
def test_001_segwit(self): 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 swap_clients = self.swap_clients
ci0 = swap_clients[0].ci(self.test_coin) ci0 = swap_clients[0].ci(self.test_coin)
@@ -972,7 +955,7 @@ class Test(BaseTest):
assert f_decoded["txid"] == ctx.TxHash().hex() assert f_decoded["txid"] == ctx.TxHash().hex()
def test_003_signature_hash(self): 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 # Test that signing a transaction manually produces the same result when signed with the wallet
swap_clients = self.swap_clients swap_clients = self.swap_clients
@@ -1047,7 +1030,7 @@ class Test(BaseTest):
assert len(sent_txid) == 64 assert len(sent_txid) == 64
def test_004_csv(self): 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 swap_clients = self.swap_clients
ci0 = swap_clients[0].ci(self.test_coin) ci0 = swap_clients[0].ci(self.test_coin)
@@ -1161,7 +1144,7 @@ class Test(BaseTest):
assert sent_spend_txid is not None assert sent_spend_txid is not None
def test_005_watchonly(self): 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 swap_clients = self.swap_clients
ci0 = swap_clients[0].ci(self.test_coin) ci0 = swap_clients[0].ci(self.test_coin)
@@ -1261,7 +1244,7 @@ class Test(BaseTest):
assert found_txid is not None assert found_txid is not None
def test_008_gettxout(self): 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) ci0 = self.swap_clients[0].ci(self.test_coin)
@@ -1373,7 +1356,7 @@ class Test(BaseTest):
assert amount_proved >= require_amount assert amount_proved >= require_amount
def test_009_wallet_encryption(self): 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"): for coin in ("part", "dcr", "xmr"):
jsw = read_json_api(1800, f"wallets/{coin}") jsw = read_json_api(1800, f"wallets/{coin}")
@@ -1412,7 +1395,7 @@ class Test(BaseTest):
assert jsw["locked"] is False assert jsw["locked"] is False
def test_010_txn_size(self): 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 swap_clients = self.swap_clients
ci = swap_clients[0].ci(self.test_coin) ci = swap_clients[0].ci(self.test_coin)
+17 -19
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- 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 # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -167,6 +167,7 @@ class TestBCH(BasicSwapTest):
@classmethod @classmethod
def prepareExtraCoins(cls): def prepareExtraCoins(cls):
super().prepareExtraCoins()
cls.bch_addr = callnoderpc( cls.bch_addr = callnoderpc(
0, 0,
"getnewaddress", "getnewaddress",
@@ -197,11 +198,12 @@ class TestBCH(BasicSwapTest):
"datadir": os.path.join(datadir, "bch_" + str(node_id)), "datadir": os.path.join(datadir, "bch_" + str(node_id)),
"bindir": BITCOINCASH_BINDIR, "bindir": BITCOINCASH_BINDIR,
"use_segwit": False, "use_segwit": False,
"wallet_name": "bsx_wallet",
} }
@classmethod @classmethod
def coins_loop(cls): def coins_loop(cls):
super(TestBCH, cls).coins_loop() super().coins_loop()
ci0 = cls.swap_clients[0].ci(cls.test_coin) ci0 = cls.swap_clients[0].ci(cls.test_coin)
try: try:
if cls.bch_addr is not None: if cls.bch_addr is not None:
@@ -212,7 +214,7 @@ class TestBCH(BasicSwapTest):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
logging.info("Finalising Bitcoincash Test") logging.info("Finalising Bitcoincash Test")
super(TestBCH, cls).tearDownClass() super().tearDownClass()
stopDaemons(cls.bch_daemons) stopDaemons(cls.bch_daemons)
cls.bch_daemons.clear() cls.bch_daemons.clear()
@@ -224,19 +226,15 @@ class TestBCH(BasicSwapTest):
return True return True
def test_001_nested_segwit(self): def test_001_nested_segwit(self):
logging.info( logging.info(f"---------- Test {self.test_coin.name} p2sh nested segwit")
"---------- Test {} p2sh nested segwit".format(self.test_coin.name)
)
logging.info("Skipped") logging.info("Skipped")
def test_002_native_segwit(self): def test_002_native_segwit(self):
logging.info( logging.info(f"---------- Test {self.test_coin.name} p2sh native segwit")
"---------- Test {} p2sh native segwit".format(self.test_coin.name)
)
logging.info("Skipped") logging.info("Skipped")
def test_003_cltv(self): 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) ci = self.swap_clients[0].ci(self.test_coin)
@@ -348,7 +346,7 @@ class TestBCH(BasicSwapTest):
assert len(tx_wallet["blockhash"]) == 64 assert len(tx_wallet["blockhash"]) == 64
def test_004_csv(self): 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) ci = self.swap_clients[0].ci(self.test_coin)
@@ -451,7 +449,7 @@ class TestBCH(BasicSwapTest):
assert len(tx_wallet["blockhash"]) == 64 assert len(tx_wallet["blockhash"]) == 64
def test_005_watchonly(self): 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) ci = self.swap_clients[0].ci(self.test_coin)
ci1 = self.swap_clients[1].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() super().test_006_getblock_verbosity()
def test_007_hdwallet(self): 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_seed = "8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b"
test_wif = ( test_wif = (
@@ -506,10 +504,10 @@ class TestBCH(BasicSwapTest):
super().test_009_scantxoutset() super().test_009_scantxoutset()
def test_010_txn_size(self): 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 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) pi = swap_clients[0].pi(SwapTypes.XMR_SWAP)
amount: int = ci.make_int(random.uniform(0.1, 2.0), r=1) 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): def test_011_p2sh(self):
# Not used in bsx for native-segwit coins # 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) ci = self.swap_clients[0].ci(self.test_coin)
@@ -717,7 +715,7 @@ class TestBCH(BasicSwapTest):
def test_011_p2sh32(self): def test_011_p2sh32(self):
# Not used in bsx for native-segwit coins # 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) ci = self.swap_clients[0].ci(self.test_coin)
@@ -806,7 +804,7 @@ class TestBCH(BasicSwapTest):
assert len(tx_wallet["blockhash"]) == 64 assert len(tx_wallet["blockhash"]) == 64
def test_012_p2sh_p2wsh(self): 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") logging.info("Skipped")
def test_01_a_full_swap(self): def test_01_a_full_swap(self):
@@ -877,7 +875,7 @@ class TestBCH(BasicSwapTest):
def test_06_preselect_inputs(self): def test_06_preselect_inputs(self):
tla_from = self.test_coin.name 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") logging.info("Skipped")
def test_07_expire_stuck_accepted(self): def test_07_expire_stuck_accepted(self):
+10 -8
View File
@@ -74,6 +74,7 @@ class TestFunctions(BaseTest):
@classmethod @classmethod
def prepareExtraCoins(cls): def prepareExtraCoins(cls):
# Save sent messages so tests can count them
for sc in cls.swap_clients: for sc in cls.swap_clients:
sc._smsg_add_to_outbox = True 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: 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 # Offerer sends the offer
# Bidder sends the bid # Bidder sends the bid
@@ -459,6 +460,12 @@ class TestFunctions(BaseTest):
if with_mercy if with_mercy
else (BidStates.BID_STALLED_FOR_TEST, BidStates.XMR_SWAP_FAILED_SWIPED) 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( wait_for_bid(
test_delay_event, test_delay_event,
swap_clients[id_leader], swap_clients[id_leader],
@@ -920,7 +927,7 @@ class BasicSwapTest(TestFunctions):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(BasicSwapTest, cls).setUpClass() super().setUpClass()
@classmethod @classmethod
def addCoinSettings(cls, settings, datadir, node_id): def addCoinSettings(cls, settings, datadir, node_id):
@@ -2480,11 +2487,6 @@ class BasicSwapTest(TestFunctions):
def test_09_expire_accepted_rev(self): def test_09_expire_accepted_rev(self):
self.do_test_09_expire_accepted(Coins.XMR, self.test_coin_from) 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): def test_11_fee_validation(self):
coin_from, coin_to = (self.test_coin_from, Coins.XMR) coin_from, coin_to = (self.test_coin_from, Coins.XMR)
logging.info( logging.info(
@@ -2822,7 +2824,7 @@ class TestBTC_PARTB(TestFunctions):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(TestBTC_PARTB, cls).setUpClass() super().setUpClass()
if False: if False:
for client in cls.swap_clients: for client in cls.swap_clients:
client.log.safe_logs = True client.log.safe_logs = True
+6 -10
View File
@@ -115,6 +115,10 @@ def modify_config(test_path, i):
with open(config_path, "w") as fp: with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4) 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( def wait_for_bid_state(
delay_event, node_port: int, bid_id: str, state=None, wait_for: int = 30 delay_event, node_port: int, bid_id: str, state=None, wait_for: int = 30
@@ -641,7 +645,7 @@ class Test(TestFunctions):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
cls.addElectrumxDaemon("bitcoin", 32793, 50001) cls.addElectrumxDaemon("bitcoin", 32793, 50001)
super(Test, cls).setUpClass() super().setUpClass()
@classmethod @classmethod
def modifyConfig(cls, test_path, i): def modifyConfig(cls, test_path, i):
@@ -754,14 +758,6 @@ class Test(TestFunctions):
self.delay_event, self.delay_event,
self.test_coin_b, self.test_coin_b,
100, 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_0,
self.port_node_1, self.port_node_1,
True, True,
@@ -788,7 +784,7 @@ class Test(TestFunctions):
True, True,
) )
self.do_test_03_follower_recover_a_lock_tx( 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): def test_03_b_follower_recover_a_lock_tx_reverse(self):
+6 -10
View File
@@ -48,15 +48,11 @@ class TestLTC(BasicSwapTest):
assert deploymentinfo["softforks"][feature_name]["active"] is True assert deploymentinfo["softforks"][feature_name]["active"] is True
def test_001_nested_segwit(self): def test_001_nested_segwit(self):
logging.info( logging.info(f"---------- Test {self.test_coin_from.name} p2sh nested segwit")
"---------- Test {} p2sh nested segwit".format(self.test_coin_from.name)
)
logging.info("Skipped") logging.info("Skipped")
def test_002_native_segwit(self): def test_002_native_segwit(self):
logging.info( logging.info(f"---------- Test {self.test_coin_from.name} p2sh native segwit")
"---------- Test {} p2sh native segwit".format(self.test_coin_from.name)
)
ci = self.swap_clients[0].ci(self.test_coin_from) ci = self.swap_clients[0].ci(self.test_coin_from)
addr_segwit = ci.rpc_wallet("getnewaddress", ["segwit test", "bech32"]) 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"] assert tx_funded_decoded["txid"] == tx_signed_decoded["txid"]
def test_007_hdwallet(self): 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_seed = "8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b"
test_wif = ( test_wif = (
@@ -136,7 +132,7 @@ class TestLTC(BasicSwapTest):
assert addr == "rltc1qps7hnjd866e9ynxadgseprkc2l56m00djr82la" assert addr == "rltc1qps7hnjd866e9ynxadgseprkc2l56m00djr82la"
def test_20_btc_coin(self): 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 swap_clients = self.swap_clients
offer_id = swap_clients[0].postOffer( 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 assert js_1["num_swapping"] == 0 and js_1["num_watched_outputs"] == 0
def test_21_mweb(self): 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 swap_clients = self.swap_clients
ci0 = swap_clients[0].ci(self.test_coin_from) ci0 = swap_clients[0].ci(self.test_coin_from)
@@ -327,7 +323,7 @@ class TestLTC(BasicSwapTest):
# TODO # TODO
def test_22_mweb_balance(self): 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 swap_clients = self.swap_clients
ci_mweb = swap_clients[0].ci(Coins.LTC_MWEB) ci_mweb = swap_clients[0].ci(Coins.LTC_MWEB)