From 23468581453768a324403d8e588558dbaa0a58d6 Mon Sep 17 00:00:00 2001 From: tecnovert Date: Sun, 6 Dec 2020 19:34:56 +0200 Subject: [PATCH] Call setLastHeightChecked() in watchXmrSwap() Remove old notes and config. New html template for XMR bids. Check the mempool for lock spend txid. Retry sepnding coin B lock tx. --- .cirrus.yml | 4 +- .travis.yml | 2 +- basicswap/basicswap.py | 162 +++++++++++++++++------ basicswap/db.py | 15 ++- basicswap/http_server.py | 2 +- basicswap/interface_btc.py | 6 + basicswap/interface_xmr.py | 3 + basicswap/templates/bid_xmr.html | 72 ++++++++++ basicswap/ui.py | 25 +++- bin/basicswap_prepare.py | 6 +- bin/basicswap_run.py | 5 - doc/docker.md | 62 --------- doc/install.md | 15 ++- doc/notes.md | 5 +- docker/coindata/.gitignore | 1 - docker/coindata/basicswap/basicswap.json | 30 ----- docker/coindata/litecoin/litecoin.conf | 3 - docker/coindata/particl/particl.conf | 16 --- docker/dockerbuild.bat | 3 - docker/dockerup.bat | 3 - tests/basicswap/test_xmr.py | 31 ++++- 21 files changed, 284 insertions(+), 187 deletions(-) create mode 100644 basicswap/templates/bid_xmr.html delete mode 100644 doc/docker.md delete mode 100644 docker/coindata/.gitignore delete mode 100644 docker/coindata/basicswap/basicswap.json delete mode 100644 docker/coindata/litecoin/litecoin.conf delete mode 100644 docker/coindata/particl/particl.conf delete mode 100755 docker/dockerbuild.bat delete mode 100755 docker/dockerup.bat diff --git a/.cirrus.yml b/.cirrus.yml index bf08d49..8667a06 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -6,6 +6,7 @@ lint_task: - pip install flake8 - pip install codespell script: + - flake8 --version - PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --exclude=basicswap/contrib,messages_pb2.py,.eggs - codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,gitianpubkeys,*.pyc,*basicswap/contrib,*mnemonics.py @@ -23,7 +24,8 @@ test_task: - LITECOIN_BINDIR: ${BIN_DIRS}/litecoin-${LTC_VERSION}/bin - MONERO_BINDIR: ${BIN_DIRS}/monero-${XMR_VERSION}/bin setup_script: - - sudo apt-get install -y wget python3-pip gnupg unzip protobuf-compiler automake libtool pkg-config + - apt-get update + - apt-get install -y wget python3-pip gnupg unzip protobuf-compiler automake libtool pkg-config - if [ ! -d "$BIN_DIRS" ]; then mkdir -p "$BIN_DIRS" ; fi - if [ ! -d "$PARTICL_BINDIR" ]; then cd "$BIN_DIRS" && wget https://github.com/tecnovert/particl-core/releases/download/v${PART_VERSION}/particl-${PART_VERSION}-x86_64-linux-gnu.tar.gz && tar xvf particl-${PART_VERSION}-x86_64-linux-gnu.tar.gz; fi - if [ ! -d "$BITCOIN_BINDIR" ]; then cd "$BIN_DIRS" && wget https://bitcoincore.org/bin/bitcoin-core-${BTC_VERSION}/bitcoin-${BTC_VERSION}-x86_64-linux-gnu.tar.gz && tar xvf bitcoin-${BTC_VERSION}-x86_64-linux-gnu.tar.gz; fi diff --git a/.travis.yml b/.travis.yml index 7c7707f..5410f7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,7 +49,7 @@ jobs: env: cache: false install: - - travis_retry pip install flake8==3.5.0 + - travis_retry pip install flake8==3.7.0 - travis_retry pip install codespell==1.15.0 before_script: script: diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index b8e4bdb..904236d 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -184,6 +184,9 @@ class EventTypes(IntEnum): class EventLogTypes(IntEnum): FAILED_TX_B_PUBLISH = auto() + LOCK_TX_A_PUBLISHED = auto() + LOCK_TX_B_PUBLISHED = auto() + FAILED_TX_B_SPEND = auto() class XmrSplitMsgTypes(IntEnum): @@ -278,6 +281,22 @@ def strTxState(state): return 'Unknown' +def strTxType(tx_type): + if tx_type == TxTypes.XMR_SWAP_A_LOCK: + return 'Chain A Lock Tx' + if tx_type == TxTypes.XMR_SWAP_A_LOCK_SPEND: + return 'Chain A Lock Spend Tx' + if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND: + return 'Chain A Lock Refund Tx' + if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SPEND: + return 'Chain A Lock Refund Spend Tx' + if tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND_SWIPE: + return 'Chain A Lock Refund Swipe Tx' + if tx_type == TxTypes.XMR_SWAP_B_LOCK: + return 'Chain B Lock Tx' + return 'Unknown' + + def getLockName(lock_type): if lock_type == SEQUENCE_LOCK_BLOCKS: return 'Sequence lock, blocks' @@ -417,8 +436,8 @@ class BasicSwap(BaseApp): self.min_delay_event = self.settings.get('min_delay_event', 10) self.max_delay_event = self.settings.get('max_delay_event', 60) - self.min_delay_retry = self.settings.get('min_delay_retry', 20) - self.max_delay_retry = self.settings.get('max_delay_retry', 120) + self.min_delay_retry = self.settings.get('min_delay_retry', 60) + self.max_delay_retry = self.settings.get('max_delay_retry', 5 * 60) self.swaps_in_progress = dict() @@ -1148,7 +1167,7 @@ class BasicSwap(BaseApp): self.mxDB.acquire() try: session = scoped_session(self.session_factory) - record = session.query(PooledAddress).filter(sa.and_(PooledAddress.coin_type == int(coin_type), PooledAddress.bid_id == None)).first() # noqa E712 + record = session.query(PooledAddress).filter(sa.and_(PooledAddress.coin_type == int(coin_type), PooledAddress.bid_id == None)).first() # noqa: E712,E711 if not record: address = self.getReceiveAddressForCoin(coin_type) record = PooledAddress( @@ -1526,10 +1545,15 @@ class BasicSwap(BaseApp): try: session = scoped_session(self.session_factory) bid = session.query(Bid).filter_by(bid_id=bid_id).first() + offer = None if bid: - 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() - return bid, session.query(Offer).filter_by(offer_id=bid.offer_id).first() if bid is not None else None + 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() + return bid, offer finally: session.close() session.remove() @@ -1666,7 +1690,8 @@ class BasicSwap(BaseApp): xmr_swap = XmrSwap() xmr_swap.contract_count = self.getNewContractId() xmr_swap.dest_af = msg_buf.dest_af - xmr_swap.b_restore_height = self.ci(coin_to).getChainHeight() + xmr_swap.start_chain_a_height = ci_from.getChainHeight() + xmr_swap.b_restore_height = ci_to.getChainHeight() kbvf = self.getPathKey(coin_from, coin_to, bid_created_at, xmr_swap.contract_count, 1, for_ed25519=True) kbsf = self.getPathKey(coin_from, coin_to, bid_created_at, xmr_swap.contract_count, 2, for_ed25519=True) @@ -2492,33 +2517,33 @@ class BasicSwap(BaseApp): bid.setState(BidStates.XMR_SWAP_FAILED_REFUNDED) self.saveBidInSession(bid_id, bid, session, xmr_swap) session.commit() - return rv - - if len(xmr_swap.al_lock_refund_tx_sig) > 0 and len(xmr_swap.af_lock_refund_tx_sig) > 0: - try: - txid = ci_from.publishTx(xmr_swap.a_lock_refund_tx) - - self.log.info('Submitted coin a lock refund tx for bid {}'.format(bid_id.hex())) - bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx( - bid_id=bid_id, - tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, - txid=bytes.fromhex(txid), - ) - self.saveBidInSession(bid_id, bid, session, xmr_swap) - session.commit() return rv - except Exception as ex: - if 'Transaction already in block chain' in str(ex): - self.log.info('Found coin a lock refund tx for bid {}'.format(bid_id.hex())) - txid = ci_from.getTxHash(xmr_swap.a_lock_refund_tx) + else: # not XMR_SWAP_A_LOCK_REFUND in bid.txns + if len(xmr_swap.al_lock_refund_tx_sig) > 0 and len(xmr_swap.af_lock_refund_tx_sig) > 0: + try: + txid = ci_from.publishTx(xmr_swap.a_lock_refund_tx) + + self.log.info('Submitted coin a lock refund tx for bid {}'.format(bid_id.hex())) bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx( bid_id=bid_id, tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, - txid=txid, + txid=bytes.fromhex(txid), ) self.saveBidInSession(bid_id, bid, session, xmr_swap) session.commit() return rv + except Exception as ex: + if 'Transaction already in block chain' in str(ex): + self.log.info('Found coin a lock refund tx for bid {}'.format(bid_id.hex())) + txid = ci_from.getTxHash(xmr_swap.a_lock_refund_tx) + bid.txns[TxTypes.XMR_SWAP_A_LOCK_REFUND] = SwapTx( + bid_id=bid_id, + tx_type=TxTypes.XMR_SWAP_A_LOCK_REFUND, + txid=txid, + ) + self.saveBidInSession(bid_id, bid, session, xmr_swap) + session.commit() + return rv state = BidStates(bid.state) if state == BidStates.SWAP_COMPLETED: @@ -2585,7 +2610,14 @@ class BasicSwap(BaseApp): elif state == BidStates.XMR_SWAP_SECRET_SHARED: # Wait for script spend tx to confirm # TODO: Use explorer to get tx / block hash for getrawtransaction - pass + + if bid.was_received: + try: + txn_hex = ci_from.getMempoolTx(xmr_swap.a_lock_spend_tx_id) + self.log.info('Found lock spend txn in %s mempool, %s', ci_from.coin_name(), xmr_swap.a_lock_spend_tx_id.hex()) + self.process_XMR_SWAP_A_LOCK_tx_spend(bid_id, xmr_swap.a_lock_spend_tx_id.hex(), txn_hex) + except Exception as e: + self.log.debug('getrawtransaction lock spend tx failed: %s', str(e)) elif state == BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED: txid_hex = bid.xmr_b_lock_tx.spend_txid.hex() @@ -2604,6 +2636,7 @@ class BasicSwap(BaseApp): session.close() session.remove() self.mxDB.release() + return rv def checkBidState(self, bid_id, bid, offer): @@ -2858,7 +2891,7 @@ class BasicSwap(BaseApp): self.removeWatchedOutput(coin_to, bid_id, bid.participate_tx.txid.hex()) self.saveBid(bid_id, bid) - def process_XMR_SWAP_A_LOCK_tx_spend(self, bid_id, spend_txid_hex, spend_txn): + def process_XMR_SWAP_A_LOCK_tx_spend(self, bid_id, spend_txid_hex, spend_txn_hex): self.log.debug('Detected spend of XMR swap coin a lock tx for bid %s', bid_id.hex()) self.mxDB.acquire() try: @@ -2877,15 +2910,14 @@ class BasicSwap(BaseApp): spending_txid = bytes.fromhex(spend_txid_hex) if spending_txid == xmr_swap.a_lock_spend_tx_id: - self.log.debug('[rm] state %d', state) - self.log.debug('[rm] BidStates.XMR_SWAP_SECRET_SHARED %d', BidStates.XMR_SWAP_SECRET_SHARED) if state == BidStates.XMR_SWAP_SECRET_SHARED: - xmr_swap.a_lock_spend_tx = bytes.fromhex(spend_txn['hex']) + xmr_swap.a_lock_spend_tx = bytes.fromhex(spend_txn_hex) bid.setState(BidStates.XMR_SWAP_SCRIPT_TX_REDEEMED) # TODO: Wait for confirmation? if not bid.was_received: bid.setState(BidStates.SWAP_COMPLETED) if bid.was_received: + bid.setState(BidStates.SWAP_DELAYING) delay = random.randrange(self.min_delay_event, self.max_delay_event) self.log.info('Redeeming coin b lock tx for bid %s in %d seconds', bid_id.hex(), delay) self.createEventInSession(delay, EventTypes.REDEEM_XMR_SWAP_LOCK_TX_B, bid_id, session) @@ -2968,7 +3000,7 @@ class BasicSwap(BaseApp): def processSpentOutput(self, coin_type, watched_output, spend_txid_hex, spend_n, spend_txn): if watched_output.swap_type == SwapTypes.XMR_SWAP: if watched_output.tx_type == TxTypes.XMR_SWAP_A_LOCK: - self.process_XMR_SWAP_A_LOCK_tx_spend(watched_output.bid_id, spend_txid_hex, spend_txn) + self.process_XMR_SWAP_A_LOCK_tx_spend(watched_output.bid_id, spend_txid_hex, spend_txn['hex']) elif watched_output.tx_type == TxTypes.XMR_SWAP_A_LOCK_REFUND: self.process_XMR_SWAP_A_LOCK_REFUND_tx_spend(watched_output.bid_id, spend_txid_hex, spend_txn) @@ -3005,7 +3037,15 @@ class BasicSwap(BaseApp): self.log.debug('chain_blocks, last_height_checked %s %s', chain_blocks, last_height_checked) while last_height_checked < chain_blocks: block_hash = self.callcoinrpc(coin_type, 'getblockhash', [last_height_checked + 1]) - block = self.callcoinrpc(coin_type, 'getblock', [block_hash, 2]) + try: + block = self.callcoinrpc(coin_type, 'getblock', [block_hash, 2]) + except Exception as e: + if 'Block not available (pruned data)' in str(e): + # TODO: Better solution? + bci = self.callcoinrpc(coin_type, 'getblockchaininfo') + self.log.error('Coin %s last_height_checked %d set to pruneheight %d', self.ci(coin_type).coin_name(), last_height_checked, bci['pruneheight']) + last_height_checked = bci['pruneheight'] + continue for tx in block['tx']: for i, inp in enumerate(tx['vin']): @@ -3232,6 +3272,11 @@ class BasicSwap(BaseApp): try: session = scoped_session(self.session_factory) + if len(msg_data.offer_msg_id) != 28: + raise ValueError('Invalid msg_id length') + + # TODO: Add to db in case received out of order, or add extra startup logic + offer = session.query(Offer).filter_by(offer_id=msg_data.offer_msg_id).first() if offer is None: raise ValueError('Offer not found: {}'.format(msg_data.offer_msg_id.hex())) @@ -3245,7 +3290,7 @@ class BasicSwap(BaseApp): signature_enc = base64.b64encode(msg_data.signature).decode('utf-8') passed = self.callcoinrpc(Coins.PART, 'verifymessage', [offer.addr_from, signature_enc, msg_data.offer_msg_id.hex() + '_revoke']) - assert(passed is True), 'Proof of funds signature invalid' + assert(passed is True), 'Signature invalid' offer.active_ind = 2 # TODO: Remove message, or wait for expire @@ -3558,6 +3603,7 @@ class BasicSwap(BaseApp): pkbvf=ci_to.getPubkey(bid_data.kbvf), kbsf_dleag=bid_data.kbsf_dleag, b_restore_height=ci_to.getChainHeight(), + start_chain_a_height=ci_from.getChainHeight(), ) else: bid.created_at = msg['sent'] @@ -3650,8 +3696,11 @@ class BasicSwap(BaseApp): def watchXmrSwap(self, bid, offer, xmr_swap): self.log.debug('XMR swap in progress, bid %s', bid.bid_id.hex()) self.swaps_in_progress[bid.bid_id] = (bid, offer) - self.addWatchedOutput(Coins(offer.coin_from), bid.bid_id, bid.xmr_a_lock_tx.txid.hex(), bid.xmr_a_lock_tx.vout, TxTypes.XMR_SWAP_A_LOCK, SwapTypes.XMR_SWAP) - self.addWatchedOutput(Coins(offer.coin_from), bid.bid_id, xmr_swap.a_lock_refund_tx_id.hex(), 0, TxTypes.XMR_SWAP_A_LOCK_REFUND, SwapTypes.XMR_SWAP) + + coin_from = Coins(offer.coin_from) + self.setLastHeightChecked(coin_from, xmr_swap.start_chain_a_height) + self.addWatchedOutput(coin_from, bid.bid_id, bid.xmr_a_lock_tx.txid.hex(), bid.xmr_a_lock_tx.vout, TxTypes.XMR_SWAP_A_LOCK, SwapTypes.XMR_SWAP) + self.addWatchedOutput(coin_from, bid.bid_id, xmr_swap.a_lock_refund_tx_id.hex(), 0, TxTypes.XMR_SWAP_A_LOCK_REFUND, SwapTypes.XMR_SWAP) bid.in_progress = 1 def sendXmrBidTxnSigsFtoL(self, bid_id, session): @@ -3938,7 +3987,28 @@ class BasicSwap(BaseApp): address_to = ci_to.getMainWalletAddress() - txid = ci_to.spendBLockTx(address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, xmr_swap.b_restore_height) + try: + txid = ci_to.spendBLockTx(address_to, xmr_swap.vkbv, vkbs, bid.amount_to, xmr_offer.b_fee_rate, xmr_swap.b_restore_height) + self.log.debug('Submitted lock B spend txn %s to %s chain for bid %s', txid.hex(), ci_to.coin_name(), bid_id.hex()) + except Exception as ex: + # TODO: Make min-conf 10? + error_msg = 'spendBLockTx failed for bid {} with error {}'.format(bid_id.hex(), str(ex)) + num_retries = self.countBidEvents(bid, EventLogTypes.FAILED_TX_B_SPEND, session) + if num_retries > 0: + error_msg += ', retry no. {}'.format(num_retries) + self.log.error(error_msg) + + str_error = str(ex) + if num_retries < 100 and 'Invalid unlocked_balance' in str_error: + delay = random.randrange(self.min_delay_retry, self.max_delay_retry) + self.log.info('Retrying sending xmr swap chain B spend tx for bid %s in %d seconds', bid_id.hex(), delay) + self.createEventInSession(delay, EventTypes.REDEEM_XMR_SWAP_LOCK_TX_B, bid_id, session) + else: + self.setBidError(bid_id, bid, 'spendBLockTx failed: ' + str(ex), save_bid=False) + self.saveBidInSession(bid_id, bid, session, xmr_swap, save_in_progress=offer) + + self.logBidEvent(bid, EventLogTypes.FAILED_TX_B_SPEND, str_error, session) + return bid.xmr_b_lock_tx.spend_txid = txid bid.setState(BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED) @@ -4176,7 +4246,17 @@ class BasicSwap(BaseApp): msg_id = message[2:] options = {'encoding': 'hex', 'setread': True} - msg = self.callrpc('smsg', [msg_id.hex(), options]) + num_tries = 5 + for i in range(num_tries + 1): + try: + msg = self.callrpc('smsg', [msg_id.hex(), options]) + break + except Exception as e: + if 'Unknown message id' in str(e) and i < num_tries: + time.sleep(1) + else: + raise e + self.processMsg(msg) def update(self): @@ -4249,7 +4329,6 @@ class BasicSwap(BaseApp): if bid.state != data['bid_state']: bid.setState(data['bid_state']) self.log.debug('Set state to %s', strBidState(bid.state)) - self.log.debug('[rm] Set bid.state %d', bid.state) has_changed = True if has_changed: @@ -4262,7 +4341,7 @@ class BasicSwap(BaseApp): else: self.log.debug('TODO - determine in-progress for manualBidUpdate') if offer.swap_type == SwapTypes.XMR_SWAP: - if bid.state and bid.state == BidStates.XMR_SWAP_SECRET_SHARED: + if bid.state and bid.state in (BidStates.XMR_SWAP_SECRET_SHARED, BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED): activate_bid = True if activate_bid: @@ -4270,7 +4349,6 @@ class BasicSwap(BaseApp): else: self.deactivateBid(session, offer, bid) - self.log.debug('[rm] bid.state %d', bid.state) self.saveBidInSession(bid_id, bid, session) session.commit() finally: @@ -4381,7 +4459,7 @@ class BasicSwap(BaseApp): session = scoped_session(self.session_factory) if sent: - q = session.query(Offer).filter(Offer.was_sent == True) # noqa E712 + q = session.query(Offer).filter(Offer.was_sent == True) # noqa: E712 else: q = session.query(Offer).filter(sa.and_(Offer.expire_at > now, Offer.active_ind == 1)) diff --git a/basicswap/db.py b/basicswap/db.py index 782cb9f..093a7d7 100644 --- a/basicswap/db.py +++ b/basicswap/db.py @@ -12,7 +12,7 @@ from sqlalchemy.ext.declarative import declarative_base from enum import IntEnum, auto -CURRENT_DB_VERSION = 3 +CURRENT_DB_VERSION = 4 Base = declarative_base() @@ -317,7 +317,8 @@ class XmrSwap(Base): b_lock_tx_id = sa.Column(sa.LargeBinary) - b_restore_height = sa.Column(sa.Integer) # Height of xmr chain before the swap + start_chain_a_height = sa.Column(sa.Integer) # Height of script chain before the swap + b_restore_height = sa.Column(sa.Integer) # Height of scriptless chain before the swap class XmrSplitData(Base): @@ -331,6 +332,16 @@ class XmrSplitData(Base): created_at = sa.Column(sa.BigInteger) +class RevokedMessage(Base): + __tablename__ = 'revoked_messages' + + record_id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) + active_ind = sa.Column(sa.Integer) + msg_id = sa.Column(sa.LargeBinary) + created_at = sa.Column(sa.BigInteger) + expires_at = sa.Column(sa.BigInteger) + + class TableTypes(IntEnum): OFFER = auto() BID = auto() diff --git a/basicswap/http_server.py b/basicswap/http_server.py index ddfe416..9d26395 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -558,7 +558,7 @@ class HttpHandler(BaseHTTPRequestHandler): if len(old_states) > 0: old_states.sort(key=lambda x: x[0]) - template = env.get_template('bid.html') + template = env.get_template('bid_xmr.html') if offer.swap_type == SwapTypes.XMR_SWAP else env.get_template('bid.html') return bytes(template.render( title=self.server.title, h2=self.server.title, diff --git a/basicswap/interface_btc.py b/basicswap/interface_btc.py index b7554a8..5f655db 100644 --- a/basicswap/interface_btc.py +++ b/basicswap/interface_btc.py @@ -134,6 +134,12 @@ class BTCInterface(CoinInterface): def getBlockchainInfo(self): return self.rpc_callback('getblockchaininfo') + def getChainHeight(self): + return self.rpc_callback('getblockchaininfo')['blocks'] + + def getMempoolTx(self, txid): + return self.rpc_callback('getrawtransaction', [txid.hex()]) + def initialiseWallet(self, key_bytes): wif_prefix = chainparams[self.coin_type()][self._network]['key_prefix'] key_wif = toWIF(wif_prefix, key_bytes) diff --git a/basicswap/interface_xmr.py b/basicswap/interface_xmr.py index 980bfe9..2f0f94b 100644 --- a/basicswap/interface_xmr.py +++ b/basicswap/interface_xmr.py @@ -372,6 +372,9 @@ class XMRInterface(CoinInterface): if rv['balance'] < cb_swap_value: logging.error('wallet {} balance {}, expected {}'.format(wallet_filename, rv['balance'], cb_swap_value)) raise ValueError('Invalid balance') + if rv['unlocked_balance'] < cb_swap_value: + logging.error('wallet {} balance {}, expected {}, blocks_to_unlock {}'.format(wallet_filename, rv['unlocked_balance'], cb_swap_value, rv['blocks_to_unlock'])) + raise ValueError('Invalid unlocked_balance') params = {'address': address_to} rv = self.rpc_wallet_cb('sweep_all', params) diff --git a/basicswap/templates/bid_xmr.html b/basicswap/templates/bid_xmr.html new file mode 100644 index 0000000..d486cd7 --- /dev/null +++ b/basicswap/templates/bid_xmr.html @@ -0,0 +1,72 @@ +{% include 'header.html' %} + +

