diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 8a71404..5014cbd 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -746,14 +746,11 @@ class BasicSwap(BaseApp): if not offer: raise ValueError('Offer not found') + self.loadBidTxns(bid, session) if offer.swap_type == SwapTypes.XMR_SWAP: xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid.bid_id).first() - self.loadBidTxns(bid, session) self.watchXmrSwap(bid, offer, xmr_swap) else: - bid.initiate_tx = session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid.bid_id, SwapTx.tx_type == TxTypes.ITX)).first() - bid.participate_tx = session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid.bid_id, SwapTx.tx_type == TxTypes.PTX)).first() - self.swaps_in_progress[bid.bid_id] = (bid, offer) coin_from = Coins(offer.coin_from) @@ -1787,37 +1784,42 @@ class BasicSwap(BaseApp): session.remove() self.mxDB.release() - def getBid(self, bid_id): - self.mxDB.acquire() + def getBid(self, bid_id, session=None): + use_session = None try: - session = scoped_session(self.session_factory) - bid = session.query(Bid).filter_by(bid_id=bid_id).first() + if session: + use_session = session + else: + self.mxDB.acquire() + use_session = scoped_session(self.session_factory) + bid = use_session.query(Bid).filter_by(bid_id=bid_id).first() if bid: - self.loadBidTxns(bid, session) + self.loadBidTxns(bid, use_session) return bid finally: - session.close() - session.remove() - self.mxDB.release() + if session is None: + use_session.close() + use_session.remove() + self.mxDB.release() - def getBidAndOffer(self, bid_id): - self.mxDB.acquire() + def getBidAndOffer(self, bid_id, session=None): try: - session = scoped_session(self.session_factory) - bid = session.query(Bid).filter_by(bid_id=bid_id).first() + if session: + use_session = session + else: + self.mxDB.acquire() + use_session = scoped_session(self.session_factory) + bid = use_session.query(Bid).filter_by(bid_id=bid_id).first() offer = None if bid: - offer = session.query(Offer).filter_by(offer_id=bid.offer_id).first() - if offer and offer.swap_type == SwapTypes.XMR_SWAP: - self.loadBidTxns(bid, session) - else: - bid.initiate_tx = session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid_id, SwapTx.tx_type == TxTypes.ITX)).first() - bid.participate_tx = session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid_id, SwapTx.tx_type == TxTypes.PTX)).first() + offer = use_session.query(Offer).filter_by(offer_id=bid.offer_id).first() + self.loadBidTxns(bid, use_session) return bid, offer finally: - session.close() - session.remove() - self.mxDB.release() + if session is None: + use_session.close() + use_session.remove() + self.mxDB.release() def getXmrBidAndOffer(self, bid_id, list_events=True): self.mxDB.acquire() @@ -1834,10 +1836,7 @@ class BasicSwap(BaseApp): if offer and offer.swap_type == SwapTypes.XMR_SWAP: xmr_swap = session.query(XmrSwap).filter_by(bid_id=bid.bid_id).first() xmr_offer = session.query(XmrOffer).filter_by(offer_id=bid.offer_id).first() - self.loadBidTxns(bid, session) - else: - bid.initiate_tx = session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid_id, SwapTx.tx_type == TxTypes.ITX)).first() - bid.participate_tx = session.query(SwapTx).filter(sa.and_(SwapTx.bid_id == bid_id, SwapTx.tx_type == TxTypes.PTX)).first() + self.loadBidTxns(bid, session) if list_events: events = self.list_bid_events(bid.bid_id, session) @@ -2941,7 +2940,7 @@ class BasicSwap(BaseApp): rv = True # Remove from swaps_in_progress elif state == BidStates.XMR_SWAP_FAILED_SWIPED: rv = True # Remove from swaps_in_progress - elif state in (BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX, BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX): + elif state == BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX: if bid.xmr_a_lock_tx is None: return rv @@ -3337,19 +3336,14 @@ class BasicSwap(BaseApp): bid.setPTxState(TxStates.TX_REDEEMED) if bid.was_sent: - txn = self.createRedeemTxn(coin_from, bid, for_txn_type='initiate') - if bid.debug_ind == DebugTypes.DONT_SPEND_ITX: self.log.debug('bid %s: Abandoning bid for testing: %d, %s.', bid_id.hex(), bid.debug_ind, DebugTypes(bid.debug_ind).name) bid.setState(BidStates.BID_ABANDONED) self.logBidEvent(bid.bid_id, EventLogTypes.DEBUG_TWEAK_APPLIED, 'ind {}'.format(bid.debug_ind), None) else: - txid = self.submitTxn(coin_from, txn) - - bid.initiate_tx.spend_txid = bytes.fromhex(txid) - # bid.initiate_txn_redeem = bytes.fromhex(txn) # Worth keeping? - self.log.debug('Submitted initiate redeem txn %s to %s chain for bid %s', txid, chainparams[coin_from]['name'], bid_id.hex()) - + delay = random.randrange(self.min_delay_event_short, self.max_delay_event_short) + self.log.info('Redeeming ITX for bid %s in %d seconds', bid_id.hex(), delay) + self.createAction(delay, ActionTypes.REDEEM_ITX, bid_id) # TODO: Wait for depth? new state SWAP_TXI_REDEEM_SENT? self.removeWatchedOutput(coin_to, bid_id, bid.participate_tx.txid.hex()) @@ -3582,6 +3576,8 @@ class BasicSwap(BaseApp): self.recoverXmrBidCoinBLockTx(row.linked_id, session) elif row.action_type == ActionTypes.SEND_XMR_SWAP_LOCK_SPEND_MSG: self.sendXmrBidCoinALockSpendTxMsg(row.linked_id, session) + elif row.action_type == ActionTypes.REDEEM_ITX: + atomic_swap_1.redeemITx(self, row.linked_id, session) else: self.log.warning('Unknown event type: %d', row.event_type) except Exception as ex: @@ -4843,6 +4839,7 @@ class BasicSwap(BaseApp): ci_from.verifyCompact(xmr_swap.pkal, 'proof key owned for swap', xmr_swap.kal_sig) bid.setState(BidStates.XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX) + bid.setState(BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX) self.saveBid(bid_id, bid, xmr_swap=xmr_swap) except Exception as ex: if self.debug: diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index 7854e8c..ad50cf6 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -135,6 +135,7 @@ class ActionTypes(IntEnum): REDEEM_XMR_SWAP_LOCK_TX_B = auto() # Leader RECOVER_XMR_SWAP_LOCK_TX_B = auto() SEND_XMR_SWAP_LOCK_SPEND_MSG = auto() + REDEEM_ITX = auto() class EventLogTypes(IntEnum): diff --git a/basicswap/http_server.py b/basicswap/http_server.py index c398ad7..a53a9a0 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -6,7 +6,6 @@ import os import json -import struct import traceback import threading import http.client @@ -55,6 +54,7 @@ from .ui.util import ( listBidStates, get_data_entry, have_data_entry, + listOldBidStates, get_data_entry_or, listAvailableCoins, set_pagination_filters, @@ -695,23 +695,7 @@ class HttpHandler(BaseHTTPRequestHandler): data['show_bidder_seq_diagram'] = show_bidder_seq_diagram data['show_offerer_seq_diagram'] = show_offerer_seq_diagram - old_states = [] - num_states = len(bid.states) // 12 - for i in range(num_states): - up = struct.unpack_from(' 0: - old_states.sort(key=lambda x: x[0]) + old_states = listOldBidStates(bid) if len(data['addr_from_label']) > 0: data['addr_from_label'] = '(' + data['addr_from_label'] + ')' diff --git a/basicswap/js_server.py b/basicswap/js_server.py index bdb3c42..fb80149 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2020-2021 tecnovert +# Copyright (c) 2020-2022 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -27,6 +27,7 @@ from .ui.util import ( get_data_entry_or, have_data_entry, tickerToCoinId, + listOldBidStates, ) from .ui.page_offers import postNewOffer from .protocols.xmr_swap_1 import recoverNoScriptTxnWithKey, getChainBSplitKey @@ -230,6 +231,10 @@ def js_bids(self, url_split, post_string, is_json): remote_key = get_data_entry(post_data, 'remote_key') return bytes(json.dumps({'txid': recoverNoScriptTxnWithKey(swap_client, bid_id, remote_key).hex()}), 'UTF-8') + if len(url_split) > 4 and url_split[4] == 'states': + old_states = listOldBidStates(bid) + return bytes(json.dumps(old_states), 'UTF-8') + edit_bid = False show_txns = False data = describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, events, edit_bid, show_txns, for_api=True) diff --git a/basicswap/protocols/atomic_swap_1.py b/basicswap/protocols/atomic_swap_1.py index 3763c25..963bedb 100644 --- a/basicswap/protocols/atomic_swap_1.py +++ b/basicswap/protocols/atomic_swap_1.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2020 tecnovert +# Copyright (c) 2020-2022 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -11,7 +11,6 @@ from basicswap.script import ( OpCodes, ) - INITIATE_TX_TIMEOUT = 40 * 60 # TODO: make variable per coin @@ -48,3 +47,14 @@ def buildContractScript(lock_val, secret_hash, pkh_redeem, pkh_refund, op_lock=O def extractScriptSecretHash(script): return script[7:39] + + +def redeemITx(self, bid_id, session): + bid, offer = self.getBidAndOffer(bid_id, session) + ci_from = self.ci(offer.coin_from) + + txn = self.createRedeemTxn(ci_from.coin_type(), bid, for_txn_type='initiate') + txid = self.submitTxn(ci_from.coin_type(), txn) + + bid.initiate_tx.spend_txid = bytes.fromhex(txid) + self.log.debug('Submitted initiate redeem txn %s to %s chain for bid %s', txid, ci_from.coin_name(), bid_id.hex()) diff --git a/basicswap/static/sequence_diagrams/bidder.alt.xu.min.svg b/basicswap/static/sequence_diagrams/bidder.alt.xu.min.svg index 2828531..df1f3ae 100644 --- a/basicswap/static/sequence_diagrams/bidder.alt.xu.min.svg +++ b/basicswap/static/sequence_diagrams/bidder.alt.xu.min.svg @@ -1 +1,297 @@ - NetworkOffererBidder Sends OfferDetects OfferSends BidSends Initiate TxSends BidAcceptDetects Initiate TxWait for ITX to confirmWait for ITX to confirmSends Participate TxDetects Participate TxWait for PTX to confirmWait for PTX to confirmSends Participate Redeem TxDetects Participate Redeem TxSends Initiate Redeem TxWait for ITX Redeem to confirmfail pathWait for PTX locktime to expirePTX Refund TxWait for PTX Refund to confirmBid SentUser accepts bidOfferer generates secret_value and sends Hash(secret_value) tothe BidderITX can be spent by knowledge of thesecret_value and the bidder_redeem_key or after a timeoutby the offerer_refund_keyBid AcceptedITX SentITX ConfirmedPTX can be spent by knowledge of thesecret_value and the offerer_redeem_key or after a timeoutby the bidder_refund_keyPTX SentPTX ConfirmedBid ParticipatingReveals secret_valuealt: success pathPTX RedeemedITX RedeemedBid CompletedPTX Refunded \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Network + + Offerer + + Bidder + + + + + + + Sends Offer + + + Detects Offer + + + Sends Bid + + + Sends Initiate Tx + + + Sends BidAccept + + + Detects Initiate Tx + + + Wait for ITX to confirm + + + Wait for ITX to confirm + + + Sends Participate Tx + + + Detects Participate Tx + + + Wait for PTX to confirm + + + Wait for PTX to confirm + + + Sends Participate Redeem Tx + + + Detects Participate Redeem Tx + + + Sends Initiate Redeem Tx + + + Wait for ITX Redeem to confirm + + + fail path + + + Wait for PTX locktime to expire + + + PTX Refund Tx + + + Wait for PTX Refund to confirm + + + + Bid Sent + + User accepts bid + + Offerer generates secret_value and sends Hash(secret_value) to + the Bidder + + ITX can be spent by knowledge of the + secret_value and the bidder_redeem_key or after a timeout + by the offerer_refund_key + + Bid Accepted + + ITX Sent + + Bid Initiated + + ITX Confirmed + + PTX can be spent by knowledge of the + secret_value and the offerer_redeem_key or after a timeout + by the bidder_refund_key + + PTX Sent + + PTX Confirmed + + Bid Participating + + Reveals secret_value + + alt: success path + + PTX Redeemed + + ITX Redeemed + + Bid Completed + + PTX Refunded + + + diff --git a/basicswap/static/sequence_diagrams/xmr.bidder.alt.xu.min.svg b/basicswap/static/sequence_diagrams/xmr.bidder.alt.xu.min.svg index ae54aed..c402bf4 100644 --- a/basicswap/static/sequence_diagrams/xmr.bidder.alt.xu.min.svg +++ b/basicswap/static/sequence_diagrams/xmr.bidder.alt.xu.min.svg @@ -1,4 +1,4 @@ - + @@ -35,10 +35,10 @@ - + - - + + @@ -93,15 +93,15 @@ - - - - - - - - - + + + + + + + + + @@ -109,23 +109,23 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + @@ -145,15 +145,15 @@ - - - - - - - - - + + + + + + + + + @@ -169,19 +169,19 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -205,15 +205,15 @@ - - - - - - - - - + + + + + + + + + @@ -225,7 +225,11 @@ - + + + + + @@ -261,83 +265,83 @@ Sends script-coin-lock-tx - - - Wait for - - script-coin-lock-tx to - - confirm - - - Sends noscript-coin-lock-tx - - - Wait for - - noscript-coin-lock-tx to - - confirm - - - Wait for - - noscript-coin-lock-tx to - - confirm - - - Sends script-coin-lock-tx - - release message - - - Sends script-coin-lock-spend-tx - - - fail path - - - Wait for - - script-coin-lock-tx lock to - - expire - - - Sends - - script-coin-lock-pre-refund-tx - - - script-coin-lock-pre-refund-tx - - - Wait for - - pre-refund tx to confirm - - - Sends - - script-coin-lock-pre-refund-spend-tx - - - Detects script-coin-lock-pre-refund-spend-tx - - - Sends scriptless-coin-lock-recover-tx - - - bidder swipes script coin lock tx - - - Wait for - - pre-refund tx lock to expire - - - Sends script-coin-lock-pre-refund-swipe-tx + + + Wait for + + script-coin-lock-tx to + + confirm + + + Sends noscript-coin-lock-tx + + + Wait for + + noscript-coin-lock-tx to + + confirm + + + Wait for + + noscript-coin-lock-tx to + + confirm + + + Sends script-coin-lock-tx + + release message + + + Sends script-coin-lock-spend-tx + + + fail path + + + Wait for + + script-coin-lock-tx lock to + + expire + + + Sends + + script-coin-lock-pre-refund-tx + + + script-coin-lock-pre-refund-tx + + + Wait for + + pre-refund tx to confirm + + + Sends + + script-coin-lock-pre-refund-spend-tx + + + Detects script-coin-lock-pre-refund-spend-tx + + + Sends scriptless-coin-lock-recover-tx + + + bidder swipes script coin lock tx + + + Wait for + + pre-refund tx lock to expire + + + Sends script-coin-lock-pre-refund-swipe-tx @@ -363,45 +367,48 @@ the offerer's signature for it. Bid Script coin spend tx valid - - Bid Script coin locked - - alt: success path - - Bid Scriptless coin locked - - The XmrBidLockReleaseMessage contains the offerer's OTVES for it. - The bidder decodes the offerer's signature - from the OTVES. When the offerer has the - plaintext signature, they can decode the bidder's noscript-coin-lock-tx - signature. - - Script coin lock released - - Script tx redeemed + + Exchanged script lock spend tx + msg + + Bid Script coin locked + + alt: success path + + Bid Scriptless coin locked + + The XmrBidLockReleaseMessage contains the offerer's OTVES for it. + The bidder decodes the offerer's signature + from the OTVES. When the offerer has the + plaintext signature, they can decode the bidder's noscript-coin-lock-tx + signature. + + Script coin lock released - Bid Completed - - tx can be sent by either party. - - Bid Script pre-refund tx in - chain - - alt: offerer refunds script coin lock tx - - Refunds the script lock tx, with the offerer's cleartext signature - the bidder can refund the noscript lock tx. - Once the lock expires the pre-refund tx can be spent by the bidder. - - Bid Failed, refunded - - Bidder recovers the offerer's scriptless chain key-shard. - - Bid Scriptless tx recovered + Script tx redeemed + + Bid Completed + + tx can be sent by either party. + + Bid Script pre-refund tx in + chain + + alt: offerer refunds script coin lock tx + + Refunds the script lock tx, with the offerer's cleartext signature + the bidder can refund the noscript lock tx. + Once the lock expires the pre-refund tx can be spent by the bidder. + + Bid Failed, refunded + + Bidder recovers the offerer's scriptless chain key-shard. - Bid Failed, refunded - - Bid Failed, swiped + Bid Scriptless tx recovered + + Bid Failed, refunded + + Bid Failed, swiped diff --git a/basicswap/ui/util.py b/basicswap/ui/util.py index 6e10591..daf02b3 100644 --- a/basicswap/ui/util.py +++ b/basicswap/ui/util.py @@ -5,6 +5,7 @@ # file LICENSE or http://www.opensource.org/licenses/mit-license.php. import json +import struct import traceback from basicswap.util import ( make_int, @@ -358,6 +359,29 @@ def describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, bid_events, edit_b return data +def listOldBidStates(bid): + old_states = [] + num_states = len(bid.states) // 12 + for i in range(num_states): + up = struct.unpack_from(' 0: + old_states.sort(key=lambda x: x[0]) + return old_states + + def getCoinName(c): if c == Coins.PART_ANON: return chainparams[Coins.PART]['name'].capitalize() + 'Anon' diff --git a/doc/protocols/sequence_diagrams/bidder.alt.xu b/doc/protocols/sequence_diagrams/bidder.alt.xu index 4eb69f4..b0f2e6a 100644 --- a/doc/protocols/sequence_diagrams/bidder.alt.xu +++ b/doc/protocols/sequence_diagrams/bidder.alt.xu @@ -26,6 +26,7 @@ xu { N >> B [label="Detects Initiate Tx"]; B abox B [label="ITX Sent", textbgcolor="#4bdbf1"]; B => B [label="Wait for ITX to confirm"], O => O [label="Wait for ITX to confirm"]; + B abox B [label="Bid Initiated"]; B abox B [label="ITX Confirmed", textbgcolor="#4bdbf1"]; B =>> N [label="Sends Participate Tx"], C note C2 diff --git a/doc/protocols/sequence_diagrams/xmr.bidder.alt.xu b/doc/protocols/sequence_diagrams/xmr.bidder.alt.xu index f001172..04de528 100644 --- a/doc/protocols/sequence_diagrams/xmr.bidder.alt.xu +++ b/doc/protocols/sequence_diagrams/xmr.bidder.alt.xu @@ -28,6 +28,7 @@ xu { textbgcolor="#FFFFCC"]; O =>> N [label="Sends script-coin-lock-tx"], B abox B [label="Bid Script coin spend tx valid"]; + B abox B [label="Exchanged script lock spend tx msg"]; B => B [label="Wait for script-coin-lock-tx to confirm"]; B abox B [label="Bid Script coin locked"]; CB alt C [label="success path"] { diff --git a/tests/basicswap/common.py b/tests/basicswap/common.py index ac6b292..cfaf441 100644 --- a/tests/basicswap/common.py +++ b/tests/basicswap/common.py @@ -286,3 +286,92 @@ def make_rpc_func(node_id, base_rpc_port=BASE_RPC_PORT): nonlocal node_id, auth return callrpc(base_rpc_port + node_id, auth, method, params, wallet) return rpc_func + + +def extract_states_from_xu_file(file_path): + states = {} + + alt_counter = 0 + active_path = 0 + states[active_path] = [] + path_stack = [active_path, ] + with open(file_path) as fp: + for line in fp: + line = line.strip() + if line.startswith('#'): + continue + + if line == '};': + if len(path_stack) > 1: + path_stack.pop() + active_path = path_stack[-1] + continue + + split_line = line.split('[') + if len(split_line) < 2: + continue + + definitions = split_line[0].split(' ') + if len(definitions) < 2: + continue + + if definitions[1] == 'alt': + alt_counter += 1 + path_stack.append(alt_counter) + + states[alt_counter] = [s for s in states[active_path]] + continue + + if definitions[0] == '---': + active_path = path_stack[-1] + continue + + if definitions[1] != 'abox': + continue + + tag_start = 'label="' + tag_end = '"' + pos_start = split_line[1].find(tag_start) + if pos_start < 0: + continue + pos_start += len(tag_start) + pos_end = split_line[1].find(tag_end, pos_start) + if pos_end < 0: + continue + label = split_line[1][pos_start:pos_end] + + if line.find('textbgcolor') > 0: + # transaction status + pass + + states[active_path].append(label) + + return states + + +def compare_bid_states(states, expect_states): + + for i in range(len(states) - 1, -1, -1): + if states[i][1] == 'Bid Delaying': + del states[i] + + assert(len(states) == len(expect_states)) + + for i, s in enumerate(states): + if s[1] != expect_states[i]: + if 'Bid ' + expect_states[i] == s[1]: + logging.warning(f'Expected state {expect_states[i]} not an exact match to {s[1]}.') + continue + if [s[0], expect_states[i]] in states: + logging.warning(f'Expected state {expect_states[i]} found out of order at the same time as {s[1]}.') + continue + raise ValueError(f'Expected state {expect_states[i]}, found {s[1]}') + assert(s[1] == expect_states[i]) + return True + + +def read_json_api(port, path=None): + url = f'http://127.0.0.1:{port}/json' + if path is not None: + url += '/' + path + return json.loads(urlopen(url).read()) diff --git a/tests/basicswap/test_run.py b/tests/basicswap/test_run.py index 67f6701..320f129 100644 --- a/tests/basicswap/test_run.py +++ b/tests/basicswap/test_run.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2019-2021 tecnovert +# Copyright (c) 2019-2022 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -13,11 +13,11 @@ $ pytest -v -s tests/basicswap/test_run.py::Test::test_04_ltc_btc """ +import os import json import random import logging import unittest -from urllib.request import urlopen from basicswap.basicswap import ( Coins, @@ -41,9 +41,12 @@ from tests.basicswap.common import ( wait_for_bid_tx_state, wait_for_in_progress, post_json_req, + read_json_api, TEST_HTTP_PORT, LTC_BASE_RPC_PORT, BTC_BASE_RPC_PORT, + compare_bid_states, + extract_states_from_xu_file, ) from .test_xmr import BaseTest, test_delay_event, callnoderpc @@ -69,6 +72,10 @@ class Test(BaseTest): wait_for_balance(test_delay_event, 'http://127.0.0.1:1801/json/wallets/btc', 'balance', 1000.0) wait_for_balance(test_delay_event, 'http://127.0.0.1:1801/json/wallets/ltc', 'balance', 1000.0) + diagrams_dir = 'doc/protocols/sequence_diagrams' + cls.states_bidder = extract_states_from_xu_file(os.path.join(diagrams_dir, 'bidder.alt.xu')) + cls.states_offerer = extract_states_from_xu_file(os.path.join(diagrams_dir, 'offerer.alt.xu')) + @classmethod def tearDownClass(cls): logging.info('Finalising test') @@ -133,11 +140,19 @@ class Test(BaseTest): wait_for_bid(test_delay_event, swap_clients[0], 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) - js_0 = json.loads(urlopen('http://127.0.0.1:1800/json').read()) - js_1 = json.loads(urlopen('http://127.0.0.1:1801/json').read()) + js_0 = read_json_api(1800) + js_1 = read_json_api(1801) assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0) assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0) + bid_id_hex = bid_id.hex() + path = f'bids/{bid_id_hex}/states' + offerer_states = read_json_api(1800, path) + bidder_states = read_json_api(1801, path) + + assert(compare_bid_states(offerer_states, self.states_offerer[0]) is True) + assert(compare_bid_states(bidder_states, self.states_bidder[0]) is True) + def test_03_ltc_part(self): logging.info('---------- Test LTC to PART') swap_clients = self.swap_clients @@ -156,8 +171,8 @@ class Test(BaseTest): wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60) wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) - js_0 = json.loads(urlopen('http://127.0.0.1:1800/json').read()) - js_1 = json.loads(urlopen('http://127.0.0.1:1801/json').read()) + js_0 = read_json_api(1800) + js_1 = read_json_api(1801) assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0) assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0) @@ -179,11 +194,8 @@ class Test(BaseTest): wait_for_bid(test_delay_event, swap_clients[0], 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) - js_0bid = json.loads(urlopen('http://127.0.0.1:1800/json/bids/{}'.format(bid_id.hex())).read()) - - js_0 = json.loads(urlopen('http://127.0.0.1:1800/json').read()) - js_1 = json.loads(urlopen('http://127.0.0.1:1801/json').read()) - + js_0 = read_json_api(1800) + js_1 = read_json_api(1801) assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0) assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0) @@ -206,13 +218,13 @@ class Test(BaseTest): wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, wait_for=60) - js_0_bid = json.loads(urlopen('http://127.0.0.1:1800/json/bids/{}'.format(bid_id.hex())).read()) - js_1_bid = json.loads(urlopen('http://127.0.0.1:1801/json/bids/{}'.format(bid_id.hex())).read()) + js_0_bid = read_json_api(1800, 'bids/{}'.format(bid_id.hex())) + js_1_bid = read_json_api(1801, 'bids/{}'.format(bid_id.hex())) assert(js_0_bid['itx_state'] == 'Refunded') assert(js_1_bid['ptx_state'] == 'Unknown') - js_0 = json.loads(urlopen('http://127.0.0.1:1800/json').read()) - js_1 = json.loads(urlopen('http://127.0.0.1:1801/json').read()) + js_0 = read_json_api(1800) + js_1 = read_json_api(1801) assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0) assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0) @@ -220,7 +232,7 @@ class Test(BaseTest): logging.info('---------- Test same client, BTC to LTC') swap_clients = self.swap_clients - js_0_before = json.loads(urlopen('http://127.0.0.1:1800/json').read()) + js_0_before = read_json_api(1800) offer_id = swap_clients[0].postOffer(Coins.BTC, Coins.LTC, 10 * COIN, 10 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST) @@ -235,7 +247,7 @@ class Test(BaseTest): wait_for_bid_tx_state(test_delay_event, swap_clients[0], bid_id, TxStates.TX_REDEEMED, TxStates.TX_REDEEMED, wait_for=60) wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60) - js_0 = json.loads(urlopen('http://127.0.0.1:1800/json').read()) + js_0 = read_json_api(1800) assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0) assert(js_0['num_recv_bids'] == js_0_before['num_recv_bids'] + 1 and js_0['num_sent_bids'] == js_0_before['num_sent_bids'] + 1) @@ -243,7 +255,7 @@ class Test(BaseTest): logging.info('---------- Test error, BTC to LTC, set fee above bid value') swap_clients = self.swap_clients - js_0_before = json.loads(urlopen('http://127.0.0.1:1800/json').read()) + js_0_before = read_json_api(1800) offer_id = swap_clients[0].postOffer(Coins.BTC, Coins.LTC, 0.001 * COIN, 1.0 * COIN, 0.001 * COIN, SwapTypes.SELLER_FIRST) @@ -307,13 +319,13 @@ class Test(BaseTest): wait_for_bid(test_delay_event, swap_clients[0], 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) - js_0_bid = json.loads(urlopen('http://127.0.0.1:1800/json/bids/{}'.format(bid_id.hex())).read()) - js_1_bid = json.loads(urlopen('http://127.0.0.1:1801/json/bids/{}'.format(bid_id.hex())).read()) + js_0_bid = read_json_api(1800, 'bids/{}'.format(bid_id.hex())) + js_1_bid = read_json_api(1801, 'bids/{}'.format(bid_id.hex())) assert(js_0_bid['itx_state'] == 'Refunded') assert(js_1_bid['ptx_state'] == 'Refunded') - js_0 = json.loads(urlopen('http://127.0.0.1:1800/json').read()) - js_1 = json.loads(urlopen('http://127.0.0.1:1801/json').read()) + js_0 = read_json_api(1800) + js_1 = read_json_api(1801) assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0) assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0) @@ -339,13 +351,13 @@ class Test(BaseTest): wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=120) wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, wait_for=120) - js_0_bid = json.loads(urlopen('http://127.0.0.1:1800/json/bids/{}'.format(bid_id.hex())).read()) - js_1_bid = json.loads(urlopen('http://127.0.0.1:1801/json/bids/{}'.format(bid_id.hex())).read()) + js_0_bid = read_json_api(1800, 'bids/{}'.format(bid_id.hex())) + js_1_bid = read_json_api(1801, 'bids/{}'.format(bid_id.hex())) assert(js_0_bid['itx_state'] == 'Refunded') assert(js_1_bid['ptx_state'] == 'Unknown') - js_0 = json.loads(urlopen('http://127.0.0.1:1800/json').read()) - js_1 = json.loads(urlopen('http://127.0.0.1:1801/json').read()) + js_0 = read_json_api(1800) + js_1 = read_json_api(1801) assert(js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0) assert(js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0) ''' @@ -354,7 +366,7 @@ class Test(BaseTest): logging.info('---------- Test LTC withdrawals') ltc_addr = callnoderpc(0, 'getnewaddress', ['Withdrawal test', 'legacy'], base_rpc_port=LTC_BASE_RPC_PORT) - wallets0 = json.loads(urlopen('http://127.0.0.1:{}/json/wallets'.format(TEST_HTTP_PORT + 0)).read()) + wallets0 = read_json_api(TEST_HTTP_PORT + 0, 'wallets') assert(float(wallets0['3']['balance']) > 100) post_json = { @@ -371,8 +383,8 @@ class Test(BaseTest): # Participant loses PTX value without gaining ITX value swap_clients = self.swap_clients - js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) - js_w1_before = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + js_w0_before = read_json_api(1800, 'wallets') + js_w1_before = read_json_api(1801, 'wallets') swap_value = make_int(random.uniform(2.0, 20.0), scale=8, r=1) logging.info('swap_value {}'.format(format_amount(swap_value, 8))) @@ -389,8 +401,8 @@ class Test(BaseTest): wait_for_bid_tx_state(test_delay_event, swap_clients[0], bid_id, TxStates.TX_REFUNDED, TxStates.TX_REDEEMED, wait_for=60) - js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) - js_w1_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + js_w0_after = read_json_api(1800, 'wallets') + js_w1_after = read_json_api(1801, 'wallets') ltc_swap_value = swap_value btc_swap_value = swap_value // 2 diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index 57d2d4d..07f670e 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -16,7 +16,6 @@ import unittest import traceback import threading import subprocess -from urllib.request import urlopen import basicswap.config as cfg from basicswap.db import ( @@ -70,6 +69,9 @@ from tests.basicswap.common import ( wait_for_none_active, wait_for_balance, post_json_req, + read_json_api, + compare_bid_states, + extract_states_from_xu_file, TEST_HTTP_HOST, TEST_HTTP_PORT, BASE_RPC_PORT, @@ -339,6 +341,10 @@ class BaseTest(unittest.TestCase): cls.stream_fp.setFormatter(formatter) logger.addHandler(cls.stream_fp) + diagrams_dir = 'doc/protocols/sequence_diagrams' + cls.states_bidder = extract_states_from_xu_file(os.path.join(diagrams_dir, 'xmr.bidder.alt.xu')) + cls.states_offerer = extract_states_from_xu_file(os.path.join(diagrams_dir, 'xmr.offerer.alt.xu')) + try: logging.info('Preparing coin nodes.') for i in range(NUM_NODES): @@ -547,7 +553,7 @@ class Test(BaseTest): logging.info('---------- Test PART to XMR') swap_clients = self.swap_clients - js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + js_1 = read_json_api(1801, 'wallets') assert(make_int(js_1[str(int(Coins.XMR))]['balance'], scale=12) > 0) assert(make_int(js_1[str(int(Coins.XMR))]['unconfirmed'], scale=12) > 0) @@ -569,14 +575,22 @@ class Test(BaseTest): wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True) - js_0_end = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + js_0_end = read_json_api(1800, 'wallets') end_xmr = float(js_0_end['6']['balance']) + float(js_0_end['6']['unconfirmed']) assert(end_xmr > 10.9 and end_xmr < 11.0) + bid_id_hex = bid_id.hex() + path = f'bids/{bid_id_hex}/states' + offerer_states = read_json_api(1800, path) + bidder_states = read_json_api(1801, path) + + assert(compare_bid_states(offerer_states, self.states_offerer[0]) is True) + assert(compare_bid_states(bidder_states, self.states_bidder[0]) is True) + def test_011_smsgaddresses(self): logging.info('---------- Test address management and private offers') swap_clients = self.swap_clients - js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/smsgaddresses').read()) + js_1 = read_json_api(1801, 'smsgaddresses') post_json = { 'addressnote': 'testing', @@ -585,7 +599,7 @@ class Test(BaseTest): new_address = json_rv['new_address'] new_address_pk = json_rv['pubkey'] - js_2 = json.loads(urlopen('http://127.0.0.1:1801/json/smsgaddresses').read()) + js_2 = read_json_api(1801, 'smsgaddresses') assert(len(js_2) == len(js_1) + 1) found = False for addr in js_2: @@ -611,7 +625,7 @@ class Test(BaseTest): json_rv = json.loads(post_json_req('http://127.0.0.1:1801/json/smsgaddresses/edit', post_json)) assert(json_rv['edited_address'] == new_address) - js_3 = json.loads(urlopen('http://127.0.0.1:1801/json/smsgaddresses').read()) + js_3 = read_json_api(1801, 'smsgaddresses') found = False for addr in js_3: if addr['addr'] == new_address: @@ -664,17 +678,17 @@ class Test(BaseTest): wait_for_offer(test_delay_event, swap_clients[1], bytes.fromhex(offer_id_hex)) - rv = json.loads(urlopen(f'http://127.0.0.1:1801/json/offers/{offer_id_hex}').read()) + rv = read_json_api(1801, f'offers/{offer_id_hex}') assert(rv[0]['addr_to'] == new_address) - rv = json.loads(urlopen(f'http://127.0.0.1:1800/json/offers/{offer_id_hex}').read()) + rv = read_json_api(1800, f'offers/{offer_id_hex}') assert(rv[0]['addr_to'] == new_address) def test_02_leader_recover_a_lock_tx(self): logging.info('---------- Test PART to XMR leader recovers coin a lock tx') swap_clients = self.swap_clients - js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + js_w0_before = read_json_api(1800, 'wallets') offer_id = swap_clients[0].postOffer( Coins.PART, Coins.XMR, 101 * COIN, 0.12 * XMR_COIN, 101 * COIN, SwapTypes.XMR_SWAP, @@ -696,13 +710,13 @@ class Test(BaseTest): wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=180) wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True) - js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + js_w0_after = read_json_api(1800, 'wallets') def test_03_follower_recover_a_lock_tx(self): logging.info('---------- Test PART to XMR follower recovers coin a lock tx') swap_clients = self.swap_clients - js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + js_w0_before = read_json_api(1800, 'wallets') offer_id = swap_clients[0].postOffer( Coins.PART, Coins.XMR, 101 * COIN, 0.13 * XMR_COIN, 101 * COIN, SwapTypes.XMR_SWAP, @@ -725,7 +739,7 @@ class Test(BaseTest): wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_STALLED_FOR_TEST, wait_for=180) wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=80, sent=True) - js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + js_w0_after = read_json_api(1800, 'wallets') wait_for_none_active(test_delay_event, 1800) wait_for_none_active(test_delay_event, 1801) @@ -782,8 +796,8 @@ class Test(BaseTest): logging.info('---------- Test Multiple concurrent swaps') swap_clients = self.swap_clients - js_w0_before = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) - js_w1_before = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + js_w0_before = read_json_api(1800, 'wallets') + js_w1_before = read_json_api(1801, 'wallets') amt_1 = make_int(random.uniform(0.001, 49.0), scale=8, r=1) amt_2 = make_int(random.uniform(0.001, 49.0), scale=8, r=1) @@ -831,8 +845,8 @@ class Test(BaseTest): wait_for_none_active(test_delay_event, 1800) wait_for_none_active(test_delay_event, 1801) - js_w0_after = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) - js_w1_after = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + js_w0_after = read_json_api(1800, 'wallets') + js_w1_after = read_json_api(1801, 'wallets') assert(make_int(js_w1_after['2']['balance'], scale=8, r=1) - (make_int(js_w1_before['2']['balance'], scale=8, r=1) + amt_1) < 1000) def test_07_revoke_offer(self): @@ -848,10 +862,10 @@ class Test(BaseTest): def test_08_withdraw(self): logging.info('---------- Test XMR withdrawals') swap_clients = self.swap_clients - js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + js_0 = read_json_api(1800, 'wallets') address_to = js_0[str(int(Coins.XMR))]['deposit_address'] - js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets').read()) + js_1 = read_json_api(1801, 'wallets') assert(float(js_1[str(int(Coins.XMR))]['balance']) > 0.0) swap_clients[1].withdrawCoin(Coins.XMR, 1.1, address_to, False) @@ -961,12 +975,12 @@ class Test(BaseTest): logging.info('---------- Test Particl anon transactions') swap_clients = self.swap_clients - js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read()) + js_0 = read_json_api(1800, 'wallets/part') 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()) + js_1 = read_json_api(1801, 'wallets/part') assert(float(js_1['balance']) > 200.0) node1_anon_before = js_1['anon_balance'] + js_1['anon_pending'] @@ -982,7 +996,7 @@ class Test(BaseTest): logging.info('Waiting for anon balance') 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()) + js_1 = read_json_api(1801, 'wallets/part') node1_anon_before = js_1['anon_balance'] + js_1['anon_pending'] callnoderpc(1, 'reservebalance', [False]) @@ -1021,21 +1035,21 @@ class Test(BaseTest): wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) 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()) + js_1 = read_json_api(1801, 'wallets/part') 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()) + js_0 = read_json_api(1800, 'wallets/part') assert(js_0['anon_balance'] + js_0['anon_pending'] > node0_anon_before + (amount_to - 0.05)) def test_12_particl_blind(self): logging.info('---------- Test Particl blind transactions') swap_clients = self.swap_clients - js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read()) + js_0 = read_json_api(1800, 'wallets/part') node0_blind_before = js_0['blind_balance'] + js_0['blind_unconfirmed'] 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()) + js_1 = read_json_api(1801, 'wallets/part') assert(float(js_1['balance']) > 200.0) node1_blind_before = js_1['blind_balance'] + js_1['blind_unconfirmed'] @@ -1050,7 +1064,7 @@ class Test(BaseTest): logging.info('Waiting for blind balance') wait_for_balance(test_delay_event, 'http://127.0.0.1:1800/json/wallets/part', 'blind_balance', 100.0 + node0_blind_before) - js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read()) + js_0 = read_json_api(1800, 'wallets/part') node0_blind_before = js_0['blind_balance'] + js_0['blind_unconfirmed'] amt_swap = make_int(random.uniform(0.1, 2.0), scale=8, r=1) @@ -1070,11 +1084,11 @@ class Test(BaseTest): wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True) amount_from = float(format_amount(amt_swap, 8)) - js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/wallets/part').read()) + js_1 = read_json_api(1801, 'wallets/part') node1_blind_after = js_1['blind_balance'] + js_1['blind_unconfirmed'] assert(node1_blind_after > node1_blind_before + (amount_from - 0.05)) - js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets/part').read()) + js_0 = read_json_api(1800, 'wallets/part') node0_blind_after = js_0['blind_balance'] + js_0['blind_unconfirmed'] assert(node0_blind_after < node0_blind_before - amount_from) @@ -1084,10 +1098,10 @@ class Test(BaseTest): logging.info('Disabling XMR mining') pause_event.clear() - js_0 = json.loads(urlopen('http://127.0.0.1:1800/json/wallets').read()) + js_0 = read_json_api(1800, 'wallets') address_to = js_0[str(int(Coins.XMR))]['deposit_address'] - wallets1 = json.loads(urlopen('http://127.0.0.1:{}/json/wallets'.format(TEST_HTTP_PORT + 1)).read()) + wallets1 = read_json_api(TEST_HTTP_PORT + 1, 'wallets') xmr_total = float(wallets1[str(int(Coins.XMR))]['balance']) assert(xmr_total > 10)