From c4321b774096401e4a673904bca830ca87ea14dd Mon Sep 17 00:00:00 2001 From: tecnovert Date: Tue, 20 Dec 2022 22:19:01 +0200 Subject: [PATCH] Add PARTct to coin code. --- basicswap/basicswap.py | 2 +- basicswap/interface/btc.py | 4 +- basicswap/interface/part.py | 120 ++++++++++++++++++++++- basicswap/interface/xmr.py | 3 +- basicswap/templates/offer_new_1.html | 7 +- tests/basicswap/test_partblind_xmr.py | 135 ++++++++++++++++++++++++-- 6 files changed, 250 insertions(+), 21 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 5f4b5da..3d00412 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -4765,7 +4765,7 @@ class BasicSwap(BaseApp): self.logBidEvent(bid.bid_id, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), session) try: - b_lock_tx_id = ci_to.publishBLockTx(xmr_swap.pkbv, xmr_swap.pkbs, bid.amount_to, xmr_offer.b_fee_rate, unlock_time=unlock_time) + b_lock_tx_id = ci_to.publishBLockTx(xmr_swap.vkbv, xmr_swap.pkbs, bid.amount_to, xmr_offer.b_fee_rate, unlock_time=unlock_time) if bid.debug_ind == DebugTypes.B_LOCK_TX_MISSED_SEND: self.log.debug('XMR bid %s: Debug %d - Losing xmr lock tx %s.', bid_id.hex(), bid.debug_ind, b_lock_tx_id.hex()) self.logBidEvent(bid.bid_id, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), session) diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 97bb771..dd3c50f 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -987,7 +987,7 @@ class BTCInterface(CoinInterface): tx = self.loadTx(tx_bytes) return tx.wit.vtxinwit[0].scriptWitness.stack[2] - def createBLockTx(self, Kbs, output_amount): + def createBLockTx(self, Kbs, output_amount, vkbv=None) -> bytes: tx = CTransaction() tx.nVersion = self.txVersion() p2wpkh_script_pk = self.getPkDest(Kbs) @@ -997,7 +997,7 @@ class BTCInterface(CoinInterface): def encodeSharedAddress(self, Kbv, Kbs): return self.pubkey_to_segwit_address(Kbs) - def publishBLockTx(self, Kbv, Kbs, output_amount, feerate, delay_for: int = 10, unlock_time: int = 0) -> bytes: + def publishBLockTx(self, kbv, Kbs, output_amount, feerate, delay_for: int = 10, unlock_time: int = 0) -> bytes: b_lock_tx = self.createBLockTx(Kbs, output_amount) b_lock_tx = self.fundTx(b_lock_tx, feerate) diff --git a/basicswap/interface/part.py b/basicswap/interface/part.py index d389f09..c2108f6 100644 --- a/basicswap/interface/part.py +++ b/basicswap/interface/part.py @@ -625,6 +625,122 @@ class PARTInterfaceBlind(PARTInterface): def getSpendableBalance(self): return self.make_int(self.rpc_callback('getbalances')['mine']['blind_trusted']) + def publishBLockTx(self, vkbv, Kbs, output_amount, feerate, delay_for: int = 10, unlock_time: int = 0) -> bytes: + Kbv = self.getPubkey(vkbv) + sx_addr = self.formatStealthAddress(Kbv, Kbs) + self._log.debug('sx_addr: {}'.format(sx_addr)) + + # TODO: Fund from other balances + params = ['blind', 'blind', + [{'address': sx_addr, 'amount': self.format_amount(output_amount)}, ], + '', '', self._anon_tx_ring_size, 1, False, + {'conf_target': self._conf_target, 'blind_watchonly_visible': True}] + + txid = self.rpc_callback('sendtypeto', params) + return bytes.fromhex(txid) + + def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender): + Kbv = self.getPubkey(kbv) + sx_addr = self.formatStealthAddress(Kbv, Kbs) + + # Tx recipient must import the stealth address as watch only + if bid_sender: + cb_swap_value *= -1 + else: + addr_info = self.rpc_callback('getaddressinfo', [sx_addr]) + if not addr_info['iswatchonly']: + wif_prefix = self.chainparams_network()['key_prefix'] + wif_scan_key = toWIF(wif_prefix, kbv) + self.rpc_callback('importstealthaddress', [wif_scan_key, Kbs.hex()]) + self._log.info('Imported watch-only sx_addr: {}'.format(sx_addr)) + self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height)) + self.rpc_callback('rescanblockchain', [restore_height]) + + params = [{'include_watchonly': True, 'search': sx_addr}] + txns = self.rpc_callback('filtertransactions', params) + + if len(txns) == 1: + tx = txns[0] + assert (tx['outputs'][0]['stealth_address'] == sx_addr) # Should not be possible + ensure(tx['outputs'][0]['type'] == 'blind', 'Output is not anon') + + if make_int(tx['outputs'][0]['amount']) == cb_swap_value: + height = 0 + if tx['confirmations'] > 0: + chain_height = self.rpc_callback('getblockcount') + height = chain_height - (tx['confirmations'] - 1) + return {'txid': tx['txid'], 'amount': cb_swap_value, 'height': height} + else: + self._log.warning('Incorrect amount detected for coin b lock txn: {}'.format(tx['txid'])) + return -1 + return None + + def spendBLockTx(self, chain_b_lock_txid, address_to, kbv, kbs, cb_swap_value, b_fee, restore_height, spend_actual_balance=False): + Kbv = self.getPubkey(kbv) + Kbs = self.getPubkey(kbs) + sx_addr = self.formatStealthAddress(Kbv, Kbs) + addr_info = self.rpc_callback('getaddressinfo', [sx_addr]) + if not addr_info['ismine']: + wif_prefix = self.chainparams_network()['key_prefix'] + wif_scan_key = toWIF(wif_prefix, kbv) + wif_spend_key = toWIF(wif_prefix, kbs) + self.rpc_callback('importstealthaddress', [wif_scan_key, wif_spend_key]) + self._log.info('Imported spend key for sx_addr: {}'.format(sx_addr)) + self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height)) + self.rpc_callback('rescanblockchain', [restore_height]) + + # TODO: Remove workaround + # utxos = self.rpc_callback('listunspentblind', [1, 9999999, [sx_addr]]) + utxos = [] + all_utxos = self.rpc_callback('listunspentblind', [1, 9999999]) + for utxo in all_utxos: + if utxo.get('stealth_address', '_') == sx_addr: + utxos.append(utxo) + if len(utxos) < 1: + raise TemporaryError('No spendable outputs') + elif len(utxos) > 1: + raise ValueError('Too many spendable outputs') + + utxo = utxos[0] + utxo_sats = make_int(utxo['amount']) + + if spend_actual_balance and utxo_sats != cb_swap_value: + self._log.warning('Spending actual balance {}, not swap value {}.'.format(utxo_sats, cb_swap_value)) + cb_swap_value = utxo_sats + + inputs = [{'tx': utxo['txid'], 'n': utxo['vout']}, ] + params = ['blind', 'blind', + [{'address': address_to, 'amount': self.format_amount(cb_swap_value), 'subfee': True}, ], + '', '', self._anon_tx_ring_size, 1, False, + {'conf_target': self._conf_target, 'inputs': inputs, 'show_fee': True}] + rv = self.rpc_callback('sendtypeto', params) + return bytes.fromhex(rv['txid']) + + def findTxnByHash(self, txid_hex): + # txindex is enabled for Particl + + try: + rv = self.rpc_callback('getrawtransaction', [txid_hex, True]) + except Exception as ex: + self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex)) + return None + + if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed: + return {'txid': txid_hex, 'amount': 0, 'height': rv['height']} + + return None + + def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str: + txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}]) + + options = { + 'lockUnspents': lock_unspents, + 'conf_target': self._conf_target, + } + if sub_fee: + options['subtractFeeFromOutputs'] = [0,] + return self.rpc_callback('fundrawtransactionfrom', ['blind', txn, options])['hex'] + class PARTInterfaceAnon(PARTInterface): @staticmethod @@ -638,9 +754,9 @@ class PARTInterfaceAnon(PARTInterface): def coin_name(self): return super().coin_name() + ' Anon' - def publishBLockTx(self, Kbv, Kbs, output_amount, feerate, delay_for: int = 10, unlock_time: int = 0) -> bytes: + def publishBLockTx(self, kbv, Kbs, output_amount, feerate, delay_for: int = 10, unlock_time: int = 0) -> bytes: + Kbv = self.getPubkey(kbv) sx_addr = self.formatStealthAddress(Kbv, Kbs) - self._log.debug('sx_addr: {}'.format(sx_addr)) # TODO: Fund from other balances params = ['anon', 'anon', diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index dc855ac..64f5e3e 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -258,11 +258,12 @@ class XMRInterface(CoinInterface): def encodeSharedAddress(self, Kbv, Kbs): return xmr_util.encode_address(Kbv, Kbs) - def publishBLockTx(self, Kbv, Kbs, output_amount, feerate, delay_for: int = 10, unlock_time: int = 0) -> bytes: + def publishBLockTx(self, kbv, Kbs, output_amount, feerate, delay_for: int = 10, unlock_time: int = 0) -> bytes: with self._mx_wallet: self.openWallet(self._wallet_filename) self.rpc_wallet_cb('refresh') + Kbv = self.getPubkey(kbv) shared_addr = xmr_util.encode_address(Kbv, Kbs) params = {'destinations': [{'amount': output_amount, 'address': shared_addr}], 'unlock_time': unlock_time} diff --git a/basicswap/templates/offer_new_1.html b/basicswap/templates/offer_new_1.html index 420eaf2..9513ad5 100644 --- a/basicswap/templates/offer_new_1.html +++ b/basicswap/templates/offer_new_1.html @@ -473,7 +473,7 @@ function set_swap_type_enabled(coin_from, coin_to, swap_type) { let make_hidden = false; - if (coin_to == '6' /* XMR */ || coin_to == '8' /* PART_ANON */) { + if (coin_to == '6' /* XMR */ || coin_to == '8' /* PART_ANON */ || coin_to == '7' /* PART_BLIND */ || coin_from == '7' /* PART_BLIND */) { swap_type.disabled = true; swap_type.value = 'xmr_swap'; make_hidden = true; @@ -486,12 +486,8 @@ swap_type.disabled = false; } let swap_type_hidden = document.getElementById('swap_type_hidden'); - console.log('make_hidden', make_hidden); - console.log('swap_type_hidden', swap_type_hidden); - console.log('swap_type.value', swap_type.value); if (make_hidden) { if (!swap_type_hidden) { - console.log('createElement'); swap_type_hidden = document.createElement('input'); swap_type_hidden.setAttribute('id', 'swap_type_hidden'); swap_type_hidden.setAttribute('type', 'hidden'); @@ -501,7 +497,6 @@ swap_type_hidden.setAttribute('value', swap_type.value); } else if (swap_type_hidden) { - console.log('remove element'); swap_type_hidden.parentNode.removeChild(swap_type_hidden); } } diff --git a/tests/basicswap/test_partblind_xmr.py b/tests/basicswap/test_partblind_xmr.py index 0754dc6..c85f11a 100644 --- a/tests/basicswap/test_partblind_xmr.py +++ b/tests/basicswap/test_partblind_xmr.py @@ -34,6 +34,7 @@ from tests.basicswap.common import ( wait_for_offer, wait_for_none_active, wait_for_balance, + wait_for_unspent, ) from .test_xmr import BaseTest, test_delay_event @@ -43,6 +44,8 @@ logger = logging.getLogger() class Test(BaseTest): __test__ = True + test_coin_from = Coins.PART_BLIND + has_segwit = True @classmethod def setUpClass(cls): @@ -65,6 +68,21 @@ class Test(BaseTest): js_0 = read_json_api(1800, 'wallets/part') node0_blind_before = js_0['blind_balance'] + js_0['blind_unconfirmed'] + def ensure_balance(self, coin_type, node_id, amount): + tla = 'PART' + js_w = read_json_api(1800 + node_id, 'wallets') + print('js_w', js_w) + if float(js_w[tla]['blind_balance']) < amount: + post_json = { + 'value': amount, + 'type_to': 'blind', + 'address': js_w[tla]['stealth_address'], + 'subfee': False, + } + json_rv = read_json_api(1800, 'wallets/{}/withdraw'.format(tla.lower()), post_json) + assert (len(json_rv['txid']) == 64) + wait_for_balance(test_delay_event, 'http://127.0.0.1:{}/json/wallets/{}'.format(1800 + node_id, tla.lower()), 'blind_balance', amount) + def getBalance(self, js_wallets): return float(js_wallets[Coins.PART.name]['blind_balance']) + float(js_wallets[Coins.PART.name]['blind_unconfirmed']) @@ -208,18 +226,17 @@ class Test(BaseTest): redeemed_txid = json.loads(urlopen('http://127.0.0.1:1801/json/bids/{}'.format(bid_id.hex()), data=data).read())['txid'] assert (len(redeemed_txid) == 64) - def test_04_follower_recover_b_lock_tx(self): - logging.info('---------- Test PARTct to XMR follower recovers coin b lock tx') + def do_test_04_follower_recover_b_lock_tx(self, coin_from, coin_to): + logging.info('---------- Test {} to {} follower recovers coin b lock tx'.format(coin_from.name, coin_to.name)) swap_clients = self.swap_clients + ci_from = swap_clients[0].ci(coin_from) + ci_to = swap_clients[0].ci(coin_to) - js_w0_before = read_json_api(1800, 'wallets') - js_w1_before = read_json_api(1801, 'wallets') - - amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) - rate_swap = make_int(random.uniform(0.2, 20.0), scale=12, 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) offer_id = swap_clients[0].postOffer( - Coins.PART_BLIND, Coins.XMR, 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=28) wait_for_offer(test_delay_event, swap_clients[1], offer_id) offer = swap_clients[1].getOffer(offer_id) @@ -238,9 +255,13 @@ class Test(BaseTest): wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=180) wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True) + def test_04_follower_recover_b_lock_tx(self): + js_w0_before = read_json_api(1800, 'wallets') + js_w1_before = read_json_api(1801, 'wallets') + + self.do_test_04_follower_recover_b_lock_tx(self.test_coin_from, Coins.XMR) js_w0_after = read_json_api(1800, 'wallets') js_w1_after = read_json_api(1801, 'wallets') - node0_blind_before = self.getBalance(js_w0_before) node0_blind_after = self.getBalance(js_w0_after) assert (node0_blind_before - node0_blind_after < 0.02) @@ -249,6 +270,102 @@ class Test(BaseTest): node1_xmr_after = self.getXmrBalance(js_w1_after) assert (node1_xmr_before - node1_xmr_after < 0.02) + def test_04_follower_recover_b_lock_tx_from_part(self): + self.ensure_balance(self.test_coin_from, 1, 50.0) + self.do_test_04_follower_recover_b_lock_tx(Coins.PART, self.test_coin_from) + + def do_test_05_self_bid(self, coin_from, coin_to): + logging.info('---------- Test {} to {} same client'.format(coin_from.name, coin_to.name)) + + swap_clients = self.swap_clients + ci_to = swap_clients[0].ci(coin_to) + + self.ensure_balance(coin_from, 1, 50.0) + + amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) + rate_swap = ci_to.make_int(random.uniform(0.2, 20.0), r=1) + + offer_id = swap_clients[1].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, auto_accept_bids=True) + bid_id = swap_clients[1].postXmrBid(offer_id, amt_swap) + + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) + + def test_05_self_bid(self): + if not self.has_segwit: + return + self.do_test_05_self_bid(self.test_coin_from, Coins.XMR) + + def test_05_self_bid_to_part(self): + if not self.has_segwit: + return + self.do_test_05_self_bid(self.test_coin_from, Coins.PART) + + def test_05_self_bid_from_part(self): + self.do_test_05_self_bid(Coins.PART, self.test_coin_from) + + def test_06_preselect_inputs(self): + raise ValueError('TODO') + tla_from = self.test_coin_from.name + logging.info('---------- Test {} Preselected inputs'.format(tla_from)) + swap_clients = self.swap_clients + + # Prepare balance + self.ensure_balance(self.test_coin_from, 2, 100.0) + + js_w2 = read_json_api(1802, 'wallets') + print('[rm] js_w2', js_w2) + post_json = { + 'value': float(js_w2['PART']['blind_balance']), + 'type_from': 'blind', + 'type_to': 'blind', + 'address': js_w2['PART']['stealth_address'], + 'subfee': True, + } + json_rv = read_json_api(1802, 'wallets/{}/withdraw'.format('part'), post_json) + wait_for_balance(test_delay_event, 'http://127.0.0.1:1802/json/wallets/{}'.format('part'), 'blind_balance', 10.0) + assert (len(json_rv['txid']) == 64) + + # Create prefunded ITX + ci = swap_clients[2].ci(self.test_coin_from) + ci_to = swap_clients[2].ci(Coins.XMR) + pi = swap_clients[2].pi(SwapTypes.XMR_SWAP) + js_w2 = read_json_api(1802, 'wallets') + swap_value = ci.make_int(js_w2['PART']['blind_balance']) + assert (swap_value > ci.make_int(95)) + + itx = pi.getFundedInitiateTxTemplate(ci, swap_value, True) + itx_decoded = ci.describeTx(itx.hex()) + n = pi.findMockVout(ci, itx_decoded) + value_after_subfee = ci.make_int(itx_decoded['vout'][n]['value']) + assert (value_after_subfee < swap_value) + swap_value = value_after_subfee + wait_for_unspent(test_delay_event, ci, swap_value) + + extra_options = {'prefunded_itx': itx} + rate_swap = ci_to.make_int(random.uniform(0.2, 20.0)) + offer_id = swap_clients[2].postOffer(self.test_coin_from, Coins.XMR, swap_value, rate_swap, swap_value, SwapTypes.XMR_SWAP, extra_options=extra_options) + + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offer = swap_clients[1].getOffer(offer_id) + bid_id = swap_clients[1].postBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[2], bid_id, BidStates.BID_RECEIVED) + swap_clients[2].acceptBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[2], bid_id, BidStates.SWAP_COMPLETED, wait_for=120) + wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=120) + + # Verify expected inputs were used + bid, _, _, _, _ = swap_clients[2].getXmrBidAndOffer(bid_id) + assert (bid.xmr_a_lock_tx) + wtx = ci.rpc_callback('gettransaction', [bid.xmr_a_lock_tx.txid.hex(),]) + itx_after = ci.describeTx(wtx['hex']) + assert (len(itx_after['vin']) == len(itx_decoded['vin'])) + for i, txin in enumerate(itx_decoded['vin']): + txin_after = itx_after['vin'][i] + assert (txin['txid'] == txin_after['txid']) + assert (txin['vout'] == txin_after['vout']) + if __name__ == '__main__': unittest.main()