Bid {{ bid_id }}

+{% if refresh %} +

Page Refresh: {{ refresh }} seconds

+{% endif %} + +{% for m in messages %} +

{{ m }}

+{% endfor %} + + + + + + + + + + + +
Swap{{ data.amt_from }} {{ data.ticker_from }} for {{ data.amt_to }} {{ data.ticker_to }}
Bid State{{ data.bid_state }}
StateDescription {{ data.state_description }}
Offer{{ data.offer_id }}
Address From{{ data.addr_from }}
Created At{{ data.created_at }}
Expired At{{ data.expired_at }}
Sent{{ data.was_sent }}
Received{{ data.was_received }}
+ +{% if data.show_txns %} +

Transactions

+ + +{% for tx in data.txns %} + +{% endfor %} +
Tx TypeTx ID
{{ tx.type }}{{ tx.txid }}
+{% endif %} + +
+{% if edit_bid %} +

Edit Bid

+ + +
Change Bid State +
+ + +{% else %} +{% if data.was_received == 'True' %} +
+{% endif %} + +{% if data.show_txns %} + +{% else %} + +{% endif %} + +{% endif %} + +
+ + +

Old States

+ + +{% for s in old_states %} + +{% endfor %} +
StateSet At
{{ s[1] }}{{ s[0] | formatts }}
+ +

