automation: Accept multiple concurrent bids.

This commit is contained in:
tecnovert 2022-06-08 22:21:46 +02:00
parent d909115ea4
commit 89c60851ac
No known key found for this signature in database
GPG key ID: 8ED6D8750C4E3F93
6 changed files with 101 additions and 19 deletions

View file

@ -38,6 +38,7 @@ from . import __version__
from .rpc_xmr import make_xmr_rpc2_func
from .util import (
TemporaryError,
AutomationConstraint,
format_amount,
format_timestamp,
DeserialiseNum,
@ -3541,7 +3542,11 @@ class BasicSwap(BaseApp):
self.mxDB.release()
def countQueuedActions(self, session, bid_id, action_type):
q = session.query(Action).filter(sa.and_(Action.active_ind == 1, Action.linked_id == bid_id, Action.action_type == action_type))
q = session.query(Action).filter(sa.and_(Action.active_ind == 1, Action.linked_id == bid_id, Action.action_type == int(action_type)))
return q.count()
def countQueuedAcceptActions(self, session, bid_id):
q = session.query(Action).filter(sa.and_(Action.active_ind == 1, Action.linked_id == bid_id, sa.or_(Action.action_type == int(ActionTypes.ACCEPT_XMR_BID), Action.action_type == int(ActionTypes.ACCEPT_BID))))
return q.count()
def checkQueuedActions(self):
@ -3607,6 +3612,8 @@ class BasicSwap(BaseApp):
self.receiveXmrBid(bid, session)
except Exception as ex:
self.log.info('Verify xmr bid {} failed: {}'.format(bid.bid_id.hex(), str(ex)))
if self.debug:
self.log.error(traceback.format_exc())
bid.setState(BidStates.BID_ERROR, 'Failed validation: ' + str(ex))
session.add(bid)
self.updateBidInProgress(bid)
@ -3675,7 +3682,10 @@ class BasicSwap(BaseApp):
elif offer_data.swap_type == SwapTypes.XMR_SWAP:
ensure(coin_from not in non_script_type_coins, 'Invalid coin from type')
ensure(coin_to in non_script_type_coins, 'Invalid coin to type')
self.log.debug('TODO - More restrictions')
ensure(len(offer_data.proof_address) == 0, 'Unexpected data')
ensure(len(offer_data.proof_signature) == 0, 'Unexpected data')
ensure(len(offer_data.pkhash_seller) == 0, 'Unexpected data')
ensure(len(offer_data.secret_hash) == 0, 'Unexpected data')
else:
raise ValueError('Unknown swap type {}.'.format(offer_data.swap_type))
@ -3785,6 +3795,30 @@ class BasicSwap(BaseApp):
session.remove()
self.mxDB.release()
def getCompletedAndActiveBidsValue(self, offer, session):
bids = []
total_value = 0
q = session.execute('SELECT bid_id, amount, state FROM bids WHERE active_ind = 1 AND offer_id = x\'{}\''.format(offer.offer_id.hex()))
for row in q:
bid_id, amount, state = row
if state == BidStates.SWAP_COMPLETED:
bids.append((bid_id, amount, state, 1))
total_value += amount
continue
if state == BidStates.BID_ACCEPTED:
bids.append((bid_id, amount, state, 2))
total_value += amount
continue
if bid_id in self.swaps_in_progress:
bids.append((bid_id, amount, state, 3))
total_value += amount
continue
if self.countQueuedAcceptActions(session, bid_id) > 0:
bids.append((bid_id, amount, state, 4))
total_value += amount
continue
return bids, total_value
def shouldAutoAcceptBid(self, offer, bid, session=None):
use_session = None
try:
@ -3805,36 +3839,49 @@ class BasicSwap(BaseApp):
if not offer.amount_negotiable:
if bid.amount != offer.amount_from:
self.log.info('Not auto accepting bid %s, want exact amount match', bid.bid_id.hex())
return False
raise AutomationConstraint('Need exact amount match')
if bid.amount < offer.min_bid_amount:
self.log.info('Not auto accepting bid %s, bid amount below minimum', bid.bid_id.hex())
return False
raise AutomationConstraint('Bid amount below offer minimum')
if opts.get('exact_rate_only', False) is True:
if bid.rate != offer.rate:
self.log.info('Not auto accepting bid %s, want exact rate match', bid.bid_id.hex())
return False
raise AutomationConstraint('Need exact rate match')
max_bids = opts.get('max_bids', 1)
# Auto accept bid if set and no other non-abandoned bid for this order exists
if self.countAcceptedBids(offer.offer_id) >= max_bids:
self.log.info('Not auto accepting bid %s, already have', bid.bid_id.hex())
return False
active_bids, total_bids_value = self.getCompletedAndActiveBidsValue(offer, session)
if total_bids_value + bid.amount > offer.amount_from:
raise AutomationConstraint('Over remaining offer value {}'.format(offer.amount_from - total_bids_value))
num_not_completed = 0
for active_bid in active_bids:
if active_bid[3] != 1:
num_not_completed += 1
max_concurrent_bids = opts.get('max_concurrent_bids', 1)
if num_not_completed >= max_concurrent_bids:
raise AutomationConstraint('Already have {} bids to complete'.format(num_not_completed))
if strategy.only_known_identities:
identity_stats = use_session.query(KnownIdentity).filter_by(address=bid.bid_addr).first()
if not identity_stats:
return False
raise AutomationConstraint('Unknown bidder')
# TODO: More options
if identity_stats.num_recv_bids_successful < 1:
return False
raise AutomationConstraint('Bidder has too few successful swaps')
if identity_stats.num_recv_bids_successful <= identity_stats.num_recv_bids_failed:
return False
raise AutomationConstraint('Bidder has too many failed swaps')
return True
except AutomationConstraint as e:
self.log.info('Not auto accepting bid {}, {}'.format(bid.bid_id.hex(), str(e)))
if self.debug:
self.logEvent(Concepts.AUTOMATION,
bid.bid_id,
EventLogTypes.AUTOMATION_CONSTRAINT,
str(e),
use_session)
return False
except Exception as e:
self.log.error('shouldAutoAcceptBid: %s', str(e))
return False

View file

@ -156,6 +156,7 @@ class EventLogTypes(IntEnum):
LOCK_TX_A_REFUND_TX_SEEN = auto()
LOCK_TX_A_REFUND_SPEND_TX_SEEN = auto()
ERROR = auto()
AUTOMATION_CONSTRAINT = auto()
class XmrSplitMsgTypes(IntEnum):

View file

@ -21,6 +21,7 @@ class Concepts(IntEnum):
OFFER = auto()
BID = auto()
NETWORK_MESSAGE = auto()
AUTOMATION = auto()
def strConcepts(state):

View file

@ -29,14 +29,14 @@ def upgradeDatabaseData(self, data_version):
label='Accept All',
type_ind=Concepts.OFFER,
data=json.dumps({'exact_rate_only': True,
'max_bids': 1}).encode('utf-8'),
'max_concurrent_bids': 5}).encode('utf-8'),
only_known_identities=False))
session.add(AutomationStrategy(
active_ind=1,
label='Accept Known',
type_ind=Concepts.OFFER,
data=json.dumps({'exact_rate_only': True,
'max_bids': 1}).encode('utf-8'),
'max_concurrent_bids': 5}).encode('utf-8'),
only_known_identities=True,
note='Accept bids from identities with previously successful swaps only'))

