particl: Can swap anon outputs

2024-05-20_merge
tecnovert 3 years ago
parent 6dd6063182
commit 1ef71ea79b
No known key found for this signature in database
GPG Key ID: 8ED6D8750C4E3F93
  1. 2
      basicswap/__init__.py
  2. 5
      basicswap/base.py
  3. 19
      basicswap/basicswap.py
  4. 4
      basicswap/basicswap_util.py
  5. 6
      basicswap/interface_btc.py
  6. 91
      basicswap/interface_part.py
  7. 10
      basicswap/interface_xmr.py
  8. 8
      basicswap/util.py
  9. 1
      tests/basicswap/extended/test_network.py
  10. 37
      tests/basicswap/test_xmr.py

@ -1,3 +1,3 @@
name = "basicswap"
__version__ = "0.0.23"
__version__ = "0.0.24"

@ -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

@ -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:
if coin_to == Coins.XMR:
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)
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:

@ -371,3 +371,7 @@ def isActiveBidState(state):
if state == BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED:
return True
return False
class TemporaryError(ValueError):
pass

@ -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):

@ -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))
# 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']}
def spendBLockTx(self, address_to, kbv, kbs, cb_swap_value, b_fee, restore_height):
raise ValueError('TODO - new core release')
return None

@ -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:

@ -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:

@ -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

@ -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')

Loading…
Cancel
Save