diff --git a/genesis.json b/genesis.json index 99412cfc..8baacd60 100644 --- a/genesis.json +++ b/genesis.json @@ -1,5 +1,5 @@ { - "initial_timestamp": "2017-07-18T18:33:30", + "initial_timestamp": "2018-02-01T00:00:00", "max_core_supply": "1000000000000000", "initial_parameters": { "current_fees": { @@ -243,7 +243,7 @@ } ],[ 57,{ - "fee": 100000 + "fee": 1 } ],[ 58,{ @@ -261,7 +261,7 @@ 62,{} ],[ 63,{ - "fee": 100000 + "fee": 0 } ],[ 64,{} @@ -295,7 +295,7 @@ ], "scale": 10000 }, - "block_interval": 5, + "block_interval": 3, "maintenance_interval": 600, "maintenance_skip_slots": 3, "committee_proposal_review_period": 900, @@ -536,4 +536,4 @@ "num_special_accounts": 0, "num_special_assets": 0 } -} \ No newline at end of file +} diff --git a/libraries/chain/CMakeLists.txt b/libraries/chain/CMakeLists.txt index 832ac6fd..399720fa 100644 --- a/libraries/chain/CMakeLists.txt +++ b/libraries/chain/CMakeLists.txt @@ -102,10 +102,11 @@ add_library( graphene_chain event_group_evaluator.cpp protocol/event.cpp event_evaluator.cpp + event_object.cpp protocol/betting_market.cpp betting_market_evaluator.cpp betting_market_object.cpp - db_bet.cpp + betting_market_group_object.cpp ${HEADERS} ${PROTOCOL_HEADERS} diff --git a/libraries/chain/betting_market_evaluator.cpp b/libraries/chain/betting_market_evaluator.cpp index eb88bb3e..70ec164f 100644 --- a/libraries/chain/betting_market_evaluator.cpp +++ b/libraries/chain/betting_market_evaluator.cpp @@ -21,6 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ +#define DEFAULT_LOGGER "betting" #include #include @@ -109,8 +110,8 @@ object_id_type betting_market_group_create_evaluator::do_apply(const betting_mar 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; + betting_market_group_obj.never_in_play = op.never_in_play; + betting_market_group_obj.delay_before_settling = op.delay_before_settling; }); return new_betting_market_group.id; } FC_CAPTURE_AND_RETHROW( (op) ) } @@ -121,12 +122,9 @@ void_result betting_market_group_update_evaluator::do_evaluate(const betting_mar FC_ASSERT(trx_state->_is_proposed_trx); _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"); + FC_ASSERT(op.new_description || op.new_rules_id || op.status, "nothing to change"); - if (op.new_rules_id.valid()) + if (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 @@ -141,11 +139,9 @@ void_result betting_market_group_update_evaluator::do_evaluate(const betting_mar FC_ASSERT(d.find_object(_rules_id), "invalid rules specified"); } - if (op.freeze.valid()) - FC_ASSERT(_betting_market_group->frozen != *op.freeze, "freeze would not change the state of the betting market group"); + if (op.status) + FC_ASSERT(_betting_market_group->get_status() != *op.status, "status would not change the state of the betting market group"); - if (op.delay_bets.valid()) - 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) ) } @@ -153,39 +149,40 @@ void_result betting_market_group_update_evaluator::do_apply(const betting_market { try { database& d = db(); d.modify(*_betting_market_group, [&](betting_market_group_object& betting_market_group) { - if (op.new_description.valid()) + if (op.new_description) betting_market_group.description = *op.new_description; - if (op.new_rules_id.valid()) + if (op.new_rules_id) betting_market_group.rules_id = _rules_id; - if (op.freeze.valid()) - betting_market_group.frozen = *op.freeze; - if (op.delay_bets.valid()) + + bool bets_were_delayed = betting_market_group.bets_are_delayed(); + if (op.status) + betting_market_group.dispatch_new_status(d, *op.status); + + bool bets_are_delayed = betting_market_group.bets_are_delayed(); + + // if we have transitioned from in-play to not-in-play-but-still-accepting-bets, + // place all delayed bets now + if (betting_market_group.bets_are_allowed() && + bets_were_delayed && !bets_are_delayed) { - 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) + 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) { - // 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) { - const bet_object& delayed_bet = *bet_iter; - ++bet_iter; - last = bet_iter == bet_odds_idx.end() || !bet_iter->end_of_delay; + 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(); + }); - 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); - } + d.place_bet(delayed_bet); } } } @@ -271,7 +268,9 @@ void_result bet_place_evaluator::do_evaluate(const bet_place_operation& op) FC_ASSERT( op.amount_to_bet.asset_id == _betting_market_group->asset_id, "Asset type bet does not match the market's asset type" ); - FC_ASSERT( !_betting_market_group->frozen, "Unable to place bets while the market is frozen" ); + ddump((_betting_market_group->get_status())); + FC_ASSERT( _betting_market_group->get_status() != betting_market_group_status::frozen, + "Unable to place bets while the market is frozen" ); _asset = &_betting_market_group->asset_id(d); FC_ASSERT( is_authorized_asset( d, *fee_paying_account, *_asset ) ); @@ -312,15 +311,19 @@ object_id_type bet_place_evaluator::do_apply(const bet_place_operation& op) 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; + if (_betting_market_group->bets_are_delayed()) { + // the bet will be included in the block at time `head_block_time() + block_interval`, so make the delay relative + // to the time it's included in a block + bet_obj.end_of_delay = d.head_block_time() + _current_params->block_interval + _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); - if (!_betting_market_group->delay_bets || _current_params->live_betting_delay_time <= 0) + ddump((_betting_market_group->bets_are_delayed())(_current_params->live_betting_delay_time)); + if (!_betting_market_group->bets_are_delayed() || _current_params->live_betting_delay_time <= 0) d.place_bet(new_bet); return new_bet_id; diff --git a/libraries/chain/betting_market_group_object.cpp b/libraries/chain/betting_market_group_object.cpp new file mode 100644 index 00000000..af2c77b2 --- /dev/null +++ b/libraries/chain/betting_market_group_object.cpp @@ -0,0 +1,541 @@ +#define DEFAULT_LOGGER "betting" +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace graphene { namespace chain { + enum class betting_market_group_state { + upcoming, + frozen_upcoming, + in_play, + frozen_in_play, + closed, + graded, + canceled, + settled + }; +} } + +FC_REFLECT_ENUM(graphene::chain::betting_market_group_state, + (upcoming) + (frozen_upcoming) + (in_play) + (frozen_in_play) + (closed) + (graded) + (canceled) + (settled)) + +namespace graphene { namespace chain { + +namespace msm = boost::msm; +namespace mpl = boost::mpl; + +// betting market object implementation +namespace +{ + // Events -- most events happen when the witnesses publish an update operation with a new + // status, so if they publish an event with the status set to `frozen`, we'll generate a `frozen_event` + struct upcoming_event + { + database& db; + upcoming_event(database& db) : db(db) {} + }; + struct frozen_event + { + database& db; + frozen_event(database& db) : db(db) {} + }; + struct in_play_event + { + database& db; + in_play_event(database& db) : db(db) {} + }; + struct closed_event + { + database& db; + bool closed_by_event; + closed_event(database& db, bool closed_by_event) : db(db), closed_by_event(closed_by_event) {} + }; + struct graded_event + { + database& db; + graded_event(database& db) : db(db) {} + }; + struct re_grading_event + { + database& db; + re_grading_event(database& db) : db(db) {} + }; + struct settled_event + { + database& db; + settled_event(database& db) : db(db) {} + }; + struct canceled_event + { + database& db; + + // true if this was triggered by setting event to canceled state, + // false if this was triggered directly on this betting market group + bool canceled_by_event; + + canceled_event(database& db, bool canceled_by_event = false) : db(db), canceled_by_event(canceled_by_event) {} + }; + + // Events + struct betting_market_group_state_machine_ : public msm::front::state_machine_def + { + // disable a few state machine features we don't use for performance + typedef int no_exception_thrown; + typedef int no_message_queue; + + // States + struct upcoming : public msm::front::state<> + { + template + void on_entry(const Event& event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> upcoming", ("id", fsm.betting_market_group_obj->id)); + // when a betting market group goes from frozen -> upcoming, transition the markets from frozen -> unresolved + auto& betting_market_index = event.db.template get_index_type().indices().template get(); + for (const betting_market_object& betting_market : + boost::make_iterator_range(betting_market_index.equal_range(fsm.betting_market_group_obj->id))) + try + { + event.db.modify(betting_market, [&event](betting_market_object& betting_market) { + betting_market.on_unresolved_event(event.db); + }); + } + catch (const graphene::chain::no_transition&) + { + } + } + }; + struct frozen_upcoming : public msm::front::state<> + { + template + void on_entry(const Event& event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> frozen_upcoming", ("id", fsm.betting_market_group_obj->id)); + + auto& betting_market_index = event.db.template get_index_type().indices().template get(); + for (const betting_market_object& betting_market : + boost::make_iterator_range(betting_market_index.equal_range(fsm.betting_market_group_obj->id))) + event.db.modify(betting_market, [&event](betting_market_object& betting_market) { + betting_market.on_frozen_event(event.db); + }); + } + }; + + struct in_play : public msm::front::state<> { + template + void on_entry(const Event& event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> in_play", ("id", fsm.betting_market_group_obj->id)); + // when an event goes in-play, cancel all unmatched bets in its betting markets + auto& betting_market_index = event.db.template get_index_type().indices().template get(); + for (const betting_market_object& betting_market : + boost::make_iterator_range(betting_market_index.equal_range(fsm.betting_market_group_obj->id))) + event.db.modify(betting_market, [&event](betting_market_object& betting_market) { + betting_market.cancel_all_unmatched_bets(event.db); + try { + event.db.modify(betting_market, [&event](betting_market_object& betting_market) { + betting_market.on_unresolved_event(event.db); + }); + } catch (const graphene::chain::no_transition&) { + // if this wasn't a transition from frozen state, this wasn't necessary + } + }); + } + }; + struct frozen_in_play : public msm::front::state<> { + template + void on_entry(const Event& event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> frozen_in_play", ("id", fsm.betting_market_group_obj->id)); + auto& betting_market_index = event.db.template get_index_type().indices().template get(); + for (const betting_market_object& betting_market : + boost::make_iterator_range(betting_market_index.equal_range(fsm.betting_market_group_obj->id))) + event.db.modify(betting_market, [&event](betting_market_object& betting_market) { + betting_market.on_frozen_event(event.db); + }); + } + }; + struct closed : public msm::front::state<> { + template + void on_entry(const Event& fsm_event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> closed", ("id", fsm.betting_market_group_obj->id)); + auto& betting_market_index = fsm_event.db.template get_index_type().indices().template get(); + for (const betting_market_object& betting_market : + boost::make_iterator_range(betting_market_index.equal_range(fsm.betting_market_group_obj->id))) + fsm_event.db.modify(betting_market, [&fsm_event](betting_market_object& betting_market) { + betting_market.cancel_all_unmatched_bets(fsm_event.db); + betting_market.on_closed_event(fsm_event.db); + }); + + // then notify the event that this betting market is now closed so it can change its status accordingly + if (!fsm_event.closed_by_event) { + const event_object& event = fsm.betting_market_group_obj->event_id(fsm_event.db); + fsm_event.db.modify(event, [&fsm_event,&fsm](event_object& event_obj) { + event_obj.on_betting_market_group_closed(fsm_event.db, fsm.betting_market_group_obj->id); + }); + } + } + }; + struct graded : public msm::front::state<> { + template + void on_entry(const Event& event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> graded", ("id", fsm.betting_market_group_obj->id)); + fsm.betting_market_group_obj->settling_time = event.db.head_block_time() + fsm.betting_market_group_obj->delay_before_settling; + dlog("grading complete, setting settling time to ${settling_time}", ("settling_time", fsm.betting_market_group_obj->settling_time)); + } + }; + struct re_grading : public msm::front::state<> { + template + void on_entry(const Event& event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> re_grading", ("id", fsm.betting_market_group_obj->id)); + } + }; + struct settled : public msm::front::state<> { + template + void on_entry(const Event& fsm_event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> settled", ("id", fsm.betting_market_group_obj->id)); + // TODO: what triggers this? I guess it will be the blockchain when its settling delay expires. So in that case, it should + // trigger the payout in the betting markets + auto& betting_market_index = fsm_event.db.template get_index_type().indices().template get(); + for (const betting_market_object& betting_market : + boost::make_iterator_range(betting_market_index.equal_range(fsm.betting_market_group_obj->id))) + fsm_event.db.modify(betting_market, [&fsm_event](betting_market_object& betting_market) { + betting_market.on_settled_event(fsm_event.db); + }); + + // then notify the event that this betting market is now resolved so it can change its status accordingly + const event_object& event = fsm.betting_market_group_obj->event_id(fsm_event.db); + fsm_event.db.modify(event, [&fsm_event,&fsm](event_object& event_obj) { + event_obj.on_betting_market_group_resolved(fsm_event.db, fsm.betting_market_group_obj->id, false); + }); + } + }; + struct canceled : public msm::front::state<>{ + void on_entry(const canceled_event& fsm_event, betting_market_group_state_machine_& fsm) { + dlog("betting market group ${id} -> canceled", ("id", fsm.betting_market_group_obj->id)); + auto& betting_market_index = fsm_event.db.get_index_type().indices().get(); + auto betting_markets_in_group = boost::make_iterator_range(betting_market_index.equal_range(fsm.betting_market_group_obj->id)); + + for (const betting_market_object& betting_market : betting_markets_in_group) + fsm_event.db.modify(betting_market, [&fsm_event](betting_market_object& betting_market_obj) { + betting_market_obj.on_canceled_event(fsm_event.db); + }); + + if (!fsm_event.canceled_by_event) { + const event_object& event = fsm.betting_market_group_obj->event_id(fsm_event.db); + fsm_event.db.modify(event, [&fsm_event,&fsm](event_object& event_obj) { + event_obj.on_betting_market_group_resolved(fsm_event.db, fsm.betting_market_group_obj->id, true); + }); + } + + fsm.betting_market_group_obj->settling_time = fsm_event.db.head_block_time(); + dlog("cancel complete, setting settling time to ${settling_time}", ("settling_time", fsm.betting_market_group_obj->settling_time)); + } + }; + + typedef upcoming initial_state; + + // actions + void cancel_all_unmatched_bets(const in_play_event& event) { + event.db.cancel_all_unmatched_bets_on_betting_market_group(*betting_market_group_obj); + } + + // guards + bool in_play_is_allowed(const in_play_event& event) { + return !betting_market_group_obj->never_in_play; + } + + typedef betting_market_group_state_machine_ x; // makes transition table cleaner + + // Transition table for betting market + struct transition_table : mpl::vector< + // Start Event Next Action Guard + // +-------------------+------------------+---------------------+------------------------------+----------------------+ + _row < upcoming, frozen_event, frozen_upcoming >, + row < upcoming, in_play_event, in_play, &x::cancel_all_unmatched_bets, &x::in_play_is_allowed >, + _row < upcoming, closed_event, closed >, + _row < upcoming, canceled_event, canceled >, + // +-------------------+------------------+---------------------+------------------------------+----------------------+ + _row < frozen_upcoming, upcoming_event, upcoming >, + _row < frozen_upcoming, in_play_event, upcoming >, + row < frozen_upcoming, in_play_event, in_play, &x::cancel_all_unmatched_bets, &x::in_play_is_allowed >, + _row < frozen_upcoming, closed_event, closed >, + _row < frozen_upcoming, canceled_event, canceled >, + // +-------------------+------------------+---------------------+------------------------------+----------------------+ + _row < in_play, frozen_event, frozen_in_play >, + _row < in_play, closed_event, closed >, + _row < in_play, canceled_event, canceled >, + // +-------------------+------------------+---------------------+------------------------------+----------------------+ + _row < frozen_in_play, in_play_event, in_play >, + _row < frozen_in_play, closed_event, closed >, + _row < frozen_in_play, canceled_event, canceled >, + // +-------------------+------------------+---------------------+------------------------------+----------------------+ + _row < closed, graded_event, graded >, + _row < closed, canceled_event, canceled >, + // +-------------------+------------------+---------------------+------------------------------+----------------------+ + //_row < graded re_grading_event, re_grading >, + _row < graded, settled_event, settled >, + _row < graded, canceled_event, canceled > + // +-------------------+------------------+---------------------+------------------------------+----------------------+ + //_row < re_grading, graded_event, graded >, + //_row < re_grading, canceled_event, canceled > + // +-------------------+------------------+---------------------+------------------------------+----------------------+ + > {}; + + template + void no_transition(Event const& e, Fsm& ,int state) + { + FC_THROW_EXCEPTION(graphene::chain::no_transition, "No transition"); + } + + betting_market_group_object* betting_market_group_obj; + betting_market_group_state_machine_(betting_market_group_object* betting_market_group_obj) : betting_market_group_obj(betting_market_group_obj) {} + }; + typedef msm::back::state_machine betting_market_group_state_machine; + +} // end anonymous namespace + +class betting_market_group_object::impl { +public: + betting_market_group_state_machine state_machine; + + impl(betting_market_group_object* self) : state_machine(self) {} +}; + +betting_market_group_object::betting_market_group_object() : + my(new impl(this)) +{ + dlog("betting_market_group_object ctor"); +} + +betting_market_group_object::betting_market_group_object(const betting_market_group_object& rhs) : + graphene::db::abstract_object(rhs), + description(rhs.description), + event_id(rhs.event_id), + rules_id(rhs.rules_id), + asset_id(rhs.asset_id), + total_matched_bets_amount(rhs.total_matched_bets_amount), + never_in_play(rhs.never_in_play), + delay_before_settling(rhs.delay_before_settling), + settling_time(rhs.settling_time), + my(new impl(this)) +{ + my->state_machine = rhs.my->state_machine; + my->state_machine.betting_market_group_obj = this; +} + +betting_market_group_object& betting_market_group_object::operator=(const betting_market_group_object& rhs) +{ + //graphene::db::abstract_object::operator=(rhs); + id = rhs.id; + description = rhs.description; + event_id = rhs.event_id; + rules_id = rhs.rules_id; + asset_id = rhs.asset_id; + total_matched_bets_amount = rhs.total_matched_bets_amount; + never_in_play = rhs.never_in_play; + delay_before_settling = rhs.delay_before_settling; + settling_time = rhs.settling_time; + + my->state_machine = rhs.my->state_machine; + my->state_machine.betting_market_group_obj = this; + + return *this; +} + +betting_market_group_object::~betting_market_group_object() +{ +} + +namespace { + + bool verify_betting_market_group_status_constants() + { + unsigned error_count = 0; + typedef msm::back::generate_state_set::type all_states; + static char const* filled_state_names[mpl::size::value]; + mpl::for_each > + (msm::back::fill_state_names(filled_state_names)); + for (unsigned i = 0; i < mpl::size::value; ++i) + { + try + { + // this is an approximate test, the state name provided by typeinfo will be mangled, but should + // at least contain the string we're looking for + const char* fc_reflected_value_name = fc::reflector::to_string((betting_market_group_state)i); + if (!strcmp(fc_reflected_value_name, filled_state_names[i])) + fc_elog(fc::logger::get("default"), + "Error, state string mismatch between fc and boost::msm for int value ${int_value}: boost::msm -> ${boost_string}, fc::reflect -> ${fc_string}", + ("int_value", i)("boost_string", filled_state_names[i])("fc_string", fc_reflected_value_name)); + } + catch (const fc::bad_cast_exception&) + { + fc_elog(fc::logger::get("default"), + "Error, no reflection for value ${int_value} in enum betting_market_group_status", + ("int_value", i)); + ++error_count; + } + } + dlog("Done checking constants"); + + return error_count == 0; + } +} // end anonymous namespace + +betting_market_group_status betting_market_group_object::get_status() const +{ + static bool state_constants_are_correct = verify_betting_market_group_status_constants(); + (void)&state_constants_are_correct; + betting_market_group_state state = (betting_market_group_state)my->state_machine.current_state()[0]; + + ddump((state)); + + switch (state) + { + case betting_market_group_state::upcoming: + return betting_market_group_status::upcoming; + case betting_market_group_state::frozen_upcoming: + return betting_market_group_status::frozen; + case betting_market_group_state::in_play: + return betting_market_group_status::in_play; + case betting_market_group_state::frozen_in_play: + return betting_market_group_status::frozen; + case betting_market_group_state::closed: + return betting_market_group_status::closed; + case betting_market_group_state::graded: + return betting_market_group_status::graded; + case betting_market_group_state::canceled: + return betting_market_group_status::canceled; + case betting_market_group_state::settled: + return betting_market_group_status::settled; + default: + FC_THROW("Unexpected betting market group state"); + }; +} + +void betting_market_group_object::pack_impl(std::ostream& stream) const +{ + boost::archive::binary_oarchive oa(stream, boost::archive::no_header|boost::archive::no_codecvt|boost::archive::no_xml_tag_checking); + oa << my->state_machine; +} + +void betting_market_group_object::unpack_impl(std::istream& stream) +{ + boost::archive::binary_iarchive ia(stream, boost::archive::no_header|boost::archive::no_codecvt|boost::archive::no_xml_tag_checking); + ia >> my->state_machine; +} + +void betting_market_group_object::on_upcoming_event(database& db) +{ + my->state_machine.process_event(upcoming_event(db)); +} + +void betting_market_group_object::on_in_play_event(database& db) +{ + my->state_machine.process_event(in_play_event(db)); +} + +void betting_market_group_object::on_frozen_event(database& db) +{ + my->state_machine.process_event(frozen_event(db)); +} + +void betting_market_group_object::on_closed_event(database& db, bool closed_by_event) +{ + my->state_machine.process_event(closed_event(db, closed_by_event)); +} + +void betting_market_group_object::on_graded_event(database& db) +{ + my->state_machine.process_event(graded_event(db)); +} + +void betting_market_group_object::on_re_grading_event(database& db) +{ + my->state_machine.process_event(re_grading_event(db)); +} + +void betting_market_group_object::on_settled_event(database& db) +{ + my->state_machine.process_event(settled_event(db)); +} + +void betting_market_group_object::on_canceled_event(database& db, bool canceled_by_event) +{ + my->state_machine.process_event(canceled_event(db, canceled_by_event)); +} + +// These are the only statuses that can be explicitly set by witness operations. +// Other states can only be reached indirectly (i.e., settling happens a fixed +// delay after grading) +void betting_market_group_object::dispatch_new_status(database& db, betting_market_group_status new_status) +{ + switch (new_status) { + case betting_market_group_status::upcoming: // by witnesses to unfreeze a bmg + on_upcoming_event(db); + break; + case betting_market_group_status::in_play: // by witnesses to make a bmg in-play + on_in_play_event(db); + break; + case betting_market_group_status::closed: // by witnesses to close a bmg + on_closed_event(db, false); + break; + case betting_market_group_status::frozen: // by witnesses to freeze a bmg + on_frozen_event(db); + break; + case betting_market_group_status::canceled: // by witnesses to cancel a bmg + on_canceled_event(db, false); + break; + default: + FC_THROW("The status ${new_status} cannot be set directly", ("new_status", new_status)); + } +} + + +} } // graphene::chain + +namespace fc { + // Manually reflect betting_market_group_object to variant to properly reflect "state" + void to_variant(const graphene::chain::betting_market_group_object& betting_market_group_obj, fc::variant& v) + { + fc::mutable_variant_object o; + o("id", betting_market_group_obj.id) + ("description", betting_market_group_obj.description) + ("event_id", betting_market_group_obj.event_id) + ("rules_id", betting_market_group_obj.rules_id) + ("asset_id", betting_market_group_obj.asset_id) + ("total_matched_bets_amount", betting_market_group_obj.total_matched_bets_amount) + ("never_in_play", betting_market_group_obj.never_in_play) + ("delay_before_settling", betting_market_group_obj.delay_before_settling) + ("settling_time", betting_market_group_obj.settling_time) + ("status", betting_market_group_obj.get_status()); + + v = o; + } + + // Manually reflect betting_market_group_object to variant to properly reflect "state" + void from_variant(const fc::variant& v, graphene::chain::betting_market_group_object& betting_market_group_obj) + { + betting_market_group_obj.id = v["id"].as(); + betting_market_group_obj.description = v["description"].as(); + betting_market_group_obj.event_id = v["event_id"].as(); + betting_market_group_obj.asset_id = v["asset_id"].as(); + betting_market_group_obj.total_matched_bets_amount = v["total_matched_bets_amount"].as(); + betting_market_group_obj.never_in_play = v["never_in_play"].as(); + betting_market_group_obj.delay_before_settling = v["delay_before_settling"].as(); + betting_market_group_obj.settling_time = v["settling_time"].as>(); + graphene::chain::betting_market_group_status status = v["status"].as(); + const_cast(betting_market_group_obj.my->state_machine.current_state())[0] = (int)status; + } +} //end namespace fc + diff --git a/libraries/chain/betting_market_object.cpp b/libraries/chain/betting_market_object.cpp index fbcae738..9aec3c3c 100644 --- a/libraries/chain/betting_market_object.cpp +++ b/libraries/chain/betting_market_object.cpp @@ -1,7 +1,37 @@ +#define DEFAULT_LOGGER "betting" #include +#include #include +#include +#include +#include +#include +#include + namespace graphene { namespace chain { + enum class betting_market_state { + unresolved, + frozen, + closed, + canceled, + graded, + settled + }; +} } +FC_REFLECT_ENUM(graphene::chain::betting_market_state, + (unresolved) + (frozen) + (closed) + (canceled) + (graded) + (settled)) + + +namespace graphene { namespace chain { + +namespace msm = boost::msm; +namespace mpl = boost::mpl; /* static */ share_type bet_object::get_approximate_matching_amount(share_type bet_amount, bet_multiplier_type backer_multiplier, bet_type back_or_lay, bool round_up /* = false */) { @@ -86,5 +116,326 @@ share_type betting_market_position_object::reduce() return immediate_winnings; } +// betting market object implementation +namespace +{ + // Events -- most events happen when the witnesses publish an update operation with a new + // status, so if they publish an event with the status set to `frozen`, we'll generate a `frozen_event` + struct unresolved_event + { + database& db; + unresolved_event(database& db) : db(db) {} + }; + struct frozen_event + { + database& db; + frozen_event(database& db) : db(db) {} + }; + struct closed_event + { + database& db; + closed_event(database& db) : db(db) {} + }; + struct graded_event + { + database& db; + betting_market_resolution_type new_grading; + graded_event(database& db, betting_market_resolution_type new_grading) : db(db), new_grading(new_grading) {} + }; + struct settled_event + { + database& db; + settled_event(database& db) : db(db) {} + }; + struct canceled_event + { + database& db; + canceled_event(database& db) : db(db) {} + }; + + // Events + struct betting_market_state_machine_ : public msm::front::state_machine_def + { + // disable a few state machine features we don't use for performance + typedef int no_exception_thrown; + typedef int no_message_queue; + + // States + struct unresolved : public msm::front::state<>{ + template + void on_entry(const Event& event, betting_market_state_machine_& fsm) { + dlog("betting market ${id} -> unresolved", ("id", fsm.betting_market_obj->id)); + } + }; + struct frozen : public msm::front::state<>{ + template + void on_entry(const Event& event, betting_market_state_machine_& fsm) { + dlog("betting market ${id} -> frozen", ("id", fsm.betting_market_obj->id)); + } + }; + struct closed : public msm::front::state<>{ + template + void on_entry(const Event& event, betting_market_state_machine_& fsm) { + dlog("betting market ${id} -> closed", ("id", fsm.betting_market_obj->id)); + } + }; + struct graded : public msm::front::state<>{ + void on_entry(const graded_event& event, betting_market_state_machine_& fsm) { + dlog("betting market ${id} -> graded", ("id", fsm.betting_market_obj->id)); + fsm.betting_market_obj->resolution = event.new_grading; + } + }; + struct settled : public msm::front::state<>{ + template + void on_entry(const Event& event, betting_market_state_machine_& fsm) { + dlog("betting market ${id} -> settled", ("id", fsm.betting_market_obj->id)); + } + }; + struct canceled : public msm::front::state<>{ + void on_entry(const canceled_event& event, betting_market_state_machine_& fsm) { + dlog("betting market ${id} -> canceled", ("id", fsm.betting_market_obj->id)); + fsm.betting_market_obj->resolution = betting_market_resolution_type::cancel; + } + }; + + typedef unresolved initial_state; + typedef betting_market_state_machine_ x; // makes transition table cleaner + + + // Transition table for betting market + struct transition_table : mpl::vector< + // Start Event Next Action Guard + // +---------------------------+-----------------------------+----------------------------+---------------------+----------------------+ + _row < unresolved, frozen_event, frozen >, + _row < unresolved, closed_event, closed >, + _row < unresolved, canceled_event, canceled >, + // +---------------------------+-----------------------------+----------------------------+---------------------+----------------------+ + _row < frozen, unresolved_event, unresolved >, + _row < frozen, closed_event, closed >, + _row < frozen, canceled_event, canceled >, + // +---------------------------+-----------------------------+----------------------------+---------------------+----------------------+ + _row < closed, graded_event, graded >, + _row < closed, canceled_event, canceled >, + // +---------------------------+-----------------------------+----------------------------+---------------------+----------------------+ + _row < graded, settled_event, settled >, + _row < graded, canceled_event, canceled > + // +---------------------------+-----------------------------+----------------------------+---------------------+----------------------+ + > {}; + + template + void no_transition(Event const& e, Fsm& ,int state) + { + FC_THROW_EXCEPTION(graphene::chain::no_transition, "No transition"); + } + + betting_market_object* betting_market_obj; + betting_market_state_machine_(betting_market_object* betting_market_obj) : betting_market_obj(betting_market_obj) {} + }; + typedef msm::back::state_machine betting_market_state_machine; + +} // end anonymous namespace + +class betting_market_object::impl { +public: + betting_market_state_machine state_machine; + + impl(betting_market_object* self) : state_machine(self) {} +}; + +betting_market_object::betting_market_object() : + my(new impl(this)) +{ +} + +betting_market_object::betting_market_object(const betting_market_object& rhs) : + graphene::db::abstract_object(rhs), + group_id(rhs.group_id), + description(rhs.description), + payout_condition(rhs.payout_condition), + my(new impl(this)) +{ + my->state_machine = rhs.my->state_machine; + my->state_machine.betting_market_obj = this; +} + +betting_market_object& betting_market_object::operator=(const betting_market_object& rhs) +{ + //graphene::db::abstract_object::operator=(rhs); + id = rhs.id; + group_id = rhs.group_id; + description = rhs.description; + payout_condition = rhs.payout_condition; + + my->state_machine = rhs.my->state_machine; + my->state_machine.betting_market_obj = this; + + return *this; +} + +betting_market_object::~betting_market_object() +{ +} + +namespace { + + + bool verify_betting_market_status_constants() + { + unsigned error_count = 0; + typedef msm::back::generate_state_set::type all_states; + static char const* filled_state_names[mpl::size::value]; + mpl::for_each > + (msm::back::fill_state_names(filled_state_names)); + for (unsigned i = 0; i < mpl::size::value; ++i) + { + try + { + // this is an approximate test, the state name provided by typeinfo will be mangled, but should + // at least contain the string we're looking for + const char* fc_reflected_value_name = fc::reflector::to_string((betting_market_state)i); + if (!strcmp(fc_reflected_value_name, filled_state_names[i])) + fc_elog(fc::logger::get("default"), + "Error, state string mismatch between fc and boost::msm for int value ${int_value}: boost::msm -> ${boost_string}, fc::reflect -> ${fc_string}", + ("int_value", i)("boost_string", filled_state_names[i])("fc_string", fc_reflected_value_name)); + } + catch (const fc::bad_cast_exception&) + { + fc_elog(fc::logger::get("default"), + "Error, no reflection for value ${int_value} in enum betting_market_status", + ("int_value", i)); + ++error_count; + } + } + dlog("Done checking constants"); + + return error_count == 0; + } +} // end anonymous namespace + + +betting_market_status betting_market_object::get_status() const +{ + static bool state_constants_are_correct = verify_betting_market_status_constants(); + (void)&state_constants_are_correct; + betting_market_state state = (betting_market_state)my->state_machine.current_state()[0]; + + edump((state)); + + switch (state) + { + case betting_market_state::unresolved: + return betting_market_status::unresolved; + case betting_market_state::frozen: + return betting_market_status::frozen; + case betting_market_state::closed: + return betting_market_status::unresolved; + case betting_market_state::canceled: + return betting_market_status::canceled; + case betting_market_state::graded: + return betting_market_status::graded; + case betting_market_state::settled: + return betting_market_status::settled; + default: + FC_THROW("Unexpected betting market state"); + }; +} + +void betting_market_object::cancel_all_unmatched_bets(database& db) const +{ + const auto& bet_odds_idx = db.get_index_type().indices().get(); + + // first, cancel all bets on the active books + auto book_itr = bet_odds_idx.lower_bound(std::make_tuple(id)); + auto book_end = bet_odds_idx.upper_bound(std::make_tuple(id)); + while (book_itr != book_end) + { + auto old_book_itr = book_itr; + ++book_itr; + db.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 == id) + db.cancel_bet(*old_book_itr, true); + } +} + +void betting_market_object::pack_impl(std::ostream& stream) const +{ + boost::archive::binary_oarchive oa(stream, boost::archive::no_header|boost::archive::no_codecvt|boost::archive::no_xml_tag_checking); + oa << my->state_machine; +} + +void betting_market_object::unpack_impl(std::istream& stream) +{ + boost::archive::binary_iarchive ia(stream, boost::archive::no_header|boost::archive::no_codecvt|boost::archive::no_xml_tag_checking); + ia >> my->state_machine; +} + +void betting_market_object::on_unresolved_event(database& db) +{ + my->state_machine.process_event(unresolved_event(db)); +} + +void betting_market_object::on_frozen_event(database& db) +{ + my->state_machine.process_event(frozen_event(db)); +} + +void betting_market_object::on_closed_event(database& db) +{ + my->state_machine.process_event(closed_event(db)); +} + +void betting_market_object::on_graded_event(database& db, betting_market_resolution_type new_grading) +{ + my->state_machine.process_event(graded_event(db, new_grading)); +} + +void betting_market_object::on_settled_event(database& db) +{ + my->state_machine.process_event(settled_event(db)); +} + +void betting_market_object::on_canceled_event(database& db) +{ + my->state_machine.process_event(canceled_event(db)); +} + } } // graphene::chain +namespace fc { + // Manually reflect betting_market_object to variant to properly reflect "state" + void to_variant(const graphene::chain::betting_market_object& event_obj, fc::variant& v) + { + fc::mutable_variant_object o; + o("id", event_obj.id) + ("group_id", event_obj.group_id) + ("description", event_obj.description) + ("payout_condition", event_obj.payout_condition) + ("resolution", event_obj.resolution) + ("status", event_obj.get_status()); + + v = o; + } + + // Manually reflect betting_market_object to variant to properly reflect "state" + void from_variant(const fc::variant& v, graphene::chain::betting_market_object& event_obj) + { + event_obj.id = v["id"].as(); + event_obj.group_id = v["name"].as(); + event_obj.description = v["description"].as(); + event_obj.payout_condition = v["payout_condition"].as(); + event_obj.resolution = v["resolution"].as>(); + graphene::chain::betting_market_status status = v["status"].as(); + const_cast(event_obj.my->state_machine.current_state())[0] = (int)status; + } +} //end namespace fc + diff --git a/libraries/chain/db_bet.cpp b/libraries/chain/db_bet.cpp index 72bbfa02..dae5d866 100644 --- a/libraries/chain/db_bet.cpp +++ b/libraries/chain/db_bet.cpp @@ -1,3 +1,4 @@ +#define DEFAULT_LOGGER "betting" #include #include #include @@ -5,6 +6,8 @@ #include #include +#include +#include namespace graphene { namespace chain { @@ -50,30 +53,36 @@ void database::cancel_all_unmatched_bets_on_betting_market(const betting_market_ } } -void database::cancel_all_betting_markets_for_event(const event_object& event_obj) -{ - auto& betting_market_group_index = get_index_type().indices().get(); - auto& betting_market_index = get_index_type().indices().get(); - - //for each betting market group of event - for (const betting_market_group_object& betting_market_group : - boost::make_iterator_range(betting_market_group_index.equal_range(event_obj.id))) - resolve_betting_market_group(betting_market_group, {}); -} - void database::validate_betting_market_group_resolutions(const betting_market_group_object& betting_market_group, const std::map& resolutions) { - auto& betting_market_index = get_index_type().indices().get(); - auto betting_market_itr = betting_market_index.lower_bound(betting_market_group.id); - while (betting_market_itr != betting_market_index.end() && betting_market_itr->group_id == betting_market_group.id) - { - const betting_market_object& betting_market = *betting_market_itr; - // every betting market in the group tied with resolution - //idump((betting_market.id)(resolutions)); - assert(resolutions.count(betting_market.id)); - ++betting_market_itr; - } + auto& betting_market_index = get_index_type().indices().get(); + auto betting_markets_in_group = boost::make_iterator_range(betting_market_index.equal_range(betting_market_group.id)); + + // we must have one resolution for each betting market + FC_ASSERT(resolutions.size() == boost::size(betting_markets_in_group), + "You must publish resolutions for all ${size} markets in the group, you published ${published}", ("size", boost::size(betting_markets_in_group))("published", resolutions.size())); + + // both are sorted by id, we can walk through both and verify that they match + unsigned number_of_wins = 0; + unsigned number_of_cancels = 0; + for (const auto& zipped : boost::combine(resolutions, betting_markets_in_group)) + { + const auto& resolution = boost::get<0>(zipped); + const auto& betting_market = boost::get<1>(zipped); + FC_ASSERT(resolution.first == betting_market.id, "Missing resolution for betting market ${id}", ("id", betting_market.id)); + if (resolution.second == betting_market_resolution_type::cancel) + ++number_of_cancels; + else if (resolution.second == betting_market_resolution_type::win) + ++number_of_wins; + else + FC_ASSERT(resolution.second == betting_market_resolution_type::not_win); + } + + if (number_of_cancels != 0) + FC_ASSERT(number_of_cancels == resolutions.size(), "You must cancel all betting markets or none of the betting markets in the group"); + else + FC_ASSERT(number_of_wins == 1, "There must be exactly one winning market"); } void database::cancel_all_unmatched_bets_on_betting_market_group(const betting_market_group_object& betting_market_group) @@ -90,11 +99,40 @@ void database::cancel_all_unmatched_bets_on_betting_market_group(const betting_m } void database::resolve_betting_market_group(const betting_market_group_object& betting_market_group, - const std::map& resolutions, - bool do_not_remove) + const std::map& resolutions) { - bool cancel = resolutions.size() == 0; + auto& betting_market_index = get_index_type().indices().get(); + auto betting_markets_in_group = boost::make_iterator_range(betting_market_index.equal_range(betting_market_group.id)); + bool group_was_canceled = resolutions.begin()->second == betting_market_resolution_type::cancel; + + if (group_was_canceled) + modify(betting_market_group, [group_was_canceled,this](betting_market_group_object& betting_market_group_obj) { + betting_market_group_obj.on_canceled_event(*this, false); // this cancels the betting markets + }); + else { + // TODO: this should be pushed into the bmg's on_graded_event + + // both are sorted by id, we can walk through both and verify that they match + for (const auto& zipped : boost::combine(resolutions, betting_markets_in_group)) + { + const auto& resolution = boost::get<0>(zipped); + const auto& betting_market = boost::get<1>(zipped); + + modify(betting_market, [this,&resolution](betting_market_object& betting_market_obj) { + betting_market_obj.on_graded_event(*this, resolution.second); + }); + } + + modify(betting_market_group, [group_was_canceled,this](betting_market_group_object& betting_market_group_obj) { + betting_market_group_obj.on_graded_event(*this); + }); + } +} + +void database::settle_betting_market_group(const betting_market_group_object& betting_market_group) +{ + ilog("Settling betting market group ${id}", ("id", betting_market_group.id)); // we pay the rake fee to the dividend distribution account for the core asset, go ahead // and look up that account now fc::optional rake_account_id; @@ -105,6 +143,10 @@ void database::resolve_betting_market_group(const betting_market_group_object& b rake_account_id = core_asset_dividend_data_obj.dividend_distribution_account; } + // collect the resolutions of all markets in the BMG: they were previously published and + // stored in the individual betting markets + std::map resolutions_by_market_id; + // collecting bettors and their positions std::map > bettor_positions_map; @@ -116,11 +158,13 @@ void database::resolve_betting_market_group(const betting_market_group_object& b while (betting_market_itr != betting_market_index.end() && betting_market_itr->group_id == betting_market_group.id) { const betting_market_object& betting_market = *betting_market_itr; + FC_ASSERT(betting_market_itr->resolution, "Unexpected error settling betting market ${market_id}: no published resolution", + ("market_id", betting_market_itr->id)); + resolutions_by_market_id.emplace(betting_market.id, *betting_market_itr->resolution); + ++betting_market_itr; cancel_all_unmatched_bets_on_betting_market(betting_market); - // [ROL] why tuple - //auto position_itr = position_index.lower_bound(std::make_tuple(betting_market.id)); auto position_itr = position_index.lower_bound(betting_market.id); while (position_itr != position_index.end() && position_itr->betting_market_id == betting_market.id) @@ -144,14 +188,24 @@ void database::resolve_betting_market_group(const betting_market_group_object& b for (const betting_market_position_object* position : bettor_positions) { betting_market_resolution_type resolution; - if (cancel) - resolution = betting_market_resolution_type::cancel; - else + try { - // checked in evaluator, should never happen, see above - assert(resolutions.count(position->betting_market_id)); - resolution = resolutions.at(position->betting_market_id); + resolution = resolutions_by_market_id.at(position->betting_market_id); } + catch (std::out_of_range&) + { + FC_THROW_EXCEPTION(fc::key_not_found_exception, "Unexpected betting market ID, shouldn't happen"); + } + + ///if (cancel) + /// resolution = betting_market_resolution_type::cancel; + ///else + ///{ + /// // checked in evaluator, should never happen, see above + /// assert(resolutions.count(position->betting_market_id)); + /// resolution = resolutions.at(position->betting_market_id); + ///} + switch (resolution) { @@ -194,29 +248,41 @@ void database::resolve_betting_market_group(const betting_market_group_object& b push_applied_operation(betting_market_group_resolved_operation(bettor_id, betting_market_group.id, - resolutions, + resolutions_by_market_id, payout_amounts, rake_amount)); } - betting_market_itr = betting_market_index.lower_bound(betting_market_group.id); - while (betting_market_itr != betting_market_index.end() && betting_market_itr->group_id == betting_market_group.id) - { - const betting_market_object& betting_market = *betting_market_itr; - ++betting_market_itr; - if (!do_not_remove) - remove(betting_market); - } - if (!do_not_remove) - remove(betting_market_group); -} + // At this point, the betting market group will either be in the "graded" or "canceled" state, + // if it was graded, mark it as settled. if it's canceled, let it remain canceled. -#if 0 -void database::get_required_deposit_for_bet(const betting_market_object& betting_market, - betting_market_resolution_type resolution) -{ + bool was_canceled = betting_market_group.get_status() == betting_market_group_status::canceled; + + if (!was_canceled) + modify(betting_market_group, [&](betting_market_group_object& group) { + group.on_settled_event(*this); + }); + + betting_market_itr = betting_market_index.lower_bound(betting_market_group.id); + while (betting_market_itr != betting_market_index.end() && betting_market_itr->group_id == betting_market_group.id) { + const betting_market_object& betting_market = *betting_market_itr; + + ++betting_market_itr; + dlog("removing betting market ${id}", ("id", betting_market.id)); + remove(betting_market); + } + + const event_object& event = betting_market_group.event_id(*this); + + dlog("removing betting market group ${id}", ("id", betting_market_group.id)); + remove(betting_market_group); + + if (event.get_status() == event_status::canceled || + event.get_status() == event_status::settled) { + dlog("removing event ${id}", ("id", event.id)); + remove(event); + } } -#endif share_type adjust_betting_position(database& db, account_id_type bettor_id, betting_market_id_type betting_market_id, bet_type back_or_lay, share_type bet_amount, share_type matched_amount) { try { diff --git a/libraries/chain/db_block.cpp b/libraries/chain/db_block.cpp index 1a886e90..b63dfde4 100644 --- a/libraries/chain/db_block.cpp +++ b/libraries/chain/db_block.cpp @@ -551,6 +551,7 @@ void database::_apply_block( const signed_block& next_block ) update_expired_feeds(); update_withdraw_permissions(); update_tournaments(); + update_betting_markets(next_block.timestamp); // n.b., update_maintenance_flag() happens this late // because get_slot_time() / get_slot_at_time() is needed above @@ -568,7 +569,6 @@ void database::_apply_block( const signed_block& next_block ) _applied_ops.clear(); notify_changed_objects(); - } FC_CAPTURE_AND_RETHROW( (next_block.block_num()) ) } diff --git a/libraries/chain/db_update.cpp b/libraries/chain/db_update.cpp index a643c989..900c8c0b 100644 --- a/libraries/chain/db_update.cpp +++ b/libraries/chain/db_update.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include @@ -222,8 +223,7 @@ void database::place_delayed_bets() // 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) + if (betting_market.get_status() == betting_market_status::unresolved) { 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 @@ -655,4 +655,28 @@ void database::update_tournaments() initiate_next_games(*this); } +void process_settled_betting_markets(database& db, fc::time_point_sec current_block_time) +{ + // after a betting market is graded, it goes through a delay period in which it + // can be flagged for re-grading. If it isn't flagged during this interval, + // it is automatically settled (paid). Process these now. + const auto& betting_market_group_index = db.get_index_type().indices().get(); + + // this index will be sorted with all bmgs with no settling time set first, followed by + // ones with the settling time set by increasing time. Start at the first bmg with a time set + auto betting_market_group_iter = betting_market_group_index.upper_bound(fc::optional()); + while (betting_market_group_iter != betting_market_group_index.end() && + *betting_market_group_iter->settling_time <= current_block_time) + { + auto next_iter = std::next(betting_market_group_iter); + db.settle_betting_market_group(*betting_market_group_iter); + betting_market_group_iter = next_iter; + } +} + +void database::update_betting_markets(fc::time_point_sec current_block_time) +{ + process_settled_betting_markets(*this, current_block_time); +} + } } diff --git a/libraries/chain/event_evaluator.cpp b/libraries/chain/event_evaluator.cpp index 41e104f2..f4e032da 100644 --- a/libraries/chain/event_evaluator.cpp +++ b/libraries/chain/event_evaluator.cpp @@ -58,7 +58,6 @@ object_id_type event_create_evaluator::do_apply(const event_create_operation& op const event_object& new_event = d.create( [&]( event_object& event_obj ) { event_obj.name = op.name; - event_obj.status = event_status::upcoming; event_obj.season = op.season; event_obj.start_time = op.start_time; event_obj.event_group_id = event_group_id; @@ -74,28 +73,19 @@ object_id_type event_create_evaluator::do_apply(const event_create_operation& op void_result event_update_evaluator::do_evaluate(const event_update_operation& op) { try { FC_ASSERT(trx_state->_is_proposed_trx); - FC_ASSERT(op.new_event_group_id.valid() || - op.new_name.valid() || - op.new_season.valid() || - op.new_start_time.valid() || - op.is_live_market.valid(), "nothing to change"); + FC_ASSERT(op.new_event_group_id || op.new_name || op.new_season || + op.new_start_time || op.new_status, "nothing to change"); - if (op.new_event_group_id.valid()) + if (op.new_event_group_id) { - object_id_type resolved_event_group_id = *op.new_event_group_id; - if (is_relative(*op.new_event_group_id)) - resolved_event_group_id = get_relative_id(*op.new_event_group_id); + object_id_type resolved_event_group_id = *op.new_event_group_id; + if (is_relative(*op.new_event_group_id)) + resolved_event_group_id = get_relative_id(*op.new_event_group_id); - FC_ASSERT(resolved_event_group_id.space() == event_group_id_type::space_id && - resolved_event_group_id.type() == event_group_id_type::type_id, - "event_group_id must refer to a event_group_id_type"); - event_group_id = resolved_event_group_id; - } - - if (op.is_live_market.valid()) - { - const auto& _event_object = &op.event_id(db()); - FC_ASSERT(_event_object->is_live_market != *op.is_live_market, "is_live_market would not change the state of the event"); + FC_ASSERT(resolved_event_group_id.space() == event_group_id_type::space_id && + resolved_event_group_id.type() == event_group_id_type::type_id, + "event_group_id must refer to a event_group_id_type"); + event_group_id = resolved_event_group_id; } return void_result(); @@ -103,25 +93,21 @@ void_result event_update_evaluator::do_evaluate(const event_update_operation& op void_result event_update_evaluator::do_apply(const event_update_operation& op) { try { - database& _db = db(); - _db.modify( - _db.get(op.event_id), - [&]( event_object& eo ) - { - if (eo.status > event_status::STATUS_COUNT) - eo.status = event_status::upcoming; - if( op.new_name.valid() ) - eo.name = *op.new_name; - if( op.new_season.valid() ) - eo.season = *op.new_season; - if( op.new_start_time.valid() ) - eo.start_time = *op.new_start_time; - if( op.new_event_group_id.valid() ) - eo.event_group_id = event_group_id; - if( op.is_live_market.valid() ) - eo.is_live_market = *op.is_live_market; - }); - return void_result(); + database& _db = db(); + _db.modify(_db.get(op.event_id), + [&](event_object& eo) { + if( op.new_name ) + eo.name = *op.new_name; + if( op.new_season ) + eo.season = *op.new_season; + if( op.new_start_time ) + eo.start_time = *op.new_start_time; + if( op.new_event_group_id ) + eo.event_group_id = event_group_id; + if( op.new_status ) + eo.dispatch_new_status(_db, *op.new_status); + }); + return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } void_result event_update_status_evaluator::do_evaluate(const event_update_status_operation& op) @@ -138,14 +124,13 @@ void_result event_update_status_evaluator::do_evaluate(const event_update_status void_result event_update_status_evaluator::do_apply(const event_update_status_operation& op) { try { database& d = db(); - //if event is canceled, first cancel all associated betting markets - if (op.status == event_status::canceled) - d.cancel_all_betting_markets_for_event(*_event_to_update); - //update event - d.modify( *_event_to_update, [&]( event_object& event_obj) { + + d.modify( *_event_to_update, [&](event_object& event_obj) { + if (_event_to_update->get_status() != op.status) + event_obj.dispatch_new_status(d, op.status); event_obj.scores = op.scores; - event_obj.status = op.status; }); + return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } diff --git a/libraries/chain/event_object.cpp b/libraries/chain/event_object.cpp new file mode 100644 index 00000000..eaa06710 --- /dev/null +++ b/libraries/chain/event_object.cpp @@ -0,0 +1,547 @@ +#define DEFAULT_LOGGER "betting" +#define BOOST_MPL_CFG_NO_PREPROCESSED_HEADERS +#define BOOST_MPL_LIMIT_VECTOR_SIZE 30 +#define BOOST_MPL_LIMIT_MAP_SIZE 30 +#include +#include +#include +#include +#include +#include +#include +#include + +namespace graphene { namespace chain { + enum class event_state { + upcoming, + frozen_upcoming, + in_progress, + frozen_in_progress, + finished, + canceled, + settled + }; +} } + +FC_REFLECT_ENUM(graphene::chain::event_state, + (upcoming) + (frozen_upcoming) + (in_progress) + (frozen_in_progress) + (finished) + (canceled) + (settled)) + +namespace graphene { namespace chain { + + namespace msm = boost::msm; + namespace mpl = boost::mpl; + + namespace + { + + // Events -- most events happen when the witnesses publish an event_update operation with a new + // status, so if they publish an event with the status set to `frozen`, we'll generate a `frozen_event` + struct upcoming_event + { + database& db; + upcoming_event(database& db) : db(db) {} + }; + struct in_progress_event + { + database& db; + in_progress_event(database& db) : db(db) {} + }; + struct frozen_event + { + database& db; + frozen_event(database& db) : db(db) {} + }; + struct finished_event + { + database& db; + finished_event(database& db) : db(db) {} + }; + struct canceled_event + { + database& db; + canceled_event(database& db) : db(db) {} + }; + + // event triggered when a betting market group in this event is resolved, + // when we get this, check and see if all betting market groups are now + // canceled/settled; if so, transition the event to canceled/settled, + // otherwise remain in the current state + struct betting_market_group_resolved_event + { + database& db; + betting_market_group_id_type resolved_group; + bool was_canceled; + betting_market_group_resolved_event(database& db, betting_market_group_id_type resolved_group, bool was_canceled) : db(db), resolved_group(resolved_group), was_canceled(was_canceled) {} + }; + + // event triggered when a betting market group is closed. When we get this, + // if all child betting market groups are closed, transition to finished + struct betting_market_group_closed_event + { + database& db; + betting_market_group_id_type closed_group; + betting_market_group_closed_event(database& db, betting_market_group_id_type closed_group) : db(db), closed_group(closed_group) {} + }; + + // Events + struct event_state_machine_ : public msm::front::state_machine_def + + { + // disable a few state machine features we don't use for performance + typedef int no_exception_thrown; + typedef int no_message_queue; + + // States + struct upcoming : public msm::front::state<> { + void on_entry(const betting_market_group_resolved_event& event, event_state_machine_& fsm) {} // transition to self + void on_entry(const upcoming_event& event, event_state_machine_& fsm) { + dlog("event ${id} -> upcoming", ("id", fsm.event_obj->id)); + auto& betting_market_group_index = event.db.get_index_type().indices().get(); + for (const betting_market_group_object& betting_market_group : + boost::make_iterator_range(betting_market_group_index.equal_range(fsm.event_obj->id))) + try + { + event.db.modify(betting_market_group, [&event](betting_market_group_object& betting_market_group_obj) { + betting_market_group_obj.on_upcoming_event(event.db); + }); + } + catch (const graphene::chain::no_transition&) + { + // it's possible a betting market group has already been closed or canceled, + // in which case we can't freeze the group. just ignore the exception + } + } + }; + struct in_progress : public msm::front::state<> { + void on_entry(const betting_market_group_resolved_event& event, event_state_machine_& fsm) {} // transition to self + void on_entry(const in_progress_event& event, event_state_machine_& fsm) { + dlog("event ${id} -> in_progress", ("id", fsm.event_obj->id)); + auto& betting_market_group_index = event.db.get_index_type().indices().get(); + for (const betting_market_group_object& betting_market_group : + boost::make_iterator_range(betting_market_group_index.equal_range(fsm.event_obj->id))) + try + { + event.db.modify(betting_market_group, [&event](betting_market_group_object& betting_market_group_obj) { + betting_market_group_obj.on_in_play_event(event.db); + }); + } + catch (const graphene::chain::no_transition&) + { + // it's possible a betting market group has already been closed or canceled, + // in which case we can't freeze the group. just ignore the exception + } + } + }; + struct frozen_upcoming : public msm::front::state<> { + void on_entry(const betting_market_group_resolved_event& event, event_state_machine_& fsm) {} // transition to self + template + void on_entry(const Event& event, event_state_machine_& fsm) { + dlog("event ${id} -> frozen_upcoming", ("id", fsm.event_obj->id)); + } + }; + struct frozen_in_progress : public msm::front::state<> { + void on_entry(const betting_market_group_resolved_event& event, event_state_machine_& fsm) {} // transition to self + template + void on_entry(const Event& event, event_state_machine_& fsm) { + dlog("event ${id} -> frozen_in_progress", ("id", fsm.event_obj->id)); + } + }; + struct finished : public msm::front::state<> { + template + void on_entry(const Event& event, event_state_machine_& fsm) { + dlog("event ${id} -> finished", ("id", fsm.event_obj->id)); + } + }; + struct settled : public msm::front::state<>{ + template + void on_entry(const Event& event, event_state_machine_& fsm) { + dlog("event ${id} -> settled", ("id", fsm.event_obj->id)); + } + }; + struct canceled : public msm::front::state<> { + template + void on_entry(const Event& event, event_state_machine_& fsm) { + dlog("event ${id} -> canceled", ("id", fsm.event_obj->id)); + } + }; + + // actions + void record_whether_group_settled_or_canceled(const betting_market_group_resolved_event& event) { + if (!event.was_canceled) + event_obj->at_least_one_betting_market_group_settled = true; + } + + void freeze_betting_market_groups(const frozen_event& event) { + auto& betting_market_group_index = event.db.get_index_type().indices().get(); + for (const betting_market_group_object& betting_market_group : + boost::make_iterator_range(betting_market_group_index.equal_range(event_obj->id))) + { + try + { + event.db.modify(betting_market_group, [&event](betting_market_group_object& betting_market_group_obj) { + betting_market_group_obj.on_frozen_event(event.db); + }); + } + catch (const graphene::chain::no_transition&) + { + // it's possible a betting market group has already been closed or canceled, + // in which case we can't freeze the group. just ignore the exception + } + } + } + + void close_all_betting_market_groups(const finished_event& event) { + auto& betting_market_group_index = event.db.get_index_type().indices().get(); + for (const betting_market_group_object& betting_market_group : + boost::make_iterator_range(betting_market_group_index.equal_range(event_obj->id))) + { + try + { + event.db.modify(betting_market_group, [&event](betting_market_group_object& betting_market_group_obj) { + betting_market_group_obj.on_closed_event(event.db, true); + }); + } + catch (const graphene::chain::no_transition&) + { + // it's possible a betting market group has already been closed or canceled, + // in which case we can't close the group. just ignore the exception + } + } + } + + void cancel_all_betting_market_groups(const canceled_event& event) { + auto& betting_market_group_index = event.db.template get_index_type().indices().template get(); + for (const betting_market_group_object& betting_market_group : + boost::make_iterator_range(betting_market_group_index.equal_range(event_obj->id))) + event.db.modify(betting_market_group, [&event](betting_market_group_object& betting_market_group_obj) { + betting_market_group_obj.on_canceled_event(event.db, true); + }); + } + + // Guards + bool all_betting_market_groups_are_closed(const betting_market_group_closed_event& event) + { + auto& betting_market_group_index = event.db.get_index_type().indices().get(); + for (const betting_market_group_object& betting_market_group : + boost::make_iterator_range(betting_market_group_index.equal_range(event_obj->id))) + if (betting_market_group.id != event.closed_group) + { + betting_market_group_status status = betting_market_group.get_status(); + if (status != betting_market_group_status::closed && + status != betting_market_group_status::graded && + status != betting_market_group_status::re_grading && + status != betting_market_group_status::settled && + status != betting_market_group_status::canceled) + return false; + } + return true; + } + + bool all_betting_market_groups_are_canceled(const betting_market_group_resolved_event& event) + { + // if the bmg that just resolved was settled, obviously all didn't cancel + if (!event.was_canceled) + return false; + // if a previously-resolved group was settled, all didn't cancel + if (event_obj->at_least_one_betting_market_group_settled) + return false; + auto& betting_market_group_index = event.db.get_index_type().indices().get(); + for (const betting_market_group_object& betting_market_group : + boost::make_iterator_range(betting_market_group_index.equal_range(event_obj->id))) + if (betting_market_group.id != event.resolved_group) + if (betting_market_group.get_status() != betting_market_group_status::canceled) + return false; + return true; + } + + bool all_betting_market_groups_are_resolved(const betting_market_group_resolved_event& event) + { + if (!event.was_canceled) + event_obj->at_least_one_betting_market_group_settled = true; + + auto& betting_market_group_index = event.db.get_index_type().indices().get(); + for (const betting_market_group_object& betting_market_group : + boost::make_iterator_range(betting_market_group_index.equal_range(event_obj->id))) { + if (betting_market_group.id != event.resolved_group) { + betting_market_group_status status = betting_market_group.get_status(); + if (status != betting_market_group_status::canceled && status != betting_market_group_status::settled) + return false; + } + } + return true; + } + + typedef upcoming initial_state; + typedef event_state_machine_ x; // makes transition table cleaner + + // Transition table for tournament + struct transition_table : mpl::vector< + // Start Event Next Action Guard + // +---------------------+-----------------------------+--------------------+--------------------------------+----------------------+ + _row < upcoming, in_progress_event, in_progress >, + a_row< upcoming, finished_event, finished, &x::close_all_betting_market_groups >, + a_row< upcoming, frozen_event, frozen_upcoming, &x::freeze_betting_market_groups >, + a_row< upcoming, canceled_event, canceled, &x::cancel_all_betting_market_groups >, + a_row< upcoming, betting_market_group_resolved_event,upcoming, &x::record_whether_group_settled_or_canceled >, + g_row< upcoming, betting_market_group_closed_event, finished, &x::all_betting_market_groups_are_closed >, + // +---------------------+-----------------------------+--------------------+--------------------------------+----------------------+ + _row < frozen_upcoming, upcoming_event, upcoming >, + _row < frozen_upcoming, in_progress_event, in_progress >, + a_row< frozen_upcoming, finished_event, finished, &x::close_all_betting_market_groups >, + a_row< frozen_upcoming, canceled_event, canceled, &x::cancel_all_betting_market_groups >, + a_row< frozen_upcoming, betting_market_group_resolved_event,frozen_upcoming, &x::record_whether_group_settled_or_canceled >, + g_row< frozen_upcoming, betting_market_group_closed_event, finished, &x::all_betting_market_groups_are_closed >, + // +---------------------+-----------------------------+--------------------+--------------------------------+----------------------+ + a_row< in_progress, frozen_event, frozen_in_progress, &x::freeze_betting_market_groups >, + a_row< in_progress, finished_event, finished, &x::close_all_betting_market_groups >, + a_row< in_progress, canceled_event, canceled, &x::cancel_all_betting_market_groups >, + a_row< in_progress, betting_market_group_resolved_event,in_progress, &x::record_whether_group_settled_or_canceled >, + g_row< in_progress, betting_market_group_closed_event, finished, &x::all_betting_market_groups_are_closed >, + // +---------------------+-----------------------------+--------------------+--------------------------------+----------------------+ + _row < frozen_in_progress, in_progress_event, in_progress >, + a_row< frozen_in_progress, finished_event, finished, &x::close_all_betting_market_groups >, + a_row< frozen_in_progress, canceled_event, canceled, &x::cancel_all_betting_market_groups >, + a_row< frozen_in_progress, betting_market_group_resolved_event,frozen_in_progress, &x::record_whether_group_settled_or_canceled >, + g_row< frozen_in_progress, betting_market_group_closed_event, finished, &x::all_betting_market_groups_are_closed >, + // +---------------------+-----------------------------+--------------------+--------------------------------+----------------------+ + a_row< finished, canceled_event, canceled, &x::cancel_all_betting_market_groups >, + g_row< finished, betting_market_group_resolved_event,settled, &x::all_betting_market_groups_are_resolved >, + g_row< finished, betting_market_group_resolved_event,canceled, &x::all_betting_market_groups_are_canceled > + > {}; + + template + void no_transition(Event const& e, Fsm& ,int state) + { + FC_THROW_EXCEPTION(graphene::chain::no_transition, "No transition"); + } + + event_object* event_obj; + event_state_machine_(event_object* event_obj) : event_obj(event_obj) {} + }; + typedef msm::back::state_machine event_state_machine; + + } // end anonymous namespace + + class event_object::impl { + public: + event_state_machine state_machine; + + impl(event_object* self) : state_machine(self) {} + }; + + event_object::event_object() : + at_least_one_betting_market_group_settled(false), + my(new impl(this)) + { + } + + event_object::event_object(const event_object& rhs) : + graphene::db::abstract_object(rhs), + name(rhs.name), + season(rhs.season), + start_time(rhs.start_time), + event_group_id(rhs.event_group_id), + at_least_one_betting_market_group_settled(rhs.at_least_one_betting_market_group_settled), + scores(rhs.scores), + my(new impl(this)) + { + my->state_machine = rhs.my->state_machine; + my->state_machine.event_obj = this; + } + + event_object& event_object::operator=(const event_object& rhs) + { + //graphene::db::abstract_object::operator=(rhs); + id = rhs.id; + name = rhs.name; + season = rhs.season; + start_time = rhs.start_time; + event_group_id = rhs.event_group_id; + at_least_one_betting_market_group_settled = rhs.at_least_one_betting_market_group_settled; + scores = rhs.scores; + + my->state_machine = rhs.my->state_machine; + my->state_machine.event_obj = this; + + return *this; + } + + event_object::~event_object() + { + } + + namespace { + + bool verify_event_status_constants() + { + unsigned error_count = 0; + typedef msm::back::generate_state_set::type all_states; + static char const* filled_state_names[mpl::size::value]; + mpl::for_each > + (msm::back::fill_state_names(filled_state_names)); + for (unsigned i = 0; i < mpl::size::value; ++i) + { + try + { + // this is an approximate test, the state name provided by typeinfo will be mangled, but should + // at least contain the string we're looking for + const char* fc_reflected_value_name = fc::reflector::to_string((event_state)i); + if (!strcmp(fc_reflected_value_name, filled_state_names[i])) + fc_elog(fc::logger::get("default"), + "Error, state string mismatch between fc and boost::msm for int value ${int_value}: boost::msm -> ${boost_string}, fc::reflect -> ${fc_string}", + ("int_value", i)("boost_string", filled_state_names[i])("fc_string", fc_reflected_value_name)); + } + catch (const fc::bad_cast_exception&) + { + fc_elog(fc::logger::get("default"), + "Error, no reflection for value ${int_value} in enum event_status", + ("int_value", i)); + ++error_count; + } + } + dlog("Done checking constants"); + + return error_count == 0; + } + } // end anonymous namespace + + event_status event_object::get_status() const + { + static bool state_constants_are_correct = verify_event_status_constants(); + (void)&state_constants_are_correct; + event_state state = (event_state)my->state_machine.current_state()[0]; + + ddump((state)); + + switch (state) + { + case event_state::upcoming: + return event_status::upcoming; + case event_state::frozen_upcoming: + case event_state::frozen_in_progress: + return event_status::frozen; + case event_state::in_progress: + return event_status::in_progress; + case event_state::finished: + return event_status::finished; + case event_state::canceled: + return event_status::canceled; + case event_state::settled: + return event_status::settled; + default: + FC_THROW("Unexpected event state"); + }; + } + + void event_object::pack_impl(std::ostream& stream) const + { + boost::archive::binary_oarchive oa(stream, boost::archive::no_header|boost::archive::no_codecvt|boost::archive::no_xml_tag_checking); + oa << my->state_machine; + } + + void event_object::unpack_impl(std::istream& stream) + { + boost::archive::binary_iarchive ia(stream, boost::archive::no_header|boost::archive::no_codecvt|boost::archive::no_xml_tag_checking); + ia >> my->state_machine; + } + + void event_object::on_upcoming_event(database& db) + { + my->state_machine.process_event(upcoming_event(db)); + } + + void event_object::on_in_progress_event(database& db) + { + my->state_machine.process_event(in_progress_event(db)); + } + + void event_object::on_frozen_event(database& db) + { + my->state_machine.process_event(frozen_event(db)); + } + + void event_object::on_finished_event(database& db) + { + my->state_machine.process_event(finished_event(db)); + } + + void event_object::on_canceled_event(database& db) + { + my->state_machine.process_event(canceled_event(db)); + } + + void event_object::on_betting_market_group_resolved(database& db, betting_market_group_id_type resolved_group, bool was_canceled) + { + my->state_machine.process_event(betting_market_group_resolved_event(db, resolved_group, was_canceled)); + } + + void event_object::on_betting_market_group_closed(database& db, betting_market_group_id_type closed_group) + { + my->state_machine.process_event(betting_market_group_closed_event(db, closed_group)); + } + + // These are the only statuses that can be explicitly set by witness operations. The missing + // status, 'settled', is automatically set when all of the betting market groups have + // settled/canceled + void event_object::dispatch_new_status(database& db, event_status new_status) + { + switch (new_status) { + case event_status::upcoming: // by witnesses to unfreeze a frozen event + on_upcoming_event(db); + break; + case event_status::in_progress: // by witnesses when the event starts + on_in_progress_event(db); + break; + case event_status::frozen: // by witnesses when the event needs to be frozen + on_frozen_event(db); + break; + case event_status::finished: // by witnesses when the event is complete + on_finished_event(db); + break; + case event_status::canceled: // by witnesses to cancel the event + on_canceled_event(db); + break; + default: + FC_THROW("Status ${new_status} cannot be explicitly set", ("new_status", new_status)); + } + } + +} } // graphene::chain + +namespace fc { + // Manually reflect event_object to variant to properly reflect "state" + void to_variant(const graphene::chain::event_object& event_obj, fc::variant& v) + { + fc::mutable_variant_object o; + o("id", event_obj.id) + ("name", event_obj.name) + ("season", event_obj.season) + ("start_time", event_obj.start_time) + ("event_group_id", event_obj.event_group_id) + ("scores", event_obj.scores) + ("status", event_obj.get_status()); + + v = o; + } + + // Manually reflect event_object to variant to properly reflect "state" + void from_variant(const fc::variant& v, graphene::chain::event_object& event_obj) + { + event_obj.id = v["id"].as(); + event_obj.name = v["name"].as(); + event_obj.season = v["season"].as(); + event_obj.start_time = v["start_time"].as >(); + event_obj.event_group_id = v["event_group_id"].as(); + event_obj.scores = v["scores"].as>(); + graphene::chain::event_status status = v["status"].as(); + const_cast(event_obj.my->state_machine.current_state())[0] = (int)status; + } +} //end namespace fc + + diff --git a/libraries/chain/include/graphene/chain/betting_market_object.hpp b/libraries/chain/include/graphene/chain/betting_market_object.hpp index 74b257d2..4f6665c8 100644 --- a/libraries/chain/include/graphene/chain/betting_market_object.hpp +++ b/libraries/chain/include/graphene/chain/betting_market_object.hpp @@ -31,10 +31,24 @@ #include namespace graphene { namespace chain { + class betting_market_object; + class betting_market_group_object; +} } +namespace fc { + void to_variant(const graphene::chain::betting_market_object& betting_market_obj, fc::variant& v); + void from_variant(const fc::variant& v, graphene::chain::betting_market_object& betting_market_obj); + void to_variant(const graphene::chain::betting_market_group_object& betting_market_group_obj, fc::variant& v); + void from_variant(const fc::variant& v, graphene::chain::betting_market_group_object& betting_market_group_obj); +} //end namespace fc + +namespace graphene { namespace chain { + +FC_DECLARE_EXCEPTION(no_transition, 100000, "Invalid state transition"); class database; struct by_event_id; +struct by_settling_time; struct by_betting_market_group_id; class betting_market_rules_object : public graphene::db::abstract_object< betting_market_rules_object > @@ -54,6 +68,11 @@ class betting_market_group_object : public graphene::db::abstract_object< bettin static const uint8_t space_id = protocol_ids; static const uint8_t type_id = betting_market_group_object_type; + betting_market_group_object(); + betting_market_group_object(const betting_market_group_object& rhs); + ~betting_market_group_object(); + betting_market_group_object& operator=(const betting_market_group_object& rhs); + internationalized_string_type description; event_id_type event_id; @@ -64,9 +83,51 @@ class betting_market_group_object : public graphene::db::abstract_object< bettin share_type total_matched_bets_amount; - bool frozen; + bool never_in_play; - bool delay_bets; + uint32_t delay_before_settling; + + fc::optional settling_time; // the time the payout will occur (set after grading) + + bool bets_are_allowed() const { + return get_status() == betting_market_group_status::upcoming || + get_status() == betting_market_group_status::in_play; + } + + bool bets_are_delayed() const { + return get_status() == betting_market_group_status::in_play; + } + + betting_market_group_status get_status() const; + + // serialization functions: + // for serializing to raw, go through a temporary sstream object to avoid + // having to implement serialization in the header file + template + friend Stream& operator<<( Stream& s, const betting_market_group_object& betting_market_group_obj ); + + template + friend Stream& operator>>( Stream& s, betting_market_group_object& betting_market_group_obj ); + + friend void ::fc::to_variant(const graphene::chain::betting_market_group_object& betting_market_group_obj, fc::variant& v); + friend void ::fc::from_variant(const fc::variant& v, graphene::chain::betting_market_group_object& betting_market_group_obj); + + void pack_impl(std::ostream& stream) const; + void unpack_impl(std::istream& stream); + + void on_upcoming_event(database& db); + void on_in_play_event(database& db); + void on_frozen_event(database& db); + void on_closed_event(database& db, bool closed_by_event); + void on_graded_event(database& db); + void on_re_grading_event(database& db); + void on_settled_event(database& db); + void on_canceled_event(database& db, bool canceled_by_event); + void dispatch_new_status(database& db, betting_market_group_status new_status); + + private: + class impl; + std::unique_ptr my; }; class betting_market_object : public graphene::db::abstract_object< betting_market_object > @@ -75,11 +136,49 @@ class betting_market_object : public graphene::db::abstract_object< betting_mark static const uint8_t space_id = protocol_ids; static const uint8_t type_id = betting_market_object_type; + betting_market_object(); + betting_market_object(const betting_market_object& rhs); + ~betting_market_object(); + betting_market_object& operator=(const betting_market_object& rhs); + betting_market_group_id_type group_id; internationalized_string_type description; internationalized_string_type payout_condition; + + // once the market is graded, this holds the proposed grading + // after settling/canceling, this is the actual grading + fc::optional resolution; + + betting_market_status get_status() const; + + void cancel_all_unmatched_bets(database& db) const; + + // serialization functions: + // for serializing to raw, go through a temporary sstream object to avoid + // having to implement serialization in the header file + template + friend Stream& operator<<( Stream& s, const betting_market_object& betting_market_obj ); + + template + friend Stream& operator>>( Stream& s, betting_market_object& betting_market_obj ); + + friend void ::fc::to_variant(const graphene::chain::betting_market_object& betting_market_obj, fc::variant& v); + friend void ::fc::from_variant(const fc::variant& v, graphene::chain::betting_market_object& betting_market_obj); + + void pack_impl(std::ostream& stream) const; + void unpack_impl(std::istream& stream); + + void on_unresolved_event(database& db); + void on_frozen_event(database& db); + void on_closed_event(database& db); + void on_graded_event(database& db, betting_market_resolution_type new_grading); + void on_settled_event(database& db); + void on_canceled_event(database& db); + private: + class impl; + std::unique_ptr my; }; class bet_object : public graphene::db::abstract_object< bet_object > @@ -147,7 +246,8 @@ typedef multi_index_container< betting_market_group_object, indexed_by< ordered_unique< tag, member< object, object_id_type, &object::id > >, - ordered_non_unique< tag, member > + ordered_non_unique< tag, member >, + ordered_non_unique< tag, member, &betting_market_group_object::settling_time> > > > betting_market_group_object_multi_index_type; typedef generic_index betting_market_group_object_index; @@ -513,11 +613,101 @@ typedef multi_index_container< > > betting_market_position_multi_index_type; typedef generic_index betting_market_position_index; + + +template +inline Stream& operator<<( Stream& s, const betting_market_object& betting_market_obj ) +{ + // pack all fields exposed in the header in the usual way + // instead of calling the derived pack, just serialize the one field in the base class + // fc::raw::pack >(s, betting_market_obj); + fc::raw::pack(s, betting_market_obj.id); + fc::raw::pack(s, betting_market_obj.group_id); + fc::raw::pack(s, betting_market_obj.description); + fc::raw::pack(s, betting_market_obj.payout_condition); + fc::raw::pack(s, betting_market_obj.resolution); + + // fc::raw::pack the contents hidden in the impl class + std::ostringstream stream; + betting_market_obj.pack_impl(stream); + std::string stringified_stream(stream.str()); + fc::raw::pack(s, stream.str()); + + return s; +} +template +inline Stream& operator>>( Stream& s, betting_market_object& betting_market_obj ) +{ + // unpack all fields exposed in the header in the usual way + //fc::raw::unpack >(s, betting_market_obj); + fc::raw::unpack(s, betting_market_obj.id); + fc::raw::unpack(s, betting_market_obj.group_id); + fc::raw::unpack(s, betting_market_obj.description); + fc::raw::unpack(s, betting_market_obj.payout_condition); + fc::raw::unpack(s, betting_market_obj.resolution); + + // fc::raw::unpack the contents hidden in the impl class + std::string stringified_stream; + fc::raw::unpack(s, stringified_stream); + std::istringstream stream(stringified_stream); + betting_market_obj.unpack_impl(stream); + + return s; +} + + +template +inline Stream& operator<<( Stream& s, const betting_market_group_object& betting_market_group_obj ) +{ + // pack all fields exposed in the header in the usual way + // instead of calling the derived pack, just serialize the one field in the base class + // fc::raw::pack >(s, betting_market_group_obj); + fc::raw::pack(s, betting_market_group_obj.id); + fc::raw::pack(s, betting_market_group_obj.description); + fc::raw::pack(s, betting_market_group_obj.event_id); + fc::raw::pack(s, betting_market_group_obj.rules_id); + fc::raw::pack(s, betting_market_group_obj.asset_id); + fc::raw::pack(s, betting_market_group_obj.total_matched_bets_amount); + fc::raw::pack(s, betting_market_group_obj.never_in_play); + fc::raw::pack(s, betting_market_group_obj.delay_before_settling); + fc::raw::pack(s, betting_market_group_obj.settling_time); + // fc::raw::pack the contents hidden in the impl class + std::ostringstream stream; + betting_market_group_obj.pack_impl(stream); + std::string stringified_stream(stream.str()); + fc::raw::pack(s, stream.str()); + + return s; +} +template +inline Stream& operator>>( Stream& s, betting_market_group_object& betting_market_group_obj ) +{ + // unpack all fields exposed in the header in the usual way + //fc::raw::unpack >(s, betting_market_group_obj); + fc::raw::unpack(s, betting_market_group_obj.id); + fc::raw::unpack(s, betting_market_group_obj.description); + fc::raw::unpack(s, betting_market_group_obj.event_id); + fc::raw::unpack(s, betting_market_group_obj.rules_id); + fc::raw::unpack(s, betting_market_group_obj.asset_id); + fc::raw::unpack(s, betting_market_group_obj.total_matched_bets_amount); + fc::raw::unpack(s, betting_market_group_obj.never_in_play); + fc::raw::unpack(s, betting_market_group_obj.delay_before_settling); + fc::raw::unpack(s, betting_market_group_obj.settling_time); + + // fc::raw::unpack the contents hidden in the impl class + std::string stringified_stream; + fc::raw::unpack(s, stringified_stream); + std::istringstream stream(stringified_stream); + betting_market_group_obj.unpack_impl(stream); + + return s; +} + } } // graphene::chain FC_REFLECT_DERIVED( graphene::chain::betting_market_rules_object, (graphene::db::object), (name)(description) ) -FC_REFLECT_DERIVED( graphene::chain::betting_market_group_object, (graphene::db::object), (description)(event_id)(rules_id)(asset_id)(frozen)(delay_bets)(total_matched_bets_amount) ) -FC_REFLECT_DERIVED( graphene::chain::betting_market_object, (graphene::db::object), (group_id)(description)(payout_condition) ) +FC_REFLECT_DERIVED( graphene::chain::betting_market_group_object, (graphene::db::object), (description) ) +FC_REFLECT_DERIVED( graphene::chain::betting_market_object, (graphene::db::object), (group_id) ) FC_REFLECT_DERIVED( graphene::chain::bet_object, (graphene::db::object), (bettor_id)(betting_market_id)(amount_to_bet)(backer_multiplier)(back_or_lay)(end_of_delay) ) FC_REFLECT_DERIVED( graphene::chain::betting_market_position_object, (graphene::db::object), (bettor_id)(betting_market_id)(pay_if_payout_condition)(pay_if_not_payout_condition)(pay_if_canceled)(pay_if_not_canceled)(fees_collected) ) diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index 00103d49..ad3185db 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -391,12 +391,11 @@ namespace graphene { namespace chain { void cancel_bet(const bet_object& bet, bool create_virtual_op = true); void cancel_all_unmatched_bets_on_betting_market(const betting_market_object& betting_market); void cancel_all_unmatched_bets_on_betting_market_group(const betting_market_group_object& betting_market_group); - void cancel_all_betting_markets_for_event(const event_object&); void validate_betting_market_group_resolutions(const betting_market_group_object& betting_market_group, const std::map& resolutions); void resolve_betting_market_group(const betting_market_group_object& betting_market_group, - const std::map& resolutions, - bool do_not_remove = false); + const std::map& resolutions); + void settle_betting_market_group(const betting_market_group_object& betting_market_group); /** * @brief Process a new bet * @param new_bet_object The new bet to process @@ -481,6 +480,7 @@ namespace graphene { namespace chain { void update_maintenance_flag( bool new_maintenance_flag ); void update_withdraw_permissions(); void update_tournaments(); + void update_betting_markets(fc::time_point_sec current_block_time); bool check_for_blackswan( const asset_object& mia, bool enable_black_swan = true ); ///Steps performed only at maintenance intervals diff --git a/libraries/chain/include/graphene/chain/event_object.hpp b/libraries/chain/include/graphene/chain/event_object.hpp index 88ccd843..52fb9f53 100644 --- a/libraries/chain/include/graphene/chain/event_object.hpp +++ b/libraries/chain/include/graphene/chain/event_object.hpp @@ -26,6 +26,16 @@ #include #include #include +#include + +namespace graphene { namespace chain { + class event_object; +} } + +namespace fc { + void to_variant(const graphene::chain::event_object& event_obj, fc::variant& v); + void from_variant(const fc::variant& v, graphene::chain::event_object& event_obj); +} //end namespace fc namespace graphene { namespace chain { @@ -37,6 +47,11 @@ class event_object : public graphene::db::abstract_object< event_object > static const uint8_t space_id = protocol_ids; static const uint8_t type_id = event_object_type; + event_object(); + event_object(const event_object& rhs); + ~event_object(); + event_object& operator=(const event_object& rhs); + internationalized_string_type name; internationalized_string_type season; @@ -45,10 +60,38 @@ class event_object : public graphene::db::abstract_object< event_object > event_group_id_type event_group_id; - bool is_live_market; + bool at_least_one_betting_market_group_settled; - event_status status; + event_status get_status() const; vector scores; + + // serialization functions: + // for serializing to raw, go through a temporary sstream object to avoid + // having to implement serialization in the header file + template + friend Stream& operator<<( Stream& s, const event_object& event_obj ); + + template + friend Stream& operator>>( Stream& s, event_object& event_obj ); + + friend void ::fc::to_variant(const graphene::chain::event_object& event_obj, fc::variant& v); + friend void ::fc::from_variant(const fc::variant& v, graphene::chain::event_object& event_obj); + + void pack_impl(std::ostream& stream) const; + void unpack_impl(std::istream& stream); + + void on_upcoming_event(database& db); + void on_in_progress_event(database& db); + void on_frozen_event(database& db); + void on_finished_event(database& db); + void on_canceled_event(database& db); + void on_settled_event(database& db); + void on_betting_market_group_resolved(database& db, betting_market_group_id_type resolved_group, bool was_canceled); + void on_betting_market_group_closed(database& db, betting_market_group_id_type closed_group); + void dispatch_new_status(database& db, event_status new_status); + private: + class impl; + std::unique_ptr my; }; struct by_event_group_id; @@ -59,6 +102,52 @@ typedef multi_index_container< ordered_non_unique< tag, member< event_object, event_group_id_type, &event_object::event_group_id > > > > event_object_multi_index_type; typedef generic_index event_object_index; -} } // graphene::chain -FC_REFLECT_DERIVED( graphene::chain::event_object, (graphene::db::object), (name)(season)(start_time)(event_group_id)(is_live_market)(status)(scores) ) + template + inline Stream& operator<<( Stream& s, const event_object& event_obj ) + { + fc_elog(fc::logger::get("event"), "In event_obj to_raw"); + // pack all fields exposed in the header in the usual way + // instead of calling the derived pack, just serialize the one field in the base class + // fc::raw::pack >(s, event_obj); + fc::raw::pack(s, event_obj.id); + fc::raw::pack(s, event_obj.name); + fc::raw::pack(s, event_obj.season); + fc::raw::pack(s, event_obj.start_time); + fc::raw::pack(s, event_obj.event_group_id); + fc::raw::pack(s, event_obj.at_least_one_betting_market_group_settled); + fc::raw::pack(s, event_obj.scores); + + // fc::raw::pack the contents hidden in the impl class + std::ostringstream stream; + event_obj.pack_impl(stream); + std::string stringified_stream(stream.str()); + fc::raw::pack(s, stream.str()); + + return s; + } + template + inline Stream& operator>>( Stream& s, event_object& event_obj ) + { + fc_elog(fc::logger::get("event"), "In event_obj from_raw"); + // unpack all fields exposed in the header in the usual way + //fc::raw::unpack >(s, event_obj); + fc::raw::unpack(s, event_obj.id); + fc::raw::unpack(s, event_obj.name); + fc::raw::unpack(s, event_obj.season); + fc::raw::unpack(s, event_obj.start_time); + fc::raw::unpack(s, event_obj.event_group_id); + fc::raw::unpack(s, event_obj.at_least_one_betting_market_group_settled); + fc::raw::unpack(s, event_obj.scores); + + // fc::raw::unpack the contents hidden in the impl class + std::string stringified_stream; + fc::raw::unpack(s, stringified_stream); + std::istringstream stream(stringified_stream); + event_obj.unpack_impl(stream); + + return s; + } +} } // graphene::chain +FC_REFLECT(graphene::chain::event_object, (name)) + diff --git a/libraries/chain/include/graphene/chain/protocol/betting_market.hpp b/libraries/chain/include/graphene/chain/protocol/betting_market.hpp index 33ff9abf..a27758c7 100644 --- a/libraries/chain/include/graphene/chain/protocol/betting_market.hpp +++ b/libraries/chain/include/graphene/chain/protocol/betting_market.hpp @@ -69,6 +69,35 @@ struct betting_market_rules_update_operation : public base_operation void validate()const; }; +enum class betting_market_status +{ + unresolved, /// no grading has been published for this betting market + frozen, /// bets are suspended, no bets allowed + graded, /// grading of win or not_win has been published + canceled, /// the betting market is canceled, no further bets are allowed + settled, /// the betting market has been paid out + BETTING_MARKET_STATUS_COUNT +}; + + +/** + * The status of a betting market group. This controls the behavior of the betting + * markets in the group. + */ +enum class betting_market_group_status +{ + upcoming, /// betting markets are accepting bets, will never go "in_play" + in_play, /// betting markets are delaying bets + closed, /// betting markets are no longer accepting bets + graded, /// witnesses have published win/not win for the betting markets + re_grading, /// initial win/not win grading has been challenged + settled, /// paid out + frozen, /// betting markets are not accepting bets + canceled, /// canceled + BETTING_MARKET_GROUP_STATUS_COUNT +}; + + struct betting_market_group_create_operation : public base_operation { struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; @@ -97,6 +126,20 @@ struct betting_market_group_create_operation : public base_operation */ asset_id_type asset_id; + /** + * If true, this market will never go "in-play" + */ + bool never_in_play; + + /** + * After a grading has been published, the blockchain will wait this many + * seconds before settling (paying the winners). + * If the published grading is flagged (challenged) during this period, + * settling will be delayed indefinitely until the betting market + * group is re-graded (not implemented) + */ + uint32_t delay_before_settling; + extensions_type extensions; account_id_type fee_payer()const { return GRAPHENE_WITNESS_ACCOUNT; } @@ -114,9 +157,7 @@ struct betting_market_group_update_operation : public base_operation optional new_rules_id; - optional freeze; - - optional delay_bets; + optional status; extensions_type extensions; @@ -384,13 +425,33 @@ FC_REFLECT( graphene::chain::betting_market_rules_update_operation::fee_paramete FC_REFLECT( graphene::chain::betting_market_rules_update_operation, (fee)(new_name)(new_description)(extensions)(betting_market_rules_id) ) +FC_REFLECT_ENUM( graphene::chain::betting_market_status, + (unresolved) + (frozen) + (graded) + (canceled) + (settled) + (BETTING_MARKET_STATUS_COUNT) ) +FC_REFLECT_ENUM( graphene::chain::betting_market_group_status, + (upcoming) + (in_play) + (closed) + (graded) + (re_grading) + (settled) + (frozen) + (canceled) + (BETTING_MARKET_GROUP_STATUS_COUNT) ) + FC_REFLECT( graphene::chain::betting_market_group_create_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::betting_market_group_create_operation, - (fee)(description)(event_id)(rules_id)(asset_id)(extensions) ) + (fee)(description)(event_id)(rules_id)(asset_id) + (never_in_play)(delay_before_settling) + (extensions) ) 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_rules_id)(freeze)(delay_bets)(extensions) ) + (fee)(betting_market_group_id)(new_description)(new_rules_id)(status)(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/chain/include/graphene/chain/protocol/event.hpp b/libraries/chain/include/graphene/chain/protocol/event.hpp index 6f50e612..f0da95f7 100644 --- a/libraries/chain/include/graphene/chain/protocol/event.hpp +++ b/libraries/chain/include/graphene/chain/protocol/event.hpp @@ -54,6 +54,26 @@ struct event_create_operation : public base_operation void validate()const; }; +/** + * The status of an event. This is used to display in the UI, and setting + * the event's status to certain values will propagate down to the + * betting market groups in the event: + * - when set to `in_progress`, all betting market groups are set to `in_play` + * - when set to `completed`, all betting market groups are set to `closed` + * - when set to `frozen`, all betting market groups are set to `frozen` + * - when set to `canceled`, all betting market groups are set to `canceled` + */ +enum class event_status +{ + upcoming, /// Event has not started yet, betting is allowed + in_progress, /// Event is in progress, if "in-play" betting is enabled, bets will be delayed + frozen, /// Betting is temporarily disabled + finished, /// Event has finished, no more betting allowed + canceled, /// Event has been canceled, all betting markets have been canceled + settled, /// All betting markets have been paid out + STATUS_COUNT +}; + struct event_update_operation : public base_operation { struct fee_parameters_type { uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; }; @@ -69,7 +89,7 @@ struct event_update_operation : public base_operation optional new_start_time; - optional is_live_market; + optional new_status; extensions_type extensions; @@ -77,20 +97,6 @@ struct event_update_operation : public base_operation void validate()const; }; -/** - * The status of an event; this is only used for display, the blockchain does - * not care about the event's status - */ -enum class event_status -{ - upcoming, - in_progress, - frozen, - completed, - canceled, - STATUS_COUNT -}; - /** * The current (or final) score of an event. * This is only used for display to the user, witnesses must resolve each @@ -132,9 +138,9 @@ FC_REFLECT( graphene::chain::event_create_operation, FC_REFLECT( graphene::chain::event_update_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::event_update_operation, - (fee)(event_id)(new_event_group_id)(new_name)(new_season)(new_start_time)(is_live_market)(extensions) ) + (fee)(event_id)(new_event_group_id)(new_name)(new_season)(new_start_time)(new_status)(extensions) ) -FC_REFLECT_ENUM( graphene::chain::event_status, (upcoming)(in_progress)(frozen)(completed)(canceled)(STATUS_COUNT) ) +FC_REFLECT_ENUM( graphene::chain::event_status, (upcoming)(in_progress)(frozen)(finished)(canceled)(settled)(STATUS_COUNT) ) FC_REFLECT( graphene::chain::event_update_status_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::event_update_status_operation, (fee)(event_id)(status)(scores)(extensions) ) diff --git a/libraries/plugins/bookie/bookie_plugin.cpp b/libraries/plugins/bookie/bookie_plugin.cpp index 0932ddf3..564df3a1 100644 --- a/libraries/plugins/bookie/bookie_plugin.cpp +++ b/libraries/plugins/bookie/bookie_plugin.cpp @@ -96,6 +96,111 @@ void persistent_bet_object_helper::object_modified(const object& after) } //////////// end bet_object /////////////////// +class persistent_betting_market_object_helper : public secondary_index +{ + public: + virtual ~persistent_betting_market_object_helper() {} + + virtual void object_inserted(const object& obj) override; + //virtual void object_removed( const object& obj ) override; + //virtual void about_to_modify( const object& before ) override; + virtual void object_modified(const object& after) override; + void set_plugin_instance(bookie_plugin* instance) { _bookie_plugin = instance; } + private: + bookie_plugin* _bookie_plugin; +}; + +void persistent_betting_market_object_helper::object_inserted(const object& obj) +{ + const betting_market_object& betting_market_obj = *boost::polymorphic_downcast(&obj); + _bookie_plugin->database().create([&](persistent_betting_market_object& saved_betting_market_obj) { + saved_betting_market_obj.ephemeral_betting_market_object = betting_market_obj; + }); +} +void persistent_betting_market_object_helper::object_modified(const object& after) +{ + database& db = _bookie_plugin->database(); + auto& persistent_betting_markets_by_betting_market_id = db.get_index_type().indices().get(); + const betting_market_object& betting_market_obj = *boost::polymorphic_downcast(&after); + auto iter = persistent_betting_markets_by_betting_market_id.find(betting_market_obj.id); + assert (iter != persistent_betting_markets_by_betting_market_id.end()); + if (iter != persistent_betting_markets_by_betting_market_id.end()) + db.modify(*iter, [&](persistent_betting_market_object& saved_betting_market_obj) { + saved_betting_market_obj.ephemeral_betting_market_object = betting_market_obj; + }); +} + +//////////// end betting_market_object /////////////////// +class persistent_betting_market_group_object_helper : public secondary_index +{ + public: + virtual ~persistent_betting_market_group_object_helper() {} + + virtual void object_inserted(const object& obj) override; + //virtual void object_removed( const object& obj ) override; + //virtual void about_to_modify( const object& before ) override; + virtual void object_modified(const object& after) override; + void set_plugin_instance(bookie_plugin* instance) { _bookie_plugin = instance; } + private: + bookie_plugin* _bookie_plugin; +}; + +void persistent_betting_market_group_object_helper::object_inserted(const object& obj) +{ + const betting_market_group_object& betting_market_group_obj = *boost::polymorphic_downcast(&obj); + _bookie_plugin->database().create([&](persistent_betting_market_group_object& saved_betting_market_group_obj) { + saved_betting_market_group_obj.ephemeral_betting_market_group_object = betting_market_group_obj; + }); +} +void persistent_betting_market_group_object_helper::object_modified(const object& after) +{ + database& db = _bookie_plugin->database(); + auto& persistent_betting_market_groups_by_betting_market_group_id = db.get_index_type().indices().get(); + const betting_market_group_object& betting_market_group_obj = *boost::polymorphic_downcast(&after); + auto iter = persistent_betting_market_groups_by_betting_market_group_id.find(betting_market_group_obj.id); + assert (iter != persistent_betting_market_groups_by_betting_market_group_id.end()); + if (iter != persistent_betting_market_groups_by_betting_market_group_id.end()) + db.modify(*iter, [&](persistent_betting_market_group_object& saved_betting_market_group_obj) { + saved_betting_market_group_obj.ephemeral_betting_market_group_object = betting_market_group_obj; + }); +} + +//////////// end betting_market_group_object /////////////////// +class persistent_event_object_helper : public secondary_index +{ + public: + virtual ~persistent_event_object_helper() {} + + virtual void object_inserted(const object& obj) override; + //virtual void object_removed( const object& obj ) override; + //virtual void about_to_modify( const object& before ) override; + virtual void object_modified(const object& after) override; + void set_plugin_instance(bookie_plugin* instance) { _bookie_plugin = instance; } + private: + bookie_plugin* _bookie_plugin; +}; + +void persistent_event_object_helper::object_inserted(const object& obj) +{ + const event_object& event_obj = *boost::polymorphic_downcast(&obj); + _bookie_plugin->database().create([&](persistent_event_object& saved_event_obj) { + saved_event_obj.ephemeral_event_object = event_obj; + }); +} +void persistent_event_object_helper::object_modified(const object& after) +{ + database& db = _bookie_plugin->database(); + auto& persistent_events_by_event_id = db.get_index_type().indices().get(); + const event_object& event_obj = *boost::polymorphic_downcast(&after); + auto iter = persistent_events_by_event_id.find(event_obj.id); + assert (iter != persistent_events_by_event_id.end()); + if (iter != persistent_events_by_event_id.end()) + db.modify(*iter, [&](persistent_event_object& saved_event_obj) { + saved_event_obj.ephemeral_event_object = event_obj; + }); +} + +//////////// end event_object /////////////////// class bookie_plugin_impl { public: @@ -154,122 +259,14 @@ bookie_plugin_impl::~bookie_plugin_impl() void bookie_plugin_impl::on_objects_new(const vector& new_object_ids) { - //idump((new_object_ids)); - graphene::chain::database& db = database(); - auto& event_id_index = db.get_index_type().indices().get(); - auto& betting_market_group_id_index = db.get_index_type().indices().get(); - auto& betting_market_id_index = db.get_index_type().indices().get(); - - for (const object_id_type& new_object_id : new_object_ids) - { - if (new_object_id.space() == event_id_type::space_id && - new_object_id.type() == event_id_type::type_id) - { - event_id_type new_event_id = new_object_id; - ilog("Creating new persistent event object ${id}", ("id", new_event_id)); - db.create([&](persistent_event_object& saved_event_obj) { - saved_event_obj.ephemeral_event_object = new_event_id(db); - }); - } - else if (new_object_id.space() == betting_market_group_object::space_id && - new_object_id.type() == betting_market_group_object::type_id) - { - betting_market_group_id_type new_betting_market_group_id = new_object_id; - ilog("Creating new persistent betting_market_group object ${id}", ("id", new_betting_market_group_id)); - db.create([&](persistent_betting_market_group_object& saved_betting_market_group_obj) { - saved_betting_market_group_obj.ephemeral_betting_market_group_object = new_betting_market_group_id(db); - }); - } - else if (new_object_id.space() == betting_market_object::space_id && - new_object_id.type() == betting_market_object::type_id) - { - betting_market_id_type new_betting_market_id = new_object_id; - ilog("Creating new persistent betting_market object ${id}", ("id", new_betting_market_id)); - db.create([&](persistent_betting_market_object& saved_betting_market_obj) { - saved_betting_market_obj.ephemeral_betting_market_object = new_betting_market_id(db); - }); - } - } } void bookie_plugin_impl::on_objects_removed(const vector& removed_object_ids) { - //idump((removed_object_ids)); } void bookie_plugin_impl::on_objects_changed(const vector& changed_object_ids) { - //idump((changed_object_ids)); - graphene::chain::database& db = database(); - auto& event_id_index = db.get_index_type().indices().get(); - auto& betting_market_group_id_index = db.get_index_type().indices().get(); - auto& betting_market_id_index = db.get_index_type().indices().get(); - - for (const object_id_type& changed_object_id : changed_object_ids) - { - if (changed_object_id.space() == event_id_type::space_id && - changed_object_id.type() == event_id_type::type_id) - { - event_id_type changed_event_id = changed_object_id; - const persistent_event_object* old_event_obj = nullptr; - - auto persistent_event_iter = event_id_index.find(changed_event_id); - if (persistent_event_iter != event_id_index.end()) - old_event_obj = &*persistent_event_iter; - - if (old_event_obj) - { - ilog("Modifying persistent event object ${id}", ("id", changed_event_id)); - db.modify(*old_event_obj, [&](persistent_event_object& saved_event_obj) { - saved_event_obj.ephemeral_event_object = changed_event_id(db); - }); - } - else - elog("Received change notification on event ${event_id} that we didn't know about", ("event_id", changed_event_id)); - } - else if (changed_object_id.space() == betting_market_group_object::space_id && - changed_object_id.type() == betting_market_group_object::type_id) - { - betting_market_group_id_type changed_betting_market_group_id = changed_object_id; - const persistent_betting_market_group_object* old_betting_market_group_obj = nullptr; - - auto persistent_betting_market_group_iter = betting_market_group_id_index.find(changed_betting_market_group_id); - if (persistent_betting_market_group_iter != betting_market_group_id_index.end()) - old_betting_market_group_obj = &*persistent_betting_market_group_iter; - - if (old_betting_market_group_obj) - { - ilog("Modifying persistent betting_market_group object ${id}", ("id", changed_betting_market_group_id)); - db.modify(*old_betting_market_group_obj, [&](persistent_betting_market_group_object& saved_betting_market_group_obj) { - saved_betting_market_group_obj.ephemeral_betting_market_group_object = changed_betting_market_group_id(db); - }); - } - else - elog("Received change notification on betting market group ${betting_market_group_id} that we didn't know about", - ("betting_market_group_id", changed_betting_market_group_id)); - } - else if (changed_object_id.space() == betting_market_object::space_id && - changed_object_id.type() == betting_market_object::type_id) - { - betting_market_id_type changed_betting_market_id = changed_object_id; - const persistent_betting_market_object* old_betting_market_obj = nullptr; - - auto persistent_betting_market_iter = betting_market_id_index.find(changed_betting_market_id); - if (persistent_betting_market_iter != betting_market_id_index.end()) - old_betting_market_obj = &*persistent_betting_market_iter; - - if (old_betting_market_obj) - { - ilog("Modifying persistent betting_market object ${id}", ("id", changed_betting_market_id)); - db.modify(*old_betting_market_obj, [&](persistent_betting_market_object& saved_betting_market_obj) { - saved_betting_market_obj.ephemeral_betting_market_object = changed_betting_market_id(db); - }); - } - else - elog("Received change notification on betting market ${betting_market_id} that we didn't know about", - ("betting_market_id", changed_betting_market_id)); - } - } } bool is_operation_history_object_stored(operation_history_id_type id) @@ -287,7 +284,8 @@ bool is_operation_history_object_stored(operation_history_id_type id) } void bookie_plugin_impl::on_block_applied( const signed_block& ) -{ +{ try { + graphene::chain::database& db = database(); const vector >& hist = db.get_applied_operations(); for( const optional& o_op : hist ) @@ -315,11 +313,28 @@ void bookie_plugin_impl::on_block_applied( const signed_block& ) }); const bet_object& bet_obj = bet_iter->ephemeral_bet_object; - const betting_market_object& betting_market = bet_obj.betting_market_id(db); // TODO: this needs to look at the persistent version - const betting_market_group_object& betting_market_group = betting_market.group_id(db); // TODO: as does this - db.modify( betting_market_group, [&]( betting_market_group_object& obj ){ - obj.total_matched_bets_amount += amount_bet.amount; - }); + auto& persistent_betting_market_idx = db.get_index_type().indices().get(); + auto persistent_betting_market_object_iter = persistent_betting_market_idx.find(bet_obj.betting_market_id); + FC_ASSERT(persistent_betting_market_object_iter != persistent_betting_market_idx.end()); + const betting_market_object& betting_market = persistent_betting_market_object_iter->ephemeral_betting_market_object; + + auto& persistent_betting_market_group_idx = db.get_index_type().indices().get(); + auto persistent_betting_market_group_object_iter = persistent_betting_market_group_idx.find(betting_market.group_id); + FC_ASSERT(persistent_betting_market_group_object_iter != persistent_betting_market_group_idx.end()); + const betting_market_group_object& betting_market_group = persistent_betting_market_group_object_iter->ephemeral_betting_market_group_object; + + // if the object is still in the main database, keep the running total there + // otherwise, add it directly to the persistent version + auto& betting_market_group_idx = db.get_index_type().indices().get(); + auto betting_market_group_iter = betting_market_group_idx.find(betting_market_group.id); + if (betting_market_group_iter != betting_market_group_idx.end()) + db.modify( *betting_market_group_iter, [&]( betting_market_group_object& obj ){ + obj.total_matched_bets_amount += amount_bet.amount; + }); + else + db.modify( *persistent_betting_market_group_object_iter, [&]( persistent_betting_market_group_object& obj ){ + obj.ephemeral_betting_market_group_object.total_matched_bets_amount += amount_bet.amount; + }); } } else if( op.op.which() == operation::tag::value ) @@ -382,7 +397,7 @@ void bookie_plugin_impl::on_block_applied( const signed_block& ) } } -} +} FC_RETHROW_EXCEPTIONS( warn, "" ) } void bookie_plugin_impl::fill_localized_event_strings() { @@ -470,6 +485,21 @@ void bookie_plugin::plugin_initialize(const boost::program_options::variables_ma detail::persistent_bet_object_helper* persistent_bet_object_helper_index = nonconst_bet_object_idx.add_secondary_index(); persistent_bet_object_helper_index->set_plugin_instance(this); + const primary_index& betting_market_object_idx = database().get_index_type >(); + primary_index& nonconst_betting_market_object_idx = const_cast&>(betting_market_object_idx); + detail::persistent_betting_market_object_helper* persistent_betting_market_object_helper_index = nonconst_betting_market_object_idx.add_secondary_index(); + persistent_betting_market_object_helper_index->set_plugin_instance(this); + + const primary_index& betting_market_group_object_idx = database().get_index_type >(); + primary_index& nonconst_betting_market_group_object_idx = const_cast&>(betting_market_group_object_idx); + detail::persistent_betting_market_group_object_helper* persistent_betting_market_group_object_helper_index = nonconst_betting_market_group_object_idx.add_secondary_index(); + persistent_betting_market_group_object_helper_index->set_plugin_instance(this); + + const primary_index& event_object_idx = database().get_index_type >(); + primary_index& nonconst_event_object_idx = const_cast&>(event_object_idx); + detail::persistent_event_object_helper* persistent_event_object_helper_index = nonconst_event_object_idx.add_secondary_index(); + persistent_event_object_helper_index->set_plugin_instance(this); + ilog("bookie plugin: plugin_startup() end"); } diff --git a/libraries/wallet/include/graphene/wallet/wallet.hpp b/libraries/wallet/include/graphene/wallet/wallet.hpp index 2585b439..909127b3 100644 --- a/libraries/wallet/include/graphene/wallet/wallet.hpp +++ b/libraries/wallet/include/graphene/wallet/wallet.hpp @@ -1637,7 +1637,7 @@ class wallet_api fc::optional event_group_id, fc::optional name, fc::optional season, - fc::optional is_live_market, + fc::optional status, fc::optional start_time, bool broadcast = false); @@ -1671,7 +1671,7 @@ class wallet_api betting_market_group_id_type betting_market_group_id, fc::optional description, fc::optional rules_id, - fc::optional freeze, + fc::optional status, bool broadcast = false); signed_transaction propose_create_betting_market( diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index 9be0bfce..1497a996 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -5238,7 +5238,7 @@ signed_transaction wallet_api::propose_update_event( fc::optional event_group_id, fc::optional name, fc::optional season, - fc::optional is_live_market, + fc::optional status, fc::optional start_time, bool broadcast /*= false*/) { @@ -5251,7 +5251,7 @@ signed_transaction wallet_api::propose_update_event( event_update_op.new_start_time = start_time; event_update_op.new_name = name; event_update_op.new_season = season; - event_update_op.is_live_market = is_live_market; + event_update_op.new_status = status; proposal_create_operation prop_op; prop_op.expiration_time = expiration_time; @@ -5367,7 +5367,7 @@ signed_transaction wallet_api::propose_update_betting_market_group( betting_market_group_id_type betting_market_group_id, fc::optional description, fc::optional rules_id, - fc::optional freeze, + fc::optional status, bool broadcast /*= false*/) { FC_ASSERT( !is_locked() ); @@ -5377,7 +5377,7 @@ signed_transaction wallet_api::propose_update_betting_market_group( 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_rules_id = rules_id; - betting_market_group_update_op.freeze = freeze; + betting_market_group_update_op.status = status; proposal_create_operation prop_op; prop_op.expiration_time = expiration_time; diff --git a/tests/betting/betting_tests.cpp b/tests/betting/betting_tests.cpp index 2838a9c2..1a3fcadd 100644 --- a/tests/betting/betting_tests.cpp +++ b/tests/betting/betting_tests.cpp @@ -21,11 +21,19 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +// disable auto_ptr deprecated warning, see https://svn.boost.org/trac10/ticket/11622 +#include +#include +#pragma GCC diagnostic pop + +#include "../common/database_fixture.hpp" + #include #include #include -#include "../common/database_fixture.hpp" #include #include #include @@ -42,83 +50,169 @@ using namespace graphene::chain; using namespace graphene::chain::test; +using namespace graphene::chain::keywords; -#define CREATE_ICE_HOCKEY_BETTING_MARKET() \ - const sport_object& ice_hockey = create_sport({{"en", "Ice Hockey"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}); \ - const event_group_object& nhl = create_event_group({{"en", "NHL"}, {"zh_Hans", "國家冰球聯盟"}, {"ja", "ナショナルホッケーリーグ"}}, ice_hockey.id); \ - const event_object& capitals_vs_blackhawks = create_event({{"en", "Washington Capitals/Chicago Blackhawks"}, {"zh_Hans", "華盛頓首都隊/芝加哥黑鷹"}, {"ja", "ワシントン・キャピタルズ/シカゴ・ブラックホークス"}}, {{"en", "2016-17"}}, nhl.id); \ - const betting_market_rules_object& betting_market_rules = create_betting_market_rules({{"en", "NHL Rules v1.0"}}, {{"en", "The winner will be the team with the most points at the end of the game. The team with fewer points will not be the winner."}}); \ - const betting_market_group_object& moneyline_betting_markets = create_betting_market_group({{"en", "Moneyline"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type()); \ - const betting_market_object& capitals_win_market = create_betting_market(moneyline_betting_markets.id, {{"en", "Washington Capitals win"}}); \ - const betting_market_object& blackhawks_win_market = create_betting_market(moneyline_betting_markets.id, {{"en", "Chicago Blackhawks win"}}); +#define CREATE_ICE_HOCKEY_BETTING_MARKET(never_in_play, delay_before_settling) \ + create_sport({{"en", "Ice Hockey"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}); \ + generate_blocks(1); \ + const sport_object& ice_hockey = *db.get_index_type().indices().get().rbegin(); \ + create_event_group({{"en", "NHL"}, {"zh_Hans", "國家冰球聯盟"}, {"ja", "ナショナルホッケーリーグ"}}, ice_hockey.id); \ + generate_blocks(1); \ + const event_group_object& nhl = *db.get_index_type().indices().get().rbegin(); \ + create_event({{"en", "Washington Capitals/Chicago Blackhawks"}, {"zh_Hans", "華盛頓首都隊/芝加哥黑鷹"}, {"ja", "ワシントン・キャピタルズ/シカゴ・ブラックホークス"}}, {{"en", "2016-17"}}, nhl.id); \ + generate_blocks(1); \ + const event_object& capitals_vs_blackhawks = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market_rules({{"en", "NHL Rules v1.0"}}, {{"en", "The winner will be the team with the most points at the end of the game. The team with fewer points will not be the winner."}}); \ + generate_blocks(1); \ + const betting_market_rules_object& betting_market_rules = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market_group({{"en", "Moneyline"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling); \ + generate_blocks(1); \ + const betting_market_group_object& moneyline_betting_markets = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(moneyline_betting_markets.id, {{"en", "Washington Capitals win"}}); \ + generate_blocks(1); \ + const betting_market_object& capitals_win_market = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(moneyline_betting_markets.id, {{"en", "Chicago Blackhawks win"}}); \ + generate_blocks(1); \ + const betting_market_object& blackhawks_win_market = *db.get_index_type().indices().get().rbegin(); \ + (void)capitals_win_market; (void)blackhawks_win_market; + +// create the basic betting market, plus groups for the first, second, and third period results +#define CREATE_EXTENDED_ICE_HOCKEY_BETTING_MARKET(never_in_play, delay_before_settling) \ + CREATE_ICE_HOCKEY_BETTING_MARKET(never_in_play, delay_before_settling) \ + create_betting_market_group({{"en", "First Period Result"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling); \ + generate_blocks(1); \ + const betting_market_group_object& first_period_result_betting_markets = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(first_period_result_betting_markets.id, {{"en", "Washington Capitals win"}}); \ + generate_blocks(1); \ + const betting_market_object& first_period_capitals_win_market = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(first_period_result_betting_markets.id, {{"en", "Chicago Blackhawks win"}}); \ + generate_blocks(1); \ + const betting_market_object& first_period_blackhawks_win_market = *db.get_index_type().indices().get().rbegin(); \ + (void)first_period_capitals_win_market; (void)first_period_blackhawks_win_market; \ + \ + create_betting_market_group({{"en", "Second Period Result"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling); \ + generate_blocks(1); \ + const betting_market_group_object& second_period_result_betting_markets = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(second_period_result_betting_markets.id, {{"en", "Washington Capitals win"}}); \ + generate_blocks(1); \ + const betting_market_object& second_period_capitals_win_market = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(second_period_result_betting_markets.id, {{"en", "Chicago Blackhawks win"}}); \ + generate_blocks(1); \ + const betting_market_object& second_period_blackhawks_win_market = *db.get_index_type().indices().get().rbegin(); \ + (void)second_period_capitals_win_market; (void)second_period_blackhawks_win_market; \ + \ + create_betting_market_group({{"en", "Third Period Result"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type(), never_in_play, delay_before_settling); \ + generate_blocks(1); \ + const betting_market_group_object& third_period_result_betting_markets = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(third_period_result_betting_markets.id, {{"en", "Washington Capitals win"}}); \ + generate_blocks(1); \ + const betting_market_object& third_period_capitals_win_market = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(third_period_result_betting_markets.id, {{"en", "Chicago Blackhawks win"}}); \ + generate_blocks(1); \ + const betting_market_object& third_period_blackhawks_win_market = *db.get_index_type().indices().get().rbegin(); \ + (void)third_period_capitals_win_market; (void)third_period_blackhawks_win_market; + +#define CREATE_TENNIS_BETTING_MARKET() \ + create_betting_market_rules({{"en", "Tennis Rules v1.0"}}, {{"en", "The winner is the player who wins the last ball in the match."}}); \ + generate_blocks(1); \ + const betting_market_rules_object& tennis_rules = *db.get_index_type().indices().get().rbegin(); \ + create_sport({{"en", "Tennis"}}); \ + generate_blocks(1); \ + const sport_object& tennis = *db.get_index_type().indices().get().rbegin(); \ + create_event_group({{"en", "Wimbledon"}}, tennis.id); \ + generate_blocks(1); \ + const event_group_object& wimbledon = *db.get_index_type().indices().get().rbegin(); \ + create_event({{"en", "R. Federer/T. Berdych"}}, {{"en", "2017"}}, wimbledon.id); \ + generate_blocks(1); \ + const event_object& berdych_vs_federer = *db.get_index_type().indices().get().rbegin(); \ + create_event({{"en", "M. Cilic/S. Querrye"}}, {{"en", "2017"}}, wimbledon.id); \ + generate_blocks(1); \ + const event_object& cilic_vs_querrey = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market_group({{"en", "Moneyline 1st sf"}}, berdych_vs_federer.id, tennis_rules.id, asset_id_type(), false, 0); \ + generate_blocks(1); \ + const betting_market_group_object& moneyline_berdych_vs_federer = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market_group({{"en", "Moneyline 2nd sf"}}, cilic_vs_querrey.id, tennis_rules.id, asset_id_type(), false, 0); \ + generate_blocks(1); \ + const betting_market_group_object& moneyline_cilic_vs_querrey = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(moneyline_berdych_vs_federer.id, {{"en", "T. Berdych defeats R. Federer"}}); \ + generate_blocks(1); \ + const betting_market_object& berdych_wins_market = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(moneyline_berdych_vs_federer.id, {{"en", "R. Federer defeats T. Berdych"}}); \ + generate_blocks(1); \ + const betting_market_object& federer_wins_market = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(moneyline_cilic_vs_querrey.id, {{"en", "M. Cilic defeats S. Querrey"}}); \ + generate_blocks(1); \ + const betting_market_object& cilic_wins_market = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(moneyline_cilic_vs_querrey.id, {{"en", "S. Querrey defeats M. Cilic"}});\ + generate_blocks(1); \ + const betting_market_object& querrey_wins_market = *db.get_index_type().indices().get().rbegin(); \ + create_event({{"en", "R. Federer/M. Cilic"}}, {{"en", "2017"}}, wimbledon.id); \ + generate_blocks(1); \ + const event_object& cilic_vs_federer = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market_group({{"en", "Moneyline final"}}, cilic_vs_federer.id, tennis_rules.id, asset_id_type(), false, 0); \ + generate_blocks(1); \ + const betting_market_group_object& moneyline_cilic_vs_federer = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(moneyline_cilic_vs_federer.id, {{"en", "R. Federer defeats M. Cilic"}}); \ + generate_blocks(1); \ + const betting_market_object& federer_wins_final_market = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market(moneyline_cilic_vs_federer.id, {{"en", "M. Cilic defeats R. Federer"}}); \ + generate_blocks(1); \ + const betting_market_object& cilic_wins_final_market = *db.get_index_type().indices().get().rbegin(); \ + (void)federer_wins_market;(void)cilic_wins_market;(void)federer_wins_final_market; (void)cilic_wins_final_market; (void)berdych_wins_market; (void)querrey_wins_market; BOOST_FIXTURE_TEST_SUITE( betting_tests, database_fixture ) -#if 0 -BOOST_AUTO_TEST_CASE(generate_block) -{ - try - { - ACTORS( (alice)(bob) ); - - // failure if ACTORS - generate_blocks(10); - - } FC_LOG_AND_RETHROW() -} -#endif - BOOST_AUTO_TEST_CASE(try_create_sport) { try { - sport_create_operation sport_create_op; - sport_create_op.name = {{"en", "Ice Hockey"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}; + sport_create_operation sport_create_op; + sport_create_op.name = {{"en", "Ice Hockey"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}; - proposal_id_type proposal_id = propose_operation(sport_create_op); + proposal_id_type proposal_id = propose_operation(sport_create_op); - const auto& active_witnesses = db.get_global_properties().active_witnesses; - int voting_count = active_witnesses.size() / 2; + const auto& active_witnesses = db.get_global_properties().active_witnesses; + int voting_count = active_witnesses.size() / 2; - // 5 for - std::vector witnesses; - for (const witness_id_type& witness_id : active_witnesses) - { - witnesses.push_back(witness_id); - if (--voting_count == 0) - break; - } - process_proposal_by_witnesses(witnesses, proposal_id); + // 5 for + std::vector witnesses; + for (const witness_id_type& witness_id : active_witnesses) + { + witnesses.push_back(witness_id); + if (--voting_count == 0) + break; + } + process_proposal_by_witnesses(witnesses, proposal_id); - // 1st out - witnesses.clear(); - auto itr = active_witnesses.begin(); - witnesses.push_back(*itr); - process_proposal_by_witnesses(witnesses, proposal_id, true); + // 1st out + witnesses.clear(); + auto itr = active_witnesses.begin(); + witnesses.push_back(*itr); + process_proposal_by_witnesses(witnesses, proposal_id, true); - const auto& sport_index = db.get_index_type().indices().get(); - // not yet approved - BOOST_REQUIRE(sport_index.rbegin() == sport_index.rend()); + const auto& sport_index = db.get_index_type().indices().get(); + // not yet approved + BOOST_REQUIRE(sport_index.rbegin() == sport_index.rend()); - // 6th for - witnesses.clear(); - itr += 5; - witnesses.push_back(*itr); - process_proposal_by_witnesses(witnesses, proposal_id); + // 6th for + witnesses.clear(); + itr += 5; + witnesses.push_back(*itr); + process_proposal_by_witnesses(witnesses, proposal_id); - // not yet approved - BOOST_REQUIRE(sport_index.rbegin() == sport_index.rend()); + // not yet approved + BOOST_REQUIRE(sport_index.rbegin() == sport_index.rend()); - // 7th for - witnesses.clear(); - ++itr; - witnesses.push_back(*itr); - process_proposal_by_witnesses(witnesses, proposal_id); + // 7th for + witnesses.clear(); + ++itr; + witnesses.push_back(*itr); + process_proposal_by_witnesses(witnesses, proposal_id); - // done - BOOST_REQUIRE(sport_index.rbegin() != sport_index.rend()); - sport_id_type sport_id = (*sport_index.rbegin()).id; - BOOST_REQUIRE(sport_id == sport_id_type()); + // done + BOOST_REQUIRE(sport_index.rbegin() != sport_index.rend()); + sport_id_type sport_id = (*sport_index.rbegin()).id; + BOOST_REQUIRE(sport_id == sport_id_type()); } FC_LOG_AND_RETHROW() } @@ -128,7 +222,7 @@ BOOST_AUTO_TEST_CASE(simple_bet_win) try { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); // give alice and bob 10k each transfer(account_id_type(), alice_id, asset(10000)); @@ -141,7 +235,6 @@ BOOST_AUTO_TEST_CASE(simple_bet_win) // reverse positions at 1:1 place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(1100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(1100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); - } FC_LOG_AND_RETHROW() } @@ -150,7 +243,7 @@ BOOST_AUTO_TEST_CASE(binned_order_books) try { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); graphene::bookie::bookie_api bookie_api(app); @@ -180,8 +273,8 @@ BOOST_AUTO_TEST_CASE(binned_order_books) // the binned orders returned should be chosen so that we if we assume those orders are real and we place // matching lay orders, we will completely consume the underlying orders and leave no orders on the books - BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_back_bets.size(), 2); - BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_lay_bets.size(), 0); + BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_back_bets.size(), 2u); + BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_lay_bets.size(), 0u); for (const graphene::bookie::order_bin& binned_order : binned_orders_point_one.aggregated_back_bets) { // compute the matching lay order @@ -212,8 +305,8 @@ BOOST_AUTO_TEST_CASE(binned_order_books) // the binned orders returned should be chosen so that we if we assume those orders are real and we place // matching lay orders, we will completely consume the underlying orders and leave no orders on the books - BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_back_bets.size(), 0); - BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_lay_bets.size(), 2); + BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_back_bets.size(), 0u); + BOOST_CHECK_EQUAL(binned_orders_point_one.aggregated_lay_bets.size(), 2u); for (const graphene::bookie::order_bin& binned_order : binned_orders_point_one.aggregated_lay_bets) { // compute the matching lay order @@ -240,7 +333,7 @@ BOOST_AUTO_TEST_CASE( peerplays_sport_create_test ) try { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); // give alice and bob 10M each transfer(account_id_type(), alice_id, asset(10000000)); @@ -254,11 +347,14 @@ BOOST_AUTO_TEST_CASE( peerplays_sport_create_test ) BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + // caps win resolve_betting_market_group(moneyline_betting_markets.id, {{capitals_win_market.id, betting_market_resolution_type::win}, - {blackhawks_win_market.id, betting_market_resolution_type::cancel}}); + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; @@ -274,7 +370,7 @@ BOOST_AUTO_TEST_CASE( cancel_unmatched_in_betting_group_test ) try { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); // give alice and bob 10M each transfer(account_id_type(), alice_id, asset(10000000)); @@ -302,234 +398,227 @@ BOOST_AUTO_TEST_CASE( cancel_unmatched_in_betting_group_test ) BOOST_AUTO_TEST_CASE(inexact_odds) { - try - { - ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(); + try + { + generate_blocks(1); + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); - transfer(account_id_type(), alice_id, asset(10000000)); - share_type alice_expected_balance = 10000000; - BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + transfer(account_id_type(), alice_id, asset(10000000)); + share_type alice_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); - // lay 46 at 1.94 odds (50:47) -- this is too small to be placed on the books and there's - // nothing for it to match, so it should be canceled - place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(46, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); - BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + // lay 46 at 1.94 odds (50:47) -- this is too small to be placed on the books and there's + // nothing for it to match, so it should be canceled + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(46, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); - // lay 47 at 1.94 odds (50:47) -- this is an exact amount, nothing surprising should happen here - place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(47, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); - alice_expected_balance -= 47; - BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + // lay 47 at 1.94 odds (50:47) -- this is an exact amount, nothing surprising should happen here + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(47, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + alice_expected_balance -= 47; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); - // lay 100 at 1.91 odds (100:91) -- this is an inexact match, we should get refunded 9 and leave a bet for 91 on the books - place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 191 * GRAPHENE_BETTING_ODDS_PRECISION / 100); - alice_expected_balance -= 91; - BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); + // lay 100 at 1.91 odds (100:91) -- this is an inexact match, we should get refunded 9 and leave a bet for 91 on the books + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(100, asset_id_type()), 191 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + alice_expected_balance -= 91; + BOOST_REQUIRE_EQUAL(get_balance(alice_id, asset_id_type()), alice_expected_balance.value); - transfer(account_id_type(), bob_id, asset(10000000)); - share_type bob_expected_balance = 10000000; - BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + transfer(account_id_type(), bob_id, asset(10000000)); + share_type bob_expected_balance = 10000000; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); - // now have bob match it with a back of 300 at 1.91 - // This should: - // match the full 47 @ 1.94 with 50 - // match the full 91 @ 1.91 with 100 - // leaving 150 - // back bets at 100:91 must be a multiple of 100, so refund 50 - // leaves a back bet of 100 @ 1.91 on the books - place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(300, asset_id_type()), 191 * GRAPHENE_BETTING_ODDS_PRECISION / 100); - bob_expected_balance -= 250; - BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); - } - FC_LOG_AND_RETHROW() + // now have bob match it with a back of 300 at 1.91 + // This should: + // match the full 47 @ 1.94 with 50 + // match the full 91 @ 1.91 with 100 + // leaving 150 + // back bets at 100:91 must be a multiple of 100, so refund 50 + // leaves a back bet of 100 @ 1.91 on the books + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(300, asset_id_type()), 191 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + bob_expected_balance -= 250; + BOOST_REQUIRE_EQUAL(get_balance(bob_id, asset_id_type()), bob_expected_balance.value); + } + FC_LOG_AND_RETHROW() } BOOST_AUTO_TEST_CASE(persistent_objects_test) { - try - { - 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 + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); - CREATE_ICE_HOCKEY_BETTING_MARKET(); - } - graphene::bookie::bookie_api bookie_api(app); + graphene::bookie::bookie_api bookie_api(app); - 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); + 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); - 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(); - betting_market_group_id_type caps_vs_blackhawks_moneyline_betting_market_id = caps_vs_blackhawks_moneyline_betting_market.id; - 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(); - betting_market_id_type capitals_win_market_id = capitals_win_market.id; - const betting_market_object& blackhawks_win_market = *std::next(db.get_index_type().indices().get().begin()); - betting_market_id_type blackhawks_win_market_id = blackhawks_win_market.id; + idump((capitals_win_market.get_status())); - // lay 46 at 1.94 odds (50:47) -- this is too small to be placed on the books and there's - // nothing for it to match, so it should be canceled - bet_id_type automatically_canceled_bet_id = place_bet(alice_id, capitals_win_market_id, bet_type::lay, asset(46, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); - generate_blocks(1); - BOOST_CHECK_MESSAGE(!db.find(automatically_canceled_bet_id), "Bet should have been canceled, but the blockchain still knows about it"); - fc::variants objects_from_bookie = bookie_api.get_objects({automatically_canceled_bet_id}); - idump((objects_from_bookie)); - BOOST_REQUIRE_EQUAL(objects_from_bookie.size(), 1); - BOOST_CHECK_MESSAGE(objects_from_bookie[0]["id"].as() == automatically_canceled_bet_id, "Bookie Plugin didn't return a deleted bet it"); + // lay 46 at 1.94 odds (50:47) -- this is too small to be placed on the books and there's + // nothing for it to match, so it should be canceled + bet_id_type automatically_canceled_bet_id = place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(46, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + generate_blocks(1); + BOOST_CHECK_MESSAGE(!db.find(automatically_canceled_bet_id), "Bet should have been canceled, but the blockchain still knows about it"); + fc::variants objects_from_bookie = bookie_api.get_objects({automatically_canceled_bet_id}); + idump((objects_from_bookie)); + BOOST_REQUIRE_EQUAL(objects_from_bookie.size(), 1u); + BOOST_CHECK_MESSAGE(objects_from_bookie[0]["id"].as() == automatically_canceled_bet_id, "Bookie Plugin didn't return a deleted bet it"); - // lay 47 at 1.94 odds (50:47) -- this bet should go on the order books normally - bet_id_type first_bet_on_books = place_bet(alice_id, capitals_win_market_id, bet_type::lay, asset(47, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); - generate_blocks(1); - BOOST_CHECK_MESSAGE(db.find(first_bet_on_books), "Bet should exist on the blockchain"); - objects_from_bookie = bookie_api.get_objects({first_bet_on_books}); - idump((objects_from_bookie)); - BOOST_REQUIRE_EQUAL(objects_from_bookie.size(), 1); - BOOST_CHECK_MESSAGE(objects_from_bookie[0]["id"].as() == first_bet_on_books, "Bookie Plugin didn't return a bet that is currently on the books"); + // lay 47 at 1.94 odds (50:47) -- this bet should go on the order books normally + bet_id_type first_bet_on_books = place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(47, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + generate_blocks(1); + BOOST_CHECK_MESSAGE(db.find(first_bet_on_books), "Bet should exist on the blockchain"); + objects_from_bookie = bookie_api.get_objects({first_bet_on_books}); + idump((objects_from_bookie)); + BOOST_REQUIRE_EQUAL(objects_from_bookie.size(), 1u); + BOOST_CHECK_MESSAGE(objects_from_bookie[0]["id"].as() == first_bet_on_books, "Bookie Plugin didn't return a bet that is currently on the books"); - // place a bet that exactly matches 'first_bet_on_books', should result in empty books (thus, no bet_objects from the blockchain) - bet_id_type matching_bet = place_bet(bob_id, capitals_win_market_id, bet_type::back, asset(50, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); - BOOST_CHECK_MESSAGE(!db.find(first_bet_on_books), "Bet should have been filled, but the blockchain still knows about it"); - BOOST_CHECK_MESSAGE(!db.find(matching_bet), "Bet should have been filled, but the blockchain still knows about it"); - generate_blocks(1); // the bookie plugin doesn't detect matches until a block is generated + // place a bet that exactly matches 'first_bet_on_books', should result in empty books (thus, no bet_objects from the blockchain) + bet_id_type matching_bet = place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(50, asset_id_type()), 194 * GRAPHENE_BETTING_ODDS_PRECISION / 100); + BOOST_CHECK_MESSAGE(!db.find(first_bet_on_books), "Bet should have been filled, but the blockchain still knows about it"); + BOOST_CHECK_MESSAGE(!db.find(matching_bet), "Bet should have been filled, but the blockchain still knows about it"); + generate_blocks(1); // the bookie plugin doesn't detect matches until a block is generated - objects_from_bookie = bookie_api.get_objects({first_bet_on_books, matching_bet}); - idump((objects_from_bookie)); - BOOST_REQUIRE_EQUAL(objects_from_bookie.size(), 2); - BOOST_CHECK_MESSAGE(objects_from_bookie[0]["id"].as() == first_bet_on_books, "Bookie Plugin didn't return a bet that has been filled"); - BOOST_CHECK_MESSAGE(objects_from_bookie[1]["id"].as() == matching_bet, "Bookie Plugin didn't return a bet that has been filled"); + objects_from_bookie = bookie_api.get_objects({first_bet_on_books, matching_bet}); + idump((objects_from_bookie)); + BOOST_REQUIRE_EQUAL(objects_from_bookie.size(), 2u); + BOOST_CHECK_MESSAGE(objects_from_bookie[0]["id"].as() == first_bet_on_books, "Bookie Plugin didn't return a bet that has been filled"); + BOOST_CHECK_MESSAGE(objects_from_bookie[1]["id"].as() == matching_bet, "Bookie Plugin didn't return a bet that has been filled"); - resolve_betting_market_group(caps_vs_blackhawks_moneyline_betting_market_id, - {{capitals_win_market_id, betting_market_resolution_type::cancel}, - {blackhawks_win_market_id, betting_market_resolution_type::cancel}}); - generate_blocks(1); + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); - // test get_matched_bets_for_bettor - std::vector alice_matched_bets = bookie_api.get_matched_bets_for_bettor(alice_id); - for (const graphene::bookie::matched_bet_object& matched_bet : alice_matched_bets) - { - idump((matched_bet)); - for (operation_history_id_type id : matched_bet.associated_operations) - idump((id(db))); - } - BOOST_REQUIRE_EQUAL(alice_matched_bets.size(), 1); - BOOST_CHECK(alice_matched_bets[0].amount_matched == 47); - std::vector bob_matched_bets = bookie_api.get_matched_bets_for_bettor(bob_id); - for (const graphene::bookie::matched_bet_object& matched_bet : bob_matched_bets) - { - idump((matched_bet)); - for (operation_history_id_type id : matched_bet.associated_operations) - idump((id(db))); - } - BOOST_REQUIRE_EQUAL(bob_matched_bets.size(), 1); - BOOST_CHECK(bob_matched_bets[0].amount_matched == 50); + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::cancel}, + {blackhawks_win_market.id, betting_market_resolution_type::cancel}}); - // test getting markets - // test that we cannot get them from the database directly - BOOST_CHECK_THROW(capitals_win_market_id(db), fc::exception); - BOOST_CHECK_THROW(blackhawks_win_market_id(db), fc::exception); + // as soon as the market is resolved during the generate_block(), these markets + // should be deleted and our references will go out of scope. Save the + // market ids here so we can verify that they were really deleted + betting_market_id_type capitals_win_market_id = capitals_win_market.id; + betting_market_id_type blackhawks_win_market_id = blackhawks_win_market.id; - objects_from_bookie = bookie_api.get_objects({capitals_win_market_id, blackhawks_win_market_id}); - BOOST_REQUIRE_EQUAL(objects_from_bookie.size(), 2); - idump((objects_from_bookie)); - BOOST_CHECK(!objects_from_bookie[0].is_null()); - BOOST_CHECK(!objects_from_bookie[1].is_null()); - } - FC_LOG_AND_RETHROW() + generate_blocks(1); + + // test get_matched_bets_for_bettor + std::vector alice_matched_bets = bookie_api.get_matched_bets_for_bettor(alice_id); + for (const graphene::bookie::matched_bet_object& matched_bet : alice_matched_bets) + { + idump((matched_bet)); + for (operation_history_id_type id : matched_bet.associated_operations) + idump((id(db))); + } + BOOST_REQUIRE_EQUAL(alice_matched_bets.size(), 1u); + BOOST_CHECK(alice_matched_bets[0].amount_matched == 47); + std::vector bob_matched_bets = bookie_api.get_matched_bets_for_bettor(bob_id); + for (const graphene::bookie::matched_bet_object& matched_bet : bob_matched_bets) + { + idump((matched_bet)); + for (operation_history_id_type id : matched_bet.associated_operations) + idump((id(db))); + } + BOOST_REQUIRE_EQUAL(bob_matched_bets.size(), 1u); + BOOST_CHECK(bob_matched_bets[0].amount_matched == 50); + + // test getting markets + // test that we cannot get them from the database directly + BOOST_CHECK_THROW(capitals_win_market_id(db), fc::exception); + BOOST_CHECK_THROW(blackhawks_win_market_id(db), fc::exception); + + objects_from_bookie = bookie_api.get_objects({capitals_win_market_id, blackhawks_win_market_id}); + BOOST_REQUIRE_EQUAL(objects_from_bookie.size(), 2u); + idump((objects_from_bookie)); + BOOST_CHECK(!objects_from_bookie[0].is_null()); + BOOST_CHECK(!objects_from_bookie[1].is_null()); + } + 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() + try + { + const auto& bet_odds_idx = db.get_index_type().indices().get(); + + ACTORS( (alice)(bob) ); + + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + generate_blocks(1); + + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::in_play); + 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); + + for (const auto& bet : boost::make_iterator_range(first_bet_in_market, last_bet_in_market)) + edump((bet)); + // 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); + + edump((db.get_global_properties().parameters.block_interval)(db.head_block_time())); + // the bet should not enter the order books before a block has been generated + 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)); + for (const auto& bet : bet_odds_idx) + edump((bet)); + 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)); + edump((std::distance(first_bet_in_market, last_bet_in_market))); + BOOST_CHECK(std::distance(first_bet_in_market, last_bet_in_market) == 1); + for (const auto& bet : boost::make_iterator_range(first_bet_in_market, last_bet_in_market)) + edump((bet)); + + // 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(moneyline_betting_markets.id); + BOOST_CHECK(bet_odds_idx.empty()); + } + FC_LOG_AND_RETHROW() } BOOST_AUTO_TEST_CASE( chained_market_create_test ) @@ -607,7 +696,7 @@ BOOST_AUTO_TEST_CASE( chained_market_create_test ) db.push_transaction(tx); } - BOOST_REQUIRE_EQUAL(db.get_index_type().indices().size(), 1); + BOOST_REQUIRE_EQUAL(db.get_index_type().indices().size(), 1u); { const proposal_object& prop = *db.get_index_type().indices().begin(); @@ -655,7 +744,7 @@ struct simple_bet_test_fixture : database_fixture { simple_bet_test_fixture() { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); // give alice and bob 10k each transfer(account_id_type(), alice_id, asset(10000)); @@ -672,6 +761,10 @@ struct simple_bet_test_fixture : database_fixture { capitals_win_betting_market_id = capitals_win_market.id; blackhawks_win_betting_market_id = blackhawks_win_market.id; moneyline_betting_markets_id = moneyline_betting_markets.id; + + // close betting to prepare for the next operation which will be grading or cancel + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + generate_blocks(1); } }; @@ -683,7 +776,8 @@ BOOST_AUTO_TEST_CASE( win ) { resolve_betting_market_group(moneyline_betting_markets_id, {{capitals_win_betting_market_id, betting_market_resolution_type::win}, - {blackhawks_win_betting_market_id, betting_market_resolution_type::cancel}}); + {blackhawks_win_betting_market_id, betting_market_resolution_type::not_win}}); + generate_blocks(1); GET_ACTOR(alice); GET_ACTOR(bob); @@ -695,6 +789,7 @@ BOOST_AUTO_TEST_CASE( win ) BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000 - 100 + 1100 - 1100 + 0); rake_value = (-1000 - 1100 + 2200) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + edump((rake_fee_percentage)(rake_value)); // bob starts with 10000, pays 1000 (bet), wins 0, then pays 1100 (bet), wins 2200 BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000 - 1000 + 0 - 1100 + 2200 - rake_value); @@ -707,7 +802,8 @@ BOOST_AUTO_TEST_CASE( not_win ) { resolve_betting_market_group(moneyline_betting_markets_id, {{capitals_win_betting_market_id, betting_market_resolution_type::not_win}, - {blackhawks_win_betting_market_id, betting_market_resolution_type::cancel}}); + {blackhawks_win_betting_market_id, betting_market_resolution_type::win}}); + generate_blocks(1); GET_ACTOR(alice); GET_ACTOR(bob); @@ -731,6 +827,7 @@ BOOST_AUTO_TEST_CASE( cancel ) resolve_betting_market_group(moneyline_betting_markets_id, {{capitals_win_betting_market_id, betting_market_resolution_type::cancel}, {blackhawks_win_betting_market_id, betting_market_resolution_type::cancel}}); + generate_blocks(1); GET_ACTOR(alice); GET_ACTOR(bob); @@ -748,7 +845,7 @@ struct simple_bet_test_fixture_2 : database_fixture { simple_bet_test_fixture_2() { ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); // give alice and bob 10k each transfer(account_id_type(), alice_id, asset(10000)); @@ -782,7 +879,7 @@ BOOST_AUTO_TEST_CASE(sport_update_test) try { ACTORS( (alice) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); update_sport(ice_hockey.id, {{"en", "Hockey on Ice"}, {"zh_Hans", "冰"}, {"ja", "アイスホッケ"}}); transfer(account_id_type(), alice_id, asset(10000000)); @@ -795,92 +892,93 @@ BOOST_AUTO_TEST_CASE(sport_update_test) BOOST_AUTO_TEST_CASE(event_group_update_test) { - try - { - ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(); + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + transfer(account_id_type(), alice_id, asset(10000000)); + transfer(account_id_type(), bob_id, asset(10000000)); + + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + const sport_object& ice_on_hockey = create_sport({{"en", "Hockey on Ice"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}); \ + fc::optional sport_id = ice_on_hockey.id; + + fc::optional name = internationalized_string_type({{"en", "IBM"}, {"zh_Hans", "國家冰球聯"}, {"ja", "ナショナルホッケーリー"}}); + + update_event_group(nhl.id, fc::optional(), name); + update_event_group(nhl.id, sport_id, fc::optional()); + update_event_group(nhl.id, sport_id, name); + + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); - transfer(account_id_type(), alice_id, asset(10000000)); - transfer(account_id_type(), bob_id, asset(10000000)); - - place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); - - const sport_object& ice_on_hockey = create_sport({{"en", "Hockey on Ice"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}); \ - fc::optional sport_id = ice_on_hockey.id; - - fc::optional name = internationalized_string_type({{"en", "IBM"}, {"zh_Hans", "國家冰球聯"}, {"ja", "ナショナルホッケーリー"}}); - - update_event_group(nhl.id, fc::optional(), name); - update_event_group(nhl.id, sport_id, fc::optional()); - update_event_group(nhl.id, sport_id, name); - - place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); - - BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); - BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); - - // caps win - resolve_betting_market_group(moneyline_betting_markets.id, - {{capitals_win_market.id, betting_market_resolution_type::win}, - {blackhawks_win_market.id, betting_market_resolution_type::cancel}}); - - - uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; - uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; - BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); - BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 + 2000000 - rake_value); - BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); - - } FC_LOG_AND_RETHROW() + // caps win + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 + 2000000 - rake_value); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + + } FC_LOG_AND_RETHROW() } BOOST_AUTO_TEST_CASE(event_update_test) { - try - { - ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(); + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); - transfer(account_id_type(), alice_id, asset(10000000)); - transfer(account_id_type(), bob_id, asset(10000000)); + transfer(account_id_type(), alice_id, asset(10000000)); + transfer(account_id_type(), bob_id, asset(10000000)); - place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); - fc::optional empty; - fc::optional name = internationalized_string_type({{"en", "Washington Capitals vs. Chicago Blackhawks"}, {"zh_Hans", "華盛頓首都隊/芝加哥黑"}, {"ja", "ワシントン・キャピタルズ/シカゴ・ブラックホーク"}}); - fc::optional season = internationalized_string_type({{"en", "2017-18"}}); - fc::optional empty_object_id; - fc::optional empty_bool; + fc::optional name = internationalized_string_type({{"en", "Washington Capitals vs. Chicago Blackhawks"}, {"zh_Hans", "華盛頓首都隊/芝加哥黑"}, {"ja", "ワシントン・キャピタルズ/シカゴ・ブラックホーク"}}); + fc::optional season = internationalized_string_type({{"en", "2017-18"}}); - update_event(capitals_vs_blackhawks.id, empty_object_id, name, empty, empty_bool); - update_event(capitals_vs_blackhawks.id, empty_object_id, empty, season, empty_bool); - update_event(capitals_vs_blackhawks.id, empty_object_id, name, season, empty_bool); + update_event(capitals_vs_blackhawks.id, _name = name); + update_event(capitals_vs_blackhawks.id, _season = season); + update_event(capitals_vs_blackhawks.id, _name = name, _season = season); - const sport_object& ice_on_hockey = create_sport({{"en", "Hockey on Ice"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}); \ - const event_group_object& nhl2 = create_event_group({{"en", "NHL2"}, {"zh_Hans", "國家冰球聯盟"}, {"ja", "ナショナルホッケーリーグ"}}, ice_on_hockey.id); \ - fc::optional event_group_id = nhl2.id; + const sport_object& ice_on_hockey = create_sport({{"en", "Hockey on Ice"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}); + const event_group_object& nhl2 = create_event_group({{"en", "NHL2"}, {"zh_Hans", "國家冰球聯盟"}, {"ja", "ナショナルホッケーリーグ"}}, ice_on_hockey.id); - update_event(capitals_vs_blackhawks.id, event_group_id , empty, empty, empty_bool); + update_event(capitals_vs_blackhawks.id, _event_group_id = nhl2.id); - place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); - BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); - BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); - // caps win - resolve_betting_market_group(moneyline_betting_markets.id, - {{capitals_win_market.id, betting_market_resolution_type::win}, - {blackhawks_win_market.id, betting_market_resolution_type::cancel}}); + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + + // caps win + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); - uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; - uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; - BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); - BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 + 2000000 - rake_value); - BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); - - } FC_LOG_AND_RETHROW() + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 + 2000000 - rake_value); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + } FC_LOG_AND_RETHROW() } BOOST_AUTO_TEST_CASE(betting_market_rules_update_test) @@ -888,7 +986,7 @@ BOOST_AUTO_TEST_CASE(betting_market_rules_update_test) try { ACTORS( (alice) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); fc::optional empty; fc::optional name = internationalized_string_type({{"en", "NHL Rules v1.1"}}); @@ -908,91 +1006,754 @@ BOOST_AUTO_TEST_CASE(betting_market_rules_update_test) BOOST_AUTO_TEST_CASE(betting_market_group_update_test) { - try - { - ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(); + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); - transfer(account_id_type(), alice_id, asset(10000000)); - place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + transfer(account_id_type(), alice_id, asset(10000000)); + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); - fc::optional dempty; - fc::optional empty_object_id; + internationalized_string_type new_description = internationalized_string_type({{"en", "Money line"}}); - fc::optional new_desc = internationalized_string_type({{"en", "Money line"}}); + const betting_market_rules_object& new_betting_market_rules = create_betting_market_rules({{"en", "NHL Rules v2.0"}}, {{"en", "The winner will be the team with the most points at the end of the game. The team with fewer points will not be the winner."}}); + fc::optional new_rule = new_betting_market_rules.id; - const event_object& odd_vs_even = create_event({{"en", "Capitals vs. Blackhawks"}}, {{"en", "2017-18"}}, nhl.id); \ - fc::optional new_event = odd_vs_even.id; + update_betting_market_group(moneyline_betting_markets.id, _description = new_description); + update_betting_market_group(moneyline_betting_markets.id, _rules_id = new_betting_market_rules.id); + update_betting_market_group(moneyline_betting_markets.id, _description = new_description, _rules_id = new_betting_market_rules.id); - const betting_market_rules_object& new_betting_market_rules = create_betting_market_rules({{"en", "NHL Rules v2.0"}}, {{"en", "The winner will be the team with the most points at the end of the game. The team with fewer points will not be the winner."}}); - fc::optional new_rule = new_betting_market_rules.id; - fc::optional 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); - 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); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); - 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); - - BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); - BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); - - // caps win - resolve_betting_market_group(moneyline_betting_markets.id, - {{capitals_win_market.id, betting_market_resolution_type::win}, - {blackhawks_win_market.id, betting_market_resolution_type::cancel}}); + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + // caps win + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); - uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; - uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; - BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); - BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 + 2000000 - rake_value); - BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 + 2000000 - rake_value); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); } FC_LOG_AND_RETHROW() } BOOST_AUTO_TEST_CASE(betting_market_update_test) { - try - { - ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(); + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); - transfer(account_id_type(), alice_id, asset(10000000)); - place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + transfer(account_id_type(), alice_id, asset(10000000)); + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); - const betting_market_group_object& new_moneyline_betting_markets = create_betting_market_group({{"en", "New Moneyline"}}, capitals_vs_blackhawks.id, betting_market_rules.id, asset_id_type()); \ - fc::optional betting_market_group = new_moneyline_betting_markets.id; + fc::optional payout_condition = internationalized_string_type({{"en", "Washington Capitals lose"}}); - fc::optional payout_condition = internationalized_string_type({{"en", "Washington Capitals lose"}}); + // update the payout condition + update_betting_market(capitals_win_market.id, fc::optional(), payout_condition); - update_betting_market(capitals_win_market.id, betting_market_group, fc::optional()); - update_betting_market(capitals_win_market.id, fc::optional(), payout_condition); - update_betting_market(capitals_win_market.id, betting_market_group, payout_condition); + 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); - update_betting_market(blackhawks_win_market.id, betting_market_group, fc::optional()); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); - 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); + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + // caps win + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); - BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); - BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 + 2000000 - rake_value); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); - // caps win - resolve_betting_market_group(new_moneyline_betting_markets.id, - {{capitals_win_market.id, betting_market_resolution_type::win}, - {blackhawks_win_market.id, betting_market_resolution_type::cancel}}); + } FC_LOG_AND_RETHROW() +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_FIXTURE_TEST_SUITE( event_status_tests, database_fixture ) + +// This tests a normal progression by setting the event state and +// letting it trickle down: +// - upcoming +// - finished +// - settled +BOOST_AUTO_TEST_CASE(event_driven_standard_progression_1) +{ + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + graphene::bookie::bookie_api bookie_api(app); + // save the event id for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event to finished"); + update_event(capitals_vs_blackhawks.id, _status = event_status::finished); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("grading betting market"); + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + // as soon as a block is generated, the betting market group will settle, and the market + // and group will cease to exist. The event should transition to "settled", then + // removed. + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id}); + + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "settled"); + } FC_LOG_AND_RETHROW() +} + +// This tests a normal progression by setting the event state and +// letting it trickle down. Like the above, with delayed settling: +// - upcoming +// - finished +// - settled +BOOST_AUTO_TEST_CASE(event_driven_standard_progression_1_with_delay) +{ + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 60 /* seconds */); + graphene::bookie::bookie_api bookie_api(app); + // save the event id for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event to finished"); + update_event(capitals_vs_blackhawks.id, _status = event_status::finished); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("grading betting market"); + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + // it should be waiting 60 seconds before it settles + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::graded); + + generate_blocks(60); + // as soon as a block is generated, the betting market group will settle, and the market + // and group will cease to exist. The event should transition to "settled", then + // removed. + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id}); + + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "settled"); + } FC_LOG_AND_RETHROW() +} + +// This tests a normal progression by setting the event state and +// letting it trickle down: +// - upcoming +// - frozen +// - upcoming +// - in_progress +// - frozen +// - in_progress +// - finished +// - settled +BOOST_AUTO_TEST_CASE(event_driven_standard_progression_2) +{ + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + graphene::bookie::bookie_api bookie_api(app); + // save the event id for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event frozen"); + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::frozen); + + BOOST_TEST_MESSAGE("setting the event back to upcoming"); + update_event(capitals_vs_blackhawks.id, _status = event_status::upcoming); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event in-progress"); + update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::in_progress); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + + BOOST_TEST_MESSAGE("setting the event frozen"); + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::frozen); + + BOOST_TEST_MESSAGE("setting the event back in-progress"); + update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::in_progress); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event to finished"); + update_event(capitals_vs_blackhawks.id, _status = event_status::finished); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("grading betting market"); + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + // as soon as a block is generated, the betting market group will settle, and the market + // and group will cease to exist. The event should transition to "settled", then + // removed. + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id}); + + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "settled"); + } FC_LOG_AND_RETHROW() +} + +// This tests a normal progression by setting the event state and +// letting it trickle down. This is the same as the above test, but the +// never-in-play flag is set: +// - upcoming +// - frozen +// - upcoming +// - in_progress +// - frozen +// - in_progress +// - finished +// - settled +BOOST_AUTO_TEST_CASE(event_driven_standard_progression_2_never_in_play) +{ + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(true, 0); + + graphene::bookie::bookie_api bookie_api(app); + // save the event id for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event frozen"); + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::frozen); + + BOOST_TEST_MESSAGE("setting the event back to upcoming"); + update_event(capitals_vs_blackhawks.id, _status = event_status::upcoming); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event in-progress"); + update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::in_progress); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + + BOOST_TEST_MESSAGE("setting the event frozen"); + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::frozen); + + BOOST_TEST_MESSAGE("setting the event back in-progress"); + update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::in_progress); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event to finished"); + update_event(capitals_vs_blackhawks.id, _status = event_status::finished); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("grading betting market"); + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + // as soon as a block is generated, the betting market group will settle, and the market + // and group will cease to exist. The event should transition to "settled", then + // removed. + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id}); + + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "settled"); + } FC_LOG_AND_RETHROW() +} + +// This tests a slightly different normal progression by setting the event state and +// letting it trickle down: +// - upcoming +// - frozen +// - in_progress +// - frozen +// - in_progress +// - finished +// - canceled +BOOST_AUTO_TEST_CASE(event_driven_standard_progression_3) +{ + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + graphene::bookie::bookie_api bookie_api(app); + // save the event id for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event frozen"); + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::frozen); + + BOOST_TEST_MESSAGE("setting the event in progress"); + update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::in_progress); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + + BOOST_TEST_MESSAGE("setting the event frozen"); + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::frozen); + + BOOST_TEST_MESSAGE("setting the event back in-progress"); + update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::in_progress); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event to finished"); + update_event(capitals_vs_blackhawks.id, _status = event_status::finished); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event to canceled"); + update_event(capitals_vs_blackhawks.id, _status = event_status::canceled); + generate_blocks(1); + + // as soon as a block is generated, the betting market group will cancel, and the market + // and group will cease to exist. The event should transition to "canceled", then be removed + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id}); + + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "canceled"); + + } FC_LOG_AND_RETHROW() +} + +// This tests that we reject illegal transitions +// - anything -> settled +// - in_progress -> upcoming +// - frozen after in_progress -> upcoming +// - finished -> upcoming, in_progress, frozen +// - canceled -> anything +BOOST_AUTO_TEST_CASE(event_driven_progression_errors_1) +{ + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + graphene::bookie::bookie_api bookie_api(app); + // save the event id for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_REQUIRE(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_REQUIRE(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_REQUIRE(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_REQUIRE(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + // settled is the only illegal transition from upcoming + BOOST_TEST_MESSAGE("verifying we can't jump to settled"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::settled, _force = true), fc::exception); + + BOOST_TEST_MESSAGE("setting the event frozen"); + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_REQUIRE(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_REQUIRE(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_REQUIRE(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_REQUIRE(blackhawks_win_market.get_status() == betting_market_status::frozen); + + // settled is the only illegal transition from this frozen event + BOOST_TEST_MESSAGE("verifying we can't jump to settled"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::settled, _force = true), fc::exception); + + BOOST_TEST_MESSAGE("setting the event in progress"); + update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress); + generate_blocks(1); + BOOST_REQUIRE(capitals_vs_blackhawks.get_status() == event_status::in_progress); + BOOST_REQUIRE(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + + // we can't go back to upcoming from in_progress. + // settled is disallowed everywhere + BOOST_TEST_MESSAGE("verifying we can't jump to upcoming"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::upcoming, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to settled"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::settled, _force = true), fc::exception); + + BOOST_TEST_MESSAGE("setting the event frozen"); + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_REQUIRE(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_REQUIRE(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_REQUIRE(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_REQUIRE(blackhawks_win_market.get_status() == betting_market_status::frozen); + + // we can't go back to upcoming from frozen once we've gone in_progress. + // settled is disallowed everywhere + BOOST_TEST_MESSAGE("verifying we can't jump to upcoming"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::upcoming, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to settled"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::settled, _force = true), fc::exception); + + BOOST_TEST_MESSAGE("setting the event to finished"); + update_event(capitals_vs_blackhawks.id, _status = event_status::finished); + generate_blocks(1); + BOOST_REQUIRE(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_REQUIRE(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_REQUIRE(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_REQUIRE(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + // we can't go back to upcoming, in_progress, or frozen once we're finished. + // settled is disallowed everywhere + BOOST_TEST_MESSAGE("verifying we can't jump to upcoming"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::upcoming, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to in_progress"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::in_progress, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to frozen"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::frozen, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to settled"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks.id, _status = event_status::settled, _force = true), fc::exception); + + BOOST_TEST_MESSAGE("setting the event to canceled"); + update_event(capitals_vs_blackhawks.id, _status = event_status::canceled); + generate_blocks(1); + + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id}); + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "canceled"); + + // we can't go back to upcoming, in_progress, frozen, or finished once we're canceled. + // (this won't work because the event has been deleted) + BOOST_TEST_MESSAGE("verifying we can't jump to upcoming"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::upcoming, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to in_progress"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::in_progress, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to frozen"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::frozen, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to finished"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::finished, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to settled"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::settled, _force = true), fc::exception); + } FC_LOG_AND_RETHROW() +} + +// This tests that we can't transition out of settled (all other transitions tested in the previous test) +// - settled -> anything +BOOST_AUTO_TEST_CASE(event_driven_progression_errors_2) +{ + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + graphene::bookie::bookie_api bookie_api(app); + // save the event id for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_REQUIRE(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_REQUIRE(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_REQUIRE(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_REQUIRE(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting the event to finished"); + update_event(capitals_vs_blackhawks.id, _status = event_status::finished); + generate_blocks(1); + BOOST_REQUIRE(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_REQUIRE(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_REQUIRE(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_REQUIRE(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("grading betting market"); + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + // as soon as a block is generated, the betting market group will settle, and the market + // and group will cease to exist. The event should transition to "settled", then removed + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id}); + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "settled"); + + // we can't go back to upcoming, in_progress, frozen, or finished once we're canceled. + // (this won't work because the event has been deleted) + BOOST_TEST_MESSAGE("verifying we can't jump to upcoming"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::upcoming, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to in_progress"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::in_progress, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to frozen"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::frozen, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to finished"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::finished, _force = true), fc::exception); + BOOST_TEST_MESSAGE("verifying we can't jump to canceled"); + BOOST_CHECK_THROW(update_event(capitals_vs_blackhawks_id, _status = event_status::canceled, _force = true), fc::exception); + } FC_LOG_AND_RETHROW() +} + +// This tests a normal progression by setting the betting_market_group state and +// letting it trickle up to the event: +BOOST_AUTO_TEST_CASE(betting_market_group_driven_standard_progression) +{ + try + { + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + graphene::bookie::bookie_api bookie_api(app); + // save the event id for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("setting betting market to frozen"); + // the event should stay in the upcoming state + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::frozen); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::frozen); + + BOOST_TEST_MESSAGE("setting the event frozen"); + // this should only change the status of the event, just verify that nothing weird happens when + // we try to set the bmg to frozen when it's already frozen + update_event(capitals_vs_blackhawks.id, _status = event_status::frozen); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::frozen); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::frozen); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::frozen); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::frozen); + + BOOST_TEST_MESSAGE("setting betting market to closed"); + // the event should go to finished automatically + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("grading betting market"); + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + // as soon as a block is generated, the betting market group will settle, and the market + // and group will cease to exist. The event should transition to "settled" + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id}); + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "settled"); + } FC_LOG_AND_RETHROW() +} + +// This tests a normal progression in an event with multiple betting market groups +// letting info it trickle up from the group to the event: +BOOST_AUTO_TEST_CASE(multi_betting_market_group_driven_standard_progression) +{ + try + { + CREATE_EXTENDED_ICE_HOCKEY_BETTING_MARKET(false, 0); + + graphene::bookie::bookie_api bookie_api(app); + // save the event id for checking after it is deleted + event_id_type capitals_vs_blackhawks_id = capitals_vs_blackhawks.id; + + BOOST_TEST_MESSAGE("verify everything is in the correct initial state"); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(first_period_result_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(first_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(first_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(second_period_result_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(second_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(second_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(third_period_result_betting_markets.get_status() == betting_market_group_status::upcoming); + BOOST_CHECK(third_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(third_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("the game is starting, setting the main betting market and the first period to in_play"); + // the event should stay in the upcoming state + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::in_play); + update_betting_market_group(first_period_result_betting_markets.id, _status = betting_market_group_status::in_play); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(first_period_result_betting_markets.get_status() == betting_market_group_status::in_play); + BOOST_CHECK(first_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(first_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); - uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; - uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; - BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); - BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 + 2000000 - rake_value); - BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + BOOST_TEST_MESSAGE("the first period is over, starting the second period"); + update_betting_market_group(first_period_result_betting_markets.id, _status = betting_market_group_status::closed); + update_betting_market_group(second_period_result_betting_markets.id, _status = betting_market_group_status::in_play); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(first_period_result_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(first_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(first_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(second_period_result_betting_markets.get_status() == betting_market_group_status::in_play); + BOOST_CHECK(second_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(second_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_TEST_MESSAGE("grading the first period market"); + resolve_betting_market_group(first_period_result_betting_markets.id, + {{first_period_capitals_win_market.id, betting_market_resolution_type::win}, + {first_period_blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + BOOST_TEST_MESSAGE("the second period is over, starting the third period"); + update_betting_market_group(second_period_result_betting_markets.id, _status = betting_market_group_status::closed); + update_betting_market_group(third_period_result_betting_markets.id, _status = betting_market_group_status::in_play); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::upcoming); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::in_play); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(second_period_result_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(second_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(second_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(third_period_result_betting_markets.get_status() == betting_market_group_status::in_play); + BOOST_CHECK(third_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(third_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("grading the second period market"); + resolve_betting_market_group(second_period_result_betting_markets.id, + {{second_period_capitals_win_market.id, betting_market_resolution_type::win}, + {second_period_blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + BOOST_TEST_MESSAGE("the game is over, closing 3rd period and game"); + update_betting_market_group(third_period_result_betting_markets.id, _status = betting_market_group_status::closed); + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + generate_blocks(1); + BOOST_CHECK(capitals_vs_blackhawks.get_status() == event_status::finished); + BOOST_CHECK(moneyline_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(blackhawks_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(third_period_result_betting_markets.get_status() == betting_market_group_status::closed); + BOOST_CHECK(third_period_capitals_win_market.get_status() == betting_market_status::unresolved); + BOOST_CHECK(third_period_blackhawks_win_market.get_status() == betting_market_status::unresolved); + + BOOST_TEST_MESSAGE("grading the third period and game"); + resolve_betting_market_group(third_period_result_betting_markets.id, + {{third_period_capitals_win_market.id, betting_market_resolution_type::win}, + {third_period_blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + // as soon as a block is generated, the two betting market groups will settle, and the market + // and group will cease to exist. The event should transition to "settled" + fc::variants objects_from_bookie = bookie_api.get_objects({capitals_vs_blackhawks_id}); + BOOST_CHECK_EQUAL(objects_from_bookie[0]["status"].as(), "settled"); } FC_LOG_AND_RETHROW() } @@ -1027,60 +1788,60 @@ BOOST_AUTO_TEST_SUITE(other_betting_tests) BOOST_FIXTURE_TEST_CASE( another_event_group_update_test, database_fixture) { - try - { - ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(); - - transfer(account_id_type(), alice_id, asset(10000000)); - transfer(account_id_type(), bob_id, asset(10000000)); - - place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); - - fc::optional name = internationalized_string_type({{"en", "IBM"}, {"zh_Hans", "國家冰球聯"}, {"ja", "ナショナルホッケーリー"}}); - - const sport_object& ice_on_hockey = create_sport({{"en", "Hockey on Ice"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}); \ - fc::optional sport_id = ice_on_hockey.id; - - update_event_group(nhl.id, fc::optional(), name); - update_event_group(nhl.id, sport_id, fc::optional()); - update_event_group(nhl.id, sport_id, name); - - // trx_state->_is_proposed_trx - //GRAPHENE_REQUIRE_THROW(try_update_event_group(nhl.id, fc::optional(), fc::optional(), true), fc::exception); - TRY_EXPECT_THROW(try_update_event_group(nhl.id, fc::optional(), fc::optional(), true), fc::exception, "_is_proposed_trx"); - - // #! nothing to change - //GRAPHENE_REQUIRE_THROW(try_update_event_group(nhl.id, fc::optional(), fc::optional()), fc::exception); - TRY_EXPECT_THROW(try_update_event_group(nhl.id, fc::optional(), fc::optional()), fc::exception, "nothing to change"); - - // #! sport_id must refer to a sport_id_type - sport_id = capitals_win_market.id; - //GRAPHENE_REQUIRE_THROW(try_update_event_group(nhl.id, sport_id, fc::optional()), fc::exception); - TRY_EXPECT_THROW(try_update_event_group(nhl.id, sport_id, fc::optional()), fc::exception, "sport_id must refer to a sport_id_type"); - - // #! invalid sport specified - sport_id = sport_id_type(13); - //GRAPHENE_REQUIRE_THROW(try_update_event_group(nhl.id, sport_id, fc::optional()), fc::exception); - TRY_EXPECT_THROW(try_update_event_group(nhl.id, sport_id, fc::optional()), fc::exception, "invalid sport specified"); - - place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); - - BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); - BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); - - // caps win - resolve_betting_market_group(moneyline_betting_markets.id, - {{capitals_win_market.id, betting_market_resolution_type::win}, - {blackhawks_win_market.id, betting_market_resolution_type::cancel}}); - - - uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; - uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; - BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); - BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 + 2000000 - rake_value); - BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); - + try + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + transfer(account_id_type(), alice_id, asset(10000000)); + transfer(account_id_type(), bob_id, asset(10000000)); + + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + fc::optional name = internationalized_string_type({{"en", "IBM"}, {"zh_Hans", "國家冰球聯"}, {"ja", "ナショナルホッケーリー"}}); + + const sport_object& ice_on_hockey = create_sport({{"en", "Hockey on Ice"}, {"zh_Hans", "冰球"}, {"ja", "アイスホッケー"}}); \ + fc::optional sport_id = ice_on_hockey.id; + + update_event_group(nhl.id, fc::optional(), name); + update_event_group(nhl.id, sport_id, fc::optional()); + update_event_group(nhl.id, sport_id, name); + + // trx_state->_is_proposed_trx + //GRAPHENE_REQUIRE_THROW(try_update_event_group(nhl.id, fc::optional(), fc::optional(), true), fc::exception); + TRY_EXPECT_THROW(try_update_event_group(nhl.id, fc::optional(), fc::optional(), true), fc::exception, "_is_proposed_trx"); + + // #! nothing to change + //GRAPHENE_REQUIRE_THROW(try_update_event_group(nhl.id, fc::optional(), fc::optional()), fc::exception); + TRY_EXPECT_THROW(try_update_event_group(nhl.id, fc::optional(), fc::optional()), fc::exception, "nothing to change"); + + // #! sport_id must refer to a sport_id_type + sport_id = capitals_win_market.id; + //GRAPHENE_REQUIRE_THROW(try_update_event_group(nhl.id, sport_id, fc::optional()), fc::exception); + TRY_EXPECT_THROW(try_update_event_group(nhl.id, sport_id, fc::optional()), fc::exception, "sport_id must refer to a sport_id_type"); + + // #! invalid sport specified + sport_id = sport_id_type(13); + //GRAPHENE_REQUIRE_THROW(try_update_event_group(nhl.id, sport_id, fc::optional()), fc::exception); + TRY_EXPECT_THROW(try_update_event_group(nhl.id, sport_id, fc::optional()), fc::exception, "invalid sport specified"); + + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + + update_betting_market_group(moneyline_betting_markets.id, _status = betting_market_group_status::closed); + // caps win + resolve_betting_market_group(moneyline_betting_markets.id, + {{capitals_win_market.id, betting_market_resolution_type::win}, + {blackhawks_win_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); + + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; + uint32_t rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + BOOST_TEST_MESSAGE("Rake value " + std::to_string(rake_value)); + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 + 2000000 - rake_value); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); } FC_LOG_AND_RETHROW() } @@ -1088,31 +1849,13 @@ BOOST_AUTO_TEST_SUITE_END() BOOST_FIXTURE_TEST_SUITE( tennis_bet_tests, database_fixture ) -#define CREATE_TENNIS_BETTING_MARKET() \ - const betting_market_rules_object& tennis_rules = create_betting_market_rules({{"en", "Tennis Rules v1.0"}}, {{"en", "The winner is the player who wins the last ball in the match."}}); \ - const sport_object& tennis = create_sport({{"en", "Tennis"}}); \ - const event_group_object& wimbledon = create_event_group({{"en", "Wimbledon"}}, tennis.id); \ - const event_object& berdych_vs_federer = create_event({{"en", "R. Federer/T. Berdych"}}, {{"en", "2017"}}, wimbledon.id); \ - const event_object& cilic_vs_querrey = create_event({{"en", "M. Cilic/S. Querrye"}}, {{"en", "2017"}}, wimbledon.id); \ - const betting_market_group_object& moneyline_berdych_vs_federer = create_betting_market_group({{"en", "Moneyline 1st sf"}}, berdych_vs_federer.id, tennis_rules.id, asset_id_type()); \ - const betting_market_group_object& moneyline_cilic_vs_querrey = create_betting_market_group({{"en", "Moneyline 2nd sf"}}, cilic_vs_querrey.id, tennis_rules.id, asset_id_type()); \ - const betting_market_object& berdych_wins_market = create_betting_market(moneyline_berdych_vs_federer.id, {{"en", "T. Berdych defeats R. Federer"}}); \ - const betting_market_object& federer_wins_market = create_betting_market(moneyline_berdych_vs_federer.id, {{"en", "R. Federer defeats T. Berdych"}}); \ - const betting_market_object& cilic_wins_market = create_betting_market(moneyline_cilic_vs_querrey.id, {{"en", "M. Cilic defeats S. Querrey"}}); \ - const betting_market_object& querrey_wins_market = create_betting_market(moneyline_cilic_vs_querrey.id, {{"en", "S. Querrey defeats M. Cilic"}});\ - \ - const event_object& cilic_vs_federer = create_event({{"en", "R. Federer/M. Cilic"}}, {{"en", "2017"}}, wimbledon.id); \ - const betting_market_group_object& moneyline_cilic_vs_federer = create_betting_market_group({{"en", "Moneyline final"}}, cilic_vs_federer.id, tennis_rules.id, asset_id_type()); \ - const betting_market_object& federer_wins_final_market = create_betting_market(moneyline_cilic_vs_federer.id, {{"en", "R. Federer defeats M. Cilic"}}); \ - const betting_market_object& cilic_wins_final_market = create_betting_market(moneyline_cilic_vs_federer.id, {{"en", "M. Cilic defeats R. Federer"}}); - - BOOST_AUTO_TEST_CASE( wimbledon_2017_gentelmen_singles_sf_test ) { try { ACTORS( (alice)(bob) ); CREATE_TENNIS_BETTING_MARKET(); + generate_blocks(1); uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage; @@ -1139,10 +1882,12 @@ BOOST_AUTO_TEST_CASE( wimbledon_2017_gentelmen_singles_sf_test ) BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 - 100000); BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000 - 100000); + update_betting_market_group(moneyline_berdych_vs_federer.id, _status = betting_market_group_status::closed); // federer wins resolve_betting_market_group(moneyline_berdych_vs_federer.id, - {{berdych_wins_market.id, betting_market_resolution_type::/*don't use cancel - there are bets for berdych*/not_win}, + {{berdych_wins_market.id, betting_market_resolution_type::not_win}, {federer_wins_market.id, betting_market_resolution_type::win}}); + generate_blocks(1); uint32_t bob_rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; BOOST_TEST_MESSAGE("Bob's rake value " + std::to_string(bob_rake_value)); @@ -1150,10 +1895,12 @@ BOOST_AUTO_TEST_CASE( wimbledon_2017_gentelmen_singles_sf_test ) BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 - 100000); BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000 - 100000 + 2000000 - bob_rake_value); + update_betting_market_group(moneyline_cilic_vs_querrey.id, _status = betting_market_group_status::closed); // cilic wins resolve_betting_market_group(moneyline_cilic_vs_querrey.id, {{cilic_wins_market.id, betting_market_resolution_type::win}, - {querrey_wins_market.id, betting_market_resolution_type::/*may use cancel - no bets for querrey*/not_win}}); + {querrey_wins_market.id, betting_market_resolution_type::not_win}}); + generate_blocks(1); uint32_t alice_rake_value = (-100000 + 200000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; BOOST_TEST_MESSAGE("Alice rake value " + std::to_string(alice_rake_value)); @@ -1182,13 +1929,7 @@ BOOST_AUTO_TEST_CASE( wimbledon_2017_gentelmen_singles_final_test ) BOOST_TEST_MESSAGE("cilic_wins_final_market " << fc::variant(cilic_wins_final_market.id).as()); betting_market_group_id_type moneyline_cilic_vs_federer_id = moneyline_cilic_vs_federer.id; - fc::optional empty_str; - fc::optional empty_obj; - fc::optional empty_bool; - fc::optional true_bool = true; - update_betting_market_group(moneyline_cilic_vs_federer_id, empty_str, - empty_obj, - empty_bool, true_bool); + update_betting_market_group(moneyline_cilic_vs_federer_id, _status = betting_market_group_status::in_play); place_bet(alice_id, cilic_wins_final_market.id, bet_type::back, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); place_bet(bob_id, cilic_wins_final_market.id, bet_type::lay, asset(1000000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); @@ -1196,10 +1937,7 @@ BOOST_AUTO_TEST_CASE( wimbledon_2017_gentelmen_singles_final_test ) auto cilic_wins_final_market_id = cilic_wins_final_market.id; auto federer_wins_final_market_id = federer_wins_final_market.id; - update_event(cilic_vs_federer.id, - fc::optional(), - internationalized_string_type({{"en", "R. Federer vs. M. Cilic"}}), - fc::optional(), fc::optional()); + update_event(cilic_vs_federer.id, _name = internationalized_string_type({{"en", "R. Federer vs. M. Cilic"}})); generate_blocks(13); @@ -1209,10 +1947,12 @@ BOOST_AUTO_TEST_CASE( wimbledon_2017_gentelmen_singles_final_test ) BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000); + update_betting_market_group(moneyline_cilic_vs_federer_id, _status = betting_market_group_status::closed); // federer wins resolve_betting_market_group(moneyline_cilic_vs_federer_id, {{cilic_wins_final_market_id, betting_market_resolution_type::/*don't use cancel - there are bets for berdych*/not_win}, {federer_wins_final_market_id, betting_market_resolution_type::win}}); + generate_blocks(1); uint32_t bob_rake_value = (-1000000 + 2000000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; BOOST_TEST_MESSAGE("Bob's rake value " + std::to_string(bob_rake_value)); @@ -1220,8 +1960,8 @@ BOOST_AUTO_TEST_CASE( wimbledon_2017_gentelmen_singles_final_test ) BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000); BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000 + 2000000 - bob_rake_value); - } FC_LOG_AND_RETHROW() - } + } FC_LOG_AND_RETHROW() +} BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index 2224db67..481c8708 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -1150,6 +1150,19 @@ void database_fixture::process_operation_by_witnesses(operation op) } } +void database_fixture::force_operation_by_witnesses(operation op) +{ + const chain_parameters& params = db.get_global_properties().parameters; + signed_transaction trx; + trx.operations = {op}; + for( auto& op : trx.operations ) + db.current_fee_schedule().set_fee(op); + trx.validate(); + trx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3)); + sign(trx, init_account_priv_key); + PUSH_TX(db, trx); +} + void database_fixture::set_is_proposed_trx(operation op) { const flat_set& active_witnesses = db.get_global_properties().active_witnesses; @@ -1306,20 +1319,25 @@ const event_object& database_fixture::create_event(internationalized_string_type return *event_index.rbegin(); } FC_CAPTURE_AND_RETHROW( (event_group_id) ) } -void database_fixture::update_event(event_id_type event_id, - fc::optional event_group_id, - fc::optional name, - fc::optional season, - fc::optional is_live_market) +void database_fixture::update_event_impl(event_id_type event_id, + fc::optional event_group_id, + fc::optional name, + fc::optional season, + fc::optional status, + bool force) { try { event_update_operation event_update_op; event_update_op.event_id = event_id; event_update_op.new_event_group_id = event_group_id; event_update_op.new_name = name; event_update_op.new_season = season; - event_update_op.is_live_market = is_live_market; - process_operation_by_witnesses(event_update_op); -} FC_CAPTURE_AND_RETHROW( (name)(season) ) } + event_update_op.new_status = status; + + if (force) + force_operation_by_witnesses(event_update_op); + else + process_operation_by_witnesses(event_update_op); +} FC_CAPTURE_AND_RETHROW( (event_id) ) } const betting_market_rules_object& database_fixture::create_betting_market_rules(internationalized_string_type name, internationalized_string_type description) { try { @@ -1342,33 +1360,44 @@ void database_fixture::update_betting_market_rules(betting_market_rules_id_type process_operation_by_witnesses(betting_market_rules_update_op); } FC_CAPTURE_AND_RETHROW( (name)(description) ) } -const betting_market_group_object& database_fixture::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) +const betting_market_group_object& database_fixture::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, + bool never_in_play, + uint32_t delay_before_settling) { try { betting_market_group_create_operation betting_market_group_create_op; betting_market_group_create_op.description = description; betting_market_group_create_op.event_id = event_id; betting_market_group_create_op.rules_id = rules_id; betting_market_group_create_op.asset_id = asset_id; + betting_market_group_create_op.never_in_play = never_in_play; + betting_market_group_create_op.delay_before_settling = delay_before_settling; + process_operation_by_witnesses(betting_market_group_create_op); const auto& betting_market_group_index = db.get_index_type().indices().get(); return *betting_market_group_index.rbegin(); } FC_CAPTURE_AND_RETHROW( (event_id) ) } -void database_fixture::update_betting_market_group(betting_market_group_id_type betting_market_group_id, - fc::optional description, - fc::optional rules_id, - fc::optional freeze, - fc::optional delay_bets) +void database_fixture::update_betting_market_group_impl(betting_market_group_id_type betting_market_group_id, + fc::optional description, + fc::optional rules_id, + fc::optional status, + bool force) { 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_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)(rules_id)(freeze)(delay_bets)) } + betting_market_group_update_op.status = status; + + if (force) + force_operation_by_witnesses(betting_market_group_update_op); + else + process_operation_by_witnesses(betting_market_group_update_op); +} FC_CAPTURE_AND_RETHROW( (betting_market_group_id)(description)(rules_id)(status)) } const betting_market_object& database_fixture::create_betting_market(betting_market_group_id_type group_id, internationalized_string_type payout_condition) @@ -1412,6 +1441,7 @@ void database_fixture::update_betting_market(betting_market_id_type betting_mark return ptx.operation_results.front().get().as(); } FC_CAPTURE_AND_RETHROW( (bettor_id)(back_or_lay)(amount_to_bet) ) } + void database_fixture::resolve_betting_market_group(betting_market_group_id_type betting_market_group_id, std::map resolutions) { try { diff --git a/tests/common/database_fixture.hpp b/tests/common/database_fixture.hpp index ff84c190..3e39d34c 100644 --- a/tests/common/database_fixture.hpp +++ b/tests/common/database_fixture.hpp @@ -30,6 +30,8 @@ #include +#include + #include using namespace graphene::db; @@ -144,6 +146,18 @@ extern uint32_t GRAPHENE_TESTING_GENESIS_TIMESTAMP; namespace graphene { namespace chain { + namespace keywords { + BOOST_PARAMETER_NAME(event_id) + BOOST_PARAMETER_NAME(event_group_id) + BOOST_PARAMETER_NAME(name) + BOOST_PARAMETER_NAME(season) + BOOST_PARAMETER_NAME(status) + BOOST_PARAMETER_NAME(force) + BOOST_PARAMETER_NAME(betting_market_group_id) + BOOST_PARAMETER_NAME(description) + BOOST_PARAMETER_NAME(rules_id) + } + struct database_fixture { // the reason we use an app is to exercise the indexes of built-in // plugins @@ -285,6 +299,7 @@ struct database_fixture { asset_id_type dividend_payout_asset_type) const; vector< operation_history_object > get_operation_history( account_id_type account_id )const; void process_operation_by_witnesses(operation op); + void force_operation_by_witnesses(operation op); void set_is_proposed_trx(operation op); const sport_object& create_sport(internationalized_string_type name); void update_sport(sport_id_type sport_id, internationalized_string_type name); @@ -297,21 +312,47 @@ struct database_fixture { fc::optional name, bool dont_set_is_proposed_trx = false); const event_object& create_event(internationalized_string_type name, internationalized_string_type season, event_group_id_type event_group_id); - void update_event(event_id_type event_id, - fc::optional event_group_id, - fc::optional name, - fc::optional season, - fc::optional is_live_market); + void update_event_impl(event_id_type event_id, + fc::optional event_group_id, + fc::optional name, + fc::optional season, + fc::optional status, + bool force); + BOOST_PARAMETER_MEMBER_FUNCTION((void), update_event, keywords::tag, + (required (event_id, (event_id_type))) + (optional (event_group_id, (fc::optional), fc::optional()) + (name, (fc::optional), fc::optional()) + (season, (fc::optional), fc::optional()) + (status, (fc::optional), fc::optional()) + (force, (bool), false))) + { + update_event_impl(event_id, event_group_id, name, season, status, force); + } + const betting_market_rules_object& create_betting_market_rules(internationalized_string_type name, internationalized_string_type description); void update_betting_market_rules(betting_market_rules_id_type rules_id, fc::optional name, fc::optional description); - 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 rules_id, - fc::optional freeze, - fc::optional delay_bets); + 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, + bool never_in_play, + uint32_t delay_before_settling); + void update_betting_market_group_impl(betting_market_group_id_type betting_market_group_id, + fc::optional description, + fc::optional rules_id, + fc::optional status, + bool force); + BOOST_PARAMETER_MEMBER_FUNCTION((void), update_betting_market_group, keywords::tag, + (required (betting_market_group_id, (betting_market_group_id_type))) + (optional (description, (fc::optional), fc::optional()) + (rules_id, (fc::optional), fc::optional()) + (status, (fc::optional), fc::optional()) + (force, (bool), false))) + { + update_betting_market_group_impl(betting_market_group_id, description, rules_id, status, force); + } const betting_market_object& create_betting_market(betting_market_group_id_type group_id, internationalized_string_type payout_condition); void update_betting_market(betting_market_id_type betting_market_id,