Merge pull request #167 from tecnovert/auto-accept

Improve auto-accept
This commit is contained in:
tecnovert 2024-11-26 18:37:23 +00:00 committed by GitHub
commit a4dc9af301
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 213 additions and 74 deletions

View file

@ -35,6 +35,7 @@ from .rpc_xmr import make_xmr_rpc2_func
from .ui.util import getCoinName, known_chart_coins from .ui.util import getCoinName, known_chart_coins
from .util import ( from .util import (
AutomationConstraint, AutomationConstraint,
AutomationConstraintTemporary,
LockedCoinError, LockedCoinError,
TemporaryError, TemporaryError,
InactiveCoin, InactiveCoin,
@ -125,25 +126,26 @@ from .basicswap_util import (
AutomationOverrideOptions, AutomationOverrideOptions,
BidStates, BidStates,
DebugTypes, DebugTypes,
describeEventEntry,
EventLogTypes, EventLogTypes,
getLastBidState,
getOfferProofOfFundsHash,
getVoutByAddress,
getVoutByScriptPubKey,
inactive_states,
isActiveBidState,
KeyTypes, KeyTypes,
MessageTypes, MessageTypes,
NotificationTypes as NT, NotificationTypes as NT,
OfferStates, OfferStates,
strBidState,
SwapTypes, SwapTypes,
TxLockTypes, TxLockTypes,
TxStates, TxStates,
TxTypes, TxTypes,
VisibilityOverrideOptions, VisibilityOverrideOptions,
XmrSplitMsgTypes, XmrSplitMsgTypes,
canAcceptBidState,
describeEventEntry,
getLastBidState,
getOfferProofOfFundsHash,
getVoutByAddress,
getVoutByScriptPubKey,
inactive_states,
isActiveBidState,
strBidState,
) )
from basicswap.db_util import ( from basicswap.db_util import (
remove_expired_data, remove_expired_data,
@ -307,8 +309,12 @@ class BasicSwap(BaseApp):
self.check_watched_seconds = self.get_int_setting( self.check_watched_seconds = self.get_int_setting(
"check_watched_seconds", 60, 1, 10 * 60 "check_watched_seconds", 60, 1, 10 * 60
) )
self.check_xmr_swaps_seconds = self.get_int_setting( self.check_split_messages_seconds = self.get_int_setting(
"check_xmr_swaps_seconds", 20, 1, 10 * 60 "check_split_messages_seconds", 20, 1, 10 * 60
)
# Retry auto accept for bids at BID_AACCEPT_DELAY, also updates when bids complete
self.check_delayed_auto_accept_seconds = self.get_int_setting(
"check_delayed_auto_accept_seconds", 60, 1, 20 * 60
) )
self.startup_tries = self.get_int_setting( self.startup_tries = self.get_int_setting(
"startup_tries", 21, 1, 100 "startup_tries", 21, 1, 100
@ -321,7 +327,8 @@ class BasicSwap(BaseApp):
self._last_checked_progress = 0 self._last_checked_progress = 0
self._last_checked_smsg = 0 self._last_checked_smsg = 0
self._last_checked_watched = 0 self._last_checked_watched = 0
self._last_checked_xmr_swaps = 0 self._last_checked_split_messages = 0
self._last_checked_delayed_auto_accept = 0
self._possibly_revoked_offers = collections.deque( self._possibly_revoked_offers = collections.deque(
[], maxlen=48 [], maxlen=48
) # TODO: improve ) # TODO: improve
@ -409,7 +416,6 @@ class BasicSwap(BaseApp):
bytes.fromhex(self.network_pubkey), bytes.fromhex(self.network_pubkey),
) )
self.db_echo: bool = self.settings.get("db_echo", False)
self.sqlite_file: str = os.path.join( self.sqlite_file: str = os.path.join(
self.data_dir, self.data_dir,
"db{}.sqlite".format("" if self.chain == "mainnet" else ("_" + self.chain)), "db{}.sqlite".format("" if self.chain == "mainnet" else ("_" + self.chain)),
@ -3229,7 +3235,7 @@ class BasicSwap(BaseApp):
now: int = self.getTime() now: int = self.getTime()
ensure(bid.expire_at > now, "Bid expired") ensure(bid.expire_at > now, "Bid expired")
ensure( ensure(
bid.state in (BidStates.BID_RECEIVED,), canAcceptBidState(bid.state),
"Wrong bid state: {}".format(BidStates(bid.state).name), "Wrong bid state: {}".format(BidStates(bid.state).name),
) )
@ -3688,7 +3694,7 @@ class BasicSwap(BaseApp):
last_bid_state = getLastBidState(bid.states) last_bid_state = getLastBidState(bid.states)
ensure( ensure(
last_bid_state == BidStates.BID_RECEIVED, canAcceptBidState(last_bid_state),
"Wrong bid state: {}".format(str(BidStates(last_bid_state))), "Wrong bid state: {}".format(str(BidStates(last_bid_state))),
) )
@ -3990,7 +3996,7 @@ class BasicSwap(BaseApp):
last_bid_state = getLastBidState(bid.states) last_bid_state = getLastBidState(bid.states)
ensure( ensure(
last_bid_state == BidStates.BID_RECEIVED, canAcceptBidState(last_bid_state),
"Wrong bid state: {}".format(str(BidStates(last_bid_state))), "Wrong bid state: {}".format(str(BidStates(last_bid_state))),
) )
@ -6790,12 +6796,12 @@ class BasicSwap(BaseApp):
if ( if (
bid bid
and bid.state == BidStates.SWAP_DELAYING and bid.state == BidStates.SWAP_DELAYING
and last_state == BidStates.BID_RECEIVED and canAcceptBidState(last_state)
): ):
new_state = ( new_state = (
BidStates.BID_ERROR BidStates.BID_ERROR
if offer.bid_reversed if offer.bid_reversed
else BidStates.BID_RECEIVED else last_state
) )
bid.setState(new_state) bid.setState(new_state)
self.saveBidInSession(bid_id, bid, cursor) self.saveBidInSession(bid_id, bid, cursor)
@ -6819,7 +6825,8 @@ class BasicSwap(BaseApp):
if reload_in_progress: if reload_in_progress:
self.loadFromDB() self.loadFromDB()
def checkXmrSwaps(self) -> None: def checkSplitMessages(self) -> None:
# Combines split data messages
now: int = self.getTime() now: int = self.getTime()
ttl_xmr_split_messages = 60 * 60 ttl_xmr_split_messages = 60 * 60
bid_cursor = None bid_cursor = None
@ -6930,6 +6937,27 @@ class BasicSwap(BaseApp):
self.closeDBCursor(bid_cursor) self.closeDBCursor(bid_cursor)
self.closeDB(cursor) self.closeDB(cursor)
def checkDelayedAutoAccept(self) -> None:
bids_cursor = None
try:
cursor = self.openDB()
bids_cursor = self.getNewDBCursor()
for bid in self.query(
Bid, bids_cursor, {"state": int(BidStates.BID_AACCEPT_DELAY)}
):
offer = self.getOffer(bid.offer_id, cursor=cursor)
if self.shouldAutoAcceptBid(offer, bid, cursor=cursor):
delay = self.get_delay_event_seconds()
self.log.info(
"Auto accepting bid %s in %d seconds", bid.bid_id.hex(), delay
)
self.createActionInSession(
delay, ActionTypes.ACCEPT_BID, bid.bid_id, cursor
)
finally:
self.closeDBCursor(bids_cursor)
self.closeDB(cursor)
def processOffer(self, msg) -> None: def processOffer(self, msg) -> None:
offer_bytes = bytes.fromhex(msg["hex"][2:-2]) offer_bytes = bytes.fromhex(msg["hex"][2:-2])
@ -7222,6 +7250,10 @@ class BasicSwap(BaseApp):
try: try:
use_cursor = self.openDB(cursor) use_cursor = self.openDB(cursor)
if self.countQueuedActions(use_cursor, bid.bid_id, ActionTypes.ACCEPT_BID):
# Bid is already queued to be accepted
return False
link = self.queryOne( link = self.queryOne(
AutomationLink, AutomationLink,
use_cursor, use_cursor,
@ -7250,6 +7282,12 @@ class BasicSwap(BaseApp):
self.log.debug("Evaluating against strategy {}".format(strategy.record_id)) self.log.debug("Evaluating against strategy {}".format(strategy.record_id))
now: int = self.getTime()
if bid.expire_at < now:
raise AutomationConstraint(
"Bid expired"
) # State will be set to expired in expireBidsAndOffers
if not offer.amount_negotiable: if not offer.amount_negotiable:
if bid_amount != offer.amount_from: if bid_amount != offer.amount_from:
raise AutomationConstraint("Need exact amount match") raise AutomationConstraint("Need exact amount match")
@ -7287,7 +7325,7 @@ class BasicSwap(BaseApp):
f"active_bids {num_not_completed}, max_concurrent_bids {max_concurrent_bids}" f"active_bids {num_not_completed}, max_concurrent_bids {max_concurrent_bids}"
) )
if num_not_completed >= max_concurrent_bids: if num_not_completed >= max_concurrent_bids:
raise AutomationConstraint( raise AutomationConstraintTemporary(
"Already have {} bids to complete".format(num_not_completed) "Already have {} bids to complete".format(num_not_completed)
) )
@ -7296,6 +7334,15 @@ class BasicSwap(BaseApp):
) )
self.evaluateKnownIdentityForAutoAccept(strategy, identity_stats) self.evaluateKnownIdentityForAutoAccept(strategy, identity_stats)
# Ensure the coin from wallet has sufficient balance for multiple bids
bids_active_if_accepted: int = num_not_completed + 1
ci_from = self.ci(offer.coin_from)
try:
ci_from.ensureFunds(bids_active_if_accepted * bid_amount)
except Exception as e: # noqa: F841
raise AutomationConstraintTemporary("Balance too low")
self.logEvent( self.logEvent(
Concepts.BID, Concepts.BID,
bid.bid_id, bid.bid_id,
@ -7305,7 +7352,7 @@ class BasicSwap(BaseApp):
) )
return True return True
except AutomationConstraint as e: except (AutomationConstraint, AutomationConstraintTemporary) as e:
self.log.info( self.log.info(
"Not auto accepting bid {}, {}".format(bid.bid_id.hex(), str(e)) "Not auto accepting bid {}, {}".format(bid.bid_id.hex(), str(e))
) )
@ -7317,6 +7364,19 @@ class BasicSwap(BaseApp):
str(e), str(e),
use_cursor, use_cursor,
) )
if isinstance(e, AutomationConstraintTemporary):
bid.setState(BidStates.BID_AACCEPT_DELAY)
else:
bid.setState(BidStates.BID_AACCEPT_FAIL)
self.updateDB(
bid,
use_cursor,
[
"bid_id",
],
)
return False return False
except Exception as e: except Exception as e:
self.logException(f"shouldAutoAcceptBid {e}") self.logException(f"shouldAutoAcceptBid {e}")
@ -9589,18 +9649,21 @@ class BasicSwap(BaseApp):
cursor = self.openDB() cursor = self.openDB()
if check_records: 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 query = """SELECT 1, bid_id, expire_at FROM bids WHERE active_ind = 1 AND state IN (:bid_received, :bid_sent, :bid_aad, :bid_aaf, :bid_req_sent) AND expire_at <= :check_time
UNION ALL 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 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 = cursor.execute( q = cursor.execute(
query, query,
{ {
"bid_received": int(BidStates.BID_RECEIVED),
"offer_received": int(OfferStates.OFFER_RECEIVED), "offer_received": int(OfferStates.OFFER_RECEIVED),
"bid_sent": int(BidStates.BID_SENT),
"offer_sent": int(OfferStates.OFFER_SENT), "offer_sent": int(OfferStates.OFFER_SENT),
"check_time": now + self.check_expiring_bids_offers_seconds, "check_time": now + self.check_expiring_bids_offers_seconds,
"bid_sent": int(BidStates.BID_SENT),
"bid_received": int(BidStates.BID_RECEIVED),
"bid_aad": int(BidStates.BID_AACCEPT_DELAY),
"bid_aaf": int(BidStates.BID_AACCEPT_FAIL),
"bid_req_sent": int(BidStates.BID_REQUEST_SENT),
}, },
) )
for entry in q: for entry in q:
@ -9618,13 +9681,16 @@ class BasicSwap(BaseApp):
offers_to_expire.add(record_id) offers_to_expire.add(record_id)
for bid_id in bids_to_expire: 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)" query = "SELECT expire_at, states FROM bids WHERE bid_id = :bid_id AND active_ind = 1 AND state IN (:bid_received, :bid_sent, :bid_aad, :bid_aaf, :bid_req_sent)"
rows = cursor.execute( rows = cursor.execute(
query, query,
{ {
"bid_id": bid_id, "bid_id": bid_id,
"bid_received": int(BidStates.BID_RECEIVED), "bid_received": int(BidStates.BID_RECEIVED),
"bid_sent": int(BidStates.BID_SENT), "bid_sent": int(BidStates.BID_SENT),
"bid_aad": int(BidStates.BID_AACCEPT_DELAY),
"bid_aaf": int(BidStates.BID_AACCEPT_FAIL),
"bid_req_sent": int(BidStates.BID_REQUEST_SENT),
}, },
).fetchall() ).fetchall()
if len(rows) > 0: if len(rows) > 0:
@ -9699,8 +9765,8 @@ class BasicSwap(BaseApp):
now: int = self.getTime() now: int = self.getTime()
self.expireBidsAndOffers(now) self.expireBidsAndOffers(now)
to_remove = []
if now - self._last_checked_progress >= self.check_progress_seconds: if now - self._last_checked_progress >= self.check_progress_seconds:
to_remove = []
for bid_id, v in self.swaps_in_progress.items(): for bid_id, v in self.swaps_in_progress.items():
try: try:
if self.checkBidState(bid_id, v[0], v[1]) is True: if self.checkBidState(bid_id, v[0], v[1]) is True:
@ -9748,9 +9814,20 @@ class BasicSwap(BaseApp):
self.checkQueuedActions() self.checkQueuedActions()
self._last_checked_actions = now self._last_checked_actions = now
if now - self._last_checked_xmr_swaps >= self.check_xmr_swaps_seconds: if (
self.checkXmrSwaps() now - self._last_checked_split_messages
self._last_checked_xmr_swaps = now >= self.check_split_messages_seconds
):
self.checkSplitMessages()
self._last_checked_split_messages = now
if (
len(to_remove) > 0
or now - self._last_checked_delayed_auto_accept
>= self.check_delayed_auto_accept_seconds
):
self.checkDelayedAutoAccept()
self._last_checked_delayed_auto_accept = now
except Exception as ex: except Exception as ex:
self.logException(f"update {ex}") self.logException(f"update {ex}")
@ -10113,7 +10190,7 @@ class BasicSwap(BaseApp):
COUNT(CASE WHEN b.was_sent THEN 1 ELSE NULL END) AS count_sent, COUNT(CASE WHEN b.was_sent THEN 1 ELSE NULL END) AS count_sent,
COUNT(CASE WHEN b.was_sent AND (s.in_progress OR (s.swap_ended = 0 AND b.expire_at > :now AND o.expire_at > :now)) THEN 1 ELSE NULL END) AS count_sent_active, COUNT(CASE WHEN b.was_sent AND (s.in_progress OR (s.swap_ended = 0 AND b.expire_at > :now AND o.expire_at > :now)) THEN 1 ELSE NULL END) AS count_sent_active,
COUNT(CASE WHEN b.was_received THEN 1 ELSE NULL END) AS count_received, COUNT(CASE WHEN b.was_received THEN 1 ELSE NULL END) AS count_received,
COUNT(CASE WHEN b.was_received AND b.state = :received_state AND b.expire_at > :now AND o.expire_at > :now THEN 1 ELSE NULL END) AS count_available, COUNT(CASE WHEN b.was_received AND s.can_accept AND b.expire_at > :now AND o.expire_at > :now THEN 1 ELSE NULL END) AS count_available,
COUNT(CASE WHEN b.was_received AND (s.in_progress OR (s.swap_ended = 0 AND b.expire_at > :now AND o.expire_at > :now)) THEN 1 ELSE NULL END) AS count_recv_active COUNT(CASE WHEN b.was_received AND (s.in_progress OR (s.swap_ended = 0 AND b.expire_at > :now AND o.expire_at > :now)) THEN 1 ELSE NULL END) AS count_recv_active
FROM bids b FROM bids b
JOIN offers o ON b.offer_id = o.offer_id JOIN offers o ON b.offer_id = o.offer_id
@ -10131,9 +10208,7 @@ class BasicSwap(BaseApp):
try: try:
cursor = self.openDB() cursor = self.openDB()
q = cursor.execute( q = cursor.execute(q_bids_str, {"now": now}).fetchone()
q_bids_str, {"now": now, "received_state": int(BidStates.BID_RECEIVED)}
).fetchone()
bids_sent = q[0] bids_sent = q[0]
bids_sent_active = q[1] bids_sent_active = q[1]
bids_received = q[2] bids_received = q[2]

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2021-2024 tecnovert # Copyright (c) 2021-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@ -106,6 +107,8 @@ class BidStates(IntEnum):
BID_REQUEST_SENT = 29 BID_REQUEST_SENT = 29
BID_REQUEST_ACCEPTED = 30 BID_REQUEST_ACCEPTED = 30
BID_EXPIRED = 31 BID_EXPIRED = 31
BID_AACCEPT_DELAY = 32
BID_AACCEPT_FAIL = 33
class TxStates(IntEnum): class TxStates(IntEnum):
@ -330,6 +333,10 @@ def strBidState(state):
return "Unknown bid state" return "Unknown bid state"
if state == BidStates.BID_EXPIRED: if state == BidStates.BID_EXPIRED:
return "Expired" return "Expired"
if state == BidStates.BID_AACCEPT_DELAY:
return "Auto accept delay"
if state == BidStates.BID_AACCEPT_FAIL:
return "Auto accept failed"
return "Unknown" + " " + str(state) return "Unknown" + " " + str(state)
@ -539,6 +546,14 @@ inactive_states = [
] ]
def canAcceptBidState(state):
return state in (
BidStates.BID_RECEIVED,
BidStates.BID_AACCEPT_DELAY,
BidStates.BID_AACCEPT_FAIL,
)
def isActiveBidState(state): def isActiveBidState(state):
if state >= BidStates.BID_ACCEPTED and state < BidStates.SWAP_COMPLETED: if state >= BidStates.BID_ACCEPTED and state < BidStates.SWAP_COMPLETED:
return True return True

View file

@ -13,8 +13,8 @@ from enum import IntEnum, auto
from typing import Optional from typing import Optional
CURRENT_DB_VERSION = 24 CURRENT_DB_VERSION = 25
CURRENT_DB_DATA_VERSION = 4 CURRENT_DB_DATA_VERSION = 5
class Concepts(IntEnum): class Concepts(IntEnum):
@ -601,6 +601,7 @@ class BidState(Table):
in_error = Column("integer") in_error = Column("integer")
swap_failed = Column("integer") swap_failed = Column("integer")
swap_ended = Column("integer") swap_ended = Column("integer")
can_accept = Column("integer")
note = Column("string") note = Column("string")
created_at = Column("integer") created_at = Column("integer")
@ -751,7 +752,6 @@ class DBMethods:
def closeDBCursor(self, cursor): def closeDBCursor(self, cursor):
assert self.mxDB.locked() assert self.mxDB.locked()
if cursor: if cursor:
cursor.close() cursor.close()

View file

@ -18,6 +18,7 @@ from .db import (
from .basicswap_util import ( from .basicswap_util import (
BidStates, BidStates,
canAcceptBidState,
isActiveBidState, isActiveBidState,
isErrorBidState, isErrorBidState,
isFailingBidState, isFailingBidState,
@ -26,6 +27,23 @@ from .basicswap_util import (
) )
def addBidState(self, state, now, cursor):
self.add(
BidState(
active_ind=1,
state_id=int(state),
in_progress=isActiveBidState(state),
in_error=isErrorBidState(state),
swap_failed=isFailingBidState(state),
swap_ended=isFinalBidState(state),
can_accept=canAcceptBidState(state),
label=strBidState(state),
created_at=now,
),
cursor,
)
def upgradeDatabaseData(self, data_version): def upgradeDatabaseData(self, data_version):
if data_version >= CURRENT_DB_DATA_VERSION: if data_version >= CURRENT_DB_DATA_VERSION:
return return
@ -69,19 +87,7 @@ def upgradeDatabaseData(self, data_version):
) )
for state in BidStates: for state in BidStates:
self.add( addBidState(self, state, now, cursor)
BidState(
active_ind=1,
state_id=int(state),
in_progress=isActiveBidState(state),
in_error=isErrorBidState(state),
swap_failed=isFailingBidState(state),
swap_ended=isFinalBidState(state),
label=strBidState(state),
created_at=now,
),
cursor,
)
if data_version > 0 and data_version < 2: if data_version > 0 and data_version < 2:
for state in ( for state in (
@ -117,19 +123,15 @@ def upgradeDatabaseData(self, data_version):
BidStates.BID_REQUEST_SENT, BidStates.BID_REQUEST_SENT,
BidStates.BID_REQUEST_ACCEPTED, BidStates.BID_REQUEST_ACCEPTED,
): ):
self.add( addBidState(self, state, now, cursor)
BidState(
active_ind=1, if data_version > 0 and data_version < 5:
state_id=int(state), for state in (
in_progress=isActiveBidState(state), BidStates.BID_EXPIRED,
in_error=isErrorBidState(state), BidStates.BID_AACCEPT_DELAY,
swap_failed=isFailingBidState(state), BidStates.BID_AACCEPT_FAIL,
swap_ended=isFinalBidState(state), ):
label=strBidState(state), addBidState(self, state, now, cursor)
created_at=now,
),
cursor,
)
self.db_data_version = CURRENT_DB_DATA_VERSION self.db_data_version = CURRENT_DB_DATA_VERSION
self.setIntKV("db_data_version", self.db_data_version, cursor) self.setIntKV("db_data_version", self.db_data_version, cursor)
@ -405,10 +407,13 @@ def upgradeDatabase(self, db_version):
PRIMARY KEY (record_id))""" PRIMARY KEY (record_id))"""
) )
cursor.execute("ALTER TABLE bids ADD COLUMN pkhash_buyer_to BLOB") cursor.execute("ALTER TABLE bids ADD COLUMN pkhash_buyer_to BLOB")
elif current_version == 24:
db_version += 1
cursor.execute("ALTER TABLE bidstates ADD COLUMN can_accept INTEGER")
if current_version != db_version: if current_version != db_version:
self.db_version = db_version self.db_version = db_version
self.setIntKV("db_version", db_version, cursor) self.setIntKV("db_version", db_version, cursor)
cursor = self.commitDB() self.commitDB()
self.log.info("Upgraded database to version {}".format(self.db_version)) self.log.info("Upgraded database to version {}".format(self.db_version))
continue continue
except Exception as e: except Exception as e:

View file

@ -25,6 +25,7 @@ from basicswap.basicswap_util import (
BidStates, BidStates,
SwapTypes, SwapTypes,
DebugTypes, DebugTypes,
canAcceptBidState,
strTxState, strTxState,
strBidState, strBidState,
) )
@ -124,7 +125,7 @@ def page_bid(self, url_split, post_string):
if len(data["addr_from_label"]) > 0: if len(data["addr_from_label"]) > 0:
data["addr_from_label"] = "(" + data["addr_from_label"] + ")" data["addr_from_label"] = "(" + data["addr_from_label"] + ")"
data["can_accept_bid"] = True if bid.state == BidStates.BID_RECEIVED else False data["can_accept_bid"] = True if canAcceptBidState(bid.state) else False
if swap_client.debug_ui: if swap_client.debug_ui:
data["bid_actions"] = [ data["bid_actions"] = [

View file

@ -20,6 +20,7 @@ from basicswap.basicswap_util import (
ActionTypes, ActionTypes,
BidStates, BidStates,
DebugTypes, DebugTypes,
canAcceptBidState,
getLastBidState, getLastBidState,
strBidState, strBidState,
strTxState, strTxState,
@ -258,7 +259,7 @@ def describeBid(
if bid.state == BidStates.BID_RECEIVING: if bid.state == BidStates.BID_RECEIVING:
# Offerer receiving bid from bidder # Offerer receiving bid from bidder
state_description = "Waiting for bid to be fully received" state_description = "Waiting for bid to be fully received"
elif bid.state == BidStates.BID_RECEIVED: elif canAcceptBidState(bid.state):
# Offerer received bid from bidder # Offerer received bid from bidder
# TODO: Manual vs automatic # TODO: Manual vs automatic
state_description = "Bid must be accepted" state_description = "Bid must be accepted"
@ -270,7 +271,7 @@ def describeBid(
) )
elif bid.state == BidStates.SWAP_DELAYING: elif bid.state == BidStates.SWAP_DELAYING:
last_state = getLastBidState(bid.states) last_state = getLastBidState(bid.states)
if last_state == BidStates.BID_RECEIVED: if canAcceptBidState(last_state):
state_description = "Delaying before accepting bid" state_description = "Delaying before accepting bid"
elif last_state == BidStates.BID_RECEIVING_ACC: elif last_state == BidStates.BID_RECEIVING_ACC:
state_description = "Delaying before responding to accepted bid" state_description = "Delaying before responding to accepted bid"

View file

@ -26,6 +26,10 @@ class AutomationConstraint(ValueError):
pass pass
class AutomationConstraintTemporary(ValueError):
pass
class InactiveCoin(Exception): class InactiveCoin(Exception):
def __init__(self, coinid): def __init__(self, coinid):
self.coinid = coinid self.coinid = coinid

View file

@ -9,7 +9,6 @@
- Incoming expired offers no longer raise an error. - Incoming expired offers no longer raise an error.
0.13.2 0.13.2
============== ==============

View file

@ -900,6 +900,7 @@ class Test(unittest.TestCase):
waitForServer(self.delay_event, UI_PORT + 0) waitForServer(self.delay_event, UI_PORT + 0)
waitForServer(self.delay_event, UI_PORT + 1) waitForServer(self.delay_event, UI_PORT + 1)
waitForServer(self.delay_event, UI_PORT + 2)
logging.info("Reset test") logging.info("Reset test")
clear_offers(self.delay_event, 0) clear_offers(self.delay_event, 0)
@ -926,6 +927,13 @@ class Test(unittest.TestCase):
expect_balance, expect_balance,
) )
inactive_states = (
"Received",
"Completed",
"Auto accept failed",
"Auto accept delay",
)
# Try post bids at the same time # Try post bids at the same time
from multiprocessing import Process from multiprocessing import Process
@ -933,8 +941,12 @@ class Test(unittest.TestCase):
post_json = {"offer_id": offer_id, "amount_from": amount} post_json = {"offer_id": offer_id, "amount_from": amount}
read_json_api(UI_PORT + node_from, "bids/new", post_json) read_json_api(UI_PORT + node_from, "bids/new", post_json)
def test_bid_pair(amount_1, amount_2, expect_inactive, delay_event): def test_bid_pair(
logging.debug(f"test_bid_pair {amount_1} {amount_2}, {expect_inactive}") amount_1, amount_2, max_active, delay_event, final_completed=None
):
logging.debug(
f"test_bid_pair {amount_1} {amount_2}, {max_active}, {final_completed}"
)
wait_for_balance( wait_for_balance(
self.delay_event, self.delay_event,
@ -966,6 +978,31 @@ class Test(unittest.TestCase):
pbid1.join() pbid1.join()
pbid2.join() pbid2.join()
if final_completed is not None:
# bids should complete
logging.info("Waiting for bids to settle")
for i in range(50):
delay_event.wait(5)
bids = wait_for_bids(self.delay_event, 0, 2, offer_id)
if any(bid["bid_state"] == "Receiving" for bid in bids):
continue
logging.info(f"[rm] bids {bids}")
num_active_state = 0
num_completed = 0
for bid in bids:
if bid["bid_state"] == "Completed":
num_completed += 1
if bid["bid_state"] not in inactive_states:
num_active_state += 1
assert num_active_state <= max_active
if num_completed == final_completed:
return
raise ValueError(f"Failed to complete {num_completed} bids {bids}")
for i in range(5): for i in range(5):
logging.info("Waiting for bids to settle") logging.info("Waiting for bids to settle")
@ -974,16 +1011,18 @@ class Test(unittest.TestCase):
if any(bid["bid_state"] == "Receiving" for bid in bids): if any(bid["bid_state"] == "Receiving" for bid in bids):
continue continue
logging.info(f"[rm] bids {bids}")
break break
num_received_state = 0 num_active_state = 0
for bid in bids: for bid in bids:
if bid["bid_state"] == "Received": if bid["bid_state"] not in inactive_states:
num_received_state += 1 num_active_state += 1
assert num_received_state == expect_inactive assert num_active_state == max_active
# Bids with a combined value less than the offer value should both be accepted # Bids with a combined value less than the offer value should both be accepted
test_bid_pair(1.1, 1.2, 0, self.delay_event) test_bid_pair(1.1, 1.2, 2, self.delay_event)
# Only one bid of bids with a combined value greater than the offer value should be accepted # Only one bid of bids with a combined value greater than the offer value should be accepted
test_bid_pair(1.1, 9.2, 1, self.delay_event) test_bid_pair(1.1, 9.2, 1, self.delay_event)
@ -1006,7 +1045,7 @@ class Test(unittest.TestCase):
assert json_rv["note"] == "changed" assert json_rv["note"] == "changed"
# Only one bid should be active # Only one bid should be active
test_bid_pair(1.1, 1.2, 1, self.delay_event) test_bid_pair(1.1, 1.2, 1, self.delay_event, 2)
finally: finally:
logging.debug("Reset max_concurrent_bids") logging.debug("Reset max_concurrent_bids")