mirror of
https://github.com/basicswap/basicswap.git
synced 2024-11-16 15:58:17 +00:00
automation: Accept multiple concurrent bids.
This commit is contained in:
parent
d909115ea4
commit
89c60851ac
6 changed files with 101 additions and 19 deletions
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -21,6 +21,7 @@ class Concepts(IntEnum):
|
|||
OFFER = auto()
|
||||
BID = auto()
|
||||
NETWORK_MESSAGE = auto()
|
||||
AUTOMATION = auto()
|
||||
|
||||
|
||||
def strConcepts(state):
|
||||
|
|
|
@ -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'))
|
||||
|
||||
|
|
|
@ -21,6 +21,10 @@ class TemporaryError(ValueError):
|
|||
pass
|
||||
|
||||
|
||||
class AutomationConstraint(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def ensure(v, err_string):
|
||||
if not v:
|
||||
raise ValueError(err_string)
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in a new issue