diff --git a/basicswap/__init__.py b/basicswap/__init__.py index 08e5b09..c1ec3f7 100644 --- a/basicswap/__init__.py +++ b/basicswap/__init__.py @@ -1,3 +1,3 @@ name = "basicswap" -__version__ = "0.0.23" +__version__ = "0.0.24" diff --git a/basicswap/base.py b/basicswap/base.py index 3552d60..524c97b 100644 --- a/basicswap/base.py +++ b/basicswap/base.py @@ -18,6 +18,9 @@ from .rpc import ( from .util import ( pubkeyToAddress, ) +from .basicswap_util import ( + TemporaryError, +) from .chainparams import ( Coins, chainparams, @@ -136,5 +139,7 @@ class BaseApp: return out[0].decode('utf-8').strip() def is_transient_error(self, ex): + if isinstance(ex, TemporaryError): + return True str_error = str(ex).lower() return 'read timed out' in str_error or 'no connection to daemon' in str_error diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 96702fa..0e35a58 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -1756,7 +1756,7 @@ class BasicSwap(BaseApp): script = atomic_swap_1.buildContractScript(sequence, secret_hash, bid.pkhash_buyer, pkhash_refund) else: if offer.lock_type == ABS_LOCK_BLOCKS: - lock_value = self.callcoinrpc(coin_from, 'getblockchaininfo')['blocks'] + offer.lock_value + lock_value = self.callcoinrpc(coin_from, 'getblockcount') + offer.lock_value else: lock_value = int(time.time()) + offer.lock_value self.log.debug('Initiate %s lock_value %d %d', coin_from, offer.lock_value, lock_value) @@ -2225,7 +2225,7 @@ class BasicSwap(BaseApp): refund_txn = self.createRefundTxn(coin_to, txn_signed, offer, bid, participate_script, tx_type=TxTypes.PTX_REFUND) bid.participate_txn_refund = bytes.fromhex(refund_txn) - chain_height = self.callcoinrpc(coin_to, 'getblockchaininfo')['blocks'] + chain_height = self.callcoinrpc(coin_to, 'getblockcount') txjs = self.callcoinrpc(coin_to, 'decoderawtransaction', [txn_signed]) txid = txjs['txid'] @@ -2554,7 +2554,7 @@ class BasicSwap(BaseApp): return self.lookupUnspentByAddress(coin_type, address, sum_output=True) def lookupChainHeight(self, coin_type): - return self.callcoinrpc(coin_type, 'getblockchaininfo')['blocks'] + return self.callcoinrpc(coin_type, 'getblockcount') def lookupUnspentByAddress(self, coin_type, address, sum_output=False, assert_amount=None, assert_txid=None): @@ -2589,7 +2589,7 @@ class BasicSwap(BaseApp): except Exception: pass - num_blocks = self.callcoinrpc(coin_type, 'getblockchaininfo')['blocks'] + num_blocks = self.callcoinrpc(coin_type, 'getblockcount') sum_unspent = 0 self.log.debug('[rm] scantxoutset start') # scantxoutset is slow @@ -2782,7 +2782,7 @@ class BasicSwap(BaseApp): bid_changed = False # Have to use findTxB instead of relying on the first seen height to detect chain reorgs - found_tx = ci_to.findTxB(xmr_swap.vkbv, xmr_swap.pkbs, bid.amount_to, ci_to.blocks_confirmed, xmr_swap.b_restore_height) + found_tx = ci_to.findTxB(xmr_swap.vkbv, xmr_swap.pkbs, bid.amount_to, ci_to.blocks_confirmed, xmr_swap.b_restore_height, bid.was_sent) if isinstance(found_tx, int) and found_tx == -1: if self.countBidEvents(bid, EventLogTypes.LOCK_TX_B_INVALID, session) < 1: @@ -3249,7 +3249,7 @@ class BasicSwap(BaseApp): spend_txn = self.callcoinrpc(Coins.PART, 'getrawtransaction', [spend_txid, True]) self.processSpentOutput(coin_type, o, spend_txid, spend_n, spend_txn) else: - chain_blocks = self.callcoinrpc(coin_type, 'getblockchaininfo')['blocks'] + chain_blocks = self.callcoinrpc(coin_type, 'getblockcount') last_height_checked = c['last_height_checked'] self.log.debug('chain_blocks, last_height_checked %s %s', chain_blocks, last_height_checked) while last_height_checked < chain_blocks: @@ -4288,8 +4288,11 @@ class BasicSwap(BaseApp): vkbs = ci_to.sumKeys(kbsl, kbsf) try: - address_to = self.getCachedMainWalletAddress(ci_to) - txid = ci_to.spendBLockTx(address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, xmr_swap.b_restore_height) + if coin_to == Coins.XMR: + address_to = self.getCachedMainWalletAddress(ci_to) + else: + address_to = self.getCachedStealthAddressForCoin(coin_to) + txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, xmr_swap.b_restore_height) self.log.debug('Submitted lock B spend txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex()) self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_SPEND_TX_PUBLISHED, '', session) except Exception as ex: @@ -4345,7 +4348,7 @@ class BasicSwap(BaseApp): try: address_to = self.getCachedMainWalletAddress(ci_to) - txid = ci_to.spendBLockTx(address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, xmr_swap.b_restore_height) + txid = ci_to.spendBLockTx(xmr_swap.b_lock_tx_id, address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, xmr_swap.b_restore_height) self.log.debug('Submitted lock B refund txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex()) self.logBidEvent(bid.bid_id, EventLogTypes.LOCK_TX_B_REFUND_TX_PUBLISHED, '', session) except Exception as ex: diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index e3aa942..f3265e5 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -371,3 +371,7 @@ def isActiveBidState(state): if state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED: return True return False + + +class TemporaryError(ValueError): + pass diff --git a/basicswap/interface_btc.py b/basicswap/interface_btc.py index 095e37d..620a514 100644 --- a/basicswap/interface_btc.py +++ b/basicswap/interface_btc.py @@ -175,7 +175,7 @@ class BTCInterface(CoinInterface): return self.rpc_callback('getblockchaininfo') def getChainHeight(self): - return self.rpc_callback('getblockchaininfo')['blocks'] + return self.rpc_callback('getblockcount') def getMempoolTx(self, txid): return self.rpc_callback('getrawtransaction', [txid.hex()]) @@ -866,7 +866,7 @@ class BTCInterface(CoinInterface): weight = len_nwit * (wsf - 1) + len_full return (weight + wsf - 1) // wsf - def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height): + def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender): raw_dest = self.getPkDest(Kbs) rv = self.scanTxOutset(raw_dest) @@ -898,7 +898,7 @@ class BTCInterface(CoinInterface): return True return False - def spendBLockTx(self, address_to, kbv, kbs, cb_swap_value, b_fee, restore_height): + def spendBLockTx(self, chain_b_lock_txid, address_to, kbv, kbs, cb_swap_value, b_fee, restore_height): print('TODO: spendBLockTx') def getOutput(self, txid, dest_script, expect_value): diff --git a/basicswap/interface_part.py b/basicswap/interface_part.py index 100ac90..5272e40 100644 --- a/basicswap/interface_part.py +++ b/basicswap/interface_part.py @@ -15,7 +15,13 @@ from .contrib.test_framework.script import ( OP_DUP, OP_HASH160, OP_EQUALVERIFY, OP_CHECKSIG ) -from .util import encodeStealthAddress +from .util import ( + encodeStealthAddress, + toWIF, + ensure, + make_int) +from .basicswap_util import ( + TemporaryError) from .chainparams import Coins, chainparams from .interface_btc import BTCInterface @@ -117,8 +123,83 @@ class PARTInterfaceAnon(PARTInterface): txid = self.rpc_callback('sendtypeto', params) return bytes.fromhex(txid) - def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height): - raise ValueError('TODO - new core release') + 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) + self._log.debug('sx_addr: {}'.format(sx_addr)) - def spendBLockTx(self, address_to, kbv, kbs, cb_swap_value, b_fee, restore_height): - raise ValueError('TODO - new core release') + # 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 = chainparams[self.coin_type()][self._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(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'] == 'anon', '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): + 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 = chainparams[self.coin_type()][self._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(restore_height)) + self.rpc_callback('rescanblockchain', [restore_height]) + + autxos = self.rpc_callback('listunspentanon') + if len(autxos) < 1: + raise TemporaryError('No spendable outputs') + elif len(autxos) > 1: + raise ValueError('Too many spendable outputs') + + utxo = autxos[0] + + inputs = [{'tx': utxo['txid'], 'n': utxo['vout']}, ] + params = ['anon', 'anon', + [{'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 diff --git a/basicswap/interface_xmr.py b/basicswap/interface_xmr.py index 33aa606..c9b11ec 100644 --- a/basicswap/interface_xmr.py +++ b/basicswap/interface_xmr.py @@ -27,6 +27,8 @@ from .util import ( dumpj, make_int, format_amount) +from .basicswap_util import ( + TemporaryError) from .rpc_xmr import ( make_xmr_rpc_func, make_xmr_rpc2_func, @@ -232,7 +234,7 @@ class XMRInterface(CoinInterface): return tx_hash - def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height): + def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender): with self._mx_wallet: Kbv = self.getPubkey(kbv) address_b58 = xmr_util.encode_address(Kbv, Kbs) @@ -363,7 +365,7 @@ class XMRInterface(CoinInterface): return None - def spendBLockTx(self, address_to, kbv, kbs, cb_swap_value, b_fee_rate, restore_height): + def spendBLockTx(self, chain_b_lock_txid, address_to, kbv, kbs, cb_swap_value, b_fee_rate, restore_height): with self._mx_wallet: Kbv = self.getPubkey(kbv) Kbs = self.getPubkey(kbs) @@ -401,10 +403,10 @@ class XMRInterface(CoinInterface): time.sleep(1 + i) if rv['balance'] < cb_swap_value: self._log.error('wallet {} balance {}, expected {}'.format(wallet_filename, rv['balance'], cb_swap_value)) - raise ValueError('Invalid balance') + raise TemporaryError('Invalid balance') if rv['unlocked_balance'] < cb_swap_value: self._log.error('wallet {} balance {}, expected {}, blocks_to_unlock {}'.format(wallet_filename, rv['unlocked_balance'], cb_swap_value, rv['blocks_to_unlock'])) - raise ValueError('Invalid unlocked_balance') + raise TemporaryError('Invalid unlocked_balance') params = {'address': address_to} if self._fee_priority > 0: diff --git a/basicswap/util.py b/basicswap/util.py index 97b8cf5..78b0a88 100644 --- a/basicswap/util.py +++ b/basicswap/util.py @@ -246,6 +246,10 @@ def make_int(v, scale=8, r=0): # r = 0, no rounding, fail, r > 0 round up, r < elif type(v) == int: return v * 10 ** scale + sign = 1 + if v[0] == '-': + v = v[1:] + sign = -1 ep = 10 ** scale have_dp = False rv = 0 @@ -255,7 +259,6 @@ def make_int(v, scale=8, r=0): # r = 0, no rounding, fail, r > 0 round up, r < have_dp = True continue if not c.isdigit(): - raise ValueError('Invalid char: ' + c) if have_dp: ep //= 10 @@ -267,13 +270,12 @@ def make_int(v, scale=8, r=0): # r = 0, no rounding, fail, r > 0 round up, r < if int(c) > 4: rv += 1 break - rv += ep * int(c) else: rv = rv * 10 + int(c) if not have_dp: rv *= ep - return rv + return rv * sign def validate_amount(amount, scale=8) -> bool: diff --git a/tests/basicswap/extended/test_network.py b/tests/basicswap/extended/test_network.py index 3970d38..640bfc3 100644 --- a/tests/basicswap/extended/test_network.py +++ b/tests/basicswap/extended/test_network.py @@ -171,7 +171,6 @@ class Test(unittest.TestCase): cls.part_daemons = [] cls.btc_daemons = [] - cls.part_stakelimit = 0 cls.btc_addr = None logger.propagate = False diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index 0a42f4e..fc2cf76 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -33,6 +33,7 @@ from basicswap.util import ( COIN, toWIF, make_int, + format_amount, ) from basicswap.rpc import ( callrpc, @@ -279,7 +280,6 @@ class Test(unittest.TestCase): cls.xmr_daemons = [] cls.xmr_wallet_auth = [] - cls.part_stakelimit = 0 cls.xmr_addr = None cls.btc_addr = None @@ -395,6 +395,14 @@ class Test(unittest.TestCase): callrpc_xmr_na(XMR_BASE_RPC_PORT + 1, 'generateblocks', {'wallet_address': cls.xmr_addr, 'amount_of_blocks': num_blocks}) logging.info('XMR blocks: %d', callrpc_xmr_na(XMR_BASE_RPC_PORT + 1, 'get_block_count')['count']) + logging.info('Adding anon outputs') + outputs = [] + for i in range(8): + sx_addr = callnoderpc(1, 'getnewstealthaddress') + outputs.append({'address': sx_addr, 'amount': 0.5}) + for i in range(6): + callnoderpc(0, 'sendtypeto', ['part', 'anon', outputs]) + logging.info('Starting update thread.') signal.signal(signal.SIGINT, signal_handler) cls.update_thread = threading.Thread(target=run_loop, args=(cls,)) @@ -805,9 +813,12 @@ class Test(unittest.TestCase): js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read()) assert(float(js_0['anon_balance']) == 0.0) + node0_anon_before = js_0['anon_balance'] + js_0['anon_pending'] + wait_for_balance(test_delay_event, 'http://127.0.0.1:1801/json/wallets/part', 'balance', 200.0) js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/part').read()) assert(float(js_1['balance']) > 200.0) + node1_anon_before = js_1['anon_balance'] + js_1['anon_pending'] callnoderpc(1, 'reservebalance', [True, 1000000]) # Stop staking to avoid conflicts (input used by tx->anon staked before tx gets in the chain) post_json = { @@ -819,17 +830,10 @@ class Test(unittest.TestCase): json_rv = json.loads(post_json_req('http://127.0.0.1:1801/json/wallets/part/withdraw', post_json)) assert(len(json_rv['txid']) == 64) - post_json['value'] = 0.5 - for i in range(22): - json_rv = json.loads(post_json_req('http://127.0.0.1:1801/json/wallets/part/withdraw', post_json)) - assert(len(json_rv['txid']) == 64) - logging.info('Waiting for anon balance') - try: - wait_for_balance(test_delay_event, 'http://127.0.0.1:1801/json/wallets/part', 'anon_balance', 110.0) - except Exception as e: - ft = callnoderpc(0, 'filtertransactions', [{'count': 0}]) - raise e + wait_for_balance(test_delay_event, 'http://127.0.0.1:1801/json/wallets/part', 'anon_balance', 100.0 + node1_anon_before) + js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/part').read()) + node1_anon_before = js_1['anon_balance'] + js_1['anon_pending'] callnoderpc(1, 'reservebalance', [False]) post_json = { @@ -847,8 +851,6 @@ class Test(unittest.TestCase): if float(js_0['blind_balance']) >= 10.0: raise ValueError('Expect blind balance < 10') - return # TODO - amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) rate_swap = make_int(random.uniform(2.0, 20.0), scale=8, r=1) offer_id = swap_clients[0].postOffer(Coins.BTC, Coins.PART_ANON, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP) @@ -862,6 +864,7 @@ class Test(unittest.TestCase): bid, xmr_swap = swap_clients[0].getXmrBid(bid_id) assert(xmr_swap) + amount_to = float(format_amount(bid.amount_to, 8)) swap_clients[0].acceptXmrBid(bid_id) @@ -869,7 +872,13 @@ class Test(unittest.TestCase): wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True) js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/part').read()) - print('[rm] js_1', js_1) + assert(js_1['anon_balance'] < node1_anon_before - amount_to) + + js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read()) + assert(js_0['anon_balance'] + js_0['anon_pending'] > node0_anon_before + (amount_to - 0.1)) + + def test_12_particl_blind(self): + return # TODO def test_98_withdraw_all(self): logging.info('---------- Test XMR withdrawal all')