automation: Accept multiple concurrent bids.
This commit is contained in:
parent
d909115ea4
commit
89c60851ac
@ -38,6 +38,7 @@ from . import __version__
|
|||||||
from .rpc_xmr import make_xmr_rpc2_func
|
from .rpc_xmr import make_xmr_rpc2_func
|
||||||
from .util import (
|
from .util import (
|
||||||
TemporaryError,
|
TemporaryError,
|
||||||
|
AutomationConstraint,
|
||||||
format_amount,
|
format_amount,
|
||||||
format_timestamp,
|
format_timestamp,
|
||||||
DeserialiseNum,
|
DeserialiseNum,
|
||||||
@ -3541,7 +3542,11 @@ class BasicSwap(BaseApp):
|
|||||||
self.mxDB.release()
|
self.mxDB.release()
|
||||||
|
|
||||||
def countQueuedActions(self, session, bid_id, action_type):
|
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()
|
return q.count()
|
||||||
|
|
||||||
def checkQueuedActions(self):
|
def checkQueuedActions(self):
|
||||||
@ -3607,6 +3612,8 @@ class BasicSwap(BaseApp):
|
|||||||
self.receiveXmrBid(bid, session)
|
self.receiveXmrBid(bid, session)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.log.info('Verify xmr bid {} failed: {}'.format(bid.bid_id.hex(), str(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))
|
bid.setState(BidStates.BID_ERROR, 'Failed validation: ' + str(ex))
|
||||||
session.add(bid)
|
session.add(bid)
|
||||||
self.updateBidInProgress(bid)
|
self.updateBidInProgress(bid)
|
||||||
@ -3675,7 +3682,10 @@ class BasicSwap(BaseApp):
|
|||||||
elif offer_data.swap_type == SwapTypes.XMR_SWAP:
|
elif offer_data.swap_type == SwapTypes.XMR_SWAP:
|
||||||
ensure(coin_from not in non_script_type_coins, 'Invalid coin from type')
|
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')
|
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:
|
else:
|
||||||
raise ValueError('Unknown swap type {}.'.format(offer_data.swap_type))
|
raise ValueError('Unknown swap type {}.'.format(offer_data.swap_type))
|
||||||
|
|
||||||
@ -3785,6 +3795,30 @@ class BasicSwap(BaseApp):
|
|||||||
session.remove()
|
session.remove()
|
||||||
self.mxDB.release()
|
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):
|
def shouldAutoAcceptBid(self, offer, bid, session=None):
|
||||||
use_session = None
|
use_session = None
|
||||||
try:
|
try:
|
||||||
@ -3805,36 +3839,49 @@ class BasicSwap(BaseApp):
|
|||||||
|
|
||||||
if not offer.amount_negotiable:
|
if not offer.amount_negotiable:
|
||||||
if bid.amount != offer.amount_from:
|
if bid.amount != offer.amount_from:
|
||||||
self.log.info('Not auto accepting bid %s, want exact amount match', bid.bid_id.hex())
|
raise AutomationConstraint('Need exact amount match')
|
||||||
return False
|
|
||||||
|
|
||||||
if bid.amount < offer.min_bid_amount:
|
if bid.amount < offer.min_bid_amount:
|
||||||
self.log.info('Not auto accepting bid %s, bid amount below minimum', bid.bid_id.hex())
|
raise AutomationConstraint('Bid amount below offer minimum')
|
||||||
return False
|
|
||||||
|
|
||||||
if opts.get('exact_rate_only', False) is True:
|
if opts.get('exact_rate_only', False) is True:
|
||||||
if bid.rate != offer.rate:
|
if bid.rate != offer.rate:
|
||||||
self.log.info('Not auto accepting bid %s, want exact rate match', bid.bid_id.hex())
|
raise AutomationConstraint('Need exact rate match')
|
||||||
return False
|
|
||||||
|
|
||||||
max_bids = opts.get('max_bids', 1)
|
active_bids, total_bids_value = self.getCompletedAndActiveBidsValue(offer, session)
|
||||||
# Auto accept bid if set and no other non-abandoned bid for this order exists
|
|
||||||
if self.countAcceptedBids(offer.offer_id) >= max_bids:
|
if total_bids_value + bid.amount > offer.amount_from:
|
||||||
self.log.info('Not auto accepting bid %s, already have', bid.bid_id.hex())
|
raise AutomationConstraint('Over remaining offer value {}'.format(offer.amount_from - total_bids_value))
|
||||||
return False
|
|
||||||
|
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:
|
if strategy.only_known_identities:
|
||||||
identity_stats = use_session.query(KnownIdentity).filter_by(address=bid.bid_addr).first()
|
identity_stats = use_session.query(KnownIdentity).filter_by(address=bid.bid_addr).first()
|
||||||
if not identity_stats:
|
if not identity_stats:
|
||||||
return False
|
raise AutomationConstraint('Unknown bidder')
|
||||||
|
|
||||||
# TODO: More options
|
# TODO: More options
|
||||||
if identity_stats.num_recv_bids_successful < 1:
|
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:
|
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
|
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:
|
except Exception as e:
|
||||||
self.log.error('shouldAutoAcceptBid: %s', str(e))
|
self.log.error('shouldAutoAcceptBid: %s', str(e))
|
||||||
return False
|
return False
|
||||||
|
@ -156,6 +156,7 @@ class EventLogTypes(IntEnum):
|
|||||||
LOCK_TX_A_REFUND_TX_SEEN = auto()
|
LOCK_TX_A_REFUND_TX_SEEN = auto()
|
||||||
LOCK_TX_A_REFUND_SPEND_TX_SEEN = auto()
|
LOCK_TX_A_REFUND_SPEND_TX_SEEN = auto()
|
||||||
ERROR = auto()
|
ERROR = auto()
|
||||||
|
AUTOMATION_CONSTRAINT = auto()
|
||||||
|
|
||||||
|
|
||||||
class XmrSplitMsgTypes(IntEnum):
|
class XmrSplitMsgTypes(IntEnum):
|
||||||
|
@ -21,6 +21,7 @@ class Concepts(IntEnum):
|
|||||||
OFFER = auto()
|
OFFER = auto()
|
||||||
BID = auto()
|
BID = auto()
|
||||||
NETWORK_MESSAGE = auto()
|
NETWORK_MESSAGE = auto()
|
||||||
|
AUTOMATION = auto()
|
||||||
|
|
||||||
|
|
||||||
def strConcepts(state):
|
def strConcepts(state):
|
||||||
|
@ -29,14 +29,14 @@ def upgradeDatabaseData(self, data_version):
|
|||||||
label='Accept All',
|
label='Accept All',
|
||||||
type_ind=Concepts.OFFER,
|
type_ind=Concepts.OFFER,
|
||||||
data=json.dumps({'exact_rate_only': True,
|
data=json.dumps({'exact_rate_only': True,
|
||||||
'max_bids': 1}).encode('utf-8'),
|
'max_concurrent_bids': 5}).encode('utf-8'),
|
||||||
only_known_identities=False))
|
only_known_identities=False))
|
||||||
session.add(AutomationStrategy(
|
session.add(AutomationStrategy(
|
||||||
active_ind=1,
|
active_ind=1,
|
||||||
label='Accept Known',
|
label='Accept Known',
|
||||||
type_ind=Concepts.OFFER,
|
type_ind=Concepts.OFFER,
|
||||||
data=json.dumps({'exact_rate_only': True,
|
data=json.dumps({'exact_rate_only': True,
|
||||||
'max_bids': 1}).encode('utf-8'),
|
'max_concurrent_bids': 5}).encode('utf-8'),
|
||||||
only_known_identities=True,
|
only_known_identities=True,
|
||||||
note='Accept bids from identities with previously successful swaps only'))
|
note='Accept bids from identities with previously successful swaps only'))
|
||||||
|
|
||||||
|
@ -21,6 +21,10 @@ class TemporaryError(ValueError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationConstraint(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def ensure(v, err_string):
|
def ensure(v, err_string):
|
||||||
if not v:
|
if not v:
|
||||||
raise ValueError(err_string)
|
raise ValueError(err_string)
|
||||||
|
@ -875,6 +875,8 @@ class Test(BaseTest):
|
|||||||
offer = swap_clients[1].listOffers(filters={'offer_id': offer_id})[0]
|
offer = swap_clients[1].listOffers(filters={'offer_id': offer_id})[0]
|
||||||
|
|
||||||
below_min_bid = min_bid - 1
|
below_min_bid = min_bid - 1
|
||||||
|
|
||||||
|
# Ensure bids below the minimum amount fails on sender and recipient.
|
||||||
try:
|
try:
|
||||||
bid_id = swap_clients[1].postBid(offer_id, below_min_bid)
|
bid_id = swap_clients[1].postBid(offer_id, below_min_bid)
|
||||||
except Exception as e:
|
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)
|
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)
|
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):
|
def test_10_locked_refundtx(self):
|
||||||
logging.info('---------- Test Refund tx is locked')
|
logging.info('---------- Test Refund tx is locked')
|
||||||
|
Loading…
Reference in New Issue
Block a user