From d6535dbc1d4021abcdc8785e6fb2884b9a7f487d Mon Sep 17 00:00:00 2001 From: tecnovert Date: Thu, 7 Mar 2024 14:35:50 +0200 Subject: [PATCH] Set bid and offer states when expired. --- basicswap/basicswap.py | 104 +++++++++++++++++++++++++++++++++--- basicswap/basicswap_util.py | 11 ++-- basicswap/db.py | 21 +++++--- 3 files changed, 118 insertions(+), 18 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 008ef19..b9a0f54 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -104,6 +104,7 @@ from .db import ( AutomationLink, AutomationStrategy, MessageLink, + pack_state, ) from .db_upgrades import upgradeDatabase, upgradeDatabaseData from .base import BaseApp @@ -230,26 +231,30 @@ class BasicSwap(BaseApp): self._version = struct.pack('>HHH', int(v[0]), int(v[1]), int(v[2])) self._transient_instance = transient_instance - self.check_progress_seconds = self.get_int_setting('check_progress_seconds', 60, 1, 10 * 60) - self.check_watched_seconds = self.get_int_setting('check_watched_seconds', 60, 1, 10 * 60) - self.check_expired_seconds = self.get_int_setting('check_expired_seconds', 5 * 60, 1, 10 * 60) self.check_actions_seconds = self.get_int_setting('check_actions_seconds', 10, 1, 10 * 60) + self.check_expired_seconds = self.get_int_setting('check_expired_seconds', 5 * 60, 1, 10 * 60) # Expire DB records and smsg messages + self.check_expiring_bids_offers_seconds = self.get_int_setting('check_expiring_bids_offers_seconds', 60, 1, 10 * 60) # Set offer and bid states to expired + self.check_progress_seconds = self.get_int_setting('check_progress_seconds', 60, 1, 10 * 60) + self.check_smsg_seconds = self.get_int_setting('check_smsg_seconds', 10, 1, 10 * 60) + self.check_watched_seconds = self.get_int_setting('check_watched_seconds', 60, 1, 10 * 60) self.check_xmr_swaps_seconds = self.get_int_setting('check_xmr_swaps_seconds', 20, 1, 10 * 60) self.startup_tries = self.get_int_setting('startup_tries', 21, 1, 100) # Seconds waited for will be (x(1 + x+1) / 2 self.debug_ui = self.settings.get('debug_ui', False) self._debug_cases = [] - self._last_checked_progress = 0 - self._last_checked_watched = 0 - self._last_checked_expired = 0 self._last_checked_actions = 0 + self._last_checked_expired = 0 + self._last_checked_expiring_bids_offers = 0 + self._last_checked_progress = 0 + self._last_checked_smsg = 0 + self._last_checked_watched = 0 self._last_checked_xmr_swaps = 0 self._possibly_revoked_offers = collections.deque([], maxlen=48) # TODO: improve + self._expiring_bids = [] # List of bids expiring soon + self._expiring_offers = [] # List of offers expiring soon self._updating_wallets_info = {} self._last_updated_wallets_info = 0 self._zmq_queue_enabled = self.settings.get('zmq_queue_enabled', True) self._poll_smsg = self.settings.get('poll_smsg', False) - self.check_smsg_seconds = self.get_int_setting('check_smsg_seconds', 10, 1, 10 * 60) - self._last_checked_smsg = 0 self._notifications_enabled = self.settings.get('notifications_enabled', True) self._disabled_notification_types = self.settings.get('disabled_notification_types', []) @@ -6173,7 +6178,88 @@ class BasicSwap(BaseApp): self.processMsg(msg) + def expireBidsAndOffers(self, now) -> None: + bids_to_expire = set() + offers_to_expire = set() + check_records: bool = False + + for i, (bid_id, expired_at) in enumerate(self._expiring_bids): + if expired_at <= now: + bids_to_expire.add(bid_id) + self._expiring_bids.pop(i) + for i, (offer_id, expired_at) in enumerate(self._expiring_offers): + if expired_at <= now: + offers_to_expire.add(offer_id) + self._expiring_offers.pop(i) + + if now - self._last_checked_expiring_bids_offers >= self.check_expiring_bids_offers_seconds: + check_records = True + self._last_checked_expiring_bids = now + + if len(bids_to_expire) == 0 and len(offers_to_expire) == 0 and check_records is False: + return + + bids_expired: int = 0 + offers_expired: int = 0 + try: + session = self.openSession() + + if check_records: + query = '''SELECT 1, bid_id, expire_at FROM bids WHERE active_ind = 1 AND state IN (:bid_received, :bid_sent) AND expire_at <= :check_time + UNION ALL + SELECT 2, offer_id, expire_at FROM offers WHERE active_ind = 1 AND state IN (:offer_received, :offer_sent) AND expire_at <= :check_time + ''' + q = session.execute(query, {'bid_received': int(BidStates.BID_RECEIVED), + 'offer_received': int(OfferStates.OFFER_RECEIVED), + 'bid_sent': int(BidStates.BID_SENT), + 'offer_sent': int(OfferStates.OFFER_SENT), + 'check_time': now + self.check_expiring_bids_offers_seconds}) + for entry in q: + record_id = entry[1] + expire_at = entry[2] + if entry[0] == 1: + if expire_at > now: + self._expiring_bids.append((record_id, expire_at)) + else: + bids_to_expire.add(record_id) + elif entry[0] == 2: + if expire_at > now: + self._expiring_offers.append((record_id, expire_at)) + else: + offers_to_expire.add(record_id) + + for bid_id in bids_to_expire: + query = 'SELECT expire_at, states FROM bids WHERE bid_id = :bid_id AND active_ind = 1 AND state IN (:bid_received, :bid_sent)' + rows = session.execute(query, {'bid_id': bid_id, + 'bid_received': int(BidStates.BID_RECEIVED), + 'bid_sent': int(BidStates.BID_SENT)}).fetchall() + if len(rows) > 0: + new_state: int = int(BidStates.BID_EXPIRED) + states = (bytes() if rows[0][1] is None else rows[0][1]) + pack_state(new_state, now) + query = 'UPDATE bids SET state = :new_state, states = :states WHERE bid_id = :bid_id' + session.execute(query, {'bid_id': bid_id, 'new_state': new_state, 'states': states}) + bids_expired += 1 + for offer_id in offers_to_expire: + query = 'SELECT expire_at, states FROM offers WHERE offer_id = :offer_id AND active_ind = 1 AND state IN (:offer_received, :offer_sent)' + rows = session.execute(query, {'offer_id': offer_id, + 'offer_received': int(OfferStates.OFFER_RECEIVED), + 'offer_sent': int(OfferStates.OFFER_SENT)}).fetchall() + if len(rows) > 0: + new_state: int = int(OfferStates.OFFER_EXPIRED) + states = (bytes() if rows[0][1] is None else rows[0][1]) + pack_state(new_state, now) + query = 'UPDATE offers SET state = :new_state, states = :states WHERE offer_id = :offer_id' + session.execute(query, {'offer_id': offer_id, 'new_state': new_state, 'states': states}) + offers_expired += 1 + finally: + self.closeSession(session) + + if bids_expired + offers_expired > 0: + mb = '' if bids_expired == 1 else 's' + mo = '' if offers_expired == 1 else 's' + self.log.debug(f'Expired {bids_expired} bid{mb} and {offers_expired} offer{mo}') + def update(self) -> None: + # Run every half second from basicswap-run if self._zmq_queue_enabled: try: if self._read_zmq_queue: @@ -6198,6 +6284,8 @@ class BasicSwap(BaseApp): try: # TODO: Wait for blocks / txns, would need to check multiple coins now: int = self.getTime() + self.expireBidsAndOffers(now) + if now - self._last_checked_progress >= self.check_progress_seconds: to_remove = [] for bid_id, v in self.swaps_in_progress.items(): diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index e449ccd..bbe5a03 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2021-2023 tecnovert +# Copyright (c) 2021-2024 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -70,6 +70,7 @@ class OfferStates(IntEnum): OFFER_SENT = 1 OFFER_RECEIVED = 2 OFFER_ABANDONED = 3 + OFFER_EXPIRED = 4 class BidStates(IntEnum): @@ -103,6 +104,7 @@ class BidStates(IntEnum): XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX = 28 # XmrBidLockSpendTxMessage BID_REQUEST_SENT = 29 BID_REQUEST_ACCEPTED = 30 + BID_EXPIRED = 31 class TxStates(IntEnum): @@ -248,6 +250,8 @@ def strOfferState(state): return 'Received' if state == OfferStates.OFFER_ABANDONED: return 'Abandoned' + if state == OfferStates.OFFER_EXPIRED: + return 'Expired' return 'Unknown' @@ -312,7 +316,8 @@ def strBidState(state): return 'Request accepted' if state == BidStates.BID_STATE_UNKNOWN: return 'Unknown bid state' - + if state == BidStates.BID_EXPIRED: + return 'Expired' return 'Unknown' + ' ' + str(state) @@ -503,7 +508,7 @@ def strSwapDesc(swap_type): return None -inactive_states = [BidStates.SWAP_COMPLETED, BidStates.BID_ERROR, BidStates.BID_REJECTED, BidStates.SWAP_TIMEDOUT, BidStates.BID_ABANDONED] +inactive_states = [BidStates.SWAP_COMPLETED, BidStates.BID_ERROR, BidStates.BID_REJECTED, BidStates.SWAP_TIMEDOUT, BidStates.BID_ABANDONED, BidStates.BID_EXPIRED] def isActiveBidState(state): diff --git a/basicswap/db.py b/basicswap/db.py index 56f3927..8e7f6ce 100644 --- a/basicswap/db.py +++ b/basicswap/db.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2019-2023 tecnovert +# Copyright (c) 2019-2024 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. import time -import struct import sqlalchemy as sa from enum import IntEnum, auto @@ -34,6 +33,10 @@ def strConcepts(state): return 'Unknown' +def pack_state(new_state: int, now: int) -> bytes: + return int(new_state).to_bytes(4, 'little') + now.to_bytes(8, 'little') + + class DBKVInt(Base): __tablename__ = 'kv_int' @@ -96,9 +99,9 @@ class Offer(Base): now = int(time.time()) self.state = new_state if self.states is None: - self.states = struct.pack('