diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 2f5952e..d1a32f9 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -954,15 +954,13 @@ class BasicSwap(BaseApp): session.add(kv) def setIntKV(self, str_key: str, int_val: int) -> None: - self.mxDB.acquire() + session = self.openSession() try: session = scoped_session(self.session_factory) self.setIntKVInSession(str_key, int_val, session) session.commit() finally: - session.close() - session.remove() - self.mxDB.release() + self.closeSession(session, commit=False) def setStringKV(self, str_key: str, str_val: str, session=None) -> None: try: @@ -2468,7 +2466,6 @@ class BasicSwap(BaseApp): reverse_bid: bool = coin_from in self.scriptless_coins if reverse_bid: reversed_rate: int = ci_to.make_int(amount / amount_to, r=1) - amount_from: int = int((int(amount_to) * reversed_rate) // ci_to.COIN()) ensure(abs(amount_from - amount) < 20, 'invalid bid amount') # TODO: Tolerance? @@ -2476,8 +2473,9 @@ class BasicSwap(BaseApp): msg_buf.protocol_version = PROTOCOL_VERSION_ADAPTOR_SIG msg_buf.offer_msg_id = offer_id msg_buf.time_valid = valid_for_seconds - msg_buf.amount = int(amount_to) - msg_buf.rate = reversed_rate + msg_buf.amount_from = amount + msg_buf.amount_to = amount_to + msg_buf.rate = bid_rate bid_bytes = msg_buf.SerializeToString() payload_hex = str.format('{:02x}', MessageTypes.ADS_BID_LF) + bid_bytes.hex() @@ -2494,11 +2492,11 @@ class BasicSwap(BaseApp): active_ind=1, bid_id=xmr_swap.bid_id, offer_id=offer_id, - amount=msg_buf.amount, - rate=msg_buf.rate, + amount=msg_buf.amount_to, + rate=reversed_rate, created_at=bid_created_at, contract_count=xmr_swap.contract_count, - amount_to=(msg_buf.amount * msg_buf.rate) // ci_to.COIN(), + amount_to=msg_buf.amount_from, expire_at=bid_created_at + msg_buf.time_valid, bid_addr=bid_addr, was_sent=True, @@ -2507,13 +2505,12 @@ class BasicSwap(BaseApp): bid.setState(BidStates.BID_REQUEST_SENT) + session = self.openSession() try: - session = scoped_session(self.session_factory) self.saveBidInSession(xmr_swap.bid_id, bid, session, xmr_swap) session.commit() finally: - session.close() - session.remove() + self.closeSession(session, commit=False) self.log.info('Sent ADS_BID_LF %s', xmr_swap.bid_id.hex()) return xmr_swap.bid_id @@ -3922,6 +3919,10 @@ class BasicSwap(BaseApp): ensure(bid, 'Bid not found: {}.'.format(bid_id.hex())) ensure(xmr_swap, 'Adaptor-sig swap not found: {}.'.format(bid_id.hex())) + if BidStates(bid.state) == BidStates.BID_STALLED_FOR_TEST: + self.log.debug('Bid stalled %s', bid_id.hex()) + return + offer, xmr_offer = self.getXmrOfferFromSession(session, bid.offer_id, sent=False) ensure(offer, 'Offer not found: {}.'.format(bid.offer_id.hex())) ensure(xmr_offer, 'Adaptor-sig offer not found: {}.'.format(bid.offer_id.hex())) @@ -4466,7 +4467,7 @@ class BasicSwap(BaseApp): raise AutomationConstraint('Bidder has too many failed swaps') return True - def shouldAutoAcceptBid(self, offer, bid, session=None) -> bool: + def shouldAutoAcceptBid(self, offer, bid, session=None, options={}) -> bool: try: use_session = self.openSession(session) @@ -4481,30 +4482,22 @@ class BasicSwap(BaseApp): bid_amount: int = bid.amount bid_rate: int = bid.rate - reverse_bid: bool = coin_from in self.scriptless_coins - if reverse_bid: - bid_amount, bid_rate = xmr_swap_1.reverseBidAmountAndRate(self, bid, offer) + if options.get('reverse_bid', False): + bid_amount = bid.amount_to + bid_rate = options.get('bid_rate') self.log.debug('Evaluating against strategy {}'.format(strategy.record_id)) if not offer.amount_negotiable: - if reverse_bid: - if abs(bid_amount - offer.amount_from) >= 20: # TODO: Tolerance? - raise AutomationConstraint('Need exact amount match') - else: - if bid_amount != offer.amount_from: - raise AutomationConstraint('Need exact amount match') + if bid_amount != offer.amount_from: + raise AutomationConstraint('Need exact amount match') if bid_amount < offer.min_bid_amount: raise AutomationConstraint('Bid amount below offer minimum') if opts.get('exact_rate_only', False) is True: - if reverse_bid: - if abs(bid_rate - offer.rate) >= 20: # TODO: Tolerance? - raise AutomationConstraint('Need exact rate match') - else: - if bid_rate != offer.rate: - raise AutomationConstraint('Need exact rate match') + if bid_rate != offer.rate: + raise AutomationConstraint('Need exact rate match') active_bids, total_bids_value = self.getCompletedAndActiveBidsValue(offer, use_session) @@ -4772,7 +4765,7 @@ class BasicSwap(BaseApp): bid.setState(BidStates.BID_RECEIVED) - if self.shouldAutoAcceptBid(offer, bid, session) or reverse_bid: + if reverse_bid or self.shouldAutoAcceptBid(offer, bid, session): delay = random.randrange(self.min_delay_event, self.max_delay_event) self.log.info('Auto accepting %sadaptor-sig bid %s in %d seconds', 'reverse ' if reverse_bid else '', bid.bid_id.hex(), delay) self.createActionInSession(delay, ActionTypes.ACCEPT_XMR_BID, bid.bid_id, session) @@ -5135,13 +5128,14 @@ class BasicSwap(BaseApp): xmr_swap.a_lock_spend_tx_id = ci_from.getTxid(xmr_swap.a_lock_spend_tx) prevout_amount = ci_from.getLockTxSwapOutputValue(bid, xmr_swap) xmr_swap.al_lock_spend_tx_esig = ci_from.signTxOtVES(kal, xmr_swap.pkasf, xmr_swap.a_lock_spend_tx, 0, xmr_swap.a_lock_tx_script, prevout_amount) - + ''' # Double check a_lock_spend_tx is valid + # Fails for part_blind ci_from.verifySCLockSpendTx( xmr_swap.a_lock_spend_tx, xmr_swap.a_lock_tx, xmr_swap.a_lock_tx_script, xmr_swap.dest_af, a_fee_rate, xmr_swap.vkbv) - + ''' delay = random.randrange(self.min_delay_event_short, self.max_delay_event_short) self.log.info('Sending lock spend tx message for bid %s in %d seconds', bid_id.hex(), delay) self.createActionInSession(delay, ActionTypes.SEND_XMR_SWAP_LOCK_SPEND_MSG, bid_id, session) @@ -5438,7 +5432,7 @@ class BasicSwap(BaseApp): try: if offer.coin_to == Coins.XMR: address_to = self.getCachedMainWalletAddress(ci_to) - elif coin_to == Coins.PART_BLIND: + elif coin_to in (Coins.PART_BLIND, Coins.PART_ANON): address_to = self.getCachedStealthAddressForCoin(coin_to) else: address_to = self.getReceiveAddressFromPool(coin_to, bid_id, TxTypes.XMR_SWAP_B_LOCK_REFUND) @@ -5739,22 +5733,14 @@ class BasicSwap(BaseApp): self.validateBidValidTime(offer.swap_type, offer.coin_from, offer.coin_to, bid_data.time_valid) ensure(now <= msg['sent'] + bid_data.time_valid, 'Bid expired') - # Reverse the rate and amount to test against the offer - amount_from: int = bid_data.amount - amount_to: int = int((int(amount_from) * bid_data.rate) // ci_from.COIN()) - reversed_rate: int = ci_to.make_int(amount_from / amount_to, r=1) - - # Snap the amount to the offer amount if it's close enough - amount_to_check = amount_to - if abs(amount_to_check - offer.min_bid_amount) < 10: - amount_to_check = offer.min_bid_amount + amount_from: int = bid_data.amount_from + amount_to: int = (bid_data.amount_from * bid_data.rate) // ci_to.COIN() + ensure(abs(amount_to - bid_data.amount_to) < 10, 'invalid bid amount_to') # TODO: Tolerance? + reversed_rate: int = ci_from.make_int(amount_from / bid_data.amount_to, r=1) + amount_from_recovered: int = int((amount_to * reversed_rate) // ci_from.COIN()) + ensure(abs(amount_from - amount_from_recovered) < 10, 'invalid bid amount_from') # TODO: Tolerance? - reversed_rate_check = reversed_rate - # Snap the rate to the rate if it's close enough - if abs(reversed_rate_check - offer.rate) < 10: - reversed_rate_check = offer.rate - - self.validateBidAmount(offer, amount_to_check, reversed_rate_check) + self.validateBidAmount(offer, amount_from, bid_data.rate) bid_id = bytes.fromhex(msg['msgid']) @@ -5765,10 +5751,10 @@ class BasicSwap(BaseApp): bid_id=bid_id, offer_id=offer_id, protocol_version=bid_data.protocol_version, - amount=bid_data.amount, - rate=bid_data.rate, + amount=amount_to, + rate=reversed_rate, created_at=msg['sent'], - amount_to=(bid_data.amount * bid_data.rate) // ci_from.COIN(), + amount_to=amount_from, expire_at=msg['sent'] + bid_data.time_valid, bid_addr=msg['from'], was_sent=False, @@ -5799,7 +5785,8 @@ class BasicSwap(BaseApp): session = self.openSession() self.notify(NT.BID_RECEIVED, {'type': 'as_reversed', 'bid_id': bid.bid_id.hex(), 'offer_id': bid.offer_id.hex()}, session) - if self.shouldAutoAcceptBid(offer, bid, session): + options = {'reverse_bid': True, 'bid_rate': bid_data.rate} + if self.shouldAutoAcceptBid(offer, bid, session, options=options): delay = random.randrange(self.min_delay_event, self.max_delay_event) self.log.info('Auto accepting reverse adaptor-sig bid %s in %d seconds', bid.bid_id.hex(), delay) self.createActionInSession(delay, ActionTypes.ACCEPT_AS_REV_BID, bid.bid_id, session) @@ -6336,10 +6323,9 @@ class BasicSwap(BaseApp): def addWalletInfoRecord(self, coin, info_type, wi) -> None: coin_id = int(coin) - self.mxDB.acquire() + session = self.openSession() try: now: int = self.getTime() - session = scoped_session(self.session_factory) session.add(Wallets(coin_id=coin, balance_type=info_type, wallet_data=json.dumps(wi), created_at=now)) query_str = f'DELETE FROM wallets WHERE (coin_id = {coin_id} AND balance_type = {info_type}) AND record_id NOT IN (SELECT record_id FROM wallets WHERE coin_id = {coin_id} AND balance_type = {info_type} ORDER BY created_at DESC LIMIT 3 )' session.execute(query_str) @@ -6347,9 +6333,7 @@ class BasicSwap(BaseApp): except Exception as e: self.log.error(f'addWalletInfoRecord {e}') finally: - session.close() - session.remove() - self.mxDB.release() + self.closeSession(session, commit=False) def updateWalletInfo(self, coin) -> None: # Store wallet info to db so it's available after startup @@ -6452,25 +6436,21 @@ class BasicSwap(BaseApp): return rv def countAcceptedBids(self, offer_id: bytes = None) -> int: - self.mxDB.acquire() + session = self.openSession() try: - session = scoped_session(self.session_factory) if offer_id: q = session.execute('SELECT COUNT(*) FROM bids WHERE state >= {} AND offer_id = x\'{}\''.format(BidStates.BID_ACCEPTED, offer_id.hex())).first() else: q = session.execute('SELECT COUNT(*) FROM bids WHERE state >= {}'.format(BidStates.BID_ACCEPTED)).first() return q[0] finally: - session.close() - session.remove() - self.mxDB.release() + self.closeSession(session, commit=False) def listOffers(self, sent: bool = False, filters={}, with_bid_info: bool = False): - self.mxDB.acquire() + session = self.openSession() try: rv = [] now: int = self.getTime() - session = scoped_session(self.session_factory) if with_bid_info: subquery = session.query(sa.func.sum(Bid.amount).label('completed_bid_amount')).filter(sa.and_(Bid.offer_id == Offer.offer_id, Bid.state == BidStates.SWAP_COMPLETED)).correlate(Offer).scalar_subquery() @@ -6533,9 +6513,7 @@ class BasicSwap(BaseApp): rv.append(offer) return rv finally: - session.close() - session.remove() - self.mxDB.release() + self.closeSession(session, commit=False) def activeBidsQueryStr(self, now: int, offer_table: str = 'offers', bids_table: str = 'bids') -> str: offers_inset = f' AND {offer_table}.expire_at > {now}' if offer_table != '' else '' @@ -6544,11 +6522,10 @@ class BasicSwap(BaseApp): return f' ({bids_table}.state NOT IN ({inactive_states_str}) AND ({bids_table}.state > {BidStates.BID_RECEIVED} OR ({bids_table}.expire_at > {now}{offers_inset}))) ' def listBids(self, sent: bool = False, offer_id: bytes = None, for_html: bool = False, filters={}): - self.mxDB.acquire() + session = self.openSession() try: rv = [] now: int = self.getTime() - session = scoped_session(self.session_factory) query_str = 'SELECT bids.created_at, bids.expire_at, bids.bid_id, bids.offer_id, bids.amount, bids.state, bids.was_received, tx1.state, tx2.state, offers.coin_from, bids.rate, bids.bid_addr FROM bids ' + \ 'LEFT JOIN offers ON offers.offer_id = bids.offer_id ' + \ @@ -6594,9 +6571,7 @@ class BasicSwap(BaseApp): rv.append(row) return rv finally: - session.close() - session.remove() - self.mxDB.release() + self.closeSession(session, commit=False) def listSwapsInProgress(self, for_html=False): self.mxDB.acquire() @@ -6795,9 +6770,8 @@ class BasicSwap(BaseApp): self.closeSession(use_session) def addSMSGAddress(self, pubkey_hex: str, addressnote: str = None) -> None: - self.mxDB.acquire() + session = self.openSession() try: - session = scoped_session(self.session_factory) now: int = self.getTime() ci = self.ci(Coins.PART) add_addr = ci.pubkey_to_address(bytes.fromhex(pubkey_hex)) @@ -6805,26 +6779,20 @@ class BasicSwap(BaseApp): self.callrpc('smsglocalkeys', ['anon', '-', add_addr]) session.add(SmsgAddress(addr=add_addr, use_type=AddressTypes.SEND_OFFER, active_ind=1, created_at=now, note=addressnote, pubkey=pubkey_hex)) - session.commit() return add_addr finally: - session.close() - session.remove() - self.mxDB.release() + self.closeSession(session) def editSMSGAddress(self, address: str, active_ind: int, addressnote: str) -> None: - self.mxDB.acquire() + session = self.openSession() try: - session = scoped_session(self.session_factory) mode = '-' if active_ind == 0 else '+' self.callrpc('smsglocalkeys', ['recv', mode, address]) session.execute('UPDATE smsgaddresses SET active_ind = :active_ind, note = :note WHERE addr = :addr', {'active_ind': active_ind, 'note': addressnote, 'addr': address}) session.commit() finally: - session.close() - session.remove() - self.mxDB.release() + self.closeSession(session) def createCoinALockRefundSwipeTx(self, ci, bid, offer, xmr_swap, xmr_offer): self.log.debug('Creating %s lock refund swipe tx', ci.coin_name()) @@ -6887,18 +6855,15 @@ class BasicSwap(BaseApp): self.swaps_in_progress[bid.bid_id] = (bid, swap_in_progress[1]) def getAddressLabel(self, addresses): - self.mxDB.acquire() + session = self.openSession() try: - session = scoped_session(self.session_factory) rv = [] for a in addresses: v = session.query(KnownIdentity).filter_by(address=a).first() rv.append('' if (not v or not v.label) else v.label) return rv finally: - session.close() - session.remove() - self.mxDB.release() + self.closeSession(session, commit=False) def add_connection(self, host, port, peer_pubkey): self.log.info('add_connection %s %d %s', host, port, peer_pubkey.hex()) diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index adfdfb9..2fd0141 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -367,6 +367,10 @@ class XMRInterface(CoinInterface): return None def spendBLockTx(self, chain_b_lock_txid, address_to, kbv, kbs, cb_swap_value, b_fee_rate, restore_height, spend_actual_balance=False): + ''' + Notes: + "Error: No unlocked balance in the specified subaddress(es)" can mean not enough funds after tx fee. + ''' with self._mx_wallet: Kbv = self.getPubkey(kbv) Kbs = self.getPubkey(kbs) @@ -390,7 +394,6 @@ class XMRInterface(CoinInterface): self.rpc_wallet_cb('refresh') rv = self.rpc_wallet_cb('get_balance') - if rv['balance'] < cb_swap_value: self._log.warning('Balance is too low, checking for existing spend.') txns = self.rpc_wallet_cb('get_transfers', {'out': True}) diff --git a/basicswap/messages.proto b/basicswap/messages.proto index ab3ab0b..73be986 100644 --- a/basicswap/messages.proto +++ b/basicswap/messages.proto @@ -40,8 +40,6 @@ message BidMessage { bytes offer_msg_id = 1; uint64 time_valid = 2; /* seconds bid is valid for */ uint64 amount = 3; /* amount of amount_from bid is for */ - - /* optional */ uint64 rate = 4; bytes pkhash_buyer = 5; /* buyer's address to receive amount_from */ string proof_address = 6; @@ -136,10 +134,11 @@ message ADSBidIntentMessage { /* L -> F Sent from bidder, construct a reverse bid */ bytes offer_msg_id = 1; uint64 time_valid = 2; /* seconds bid is valid for */ - uint64 amount = 3; /* amount of amount_from bid is for */ - uint64 rate = 4; + uint64 amount_from = 3; /* amount of offer.coin_from bid is for */ + uint64 amount_to = 4; /* amount of offer.coin_to bid is for, equivalent to bid.amount */ + uint64 rate = 5; /* amount of offer.coin_from bid is for */ - uint32 protocol_version = 5; + uint32 protocol_version = 6; } message ADSBidIntentAcceptMessage { diff --git a/basicswap/messages_pb2.py b/basicswap/messages_pb2.py index b42ef59..7399d9e 100644 --- a/basicswap/messages_pb2.py +++ b/basicswap/messages_pb2.py @@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0emessages.proto\x12\tbasicswap\"\xa6\x04\n\x0cOfferMessage\x12\x11\n\tcoin_from\x18\x01 \x01(\r\x12\x0f\n\x07\x63oin_to\x18\x02 \x01(\r\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x16\n\x0emin_bid_amount\x18\x05 \x01(\x04\x12\x12\n\ntime_valid\x18\x06 \x01(\x04\x12\x33\n\tlock_type\x18\x07 \x01(\x0e\x32 .basicswap.OfferMessage.LockType\x12\x12\n\nlock_value\x18\x08 \x01(\r\x12\x11\n\tswap_type\x18\t \x01(\r\x12\x15\n\rproof_address\x18\n \x01(\t\x12\x17\n\x0fproof_signature\x18\x0b \x01(\t\x12\x15\n\rpkhash_seller\x18\x0c \x01(\x0c\x12\x13\n\x0bsecret_hash\x18\r \x01(\x0c\x12\x15\n\rfee_rate_from\x18\x0e \x01(\x04\x12\x13\n\x0b\x66\x65\x65_rate_to\x18\x0f \x01(\x04\x12\x18\n\x10protocol_version\x18\x10 \x01(\r\x12\x19\n\x11\x61mount_negotiable\x18\x11 \x01(\x08\x12\x17\n\x0frate_negotiable\x18\x12 \x01(\x08\"q\n\x08LockType\x12\x0b\n\x07NOT_SET\x10\x00\x12\x18\n\x14SEQUENCE_LOCK_BLOCKS\x10\x01\x12\x16\n\x12SEQUENCE_LOCK_TIME\x10\x02\x12\x13\n\x0f\x41\x42S_LOCK_BLOCKS\x10\x03\x12\x11\n\rABS_LOCK_TIME\x10\x04\"\xb4\x01\n\nBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x05 \x01(\x0c\x12\x15\n\rproof_address\x18\x06 \x01(\t\x12\x17\n\x0fproof_signature\x18\x07 \x01(\t\x12\x18\n\x10protocol_version\x18\x08 \x01(\r\"V\n\x10\x42idAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x15\n\rinitiate_txid\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63ontract_script\x18\x03 \x01(\x0c\"=\n\x12OfferRevokeMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\";\n\x10\x42idRejectMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x13\n\x0breject_code\x18\x02 \x01(\r\"\xb2\x01\n\rXmrBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x0c\n\x04pkaf\x18\x05 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x06 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x07 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x08 \x01(\x0c\x12\x18\n\x10protocol_version\x18\t \x01(\r\"T\n\x0fXmrSplitMessage\x12\x0e\n\x06msg_id\x18\x01 \x01(\x0c\x12\x10\n\x08msg_type\x18\x02 \x01(\r\x12\x10\n\x08sequence\x18\x03 \x01(\r\x12\r\n\x05\x64leag\x18\x04 \x01(\x0c\"\x80\x02\n\x13XmrBidAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkal\x18\x03 \x01(\x0c\x12\x0c\n\x04kbvl\x18\x04 \x01(\x0c\x12\x12\n\nkbsl_dleag\x18\x05 \x01(\x0c\x12\x11\n\ta_lock_tx\x18\x06 \x01(\x0c\x12\x18\n\x10\x61_lock_tx_script\x18\x07 \x01(\x0c\x12\x18\n\x10\x61_lock_refund_tx\x18\x08 \x01(\x0c\x12\x1f\n\x17\x61_lock_refund_tx_script\x18\t \x01(\x0c\x12\x1e\n\x16\x61_lock_refund_spend_tx\x18\n \x01(\x0c\x12\x1d\n\x15\x61l_lock_refund_tx_sig\x18\x0b \x01(\x0c\"r\n\x17XmrBidLockTxSigsMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12$\n\x1c\x61\x66_lock_refund_spend_tx_esig\x18\x02 \x01(\x0c\x12\x1d\n\x15\x61\x66_lock_refund_tx_sig\x18\x03 \x01(\x0c\"X\n\x18XmrBidLockSpendTxMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x17\n\x0f\x61_lock_spend_tx\x18\x02 \x01(\x0c\x12\x0f\n\x07kal_sig\x18\x03 \x01(\x0c\"M\n\x18XmrBidLockReleaseMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x1d\n\x15\x61l_lock_spend_tx_esig\x18\x02 \x01(\x0c\"w\n\x13\x41\x44SBidIntentMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x18\n\x10protocol_version\x18\x05 \x01(\r\"p\n\x19\x41\x44SBidIntentAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkaf\x18\x02 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x03 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x04 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x05 \x01(\x0c\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0emessages.proto\x12\tbasicswap\"\xa6\x04\n\x0cOfferMessage\x12\x11\n\tcoin_from\x18\x01 \x01(\r\x12\x0f\n\x07\x63oin_to\x18\x02 \x01(\r\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x16\n\x0emin_bid_amount\x18\x05 \x01(\x04\x12\x12\n\ntime_valid\x18\x06 \x01(\x04\x12\x33\n\tlock_type\x18\x07 \x01(\x0e\x32 .basicswap.OfferMessage.LockType\x12\x12\n\nlock_value\x18\x08 \x01(\r\x12\x11\n\tswap_type\x18\t \x01(\r\x12\x15\n\rproof_address\x18\n \x01(\t\x12\x17\n\x0fproof_signature\x18\x0b \x01(\t\x12\x15\n\rpkhash_seller\x18\x0c \x01(\x0c\x12\x13\n\x0bsecret_hash\x18\r \x01(\x0c\x12\x15\n\rfee_rate_from\x18\x0e \x01(\x04\x12\x13\n\x0b\x66\x65\x65_rate_to\x18\x0f \x01(\x04\x12\x18\n\x10protocol_version\x18\x10 \x01(\r\x12\x19\n\x11\x61mount_negotiable\x18\x11 \x01(\x08\x12\x17\n\x0frate_negotiable\x18\x12 \x01(\x08\"q\n\x08LockType\x12\x0b\n\x07NOT_SET\x10\x00\x12\x18\n\x14SEQUENCE_LOCK_BLOCKS\x10\x01\x12\x16\n\x12SEQUENCE_LOCK_TIME\x10\x02\x12\x13\n\x0f\x41\x42S_LOCK_BLOCKS\x10\x03\x12\x11\n\rABS_LOCK_TIME\x10\x04\"\xb4\x01\n\nBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x05 \x01(\x0c\x12\x15\n\rproof_address\x18\x06 \x01(\t\x12\x17\n\x0fproof_signature\x18\x07 \x01(\t\x12\x18\n\x10protocol_version\x18\x08 \x01(\r\"V\n\x10\x42idAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x15\n\rinitiate_txid\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63ontract_script\x18\x03 \x01(\x0c\"=\n\x12OfferRevokeMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\";\n\x10\x42idRejectMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x13\n\x0breject_code\x18\x02 \x01(\r\"\xb2\x01\n\rXmrBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x0c\n\x04pkaf\x18\x05 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x06 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x07 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x08 \x01(\x0c\x12\x18\n\x10protocol_version\x18\t \x01(\r\"T\n\x0fXmrSplitMessage\x12\x0e\n\x06msg_id\x18\x01 \x01(\x0c\x12\x10\n\x08msg_type\x18\x02 \x01(\r\x12\x10\n\x08sequence\x18\x03 \x01(\r\x12\r\n\x05\x64leag\x18\x04 \x01(\x0c\"\x80\x02\n\x13XmrBidAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkal\x18\x03 \x01(\x0c\x12\x0c\n\x04kbvl\x18\x04 \x01(\x0c\x12\x12\n\nkbsl_dleag\x18\x05 \x01(\x0c\x12\x11\n\ta_lock_tx\x18\x06 \x01(\x0c\x12\x18\n\x10\x61_lock_tx_script\x18\x07 \x01(\x0c\x12\x18\n\x10\x61_lock_refund_tx\x18\x08 \x01(\x0c\x12\x1f\n\x17\x61_lock_refund_tx_script\x18\t \x01(\x0c\x12\x1e\n\x16\x61_lock_refund_spend_tx\x18\n \x01(\x0c\x12\x1d\n\x15\x61l_lock_refund_tx_sig\x18\x0b \x01(\x0c\"r\n\x17XmrBidLockTxSigsMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12$\n\x1c\x61\x66_lock_refund_spend_tx_esig\x18\x02 \x01(\x0c\x12\x1d\n\x15\x61\x66_lock_refund_tx_sig\x18\x03 \x01(\x0c\"X\n\x18XmrBidLockSpendTxMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x17\n\x0f\x61_lock_spend_tx\x18\x02 \x01(\x0c\x12\x0f\n\x07kal_sig\x18\x03 \x01(\x0c\"M\n\x18XmrBidLockReleaseMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x1d\n\x15\x61l_lock_spend_tx_esig\x18\x02 \x01(\x0c\"\x8f\x01\n\x13\x41\x44SBidIntentMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x11\n\tamount_to\x18\x04 \x01(\x04\x12\x0c\n\x04rate\x18\x05 \x01(\x04\x12\x18\n\x10protocol_version\x18\x06 \x01(\r\"p\n\x19\x41\x44SBidIntentAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkaf\x18\x02 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x03 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x04 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x05 \x01(\x0c\x62\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'messages_pb2', globals()) @@ -44,8 +44,8 @@ if _descriptor._USE_C_DESCRIPTORS == False: _XMRBIDLOCKSPENDTXMESSAGE._serialized_end=1707 _XMRBIDLOCKRELEASEMESSAGE._serialized_start=1709 _XMRBIDLOCKRELEASEMESSAGE._serialized_end=1786 - _ADSBIDINTENTMESSAGE._serialized_start=1788 - _ADSBIDINTENTMESSAGE._serialized_end=1907 - _ADSBIDINTENTACCEPTMESSAGE._serialized_start=1909 - _ADSBIDINTENTACCEPTMESSAGE._serialized_end=2021 + _ADSBIDINTENTMESSAGE._serialized_start=1789 + _ADSBIDINTENTMESSAGE._serialized_end=1932 + _ADSBIDINTENTACCEPTMESSAGE._serialized_start=1934 + _ADSBIDINTENTACCEPTMESSAGE._serialized_end=2046 # @@protoc_insertion_point(module_scope) diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py index b960615..0407bcb 100644 --- a/tests/basicswap/test_btc_xmr.py +++ b/tests/basicswap/test_btc_xmr.py @@ -22,6 +22,7 @@ from basicswap.util import ( make_int, format_amount, ) +from basicswap.interface import Curves from tests.basicswap.util import ( read_json_api, ) @@ -51,11 +52,24 @@ from .test_xmr import BaseTest, test_delay_event, callnoderpc logger = logging.getLogger() -class BasicSwapTest(BaseTest): +class TestFunctions(BaseTest): base_rpc_port = None - def getBalance(self, js_wallets, coin): - return float(js_wallets[coin.name]['balance']) + float(js_wallets[coin.name]['unconfirmed']) + def getBalance(self, js_wallets, coin) -> float: + if coin == Coins.PART_BLIND: + coin_ticker: str = 'PART' + balance_type: str = 'blind_balance' + unconfirmed_name: str = 'blind_unconfirmed' + elif coin == Coins.PART_ANON: + coin_ticker: str = 'PART' + balance_type: str = 'anon_balance' + unconfirmed_name: str = 'anon_pending' + else: + coin_ticker: str = coin.name + balance_type: str = 'balance' + unconfirmed_name: str = 'unconfirmed' + + return float(js_wallets[coin_ticker][balance_type]) + float(js_wallets[coin_ticker][unconfirmed_name]) def callnoderpc(self, method, params=[], wallet=None, node_id=0): return callnoderpc(node_id, method, params, wallet, self.base_rpc_port) @@ -63,17 +77,276 @@ class BasicSwapTest(BaseTest): def mineBlock(self, num_blocks=1): self.callnoderpc('generatetoaddress', [num_blocks, self.btc_addr]) - def prepare_balance(self, coin_ticker: str, amount: float, port_target_node: int, port_take_from_node: int) -> None: + def prepare_balance(self, coin, amount: float, port_target_node: int, port_take_from_node: int) -> None: + if coin == Coins.PART_BLIND: + coin_ticker: str = 'PART' + balance_type: str = 'blind_balance' + address_type: str = 'stealth_address' + type_to: str = 'blind' + elif coin == Coins.PART_ANON: + coin_ticker: str = 'PART' + balance_type: str = 'anon_balance' + address_type: str = 'stealth_address' + type_to: str = 'anon' + else: + coin_ticker: str = coin.name + balance_type: str = 'balance' + address_type: str = 'deposit_address' js_w = read_json_api(port_target_node, 'wallets') - if float(js_w[coin_ticker]['balance']) < amount: - post_json = { - 'value': amount, - 'address': js_w[coin_ticker]['deposit_address'], - 'subfee': False, - } - json_rv = read_json_api(port_take_from_node, 'wallets/{}/withdraw'.format(coin_ticker.lower()), post_json) - assert (len(json_rv['txid']) == 64) - wait_for_balance(test_delay_event, 'http://127.0.0.1:{}/json/wallets/{}'.format(port_target_node, coin_ticker.lower()), 'balance', amount) + if float(js_w[coin_ticker][balance_type]) >= amount: + return + post_json = { + 'value': amount, + 'address': js_w[coin_ticker][address_type], + 'subfee': False, + } + if coin in (Coins.PART_BLIND, Coins.PART_ANON): + post_json['type_to'] = type_to + json_rv = read_json_api(port_take_from_node, 'wallets/{}/withdraw'.format(coin_ticker.lower()), post_json) + assert (len(json_rv['txid']) == 64) + wait_for_balance(test_delay_event, 'http://127.0.0.1:{}/json/wallets/{}'.format(port_target_node, coin_ticker.lower()), balance_type, amount) + + def do_test_01_full_swap(self, coin_from: Coins, coin_to: Coins) -> None: + logging.info('---------- Test {} to {}'.format(coin_from.name, coin_to.name)) + + swap_clients = self.swap_clients + reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins + ci_from = swap_clients[0].ci(coin_from) + ci_to = swap_clients[1].ci(coin_to) + ci_part0 = swap_clients[0].ci(Coins.PART) + ci_part1 = swap_clients[1].ci(Coins.PART) + + # Offerer sends the offer + # Bidder sends the bid + id_offerer: int = 0 + id_bidder: int = 1 + + # Leader sends the initial (chain a) lock tx. + # Follower sends the participate (chain b) lock tx. + id_leader: int = 1 if reverse_bid else 0 + id_follower: int = 0 if reverse_bid else 1 + logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}') + + js_0 = read_json_api(1800, 'wallets') + node0_from_before: float = self.getBalance(js_0, coin_from) + + js_1 = read_json_api(1801, 'wallets') + node1_from_before: float = self.getBalance(js_1, coin_from) + + node0_sent_messages_before: int = ci_part0.rpc_callback('smsgoutbox', ['count',])['num_messages'] + node1_sent_messages_before: int = ci_part1.rpc_callback('smsgoutbox', ['count',])['num_messages'] + + 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[id_offerer].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP) + wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id) + offer = swap_clients[id_bidder].listOffers(filters={'offer_id': offer_id})[0] + assert (offer.offer_id == offer_id) + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + + wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED) + swap_clients[id_offerer].acceptBid(bid_id) + + wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[id_bidder], bid_id, BidStates.SWAP_COMPLETED, sent=True) + + amount_from = float(ci_from.format_amount(amt_swap)) + js_1_after = read_json_api(1801, 'wallets') + node1_from_after = self.getBalance(js_1_after, coin_from) + if coin_from is not Coins.PART: # TODO: staking + assert (node1_from_after > node1_from_before + (amount_from - 0.05)) + + js_0_after = read_json_api(1800, 'wallets') + node0_from_after: float = self.getBalance(js_0_after, coin_from) + # TODO: Discard block rewards + # assert (node0_from_after < node0_from_before - amount_from) + + scale_from = 8 + amount_to = int((amt_swap * rate_swap) // (10 ** scale_from)) + amount_to_float = float(ci_to.format_amount(amount_to)) + node1_to_after: float = self.getBalance(js_1_after, coin_to) + node1_to_before: float = self.getBalance(js_1, coin_to) + if False: # TODO: set stakeaddress and xmr rewards to non wallet addresses + assert (node1_to_after < node1_to_before - amount_to_float) + + node0_sent_messages_after: int = ci_part0.rpc_callback('smsgoutbox', ['count',])['num_messages'] + node1_sent_messages_after: int = ci_part1.rpc_callback('smsgoutbox', ['count',])['num_messages'] + node0_sent_messages: int = node0_sent_messages_after - node0_sent_messages_before + node1_sent_messages: int = node1_sent_messages_after - node1_sent_messages_before + split_msgs: int = 2 if (ci_from.curve_type() != Curves.secp256k1 or ci_to.curve_type() != Curves.secp256k1) else 0 + assert (node0_sent_messages == (3 + split_msgs if reverse_bid else 4 + split_msgs)) + assert (node1_sent_messages == (4 + split_msgs if reverse_bid else 2 + split_msgs)) + + def do_test_02_leader_recover_a_lock_tx(self, coin_from: Coins, coin_to: Coins) -> None: + logging.info('---------- Test {} to {} leader recovers coin a lock tx'.format(coin_from.name, coin_to.name)) + + swap_clients = self.swap_clients + reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins + ci_from = swap_clients[0].ci(coin_from) + ci_to = swap_clients[0].ci(coin_to) + + id_offerer: int = 0 + id_bidder: int = 1 + id_leader: int = 1 if reverse_bid else 0 + id_follower: int = 0 if reverse_bid else 1 + logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}') + + js_wl_before = read_json_api(1800 + id_leader, 'wallets') + wl_from_before = self.getBalance(js_wl_before, coin_from) + + 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[id_offerer].postOffer( + coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, + lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32) + wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id) + offer = swap_clients[id_bidder].getOffer(offer_id) + + bid_id = swap_clients[id_bidder].postXmrBid(offer_id, offer.amount_from) + wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED) + + swap_clients[id_follower].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK) + swap_clients[id_offerer].acceptBid(bid_id) + + leader_sent_bid: bool = True if reverse_bid else False + wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=leader_sent_bid, wait_for=180) + wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, [BidStates.BID_STALLED_FOR_TEST, BidStates.XMR_SWAP_FAILED], sent=(not leader_sent_bid)) + + js_wl_after = read_json_api(1800 + id_leader, 'wallets') + wl_from_after = self.getBalance(js_wl_after, coin_from) + + # TODO: Discard block rewards + # assert (node0_from_before - node0_from_after < 0.02) + + def do_test_03_follower_recover_a_lock_tx(self, coin_from, coin_to, lock_value=32): + logging.info('---------- Test {} to {} follower recovers coin a lock tx'.format(coin_from.name, coin_to.name)) + + # Leader is too slow to recover the coin a lock tx and follower swipes it + # coin b lock tx remains unspent + + swap_clients = self.swap_clients + reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins + ci_from = swap_clients[0].ci(coin_from) + ci_to = swap_clients[0].ci(coin_to) + + id_offerer: int = 0 + id_bidder: int = 1 + id_leader: int = 1 if reverse_bid else 0 + id_follower: int = 0 if reverse_bid else 1 + logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}') + + js_w0_before = read_json_api(1800, 'wallets') + js_w1_before = read_json_api(1801, 'wallets') + + 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[id_offerer].postOffer( + coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, + lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=lock_value) + wait_for_offer(test_delay_event, swap_clients[1], offer_id) + offer = swap_clients[id_bidder].getOffer(offer_id) + + bid_id = swap_clients[id_bidder].postXmrBid(offer_id, offer.amount_from) + wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED) + + swap_clients[id_follower].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK) + swap_clients[id_leader].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND) + + swap_clients[id_offerer].acceptBid(bid_id) + + leader_sent_bid: bool = True if reverse_bid else False + wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.BID_STALLED_FOR_TEST, wait_for=180, sent=leader_sent_bid) + wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=80, sent=(not leader_sent_bid)) + + js_w1_after = read_json_api(1801, 'wallets') + + node1_from_before = self.getBalance(js_w1_before, coin_from) + node1_from_after = self.getBalance(js_w1_after, coin_from) + amount_from = float(format_amount(amt_swap, 8)) + # TODO: Discard block rewards + # assert (node1_from_after - node1_from_before > (amount_from - 0.02)) + + swap_clients[0].abandonBid(bid_id) + + wait_for_none_active(test_delay_event, 1800) + wait_for_none_active(test_delay_event, 1801) + + 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 + reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins + ci_from = swap_clients[0].ci(coin_from) + ci_to = swap_clients[0].ci(coin_to) + + id_offerer: int = 0 + id_bidder: int = 1 + id_leader: int = 1 if reverse_bid else 0 + id_follower: int = 0 if reverse_bid else 1 + logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}') + + js_w0_before = read_json_api(1800, 'wallets') + js_w1_before = read_json_api(1801, 'wallets') + + 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[id_offerer].postOffer( + coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, + lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32) + wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id) + offer = swap_clients[id_bidder].getOffer(offer_id) + + bid_id = swap_clients[id_bidder].postXmrBid(offer_id, offer.amount_from) + wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED) + + swap_clients[id_follower].setBidDebugInd(bid_id, DebugTypes.CREATE_INVALID_COIN_B_LOCK) + swap_clients[id_offerer].acceptBid(bid_id) + + leader_sent_bid: bool = True if reverse_bid else False + wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=200, sent=leader_sent_bid) + wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=(not leader_sent_bid)) + + js_w0_after = read_json_api(1800, 'wallets') + js_w1_after = read_json_api(1801, 'wallets') + + node0_from_before = self.getBalance(js_w0_before, coin_from) + node0_from_after = self.getBalance(js_w0_after, coin_from) + logging.info('node0 end coin_from balance {}, diff {}'.format(node0_from_after, node0_from_after - node0_from_before)) + node0_to_before = self.getBalance(js_w0_before, coin_to) + node0_to_after = self.getBalance(js_w0_after, coin_to) + logging.info('node0 end coin_to balance {}, diff {}'.format(node0_to_after, node0_to_after - node0_to_before)) + max_fee_from: float = 0.1 if coin_from == Coins.PART_ANON else 0.02 + if coin_from != Coins.PART: # TODO: Discard block rewards + assert (node0_from_before - node0_from_after < max_fee_from) + + node1_from_before = self.getBalance(js_w1_before, coin_from) + node1_from_after = self.getBalance(js_w1_after, coin_from) + logging.info('node1 end coin_from balance {}, diff {}'.format(node1_from_after, node1_from_after - node1_from_before)) + node1_to_before = self.getBalance(js_w1_before, coin_to) + node1_to_after = self.getBalance(js_w1_after, coin_to) + logging.info('node1 end coin_to balance {}, diff {}'.format(node1_to_after, node1_to_after - node1_to_before)) + + max_fee_to: float = 0.1 if coin_to == Coins.PART_ANON else 0.02 + assert (node1_to_before - node1_to_after < max_fee_to) + + 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_from = swap_clients[0].ci(coin_from) + ci_to = swap_clients[0].ci(coin_to) + + 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[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) + + +class BasicSwapTest(TestFunctions): def test_001_nested_segwit(self): logging.info('---------- Test {} p2sh nested segwit'.format(self.test_coin_from.name)) @@ -275,83 +548,6 @@ class BasicSwapTest(BaseTest): rv = read_json_api(1800, 'getcoinseed', {'coin': 'XMR'}) assert (rv['address'] == '47H7UDLzYEsR28BWttxp59SP1UVSxs4VKDJYSfmz7Wd4Fue5VWuoV9x9eejunwzVSmHWN37gBkaAPNf9VD4bTvwQKsBVWyK') - def do_test_01_full_swap(self, coin_from: Coins, coin_to: Coins) -> None: - logging.info('---------- Test {} to {}'.format(coin_from.name, coin_to.name)) - - swap_clients = self.swap_clients - reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins - ci_from = swap_clients[0].ci(coin_from) - ci_to = swap_clients[1].ci(coin_to) - ci_part0 = swap_clients[0].ci(Coins.PART) - ci_part1 = swap_clients[1].ci(Coins.PART) - - # Offerer sends the offer - # Bidder sends the bid - id_offerer: int = 0 - id_bidder: int = 1 - - # Leader sends the initial (chain a) lock tx. - # Follower sends the participate (chain b) lock tx. - id_leader: int = 1 if reverse_bid else 0 - id_follower: int = 0 if reverse_bid else 1 - logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}') - - js_0 = read_json_api(1800, 'wallets') - node0_from_before = self.getBalance(js_0, coin_from) - - js_1 = read_json_api(1801, 'wallets') - node1_from_before = self.getBalance(js_1, coin_from) - - js_0_to = read_json_api(1800, 'wallets/{}'.format(coin_to.name.lower())) - js_1_to = read_json_api(1801, 'wallets/{}'.format(coin_to.name.lower())) - - node0_sent_messages_before: int = ci_part0.rpc_callback('smsgoutbox', ['count',])['num_messages'] - node1_sent_messages_before: int = ci_part1.rpc_callback('smsgoutbox', ['count',])['num_messages'] - - 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[id_offerer].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP) - wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id) - offer = swap_clients[id_bidder].listOffers(filters={'offer_id': offer_id})[0] - assert (offer.offer_id == offer_id) - - bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) - - wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED) - swap_clients[id_offerer].acceptBid(bid_id) - - wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) - wait_for_bid(test_delay_event, swap_clients[id_bidder], bid_id, BidStates.SWAP_COMPLETED, sent=True) - - amount_from = float(ci_from.format_amount(amt_swap)) - js_1 = read_json_api(1801, 'wallets') - node1_from_after = self.getBalance(js_1, coin_from) - if coin_from is not Coins.PART: # TODO: staking - assert (node1_from_after > node1_from_before + (amount_from - 0.05)) - - js_0 = read_json_api(1800, 'wallets') - node0_from_after = self.getBalance(js_0, coin_from) - # TODO: Discard block rewards - # assert (node0_from_after < node0_from_before - amount_from) - - js_0_to_after = read_json_api(1800, 'wallets/{}'.format(coin_to.name.lower())) - js_1_to_after = read_json_api(1801, 'wallets/{}'.format(coin_to.name.lower())) - - scale_from = 8 - amount_to = int((amt_swap * rate_swap) // (10 ** scale_from)) - amount_to_float = float(ci_to.format_amount(amount_to)) - node1_to_after = float(js_1_to_after['unconfirmed']) + float(js_1_to_after['balance']) - node1_to_before = float(js_1_to['unconfirmed']) + float(js_1_to['balance']) - if False: # TODO: set stakeaddress and xmr rewards to non wallet addresses - assert (node1_to_after < node1_to_before - amount_to_float) - - node0_sent_messages_after: int = ci_part0.rpc_callback('smsgoutbox', ['count',])['num_messages'] - node1_sent_messages_after: int = ci_part1.rpc_callback('smsgoutbox', ['count',])['num_messages'] - node0_sent_messages: int = node0_sent_messages_after - node0_sent_messages_before - node1_sent_messages: int = node1_sent_messages_after - node1_sent_messages_before - assert (node0_sent_messages == (5 if reverse_bid else 6)) - assert (node1_sent_messages == (6 if reverse_bid else 4)) - def test_01_a_full_swap(self): if not self.has_segwit: return @@ -360,7 +556,7 @@ class BasicSwapTest(BaseTest): def test_01_b_full_swap_reverse(self): if not self.has_segwit: return - self.prepare_balance(Coins.XMR.name, 100.0, 1800, 1801) + self.prepare_balance(Coins.XMR, 100.0, 1800, 1801) self.do_test_01_full_swap(Coins.XMR, self.test_coin_from) def test_01_c_full_swap_to_part(self): @@ -371,47 +567,6 @@ class BasicSwapTest(BaseTest): def test_01_d_full_swap_from_part(self): self.do_test_01_full_swap(Coins.PART, self.test_coin_from) - def do_test_02_leader_recover_a_lock_tx(self, coin_from: Coins, coin_to: Coins) -> None: - logging.info('---------- Test {} to {} leader recovers coin a lock tx'.format(coin_from.name, coin_to.name)) - - swap_clients = self.swap_clients - reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins - ci_from = swap_clients[0].ci(coin_from) - ci_to = swap_clients[0].ci(coin_to) - - id_offerer: int = 0 - id_bidder: int = 1 - id_leader: int = 1 if reverse_bid else 0 - id_follower: int = 0 if reverse_bid else 1 - logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}') - - js_wl_before = read_json_api(1800 + id_leader, 'wallets') - wl_from_before = self.getBalance(js_wl_before, coin_from) - - 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[id_offerer].postOffer( - coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, - lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32) - wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id) - offer = swap_clients[id_bidder].getOffer(offer_id) - - bid_id = swap_clients[id_bidder].postXmrBid(offer_id, offer.amount_from) - wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED) - - swap_clients[id_follower].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK) - swap_clients[id_offerer].acceptBid(bid_id) - - leader_sent_bid: bool = True if reverse_bid else False - wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=leader_sent_bid, wait_for=180) - wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, [BidStates.BID_STALLED_FOR_TEST, BidStates.XMR_SWAP_FAILED], sent=(not leader_sent_bid)) - - js_wl_after = read_json_api(1800 + id_leader, 'wallets') - wl_from_after = self.getBalance(js_wl_after, coin_from) - - # TODO: Discard block rewards - # assert (node0_from_before - node0_from_after < 0.02) - def test_02_a_leader_recover_a_lock_tx(self): if not self.has_segwit: return @@ -420,7 +575,7 @@ class BasicSwapTest(BaseTest): def test_02_b_leader_recover_a_lock_tx_reverse(self): if not self.has_segwit: return - self.prepare_balance(Coins.XMR.name, 100.0, 1800, 1801) + self.prepare_balance(Coins.XMR, 100.0, 1800, 1801) self.do_test_02_leader_recover_a_lock_tx(Coins.XMR, self.test_coin_from) def test_02_c_leader_recover_a_lock_tx_to_part(self): @@ -431,59 +586,6 @@ class BasicSwapTest(BaseTest): def test_02_leader_recover_a_lock_tx_from_part(self): self.do_test_02_leader_recover_a_lock_tx(Coins.PART, self.test_coin_from) - def do_test_03_follower_recover_a_lock_tx(self, coin_from, coin_to): - logging.info('---------- Test {} to {} follower recovers coin a lock tx'.format(coin_from.name, coin_to.name)) - - # Leader is too slow to recover the coin a lock tx and follower swipes it - # coin b lock tx remains unspent - - swap_clients = self.swap_clients - reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins - ci_from = swap_clients[0].ci(coin_from) - ci_to = swap_clients[0].ci(coin_to) - - id_offerer: int = 0 - id_bidder: int = 1 - id_leader: int = 1 if reverse_bid else 0 - id_follower: int = 0 if reverse_bid else 1 - logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}') - - js_w0_before = read_json_api(1800, 'wallets') - js_w1_before = read_json_api(1801, 'wallets') - - 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[id_offerer].postOffer( - coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, - lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32) - wait_for_offer(test_delay_event, swap_clients[1], offer_id) - offer = swap_clients[id_bidder].getOffer(offer_id) - - bid_id = swap_clients[id_bidder].postXmrBid(offer_id, offer.amount_from) - wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED) - - swap_clients[id_follower].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK) - swap_clients[id_leader].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND) - - swap_clients[id_offerer].acceptBid(bid_id) - - leader_sent_bid: bool = True if reverse_bid else False - wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.BID_STALLED_FOR_TEST, wait_for=180, sent=leader_sent_bid) - wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=80, sent=(not leader_sent_bid)) - - js_w1_after = read_json_api(1801, 'wallets') - - node1_from_before = self.getBalance(js_w1_before, coin_from) - node1_from_after = self.getBalance(js_w1_after, coin_from) - amount_from = float(format_amount(amt_swap, 8)) - # TODO: Discard block rewards - # assert (node1_from_after - node1_from_before > (amount_from - 0.02)) - - swap_clients[0].abandonBid(bid_id) - - wait_for_none_active(test_delay_event, 1800) - wait_for_none_active(test_delay_event, 1801) - def test_03_a_follower_recover_a_lock_tx(self): if not self.has_segwit: return @@ -492,7 +594,7 @@ class BasicSwapTest(BaseTest): def test_03_b_follower_recover_a_lock_tx_reverse(self): if not self.has_segwit: return - self.prepare_balance(Coins.XMR.name, 100.0, 1800, 1801) + self.prepare_balance(Coins.XMR, 100.0, 1800, 1801) self.do_test_03_follower_recover_a_lock_tx(Coins.XMR, self.test_coin_from) def test_03_c_follower_recover_a_lock_tx_to_part(self): @@ -503,61 +605,6 @@ class BasicSwapTest(BaseTest): def test_03_d_follower_recover_a_lock_tx_from_part(self): self.do_test_03_follower_recover_a_lock_tx(Coins.PART, self.test_coin_from) - 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 - reverse_bid: bool = coin_from in swap_clients[0].scriptless_coins - ci_from = swap_clients[0].ci(coin_from) - ci_to = swap_clients[0].ci(coin_to) - - id_offerer: int = 0 - id_bidder: int = 1 - id_leader: int = 1 if reverse_bid else 0 - id_follower: int = 0 if reverse_bid else 1 - logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}') - - js_w0_before = read_json_api(1800, 'wallets') - js_w1_before = read_json_api(1801, 'wallets') - - 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[id_offerer].postOffer( - coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, - lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=32) - wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id) - offer = swap_clients[id_bidder].getOffer(offer_id) - - bid_id = swap_clients[id_bidder].postXmrBid(offer_id, offer.amount_from) - wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED) - - swap_clients[id_follower].setBidDebugInd(bid_id, DebugTypes.CREATE_INVALID_COIN_B_LOCK) - swap_clients[id_offerer].acceptBid(bid_id) - - leader_sent_bid: bool = True if reverse_bid else False - wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=200, sent=leader_sent_bid) - wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=(not leader_sent_bid)) - - js_w0_after = read_json_api(1800, 'wallets') - js_w1_after = read_json_api(1801, 'wallets') - - node0_from_before = self.getBalance(js_w0_before, coin_from) - node0_from_after = self.getBalance(js_w0_after, coin_from) - logging.info('node0 end coin_from balance {}, diff {}'.format(node0_from_after, node0_from_after - node0_from_before)) - node0_to_before = self.getBalance(js_w0_before, coin_to) - node0_to_after = self.getBalance(js_w0_after, coin_to) - logging.info('node0 end coin_to balance {}, diff {}'.format(node0_to_after, node0_to_after - node0_to_before)) - if coin_from != Coins.PART: # TODO: Discard block rewards - assert (node0_from_before - node0_from_after < 0.02) - - node1_from_before = self.getBalance(js_w1_before, coin_from) - node1_from_after = self.getBalance(js_w1_after, coin_from) - logging.info('node1 end coin_from balance {}, diff {}'.format(node1_from_after, node1_from_after - node1_from_before)) - node1_to_before = self.getBalance(js_w1_before, coin_to) - node1_to_after = self.getBalance(js_w1_after, coin_to) - logging.info('node1 end coin_to balance {}, diff {}'.format(node1_to_after, node1_to_after - node1_to_before)) - assert (node1_to_before - node1_to_after < 0.02) - def test_04_a_follower_recover_b_lock_tx(self): if not self.has_segwit: return @@ -566,7 +613,7 @@ class BasicSwapTest(BaseTest): def test_04_b_follower_recover_b_lock_tx_reverse(self): if not self.has_segwit: return - self.prepare_balance(Coins.XMR.name, 100.0, 1800, 1801) + self.prepare_balance(Coins.XMR, 100.0, 1800, 1801) self.do_test_04_follower_recover_b_lock_tx(Coins.XMR, self.test_coin_from) def test_04_c_follower_recover_b_lock_tx_to_part(self): @@ -577,20 +624,6 @@ class BasicSwapTest(BaseTest): def test_04_d_follower_recover_b_lock_tx_from_part(self): 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) - - 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 @@ -604,12 +637,17 @@ class BasicSwapTest(BaseTest): def test_05_self_bid_from_part(self): self.do_test_05_self_bid(Coins.PART, self.test_coin_from) + def test_05_self_bid_rev(self): + if not self.has_segwit: + return + self.do_test_05_self_bid(Coins.XMR, self.test_coin_from) + def test_06_preselect_inputs(self): tla_from = self.test_coin_from.name logging.info('---------- Test {} Preselected inputs'.format(tla_from)) swap_clients = self.swap_clients - self.prepare_balance(tla_from, 100.0, 1802, 1800) + self.prepare_balance(self.test_coin_from, 100.0, 1802, 1800) js_w2 = read_json_api(1802, 'wallets') assert (float(js_w2[tla_from]['balance']) >= 100.0) @@ -739,7 +777,52 @@ class TestBTC(BasicSwapTest): assert (js_0['PART']['encrypted'] is True) assert (js_0['PART']['locked'] is False) - super().test_01_full_swap() + super().test_01_a_full_swap() + + +class TestBTC_PARTB(TestFunctions): + __test__ = True + test_coin_from = Coins.BTC + test_coin_to = Coins.PART_BLIND + start_ltc_nodes = False + base_rpc_port = BTC_BASE_RPC_PORT + + def test_01_a_full_swap(self): + self.prepare_balance(self.test_coin_to, 100.0, 1801, 1800) + self.do_test_01_full_swap(self.test_coin_from, self.test_coin_to) + + def test_01_b_full_swap_reverse(self): + self.prepare_balance(self.test_coin_to, 100.0, 1800, 1800) + self.do_test_01_full_swap(self.test_coin_to, self.test_coin_from) + + def test_02_a_leader_recover_a_lock_tx(self): + self.prepare_balance(self.test_coin_to, 100.0, 1801, 1800) + self.do_test_02_leader_recover_a_lock_tx(self.test_coin_from, self.test_coin_to) + + def test_02_b_leader_recover_a_lock_tx_reverse(self): + self.prepare_balance(self.test_coin_to, 100.0, 1800, 1800) + self.do_test_02_leader_recover_a_lock_tx(self.test_coin_to, self.test_coin_from) + + def test_03_a_follower_recover_a_lock_tx(self): + self.prepare_balance(self.test_coin_to, 100.0, 1801, 1800) + self.do_test_03_follower_recover_a_lock_tx(self.test_coin_from, self.test_coin_to) + + def test_03_b_follower_recover_a_lock_tx_reverse(self): + self.prepare_balance(self.test_coin_to, 100.0, 1800, 1800) + self.do_test_03_follower_recover_a_lock_tx(self.test_coin_to, self.test_coin_from, lock_value=12) + + def test_04_a_follower_recover_b_lock_tx(self): + self.prepare_balance(self.test_coin_to, 100.0, 1801, 1800) + self.do_test_04_follower_recover_b_lock_tx(self.test_coin_from, self.test_coin_to) + + def test_04_b_follower_recover_b_lock_tx_reverse(self): + self.prepare_balance(self.test_coin_to, 100.0, 1800, 1800) + self.do_test_04_follower_recover_b_lock_tx(self.test_coin_to, self.test_coin_from) + + +class TestBTC_PARTA(TestBTC_PARTB): + __test__ = True + test_coin_to = Coins.PART_ANON if __name__ == '__main__': diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index 8f4515e..baad3f6 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -565,7 +565,7 @@ class BaseTest(unittest.TestCase): for i in range(8): sx_addr = callnoderpc(1, 'getnewstealthaddress') outputs.append({'address': sx_addr, 'amount': 0.5}) - for i in range(6): + for i in range(7): callnoderpc(0, 'sendtypeto', ['part', 'anon', outputs]) part_addr1 = callnoderpc(1, 'getnewaddress', ['initial addr'])