From 3a5e40187ac536b676cd1890f4e4f4bbc5a95527 Mon Sep 17 00:00:00 2001 From: tecnovert Date: Sat, 9 Nov 2024 21:26:03 +0200 Subject: [PATCH] Switch BCH from using wallet watchonly to watching scripts in BSX. Using wallet watchonly to find the lock transactions only seems to work with rescanblockchain. --- basicswap/basicswap.py | 71 ++++++++++++++++++----- basicswap/interface/bch.py | 100 +++++++++++++++++--------------- basicswap/interface/btc.py | 12 ++-- basicswap/util/__init__.py | 7 +++ tests/basicswap/test_btc_xmr.py | 1 + 5 files changed, 123 insertions(+), 68 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index ec9b74f..ba34e16 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -46,6 +46,7 @@ from .util import ( format_timestamp, DeserialiseNum, h2b, + hex_or_none, i2b, zeroIfNone, make_int, @@ -3775,7 +3776,7 @@ class BasicSwap(BaseApp): bid_changed = False found_tx = None - if ci_to.coin_type() in (Coins.DCR, ): + if ci_to.watch_blocks_for_scripts(): if bid.xmr_b_lock_tx is None or bid.xmr_b_lock_tx.txid is None: # Watching chain for dest_address with WatchedScript pass @@ -3794,15 +3795,20 @@ class BasicSwap(BaseApp): if found_tx['height'] != 0 and (bid.xmr_b_lock_tx is None or not bid.xmr_b_lock_tx.chain_height): self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_SEEN, '', session) - if bid.xmr_b_lock_tx is None or bid.xmr_b_lock_tx.chain_height is None: - self.log.debug('Found {} lock tx in chain'.format(ci_to.coin_name())) - xmr_swap.b_lock_tx_id = bytes.fromhex(found_tx['txid']) + found_txid = bytes.fromhex(found_tx['txid']) + if bid.xmr_b_lock_tx is None or bid.xmr_b_lock_tx.chain_height is None or xmr_swap.b_lock_tx_id != found_txid: + self.log.debug('Found lock tx B in {} chain'.format(ci_to.coin_name())) + xmr_swap.b_lock_tx_id = found_txid if bid.xmr_b_lock_tx is None: bid.xmr_b_lock_tx = SwapTx( bid_id=bid.bid_id, tx_type=TxTypes.XMR_SWAP_B_LOCK, txid=xmr_swap.b_lock_tx_id, ) + if bid.xmr_b_lock_tx.txid != found_txid: + self.log.debug('Updating {} lock txid: {}'.format(ci_to.coin_name(), found_txid.hex())) + bid.xmr_b_lock_tx.txid = found_txid + bid.xmr_b_lock_tx.chain_height = found_tx['height'] bid_changed = True return bid_changed @@ -3950,7 +3956,7 @@ class BasicSwap(BaseApp): self.createActionInSession(delay, ActionTypes.RECOVER_XMR_SWAP_LOCK_TX_B, bid_id, session) session.commit() elif state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX: - if bid.xmr_a_lock_tx is None: + if bid.xmr_a_lock_tx is None or bid.xmr_a_lock_tx.txid is None: return rv # TODO: Timeout waiting for transactions @@ -3961,10 +3967,9 @@ class BasicSwap(BaseApp): if lock_tx_chain_info is None: return rv - if 'txid' in lock_tx_chain_info and lock_tx_chain_info['txid'] != b2h(xmr_swap.a_lock_tx_id): - # if we find that txid was changed (by funding or otherwise), we need to update it to track correctly + if 'txid' in lock_tx_chain_info and (xmr_swap.a_lock_tx_id is None or lock_tx_chain_info['txid'] != b2h(xmr_swap.a_lock_tx_id)): + # BCH: If we find that txid was changed (by funding or otherwise), we need to update it to track correctly xmr_swap.a_lock_tx_id = h2b(lock_tx_chain_info['txid']) - xmr_swap.a_lock_tx = h2b(lock_tx_chain_info['txhex']) tx = ci_from.loadTx(xmr_swap.a_lock_refund_tx) tx.vin[0].prevout.hash = b2i(xmr_swap.a_lock_tx_id) @@ -3981,7 +3986,9 @@ class BasicSwap(BaseApp): bid.xmr_a_lock_tx.tx_data = xmr_swap.a_lock_tx bid.xmr_a_lock_tx.spend_txid = xmr_swap.a_lock_spend_tx_id - # update watcher + # Update watcher + self.removeWatchedOutput(ci_from.coin_type(), bid.bid_id, None) + self.removeWatchedOutput(ci_to.coin_type(), bid.bid_id, None) self.watchXmrSwap(bid, offer, xmr_swap, session) bid_changed = True @@ -4321,7 +4328,7 @@ class BasicSwap(BaseApp): del self.coin_clients[coin_type]['watched_outputs'][i] self.log.debug('Removed watched output %s %s %s', Coins(coin_type).name, bid_id.hex(), wo.txid_hex) - def addWatchedScript(self, coin_type, bid_id, script, tx_type, swap_type=None): + def addWatchedScript(self, coin_type, bid_id, script: bytes, tx_type, swap_type=None): self.log.debug('Adding watched script %s bid %s type %s', Coins(coin_type).name, bid_id.hex(), tx_type) watched = self.coin_clients[coin_type]['watched_scripts'] @@ -4627,7 +4634,16 @@ class BasicSwap(BaseApp): self.saveBid(watched_script.bid_id, bid) else: self.log.warning('Could not find active bid for found watched script: {}'.format(watched_script.bid_id.hex())) + elif watched_script.tx_type == TxTypes.XMR_SWAP_A_LOCK: + self.log.info('Found chain A lock txid {} for bid: {}'.format(txid.hex(), watched_script.bid_id.hex())) + bid = self.swaps_in_progress[watched_script.bid_id][0] + if bid.xmr_a_lock_tx.txid != txid: + self.log.debug('Updating xmr_a_lock_tx from {} to {}'.format(hex_or_none(bid.xmr_a_lock_tx.txid), txid.hex())) + bid.xmr_a_lock_tx.txid = txid + bid.xmr_b_lock_tx.vout = vout + self.saveBid(watched_script.bid_id, bid) elif watched_script.tx_type == TxTypes.XMR_SWAP_B_LOCK: + self.log.info('Found chain B lock txid {} for bid: {}'.format(txid.hex(), watched_script.bid_id.hex())) bid = self.swaps_in_progress[watched_script.bid_id][0] bid.xmr_b_lock_tx = SwapTx( bid_id=watched_script.bid_id, @@ -4635,6 +4651,9 @@ class BasicSwap(BaseApp): txid=txid, vout=vout, ) + if bid.xmr_b_lock_tx.txid != txid: + self.log.debug('Updating xmr_b_lock_tx from {} to {}'.format(hex_or_none(bid.xmr_b_lock_tx.txid), txid.hex())) + bid.xmr_b_lock_tx.txid = txid bid.xmr_b_lock_tx.setState(TxStates.TX_IN_CHAIN) self.saveBid(watched_script.bid_id, bid) else: @@ -4783,7 +4802,7 @@ class BasicSwap(BaseApp): if s.tx_type == TxTypes.BCH_MERCY: self.processMercyTx(coin_type, s, bytes.fromhex(tx['txid']), i, tx) else: - self.processFoundScript(coin_type, s, bytes.fromhex(tx['txid']), i, tx) + self.processFoundScript(coin_type, s, bytes.fromhex(tx['txid']), i) for o in c['watched_outputs']: for i, inp in enumerate(tx['vin']): @@ -5808,12 +5827,20 @@ class BasicSwap(BaseApp): reverse_bid: bool = self.is_reverse_ads_bid(offer.coin_from, offer.coin_to) coin_from = Coins(offer.coin_to if reverse_bid else offer.coin_from) self.setLastHeightCheckedStart(coin_from, bid.chain_a_height_start, session) - self.addWatchedOutput(coin_from, bid.bid_id, bid.xmr_a_lock_tx.txid.hex(), bid.xmr_a_lock_tx.vout, TxTypes.XMR_SWAP_A_LOCK, SwapTypes.XMR_SWAP) - lock_refund_vout = self.ci(coin_from).getLockRefundTxSwapOutput(xmr_swap) - self.addWatchedOutput(coin_from, bid.bid_id, xmr_swap.a_lock_refund_tx_id.hex(), lock_refund_vout, TxTypes.XMR_SWAP_A_LOCK_REFUND, SwapTypes.XMR_SWAP) + if bid.xmr_a_lock_tx.txid: + self.addWatchedOutput(coin_from, bid.bid_id, bid.xmr_a_lock_tx.txid.hex(), bid.xmr_a_lock_tx.vout, TxTypes.XMR_SWAP_A_LOCK, SwapTypes.XMR_SWAP) + + if xmr_swap.a_lock_refund_tx_id: + lock_refund_vout = self.ci(coin_from).getLockRefundTxSwapOutput(xmr_swap) + self.addWatchedOutput(coin_from, bid.bid_id, xmr_swap.a_lock_refund_tx_id.hex(), lock_refund_vout, TxTypes.XMR_SWAP_A_LOCK_REFUND, SwapTypes.XMR_SWAP) bid.in_progress = 1 + # Watch outputs for chain A lock tx if txid is unknown (BCH) + if bid.xmr_a_lock_tx and bid.xmr_a_lock_tx.txid is None: + find_script: bytes = self.ci(coin_from).getScriptDest(xmr_swap.a_lock_tx_script) + self.addWatchedScript(coin_from, bid.bid_id, find_script, TxTypes.XMR_SWAP_A_LOCK) + def sendXmrBidTxnSigsFtoL(self, bid_id, session) -> None: # F -> L: Sending MSG3L self.log.debug('Signing adaptor-sig bid lock txns %s', bid_id.hex()) @@ -5859,9 +5886,21 @@ class BasicSwap(BaseApp): self.addMessageLink(Concepts.BID, bid_id, MessageTypes.XMR_BID_TXN_SIGS_FL, coin_a_lock_tx_sigs_l_msg_id, session=session) self.log.info('Sent XMR_BID_TXN_SIGS_FL %s for bid %s', coin_a_lock_tx_sigs_l_msg_id.hex(), bid_id.hex()) - a_lock_tx_id = ci_from.getTxid(xmr_swap.a_lock_tx) + if ci_from.watch_blocks_for_scripts() and self.isBchXmrSwap(offer): + # BCH doesn't have segwit + # Lock txid will change when signed. + # TODO: BCH Watchonly: Remove when BCH watchonly works. + a_lock_tx_id = None + else: + a_lock_tx_id = ci_from.getTxid(xmr_swap.a_lock_tx) a_lock_tx_vout = ci_from.getTxOutputPos(xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script) - self.log.debug('Waiting for lock txn %s to %s chain for bid %s', a_lock_tx_id.hex(), ci_from.coin_name(), bid_id.hex()) + + if a_lock_tx_id: + self.log.debug('Waiting for lock tx A {} to {} chain for bid {}'.format(a_lock_tx_id.hex(), ci_from.coin_name(), bid_id.hex())) + else: + find_script: bytes = ci_from.getScriptDest(xmr_swap.a_lock_tx_script) + self.log.debug('Waiting for lock tx A with script {} to {} chain for bid {}'.format(find_script.hex(), ci_from.coin_name(), bid_id.hex())) + if bid.xmr_a_lock_tx is None: bid.xmr_a_lock_tx = SwapTx( bid_id=bid_id, diff --git a/basicswap/interface/bch.py b/basicswap/interface/bch.py index 4cc3702..614c915 100644 --- a/basicswap/interface/bch.py +++ b/basicswap/interface/bch.py @@ -9,7 +9,7 @@ from typing import Union from basicswap.contrib.test_framework.messages import COutPoint, CTransaction, CTxIn from basicswap.util import b2h, b2i, ensure, i2h from basicswap.util.script import decodePushData, decodeScriptNum -from .btc import BTCInterface, ensure_op, find_vout_for_address_from_txobj, findOutput +from .btc import BTCInterface, ensure_op, findOutput from basicswap.rpc import make_rpc_func from basicswap.chainparams import Coins from basicswap.interface.contrib.bch_test_framework.cashaddress import Address @@ -67,6 +67,11 @@ class BCHInterface(BTCInterface): def xmr_swap_a_lock_spend_tx_vsize() -> int: return 302 + @staticmethod + def watch_blocks_for_scripts() -> bool: + # TODO: BCH Watchonly: Remove when BCH watchonly works. + return True + def __init__(self, coin_settings, network, swap_client=None): super(BCHInterface, self).__init__(coin_settings, network, swap_client) # No multiwallet support @@ -116,6 +121,9 @@ class BCHInterface(BTCInterface): return address + def importWatchOnlyAddress(self, address: str, label: str): + self.rpc_wallet('importaddress', [address, label, False, True]) + def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str: txn = self.rpc('createrawtransaction', [[], {addr_to: self.format_amount(amount)}]) @@ -177,57 +185,53 @@ class BCHInterface(BTCInterface): return {'txid': txid_hex, 'amount': 0, 'height': block_height} return None - def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1): - # Add watchonly address and rescan if required - txid = None + def getLockTxHeight(self, txid: bytes, dest_address: str, bid_amount: int, rescan_from: int, find_index: bool = False, vout: int = -1): - # first lookup by dest_address - if not self.isAddressMine(dest_address, or_watch_only=False): - self.importWatchOnlyAddress(dest_address, 'bid') - self._log.info('Imported watch-only addr: {}'.format(dest_address)) - self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from)) - self.rpc_wallet('rescanblockchain', [rescan_from]) - - return_txid = True - - txns = self.rpc_wallet('listunspent', [0, 9999999, [dest_address, ]]) - for tx in txns: - if self.make_int(tx['amount']) == bid_amount: - txid = bytes.fromhex(tx['txid']) - break - - # try to look up in past transactions - if not txid: - txns = self.rpc_wallet('listtransactions', ["*", 100000, 0, True]) - for tx in txns: - if self.make_int(tx['amount']) == bid_amount and tx['category'] == 'send' and tx.get('address', '_NONE_') == dest_address: - txid = bytes.fromhex(tx['txid']) - break - - try: - # set `include_watchonly` explicitly to `True` to get transactions for watchonly addresses also in BCH - tx = self.rpc_wallet('gettransaction', [txid.hex(), True]) - - block_height = 0 - if 'blockhash' in tx: - block_header = self.rpc('getblockheader', [tx['blockhash']]) - block_height = block_header['height'] - - rv = { - 'depth': 0 if 'confirmations' not in tx else tx['confirmations'], - 'height': block_height} - - except Exception as e: - # self._log.debug('getLockTxHeight gettransaction failed: %s, %s', txid.hex(), str(e)) + ''' + TODO: BCH Watchonly + Replace with importWatchOnlyAddress when it works again + Currently importing the watchonly address only works if rescanblockchain is run on every iteration + ''' + if txid is None: + self._log.debug('TODO: getLockTxHeight') return None - if find_index: - tx_obj = self.rpc('decoderawtransaction', [tx['hex']]) - rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address) + found_vout = None + # Search for txo at vout 0 and 1 if vout is not known + if vout is None: + test_range = range(2) + else: + test_range = (vout, ) + for try_vout in test_range: + try: + txout = self.rpc('gettxout', [txid.hex(), try_vout, True]) + addresses = txout['scriptPubKey']['addresses'] + if len(addresses) != 1 or addresses[0] != dest_address: + continue + if self.make_int(txout['value']) != bid_amount: + self._log.warning('getLockTxHeight found txout {} with incorrect amount {}'.format(txid.hex(), txout['value'])) + continue + found_vout = try_vout + break + except Exception as e: + # self._log.warning('gettxout {}'.format(e)) + return None - if return_txid: - rv['txid'] = txid.hex() - rv['txhex'] = tx['hex'] + if found_vout is None: + return None + + block_height: int = 0 + confirmations: int = 0 if 'confirmations' not in txout else txout['confirmations'] + + # TODO: Better way? + if confirmations > 0: + block_height = self.getChainHeight() - confirmations + + rv = { + 'txid': txid.hex(), + 'depth': confirmations, + 'index': found_vout, + 'height': block_height} return rv diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index ca0268f..9af6830 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -1092,14 +1092,18 @@ class BTCInterface(Secp256k1Interface): return pay_fee def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int, lock_tx_vout=None) -> bytes: - self._log.info('spendBLockTx %s:\n', chain_b_lock_txid.hex()) - wtx = self.rpc_wallet('gettransaction', [chain_b_lock_txid.hex(), ]) - lock_tx = self.loadTx(bytes.fromhex(wtx['hex'])) + self._log.info('spendBLockTx: {} {}\n'.format(chain_b_lock_txid.hex(), lock_tx_vout)) + locked_n = lock_tx_vout Kbs = self.getPubkey(kbs) script_pk = self.getPkDest(Kbs) - locked_n = findOutput(lock_tx, script_pk) + + if locked_n is 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) ensure(locked_n is not None, 'Output not found in tx') + pkh_to = self.decodeAddress(address_to) tx = CTransaction() diff --git a/basicswap/util/__init__.py b/basicswap/util/__init__.py index 8efcfa0..155d603 100644 --- a/basicswap/util/__init__.py +++ b/basicswap/util/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright (c) 2018-2023 tecnovert +# Copyright (c) 2024 The Basicswap developers # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -215,3 +216,9 @@ def zeroIfNone(value) -> int: if value is None: return 0 return value + + +def hex_or_none(value: bytes) -> str: + if value is None: + return 'None' + return value.hex() diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py index b0dd708..40dd5b1 100644 --- a/tests/basicswap/test_btc_xmr.py +++ b/tests/basicswap/test_btc_xmr.py @@ -349,6 +349,7 @@ class TestFunctions(BaseTest): amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1) rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1) + logging.info(f'amount from, rate, amount to: {amt_swap}, {rate_swap}, {amt_swap * rate_swap}') offer_id = swap_clients[id_offerer].postOffer( coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=lock_value)