From 7d43512845af909d7f4fc22a6cf4b27f9945ed8d Mon Sep 17 00:00:00 2001 From: tecnovert <tecnovert@tecnovert.net> Date: Tue, 6 Dec 2022 00:45:35 +0200 Subject: [PATCH] xmr: Add prefunded itx. --- basicswap/basicswap.py | 16 ++++-- basicswap/interface/btc.py | 13 ++--- basicswap/interface/firo.py | 5 +- basicswap/interface/part.py | 5 +- basicswap/protocols/__init__.py | 12 +++++ basicswap/protocols/atomic_swap_1.py | 19 ++------ basicswap/protocols/xmr_swap_1.py | 38 +++++++++++++++ tests/basicswap/test_run.py | 10 ++++ tests/basicswap/test_xmr.py | 73 ++++++++++++++++++++++++++++ 9 files changed, 156 insertions(+), 35 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 3289c7b..7be311e 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -2424,11 +2424,17 @@ class BasicSwap(BaseApp): xmr_swap.kbsl_dleag = xmr_swap.pkbsl # MSG2F - xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script = ci_from.createSCLockTx( - bid.amount, - xmr_swap.pkal, xmr_swap.pkaf, xmr_swap.vkbv - ) - xmr_swap.a_lock_tx = ci_from.fundSCLockTx(xmr_swap.a_lock_tx, xmr_offer.a_fee_rate, xmr_swap.vkbv) + pi = self.pi(SwapTypes.XMR_SWAP) + xmr_swap.a_lock_tx_script = pi.genScriptLockTxScript(ci_from, xmr_swap.pkal, xmr_swap.pkaf) + prefunded_tx = self.getPreFundedTx(Concepts.OFFER, bid.offer_id, TxTypes.ITX_PRE_FUNDED) + if prefunded_tx: + xmr_swap.a_lock_tx = pi.promoteMockTx(ci_from, prefunded_tx, xmr_swap.a_lock_tx_script) + else: + xmr_swap.a_lock_tx = ci_from.createSCLockTx( + bid.amount, + xmr_swap.a_lock_tx_script, xmr_swap.vkbv + ) + xmr_swap.a_lock_tx = ci_from.fundSCLockTx(xmr_swap.a_lock_tx, xmr_offer.a_fee_rate, xmr_swap.vkbv) xmr_swap.a_lock_tx_id = ci_from.getTxid(xmr_swap.a_lock_tx) a_lock_tx_dest = ci_from.getScriptDest(xmr_swap.a_lock_tx_script) diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 2b6c1fa..a96e246 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -444,19 +444,11 @@ class BTCInterface(CoinInterface): return pk1, pk2 - def genScriptLockTxScript(self, Kal, Kaf): - Kal_enc = Kal if len(Kal) == 33 else self.encodePubkey(Kal) - Kaf_enc = Kaf if len(Kaf) == 33 else self.encodePubkey(Kaf) - - return CScript([2, Kal_enc, Kaf_enc, 2, CScriptOp(OP_CHECKMULTISIG)]) - - def createSCLockTx(self, value, Kal, Kaf, vkbv=None): - script = self.genScriptLockTxScript(Kal, Kaf) + def createSCLockTx(self, value: int, script: bytearray, vkbv=None) -> bytes: tx = CTransaction() tx.nVersion = self.txVersion() tx.vout.append(self.txoType()(value, self.getScriptDest(script))) - - return tx.serialize(), script + return tx.serialize() def fundSCLockTx(self, tx_bytes, feerate, vkbv=None): return self.fundTx(tx_bytes, feerate) @@ -1271,6 +1263,7 @@ class BTCInterface(CoinInterface): sign_for_addr = None for addr, value in unspent_addr.items(): + print('[rm]', value, amount_for) if value >= amount_for: sign_for_addr = addr break diff --git a/basicswap/interface/firo.py b/basicswap/interface/firo.py index 2490e68..e14bf48 100644 --- a/basicswap/interface/firo.py +++ b/basicswap/interface/firo.py @@ -117,13 +117,12 @@ class FIROInterface(BTCInterface): return rv - def createSCLockTx(self, value, Kal, Kaf, vkbv=None): - script = self.genScriptLockTxScript(Kal, Kaf) + def createSCLockTx(self, value: int, script: bytearray, vkbv=None) -> bytes: tx = CTransaction() tx.nVersion = self.txVersion() tx.vout.append(self.txoType()(value, self.getScriptDest(script))) - return tx.serialize(), script + return tx.serialize() def fundSCLockTx(self, tx_bytes, feerate, vkbv=None): return self.fundTx(tx_bytes, feerate) diff --git a/basicswap/interface/part.py b/basicswap/interface/part.py index ade3b64..bc542e1 100644 --- a/basicswap/interface/part.py +++ b/basicswap/interface/part.py @@ -166,8 +166,7 @@ class PARTInterfaceBlind(PARTInterface): ensure(v['result'] is True, 'verifycommitment failed') return output_n, blinded_info - def createSCLockTx(self, value, Kal, Kaf, vkbv): - script = self.genScriptLockTxScript(Kal, Kaf) + def createSCLockTx(self, value: int, script: bytearray, vkbv) -> bytes: # Nonce is derived from vkbv, ephemeral_key isn't used ephemeral_key = i2b(self.getNewSecretKey()) @@ -181,7 +180,7 @@ class PARTInterfaceBlind(PARTInterface): rv = self.rpc_callback('createrawparttransaction', params) tx_bytes = bytes.fromhex(rv['hex']) - return tx_bytes, script + return tx_bytes def fundSCLockTx(self, tx_bytes, feerate, vkbv): feerate_str = self.format_amount(feerate) diff --git a/basicswap/protocols/__init__.py b/basicswap/protocols/__init__.py index 13402f8..1c4769f 100644 --- a/basicswap/protocols/__init__.py +++ b/basicswap/protocols/__init__.py @@ -4,9 +4,21 @@ # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. +from basicswap.script import ( + OpCodes, +) + class ProtocolInterface: swap_type = None def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes: raise ValueError('base class') + + def getMockScript(self) -> bytearray: + return bytearray([ + OpCodes.OP_RETURN, OpCodes.OP_1]) + + def getMockScriptScriptPubkey(self, ci) -> bytearray: + script = self.getMockScript() + return ci.get_p2wsh_script_pubkey(script) if ci._use_segwit else ci.get_p2sh_script_pubkey(script) diff --git a/basicswap/protocols/atomic_swap_1.py b/basicswap/protocols/atomic_swap_1.py index 4917947..37f5161 100644 --- a/basicswap/protocols/atomic_swap_1.py +++ b/basicswap/protocols/atomic_swap_1.py @@ -76,13 +76,12 @@ def redeemITx(self, bid_id, session): class AtomicSwapInterface(ProtocolInterface): swap_type = SwapTypes.SELLER_FIRST - def getMockScript(self) -> bytearray: - return bytearray([ - OpCodes.OP_RETURN, OpCodes.OP_1]) - - def getMockScriptScriptPubkey(self, ci) -> bytearray: + def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes: script = self.getMockScript() - return ci.get_p2wsh_script_pubkey(script) if ci._use_segwit else ci.get_p2sh_script_pubkey(script) + addr_to = ci.encode_p2wsh(getP2WSH(script)) if ci._use_segwit else ci.encode_p2sh(script) + funded_tx = ci.createRawFundedTransaction(addr_to, amount, sub_fee, lock_unspents=False) + + return bytes.fromhex(funded_tx) def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray: mock_txo_script = self.getMockScriptScriptPubkey(ci) @@ -103,11 +102,3 @@ class AtomicSwapInterface(ProtocolInterface): funded_tx = ctx.serialize() return ci.signTxWithWallet(funded_tx) - - def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes: - - script = self.getMockScript() - addr_to = ci.encode_p2wsh(getP2WSH(script)) if ci._use_segwit else ci.encode_p2sh(script) - funded_tx = ci.createRawFundedTransaction(addr_to, amount, sub_fee, lock_unspents=False) - - return bytes.fromhex(funded_tx) diff --git a/basicswap/protocols/xmr_swap_1.py b/basicswap/protocols/xmr_swap_1.py index 240ff07..8cc03a6 100644 --- a/basicswap/protocols/xmr_swap_1.py +++ b/basicswap/protocols/xmr_swap_1.py @@ -9,6 +9,9 @@ from sqlalchemy.orm import scoped_session from basicswap.util import ( ensure, ) +from basicswap.util.script import ( + getP2WSH, +) from basicswap.chainparams import ( Coins, ) @@ -18,6 +21,9 @@ from basicswap.basicswap_util import ( EventLogTypes, ) from . import ProtocolInterface +from basicswap.contrib.test_framework.script import ( + CScript, CScriptOp, + OP_CHECKMULTISIG) def addLockRefundSigs(self, xmr_swap, ci): @@ -90,3 +96,35 @@ def getChainBSplitKey(swap_client, bid, xmr_swap, offer): class XmrSwapInterface(ProtocolInterface): swap_type = SwapTypes.XMR_SWAP + + def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes) -> CScript: + Kal_enc = Kal if len(Kal) == 33 else ci.encodePubkey(Kal) + Kaf_enc = Kaf if len(Kaf) == 33 else ci.encodePubkey(Kaf) + + return CScript([2, Kal_enc, Kaf_enc, 2, CScriptOp(OP_CHECKMULTISIG)]) + + def getFundedInitiateTxTemplate(self, ci, amount: int, sub_fee: bool) -> bytes: + script = self.getMockScript() + addr_to = ci.encode_p2wsh(getP2WSH(script)) if ci._use_segwit else ci.encode_p2sh(script) + funded_tx = ci.createRawFundedTransaction(addr_to, amount, sub_fee, lock_unspents=False) + + return bytes.fromhex(funded_tx) + + def promoteMockTx(self, ci, mock_tx: bytes, script: bytearray) -> bytearray: + mock_txo_script = self.getMockScriptScriptPubkey(ci) + real_txo_script = ci.getScriptDest(script) + + found: int = 0 + ctx = ci.loadTx(mock_tx) + for txo in ctx.vout: + if txo.scriptPubKey == mock_txo_script: + txo.scriptPubKey = real_txo_script + found += 1 + + if found < 1: + raise ValueError('Mocked output not found') + if found > 1: + raise ValueError('Too many mocked outputs found') + ctx.nLockTime = 0 + + return ctx.serialize() diff --git a/tests/basicswap/test_run.py b/tests/basicswap/test_run.py index f823120..85e71f1 100644 --- a/tests/basicswap/test_run.py +++ b/tests/basicswap/test_run.py @@ -560,6 +560,16 @@ class Test(BaseTest): wait_for_bid(test_delay_event, swap_clients[2], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60) + # Verify expected inputs were used + bid, offer = swap_clients[2].getBidAndOffer(bid_id) + assert (bid.initiate_tx) + wtx = ci.rpc_callback('gettransaction', [bid.initiate_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']): + assert (txin['txid'] == itx_after['vin'][i]['txid']) + assert (txin['vout'] == itx_after['vin'][i]['vout']) + def pass_99_delay(self): logging.info('Delay') for i in range(60 * 10): diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index 993b3c5..e43c38c 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -75,6 +75,7 @@ from tests.basicswap.common import ( wait_for_no_offer, wait_for_none_active, wait_for_balance, + wait_for_unspent, compare_bid_states, extract_states_from_xu_file, TEST_HTTP_HOST, @@ -1244,6 +1245,78 @@ class Test(BaseTest): swap_clients[0].abandonBid(bid_id) swap_clients[1].abandonBid(bid_id) + def test_14_sweep_balance(self): + logging.info('---------- Test sweep balance offer') + swap_clients = self.swap_clients + + # Disable staking + walletsettings = callnoderpc(2, 'walletsettings', ['stakingoptions', ]) + walletsettings['enabled'] = False + walletsettings = callnoderpc(2, 'walletsettings', ['stakingoptions', walletsettings]) + walletsettings = callnoderpc(2, 'walletsettings', ['stakingoptions', ]) + assert (walletsettings['stakingoptions']['enabled'] is False) + + # Prepare balance + js_w2 = read_json_api(1802, 'wallets') + if float(js_w2['PART']['balance']) < 100.0: + post_json = { + 'value': 100, + 'address': js_w2['PART']['deposit_address'], + 'subfee': False, + } + json_rv = read_json_api(TEST_HTTP_PORT + 0, 'wallets/part/withdraw', post_json) + assert (len(json_rv['txid']) == 64) + wait_for_balance(test_delay_event, 'http://127.0.0.1:1802/json/wallets/part', 'balance', 100.0) + + js_w2 = read_json_api(1802, 'wallets') + assert (float(js_w2['PART']['balance']) >= 100.0) + + js_w2 = read_json_api(1802, 'wallets') + post_json = { + 'value': float(js_w2['PART']['balance']), + 'address': read_json_api(1802, 'wallets/part/nextdepositaddr'), + 'subfee': True, + } + json_rv = read_json_api(TEST_HTTP_PORT + 2, 'wallets/part/withdraw', post_json) + wait_for_balance(test_delay_event, 'http://127.0.0.1:1802/json/wallets/part', 'balance', 10.0) + assert (len(json_rv['txid']) == 64) + + # Create prefunded ITX + ci = swap_clients[2].ci(Coins.PART) + pi = swap_clients[2].pi(SwapTypes.XMR_SWAP) + js_w2 = read_json_api(1802, 'wallets') + swap_value = ci.make_int(js_w2['PART']['balance']) + + itx = pi.getFundedInitiateTxTemplate(ci, swap_value, True) + itx_decoded = ci.describeTx(itx.hex()) + value_after_subfee = ci.make_int(itx_decoded['vout'][0]['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} + offer_id = swap_clients[2].postOffer(Coins.PART, Coins.XMR, swap_value, 2 * COIN, 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']): + assert (txin['txid'] == itx_after['vin'][i]['txid']) + assert (txin['vout'] == itx_after['vin'][i]['vout']) + def test_98_withdraw_all(self): logging.info('---------- Test XMR withdrawal all') try: