diff --git a/libraries/app/impacted.cpp b/libraries/app/impacted.cpp index 6a16c5e3..5ea1f4e6 100644 --- a/libraries/app/impacted.cpp +++ b/libraries/app/impacted.cpp @@ -209,6 +209,7 @@ struct get_impacted_account_visitor void operator()( const event_create_operation& op ) {} void operator()( const betting_market_group_create_operation& op ) {} void operator()( const betting_market_create_operation& op ) {} + void operator()( const betting_market_resolve_operation& op ) {} void operator()( const bet_place_operation& op ) { _impacted.insert( op.bettor_id ); diff --git a/libraries/chain/betting_market_evaluator.cpp b/libraries/chain/betting_market_evaluator.cpp index 503a6618..fe9ebc14 100644 --- a/libraries/chain/betting_market_evaluator.cpp +++ b/libraries/chain/betting_market_evaluator.cpp @@ -179,4 +179,17 @@ void_result bet_cancel_evaluator::do_apply(const bet_cancel_operation& op) return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } +void_result betting_market_resolve_evaluator::do_evaluate(const betting_market_resolve_operation& op) +{ try { + const database& d = db(); + _betting_market = &op.betting_market_id(d); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result betting_market_resolve_evaluator::do_apply(const betting_market_resolve_operation& op) +{ try { + db().resolve_betting_market(*_betting_market, op.resolution); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + } } // graphene::chain diff --git a/libraries/chain/db_bet.cpp b/libraries/chain/db_bet.cpp index 0c5c4d3d..f6ae85b8 100644 --- a/libraries/chain/db_bet.cpp +++ b/libraries/chain/db_bet.cpp @@ -19,17 +19,50 @@ void database::cancel_bet( const bet_object& bet, bool create_virtual_op ) remove(bet); } +void database::resolve_betting_market(const betting_market_object& betting_market, + betting_market_resolution_type resolution) +{ + // TODO: cancel all unmatched bets on the books for this market + auto& index = get_index_type().indices().get(); + auto position_itr = index.lower_bound(std::make_tuple(betting_market.id)); + while (position_itr != index.end() && + position_itr->betting_market_id == betting_market.id) + { + const betting_market_position_object& position = *position_itr; + ++position_itr; + + share_type payout_amount = 0; + switch (resolution) + { + case betting_market_resolution_type::win: + payout_amount += position.pay_if_payout_condition; + payout_amount += position.pay_if_not_canceled; + break; + case betting_market_resolution_type::not_win: + payout_amount += position.pay_if_not_payout_condition; + payout_amount += position.pay_if_not_canceled; + break; + case betting_market_resolution_type::cancel: + payout_amount += position.pay_if_canceled; + break; + } + + adjust_balance(position.bettor_id, asset(payout_amount, betting_market.asset_id)); + //TODO: pay the fees to the correct (dividend-distribution) account + adjust_balance(account_id_type(), asset(position.fees_collected, betting_market.asset_id)); + //TODO: generate a virtual op to notify the bettor that they won or lost + + remove(position); + } + remove(betting_market); +} + bool maybe_cull_small_bet( database& db, const bet_object& bet_object_to_cull ) { /** - * There are times when the AMOUNT_FOR_SALE * SALE_PRICE == 0 which means that we - * have hit the limit where the seller is asking for nothing in return. When this - * happens we must refund any balance back to the seller, it is too small to be - * sold at the sale price. - * - * If the order is a taker order (as opposed to a maker order), so the price is - * set by the counterparty, this check is deferred until the order becomes unmatched - * (see #555) -- however, detecting this condition is the responsibility of the caller. + * There are times when this bet can't be matched (for example, it's now laying a 2:1 bet for + * 1 satoshi, so it could only be matched by half a satoshi). Remove these bets from + * the books. */ if( bet_object_to_cull.get_matching_amount() == 0 ) diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index b908adfb..e37578aa 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -218,6 +218,7 @@ void database::initialize_evaluators() register_evaluator(); register_evaluator(); register_evaluator(); + register_evaluator(); } void database::initialize_indexes() diff --git a/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp b/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp index a56616bb..bae04611 100644 --- a/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp +++ b/libraries/chain/include/graphene/chain/betting_market_evaluator.hpp @@ -76,4 +76,15 @@ namespace graphene { namespace chain { const bet_object* _bet_to_cancel; }; + class betting_market_resolve_evaluator : public evaluator + { + public: + typedef betting_market_resolve_operation operation_type; + + void_result do_evaluate( const betting_market_resolve_operation& o ); + void_result do_apply( const betting_market_resolve_operation& o ); + private: + const betting_market_object* _betting_market; + }; + } } // graphene::chain diff --git a/libraries/chain/include/graphene/chain/betting_market_object.hpp b/libraries/chain/include/graphene/chain/betting_market_object.hpp index 92159c04..add736ec 100644 --- a/libraries/chain/include/graphene/chain/betting_market_object.hpp +++ b/libraries/chain/include/graphene/chain/betting_market_object.hpp @@ -235,6 +235,7 @@ typedef multi_index_container< typedef generic_index bet_object_index; struct by_bettor_betting_market{}; +struct by_betting_market_bettor{}; typedef multi_index_container< betting_market_position_object, indexed_by< @@ -243,8 +244,13 @@ typedef multi_index_container< composite_key< betting_market_position_object, member, - member - > > > > betting_market_position_multi_index_type; + member > >, + ordered_unique< tag, + composite_key< + betting_market_position_object, + member, + member > > + > > betting_market_position_multi_index_type; typedef generic_index betting_market_position_index; } } // graphene::chain diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index b3ed41d6..6b0a9823 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -365,6 +365,8 @@ namespace graphene { namespace chain { /// @{ @group Betting Market Helpers void cancel_bet(const bet_object& bet, bool create_virtual_op = true); + void resolve_betting_market(const betting_market_object& betting_market, + betting_market_resolution_type resolution); /** * @brief Process a new bet * @param new_bet_object The new bet to process diff --git a/libraries/chain/include/graphene/chain/protocol/operations.hpp b/libraries/chain/include/graphene/chain/protocol/operations.hpp index caec546d..816caa73 100644 --- a/libraries/chain/include/graphene/chain/protocol/operations.hpp +++ b/libraries/chain/include/graphene/chain/protocol/operations.hpp @@ -104,6 +104,7 @@ namespace graphene { namespace chain { betting_market_group_create_operation, betting_market_create_operation, bet_place_operation, + betting_market_resolve_operation, bet_matched_operation, // VIRTUAL bet_cancel_operation, bet_canceled_operation // VIRTUAL diff --git a/tests/tests/operation_tests2.cpp b/tests/tests/operation_tests2.cpp index 0661109d..ff65ad6e 100644 --- a/tests/tests/operation_tests2.cpp +++ b/tests/tests/operation_tests2.cpp @@ -1809,7 +1809,7 @@ BOOST_AUTO_TEST_CASE( peerplays_sport_create_test ) const betting_market_object& market = *db.get_index_type().indices().begin(); { - // have bob lay a bet at 1:1 odds + // have bob lay a bet for 1M (+20k fees) at 1:1 odds signed_transaction tx; bet_place_operation bet_op; bet_op.bettor_id = bob_id; @@ -1826,7 +1826,7 @@ BOOST_AUTO_TEST_CASE( peerplays_sport_create_test ) } { - // have alice back a matching bet at 1:1 odds + // have alice back a matching bet at 1:1 odds (also costing 1.02M) signed_transaction tx; bet_place_operation bet_op; bet_op.bettor_id = alice_id; @@ -1841,6 +1841,52 @@ BOOST_AUTO_TEST_CASE( peerplays_sport_create_test ) sign(tx, alice_private_key); db.push_transaction(tx); } + + // caps win + { + proposal_create_operation proposal_op; + proposal_op.fee_paying_account = (*active_witnesses.begin())(db).witness_account; + betting_market_resolve_operation betting_market_resolve_op; + betting_market_resolve_op.betting_market_id = market.id; + betting_market_resolve_op.resolution = betting_market_resolution_type::win; + proposal_op.proposed_ops.emplace_back(betting_market_resolve_op); + proposal_op.expiration_time = db.head_block_time() + fc::days(1); + signed_transaction tx; + tx.operations.push_back(proposal_op); + set_expiration(db, tx); + sign(tx, init_account_priv_key); + + db.push_transaction(tx); + } + + BOOST_REQUIRE_EQUAL(db.get_index_type().indices().size(), 1); + { + const proposal_object& prop = *db.get_index_type().indices().begin(); + + for (const witness_id_type& witness_id : active_witnesses) + { + BOOST_TEST_MESSAGE("Approving market resolve witness " << fc::variant(witness_id).as()); + const witness_object& witness = witness_id(db); + const account_object& witness_account = witness.witness_account(db); + + proposal_update_operation pup; + pup.proposal = prop.id; + pup.fee_paying_account = witness_account.id; + pup.active_approvals_to_add.insert(witness_account.id); + + signed_transaction tx; + tx.operations.push_back( pup ); + set_expiration( db, tx ); + sign(tx, init_account_priv_key); + + db.push_transaction(tx, ~0); + if (db.get_index_type().indices().size() == 0) + break; + } + } + + BOOST_CHECK_EQUAL(get_balance(alice_id, asset_id_type()), 10000000 - 1000000 - 20000 + 2000000); + BOOST_CHECK_EQUAL(get_balance(bob_id, asset_id_type()), 10000000 - 1000000 - 20000); } }