View file

@ -21,6 +21,10 @@ class TemporaryError(ValueError):
pass
class AutomationConstraint(ValueError):
pass
def ensure(v, err_string):
if not v:
raise ValueError(err_string)

View file

@ -875,6 +875,8 @@ class Test(BaseTest):
offer = swap_clients[1].listOffers(filters={'offer_id': offer_id})[0]
below_min_bid = min_bid - 1
# Ensure bids below the minimum amount fails on sender and recipient.
try:
bid_id = swap_clients[1].postBid(offer_id, below_min_bid)
except Exception as e:
@ -886,7 +888,34 @@ class Test(BaseTest):
events = wait_for_event(test_delay_event, swap_clients[0], Concepts.NETWORK_MESSAGE, bid_id)
assert('Bid amount below minimum' in events[0].event_msg)
# TODO
bid_ids = []
for i in range(5):
bid_ids.append(swap_clients[1].postBid(offer_id, min_bid))
# Should fail > max concurrent
test_delay_event.wait(1.0)
bid_id = swap_clients[1].postBid(offer_id, min_bid)
events = wait_for_event(test_delay_event, swap_clients[0], Concepts.AUTOMATION, bid_id)
assert('Already have 5 bids to complete' in events[0].event_msg)
for bid_id in bid_ids:
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)
amt_bid = make_int(5, scale=8, r=1)
# Should fail > total value
amt_bid += 1
bid_id = swap_clients[1].postBid(offer_id, amt_bid)
events = wait_for_event(test_delay_event, swap_clients[0], Concepts.AUTOMATION, bid_id)
assert('Over remaining offer value' in events[0].event_msg)
# Should pass
amt_bid -= 1
bid_id = swap_clients[1].postBid(offer_id, amt_bid)
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)
def test_10_locked_refundtx(self):
logging.info('---------- Test Refund tx is locked')