mirror of
https://github.com/basicswap/basicswap.git
synced 2026-06-10 13:01:41 +02:00
fix: get refund tx block info for CSV check
This commit is contained in:
+54
-11
@@ -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:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024 tecnovert
|
||||
|
||||
@@ -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
|
||||
|
||||
+119
-52
@@ -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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022-2024 tecnovert
|
||||
|
||||
+146
-27
@@ -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)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024 tecnovert
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024 The BasicSwap developers
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024-2026 The Basicswap developers
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022-2023 tecnovert
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2023 tecnovert
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2023 tecnovert
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2022 tecnovert
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2024 tecnovert
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2021 tecnovert
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022 tecnovert
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024 The Basicswap developers
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020-2024 tecnovert
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user