Continue extending bch interface and test coverage

Support reverse swap happy path
This commit is contained in:
mainnet-pat 2024-10-22 08:19:07 +00:00 committed by nahuhh
parent 68256fdcf7
commit 52e6e2b586
4 changed files with 320 additions and 33 deletions

View file

@ -2987,7 +2987,7 @@ class BasicSwap(BaseApp):
refundExtraArgs = dict() refundExtraArgs = dict()
lockExtraArgs = dict() lockExtraArgs = dict()
if self.isBchXmrSwap(offer): if self.isBchXmrSwap(offer):
pkh_refund_to = ci_from.decodeAddress(self.getCachedAddressForCoin(offer.coin_from, use_session)) pkh_refund_to = ci_from.decodeAddress(self.getCachedAddressForCoin(coin_from, use_session))
pkh_dest = xmr_swap.dest_af pkh_dest = xmr_swap.dest_af
# refund script # refund script
refundExtraArgs['mining_fee'] = 1000 refundExtraArgs['mining_fee'] = 1000
@ -3957,7 +3957,8 @@ 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
self.addWatchedOutput(offer.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) coin_from = Coins(offer.coin_to if reverse_bid else offer.coin_from)
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)
bid_changed = True bid_changed = True
if bid.xmr_a_lock_tx.state == TxStates.TX_NONE and lock_tx_chain_info['height'] == 0: if bid.xmr_a_lock_tx.state == TxStates.TX_NONE and lock_tx_chain_info['height'] == 0:
@ -4922,11 +4923,10 @@ class BasicSwap(BaseApp):
raise ValueError('TODO') raise ValueError('TODO')
elif offer_data.swap_type == SwapTypes.XMR_SWAP: elif offer_data.swap_type == SwapTypes.XMR_SWAP:
ensure(offer_data.protocol_version >= MINPROTO_VERSION_ADAPTOR_SIG, 'Invalid protocol version') ensure(offer_data.protocol_version >= MINPROTO_VERSION_ADAPTOR_SIG, 'Invalid protocol version')
if Coins.BCH not in (coin_from, coin_to): if reverse_bid:
if reverse_bid: ensure(ci_to.has_segwit(), 'Coin-to must support segwit for reverse bid offers')
ensure(ci_to.has_segwit(), 'Coin-to must support segwit for reverse bid offers') else:
else: ensure(ci_from.has_segwit(), 'Coin-from must support segwit')
ensure(ci_from.has_segwit(), 'Coin-from must support segwit')
ensure(len(offer_data.proof_address) == 0, 'Unexpected data') ensure(len(offer_data.proof_address) == 0, 'Unexpected data')
ensure(len(offer_data.proof_signature) == 0, 'Unexpected data') ensure(len(offer_data.proof_signature) == 0, 'Unexpected data')
ensure(len(offer_data.pkhash_seller) == 0, 'Unexpected data') ensure(len(offer_data.pkhash_seller) == 0, 'Unexpected data')

View file

