Merge pull request #482 from gerlofvanek/electrum_fixes_2

Fix: Refund-path / Maturity checks.
This commit is contained in:
tecnovert
2026-05-30 15:16:23 +00:00
committed by GitHub
2 changed files with 112 additions and 15 deletions
+38 -2
View File
@@ -5613,6 +5613,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
) )
# Check non-bip68 final # Check non-bip68 final
if not ci_from.useBackend():
try: try:
txid = ci_from.publishTx(bid.initiate_txn_refund) txid = ci_from.publishTx(bid.initiate_txn_refund)
self.log.error( self.log.error(
@@ -7686,7 +7687,16 @@ 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 TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE not in bid.txns: if (
TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE not in bid.txns
and refund_tx.block_height is not None
and ci_from.isCsvLockMature(
offer.lock_type,
xmr_offer.lock_time_2,
refund_tx.block_height,
refund_tx.block_time,
)
):
try: try:
if self.haveDebugInd( if self.haveDebugInd(
bid.bid_id, bid.bid_id,
@@ -7782,6 +7792,13 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if ( if (
len(xmr_swap.al_lock_refund_tx_sig) > 0 len(xmr_swap.al_lock_refund_tx_sig) > 0
and len(xmr_swap.af_lock_refund_tx_sig) > 0 and len(xmr_swap.af_lock_refund_tx_sig) > 0
and bid.xmr_a_lock_tx is not None
and ci_from.isCsvLockMature(
offer.lock_type,
xmr_offer.lock_time_1,
bid.xmr_a_lock_tx.block_height,
bid.xmr_a_lock_tx.block_time,
)
): ):
try: try:
@@ -8213,6 +8230,19 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
return rv 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)
if parent_tx is None or parent_tx.block_height is None:
return False
return ci.isCsvLockMature(
offer.lock_type,
refund_tx.vin[0].nSequence,
parent_tx.block_height,
parent_tx.block_time,
)
def checkBidState(self, bid_id: bytes, bid, offer): def checkBidState(self, bid_id: bytes, bid, offer):
# assert (self.mxDB.locked()) # assert (self.mxDB.locked())
# Return True to remove bid from in-progress list # Return True to remove bid from in-progress list
@@ -8473,6 +8503,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if ( if (
bid.getITxState() in (TxStates.TX_SENT, TxStates.TX_CONFIRMED) bid.getITxState() in (TxStates.TX_SENT, TxStates.TX_CONFIRMED)
and bid.initiate_txn_refund is not None and bid.initiate_txn_refund is not None
and self._isScriptRefundMature(
ci_from, offer, bid.initiate_txn_refund, bid.initiate_tx
)
): ):
try: try:
txid = ci_from.publishTx(bid.initiate_txn_refund) txid = ci_from.publishTx(bid.initiate_txn_refund)
@@ -8496,6 +8529,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if ( if (
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
)
): ):
try: try:
txid = ci_to.publishTx(bid.participate_txn_refund) txid = ci_to.publishTx(bid.participate_txn_refund)
@@ -8511,7 +8547,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
) )
# State will update when spend is detected # State will update when spend is detected
except Exception as ex: except Exception as ex:
if ci_to.isTxNonFinalError(str(ex)): if ci_to.isTxNonFinalError(str(ex)) is False:
self.log.warning( self.log.warning(
f"Error trying to submit participate refund txn: {ex}" f"Error trying to submit participate refund txn: {ex}"
) )
+65 -4
View File
@@ -502,6 +502,67 @@ class BTCInterface(Secp256k1Interface):
return height return height
return self.rpc("getblockcount") return self.rpc("getblockcount")
def getChainMedianTime(self) -> int:
if self.useBackend():
import struct
backend = self.getBackend()
if not backend:
raise ValueError("No electrum backend available")
height = backend.getBlockHeight()
start = max(0, height - 10)
count = height - start + 1
result = backend._server.call("blockchain.block.headers", [start, count])
header_bytes = bytes.fromhex(result["hex"])
returned = result.get("count", count)
times = [
struct.unpack("<I", header_bytes[i * 80 + 68 : i * 80 + 72])[0]
for i in range(returned)
]
times.sort()
return times[len(times) // 2]
return self.rpc("getblockchaininfo")["mediantime"]
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 getMempoolTx(self, txid): def getMempoolTx(self, txid):
if self._connection_type == "electrum": if self._connection_type == "electrum":
backend = self.getBackend() backend = self.getBackend()
@@ -4370,11 +4431,11 @@ class BTCInterface(Secp256k1Interface):
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:
err_lower = err_str.lower()
return ( return (
"non-BIP68-final" in err_str "non-bip68-final" in err_lower
or "non-final" in err_str or "non-final" in err_lower
or "Missing inputs" in err_str or "locktime requirement not satisfied" in err_lower
or "bad-txns-inputs-missingorspent" in err_str
) )
def combine_non_segwit_prevouts(self): def combine_non_segwit_prevouts(self):