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.
This commit is contained in:
tecnovert 2024-11-09 21:26:03 +02:00
parent c561efaba0
commit 3a5e40187a
No known key found for this signature in database
GPG key ID: 8ED6D8750C4E3F93
5 changed files with 123 additions and 68 deletions

View file

@ -46,6 +46,7 @@ from .util import (
format_timestamp, format_timestamp,
DeserialiseNum, DeserialiseNum,
h2b, h2b,
hex_or_none,
i2b, i2b,
zeroIfNone, zeroIfNone,
make_int, make_int,
@ -3775,7 +3776,7 @@ class BasicSwap(BaseApp):
bid_changed = False bid_changed = False
found_tx = None 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: if bid.xmr_b_lock_tx is None or bid.xmr_b_lock_tx.txid is None:
# Watching chain for dest_address with WatchedScript # Watching chain for dest_address with WatchedScript
pass 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): 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) 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: found_txid = bytes.fromhex(found_tx['txid'])
self.log.debug('Found {} lock tx in chain'.format(ci_to.coin_name())) 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:
xmr_swap.b_lock_tx_id = bytes.fromhex(found_tx['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: if bid.xmr_b_lock_tx is None:
bid.xmr_b_lock_tx = SwapTx( bid.xmr_b_lock_tx = SwapTx(
bid_id=bid.bid_id, bid_id=bid.bid_id,
tx_type=TxTypes.XMR_SWAP_B_LOCK, tx_type=TxTypes.XMR_SWAP_B_LOCK,
txid=xmr_swap.b_lock_tx_id, 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.xmr_b_lock_tx.chain_height = found_tx['height']
bid_changed = True bid_changed = True
return bid_changed return bid_changed
@ -3950,7 +3956,7 @@ class BasicSwap(BaseApp):
self.createActionInSession(delay, ActionTypes.RECOVER_XMR_SWAP_LOCK_TX_B, bid_id, session) self.createActionInSession(delay, ActionTypes.RECOVER_XMR_SWAP_LOCK_TX_B, bid_id, session)
session.commit() session.commit()
elif state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX: 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 return rv
# TODO: Timeout waiting for transactions # TODO: Timeout waiting for transactions
@ -3961,10 +3967,9 @@ class BasicSwap(BaseApp):
if lock_tx_chain_info is None: if lock_tx_chain_info is None:
return rv return rv
if 'txid' in lock_tx_chain_info and lock_tx_chain_info['txid'] != b2h(xmr_swap.a_lock_tx_id): 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)):
# if we find that txid was changed (by funding or otherwise), we need to update it to track correctly # 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_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 = ci_from.loadTx(xmr_swap.a_lock_refund_tx)
tx.vin[0].prevout.hash = b2i(xmr_swap.a_lock_tx_id) 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.tx_data = xmr_swap.a_lock_tx
bid.xmr_a_lock_tx.spend_txid = xmr_swap.a_lock_spend_tx_id 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) self.watchXmrSwap(bid, offer, xmr_swap, session)
bid_changed = True bid_changed = True
@ -4321,7 +4328,7 @@ class BasicSwap(BaseApp):
del self.coin_clients[coin_type]['watched_outputs'][i] 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) 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) 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'] watched = self.coin_clients[coin_type]['watched_scripts']
@ -4627,7 +4634,16 @@ class BasicSwap(BaseApp):
self.saveBid(watched_script.bid_id, bid) self.saveBid(watched_script.bid_id, bid)
else: else:
self.log.warning('Could not find active bid for found watched script: {}'.format(watched_script.bid_id.hex())) 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: 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 = self.swaps_in_progress[watched_script.bid_id][0]
bid.xmr_b_lock_tx = SwapTx( bid.xmr_b_lock_tx = SwapTx(
bid_id=watched_script.bid_id, bid_id=watched_script.bid_id,
@ -4635,6 +4651,9 @@ class BasicSwap(BaseApp):
txid=txid, txid=txid,
vout=vout, 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) bid.xmr_b_lock_tx.setState(TxStates.TX_IN_CHAIN)
self.saveBid(watched_script.bid_id, bid) self.saveBid(watched_script.bid_id, bid)
else: else:
@ -4783,7 +4802,7 @@ class BasicSwap(BaseApp):
if s.tx_type == TxTypes.BCH_MERCY: if s.tx_type == TxTypes.BCH_MERCY:
self.processMercyTx(coin_type, s, bytes.fromhex(tx['txid']), i, tx) self.processMercyTx(coin_type, s, bytes.fromhex(tx['txid']), i, tx)
else: 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 o in c['watched_outputs']:
for i, inp in enumerate(tx['vin']): 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) 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) coin_from = Coins(offer.coin_to if reverse_bid else offer.coin_from)
self.setLastHeightCheckedStart(coin_from, bid.chain_a_height_start, session) 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) if bid.xmr_a_lock_tx.txid:
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) 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 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: def sendXmrBidTxnSigsFtoL(self, bid_id, session) -> None:
# F -> L: Sending MSG3L # F -> L: Sending MSG3L
self.log.debug('Signing adaptor-sig bid lock txns %s', bid_id.hex()) 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.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()) 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) 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: if bid.xmr_a_lock_tx is None:
bid.xmr_a_lock_tx = SwapTx( bid.xmr_a_lock_tx = SwapTx(
bid_id=bid_id, bid_id=bid_id,

View file

@ -9,7 +9,7 @@ from typing import Union
from basicswap.contrib.test_framework.messages import COutPoint, CTransaction, CTxIn from basicswap.contrib.test_framework.messages import COutPoint, CTransaction, CTxIn
from basicswap.util import b2h, b2i, ensure, i2h from basicswap.util import b2h, b2i, ensure, i2h
from basicswap.util.script import decodePushData, decodeScriptNum 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.rpc import make_rpc_func
from basicswap.chainparams import Coins from basicswap.chainparams import Coins
from basicswap.interface.contrib.bch_test_framework.cashaddress import Address 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: def xmr_swap_a_lock_spend_tx_vsize() -> int:
return 302 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): def __init__(self, coin_settings, network, swap_client=None):
super(BCHInterface, self).__init__(coin_settings, network, swap_client) super(BCHInterface, self).__init__(coin_settings, network, swap_client)
# No multiwallet support # No multiwallet support
@ -116,6 +121,9 @@ class BCHInterface(BTCInterface):
return address 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: 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)}]) 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 {'txid': txid_hex, 'amount': 0, 'height': block_height}
return None return None
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False, vout: int = -1): def getLockTxHeight(self, txid: bytes, dest_address: str, bid_amount: int, rescan_from: int, find_index: bool = False, vout: int = -1):
# Add watchonly address and rescan if required
txid = None
# first lookup by dest_address '''
if not self.isAddressMine(dest_address, or_watch_only=False): TODO: BCH Watchonly
self.importWatchOnlyAddress(dest_address, 'bid') Replace with importWatchOnlyAddress when it works again
self._log.info('Imported watch-only addr: {}'.format(dest_address)) Currently importing the watchonly address only works if rescanblockchain is run on every iteration
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from)) '''
self.rpc_wallet('rescanblockchain', [rescan_from]) if txid is None:
self._log.debug('TODO: getLockTxHeight')
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))
return None return None
if find_index: found_vout = None
tx_obj = self.rpc('decoderawtransaction', [tx['hex']]) # Search for txo at vout 0 and 1 if vout is not known
rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address) 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: if found_vout is None:
rv['txid'] = txid.hex() return None
rv['txhex'] = tx['hex']
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 return rv

View file

@ -1092,14 +1092,18 @@ class BTCInterface(Secp256k1Interface):
return pay_fee 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: 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()) self._log.info('spendBLockTx: {} {}\n'.format(chain_b_lock_txid.hex(), lock_tx_vout))
wtx = self.rpc_wallet('gettransaction', [chain_b_lock_txid.hex(), ]) locked_n = lock_tx_vout
lock_tx = self.loadTx(bytes.fromhex(wtx['hex']))
Kbs = self.getPubkey(kbs) Kbs = self.getPubkey(kbs)
script_pk = self.getPkDest(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') ensure(locked_n is not None, 'Output not found in tx')
pkh_to = self.decodeAddress(address_to) pkh_to = self.decodeAddress(address_to)
tx = CTransaction() tx = CTransaction()

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2018-2023 tecnovert # Copyright (c) 2018-2023 tecnovert
# Copyright (c) 2024 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.
@ -215,3 +216,9 @@ def zeroIfNone(value) -> int:
if value is None: if value is None:
return 0 return 0
return value return value
def hex_or_none(value: bytes) -> str:
if value is None:
return 'None'
return value.hex()

View file

@ -349,6 +349,7 @@ class TestFunctions(BaseTest):
amt_swap = ci_from.make_int(random.uniform(0.1, 2.0), r=1) 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) 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( offer_id = swap_clients[id_offerer].postOffer(
coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=lock_value) lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=lock_value)