home

+ diff --git a/basicswap/ui.py b/basicswap/ui.py index e86bbc5..eeb2100 100644 --- a/basicswap/ui.py +++ b/basicswap/ui.py @@ -13,9 +13,11 @@ from .chainparams import ( Coins, ) from .basicswap import ( + SwapTypes, BidStates, TxStates, TxTypes, + strTxType, strBidState, strTxState, ) @@ -147,9 +149,24 @@ def describeBid(swap_client, bid, offer, edit_bid, show_txns): data['bid_states'] = listBidStates() if show_txns: - data['initiate_tx_refund'] = 'None' if not bid.initiate_txn_refund else bid.initiate_txn_refund.hex() - data['participate_tx_refund'] = 'None' if not bid.participate_txn_refund else bid.participate_txn_refund.hex() - data['initiate_tx_spend'] = getTxSpendHex(bid, TxTypes.ITX) - data['participate_tx_spend'] = getTxSpendHex(bid, TxTypes.PTX) + if offer.swap_type == SwapTypes.XMR_SWAP: + txns = [] + if bid.xmr_a_lock_tx: + txns.append({'type': 'Chain A Lock', 'txid': bid.xmr_a_lock_tx.txid.hex()}) + if bid.xmr_a_lock_spend_tx: + txns.append({'type': 'Chain A Lock Spend', 'txid': bid.xmr_a_lock_spend_tx.txid.hex()}) + if bid.xmr_b_lock_tx: + txns.append({'type': 'Chain B Lock', 'txid': bid.xmr_b_lock_tx.txid.hex()}) + if bid.xmr_b_lock_tx and bid.xmr_b_lock_tx.spend_txid: + txns.append({'type': 'Chain B Lock Spend', 'txid': bid.xmr_b_lock_tx.spend_txid.hex()}) + + for tx_type, tx in bid.txns.items(): + txns.append({'type': strTxType(tx_type), 'txid': tx.txid.hex()}) + data['txns'] = txns + else: + data['initiate_tx_refund'] = 'None' if not bid.initiate_txn_refund else bid.initiate_txn_refund.hex() + data['participate_tx_refund'] = 'None' if not bid.participate_txn_refund else bid.participate_txn_refund.hex() + data['initiate_tx_spend'] = getTxSpendHex(bid, TxTypes.ITX) + data['participate_tx_spend'] = getTxSpendHex(bid, TxTypes.PTX) return data diff --git a/bin/basicswap_prepare.py b/bin/basicswap_prepare.py index eebd41a..798e966 100755 --- a/bin/basicswap_prepare.py +++ b/bin/basicswap_prepare.py @@ -5,11 +5,6 @@ # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. -""" -Atomic Swap Client - Proof of Concept - -""" - import os import sys import json @@ -548,6 +543,7 @@ def main(): 'datadir': os.path.join(data_dir, 'monero'), 'bindir': os.path.join(bin_dir, 'monero'), 'restore_height': xmr_restore_height, + 'blocks_confirmed': 7, # TODO: 10? } } diff --git a/bin/basicswap_run.py b/bin/basicswap_run.py index 476ba13..125ef1a 100755 --- a/bin/basicswap_run.py +++ b/bin/basicswap_run.py @@ -5,11 +5,6 @@ # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. -""" -Atomic Swap Client - Proof of Concept - -""" - import os import sys import json diff --git a/doc/docker.md b/doc/docker.md deleted file mode 100644 index 2ac542a..0000000 --- a/doc/docker.md +++ /dev/null @@ -1,62 +0,0 @@ - -# Docker Setup - -## Build the images -``` -$ docker-compose build -``` - -## Prepare the binaries, coin dirs and settings -``` -$ docker run -t --name swap_prepare -v /tmp/coindata:/coindata i_swapclient basicswap-prepare --datadir=/coindata --withcoins=monero --withoutcoins=litecoin -``` - -Record the mnemonic from the output of the above command. - -Remove swap_prepare container (and logs): -``` -$ docker rm swap_prepare -``` - - -# Running on windows 10 - - -Work in progress - doesn't work reliably. - - -Install the latest docker toolbox from: -https://github.com/docker/toolbox/releases - -Start docker through the desktop icon, it should open a terminal - -Download basicswap -https://github.com/tecnovert/basicswap/archive/master.zip - -Extract it. - -Navigate to the docker folder. - - -If you have an existing litecoin chain, copy the contents of your datadir excluding litecoin.conf and any wallets to coindata/litecoin -If your litecoin chain is pruned create a new wallet in the existing datadir to avoid having to resync the chain. - - -Right click -> properties on the coindata folder, in the security tab make sure all users have 'Modify' rights. - - -Run the script: dockerbuild.bat - -It should open a new terminal window and start building the container. - -Once that completes run: dockerup.bat - -In the terminal that opened for docker toolbox, find the line: -docker is configured to use the default machine with IP 192.168.99.100 - -And open the ip address it displays at port 12700 in a browser: -192.168.99.100:12700 - -Should show some html. - -Now go to the view wallets page, and wait for all chains to completely sync. diff --git a/doc/install.md b/doc/install.md index c5502cd..ce0b050 100644 --- a/doc/install.md +++ b/doc/install.md @@ -7,6 +7,7 @@ ## Run Using Docker Docker must be installed and started: + $ sudo systemctl status docker | grep Active Should return a line containing `active (running)` @@ -20,8 +21,8 @@ Create the images: Prepare the datadir: Set XMR_RPC_HOST and BASE_XMR_RPC_PORT to a public XMR node or exclude to run a local node. - $ export SWAP_DATADIR=/var/data/coinswaps - $ docker run -e XMR_RPC_HOST="node.xmr.to" -e BASE_XMR_RPC_PORT=18081 -t --name swap_prepare -v $SWAP_DATADIR:/coindata i_swapclient \ + $ export COINDATA_PATH=/var/data/coinswaps + $ docker run -e XMR_RPC_HOST="node.xmr.to" -e BASE_XMR_RPC_PORT=18081 -t --name swap_prepare -v $COINDATA_PATH:/coindata i_swapclient \ basicswap-prepare --datadir=/coindata --withcoins=monero --withoutcoins=litecoin --htmlhost="0.0.0.0" --xmrrestoreheight=2245107 Record the mnemonic from the output of the above command. @@ -33,11 +34,19 @@ Remove swap_prepare container (and logs): Start the container - $ export SWAP_DATADIR=/var/data/coinswaps + $ export COINDATA_PATH=/var/data/coinswaps $ docker-compose up Open in browser: `http://localhost:12700` +### Add a coin + + $ docker-compose stop + $ export COINDATA_PATH=/var/data/coinswaps + $ docker run -t --name swap_prepare -v $COINDATA_PATH:/coindata i_swapclient basicswap-prepare --datadir=/coindata --addcoin=bitcoin + +You can copy an existing pruned datadir (excluding bitcoin.conf and any wallets) over to `$COINDATA_PATH/bitcoin` + ## Run Without Docker: diff --git a/doc/notes.md b/doc/notes.md index 7b0f4c2..ff0ab1c 100644 --- a/doc/notes.md +++ b/doc/notes.md @@ -11,9 +11,8 @@ Features still required (of many): - Option to lookup data from public explorers / nodes. - Ability to swap coin-types without running nodes for all coin-types - More swap protocols - - Method to load mnemonic into Particl. - - Load seeds for other wallets from same mnemonic. - - COIN must be defined per coin. + - Manual method to set wallet seeds from particl mnemonic + - prepare script tries to load seeds automatically, btc versions < 0.21 require a fully synced chain ## Seller first protocol: diff --git a/docker/coindata/.gitignore b/docker/coindata/.gitignore deleted file mode 100644 index 72e8ffc..0000000 --- a/docker/coindata/.gitignore +++ /dev/null @@ -1 +0,0 @@ -* diff --git a/docker/coindata/basicswap/basicswap.json b/docker/coindata/basicswap/basicswap.json deleted file mode 100644 index 32efb6e..0000000 --- a/docker/coindata/basicswap/basicswap.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "zmqhost": "tcp://127.0.0.1", - "zmqport": 20792, - "htmlhost": "0.0.0.0", - "htmlport": 12700, - "network_key": "7sW2UEcHXvuqEjkpE5mD584zRaQYs6WXYohue4jLFZPTvMSxwvgs", - "network_pubkey": "035758c4a22d7dd59165db02a56156e790224361eb3191f02197addcb3bde903d2", - "chainclients": { - "particl": { - "connection_type": "rpc", - "manage_daemon": true, - "rpcport": 19792, - "datadir": "/coindata/particl", - "bindir": "/opt/bin/particl", - "blocks_confirmed": 2 - }, - "litecoin": { - "connection_type": "rpc", - "manage_daemon": true, - "rpcport": 19795, - "datadir": "/coindata/litecoin", - "bindir": "/opt/bin/litecoin", - "use_segwit": true, - "blocks_confirmed": 2 - } - }, - "check_progress_seconds": 60, - "check_watched_seconds": 60, - "check_expired_seconds": 60 -} diff --git a/docker/coindata/litecoin/litecoin.conf b/docker/coindata/litecoin/litecoin.conf deleted file mode 100644 index 9c2375c..0000000 --- a/docker/coindata/litecoin/litecoin.conf +++ /dev/null @@ -1,3 +0,0 @@ -prune=1000 -rpcport=19795 -printtoconsole=0 diff --git a/docker/coindata/particl/particl.conf b/docker/coindata/particl/particl.conf deleted file mode 100644 index ad4451f..0000000 --- a/docker/coindata/particl/particl.conf +++ /dev/null @@ -1,16 +0,0 @@ - -[main] -port=14792 -rpcport=19792 -daemon=0 -printtoconsole=0 -server=1 -#debug=1 -debugexclude=libevent -zmqpubsmsg=tcp://127.0.0.1:20792 -acceptnonstdtxn=0 -spentindex=1 -txindex=1 - -# Load a random initial wallet master key -createdefaultmasterkey=1 diff --git a/docker/dockerbuild.bat b/docker/dockerbuild.bat deleted file mode 100755 index e839514..0000000 --- a/docker/dockerbuild.bat +++ /dev/null @@ -1,3 +0,0 @@ -set HTML_PORT=12700:12700 -docker-compose build -pause diff --git a/docker/dockerup.bat b/docker/dockerup.bat deleted file mode 100755 index 77f6778..0000000 --- a/docker/dockerup.bat +++ /dev/null @@ -1,3 +0,0 @@ -set HTML_PORT=12700:12700 -docker-compose up -pause diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index 824b71c..e532b4a 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -538,6 +538,16 @@ class Test(unittest.TestCase): return raise ValueError('wait_for_bid timed out.') + def wait_for_none_active(self, port, wait_for=30): + for i in range(wait_for): + if stop_test: + raise ValueError('Test stopped.') + time.sleep(1) + js = json.loads(urlopen('http://localhost:{}/json'.format(port)).read()) + if js['num_swapping'] == 0 and js['num_watched_outputs'] == 0: + return + raise ValueError('wait_for_none_active timed out.') + def delay_for(self, delay_for=60): logging.info('Delaying for {} seconds.'.format(delay_for)) delay_event.clear() @@ -632,6 +642,9 @@ class Test(unittest.TestCase): js_w0_after = json.loads(urlopen('http://localhost:1800/json/wallets').read()) + self.wait_for_none_active(1800) + self.wait_for_none_active(1801) + def test_04_follower_recover_b_lock_tx(self): logging.info('---------- Test PART to XMR follower recovers coin b lock tx') @@ -709,12 +722,15 @@ class Test(unittest.TestCase): self.wait_for_bid(swap_clients[0], bid1_id, BidStates.SWAP_COMPLETED, wait_for=180) self.wait_for_bid(swap_clients[1], bid1_id, BidStates.SWAP_COMPLETED, sent=True) - self.wait_for_bid(swap_clients[0], bid2_id, BidStates.SWAP_COMPLETED, wait_for=60) + self.wait_for_bid(swap_clients[0], bid2_id, BidStates.SWAP_COMPLETED, wait_for=120) self.wait_for_bid(swap_clients[1], bid2_id, BidStates.SWAP_COMPLETED, sent=True) - self.wait_for_bid(swap_clients[0], bid3_id, BidStates.SWAP_COMPLETED, wait_for=60) + self.wait_for_bid(swap_clients[0], bid3_id, BidStates.SWAP_COMPLETED, wait_for=120) self.wait_for_bid(swap_clients[1], bid3_id, BidStates.SWAP_COMPLETED, sent=True) + self.wait_for_none_active(1800) + self.wait_for_none_active(1801) + def test_07_revoke_offer(self): logging.info('---------- Test offer revocaction') swap_clients = self.swap_clients @@ -733,6 +749,17 @@ class Test(unittest.TestCase): swap_clients[1].withdrawCoin(Coins.XMR, 1.1, address_to, False) + def test_09_auto_accept(self): + logging.info('---------- Test BTC to XMR auto accept') + swap_clients = self.swap_clients + offer_id = swap_clients[0].postOffer(Coins.BTC, Coins.XMR, 11 * COIN, 101 * XMR_COIN, 10 * COIN, SwapTypes.XMR_SWAP, auto_accept_bids=True) + self.wait_for_offer(swap_clients[1], offer_id) + offer = swap_clients[1].listOffers(filters={'offer_id': offer_id})[0] + + bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from) + self.wait_for_bid(swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=180) + self.wait_for_bid(swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True) + if __name__ == '__main__': unittest.main()