diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 31f07cc..627d5cf 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -2987,7 +2987,7 @@ class BasicSwap(BaseApp): refundExtraArgs = dict() lockExtraArgs = dict() 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 # refund script 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.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 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') elif offer_data.swap_type == SwapTypes.XMR_SWAP: ensure(offer_data.protocol_version >= MINPROTO_VERSION_ADAPTOR_SIG, 'Invalid protocol version') - if Coins.BCH not in (coin_from, coin_to): - if reverse_bid: - ensure(ci_to.has_segwit(), 'Coin-to must support segwit for reverse bid offers') - else: - ensure(ci_from.has_segwit(), 'Coin-from must support segwit') + if reverse_bid: + ensure(ci_to.has_segwit(), 'Coin-to must support segwit for reverse bid offers') + else: + ensure(ci_from.has_segwit(), 'Coin-from must support segwit') ensure(len(offer_data.proof_address) == 0, 'Unexpected data') ensure(len(offer_data.proof_signature) == 0, 'Unexpected data') ensure(len(offer_data.pkhash_seller) == 0, 'Unexpected data') diff --git a/basicswap/interface/bch.py b/basicswap/interface/bch.py index 93dc351..e2b6cb3 100644 --- a/basicswap/interface/bch.py +++ b/basicswap/interface/bch.py @@ -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.chainparams import Coins 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 ( OP_TXINPUTCOUNT, OP_1, @@ -71,8 +71,13 @@ class BCHInterface(BTCInterface): def __init__(self, coin_settings, network, swap_client=None): super(BCHInterface, self).__init__(coin_settings, network, swap_client) # No multiwallet support + self.swap_client = swap_client 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): return 'bch' @@ -148,7 +153,7 @@ class BCHInterface(BTCInterface): return CScript([OP_HASH256, script_hash, OP_EQUAL]) 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) def getSpendableBalance(self) -> int: @@ -321,16 +326,19 @@ class BCHInterface(BTCInterface): address.prefix = prefix return address.cash_address() + def encodeSharedAddress(self, Kbv, Kbs): + return self.pkh_to_address(hash160(Kbs)) + def addressToLockingBytecode(self, address: str) -> bytes: 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): return self.scriptToP2SH32LockingBytecode(script) def scriptToP2SH32LockingBytecode(self, script: Union[bytes, str]) -> bytes: - if isinstance(script, str): - script = bytes.fromhex(script) - return CScript([ OP_HASH256, sha256(sha256(script)), diff --git a/basicswap/interface/contrib/bch_test_framework/cashaddress.py b/basicswap/interface/contrib/bch_test_framework/cashaddress.py index 249e6b1..7627e9f 100644 --- a/basicswap/interface/contrib/bch_test_framework/cashaddress.py +++ b/basicswap/interface/contrib/bch_test_framework/cashaddress.py @@ -210,7 +210,7 @@ class Address: if address.upper() != address and address.lower() != address: raise ValueError( - "Cash address contains uppercase and lowercase characters" + "Cash address contains uppercase and lowercase characters: " + address ) address = address.lower() diff --git a/tests/basicswap/test_bch_xmr.py b/tests/basicswap/test_bch_xmr.py index ebd8877..fe4cf78 100644 --- a/tests/basicswap/test_bch_xmr.py +++ b/tests/basicswap/test_bch_xmr.py @@ -30,10 +30,12 @@ from basicswap.util import ( ) from basicswap.interface.base import Curves from basicswap.util.crypto import sha256 +from tests.basicswap.test_btc_xmr import BasicSwapTest from tests.basicswap.util import ( read_json_api, ) from tests.basicswap.common import ( + BCH_BASE_RPC_PORT, abandon_all_swaps, wait_for_bid, wait_for_event, @@ -70,20 +72,216 @@ logger = logging.getLogger() class TestFunctions(BaseTest): - __test__ = True + __test__ = False start_bch_nodes = True - base_rpc_port = None + base_rpc_port = BCH_BASE_RPC_PORT extra_wait_time = 0 + test_coin_from = Coins.BCH + + def callnoderpc(self, method, params=[], wallet=None, node_id=0): return callnoderpc(node_id, method, params, wallet, self.base_rpc_port) 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): - deploymentinfo = self.callnoderpc('getdeploymentinfo') - assert (deploymentinfo['deployments'][feature_name]['active'] is True) + return 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): 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'] < 10) - def test_05_bch_xmr(self): - logging.info('---------- Test BCH to XMR') + def test_011_p2sh(self): + # Not used in bsx for native-segwit coins + logging.info('---------- Test {} p2sh'.format(self.test_coin_from.name)) + 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] + ci = self.swap_clients[0].ci(self.test_coin_from) - 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) - assert (xmr_swap) + # Double check output type + 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) - wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True) + 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) - 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() \ No newline at end of file