From b505c375afbd0b95c852c01816cf4bbb25b46be8 Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Tue, 15 Aug 2017 18:44:09 -0400 Subject: [PATCH] Finish delayed (live) betting. Remove the ability to change the event of a betting market group after creation. --- libraries/chain/betting_market_evaluator.cpp | 237 +++++++++--------- libraries/chain/db_bet.cpp | 14 ++ libraries/chain/db_block.cpp | 1 + libraries/chain/db_update.cpp | 47 ++++ .../chain/betting_market_evaluator.hpp | 24 +- .../graphene/chain/betting_market_object.hpp | 105 ++++++-- .../chain/include/graphene/chain/database.hpp | 1 + .../chain/protocol/betting_market.hpp | 4 +- .../wallet/include/graphene/wallet/wallet.hpp | 1 - libraries/wallet/wallet.cpp | 2 - tests/betting/betting_tests.cpp | 96 ++++++- tests/common/database_fixture.cpp | 7 +- tests/common/database_fixture.hpp | 1 - 13 files changed, 376 insertions(+), 164 deletions(-) diff --git a/libraries/chain/betting_market_evaluator.cpp b/libraries/chain/betting_market_evaluator.cpp index 5dbe31c0..eb88bb3e 100644 --- a/libraries/chain/betting_market_evaluator.cpp +++ b/libraries/chain/betting_market_evaluator.cpp @@ -42,7 +42,7 @@ void_result betting_market_rules_create_evaluator::do_evaluate(const betting_mar object_id_type betting_market_rules_create_evaluator::do_apply(const betting_market_rules_create_operation& op) { try { const betting_market_rules_object& new_betting_market_rules = - db().create( [&]( betting_market_rules_object& betting_market_rules_obj ) { + db().create([&](betting_market_rules_object& betting_market_rules_obj) { betting_market_rules_obj.name = op.name; betting_market_rules_obj.description = op.description; }); @@ -52,27 +52,25 @@ object_id_type betting_market_rules_create_evaluator::do_apply(const betting_mar void_result betting_market_rules_update_evaluator::do_evaluate(const betting_market_rules_update_operation& op) { try { FC_ASSERT(trx_state->_is_proposed_trx); - FC_ASSERT(op.new_name.valid() || op.new_description.valid()); + _rules = &op.betting_market_rules_id(db()); + FC_ASSERT(op.new_name.valid() || op.new_description.valid(), "nothing to update"); return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } void_result betting_market_rules_update_evaluator::do_apply(const betting_market_rules_update_operation& op) { try { - database& _db = db(); - _db.modify( - _db.get(op.betting_market_rules_id), - [&]( betting_market_rules_object& bmro ) - { - if( op.new_name.valid() ) - bmro.name = *op.new_name; - if( op.new_description.valid() ) - bmro.description = *op.new_description; - }); - return void_result(); + db().modify(*_rules, [&](betting_market_rules_object& betting_market_rules) { + if (op.new_name.valid()) + betting_market_rules.name = *op.new_name; + if (op.new_description.valid()) + betting_market_rules.description = *op.new_description; + }); + return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } void_result betting_market_group_create_evaluator::do_evaluate(const betting_market_group_create_operation& op) { try { + database& d = db(); FC_ASSERT(trx_state->_is_proposed_trx); // the event_id in the operation can be a relative id. If it is, @@ -84,10 +82,10 @@ void_result betting_market_group_create_evaluator::do_evaluate(const betting_mar FC_ASSERT(resolved_event_id.space() == event_id_type::space_id && resolved_event_id.type() == event_id_type::type_id, "event_id must refer to a event_id_type"); - event_id = resolved_event_id; - FC_ASSERT( db().find_object(event_id), "Invalid event specified" ); + _event_id = resolved_event_id; + FC_ASSERT(d.find_object(_event_id), "Invalid event specified"); - FC_ASSERT( db().find_object(op.asset_id), "Invalid asset specified" ); + FC_ASSERT(d.find_object(op.asset_id), "Invalid asset specified"); // the rules_id in the operation can be a relative id. If it is, // resolve it and verify that it is truly rules @@ -98,97 +96,101 @@ void_result betting_market_group_create_evaluator::do_evaluate(const betting_mar FC_ASSERT(resolved_rules_id.space() == betting_market_rules_id_type::space_id && resolved_rules_id.type() == betting_market_rules_id_type::type_id, "rules_id must refer to a betting_market_rules_id_type"); - rules_id = resolved_rules_id; - FC_ASSERT( db().find_object(rules_id), "Invalid rules specified" ); + _rules_id = resolved_rules_id; + FC_ASSERT(d.find_object(_rules_id), "Invalid rules specified"); return void_result(); -} FC_CAPTURE_AND_RETHROW( (op) ) } +} FC_CAPTURE_AND_RETHROW((op)) } object_id_type betting_market_group_create_evaluator::do_apply(const betting_market_group_create_operation& op) { try { const betting_market_group_object& new_betting_market_group = - db().create( [&]( betting_market_group_object& betting_market_group_obj ) { - betting_market_group_obj.event_id = event_id; - betting_market_group_obj.rules_id = rules_id; + db().create([&](betting_market_group_object& betting_market_group_obj) { + betting_market_group_obj.event_id = _event_id; + betting_market_group_obj.rules_id = _rules_id; betting_market_group_obj.description = op.description; betting_market_group_obj.asset_id = op.asset_id; betting_market_group_obj.frozen = false; betting_market_group_obj.delay_bets = false; - }); + }); return new_betting_market_group.id; } FC_CAPTURE_AND_RETHROW( (op) ) } void_result betting_market_group_update_evaluator::do_evaluate(const betting_market_group_update_operation& op) { try { + database& d = db(); FC_ASSERT(trx_state->_is_proposed_trx); - FC_ASSERT(op.new_event_id.valid() || - op.new_description.valid() || + _betting_market_group = &op.betting_market_group_id(d); + + FC_ASSERT(op.new_description.valid() || op.new_rules_id.valid() || op.freeze.valid() || op.delay_bets.valid(), "nothing to change"); - // the event_id in the operation can be a relative id. If it is, - // resolve it and verify that it is truly an event - if (op.new_event_id.valid()) - { - object_id_type resolved_event_id = *op.new_event_id; - if (is_relative(*op.new_event_id)) - resolved_event_id = get_relative_id(*op.new_event_id); - - FC_ASSERT(resolved_event_id.space() == event_id_type::space_id && - resolved_event_id.type() == event_id_type::type_id, - "event_id must refer to a event_id_type"); - event_id = resolved_event_id; - FC_ASSERT( db().find_object(event_id), "invalid event specified" ); - } - if (op.new_rules_id.valid()) { - // the rules_id in the operation can be a relative id. If it is, - // resolve it and verify that it is truly rules - object_id_type resolved_rules_id = *op.new_rules_id; - if (is_relative(*op.new_rules_id)) - resolved_rules_id = get_relative_id(*op.new_rules_id); + // the rules_id in the operation can be a relative id. If it is, + // resolve it and verify that it is truly rules + object_id_type resolved_rules_id = *op.new_rules_id; + if (is_relative(*op.new_rules_id)) + resolved_rules_id = get_relative_id(*op.new_rules_id); - FC_ASSERT(resolved_rules_id.space() == betting_market_rules_id_type::space_id && - resolved_rules_id.type() == betting_market_rules_id_type::type_id, - "rules_id must refer to a betting_market_rules_id_type"); - rules_id = resolved_rules_id; - FC_ASSERT( db().find_object(rules_id), "invalid rules specified" ); + FC_ASSERT(resolved_rules_id.space() == betting_market_rules_id_type::space_id && + resolved_rules_id.type() == betting_market_rules_id_type::type_id, + "rules_id must refer to a betting_market_rules_id_type"); + _rules_id = resolved_rules_id; + FC_ASSERT(d.find_object(_rules_id), "invalid rules specified"); } if (op.freeze.valid()) - { - const auto& _betting_market_group = &op.betting_market_group_id(db()); - FC_ASSERT(_betting_market_group->frozen != *op.freeze, "freeze would not change the state of the betting market group"); - } + FC_ASSERT(_betting_market_group->frozen != *op.freeze, "freeze would not change the state of the betting market group"); if (op.delay_bets.valid()) - { - const auto& _betting_market_group = &op.betting_market_group_id(db()); - FC_ASSERT(_betting_market_group->delay_bets != *op.delay_bets, "delay_bets would not change the state of the betting market group"); - } + FC_ASSERT(_betting_market_group->delay_bets != *op.delay_bets, "delay_bets would not change the state of the betting market group"); return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } void_result betting_market_group_update_evaluator::do_apply(const betting_market_group_update_operation& op) { try { - database& _db = db(); - _db.modify( - _db.get(op.betting_market_group_id), - [&]( betting_market_group_object& bmgo ) - { - if( op.new_description.valid() ) - bmgo.description = *op.new_description; - if( op.new_event_id.valid() ) - bmgo.event_id = event_id; - if( op.new_rules_id.valid() ) - bmgo.rules_id = rules_id; - if( op.freeze.valid() ) - bmgo.frozen = *op.freeze; - if( op.delay_bets.valid() ) - bmgo.delay_bets = *op.delay_bets; - }); - return void_result(); + database& d = db(); + d.modify(*_betting_market_group, [&](betting_market_group_object& betting_market_group) { + if (op.new_description.valid()) + betting_market_group.description = *op.new_description; + if (op.new_rules_id.valid()) + betting_market_group.rules_id = _rules_id; + if (op.freeze.valid()) + betting_market_group.frozen = *op.freeze; + if (op.delay_bets.valid()) + { + assert(_betting_market_group->delay_bets != *op.delay_bets); // we checked this in evaluate + betting_market_group.delay_bets = *op.delay_bets; + if (!*op.delay_bets) + { + // we have switched from delayed to not-delayed. if there are any delayed bets, + // push them through now. + const auto& bet_odds_idx = d.get_index_type().indices().get(); + auto bet_iter = bet_odds_idx.begin(); + bool last = bet_iter == bet_odds_idx.end() || !bet_iter->end_of_delay; + while (!last) + { + const bet_object& delayed_bet = *bet_iter; + ++bet_iter; + last = bet_iter == bet_odds_idx.end() || !bet_iter->end_of_delay; + + const betting_market_object& betting_market = delayed_bet.betting_market_id(d); + if (betting_market.group_id == op.betting_market_group_id && !_betting_market_group->frozen) + { + d.modify(delayed_bet, [](bet_object& bet_obj) { + // clear the end_of_delay, which will re-sort the bet into its place in the book + bet_obj.end_of_delay.reset(); + }); + + d.place_bet(delayed_bet); + } + } + } + } + }); + return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } void_result betting_market_create_evaluator::do_evaluate(const betting_market_create_operation& op) @@ -204,8 +206,8 @@ void_result betting_market_create_evaluator::do_evaluate(const betting_market_cr FC_ASSERT(resolved_betting_market_group_id.space() == betting_market_group_id_type::space_id && resolved_betting_market_group_id.type() == betting_market_group_id_type::type_id, "betting_market_group_id must refer to a betting_market_group_id_type"); - group_id = resolved_betting_market_group_id; - FC_ASSERT( db().find_object(group_id), "Invalid betting_market_group specified" ); + _group_id = resolved_betting_market_group_id; + FC_ASSERT(db().find_object(_group_id), "Invalid betting_market_group specified"); return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } @@ -213,32 +215,34 @@ void_result betting_market_create_evaluator::do_evaluate(const betting_market_cr object_id_type betting_market_create_evaluator::do_apply(const betting_market_create_operation& op) { try { const betting_market_object& new_betting_market = - db().create( [&]( betting_market_object& betting_market_obj ) { - betting_market_obj.group_id = group_id; + db().create([&](betting_market_object& betting_market_obj) { + betting_market_obj.group_id = _group_id; betting_market_obj.description = op.description; betting_market_obj.payout_condition = op.payout_condition; - }); + }); return new_betting_market.id; } FC_CAPTURE_AND_RETHROW( (op) ) } void_result betting_market_update_evaluator::do_evaluate(const betting_market_update_operation& op) { try { + database& d = db(); FC_ASSERT(trx_state->_is_proposed_trx); + _betting_market = &op.betting_market_id(d); FC_ASSERT(op.new_group_id.valid() || op.new_description.valid() || op.new_payout_condition.valid(), "nothing to change"); if (op.new_group_id.valid()) { - // the betting_market_group_id in the operation can be a relative id. If it is, - // resolve it and verify that it is truly an betting_market_group - object_id_type resolved_betting_market_group_id = *op.new_group_id; - if (is_relative(*op.new_group_id)) - resolved_betting_market_group_id = get_relative_id(*op.new_group_id); + // the betting_market_group_id in the operation can be a relative id. If it is, + // resolve it and verify that it is truly an betting_market_group + object_id_type resolved_betting_market_group_id = *op.new_group_id; + if (is_relative(*op.new_group_id)) + resolved_betting_market_group_id = get_relative_id(*op.new_group_id); - FC_ASSERT(resolved_betting_market_group_id.space() == betting_market_group_id_type::space_id && - resolved_betting_market_group_id.type() == betting_market_group_id_type::type_id, - "betting_market_group_id must refer to a betting_market_group_id_type"); - group_id = resolved_betting_market_group_id; - FC_ASSERT( db().find_object(group_id), "invalid betting_market_group specified" ); + FC_ASSERT(resolved_betting_market_group_id.space() == betting_market_group_id_type::space_id && + resolved_betting_market_group_id.type() == betting_market_group_id_type::type_id, + "betting_market_group_id must refer to a betting_market_group_id_type"); + _group_id = resolved_betting_market_group_id; + FC_ASSERT(d.find_object(_group_id), "invalid betting_market_group specified"); } return void_result(); @@ -246,19 +250,15 @@ void_result betting_market_update_evaluator::do_evaluate(const betting_market_up void_result betting_market_update_evaluator::do_apply(const betting_market_update_operation& op) { try { - database& _db = db(); - _db.modify( - _db.get(op.betting_market_id), - [&]( betting_market_object& bmo ) - { - if( op.new_group_id.valid() ) - bmo.group_id = group_id; - if( op.new_payout_condition.valid() ) - bmo.payout_condition = *op.new_payout_condition; - if( op.new_description.valid() ) - bmo.description = *op.new_description; - }); - return void_result(); + db().modify(*_betting_market, [&](betting_market_object& betting_market) { + if (op.new_group_id.valid()) + betting_market.group_id = _group_id; + if (op.new_payout_condition.valid()) + betting_market.payout_condition = *op.new_payout_condition; + if (op.new_description.valid()) + betting_market.description = *op.new_description; + }); + return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } void_result bet_place_evaluator::do_evaluate(const bet_place_operation& op) @@ -276,18 +276,18 @@ void_result bet_place_evaluator::do_evaluate(const bet_place_operation& op) _asset = &_betting_market_group->asset_id(d); FC_ASSERT( is_authorized_asset( d, *fee_paying_account, *_asset ) ); - const chain_parameters& current_params = d.get_global_properties().parameters; + _current_params = &d.get_global_properties().parameters; // are their odds valid - FC_ASSERT( op.backer_multiplier >= current_params.min_bet_multiplier && - op.backer_multiplier <= current_params.max_bet_multiplier, + FC_ASSERT( op.backer_multiplier >= _current_params->min_bet_multiplier && + op.backer_multiplier <= _current_params->max_bet_multiplier, "Bet odds are outside the blockchain's limits" ); - if (!current_params.permitted_betting_odds_increments.empty()) + if (!_current_params->permitted_betting_odds_increments.empty()) { bet_multiplier_type allowed_increment; - const auto iter = current_params.permitted_betting_odds_increments.upper_bound(op.backer_multiplier); - if (iter == current_params.permitted_betting_odds_increments.end()) - allowed_increment = std::prev(current_params.permitted_betting_odds_increments.end())->second; + const auto iter = _current_params->permitted_betting_odds_increments.upper_bound(op.backer_multiplier); + if (iter == _current_params->permitted_betting_odds_increments.end()) + allowed_increment = std::prev(_current_params->permitted_betting_odds_increments.end())->second; else allowed_increment = iter->second; FC_ASSERT(op.backer_multiplier % allowed_increment == 0, "Bet odds must be a multiple of ${allowed_increment}", ("allowed_increment", allowed_increment)); @@ -306,20 +306,22 @@ object_id_type bet_place_evaluator::do_apply(const bet_place_operation& op) { try { database& d = db(); const bet_object& new_bet = - d.create( [&]( bet_object& bet_obj ) { + d.create([&](bet_object& bet_obj) { bet_obj.bettor_id = op.bettor_id; bet_obj.betting_market_id = op.betting_market_id; bet_obj.amount_to_bet = op.amount_to_bet; bet_obj.backer_multiplier = op.backer_multiplier; bet_obj.back_or_lay = op.back_or_lay; - }); + if (_betting_market_group->delay_bets) + bet_obj.end_of_delay = d.head_block_time() + _current_params->live_betting_delay_time; + }); bet_id_type new_bet_id = new_bet.id; // save the bet id here, new_bet may be deleted during place_bet() d.adjust_balance(fee_paying_account->id, -op.amount_to_bet); - //bool bet_matched = - d.place_bet(new_bet); + if (!_betting_market_group->delay_bets || _current_params->live_betting_delay_time <= 0) + d.place_bet(new_bet); return new_bet_id; } FC_CAPTURE_AND_RETHROW( (op) ) } @@ -341,9 +343,9 @@ void_result bet_cancel_evaluator::do_apply(const bet_cancel_operation& op) void_result betting_market_group_resolve_evaluator::do_evaluate(const betting_market_group_resolve_operation& op) { try { - const database& d = db(); + database& d = db(); _betting_market_group = &op.betting_market_group_id(d); - db().validate_betting_market_group_resolutions(*_betting_market_group, op.resolutions); + d.validate_betting_market_group_resolutions(*_betting_market_group, op.resolutions); return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } @@ -355,8 +357,7 @@ void_result betting_market_group_resolve_evaluator::do_apply(const betting_marke void_result betting_market_group_cancel_unmatched_bets_evaluator::do_evaluate(const betting_market_group_cancel_unmatched_bets_operation& op) { try { - const database& d = db(); - _betting_market_group = &op.betting_market_group_id(d); + _betting_market_group = &op.betting_market_group_id(db()); return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } diff --git a/libraries/chain/db_bet.cpp b/libraries/chain/db_bet.cpp index dd9d85e9..9c8709b4 100644 --- a/libraries/chain/db_bet.cpp +++ b/libraries/chain/db_bet.cpp @@ -26,6 +26,8 @@ void database::cancel_bet( const bet_object& bet, bool create_virtual_op ) void database::cancel_all_unmatched_bets_on_betting_market(const betting_market_object& betting_market) { const auto& bet_odds_idx = get_index_type().indices().get(); + + // first, cancel all bets on the active books auto book_itr = bet_odds_idx.lower_bound(std::make_tuple(betting_market.id)); auto book_end = bet_odds_idx.upper_bound(std::make_tuple(betting_market.id)); while (book_itr != book_end) @@ -34,6 +36,18 @@ void database::cancel_all_unmatched_bets_on_betting_market(const betting_market_ ++book_itr; cancel_bet(*old_book_itr, true); } + + // then, cancel any delayed bets on that market. We don't have an index for + // that, so walk through all delayed bets + book_itr = bet_odds_idx.begin(); + while (book_itr != bet_odds_idx.end() && + book_itr->end_of_delay) + { + auto old_book_itr = book_itr; + ++book_itr; + if (old_book_itr->betting_market_id == betting_market.id) + cancel_bet(*old_book_itr, true); + } } void database::cancel_all_betting_markets_for_event(const event_object& event_obj) diff --git a/libraries/chain/db_block.cpp b/libraries/chain/db_block.cpp index 5d270537..e5ad1d92 100644 --- a/libraries/chain/db_block.cpp +++ b/libraries/chain/db_block.cpp @@ -540,6 +540,7 @@ void database::_apply_block( const signed_block& next_block ) perform_chain_maintenance(next_block, global_props); create_block_summary(next_block); + place_delayed_bets(); // must happen after update_global_dynamic_data() updates the time clear_expired_transactions(); clear_expired_proposals(); clear_expired_orders(); diff --git a/libraries/chain/db_update.cpp b/libraries/chain/db_update.cpp index 2ef09ecb..a643c989 100644 --- a/libraries/chain/db_update.cpp +++ b/libraries/chain/db_update.cpp @@ -188,6 +188,53 @@ void database::clear_expired_transactions() transaction_idx.remove(*dedupe_index.begin()); } FC_CAPTURE_AND_RETHROW() } +void database::place_delayed_bets() +{ try { + // If any bets have been placed during live betting where bets are delayed for a few seconds, see if there are + // any bets whose delays have expired. + + // Delayed bets are sorted to the beginning of the order book, so if there are any bets that need placing, + // they're right at the front of the book + const auto& bet_odds_idx = get_index_type().indices().get(); + auto iter = bet_odds_idx.begin(); + + // we use an awkward looping mechanism here because there's a case where we are processing the + // last delayed bet before the "real" order book starts and `iter` was pointing at the first + // real order. The place_bet() call can cause the that real order to be deleted, so we need + // to decide whether this is the last delayed bet before `place_bet` is called. + bool last = iter == bet_odds_idx.end() || + !iter->end_of_delay || + *iter->end_of_delay > head_block_time(); + while (!last) + { + const bet_object& bet_to_place = *iter; + ++iter; + + last = iter == bet_odds_idx.end() || + !iter->end_of_delay || + *iter->end_of_delay > head_block_time(); + + // it's possible that the betting market was active when the bet was placed, + // but has been frozen before the delay expired. If that's the case here, + // don't try to match the bet. + // Since this check happens every block, this could impact performance if a + // market with many delayed bets is frozen for a long time. + // Our current understanding is that the witnesses will typically cancel all unmatched + // bets on frozen markets to avoid this. + const betting_market_object& betting_market = bet_to_place.betting_market_id(*this); + const betting_market_group_object& betting_market_group = betting_market.group_id(*this); + if (!betting_market_group.frozen) + { + modify(bet_to_place, [](bet_object& bet_obj) { + // clear the end_of_delay, which will re-sort the bet into its place in the book + bet_obj.end_of_delay.reset(); + }); + + place_bet(bet_to_place); + } + } +} FC_CAPTURE_AND_RETHROW() } + void database::clear_expired_proposals() { const auto& proposal_expiration_index = get_index_type().indices().get(); diff --git a/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp b/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp index 84937acf..9967979d 100644 --- a/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp +++ b/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp @@ -45,6 +45,8 @@ namespace graphene { namespace chain { void_result do_evaluate( const betting_market_rules_update_operation& o ); void_result do_apply( const betting_market_rules_update_operation& o ); + private: + const betting_market_rules_object* _rules; }; class betting_market_group_create_evaluator : public evaluator @@ -52,11 +54,11 @@ namespace graphene { namespace chain { public: typedef betting_market_group_create_operation operation_type; - void_result do_evaluate( const betting_market_group_create_operation& o ); - object_id_type do_apply( const betting_market_group_create_operation& o ); + void_result do_evaluate(const betting_market_group_create_operation& o); + object_id_type do_apply(const betting_market_group_create_operation& o); private: - event_id_type event_id; - betting_market_rules_id_type rules_id; + event_id_type _event_id; + betting_market_rules_id_type _rules_id; }; class betting_market_group_update_evaluator : public evaluator @@ -64,11 +66,11 @@ namespace graphene { namespace chain { public: typedef betting_market_group_update_operation operation_type; - void_result do_evaluate( const betting_market_group_update_operation& o ); - void_result do_apply( const betting_market_group_update_operation& o ); + void_result do_evaluate(const betting_market_group_update_operation& o); + void_result do_apply(const betting_market_group_update_operation& o); private: - event_id_type event_id; - betting_market_rules_id_type rules_id; + betting_market_rules_id_type _rules_id; + const betting_market_group_object* _betting_market_group; }; class betting_market_create_evaluator : public evaluator @@ -79,7 +81,7 @@ namespace graphene { namespace chain { void_result do_evaluate( const betting_market_create_operation& o ); object_id_type do_apply( const betting_market_create_operation& o ); private: - betting_market_group_id_type group_id; + betting_market_group_id_type _group_id; }; class betting_market_update_evaluator : public evaluator @@ -90,7 +92,8 @@ namespace graphene { namespace chain { void_result do_evaluate( const betting_market_update_operation& o ); void_result do_apply( const betting_market_update_operation& o ); private: - betting_market_group_id_type group_id; + const betting_market_object* _betting_market; + betting_market_group_id_type _group_id; }; class bet_place_evaluator : public evaluator @@ -103,6 +106,7 @@ namespace graphene { namespace chain { private: const betting_market_group_object* _betting_market_group; const betting_market_object* _betting_market; + const chain_parameters* _current_params; const asset_object* _asset; share_type _stake_plus_fees; }; diff --git a/libraries/chain/include/graphene/chain/betting_market_object.hpp b/libraries/chain/include/graphene/chain/betting_market_object.hpp index 29b7e7ad..23a7baf9 100644 --- a/libraries/chain/include/graphene/chain/betting_market_object.hpp +++ b/libraries/chain/include/graphene/chain/betting_market_object.hpp @@ -98,6 +98,8 @@ class bet_object : public graphene::db::abstract_object< bet_object > bet_type back_or_lay; + fc::optional end_of_delay; + static share_type get_approximate_matching_amount(share_type bet_amount, bet_multiplier_type backer_multiplier, bet_type back_or_lay, bool round_up = false); // returns the amount of a bet that completely matches this bet @@ -161,78 +163,121 @@ typedef generic_index bool operator() (const std::tuple& lhs, const bet_object& rhs) const { - return compare(std::get<0>(lhs), rhs.betting_market_id); + return compare(fc::optional(), std::get<0>(lhs), rhs.end_of_delay, rhs.betting_market_id); } template bool operator() (const bet_object& lhs, const std::tuple& rhs) const { - return compare(lhs.betting_market_id, std::get<0>(rhs)); + return compare(lhs.end_of_delay, lhs.betting_market_id, fc::optional(), std::get<0>(rhs)); } template bool operator() (const std::tuple& lhs, const bet_object& rhs) const { - return compare(std::get<0>(lhs), std::get<1>(lhs), rhs.betting_market_id, rhs.back_or_lay); + return compare(fc::optional(), std::get<0>(lhs), std::get<1>(lhs), rhs.end_of_delay, rhs.betting_market_id, rhs.back_or_lay); } template bool operator() (const bet_object& lhs, const std::tuple& rhs) const { - return compare(lhs.betting_market_id, lhs.back_or_lay, std::get<0>(rhs), std::get<1>(rhs)); + return compare(lhs.end_of_delay, lhs.betting_market_id, lhs.back_or_lay, fc::optional(), std::get<0>(rhs), std::get<1>(rhs)); } template bool operator() (const std::tuple& lhs, const bet_object& rhs) const { - return compare(std::get<0>(lhs), std::get<1>(lhs), std::get<2>(lhs), - rhs.betting_market_id, rhs.back_or_lay, rhs.backer_multiplier); + return compare(fc::optional(), std::get<0>(lhs), std::get<1>(lhs), std::get<2>(lhs), + rhs.end_of_delay, rhs.betting_market_id, rhs.back_or_lay, rhs.backer_multiplier); } template bool operator() (const bet_object& lhs, const std::tuple& rhs) const { - return compare(lhs.betting_market_id, lhs.back_or_lay, lhs.backer_multiplier, - std::get<0>(rhs), std::get<1>(rhs), std::get<2>(rhs)); + return compare(lhs.end_of_delay, lhs.betting_market_id, lhs.back_or_lay, lhs.backer_multiplier, + fc::optional(), std::get<0>(rhs), std::get<1>(rhs), std::get<2>(rhs)); } template bool operator() (const std::tuple& lhs, const bet_object& rhs) const { - return compare(std::get<0>(lhs), std::get<1>(lhs), std::get<2>(lhs), std::get<3>(lhs), - rhs.betting_market_id, rhs.back_or_lay, rhs.backer_multiplier, rhs.id); + return compare(fc::optional(), std::get<0>(lhs), std::get<1>(lhs), std::get<2>(lhs), std::get<3>(lhs), + rhs.end_of_delay, rhs.betting_market_id, rhs.back_or_lay, rhs.backer_multiplier, rhs.id); } template bool operator() (const bet_object& lhs, const std::tuple& rhs) const { - return compare(lhs.betting_market_id, lhs.back_or_lay, lhs.backer_multiplier, lhs.id, - std::get<0>(rhs), std::get<1>(rhs), std::get<2>(rhs), std::get<3>(rhs)); + return compare(lhs.end_of_delay, lhs.betting_market_id, lhs.back_or_lay, lhs.backer_multiplier, lhs.id, + fc::optional(), std::get<0>(rhs), std::get<1>(rhs), std::get<2>(rhs), std::get<3>(rhs)); } - bool compare(const betting_market_id_type& lhs_betting_market_id, const betting_market_id_type& rhs_betting_market_id) const + bool compare(const fc::optional& lhs_end_of_delay, + const betting_market_id_type& lhs_betting_market_id, + const fc::optional& rhs_end_of_delay, + const betting_market_id_type& rhs_betting_market_id) const { + // if either bet is delayed, sort the delayed bet to the + // front. If both are delayed, the delay expiring soonest + // comes first. + if (lhs_end_of_delay || rhs_end_of_delay) + { + if (!rhs_end_of_delay) + return true; + if (!lhs_end_of_delay) + return false; + return *lhs_end_of_delay < *rhs_end_of_delay; + } + return lhs_betting_market_id < rhs_betting_market_id; } - bool compare(const betting_market_id_type& lhs_betting_market_id, bet_type lhs_bet_type, + bool compare(const fc::optional& lhs_end_of_delay, + const betting_market_id_type& lhs_betting_market_id, bet_type lhs_bet_type, + const fc::optional& rhs_end_of_delay, const betting_market_id_type& rhs_betting_market_id, bet_type rhs_bet_type) const { + // if either bet is delayed, sort the delayed bet to the + // front. If both are delayed, the delay expiring soonest + // comes first. + if (lhs_end_of_delay || rhs_end_of_delay) + { + if (!rhs_end_of_delay) + return true; + if (!lhs_end_of_delay) + return false; + return *lhs_end_of_delay < *rhs_end_of_delay; + } + if (lhs_betting_market_id < rhs_betting_market_id) return true; if (lhs_betting_market_id > rhs_betting_market_id) return false; return lhs_bet_type < rhs_bet_type; } - bool compare(const betting_market_id_type& lhs_betting_market_id, bet_type lhs_bet_type, + bool compare(const fc::optional& lhs_end_of_delay, + const betting_market_id_type& lhs_betting_market_id, bet_type lhs_bet_type, bet_multiplier_type lhs_backer_multiplier, + const fc::optional& rhs_end_of_delay, const betting_market_id_type& rhs_betting_market_id, bet_type rhs_bet_type, bet_multiplier_type rhs_backer_multiplier) const { + // if either bet is delayed, sort the delayed bet to the + // front. If both are delayed, the delay expiring soonest + // comes first. + if (lhs_end_of_delay || rhs_end_of_delay) + { + if (!rhs_end_of_delay) + return true; + if (!lhs_end_of_delay) + return false; + return *lhs_end_of_delay < *rhs_end_of_delay; + } + if (lhs_betting_market_id < rhs_betting_market_id) return true; if (lhs_betting_market_id > rhs_betting_market_id) @@ -246,11 +291,33 @@ struct compare_bet_by_odds { else return lhs_backer_multiplier > rhs_backer_multiplier; } - bool compare(const betting_market_id_type& lhs_betting_market_id, bet_type lhs_bet_type, + bool compare(const fc::optional& lhs_end_of_delay, + const betting_market_id_type& lhs_betting_market_id, bet_type lhs_bet_type, bet_multiplier_type lhs_backer_multiplier, const bet_id_type& lhs_bet_id, + const fc::optional& rhs_end_of_delay, const betting_market_id_type& rhs_betting_market_id, bet_type rhs_bet_type, bet_multiplier_type rhs_backer_multiplier, const bet_id_type& rhs_bet_id) const { + + // if either bet is delayed, sort the delayed bet to the + // front. If both are delayed, the delay expiring soonest + // comes first. + if (lhs_end_of_delay || rhs_end_of_delay) + { + if (!rhs_end_of_delay) + return true; + if (!lhs_end_of_delay) + return false; + if (*lhs_end_of_delay < *rhs_end_of_delay) + return true; + if (*lhs_end_of_delay > *rhs_end_of_delay) + return false; + // if both bets have the same delay, prefer the one + // that was placed first (lowest id) + return lhs_bet_id < rhs_bet_id; + } + + // if neither bet was delayed if (lhs_betting_market_id < rhs_betting_market_id) return true; if (lhs_betting_market_id > rhs_betting_market_id) @@ -451,6 +518,6 @@ typedef generic_index new_description; - optional new_event_id; - optional new_rules_id; optional freeze; @@ -388,7 +386,7 @@ FC_REFLECT( graphene::chain::betting_market_group_create_operation, FC_REFLECT( graphene::chain::betting_market_group_update_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::betting_market_group_update_operation, - (fee)(betting_market_group_id)(new_description)(new_event_id)(new_rules_id)(freeze)(delay_bets)(extensions) ) + (fee)(betting_market_group_id)(new_description)(new_rules_id)(freeze)(delay_bets)(extensions) ) FC_REFLECT( graphene::chain::betting_market_create_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::betting_market_create_operation, diff --git a/libraries/wallet/include/graphene/wallet/wallet.hpp b/libraries/wallet/include/graphene/wallet/wallet.hpp index 53c2d785..9d2d4ec6 100644 --- a/libraries/wallet/include/graphene/wallet/wallet.hpp +++ b/libraries/wallet/include/graphene/wallet/wallet.hpp @@ -1658,7 +1658,6 @@ class wallet_api fc::time_point_sec expiration_time, betting_market_group_id_type betting_market_group_id, fc::optional description, - fc::optional event_id, fc::optional rules_id, fc::optional freeze, bool broadcast = false); diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index 3448f6d9..4da63d2a 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -5308,7 +5308,6 @@ signed_transaction wallet_api::propose_update_betting_market_group( fc::time_point_sec expiration_time, betting_market_group_id_type betting_market_group_id, fc::optional description, - fc::optional event_id, fc::optional rules_id, fc::optional freeze, bool broadcast /*= false*/) @@ -5319,7 +5318,6 @@ signed_transaction wallet_api::propose_update_betting_market_group( betting_market_group_update_operation betting_market_group_update_op; betting_market_group_update_op.betting_market_group_id = betting_market_group_id; betting_market_group_update_op.new_description = description; - betting_market_group_update_op.new_event_id = event_id; betting_market_group_update_op.new_rules_id = rules_id; betting_market_group_update_op.freeze = freeze; diff --git a/tests/betting/betting_tests.cpp b/tests/betting/betting_tests.cpp index 2c298a43..8a92f558 100644 --- a/tests/betting/betting_tests.cpp +++ b/tests/betting/betting_tests.cpp @@ -388,6 +388,92 @@ BOOST_AUTO_TEST_CASE(persistent_objects_test) FC_LOG_AND_RETHROW() } +BOOST_AUTO_TEST_CASE(delayed_bets_test) // test live betting +{ + try + { + const auto& bet_odds_idx = db.get_index_type().indices().get(); + + ACTORS( (alice)(bob) ); + + { + // we're generating blocks, and the hacky way we keep references to the objects generated + // in this macro doesn't work, so do this in an anonymous scope to prevent us from using + // the variables it declares outside later in the function + + CREATE_ICE_HOCKEY_BETTING_MARKET(); + } + + generate_blocks(1); + + BOOST_REQUIRE_EQUAL(db.get_index_type().indices().get().size(), 1); + const betting_market_group_object& caps_vs_blackhawks_moneyline_betting_market = *db.get_index_type().indices().get().begin(); + BOOST_REQUIRE_EQUAL(db.get_index_type().indices().get().size(), 2); + const betting_market_object& capitals_win_market = *db.get_index_type().indices().get().begin(); + + + + update_betting_market_group(caps_vs_blackhawks_moneyline_betting_market.id, + fc::optional(), + fc::optional(), + fc::optional(), + true); + generate_blocks(1); + + transfer(account_id_type(), alice_id, asset(10000000)); + transfer(account_id_type(), bob_id, asset(10000000)); + share_type alice_expected_balance = 10000000; + share_type bob_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + + generate_blocks(1); + + BOOST_TEST_MESSAGE("Testing basic delayed bet mechanics"); + // alice backs 100 at odds 2 + BOOST_TEST_MESSAGE("Alice places a back bet of 100 at odds 2.0"); + bet_id_type delayed_back_bet = place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + generate_blocks(1); + + // verify the bet hasn't been placed in the active book + auto first_bet_in_market = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + BOOST_CHECK(first_bet_in_market == bet_odds_idx.end()); + + // after 3 blocks, the delay should have expired and it will be promoted to the active book + generate_blocks(2); + first_bet_in_market = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + auto last_bet_in_market = bet_odds_idx.upper_bound(std::make_tuple(capitals_win_market.id)); + BOOST_CHECK(first_bet_in_market != bet_odds_idx.end()); + BOOST_CHECK(std::distance(first_bet_in_market, last_bet_in_market) == 1); + + // bob lays 100 at odds 2 to match alice's bet currently on the books + BOOST_TEST_MESSAGE("Bob places a lay bet of 100 at odds 2.0"); + bet_id_type delayed_lay_bet = place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + generate_blocks(1); + + // bob's bet will still be delayed, so the active order book will only contain alice's bet + first_bet_in_market = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + last_bet_in_market = bet_odds_idx.upper_bound(std::make_tuple(capitals_win_market.id)); + BOOST_CHECK(std::distance(first_bet_in_market, last_bet_in_market) == 1); + + // once bob's bet's delay has expired, the two bets will annihilate each other, leaving + // an empty order book + generate_blocks(2); + BOOST_CHECK(bet_odds_idx.empty()); + + // now test that when we cancel all bets on a market, delayed bets get canceled too + BOOST_TEST_MESSAGE("Alice places a back bet of 100 at odds 2.0"); + delayed_back_bet = place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + generate_blocks(1); + first_bet_in_market = bet_odds_idx.lower_bound(std::make_tuple(capitals_win_market.id)); + BOOST_CHECK(!bet_odds_idx.empty()); + BOOST_CHECK(first_bet_in_market == bet_odds_idx.end()); + BOOST_TEST_MESSAGE("Cancel all bets"); + cancel_unmatched_bets(caps_vs_blackhawks_moneyline_betting_market.id); + BOOST_CHECK(bet_odds_idx.empty()); + } + FC_LOG_AND_RETHROW() +} BOOST_AUTO_TEST_CASE( chained_market_create_test ) { @@ -785,10 +871,10 @@ BOOST_AUTO_TEST_CASE(betting_market_group_update_test) fc::optional new_rule = new_betting_market_rules.id; fc::optional empty_bool; - update_betting_market_group(moneyline_betting_markets.id, new_desc, empty_object_id, empty_object_id, empty_bool, empty_bool); - update_betting_market_group(moneyline_betting_markets.id, dempty, new_event, empty_object_id, empty_bool, empty_bool); - update_betting_market_group(moneyline_betting_markets.id, dempty, empty_object_id, new_rule, empty_bool, empty_bool); - update_betting_market_group(moneyline_betting_markets.id, new_desc, new_event, new_rule, empty_bool, empty_bool); + update_betting_market_group(moneyline_betting_markets.id, new_desc, empty_object_id, empty_bool, empty_bool); + update_betting_market_group(moneyline_betting_markets.id, dempty, empty_object_id, empty_bool, empty_bool); + update_betting_market_group(moneyline_betting_markets.id, dempty, new_rule, empty_bool, empty_bool); + update_betting_market_group(moneyline_betting_markets.id, new_desc, new_rule, empty_bool, empty_bool); transfer(account_id_type(), bob_id, asset(10000000)); place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); @@ -1044,7 +1130,7 @@ BOOST_AUTO_TEST_CASE( wimbledon_2017_gentelmen_singles_final_test ) fc::optional empty_bool; fc::optional true_bool = true; update_betting_market_group(moneyline_cilic_vs_federer_id, empty_str, - empty_obj, empty_obj, + empty_obj, empty_bool, true_bool); place_bet(alice_id, cilic_wins_final_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index e3766b6f..2224db67 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -1357,21 +1357,18 @@ const betting_market_group_object& database_fixture::create_betting_market_group void database_fixture::update_betting_market_group(betting_market_group_id_type betting_market_group_id, fc::optional description, - fc::optional event_id, fc::optional rules_id, fc::optional freeze, - fc::optional delay_bets - ) + fc::optional delay_bets) { try { betting_market_group_update_operation betting_market_group_update_op; betting_market_group_update_op.betting_market_group_id = betting_market_group_id; betting_market_group_update_op.new_description = description; - betting_market_group_update_op.new_event_id = event_id; betting_market_group_update_op.new_rules_id = rules_id; betting_market_group_update_op.freeze = freeze; betting_market_group_update_op.delay_bets = delay_bets; process_operation_by_witnesses(betting_market_group_update_op); -} FC_CAPTURE_AND_RETHROW( (betting_market_group_id)(description)(event_id)(rules_id)(freeze)(delay_bets)) } +} FC_CAPTURE_AND_RETHROW( (betting_market_group_id)(description)(rules_id)(freeze)(delay_bets)) } const betting_market_object& database_fixture::create_betting_market(betting_market_group_id_type group_id, internationalized_string_type payout_condition) diff --git a/tests/common/database_fixture.hpp b/tests/common/database_fixture.hpp index 2e46c33f..ff84c190 100644 --- a/tests/common/database_fixture.hpp +++ b/tests/common/database_fixture.hpp @@ -309,7 +309,6 @@ struct database_fixture { const betting_market_group_object& create_betting_market_group(internationalized_string_type description, event_id_type event_id, betting_market_rules_id_type rules_id, asset_id_type asset_id); void update_betting_market_group(betting_market_group_id_type betting_market_group_id, fc::optional description, - fc::optional event_id, fc::optional rules_id, fc::optional freeze, fc::optional delay_bets);