@ -13,7 +13,7 @@ from .btc import BTCInterface, ensure_op, find_vout_for_address_from_txobj, find
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
from basicswap.util.crypto import sha256 from basicswap.util.crypto import hash160, sha256
from basicswap.interface.contrib.bch_test_framework.script import ( from basicswap.interface.contrib.bch_test_framework.script import (
OP_TXINPUTCOUNT, OP_TXINPUTCOUNT,
OP_1, OP_1,
@ -71,8 +71,13 @@ class BCHInterface(BTCInterface):
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
self.swap_client = swap_client
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host) self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
def has_segwit(self) -> bool:
# bch does not have segwit, but we return true here to avoid extra checks in basicswap.py
return True
def getExchangeName(self, exchange_name): def getExchangeName(self, exchange_name):
return 'bch' return 'bch'
@ -148,7 +153,7 @@ class BCHInterface(BTCInterface):
return CScript([OP_HASH256, script_hash, OP_EQUAL]) return CScript([OP_HASH256, script_hash, OP_EQUAL])
def withdrawCoin(self, value: float, addr_to: str, subfee: bool): def withdrawCoin(self, value: float, addr_to: str, subfee: bool):
params = [addr_to, value, '', '', subfee, True, True] params = [addr_to, value, '', '', subfee, 0, False]
return self.rpc_wallet('sendtoaddress', params) return self.rpc_wallet('sendtoaddress', params)
def getSpendableBalance(self) -> int: def getSpendableBalance(self) -> int:
@ -321,16 +326,19 @@ class BCHInterface(BTCInterface):
address.prefix = prefix address.prefix = prefix
return address.cash_address() return address.cash_address()
def encodeSharedAddress(self, Kbv, Kbs):
return self.pkh_to_address(hash160(Kbs))
def addressToLockingBytecode(self, address: str) -> bytes: def addressToLockingBytecode(self, address: str) -> bytes:
return b'\x76\xa9\x14' + bytes(Address.from_string(address).payload) + b'\x88\xac' return b'\x76\xa9\x14' + bytes(Address.from_string(address).payload) + b'\x88\xac'
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc_wallet('getbalance', ["*", 1, False]))
def getScriptDest(self, script): def getScriptDest(self, script):
return self.scriptToP2SH32LockingBytecode(script) return self.scriptToP2SH32LockingBytecode(script)
def scriptToP2SH32LockingBytecode(self, script: Union[bytes, str]) -> bytes: def scriptToP2SH32LockingBytecode(self, script: Union[bytes, str]) -> bytes:
if isinstance(script, str):
script = bytes.fromhex(script)
return CScript([ return CScript([
OP_HASH256, OP_HASH256,
sha256(sha256(script)), sha256(sha256(script)),

View file

@ -210,7 +210,7 @@ class Address:
if address.upper() != address and address.lower() != address: if address.upper() != address and address.lower() != address:
raise ValueError( raise ValueError(
"Cash address contains uppercase and lowercase characters" "Cash address contains uppercase and lowercase characters: " + address
) )
address = address.lower() address = address.lower()

View file

@ -30,10 +30,12 @@ from basicswap.util import (
) )
from basicswap.interface.base import Curves from basicswap.interface.base import Curves
from basicswap.util.crypto import sha256 from basicswap.util.crypto import sha256
from tests.basicswap.test_btc_xmr import BasicSwapTest
from tests.basicswap.util import ( from tests.basicswap.util import (
read_json_api, read_json_api,
) )
from tests.basicswap.common import ( from tests.basicswap.common import (
BCH_BASE_RPC_PORT,
abandon_all_swaps, abandon_all_swaps,
wait_for_bid, wait_for_bid,
wait_for_event, wait_for_event,
@ -70,20 +72,216 @@ logger = logging.getLogger()
class TestFunctions(BaseTest): class TestFunctions(BaseTest):
__test__ = True __test__ = False
start_bch_nodes = True start_bch_nodes = True
base_rpc_port = None base_rpc_port = BCH_BASE_RPC_PORT
extra_wait_time = 0 extra_wait_time = 0
test_coin_from = Coins.BCH
def callnoderpc(self, method, params=[], wallet=None, node_id=0): def callnoderpc(self, method, params=[], wallet=None, node_id=0):
return callnoderpc(node_id, method, params, wallet, self.base_rpc_port) return callnoderpc(node_id, method, params, wallet, self.base_rpc_port)
def mineBlock(self, num_blocks=1): def mineBlock(self, num_blocks=1):
self.callnoderpc('generatetoaddress', [num_blocks, self.btc_addr]) self.callnoderpc('generatetoaddress', [num_blocks, self.bch_addr])
def test_05_bch_xmr(self):
logging.info('---------- Test BCH to XMR')
swap_clients = self.swap_clients
offer_id = swap_clients[0].postOffer(Coins.BCH, Coins.XMR, 10 * COIN, 100 * XMR_COIN, 10 * COIN, SwapTypes.XMR_SWAP)
wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offers = swap_clients[1].listOffers(filters={'offer_id': offer_id})
offer = offers[0]
swap_clients[1].ci(Coins.XMR).setFeePriority(3)
bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED)
bid, xmr_swap = swap_clients[0].getXmrBid(bid_id)
assert (xmr_swap)
swap_clients[0].acceptXmrBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=180)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True)
swap_clients[1].ci(Coins.XMR).setFeePriority(0)
class TestBCH(BasicSwapTest):
__test__ = True
test_coin_from = Coins.BCH
start_bch_nodes = True
base_rpc_port = BCH_BASE_RPC_PORT
def mineBlock(self, num_blocks=1):
self.callnoderpc('generatetoaddress', [num_blocks, self.bch_addr])
def check_softfork_active(self, feature_name): def check_softfork_active(self, feature_name):
deploymentinfo = self.callnoderpc('getdeploymentinfo') return True
assert (deploymentinfo['deployments'][feature_name]['active'] is True)
def test_001_nested_segwit(self):
logging.info('---------- Test {} p2sh nested segwit'.format(self.test_coin_from.name))
logging.info('Skipped')
def test_002_native_segwit(self):
logging.info('---------- Test {} p2sh native segwit'.format(self.test_coin_from.name))
logging.info('Skipped')
def test_003_cltv(self):
logging.info('---------- Test {} cltv'.format(self.test_coin_from.name))
ci = self.swap_clients[0].ci(self.test_coin_from)
self.check_softfork_active('bip65')
chain_height = self.callnoderpc('getblockcount')
script = CScript([chain_height + 3, OP_CHECKLOCKTIMEVERIFY, ])
script_dest = ci.getScriptDest(script)
tx = CTransaction()
tx.nVersion = ci.txVersion()
tx.vout.append(ci.txoType()(ci.make_int(1.1), script_dest))
tx_hex = ToHex(tx)
tx_funded = ci.rpc_wallet('fundrawtransaction', [tx_hex])
utxo_pos = 0 if tx_funded['changepos'] == 1 else 1
tx_signed = ci.rpc_wallet('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex']
txid = ci.rpc('sendrawtransaction', [tx_signed, ])
addr_out = ci.rpc_wallet('getnewaddress', ['cltv test'])
pkh = ci.decodeAddress(addr_out)
script_out = ci.getScriptForPubkeyHash(pkh)
tx_spend = CTransaction()
tx_spend.nVersion = ci.txVersion()
tx_spend.nLockTime = chain_height + 3
tx_spend.vin.append(CTxIn(COutPoint(int(txid, 16), utxo_pos), scriptSig=CScript([script,])))
tx_spend.vout.append(ci.txoType()(ci.make_int(1.0999), script_out))
tx_spend_hex = ToHex(tx_spend)
tx_spend.nLockTime = chain_height + 2
tx_spend_invalid_hex = ToHex(tx_spend)
for tx_hex in [tx_spend_invalid_hex, tx_spend_hex]:
try:
txid = self.callnoderpc('sendrawtransaction', [tx_hex, ])
except Exception as e:
assert ('non-final' in str(e))
else:
assert False, 'Should fail'
self.mineBlock(5)
try:
txid = ci.rpc('sendrawtransaction', [tx_spend_invalid_hex, ])
except Exception as e:
assert ('Locktime requirement not satisfied' in str(e))
else:
assert False, 'Should fail'
txid = ci.rpc('sendrawtransaction', [tx_spend_hex, ])
self.mineBlock()
ro = ci.rpc_wallet('listreceivedbyaddress', [0, ])
sum_addr = 0
for entry in ro:
if entry['address'] == addr_out:
sum_addr += entry['amount']
assert (sum_addr == 1.0999)
# Ensure tx was mined
tx_wallet = ci.rpc_wallet('gettransaction', [txid, ])
assert (len(tx_wallet['blockhash']) == 64)
def test_004_csv(self):
logging.info('---------- Test {} csv'.format(self.test_coin_from.name))
swap_clients = self.swap_clients
ci = self.swap_clients[0].ci(self.test_coin_from)
self.check_softfork_active('csv')
script = CScript([3, OP_CHECKSEQUENCEVERIFY, ])
script_dest = ci.getScriptDest(script)
tx = CTransaction()
tx.nVersion = ci.txVersion()
tx.vout.append(ci.txoType()(ci.make_int(1.1), script_dest))
tx_hex = ToHex(tx)
tx_funded = ci.rpc_wallet('fundrawtransaction', [tx_hex])
utxo_pos = 0 if tx_funded['changepos'] == 1 else 1
tx_signed = ci.rpc_wallet('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex']
txid = ci.rpc('sendrawtransaction', [tx_signed, ])
addr_out = ci.rpc_wallet('getnewaddress', ['csv test'])
pkh = ci.decodeAddress(addr_out)
script_out = ci.getScriptForPubkeyHash(pkh)
# Double check output type
prev_tx = ci.rpc('decoderawtransaction', [tx_signed, ])
assert (prev_tx['vout'][utxo_pos]['scriptPubKey']['type'] == 'scripthash')
tx_spend = CTransaction()
tx_spend.nVersion = ci.txVersion()
tx_spend.vin.append(CTxIn(COutPoint(int(txid, 16), utxo_pos),
nSequence=3,
scriptSig=CScript([script,])))
tx_spend.vout.append(ci.txoType()(ci.make_int(1.0999), script_out))
tx_spend_hex = ToHex(tx_spend)
try:
txid = ci.rpc('sendrawtransaction', [tx_spend_hex, ])
except Exception as e:
assert ('non-BIP68-final' in str(e))
else:
assert False, 'Should fail'
self.mineBlock(3)
txid = ci.rpc('sendrawtransaction', [tx_spend_hex, ])
self.mineBlock(1)
ro = ci.rpc_wallet('listreceivedbyaddress', [0, ])
sum_addr = 0
for entry in ro:
if entry['address'] == addr_out:
sum_addr += entry['amount']
assert (sum_addr == 1.0999)
# Ensure tx was mined
tx_wallet = ci.rpc_wallet('gettransaction', [txid, ])
assert (len(tx_wallet['blockhash']) == 64)
def test_005_watchonly(self):
logging.info('---------- Test {} watchonly'.format(self.test_coin_from.name))
ci = self.swap_clients[0].ci(self.test_coin_from)
ci1 = self.swap_clients[1].ci(self.test_coin_from)
addr = ci.rpc_wallet('getnewaddress', ['watchonly test'])
ro = ci1.rpc_wallet('importaddress', [addr, '', False])
txid = ci.rpc_wallet('sendtoaddress', [addr, 1.0])
tx_hex = ci.rpc('getrawtransaction', [txid, ])
ci1.rpc_wallet('sendrawtransaction', [tx_hex, ])
ro = ci1.rpc_wallet('gettransaction', [txid, ])
assert (ro['txid'] == txid)
def test_006_getblock_verbosity(self):
super().test_006_getblock_verbosity()
def test_007_hdwallet(self):
logging.info('---------- Test {} hdwallet'.format(self.test_coin_from.name))
test_seed = '8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b'
test_wif = self.swap_clients[0].ci(self.test_coin_from).encodeKey(bytes.fromhex(test_seed))
new_wallet_name = random.randbytes(10).hex()
self.callnoderpc('createwallet', [new_wallet_name])
self.callnoderpc('sethdseed', [True, test_wif], wallet=new_wallet_name)
addr = self.callnoderpc('getnewaddress', wallet=new_wallet_name)
self.callnoderpc('unloadwallet', [new_wallet_name])
assert (addr == 'bchreg:qqxr67wf5ltty5jvm44zryywmpt7ntdaa50carjt59')
def test_008_gettxout(self):
super().test_008_gettxout()
def test_009_scantxoutset(self):
super().test_009_scantxoutset()
def test_010_bch_txn_size(self): def test_010_bch_txn_size(self):
logging.info('---------- Test {} txn_size'.format(Coins.BCH)) logging.info('---------- Test {} txn_size'.format(Coins.BCH))
@ -218,26 +416,107 @@ class TestFunctions(BaseTest):
assert (expect_size >= lock_tx_b_spend_decoded['size']) assert (expect_size >= lock_tx_b_spend_decoded['size'])
assert (expect_size - lock_tx_b_spend_decoded['size'] < 10) assert (expect_size - lock_tx_b_spend_decoded['size'] < 10)
def test_05_bch_xmr(self): def test_011_p2sh(self):
logging.info('---------- Test BCH to XMR') # Not used in bsx for native-segwit coins
logging.info('---------- Test {} p2sh'.format(self.test_coin_from.name))
swap_clients = self.swap_clients swap_clients = self.swap_clients
offer_id = swap_clients[0].postOffer(Coins.BCH, Coins.XMR, 10 * COIN, 100 * XMR_COIN, 10 * COIN, SwapTypes.XMR_SWAP) ci = self.swap_clients[0].ci(self.test_coin_from)
wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offers = swap_clients[1].listOffers(filters={'offer_id': offer_id})
offer = offers[0]
swap_clients[1].ci(Coins.XMR).setFeePriority(3) script = CScript([2, 2, OP_EQUAL, ])
bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) script_dest = ci.get_p2sh_script_pubkey(script)
tx = CTransaction()
tx.nVersion = ci.txVersion()
tx.vout.append(ci.txoType()(ci.make_int(1.1), script_dest))
tx_hex = ToHex(tx)
tx_funded = ci.rpc_wallet('fundrawtransaction', [tx_hex])
utxo_pos = 0 if tx_funded['changepos'] == 1 else 1
tx_signed = ci.rpc_wallet('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex']
txid = ci.rpc('sendrawtransaction', [tx_signed, ])
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED) addr_out = ci.rpc_wallet('getnewaddress', ['csv test'])
pkh = ci.decodeAddress(addr_out)
script_out = ci.getScriptForPubkeyHash(pkh)
bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) # Double check output type
assert (xmr_swap) prev_tx = ci.rpc('decoderawtransaction', [tx_signed, ])
assert (prev_tx['vout'][utxo_pos]['scriptPubKey']['type'] == 'scripthash')
swap_clients[0].acceptXmrBid(bid_id) tx_spend = CTransaction()
tx_spend.nVersion = ci.txVersion()
tx_spend.vin.append(CTxIn(COutPoint(int(txid, 16), utxo_pos),
scriptSig=CScript([script,])))
tx_spend.vout.append(ci.txoType()(ci.make_int(1.0999), script_out))
tx_spend_hex = ToHex(tx_spend)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) txid = ci.rpc('sendrawtransaction', [tx_spend_hex, ])
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True) self.mineBlock(1)
ro = ci.rpc_wallet('listreceivedbyaddress', [0, ])
sum_addr = 0
for entry in ro:
if entry['address'] == addr_out:
sum_addr += entry['amount']
assert (sum_addr == 1.0999)
swap_clients[1].ci(Coins.XMR).setFeePriority(0) # Ensure tx was mined
tx_wallet = ci.rpc_wallet('gettransaction', [txid, ])
assert (len(tx_wallet['blockhash']) == 64)
def test_011_p2sh32(self):
# Not used in bsx for native-segwit coins
logging.info('---------- Test {} p2sh32'.format(self.test_coin_from.name))
swap_clients = self.swap_clients
ci = self.swap_clients[0].ci(self.test_coin_from)
script = CScript([2, 2, OP_EQUAL, ])
script_dest = ci.scriptToP2SH32LockingBytecode(script)
tx = CTransaction()
tx.nVersion = ci.txVersion()
tx.vout.append(ci.txoType()(ci.make_int(1.1), script_dest))
tx_hex = ToHex(tx)
tx_funded = ci.rpc_wallet('fundrawtransaction', [tx_hex])
utxo_pos = 0 if tx_funded['changepos'] == 1 else 1
tx_signed = ci.rpc_wallet('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex']
txid = ci.rpc('sendrawtransaction', [tx_signed, ])
addr_out = ci.rpc_wallet('getnewaddress', ['csv test'])
pkh = ci.decodeAddress(addr_out)
script_out = ci.getScriptForPubkeyHash(pkh)
# Double check output type
prev_tx = ci.rpc('decoderawtransaction', [tx_signed, ])
assert (prev_tx['vout'][utxo_pos]['scriptPubKey']['type'] == 'scripthash')
tx_spend = CTransaction()
tx_spend.nVersion = ci.txVersion()
tx_spend.vin.append(CTxIn(COutPoint(int(txid, 16), utxo_pos),
scriptSig=CScript([script,])))
tx_spend.vout.append(ci.txoType()(ci.make_int(1.0999), script_out))
tx_spend_hex = ToHex(tx_spend)
txid = ci.rpc('sendrawtransaction', [tx_spend_hex, ])
self.mineBlock(1)
ro = ci.rpc_wallet('listreceivedbyaddress', [0, ])
sum_addr = 0
for entry in ro:
if entry['address'] == addr_out:
sum_addr += entry['amount']
assert (sum_addr == 1.0999)
# Ensure tx was mined
tx_wallet = ci.rpc_wallet('gettransaction', [txid, ])
assert (len(tx_wallet['blockhash']) == 64)
def test_012_p2sh_p2wsh(self):
logging.info('---------- Test {} p2sh-p2wsh'.format(self.test_coin_from.name))
logging.info('Skipped')
def test_01_a_full_swap(self):
super().test_01_a_full_swap()
def test_01_b_full_swap_reverse(self):
self.prepare_balance(Coins.BCH, 100.0, 1801, 1800)
super().test_01_b_full_swap_reverse()