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
if not ci_from.useBackend():
try:
txid = ci_from.publishTx(bid.initiate_txn_refund)
self.log.error(
@@ -7686,7 +7687,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.saveBidInSession(bid_id, bid, cursor, xmr_swap)
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:
if self.haveDebugInd(
bid.bid_id,
@@ -7782,6 +7792,13 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if (
len(xmr_swap.al_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:
@@ -8213,6 +8230,19 @@ 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)
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):
# assert (self.mxDB.locked())
# Return True to remove bid from in-progress list
@@ -8473,6 +8503,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if (
bid.getITxState() in (TxStates.TX_SENT, TxStates.TX_CONFIRMED)
and bid.initiate_txn_refund is not None
and self._isScriptRefundMature(
ci_from, offer, bid.initiate_txn_refund, bid.initiate_tx
)
):
try:
txid = ci_from.publishTx(bid.initiate_txn_refund)
@@ -8496,6 +8529,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if (
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
)
):
try:
txid = ci_to.publishTx(bid.participate_txn_refund)
@@ -8511,7 +8547,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
)
# State will update when spend is detected
except Exception as ex:
if ci_to.isTxNonFinalError(str(ex)):
if ci_to.isTxNonFinalError(str(ex)) is False:
self.log.warning(
f"Error trying to submit participate refund txn: {ex}"
)
+65 -4
View File
@@ -502,6 +502,67 @@ class BTCInterface(Secp256k1Interface):
return height
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):
if self._connection_type == "electrum":
backend = self.getBackend()
@@ -4370,11 +4431,11 @@ class BTCInterface(Secp256k1Interface):
return "Transaction already in block chain" in err_str
def isTxNonFinalError(self, err_str: str) -> bool:
err_lower = err_str.lower()
return (
"non-BIP68-final" in err_str
or "non-final" in err_str
or "Missing inputs" in err_str
or "bad-txns-inputs-missingorspent" in err_str
"non-bip68-final" in err_lower
or "non-final" in err_lower
or "locktime requirement not satisfied" in err_lower
)
def combine_non_segwit_prevouts(self):