diff --git a/libraries/app/CMakeLists.txt b/libraries/app/CMakeLists.txt index a6157560..077eb4aa 100644 --- a/libraries/app/CMakeLists.txt +++ b/libraries/app/CMakeLists.txt @@ -13,7 +13,7 @@ add_library( graphene_app # need to link graphene_debug_witness because plugins aren't sufficiently isolated #246 #target_link_libraries( graphene_app graphene_market_history graphene_account_history graphene_chain fc graphene_db graphene_net graphene_utilities graphene_debug_witness ) -target_link_libraries( graphene_app graphene_market_history graphene_account_history graphene_accounts_list graphene_chain fc graphene_db graphene_net graphene_time graphene_utilities graphene_debug_witness graphene_bookie ) +target_link_libraries( graphene_app graphene_market_history graphene_account_history graphene_accounts_list graphene_affiliate_stats graphene_chain fc graphene_db graphene_net graphene_time graphene_utilities graphene_debug_witness graphene_bookie ) target_include_directories( graphene_app PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" "${CMAKE_CURRENT_SOURCE_DIR}/../egenesis/include" ) diff --git a/libraries/app/api.cpp b/libraries/app/api.cpp index 6c6359c2..077ad0d1 100644 --- a/libraries/app/api.cpp +++ b/libraries/app/api.cpp @@ -117,6 +117,12 @@ namespace graphene { namespace app { if( _app.get_plugin( "bookie" ) ) _bookie_api = std::make_shared(std::ref(_app)); } + else if( api_name == "affiliate_stats_api" ) + { + // can only enable this API if the plugin was loaded + if( _app.get_plugin( "affiliate_stats" ) ) + _affiliate_stats_api = std::make_shared(std::ref(_app)); + } return; } @@ -281,6 +287,12 @@ namespace graphene { namespace app { return *_bookie_api; } + fc::api login_api::affiliate_stats() const + { + FC_ASSERT(_affiliate_stats_api); + return *_affiliate_stats_api; + } + #if 0 vector get_relevant_accounts( const object* obj ) { diff --git a/libraries/app/application.cpp b/libraries/app/application.cpp index 5b079b94..9572af5e 100644 --- a/libraries/app/application.cpp +++ b/libraries/app/application.cpp @@ -493,6 +493,7 @@ namespace detail { wild_access.allowed_apis.push_back( "history_api" ); wild_access.allowed_apis.push_back( "crypto_api" ); wild_access.allowed_apis.push_back( "bookie_api" ); + wild_access.allowed_apis.push_back( "affiliate_stats_api" ); _apiaccess.permission_map["*"] = wild_access; } diff --git a/libraries/app/impacted.cpp b/libraries/app/impacted.cpp index c765de39..f2ca49c9 100644 --- a/libraries/app/impacted.cpp +++ b/libraries/app/impacted.cpp @@ -275,6 +275,11 @@ struct get_impacted_account_visitor { _impacted.insert( op.payout_account_id ); } + void operator()( const affiliate_payout_operation& op ) + { + _impacted.insert( op.affiliate ); + } + void operator()( const affiliate_referral_payout_operation& op ) { } }; void operation_get_impacted_accounts( const operation& op, flat_set& result ) diff --git a/libraries/app/include/graphene/app/api.hpp b/libraries/app/include/graphene/app/api.hpp index d4532b42..2fe0c938 100644 --- a/libraries/app/include/graphene/app/api.hpp +++ b/libraries/app/include/graphene/app/api.hpp @@ -32,6 +32,7 @@ #include #include +#include #include #include @@ -362,6 +363,8 @@ namespace graphene { namespace app { fc::api debug()const; /// @brief Retrieve the bookie API (if available) fc::api bookie()const; + /// @brief Retrieve the affiliate_stats API (if available) + fc::api affiliate_stats()const; /// @brief Called to enable an API, not reflected. void enable_api( const string& api_name ); @@ -377,6 +380,7 @@ namespace graphene { namespace app { optional< fc::api > _asset_api; optional< fc::api > _debug_api; optional< fc::api > _bookie_api; + optional< fc::api > _affiliate_stats_api; }; }} // graphene::app @@ -446,4 +450,5 @@ FC_API(graphene::app::login_api, (asset) (debug) (bookie) + (affiliate_stats) ) diff --git a/libraries/chain/CMakeLists.txt b/libraries/chain/CMakeLists.txt index 399720fa..cd3c29a2 100644 --- a/libraries/chain/CMakeLists.txt +++ b/libraries/chain/CMakeLists.txt @@ -108,6 +108,8 @@ add_library( graphene_chain betting_market_object.cpp betting_market_group_object.cpp + affiliate_payout.cpp + ${HEADERS} ${PROTOCOL_HEADERS} "${CMAKE_CURRENT_BINARY_DIR}/include/graphene/chain/hardfork.hpp" diff --git a/libraries/chain/account_evaluator.cpp b/libraries/chain/account_evaluator.cpp index 51205ae5..a08e9031 100644 --- a/libraries/chain/account_evaluator.cpp +++ b/libraries/chain/account_evaluator.cpp @@ -108,6 +108,8 @@ void_result account_create_evaluator::do_evaluate( const account_create_operatio FC_ASSERT( !op.extensions.value.active_special_authority.valid() ); FC_ASSERT( !op.extensions.value.buyback_options.valid() ); } + if( d.head_block_time() < HARDFORK_999_TIME ) + FC_ASSERT( !op.extensions.value.affiliate_distributions.valid(), "Affiliate reward distributions not allowed yet" ); FC_ASSERT( fee_paying_account->is_lifetime_member(), "Only Lifetime members may register an account." ); FC_ASSERT( op.referrer(d).is_member(d.head_block_time()), "The referrer must be either a lifetime or annual subscriber." ); @@ -186,6 +188,7 @@ object_id_type account_create_evaluator::do_apply( const account_create_operatio obj.allowed_assets = o.extensions.value.buyback_options->markets; obj.allowed_assets->emplace( o.extensions.value.buyback_options->asset_to_buy ); } + obj.affiliate_distributions = o.extensions.value.affiliate_distributions; }); if( has_small_percent ) diff --git a/libraries/chain/affiliate_payout.cpp b/libraries/chain/affiliate_payout.cpp new file mode 100644 index 00000000..3138117c --- /dev/null +++ b/libraries/chain/affiliate_payout.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2018 Peerplays Blockchain Standards Association, and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include +#include +#include + +#include + +namespace graphene { namespace chain { + + share_type affiliate_payout_helper::payout( account_id_type player, share_type amount ) + { + return payout( player(_db), amount ); + } + + share_type affiliate_payout_helper::payout( const account_object& player, share_type amount ) + { + if( !player.affiliate_distributions.valid() ) + return 0; + const auto& dist = player.affiliate_distributions->_dists.find( tag ); + if( dist == player.affiliate_distributions->_dists.end() || dist->second._dist.empty() ) + return 0; + + amount = amount.value / 5; // 20% fixed + if( amount <= 0 ) + return 0; + + uint16_t remaining = GRAPHENE_100_PERCENT; + share_type paid = 0; + share_type to_pay = amount; + for( const auto& entry : dist->second._dist ) + { + const account_id_type affiliate = entry.first; + const uint16_t share = entry.second; + fc::uint128_t payout = to_pay.value; + if( share != remaining ) + { + FC_ASSERT( share < remaining ); + payout *= share; + payout /= remaining; + //ilog("Paying ${p} of ${P} for ${s} of ${r}", ("p",payout.to_uint64())("P",to_pay.value)("s",share)("r",remaining) ); + remaining -= share; + } + FC_ASSERT( payout.to_uint64() <= to_pay ); + if( payout > 0 ) + { + if ( accumulator.find(affiliate) == accumulator.end() ) + accumulator[affiliate] = payout.to_uint64(); + else + accumulator[affiliate] += payout.to_uint64(); + to_pay -= payout.to_uint64(); + paid += payout.to_uint64(); + } + } + FC_ASSERT( to_pay == 0 ); + FC_ASSERT( paid == amount ); + + _db.push_applied_operation( affiliate_referral_payout_operation( player.id, asset( amount, payout_asset ) ) ); + + return paid; + } + + void affiliate_payout_helper::commit() + { + for( const auto& entry : accumulator ) + { + asset payout = asset( entry.second, payout_asset ); + _db.adjust_balance( entry.first, payout ); + _db.push_applied_operation( affiliate_payout_operation( entry.first, tag, payout ) ); + } + accumulator.clear(); + } + +} } // graphene::chain diff --git a/libraries/chain/db_bet.cpp b/libraries/chain/db_bet.cpp index 337b28c5..8c3e1357 100644 --- a/libraries/chain/db_bet.cpp +++ b/libraries/chain/db_bet.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -167,6 +168,8 @@ void database::settle_betting_market_group(const betting_market_group_object& be rake_account_id = core_asset_dividend_data_obj.dividend_distribution_account; } + affiliate_payout_helper payout_helper( *this, betting_market_group ); + // 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; @@ -261,8 +264,12 @@ void database::settle_betting_market_group(const betting_market_group_object& be if (net_profits.value > 0 && rake_account_id) { rake_amount = ((fc::uint128_t(net_profits.value) * rake_fee_percentage + GRAPHENE_100_PERCENT - 1) / GRAPHENE_100_PERCENT).to_uint64(); + share_type affiliates_share; if (rake_amount.value) - adjust_balance(*rake_account_id, asset(rake_amount, betting_market_group.asset_id)); + affiliates_share = payout_helper.payout( bettor_id, rake_amount ); + FC_ASSERT( rake_amount.value >= affiliates_share.value ); + if (rake_amount.value > affiliates_share.value) + adjust_balance(*rake_account_id, asset(rake_amount - affiliates_share, betting_market_group.asset_id)); } // pay winning - rake @@ -300,6 +307,8 @@ void database::settle_betting_market_group(const betting_market_group_object& be fc_dlog(fc::logger::get("betting"), "removing betting market group ${id}", ("id", betting_market_group.id)); remove(betting_market_group); + + payout_helper.commit(); } void database::remove_completed_events() diff --git a/libraries/chain/db_block.cpp b/libraries/chain/db_block.cpp index b63dfde4..bcf79cfb 100644 --- a/libraries/chain/db_block.cpp +++ b/libraries/chain/db_block.cpp @@ -658,13 +658,10 @@ operation_result database::apply_operation(transaction_evaluation_state& eval_st { try { int i_which = op.which(); uint64_t u_which = uint64_t( i_which ); - if( i_which < 0 ) - assert( "Negative operation tag" && false ); - if( u_which >= _operation_evaluators.size() ) - assert( "No registered evaluator for this operation" && false ); + FC_ASSERT( i_which >= 0, "Negative operation tag in operation ${op}", ("op",op) ); + FC_ASSERT( u_which < _operation_evaluators.size(), "No registered evaluator for operation ${op}", ("op",op) ); unique_ptr& eval = _operation_evaluators[ u_which ]; - if( !eval ) - assert( "No registered evaluator for this operation" && false ); + FC_ASSERT( eval, "No registered evaluator for operation ${op}", ("op",op) ); auto op_id = push_applied_operation( op ); auto result = eval->evaluate( eval_state, op, true ); set_applied_operation_result( op_id, result ); diff --git a/libraries/chain/db_notify.cpp b/libraries/chain/db_notify.cpp index 9b7fc250..1251e091 100644 --- a/libraries/chain/db_notify.cpp +++ b/libraries/chain/db_notify.cpp @@ -262,6 +262,11 @@ struct get_impacted_account_visitor { _impacted.insert( op.payout_account_id ); } + void operator()( const affiliate_payout_operation& op ) + { + _impacted.insert( op.affiliate ); + } + void operator()( const affiliate_referral_payout_operation& op ) { } }; void operation_get_impacted_accounts( const operation& op, flat_set& result ) diff --git a/libraries/chain/hardfork.d/999.hf b/libraries/chain/hardfork.d/999.hf new file mode 100644 index 00000000..55b99fc1 --- /dev/null +++ b/libraries/chain/hardfork.d/999.hf @@ -0,0 +1,4 @@ +// Placeholder HF for affiliate reward system +#ifndef HARDFORK_999_TIME +#define HARDFORK_999_TIME (fc::time_point_sec( 1600000000 )) +#endif diff --git a/libraries/chain/include/graphene/chain/account_object.hpp b/libraries/chain/include/graphene/chain/account_object.hpp index 333168a8..51ba6748 100644 --- a/libraries/chain/include/graphene/chain/account_object.hpp +++ b/libraries/chain/include/graphene/chain/account_object.hpp @@ -227,6 +227,8 @@ namespace graphene { namespace chain { */ optional< flat_set > allowed_assets; + optional< affiliate_reward_distributions > affiliate_distributions; + bool has_special_authority()const { return (owner_special_authority.which() != special_authority::tag< no_special_authority >::value) @@ -446,7 +448,7 @@ FC_REFLECT_DERIVED( graphene::chain::account_object, (cashback_vb) (owner_special_authority)(active_special_authority) (top_n_control_flags) - (allowed_assets) + (allowed_assets)(affiliate_distributions) ) FC_REFLECT_DERIVED( graphene::chain::account_balance_object, diff --git a/libraries/chain/include/graphene/chain/affiliate_payout.hpp b/libraries/chain/include/graphene/chain/affiliate_payout.hpp new file mode 100644 index 00000000..1c0ea703 --- /dev/null +++ b/libraries/chain/include/graphene/chain/affiliate_payout.hpp @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2018 Peerplays Blockchain Standards Association, and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include +#include +#include + +namespace graphene { namespace chain { + class database; + + namespace impl { + class game_type_visitor { + public: + typedef app_tag result_type; + + inline app_tag operator()( const rock_paper_scissors_game_options& o )const { return rps; } + }; + } + + template + app_tag get_tag_for_game( const GAME& game ); + + template<> + inline app_tag get_tag_for_game( const betting_market_group_object& game ) + { + return bookie; + } + + template<> + inline app_tag get_tag_for_game( const tournament_object& game ) + { + return game.options.game_options.visit( impl::game_type_visitor() ); + } + + template + asset_id_type get_asset_for_game( const GAME& game ); + + template<> + inline asset_id_type get_asset_for_game( const betting_market_group_object& game ) + { + return game.asset_id; + } + + template<> + inline asset_id_type get_asset_for_game( const tournament_object& game ) + { + return game.options.buy_in.asset_id; + } + + class affiliate_payout_helper { + public: + template + affiliate_payout_helper( database& db, const GAME& game ) + : _db(db), tag( get_tag_for_game( game ) ), payout_asset( get_asset_for_game( game ) ) {} + + share_type payout( account_id_type player, share_type amount ); + share_type payout( const account_object& player, share_type amount ); + void commit(); + + private: + database& _db; + app_tag tag; + asset_id_type payout_asset; + std::map accumulator; + }; + +} } // graphene::chain diff --git a/libraries/chain/include/graphene/chain/config.hpp b/libraries/chain/include/graphene/chain/config.hpp index 5df38771..c8d7966f 100644 --- a/libraries/chain/include/graphene/chain/config.hpp +++ b/libraries/chain/include/graphene/chain/config.hpp @@ -151,7 +151,7 @@ #define GRAPHENE_RECENTLY_MISSED_COUNT_INCREMENT 4 #define GRAPHENE_RECENTLY_MISSED_COUNT_DECREMENT 3 -#define GRAPHENE_CURRENT_DB_VERSION "PPY1.11" +#define GRAPHENE_CURRENT_DB_VERSION "PPY_999" #define GRAPHENE_IRREVERSIBLE_THRESHOLD (70 * GRAPHENE_1_PERCENT) diff --git a/libraries/chain/include/graphene/chain/protocol/account.hpp b/libraries/chain/include/graphene/chain/protocol/account.hpp index 93dbb115..6d13a4d3 100644 --- a/libraries/chain/include/graphene/chain/protocol/account.hpp +++ b/libraries/chain/include/graphene/chain/protocol/account.hpp @@ -60,6 +60,21 @@ namespace graphene { namespace chain { void validate()const; }; + enum app_tag { + bookie = 0, + rps = 1 + }; + struct affiliate_reward_distribution + { + fc::flat_map _dist; + void validate()const; + }; + struct affiliate_reward_distributions + { + fc::flat_map _dists; + void validate()const; + }; + /** * @ingroup operations */ @@ -71,6 +86,7 @@ namespace graphene { namespace chain { optional< special_authority > owner_special_authority; optional< special_authority > active_special_authority; optional< buyback_account_options > buyback_options; + optional< affiliate_reward_distributions > affiliate_distributions; }; struct fee_parameters_type @@ -268,6 +284,10 @@ FC_REFLECT(graphene::chain::account_options, (memo_key)(voting_account)(num_witn FC_REFLECT_ENUM( graphene::chain::account_whitelist_operation::account_listing, (no_listing)(white_listed)(black_listed)(white_and_black_listed)) +FC_REFLECT_ENUM( graphene::chain::app_tag, (bookie)(rps) ) +FC_REFLECT( graphene::chain::affiliate_reward_distribution, (_dist) ); +FC_REFLECT( graphene::chain::affiliate_reward_distributions, (_dists) ); + FC_REFLECT(graphene::chain::account_create_operation::ext, (null_ext)(owner_special_authority)(active_special_authority)(buyback_options) ) FC_REFLECT( graphene::chain::account_create_operation, (fee)(registrar) diff --git a/libraries/chain/include/graphene/chain/protocol/affiliate.hpp b/libraries/chain/include/graphene/chain/protocol/affiliate.hpp new file mode 100644 index 00000000..b2c3ee88 --- /dev/null +++ b/libraries/chain/include/graphene/chain/protocol/affiliate.hpp @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2018 Peerplays Blockchain Standards Association, and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once +#include +#include +#include + +namespace graphene { namespace chain { + + /** + * Virtual op generated when an affiliate receives payout. + */ + + struct affiliate_payout_operation : public base_operation + { + affiliate_payout_operation(){} + affiliate_payout_operation( account_id_type a, app_tag t, const asset& amount ) + : affiliate(a), tag(t), payout(amount) {} + + struct fee_parameters_type { }; + asset fee; + + // Account of the receiving affiliate + account_id_type affiliate; + // App-tag for which the payout was generated + app_tag tag; + // Payout amount + asset payout; + + account_id_type fee_payer()const { return affiliate; } + void validate()const { + FC_ASSERT( false, "Virtual operation" ); + } + + share_type calculate_fee(const fee_parameters_type& params)const + { return 0; } + }; + + /** + * Virtual op generated when a player generates an affiliate payout + */ + struct affiliate_referral_payout_operation : public base_operation + { + affiliate_referral_payout_operation(){} + affiliate_referral_payout_operation( account_id_type p, const asset& amount ) + : player(p), payout(amount) {} + + struct fee_parameters_type { }; + asset fee; + + // Account of the winning player + account_id_type player; + // Payout amount + asset payout; + + account_id_type fee_payer()const { return player; } + void validate()const { + FC_ASSERT( false, "virtual operation" ); + } + + share_type calculate_fee(const fee_parameters_type& params)const + { return 0; } + }; + +} } // graphene::chain + +FC_REFLECT( graphene::chain::affiliate_payout_operation::fee_parameters_type, ) +FC_REFLECT( graphene::chain::affiliate_referral_payout_operation::fee_parameters_type, ) + +FC_REFLECT( graphene::chain::affiliate_payout_operation, (fee)(affiliate)(tag)(payout) ) +FC_REFLECT( graphene::chain::affiliate_referral_payout_operation, (fee)(player)(payout) ) diff --git a/libraries/chain/include/graphene/chain/protocol/operations.hpp b/libraries/chain/include/graphene/chain/protocol/operations.hpp index 58cc2cba..4fac8992 100644 --- a/libraries/chain/include/graphene/chain/protocol/operations.hpp +++ b/libraries/chain/include/graphene/chain/protocol/operations.hpp @@ -24,6 +24,7 @@ #pragma once #include #include +#include #include #include #include @@ -124,7 +125,9 @@ namespace graphene { namespace chain { bet_canceled_operation, // VIRTUAL betting_market_group_update_operation, betting_market_update_operation, - event_update_status_operation + event_update_status_operation, + affiliate_payout_operation, // VIRTUAL + affiliate_referral_payout_operation // VIRTUAL > operation; /// @} // operations group diff --git a/libraries/chain/proposal_evaluator.cpp b/libraries/chain/proposal_evaluator.cpp index 2eada590..a6640ea4 100644 --- a/libraries/chain/proposal_evaluator.cpp +++ b/libraries/chain/proposal_evaluator.cpp @@ -25,8 +25,11 @@ #include #include #include +#include #include +#include #include +#include #include @@ -52,8 +55,20 @@ struct proposal_operation_hardfork_visitor "Parameter extensions are not allowed yet!" ); } - void operator()(const sport_create_operation &v) const { - FC_ASSERT( block_time >= HARDFORK_1000_TIME, "sport_create_operation not allowed yet!" ); + void operator()(const graphene::chain::tournament_payout_operation &o) const { + // TODO: move check into tournament_payout_operation::validate after HARDFORK_999_TIME + FC_ASSERT( block_time < HARDFORK_999_TIME, "Not allowed!" ); + } + + void operator()(const graphene::chain::asset_settle_cancel_operation &o) const { + // TODO: move check into asset_settle_cancel_operation::validate after HARDFORK_999_TIME + FC_ASSERT( block_time < HARDFORK_999_TIME, "Not allowed!" ); + } + + void operator()(const graphene::chain::account_create_operation &aco) const { + // TODO: remove after HARDFORK_999_TIME + if (block_time < HARDFORK_999_TIME) + FC_ASSERT( !aco.extensions.value.affiliate_distributions.valid(), "Affiliate reward distributions not allowed yet" ); } void operator()(const sport_update_operation &v) const { diff --git a/libraries/chain/protocol/account.cpp b/libraries/chain/protocol/account.cpp index 3aed8fb3..cf592d5c 100644 --- a/libraries/chain/protocol/account.cpp +++ b/libraries/chain/protocol/account.cpp @@ -182,6 +182,27 @@ void account_options::validate() const "May not specify fewer witnesses or committee members than the number voted for."); } +void affiliate_reward_distribution::validate() const +{ + // sum of weights must equal 100% + uint32_t sum = 0; + for( const auto& share : _dist ) + { + FC_ASSERT( share.second > 0, "Must leave out affilates who receive 0%!" ); + FC_ASSERT( share.second <= GRAPHENE_100_PERCENT, "Can't pay out more than 100% per affiliate!" ); + sum += share.second; + FC_ASSERT( sum <= GRAPHENE_100_PERCENT, "Can't pay out more than 100% total!" ); + } + FC_ASSERT( sum == GRAPHENE_100_PERCENT, "Total affiliate distributions must cover 100%!" ); +} + +void affiliate_reward_distributions::validate() const +{ + FC_ASSERT( !_dists.empty(), "Empty affiliate reward distributions not allowed!" ); + for( const auto& dist: _dists ) + dist.second.validate(); +} + share_type account_create_operation::calculate_fee( const fee_parameters_type& k )const { auto core_fee_required = k.basic_fee; @@ -226,6 +247,8 @@ void account_create_operation::validate()const FC_ASSERT( m != extensions.value.buyback_options->asset_to_buy ); } } + if( extensions.value.affiliate_distributions.valid() ) + extensions.value.affiliate_distributions->validate(); } diff --git a/libraries/chain/tournament_object.cpp b/libraries/chain/tournament_object.cpp index 86049b81..c1b53f79 100644 --- a/libraries/chain/tournament_object.cpp +++ b/libraries/chain/tournament_object.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -311,10 +312,8 @@ namespace graphene { namespace chain { const account_id_type& winner = *event.match.match_winners.begin(); uint16_t rake_fee_percentage = event.db.get_global_properties().parameters.rake_fee_percentage; - // check whether the core asset pays dividends. If so, we transfer the rake fee - // to the core asset's dividend account - const asset_object& core_asset_obj = asset_id_type()(event.db); - optional dividend_id = core_asset_obj.dividend_data_id; + const asset_object & asset_obj = asset_id_type(0)(event.db); + optional dividend_id = asset_obj.dividend_data_id; share_type rake_amount = 0; if (dividend_id) @@ -335,7 +334,15 @@ namespace graphene { namespace chain { event.db.push_applied_operation(op); } - if (dividend_id && rake_amount.value) + if (rake_amount.value) + { + affiliate_payout_helper payout_helper( event.db, tournament_obj ); + rake_amount -= payout_helper.payout( winner, rake_amount ); + payout_helper.commit(); + FC_ASSERT( rake_amount.value >= 0 ); + } + + if (rake_amount.value) { // Adjusting balance of dividend_distribution_account const asset_dividend_data_id_type& asset_dividend_data_id_= *dividend_id; diff --git a/libraries/plugins/CMakeLists.txt b/libraries/plugins/CMakeLists.txt index 8bf98141..01079fe2 100644 --- a/libraries/plugins/CMakeLists.txt +++ b/libraries/plugins/CMakeLists.txt @@ -1,6 +1,7 @@ add_subdirectory( witness ) add_subdirectory( account_history ) add_subdirectory( accounts_list ) +add_subdirectory( affiliate_stats ) add_subdirectory( market_history ) add_subdirectory( delayed_node ) add_subdirectory( bookie ) diff --git a/libraries/plugins/account_history/include/graphene/account_history/account_history_plugin.hpp b/libraries/plugins/account_history/include/graphene/account_history/account_history_plugin.hpp index ef89c488..784c7e45 100644 --- a/libraries/plugins/account_history/include/graphene/account_history/account_history_plugin.hpp +++ b/libraries/plugins/account_history/include/graphene/account_history/account_history_plugin.hpp @@ -81,6 +81,17 @@ class account_history_plugin : public graphene::app::plugin std::unique_ptr my; }; +class affiliate_reward_index : public secondary_index +{ + public: + 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{}; + + map > _history_by_account; +}; + } } //graphene::account_history /*struct by_id; diff --git a/libraries/plugins/affiliate_stats/CMakeLists.txt b/libraries/plugins/affiliate_stats/CMakeLists.txt new file mode 100644 index 00000000..fec2544c --- /dev/null +++ b/libraries/plugins/affiliate_stats/CMakeLists.txt @@ -0,0 +1,24 @@ +file(GLOB HEADERS "include/graphene/affiliate_stats/*.hpp") + +add_library( graphene_affiliate_stats + affiliate_stats_api.cpp + affiliate_stats_plugin.cpp + ) + +target_link_libraries( graphene_affiliate_stats graphene_chain graphene_app ) +target_include_directories( graphene_affiliate_stats + PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" ) + +if(MSVC) + set_source_files_properties( affiliate_stats_plugin.cpp PROPERTIES COMPILE_FLAGS "/bigobj" ) +endif(MSVC) + +install( TARGETS + graphene_affiliate_stats + + RUNTIME DESTINATION bin + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib +) +INSTALL( FILES ${HEADERS} DESTINATION "include/graphene/affiliate_stats" ) + diff --git a/libraries/plugins/affiliate_stats/affiliate_stats_api.cpp b/libraries/plugins/affiliate_stats/affiliate_stats_api.cpp new file mode 100644 index 00000000..d37a0360 --- /dev/null +++ b/libraries/plugins/affiliate_stats/affiliate_stats_api.cpp @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2018 Peerplays Blockchain Standards Association, and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include + +#include + +#include +#include +#include + +#include + +#include + +#include + +#include + +namespace graphene { namespace affiliate_stats { + +namespace detail { + +class affiliate_stats_api_impl +{ + public: + affiliate_stats_api_impl(graphene::app::application& _app); + + std::vector list_top_referred_accounts( asset_id_type asset, uint16_t limit )const + { + std::vector result; + result.reserve( limit ); + auto& idx = app.chain_database()->get_index_type().indices().get(); + auto itr = idx.lower_bound( boost::make_tuple( asset, share_type(GRAPHENE_MAX_SHARE_SUPPLY) ) ); + while( itr != idx.end() && itr->get_asset_id() == asset && limit-- > 0 ) + result.push_back( *itr++ ); + return result; + } + + std::vector list_top_rewards_per_app( asset_id_type asset, uint16_t limit )const + { + std::vector result; + result.reserve( limit ); + auto& idx = app.chain_database()->get_index_type().indices().get(); + auto itr = idx.lower_bound( boost::make_tuple( asset, share_type(GRAPHENE_MAX_SHARE_SUPPLY) ) ); + while( itr != idx.end() && itr->get_asset_id() == asset && limit-- > 0 ) + result.push_back( *itr++ ); + return result; + } + + std::vector list_historic_referral_rewards( account_id_type affiliate, operation_history_id_type start, uint16_t limit )const + { + shared_ptr plugin = app.get_plugin( "affiliate_stats" ); + + std::vector result; + const auto& list = plugin->get_reward_history( affiliate ); + result.reserve( limit ); + auto inner = list.lower_bound( start ); + while( inner != list.end() && result.size() < limit ) + result.push_back( referral_payment( (*inner++)(*app.chain_database()) ) ); + return result; + } + + graphene::app::application& app; +}; + +affiliate_stats_api_impl::affiliate_stats_api_impl(graphene::app::application& _app) + : app(_app) {} + +} // detail + +top_referred_account::top_referred_account() {} + +top_referred_account::top_referred_account( const referral_reward_object& rro ) + : referral( rro.referral ), total_payout( rro.total_payout ) {} + +top_app::top_app() {} + +top_app::top_app( const app_reward_object& aro ) + : app( aro.app ), total_payout( aro.total_payout ) {} + + +affiliate_stats_api::affiliate_stats_api(graphene::app::application& app) + : my(std::make_shared(app)) {} + +std::vector affiliate_stats_api::list_top_referred_accounts( asset_id_type asset, uint16_t limit )const +{ + FC_ASSERT( limit <= 100 ); + return my->list_top_referred_accounts( asset, limit ); +} + +std::vector affiliate_stats_api::list_top_rewards_per_app( asset_id_type asset, uint16_t limit )const +{ + FC_ASSERT( limit <= 100 ); + return my->list_top_rewards_per_app( asset, limit ); +} + +std::vector affiliate_stats_api::list_historic_referral_rewards( account_id_type affiliate, operation_history_id_type start, uint16_t limit )const +{ + FC_ASSERT( limit <= 100 ); + return my->list_historic_referral_rewards( affiliate, start, limit ); +} + + +referral_payment::referral_payment() {} + +referral_payment::referral_payment( const operation_history_object& oho ) + : id(oho.id), block_num(oho.block_num), tag(oho.op.get().tag), + payout(oho.op.get().payout) {} + +} } // graphene::affiliate_stats + + diff --git a/libraries/plugins/affiliate_stats/affiliate_stats_plugin.cpp b/libraries/plugins/affiliate_stats/affiliate_stats_plugin.cpp new file mode 100644 index 00000000..438b1aca --- /dev/null +++ b/libraries/plugins/affiliate_stats/affiliate_stats_plugin.cpp @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2018 Peerplays Blockchain Standards Association, and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace graphene { namespace affiliate_stats { + +namespace detail { + +class affiliate_reward_index : public graphene::db::index_observer +{ + public: + affiliate_reward_index( graphene::chain::database& _db ) : db(_db) {} + virtual void on_add( const graphene::db::object& obj ) override; + virtual void on_remove( const graphene::db::object& obj ) override; + virtual void on_modify( const graphene::db::object& before ) override{}; + + std::map > _history_by_account; + private: + graphene::chain::database& db; +}; + +class affiliate_stats_plugin_impl +{ + public: + affiliate_stats_plugin_impl(affiliate_stats_plugin& _plugin) + : _self( _plugin ) { } + virtual ~affiliate_stats_plugin_impl(); + + /** this method is called as a callback after a block is applied + * and will process/index all operations that were applied in the block. + */ + void update_affiliate_stats( const signed_block& b ); + + graphene::chain::database& database() + { + return _self.database(); + } + + const std::set& get_reward_history( account_id_type& affiliate )const; + + typedef void result_type; + template + void operator()( const Operation& op ) {} + + shared_ptr _fr_index; + affiliate_stats_plugin& _self; + app_reward_index* _ar_index; + referral_reward_index* _rr_index; + private: +}; + +affiliate_stats_plugin_impl::~affiliate_stats_plugin_impl() {} + +template<> +void affiliate_stats_plugin_impl::operator()( const affiliate_payout_operation& op ) +{ + auto& by_app = _ar_index->indices().get(); + auto itr = by_app.find( boost::make_tuple( op.tag, op.payout.asset_id ) ); + if( itr == by_app.end() ) + { + database().create( [&op]( app_reward_object& aro ) { + aro.app = op.tag; + aro.total_payout = op.payout; + }); + } + else + { + database().modify( *itr, [&op]( app_reward_object& aro ) { + aro.total_payout += op.payout; + }); + } +} + +template<> +void affiliate_stats_plugin_impl::operator()( const affiliate_referral_payout_operation& op ) +{ + auto& by_referral = _rr_index->indices().get(); + auto itr = by_referral.find( boost::make_tuple( op.player, op.payout.asset_id ) ); + if( itr == by_referral.end() ) + { + database().create( [&op]( referral_reward_object& rro ) { + rro.referral = op.player; + rro.total_payout = op.payout; + }); + } + else + { + database().modify( *itr, [&op]( referral_reward_object& rro ) { + rro.total_payout += op.payout; + }); + } +} + +void affiliate_stats_plugin_impl::update_affiliate_stats( const signed_block& b ) +{ + vector >& hist = database().get_applied_operations(); + for( optional< operation_history_object >& o_op : hist ) + { + if( !o_op.valid() ) + continue; + + o_op->op.visit( *this ); + } +} + +static const std::set EMPTY; +const std::set& affiliate_stats_plugin_impl::get_reward_history( account_id_type& affiliate )const +{ + auto itr = _fr_index->_history_by_account.find( affiliate ); + if( itr == _fr_index->_history_by_account.end() ) + return EMPTY; + return itr->second; +} + + +static optional> get_account( const database& db, const object& obj ) +{ + FC_ASSERT( dynamic_cast(&obj) ); + const account_transaction_history_object& ath = static_cast(obj); + const operation_history_object& oho = db.get( ath.operation_id ); + if( oho.op.which() == operation::tag::value ) + return std::make_pair( ath.account, ath.operation_id ); + return optional>(); +} + +void affiliate_reward_index::on_add( const object& obj ) +{ + optional> acct_ath = get_account( db, obj ); + if( !acct_ath.valid() ) return; + _history_by_account[acct_ath->first].insert( acct_ath->second ); +} + +void affiliate_reward_index::on_remove( const object& obj ) +{ + optional> acct_ath = get_account( db, obj ); + if( !acct_ath.valid() ) return; + _history_by_account[acct_ath->first].erase( acct_ath->second ); +} + +} // end namespace detail + +affiliate_stats_plugin::affiliate_stats_plugin() + : my( new detail::affiliate_stats_plugin_impl(*this) ) {} + +affiliate_stats_plugin::~affiliate_stats_plugin() {} + +std::string affiliate_stats_plugin::plugin_name()const +{ + return "affiliate_stats"; +} + +void affiliate_stats_plugin::plugin_set_program_options( + boost::program_options::options_description& cli, + boost::program_options::options_description& cfg + ) +{ + cli.add_options() + ; + cfg.add(cli); +} + +void affiliate_stats_plugin::plugin_initialize(const boost::program_options::variables_map& options) +{ + database().applied_block.connect( [this]( const signed_block& b){ my->update_affiliate_stats(b); } ); + + my->_ar_index = database().add_index< primary_index< app_reward_index > >(); + my->_rr_index = database().add_index< primary_index< referral_reward_index > >(); + my->_fr_index = shared_ptr( new detail::affiliate_reward_index( database() ) ); + const_cast&>(database().get_index_type>()).add_observer( my->_fr_index ); +} + +void affiliate_stats_plugin::plugin_startup() {} + +const std::set& affiliate_stats_plugin::get_reward_history( account_id_type& affiliate )const +{ + return my->get_reward_history( affiliate ); +} + +} } // graphene::affiliate_stats diff --git a/libraries/plugins/affiliate_stats/include/graphene/affiliate_stats/affiliate_stats_api.hpp b/libraries/plugins/affiliate_stats/include/graphene/affiliate_stats/affiliate_stats_api.hpp new file mode 100644 index 00000000..29c45d9f --- /dev/null +++ b/libraries/plugins/affiliate_stats/include/graphene/affiliate_stats/affiliate_stats_api.hpp @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2018 Peerplays Blockchain Standards Association, and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include + +using namespace graphene::chain; + +namespace graphene { namespace app { + class application; +} } + +namespace graphene { namespace affiliate_stats { + +namespace detail { + class affiliate_stats_api_impl; +} + +class referral_payment { +public: + referral_payment(); + referral_payment( const operation_history_object& oho ); + operation_history_id_type id; + uint32_t block_num; + app_tag tag; + asset payout; +}; + +class top_referred_account { +public: + top_referred_account(); + top_referred_account( const referral_reward_object& rro ); + + account_id_type referral; + asset total_payout; +}; + +class top_app { +public: + top_app(); + top_app( const app_reward_object& rro ); + + app_tag app; + asset total_payout; +}; + +class affiliate_stats_api +{ + public: + affiliate_stats_api(graphene::app::application& app); + + std::vector list_historic_referral_rewards( account_id_type affiliate, operation_history_id_type start, uint16_t limit = 100 )const; + // get_pending_referral_reward() - not implemented because we have continuous payouts + // get_previous_referral_reward() - not implemented because we have continuous payouts + std::vector list_top_referred_accounts( asset_id_type asset, uint16_t limit = 100 )const; + std::vector list_top_rewards_per_app( asset_id_type asset, uint16_t limit = 100 )const; + + std::shared_ptr my; +}; + +} } // graphene::affiliate_stats + +FC_REFLECT(graphene::affiliate_stats::referral_payment, (id)(block_num)(tag)(payout) ) +FC_REFLECT(graphene::affiliate_stats::top_referred_account, (referral)(total_payout) ) +FC_REFLECT(graphene::affiliate_stats::top_app, (app)(total_payout) ) + +FC_API(graphene::affiliate_stats::affiliate_stats_api, + (list_historic_referral_rewards) + (list_top_referred_accounts) + (list_top_rewards_per_app) + ) diff --git a/libraries/plugins/affiliate_stats/include/graphene/affiliate_stats/affiliate_stats_objects.hpp b/libraries/plugins/affiliate_stats/include/graphene/affiliate_stats/affiliate_stats_objects.hpp new file mode 100644 index 00000000..1dc70276 --- /dev/null +++ b/libraries/plugins/affiliate_stats/include/graphene/affiliate_stats/affiliate_stats_objects.hpp @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2018 Peerplays Blockchain Standards Association, and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once +#include +#include + +namespace graphene { namespace affiliate_stats { +using namespace chain; + +enum stats_object_type +{ + app_reward_object_type, + referral_reward_object_type, + STATS_OBJECT_TYPE_COUNT ///< Sentry value which contains the number of different object types +}; + +class app_reward_object : public graphene::db::abstract_object +{ +public: + static const uint8_t space_id = AFFILIATE_STATS_SPACE_ID; + static const uint8_t type_id = app_reward_object_type; + + app_tag app; + asset total_payout; + + inline share_type get_amount()const { return total_payout.amount; } + inline asset_id_type get_asset_id()const { return total_payout.asset_id; } +}; + +typedef object_id app_reward_id_type; + +struct by_asset; +struct by_app_asset; +typedef multi_index_container< + app_reward_object, + indexed_by< + ordered_unique, member >, + ordered_non_unique, + composite_key< + app_reward_object, + const_mem_fun, + const_mem_fun >, + composite_key_compare< + std::less, + std::greater > + >, + ordered_unique, + composite_key< + app_reward_object, + member, + const_mem_fun > + > > > app_reward_multi_index_type; +typedef generic_index app_reward_index; + +class referral_reward_object : public graphene::db::abstract_object +{ +public: + static const uint8_t space_id = AFFILIATE_STATS_SPACE_ID; + static const uint8_t type_id = referral_reward_object_type; + + account_id_type referral; + asset total_payout; + + inline share_type get_amount()const { return total_payout.amount; } + inline asset_id_type get_asset_id()const { return total_payout.asset_id; } +}; + +typedef object_id referral_reward_id_type; + +struct by_referral_asset; +typedef multi_index_container< + referral_reward_object, + indexed_by< + ordered_unique, member >, + ordered_non_unique, + composite_key< + referral_reward_object, + const_mem_fun, + const_mem_fun >, + composite_key_compare< + std::less, + std::greater > + >, + ordered_unique, + composite_key< + referral_reward_object, + member, + const_mem_fun > + > > > referral_reward_multi_index_type; +typedef generic_index referral_reward_index; + +} } //graphene::affiliate_stats + +FC_REFLECT_DERIVED( graphene::affiliate_stats::app_reward_object, (graphene::db::object), (app)(total_payout) ) +FC_REFLECT_DERIVED( graphene::affiliate_stats::referral_reward_object, (graphene::db::object), (referral)(total_payout) ) + diff --git a/libraries/plugins/affiliate_stats/include/graphene/affiliate_stats/affiliate_stats_plugin.hpp b/libraries/plugins/affiliate_stats/include/graphene/affiliate_stats/affiliate_stats_plugin.hpp new file mode 100644 index 00000000..2d60f8ca --- /dev/null +++ b/libraries/plugins/affiliate_stats/include/graphene/affiliate_stats/affiliate_stats_plugin.hpp @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2018 Peerplays Blockchain Standards Association, and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#pragma once + +#include +#include + +#include + +namespace graphene { namespace affiliate_stats { + using namespace chain; + +// +// Plugins should #define their SPACE_ID's so plugins with +// conflicting SPACE_ID assignments can be compiled into the +// same binary (by simply re-assigning some of the conflicting #defined +// SPACE_ID's in a build script). +// +// Assignment of SPACE_ID's cannot be done at run-time because +// various template automagic depends on them being known at compile +// time. +// +#ifndef AFFILIATE_STATS_SPACE_ID +#define AFFILIATE_STATS_SPACE_ID 7 +#endif + +namespace detail +{ + class affiliate_stats_plugin_impl; +} + +class affiliate_stats_plugin : public graphene::app::plugin +{ + public: + affiliate_stats_plugin(); + virtual ~affiliate_stats_plugin(); + + std::string plugin_name()const override; + virtual void plugin_set_program_options( + boost::program_options::options_description& cli, + boost::program_options::options_description& cfg) override; + virtual void plugin_initialize(const boost::program_options::variables_map& options) override; + virtual void plugin_startup() override; + + const std::set& get_reward_history( account_id_type& affiliate )const; + + friend class detail::affiliate_stats_plugin_impl; + std::unique_ptr my; +}; + +} } //graphene::affiliate_stats diff --git a/programs/witness_node/CMakeLists.txt b/programs/witness_node/CMakeLists.txt index b3fa5c88..3d03253b 100644 --- a/programs/witness_node/CMakeLists.txt +++ b/programs/witness_node/CMakeLists.txt @@ -11,7 +11,7 @@ endif() # We have to link against graphene_debug_witness because deficiency in our API infrastructure doesn't allow plugins to be fully abstracted #246 target_link_libraries( witness_node - PRIVATE graphene_app graphene_account_history graphene_market_history graphene_witness graphene_chain graphene_debug_witness graphene_bookie graphene_egenesis_full graphene_snapshot fc ${CMAKE_DL_LIBS} ${PLATFORM_SPECIFIC_LIBS} ) + PRIVATE graphene_app graphene_account_history graphene_affiliate_stats graphene_market_history graphene_witness graphene_chain graphene_debug_witness graphene_bookie graphene_egenesis_full fc ${CMAKE_DL_LIBS} ${PLATFORM_SPECIFIC_LIBS} ) # also add dependencies to graphene_generate_genesis graphene_generate_uia_sharedrop_genesis if you want those plugins install( TARGETS diff --git a/programs/witness_node/main.cpp b/programs/witness_node/main.cpp index 426113c4..6f55d593 100644 --- a/programs/witness_node/main.cpp +++ b/programs/witness_node/main.cpp @@ -29,9 +29,10 @@ #include //#include //#include +#include #include #include -#include +//#include #include #include @@ -84,8 +85,9 @@ int main(int argc, char** argv) { //auto generate_genesis_plug = node->register_plugin(); //auto generate_uia_sharedrop_genesis_plug = node->register_plugin(); auto list_plug = node->register_plugin(); + auto affiliate_stats_plug = node->register_plugin(); auto bookie_plug = node->register_plugin(); - auto snapshot_plug = node->register_plugin(); +// auto snapshot_plug = node->register_plugin(); try { diff --git a/tests/betting/betting_tests.cpp b/tests/betting/betting_tests.cpp index 2d83f3d5..c192701b 100644 --- a/tests/betting/betting_tests.cpp +++ b/tests/betting/betting_tests.cpp @@ -28,7 +28,7 @@ #include #pragma GCC diagnostic pop -#include "../common/database_fixture.hpp" +#include "../common/betting_test_markets.hpp" #include #include @@ -41,14 +41,9 @@ #include #include -#include -#include -#include #include -#include #include -//#include struct enable_betting_logging_config { enable_betting_logging_config() @@ -1382,40 +1377,6 @@ BOOST_AUTO_TEST_CASE( cancel_one_event_in_group ) BOOST_AUTO_TEST_SUITE_END() -// set up a fixture that places a series of two matched bets, we'll use this fixture to verify -// the result in all three possible outcomes -struct simple_bet_test_fixture : database_fixture { - betting_market_id_type capitals_win_betting_market_id; - betting_market_id_type blackhawks_win_betting_market_id; - betting_market_group_id_type moneyline_betting_markets_id; - - simple_bet_test_fixture() - { - ACTORS( (alice)(bob) ); - CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); - - // give alice and bob 10k each - transfer(account_id_type(), alice_id, asset(10000)); - transfer(account_id_type(), bob_id, asset(10000)); - - // place bets at 10:1 - place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 11 * GRAPHENE_BETTING_ODDS_PRECISION); - place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000, asset_id_type()), 11 * GRAPHENE_BETTING_ODDS_PRECISION); - - // reverse positions at 1:1 - place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(1100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); - place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(1100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); - - 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); - } -}; - BOOST_FIXTURE_TEST_SUITE( simple_bet_tests, simple_bet_test_fixture ) BOOST_AUTO_TEST_CASE( win ) diff --git a/tests/common/betting_test_markets.hpp b/tests/common/betting_test_markets.hpp new file mode 100644 index 00000000..f67dc067 --- /dev/null +++ b/tests/common/betting_test_markets.hpp @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2015 Cryptonomex, Inc., and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "database_fixture.hpp" + +#include +#include +#include +#include + +using namespace graphene::chain; + +#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; + +// set up a fixture that places a series of two matched bets, we'll use this fixture to verify +// the result in all three possible outcomes +struct simple_bet_test_fixture : database_fixture { + betting_market_id_type capitals_win_betting_market_id; + betting_market_id_type blackhawks_win_betting_market_id; + betting_market_group_id_type moneyline_betting_markets_id; + + simple_bet_test_fixture() + { + ACTORS( (alice)(bob) ); + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + // give alice and bob 10k each + transfer(account_id_type(), alice_id, asset(10000)); + transfer(account_id_type(), bob_id, asset(10000)); + + // place bets at 10:1 + place_bet(alice_id, capitals_win_market.id, bet_type::back, asset(100, asset_id_type()), 11 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(bob_id, capitals_win_market.id, bet_type::lay, asset(1000, asset_id_type()), 11 * GRAPHENE_BETTING_ODDS_PRECISION); + + // reverse positions at 1:1 + place_bet(alice_id, capitals_win_market.id, bet_type::lay, asset(1100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(bob_id, capitals_win_market.id, bet_type::back, asset(1100, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + 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, graphene::chain::keywords::_status = betting_market_group_status::closed); + generate_blocks(1); + } +}; diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index 28c44564..4db37ea7 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include @@ -83,6 +84,7 @@ database_fixture::database_fixture() auto ahplugin = app.register_plugin(); auto mhplugin = app.register_plugin(); auto bookieplugin = app.register_plugin(); + auto affiliateplugin = app.register_plugin(); init_account_pub_key = init_account_priv_key.get_public_key(); boost::program_options::variables_map options; @@ -114,10 +116,13 @@ database_fixture::database_fixture() mhplugin->plugin_initialize(options); bookieplugin->plugin_set_app(&app); bookieplugin->plugin_initialize(options); + affiliateplugin->plugin_set_app(&app); + affiliateplugin->plugin_initialize(options); ahplugin->plugin_startup(); mhplugin->plugin_startup(); bookieplugin->plugin_startup(); + affiliateplugin->plugin_startup(); generate_block(); @@ -451,7 +456,7 @@ const asset_object& database_fixture::get_asset( const string& symbol )const { const auto& idx = db.get_index_type().indices().get(); const auto itr = idx.find(symbol); - assert( itr != idx.end() ); + FC_ASSERT( itr != idx.end() ); return *itr; } @@ -459,7 +464,7 @@ const account_object& database_fixture::get_account( const string& name )const { const auto& idx = db.get_index_type().indices().get(); const auto itr = idx.find(name); - assert( itr != idx.end() ); + FC_ASSERT( itr != idx.end() ); return *itr; } diff --git a/tests/common/tournament_helper.cpp b/tests/common/tournament_helper.cpp new file mode 100644 index 00000000..4cb27b08 --- /dev/null +++ b/tests/common/tournament_helper.cpp @@ -0,0 +1,433 @@ +/* + * Copyright (c) 2018 Peerplays Blockchain Standards Association, and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "tournament_helper.hpp" + +#include +#include +#include + +#include + +using namespace graphene::chain; + +tournaments_helper::tournaments_helper(database_fixture& df) : df(df) +{ + assets.insert(asset_id_type()); + current_asset_idx = 0; + optional dividend_account = get_asset_dividend_account(asset_id_type()); + if (dividend_account.valid()) + players.insert(*dividend_account); +} + +const std::set& tournaments_helper::list_tournaments()const +{ + return tournaments; +} + +const std::map> tournaments_helper::list_players_balances()const +{ + std::map> result; + for (account_id_type player_id: players) + { + for( asset_id_type asset_id: assets) + { + asset a = df.db.get_balance(player_id, asset_id); + result[player_id][a.asset_id] = a.amount; + } + } + return result; +} + +const std::map> tournaments_helper::get_players_fees()const +{ + return players_fees; +} + +void tournaments_helper::reset_players_fees() +{ + for (account_id_type player_id: players) + { + for( asset_id_type asset_id: assets) + { + players_fees[player_id][asset_id] = 0; + } + } +} + +void tournaments_helper::create_asset(const account_id_type& issuer_account_id, + const string& symbol, + uint8_t precision, + asset_options& common, + const fc::ecc::private_key& sig_priv_key) +{ + graphene::chain::database& db = df.db; + const chain_parameters& params = db.get_global_properties().parameters; + signed_transaction tx; + asset_create_operation op; + op.issuer = issuer_account_id; + op.symbol = symbol; + op.precision = precision; + op.common_options = common; + + tx.operations = {op}; + for( auto& op : tx.operations ) + db.current_fee_schedule().set_fee(op); + tx.validate(); + tx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3)); + df.sign(tx, sig_priv_key); + PUSH_TX(db, tx); + + assets.insert(asset_id_type(++current_asset_idx)); +} + +void tournaments_helper::update_dividend_asset(const asset_id_type asset_to_update_id, + dividend_asset_options new_options, + const fc::ecc::private_key& sig_priv_key) +{ + graphene::chain::database& db = df.db; + const chain_parameters& params = db.get_global_properties().parameters; + signed_transaction tx; + asset_update_dividend_operation update_op; + + update_op.issuer = asset_to_update_id(db).issuer; + update_op.asset_to_update = asset_to_update_id; + update_op.new_options = new_options; + + tx.operations = {update_op}; + for( auto& op : tx.operations ) + db.current_fee_schedule().set_fee(op); + tx.validate(); + tx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3)); + df.sign(tx, sig_priv_key); + PUSH_TX(db, tx); + + optional dividend_account = get_asset_dividend_account(asset_to_update_id); + if (dividend_account.valid()) + players.insert(*dividend_account); +} + +optional tournaments_helper::get_asset_dividend_account(const asset_id_type& asset_id)const +{ + graphene::chain::database& db = df.db; + optional result; + const asset_object& asset_obj = asset_id_type()(db); + + if (asset_obj.dividend_data_id.valid()) + { + const asset_dividend_data_object& dividend_data = (*asset_obj.dividend_data_id)(db); + result = dividend_data.dividend_distribution_account; + } + return result; +} + +const tournament_id_type tournaments_helper::create_tournament (const account_id_type& creator, + const fc::ecc::private_key& sig_priv_key, + asset buy_in, + uint32_t number_of_players, + uint32_t time_per_commit_move, + uint32_t time_per_reveal_move, + uint32_t number_of_wins, + int32_t registration_deadline, + uint32_t start_delay, + uint32_t round_delay, + bool insurance_enabled, + uint32_t number_of_gestures, + uint32_t start_time, + fc::optional > whitelist + ) +{ + if (current_tournament_idx.valid()) + current_tournament_idx = *current_tournament_idx + 1; + else + current_tournament_idx = 0; + + graphene::chain::database& db = df.db; + const chain_parameters& params = db.get_global_properties().parameters; + signed_transaction trx; + tournament_options options; + tournament_create_operation op; + rock_paper_scissors_game_options& game_options = options.game_options.get(); + + game_options.number_of_gestures = number_of_gestures; + game_options.time_per_commit_move = time_per_commit_move; + game_options.time_per_reveal_move = time_per_reveal_move; + game_options.insurance_enabled = insurance_enabled; + + options.registration_deadline = db.head_block_time() + fc::seconds(registration_deadline + *current_tournament_idx); + options.buy_in = buy_in; + options.number_of_players = number_of_players; + if (start_delay) + options.start_delay = start_delay; + if (start_time) + options.start_time = db.head_block_time() + fc::seconds(start_time); + options.round_delay = round_delay; + options.number_of_wins = number_of_wins; + if (whitelist.valid()) + options.whitelist = *whitelist; + + op.creator = creator; + op.options = options; + 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)); + df.sign(trx, sig_priv_key); + PUSH_TX(db, trx); + + tournament_id_type tournament_id = tournament_id_type(*current_tournament_idx); + tournaments.insert(tournament_id); + return tournament_id; +} + +void tournaments_helper::join_tournament(const tournament_id_type & tournament_id, + const account_id_type& player_id, + const account_id_type& payer_id, + const fc::ecc::private_key& sig_priv_key, + asset buy_in + ) +{ + graphene::chain::database& db = df.db; + const chain_parameters& params = db.get_global_properties().parameters; + signed_transaction tx; + tournament_join_operation op; + + op.payer_account_id = payer_id; + op.buy_in = buy_in; + op.player_account_id = player_id; + op.tournament_id = tournament_id; + tx.operations = {op}; + for( auto& op : tx.operations ) + db.current_fee_schedule().set_fee(op); + tx.validate(); + tx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3)); + df.sign(tx, sig_priv_key); + PUSH_TX(db, tx); + + players.insert(player_id); + players_keys[player_id] = sig_priv_key; +} + +void tournaments_helper::leave_tournament(const tournament_id_type & tournament_id, + const account_id_type& player_id, + const account_id_type& canceling_account_id, + const fc::ecc::private_key& sig_priv_key + ) +{ + graphene::chain::database& db = df.db; + const chain_parameters& params = db.get_global_properties().parameters; + signed_transaction tx; + tournament_leave_operation op; + + op.canceling_account_id = canceling_account_id; + op.player_account_id = player_id; + op.tournament_id = tournament_id; + tx.operations = {op}; + for( auto& op : tx.operations ) + db.current_fee_schedule().set_fee(op); + tx.validate(); + tx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3)); + df.sign(tx, sig_priv_key); + PUSH_TX(db, tx); + + //players.erase(player_id); +} + +// stolen from cli_wallet +void tournaments_helper::rps_throw(const game_id_type& game_id, + const account_id_type& player_id, + rock_paper_scissors_gesture gesture, + const fc::ecc::private_key& sig_priv_key + ) +{ + + graphene::chain::database& db = df.db; + const chain_parameters& params = db.get_global_properties().parameters; + + // check whether the gesture is appropriate for the game we're playing + game_object game_obj = game_id(db); + match_object match_obj = game_obj.match_id(db); + tournament_object tournament_obj = match_obj.tournament_id(db); + rock_paper_scissors_game_options game_options = tournament_obj.options.game_options.get(); + FC_ASSERT( (int)gesture < game_options.number_of_gestures ); + + account_object player_account_obj = player_id(db); + + // construct the complete throw, the commit, and reveal + rock_paper_scissors_throw full_throw; + fc::rand_bytes((char*)&full_throw.nonce1, sizeof(full_throw.nonce1)); + fc::rand_bytes((char*)&full_throw.nonce2, sizeof(full_throw.nonce2)); + full_throw.gesture = gesture; + + rock_paper_scissors_throw_commit commit_throw; + commit_throw.nonce1 = full_throw.nonce1; + std::vector full_throw_packed(fc::raw::pack(full_throw)); + commit_throw.throw_hash = fc::sha256::hash(full_throw_packed.data(), full_throw_packed.size()); + + rock_paper_scissors_throw_reveal reveal_throw; + reveal_throw.nonce2 = full_throw.nonce2; + reveal_throw.gesture = full_throw.gesture; + + // store off the reveal for applying after both players commit + committed_game_moves[commit_throw] = reveal_throw; + latest_committs[player_account_obj.id] = commit_throw; + + signed_transaction tx; + game_move_operation move_operation; + move_operation.game_id = game_id; + move_operation.player_account_id = player_account_obj.id; + move_operation.move = commit_throw; + tx.operations = {move_operation}; + for( operation& op : tx.operations ) + { + asset f = db.current_fee_schedule().set_fee(op); + players_fees[player_id][f.asset_id] -= f.amount; + } + tx.validate(); + tx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3)); + df.sign(tx, sig_priv_key); + if (/*match_obj.match_winners.empty() &&*/ game_obj.get_state() == game_state::expecting_commit_moves) // checking again + PUSH_TX(db, tx); +} + +void tournaments_helper::rps_reveal( const game_id_type& game_id, + const account_id_type& player_id, + rock_paper_scissors_gesture gesture, + const fc::ecc::private_key& sig_priv_key ) +{ + graphene::chain::database& db = df.db; + + FC_ASSERT( latest_committs.find(player_id) != latest_committs.end() ); + const auto& reveal = committed_game_moves.find( latest_committs[player_id] ); + FC_ASSERT( reveal != committed_game_moves.end() ); + FC_ASSERT( reveal->second.gesture == gesture ); + + game_move_operation move_operation; + move_operation.game_id = game_id; + move_operation.player_account_id = player_id; + move_operation.move = reveal->second; + + signed_transaction tx; + tx.operations.push_back( move_operation ); + const asset f = db.current_fee_schedule().set_fee( tx.operations[0] ); + players_fees[player_id][f.asset_id] -= f.amount; + tx.validate(); + test::set_expiration( db, tx ); + df.sign( tx, sig_priv_key ); + PUSH_TX(db, tx); +} + +// spaghetti programming +// walking through all tournaments, matches and games and throwing random moves +// optionaly skip generting randomly selected moves +// every_move_is >= 0 : every game is tie +void tournaments_helper::play_games(unsigned skip_some_commits, unsigned skip_some_reveals, int every_move_is) +{ + //try + //{ + graphene::chain::database& db = df.db; + const chain_parameters& params = db.get_global_properties().parameters; + + for(const auto& tournament_id: tournaments) + { + const tournament_object& tournament = tournament_id(db); + const tournament_details_object& tournament_details = tournament.tournament_details_id(db); + rock_paper_scissors_game_options game_options = tournament.options.game_options.get(); + for(const auto& match_id: tournament_details.matches) + { + const match_object& match = match_id(db); + for(const auto& game_id: match.games ) + { + const game_object& game = game_id(db); + const rock_paper_scissors_game_details& rps_details = game.game_details.get(); + if (game.get_state() == game_state::expecting_commit_moves) + { + for(const auto& player_id: game.players) + { + if ( players_keys.find(player_id) != players_keys.end()) + { + if (!skip_some_commits || player_id.instance.value % skip_some_commits != game_id.instance.value % skip_some_commits) + { + auto iter = std::find(game.players.begin(), game.players.end(), player_id); + unsigned player_index = std::distance(game.players.begin(), iter); + if (!rps_details.commit_moves.at(player_index)) + rps_throw(game_id, player_id, + (rock_paper_scissors_gesture) (every_move_is >= 0 ? every_move_is : (std::rand() % game_options.number_of_gestures)), players_keys[player_id]); + } + } + } + } + else if (game.get_state() == game_state::expecting_reveal_moves) + { + for (unsigned i = 0; i < 2; ++i) + { + if (rps_details.commit_moves.at(i) && + !rps_details.reveal_moves.at(i)) + { + const account_id_type& player_id = game.players[i]; + if (players_keys.find(player_id) != players_keys.end()) + { + { + + auto iter = committed_game_moves.find(*rps_details.commit_moves.at(i)); + if (iter != committed_game_moves.end()) + { + if (!skip_some_reveals || player_id.instance.value % skip_some_reveals != game_id.instance.value % skip_some_reveals) + { + const rock_paper_scissors_throw_reveal& reveal = iter->second; + + game_move_operation move_operation; + move_operation.game_id = game.id; + move_operation.player_account_id = player_id; + move_operation.move = reveal; + + signed_transaction tx; + tx.operations = {move_operation}; + for( auto& op : tx.operations ) + { + asset f = db.current_fee_schedule().set_fee(op); + players_fees[player_id][f.asset_id] -= f.amount; + } + tx.validate(); + tx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3)); + df.sign(tx, players_keys[player_id]); + if (game.get_state() == game_state::expecting_reveal_moves) // check again + PUSH_TX(db, tx); + } + } + } + } + } + } + } + } + } + } +//} +//catch (fc::exception& e) +//{ +// edump((e.to_detail_string())); +// throw; +//} +} diff --git a/tests/common/tournament_helper.hpp b/tests/common/tournament_helper.hpp new file mode 100644 index 00000000..f9ff6282 --- /dev/null +++ b/tests/common/tournament_helper.hpp @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2018 Peerplays Blockchain Standards Association, and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "../common/database_fixture.hpp" + +using namespace graphene::chain; + +// class performing operations necessary for creating tournaments, +// having players join the tournaments and playing tournaments to completion. +class tournaments_helper +{ +public: + + tournaments_helper(database_fixture& df); + + const std::set& list_tournaments()const; + + const std::map> list_players_balances()const; + + const std::map> get_players_fees()const; + + void reset_players_fees(); + + void create_asset(const account_id_type& issuer_account_id, + const string& symbol, + uint8_t precision, + asset_options& common, + const fc::ecc::private_key& sig_priv_key); + + void update_dividend_asset(const asset_id_type asset_to_update_id, + dividend_asset_options new_options, + const fc::ecc::private_key& sig_priv_key); + + optional get_asset_dividend_account( const asset_id_type& asset_id = asset_id_type() )const; + + const tournament_id_type create_tournament( const account_id_type& creator, + const fc::ecc::private_key& sig_priv_key, + asset buy_in, + uint32_t number_of_players = 2, + uint32_t time_per_commit_move = 3, + uint32_t time_per_reveal_move = 1, + uint32_t number_of_wins = 3, + int32_t registration_deadline = 3600, + uint32_t start_delay = 3, + uint32_t round_delay = 3, + bool insurance_enabled = false, + uint32_t number_of_gestures = 3, + uint32_t start_time = 0, + fc::optional > whitelist = fc::optional >() + ); + + void join_tournament(const tournament_id_type & tournament_id, + const account_id_type& player_id, + const account_id_type& payer_id, + const fc::ecc::private_key& sig_priv_key, + asset buy_in + ); + + void leave_tournament(const tournament_id_type & tournament_id, + const account_id_type& player_id, + const account_id_type& canceling_account_id, + const fc::ecc::private_key& sig_priv_key + ); + + // stolen from cli_wallet + void rps_throw(const game_id_type& game_id, + const account_id_type& player_id, + rock_paper_scissors_gesture gesture, + const fc::ecc::private_key& sig_priv_key + ); + + void rps_reveal( const game_id_type& game_id, + const account_id_type& player_id, + rock_paper_scissors_gesture gesture, + const fc::ecc::private_key& sig_priv_key ); + + // spaghetti programming + // walking through all tournaments, matches and games and throwing random moves + // optionaly skip generting randomly selected moves + // every_move_is >= 0 : every game is tie + void play_games(unsigned skip_some_commits = 0, unsigned skip_some_reveals = 0, int every_move_is = -1); + +private: + database_fixture& df; + // index of last created tournament + fc::optional current_tournament_idx; + // index of last asset + uint64_t current_asset_idx; + // assets : core and maybe others + std::set assets; + // tournaments to be played + std::set tournaments; + // all players registered in tournaments + std::set players; + // players' private keys + std::map players_keys; + // total charges for moves made by every player + std::map> players_fees; + // store of commits and reveals + std::map committed_game_moves; + // store of latest commits, for use by rps_reveal + std::map latest_committs; +}; diff --git a/tests/tests/affiliate_tests.cpp b/tests/tests/affiliate_tests.cpp new file mode 100644 index 00000000..ab109ad3 --- /dev/null +++ b/tests/tests/affiliate_tests.cpp @@ -0,0 +1,732 @@ +/* + * Copyright (c) 2018 Peerplays Blockchain Standards Association, and contributors. + * + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include "../common/betting_test_markets.hpp" +#include "../common/tournament_helper.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +using namespace graphene::chain; +using namespace graphene::db; + +class affiliate_test_helper +{ +public: + affiliate_test_helper( database_fixture& f ) : fixture(f), accounts(&fixture.db.get_index_type()) + { + fixture.generate_blocks( HARDFORK_999_TIME ); + fixture.generate_block(); + + test::set_expiration( fixture.db, fixture.trx ); + fixture.trx.clear(); + + alice_id = find_account( "alice" ); + if( alice_id == account_id_type() ) + { + ACTOR(alice); + this->alice_id = alice_id; + } + + ann_id = find_account( "ann" ); + if( ann_id == account_id_type() ) + { + ACTOR(ann); + this->ann_id = ann_id; + } + + audrey_id = find_account( "audrey" ); + if( audrey_id == account_id_type() ) + { + ACTOR(audrey); + this->audrey_id = audrey_id; + } + + paula_id = find_account("paula"); + if( paula_id == account_id_type() ) + { + // Paula: 100% to Alice for Bookie / nothing for RPS + account_create_operation aco = fixture.make_account( "paula", paula_private_key.get_public_key() ); + aco.extensions.value.affiliate_distributions = affiliate_reward_distributions(); + aco.extensions.value.affiliate_distributions->_dists[bookie]._dist[alice_id] = GRAPHENE_100_PERCENT; + fixture.trx.operations.push_back( aco ); + processed_transaction op_result = fixture.db.push_transaction( fixture.trx, ~0 ); + paula_id = op_result.operation_results[0].get(); + fixture.trx.clear(); + fixture.fund( paula_id(fixture.db), asset(20000000) ); + } + + penny_id = find_account("penny"); + if( penny_id == account_id_type() ) + { + // Penny: For Bookie 60% to Alice + 40% to Ann / For RPS 20% to Alice, 30% to Ann, 50% to Audrey + account_create_operation aco = fixture.make_account( "penny", penny_private_key.get_public_key() ); + aco.extensions.value.affiliate_distributions = affiliate_reward_distributions(); + aco.extensions.value.affiliate_distributions->_dists[bookie]._dist[alice_id] = GRAPHENE_100_PERCENT * 3 / 5; + aco.extensions.value.affiliate_distributions->_dists[bookie]._dist[ann_id] = GRAPHENE_100_PERCENT + - aco.extensions.value.affiliate_distributions->_dists[bookie]._dist[alice_id]; + aco.extensions.value.affiliate_distributions->_dists[rps]._dist[alice_id] = GRAPHENE_100_PERCENT / 5; + aco.extensions.value.affiliate_distributions->_dists[rps]._dist[audrey_id] = GRAPHENE_100_PERCENT / 2; + aco.extensions.value.affiliate_distributions->_dists[rps]._dist[ann_id] = GRAPHENE_100_PERCENT + - aco.extensions.value.affiliate_distributions->_dists[rps]._dist[alice_id] + - aco.extensions.value.affiliate_distributions->_dists[rps]._dist[audrey_id]; + fixture.trx.operations.push_back( aco ); + processed_transaction op_result = fixture.db.push_transaction( fixture.trx, ~0 ); + penny_id = op_result.operation_results[0].get(); + fixture.trx.clear(); + fixture.fund( penny_id(fixture.db), asset(30000000) ); + } + + petra_id = find_account("petra"); + if( petra_id == account_id_type() ) + { + // Petra: nothing for Bookie / For RPS 10% to Ann, 90% to Audrey + account_create_operation aco = fixture.make_account( "petra", petra_private_key.get_public_key() ); + aco.extensions.value.affiliate_distributions = affiliate_reward_distributions(); + aco.extensions.value.affiliate_distributions->_dists[rps]._dist[ann_id] = GRAPHENE_100_PERCENT / 10; + aco.extensions.value.affiliate_distributions->_dists[rps]._dist[audrey_id] = GRAPHENE_100_PERCENT + - aco.extensions.value.affiliate_distributions->_dists[rps]._dist[ann_id]; + fixture.trx.operations.push_back( aco ); + processed_transaction op_result = fixture.db.push_transaction( fixture.trx, ~0 ); + petra_id = op_result.operation_results[0].get(); + fixture.trx.clear(); + fixture.fund( petra_id(fixture.db), asset(40000000) ); + } + + update_balances(); + } + + void update_balances() + { + alice_ppy = fixture.get_balance( alice_id, asset_id_type() ); + ann_ppy = fixture.get_balance( ann_id, asset_id_type() ); + audrey_ppy = fixture.get_balance( audrey_id, asset_id_type() ); + paula_ppy = fixture.get_balance( paula_id, asset_id_type() ); + penny_ppy = fixture.get_balance( penny_id, asset_id_type() ); + petra_ppy = fixture.get_balance( petra_id, asset_id_type() ); + } + + database_fixture& fixture; + + account_id_type alice_id; + account_id_type ann_id; + account_id_type audrey_id; + account_id_type paula_id; + account_id_type penny_id; + account_id_type petra_id; + + int64_t alice_ppy; + int64_t ann_ppy; + int64_t audrey_ppy; + int64_t paula_ppy; + int64_t penny_ppy; + int64_t petra_ppy; + +private: + const account_index* accounts; + account_id_type find_account( const string& name ) + { + auto itr = accounts->indices().get().find( name ); + if( itr == accounts->indices().get().end() ) return account_id_type(); + return itr->id; + } + + static fc::ecc::private_key generate_private_key( const string& seed ) + { + return database_fixture::generate_private_key( seed ); + } + + const account_object& create_account( const string& name, const fc::ecc::public_key& key ) + { + return fixture.create_account( name, key ); + } + +public: + const fc::ecc::private_key alice_private_key = generate_private_key( "alice" ); + const fc::ecc::private_key ann_private_key = generate_private_key( "ann" ); + const fc::ecc::private_key audrey_private_key = generate_private_key( "audrey" ); + const fc::ecc::private_key paula_private_key = generate_private_key( "paula" ); + const fc::ecc::private_key penny_private_key = generate_private_key( "penny" ); + const fc::ecc::private_key petra_private_key = generate_private_key( "petra" ); +}; + +BOOST_FIXTURE_TEST_SUITE( affiliate_tests, database_fixture ) + +BOOST_AUTO_TEST_CASE( account_test ) +{ + ACTORS( (alice)(ann)(audrey) ); + fund( alice ); + + const fc::ecc::private_key paula_private_key = generate_private_key( "paula" ); + + account_create_operation aco = make_account( "paula", paula_private_key.get_public_key() ); + aco.extensions.value.affiliate_distributions = affiliate_reward_distributions(); + aco.extensions.value.affiliate_distributions->_dists[bookie]._dist[alice_id] = GRAPHENE_100_PERCENT; + test::set_expiration( db, trx ); + trx.clear(); + trx.operations.push_back( aco ); + // not allowed before hardfork + GRAPHENE_REQUIRE_THROW( db.push_transaction(trx, ~0), fc::assert_exception ); + + proposal_create_operation pco; + pco.fee_paying_account = alice_id; + aco.registrar = ann_id; + pco.proposed_ops.emplace_back( aco ); + aco.registrar = account_id_type(); + pco.expiration_time = db.head_block_time() + fc::days(1); + trx.clear(); + trx.operations.push_back( pco ); + // not allowed before hardfork + GRAPHENE_REQUIRE_THROW( db.push_transaction(trx, ~0), fc::assert_exception ); + + generate_blocks( HARDFORK_999_TIME ); + generate_block(); + + // Proposal is now allowed + pco.expiration_time = db.head_block_time() + fc::days(1); + trx.clear(); + trx.operations.push_back( pco ); + test::set_expiration( db, trx ); + db.push_transaction(trx, ~0); + + // Must contain at least one app-tag + aco.extensions.value.affiliate_distributions->_dists.clear(); + trx.clear(); + trx.operations.push_back( aco ); + GRAPHENE_REQUIRE_THROW( db.push_transaction(trx, ~0), fc::assert_exception ); + + // Distribution for app-tag must be non-empty + aco.extensions.value.affiliate_distributions->_dists[bookie]._dist.clear(); + trx.clear(); + trx.operations.push_back( aco ); + GRAPHENE_REQUIRE_THROW( db.push_transaction(trx, ~0), fc::assert_exception ); + + // If more than one app-tag given, neither can be empty + aco.extensions.value.affiliate_distributions->_dists[rps]._dist[alice_id] = GRAPHENE_100_PERCENT; + aco.extensions.value.affiliate_distributions->_dists[bookie]._dist.clear(); + trx.clear(); + trx.operations.push_back( aco ); + GRAPHENE_REQUIRE_THROW( db.push_transaction(trx, ~0), fc::assert_exception ); + + // Sum of percentage must be 100% + aco.extensions.value.affiliate_distributions->_dists[bookie]._dist[ann_id] = 1; + trx.clear(); + trx.operations.push_back( aco ); + GRAPHENE_REQUIRE_THROW( db.push_transaction(trx, ~0), fc::assert_exception ); + + // Individual percentages cannot exceed 100% + aco.extensions.value.affiliate_distributions->_dists[bookie]._dist[ann_id] = -1; + aco.extensions.value.affiliate_distributions->_dists[bookie]._dist[audrey_id] = 1 + GRAPHENE_100_PERCENT; + trx.clear(); + trx.operations.push_back( aco ); + GRAPHENE_REQUIRE_THROW( db.push_transaction(trx, ~0), fc::assert_exception ); + + // Sum of percentage must be 100% + aco.extensions.value.affiliate_distributions->_dists[bookie]._dist[ann_id] = GRAPHENE_100_PERCENT - 10; + aco.extensions.value.affiliate_distributions->_dists[bookie]._dist[audrey_id] = 9; + trx.clear(); + trx.operations.push_back( aco ); + GRAPHENE_REQUIRE_THROW( db.push_transaction(trx, ~0), fc::assert_exception ); + + // Ok + aco.extensions.value.affiliate_distributions->_dists[bookie]._dist[audrey_id] = 10; + trx.clear(); + trx.operations.push_back( aco ); + db.push_transaction(trx, ~0); + + const auto& paula = get_account( aco.name ); + BOOST_CHECK( paula.affiliate_distributions.valid() ); + BOOST_CHECK_EQUAL( 2, paula.affiliate_distributions->_dists.size() ); + + const auto& app_rps = paula.affiliate_distributions->_dists.find( rps ); + BOOST_CHECK( app_rps != paula.affiliate_distributions->_dists.end() ); + BOOST_CHECK_EQUAL( 1, app_rps->second._dist.size() ); + const auto& share_alice = app_rps->second._dist.find( alice_id ); + BOOST_CHECK( share_alice != app_rps->second._dist.end() ); + BOOST_CHECK_EQUAL( GRAPHENE_100_PERCENT, share_alice->second ); + + const auto& app_bookie = paula.affiliate_distributions->_dists.find( bookie ); + BOOST_CHECK( app_bookie != paula.affiliate_distributions->_dists.end() ); + BOOST_CHECK_EQUAL( 2, app_bookie->second._dist.size() ); + const auto& share_ann = app_bookie->second._dist.find( ann_id ); + BOOST_CHECK( share_ann != app_bookie->second._dist.end() ); + BOOST_CHECK_EQUAL( GRAPHENE_100_PERCENT - 10, share_ann->second ); + const auto& share_audrey = app_bookie->second._dist.find( audrey_id ); + BOOST_CHECK( share_audrey != app_bookie->second._dist.end() ); + BOOST_CHECK_EQUAL( 10, share_audrey->second ); +} + +BOOST_AUTO_TEST_CASE( affiliate_payout_helper_test ) +{ + ACTORS( (irene) ); + + const asset_id_type btc_id = create_user_issued_asset( "BTC", irene, 0 ).id; + issue_uia( irene, asset( 100000, btc_id ) ); + + affiliate_test_helper ath( *this ); + + int64_t alice_btc = 0; + int64_t ann_btc = 0; + int64_t audrey_btc = 0; + + { + const tournament_object& game = db.create( []( tournament_object& t ) { + t.options.game_options = rock_paper_scissors_game_options(); + t.options.buy_in = asset( 10 ); + }); + affiliate_payout_helper helper = affiliate_payout_helper( db, game ); + // Alice has no distribution set + BOOST_CHECK_EQUAL( 0, helper.payout( ath.alice_id, 1000 ).value ); + // Paula has nothing for Bookie + BOOST_CHECK_EQUAL( 0, helper.payout( ath.paula_id, 1000 ).value ); + // 20% of 4 gets rounded down to 0 + BOOST_CHECK_EQUAL( 0, helper.payout( ath.penny_id, 4 ).value ); + + // 20% of 5 = 1 is paid to Audrey + BOOST_CHECK_EQUAL( 1, helper.payout( ath.penny_id, 5 ).value ); + ath.audrey_ppy++; + + // 20% of 50 = 10: 2 to Alice, 3 to Ann, 5 to Audrey + BOOST_CHECK_EQUAL( 10, helper.payout( ath.penny_id, 50 ).value ); + ath.alice_ppy += 2; + ath.ann_ppy += 3; + ath.audrey_ppy += 5; + + // 20% of 59 = 11: 1 to Ann, 10 to Audrey + BOOST_CHECK_EQUAL( 11, helper.payout( ath.petra_id, 59 ).value ); + ath.audrey_ppy += 10; + ath.ann_ppy += 1; + + helper.commit(); + + BOOST_CHECK_EQUAL( ath.alice_ppy, get_balance( ath.alice_id, asset_id_type() ) ); + BOOST_CHECK_EQUAL( ath.ann_ppy, get_balance( ath.ann_id, asset_id_type() ) ); + BOOST_CHECK_EQUAL( ath.audrey_ppy, get_balance( ath.audrey_id, asset_id_type() ) ); + } + + { + const tournament_object& game = db.create( [btc_id]( tournament_object& t ) { + t.options.game_options = rock_paper_scissors_game_options(); + t.options.buy_in = asset( 10, btc_id ); + }); + affiliate_payout_helper helper = affiliate_payout_helper( db, game ); + // 20% of 60 = 12: 2 to Alice, 3 to Ann, 7 to Audrey + BOOST_CHECK_EQUAL( 12, helper.payout( ath.penny_id, 60 ).value ); + alice_btc += 2; + ann_btc += 3; + audrey_btc += 7; + helper.commit(); + BOOST_CHECK_EQUAL( alice_btc, get_balance( ath.alice_id, btc_id ) ); + BOOST_CHECK_EQUAL( ann_btc, get_balance( ath.ann_id, btc_id ) ); + BOOST_CHECK_EQUAL( audrey_btc, get_balance( ath.audrey_id, btc_id ) ); + } + + { + const betting_market_group_object& game = db.create( []( betting_market_group_object& b ) { + b.asset_id = asset_id_type(); + } ); + affiliate_payout_helper helper = affiliate_payout_helper( db, game ); + // Alice has no distribution set + BOOST_CHECK_EQUAL( 0, helper.payout( ath.alice_id, 1000 ).value ); + // Petra has nothing for Bookie + BOOST_CHECK_EQUAL( 0, helper.payout( ath.petra_id, 1000 ).value ); + // 20% of 4 gets rounded down to 0 + BOOST_CHECK_EQUAL( 0, helper.payout( ath.penny_id, 4 ).value ); + + // 20% of 5 = 1 is paid to Ann + BOOST_CHECK_EQUAL( 1, helper.payout( ath.penny_id, 5 ).value ); + ath.ann_ppy++; + + // 20% of 40 = 8: 8 to Alice + BOOST_CHECK_EQUAL( 8, helper.payout( ath.paula_id, 40 ).value ); + ath.alice_ppy += 8; + + // intermediate commit should clear internal accumulator + helper.commit(); + + // 20% of 59 = 11: 6 to Alice, 5 to Ann + BOOST_CHECK_EQUAL( 11, helper.payout( ath.penny_id, 59 ).value ); + ath.alice_ppy += 6; + ath.ann_ppy += 5; + + helper.commit(); + + BOOST_CHECK_EQUAL( ath.alice_ppy, get_balance( ath.alice_id, asset_id_type() ) ); + BOOST_CHECK_EQUAL( ath.ann_ppy, get_balance( ath.ann_id, asset_id_type() ) ); + BOOST_CHECK_EQUAL( ath.audrey_ppy, get_balance( ath.audrey_id, asset_id_type() ) ); + } + + { + const betting_market_group_object& game = db.create( [btc_id]( betting_market_group_object& b ) { + b.asset_id = btc_id; + } ); + affiliate_payout_helper helper = affiliate_payout_helper( db, game ); + // 20% of 60 = 12: 7 to Alice, 5 to Ann + BOOST_CHECK_EQUAL( 12, helper.payout( ath.penny_id, 60 ).value ); + alice_btc += 7; + ann_btc += 5; + helper.commit(); + BOOST_CHECK_EQUAL( alice_btc, get_balance( ath.alice_id, btc_id ) ); + BOOST_CHECK_EQUAL( ann_btc, get_balance( ath.ann_id, btc_id ) ); + BOOST_CHECK_EQUAL( audrey_btc, get_balance( ath.audrey_id, btc_id ) ); + } + + { + // Fix total supply + auto& index = db.get_index_type().indices().get(); + auto itr = index.find( boost::make_tuple( account_id_type(), asset_id_type() ) ); + BOOST_CHECK( itr != index.end() ); + db.modify( *itr, [&ath]( account_balance_object& bal ) { + bal.balance -= ath.alice_ppy + ath.ann_ppy + ath.audrey_ppy; + }); + + itr = index.find( boost::make_tuple( irene_id, btc_id ) ); + BOOST_CHECK( itr != index.end() ); + db.modify( *itr, [alice_btc,ann_btc,audrey_btc]( account_balance_object& bal ) { + bal.balance -= alice_btc + ann_btc + audrey_btc; + }); + } +} + +BOOST_AUTO_TEST_CASE( rps_tournament_payout_test ) +{ try { + ACTORS( (martha) ); + + affiliate_test_helper ath( *this ); + + fund( martha_id(db), asset(1000000000) ); + + upgrade_to_lifetime_member( martha_id(db) ); + + tournaments_helper helper(*this); + account_id_type dividend_id = *helper.get_asset_dividend_account(); + int64_t div_ppy = get_balance( dividend_id, asset_id_type() ); + const asset buy_in = asset(12000); + tournament_id_type tournament_id = helper.create_tournament( martha_id, martha_private_key, buy_in, 3, 3, 1, 1 ); + BOOST_REQUIRE(tournament_id == tournament_id_type()); + + helper.join_tournament( tournament_id, ath.paula_id, ath.paula_id, ath.paula_private_key, buy_in); + helper.join_tournament( tournament_id, ath.penny_id, ath.penny_id, ath.penny_private_key, buy_in); + helper.join_tournament( tournament_id, ath.petra_id, ath.petra_id, ath.petra_private_key, buy_in); + + generate_block(); + const tournament_object& tournament = db.get( tournament_id ); + + BOOST_CHECK_EQUAL( ath.paula_ppy - 12000, get_balance( ath.paula_id, asset_id_type() ) ); + BOOST_CHECK_EQUAL( ath.penny_ppy - 12000, get_balance( ath.penny_id, asset_id_type() ) ); + BOOST_CHECK_EQUAL( ath.petra_ppy - 12000, get_balance( ath.petra_id, asset_id_type() ) ); + + const tournament_details_object& tournament_details = tournament.tournament_details_id(db); + BOOST_CHECK_EQUAL( 3, tournament_details.matches.size() ); + + { // 3 players, match 1 is a bye for penny + const match_object& match = tournament_details.matches[0](db); + BOOST_CHECK_EQUAL( int64_t(match_state::match_complete), int64_t(match.get_state()) ); + BOOST_CHECK_EQUAL( 1, match.players.size() ); + BOOST_CHECK_EQUAL( object_id_type(ath.penny_id).instance(), object_id_type(match.players[0]).instance() ); + } + { // match 2 is paula vs. petra: paula wins + const match_object& match = tournament_details.matches[1](db); + BOOST_CHECK_EQUAL( int64_t(match_state::match_in_progress), int64_t(match.get_state()) ); + BOOST_CHECK_EQUAL( 2, match.players.size() ); + BOOST_CHECK_EQUAL( object_id_type(ath.paula_id).instance(), object_id_type(match.players[0]).instance() ); + BOOST_CHECK_EQUAL( object_id_type(ath.petra_id).instance(), object_id_type(match.players[1]).instance() ); + BOOST_CHECK_EQUAL( 1, match.games.size() ); + + const game_object& game = match.games[0](db); + BOOST_CHECK_EQUAL( int64_t(game_state::expecting_commit_moves), int64_t(game.get_state()) ); + helper.rps_throw( game.id, ath.paula_id, rock_paper_scissors_gesture::paper, ath.paula_private_key ); + helper.rps_throw( game.id, ath.petra_id, rock_paper_scissors_gesture::rock, ath.petra_private_key ); + + BOOST_CHECK_EQUAL( int64_t(game_state::expecting_reveal_moves), int64_t(game.get_state()) ); + helper.rps_reveal( game.id, ath.paula_id, rock_paper_scissors_gesture::paper, ath.paula_private_key ); + helper.rps_reveal( game.id, ath.petra_id, rock_paper_scissors_gesture::rock, ath.petra_private_key ); + + BOOST_CHECK_EQUAL( int64_t(game_state::game_complete), int64_t(game.get_state()) ); + BOOST_CHECK_EQUAL( 1, match.games.size() ); + BOOST_CHECK_EQUAL( int64_t(match_state::match_complete), int64_t(match.get_state()) ); + } + { // match 3 is penny vs. paula: penny wins + const match_object& match = tournament_details.matches[2](db); + BOOST_CHECK_EQUAL( int64_t(match_state::match_in_progress), int64_t(match.get_state()) ); + BOOST_CHECK_EQUAL( 2, match.players.size() ); + BOOST_CHECK_EQUAL( object_id_type(ath.penny_id).instance(), object_id_type(match.players[0]).instance() ); + BOOST_CHECK_EQUAL( object_id_type(ath.paula_id).instance(), object_id_type(match.players[1]).instance() ); + BOOST_CHECK_EQUAL( 1, match.games.size() ); + + const game_object& game = match.games[0](db); + BOOST_CHECK_EQUAL( int64_t(game_state::expecting_commit_moves), int64_t(game.get_state()) ); + helper.rps_throw( game.id, ath.paula_id, rock_paper_scissors_gesture::paper, ath.paula_private_key ); + helper.rps_throw( game.id, ath.penny_id, rock_paper_scissors_gesture::scissors, ath.penny_private_key ); + + BOOST_CHECK_EQUAL( int64_t(game_state::expecting_reveal_moves), int64_t(game.get_state()) ); + helper.rps_reveal( game.id, ath.paula_id, rock_paper_scissors_gesture::paper, ath.paula_private_key ); + helper.rps_reveal( game.id, ath.penny_id, rock_paper_scissors_gesture::scissors, ath.penny_private_key ); + + BOOST_CHECK_EQUAL( int64_t(game_state::game_complete), int64_t(game.get_state()) ); + BOOST_CHECK_EQUAL( int64_t(match_state::match_complete), int64_t(match.get_state()) ); + } + + BOOST_CHECK_EQUAL( 3*GRAPHENE_1_PERCENT, db.get_global_properties().parameters.rake_fee_percentage ); + // Penny wins net 3*buy_in minus rake = 36000 - 1080 = 34920 + BOOST_CHECK_EQUAL( ath.paula_ppy - 12000, get_balance( ath.paula_id, asset_id_type() ) ); + BOOST_CHECK_EQUAL( ath.penny_ppy - 12000 + 34920, get_balance( ath.penny_id, asset_id_type() ) ); + BOOST_CHECK_EQUAL( ath.petra_ppy - 12000, get_balance( ath.petra_id, asset_id_type() ) ); + + // Dividend account receives 80% of rake = 864 + BOOST_CHECK_EQUAL( div_ppy + 864, get_balance( dividend_id, asset_id_type() ) ); + + // 20% of rake = 216 is paid to Penny's affiliates: 43 to Alice, 64 to Ann, 109 to Audrey + BOOST_CHECK_EQUAL( ath.alice_ppy + 43, get_balance( ath.alice_id, asset_id_type() ) ); + BOOST_CHECK_EQUAL( ath.ann_ppy + 64, get_balance( ath.ann_id, asset_id_type() ) ); + BOOST_CHECK_EQUAL( ath.audrey_ppy + 109, get_balance( ath.audrey_id, asset_id_type() ) ); + +} FC_LOG_AND_RETHROW() } + + +BOOST_AUTO_TEST_CASE( bookie_payout_test ) +{ try { + ACTORS( (irene) ); + + const asset_id_type btc_id = create_user_issued_asset( "BTC", irene, 0 ).id; + + affiliate_test_helper ath( *this ); + + CREATE_ICE_HOCKEY_BETTING_MARKET(false, 0); + + // place bets at 10:1 + place_bet(ath.paula_id, capitals_win_market.id, bet_type::back, asset(10000, asset_id_type()), 11 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(ath.penny_id, capitals_win_market.id, bet_type::lay, asset(100000, asset_id_type()), 11 * GRAPHENE_BETTING_ODDS_PRECISION); + + // reverse positions at 1:1 + place_bet(ath.paula_id, capitals_win_market.id, bet_type::lay, asset(110000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(ath.penny_id, capitals_win_market.id, bet_type::back, asset(110000, asset_id_type()), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + update_betting_market_group(moneyline_betting_markets.id, graphene::chain::keywords::_status = betting_market_group_status::closed); + 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_block(); + + uint16_t rake_fee_percentage = db.get_global_properties().parameters.betting_rake_fee_percentage(); + BOOST_CHECK_EQUAL( 3 * GRAPHENE_1_PERCENT, rake_fee_percentage ); + uint32_t rake_value; + // rake_value = (-10000 + 110000 - 110000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + // paula starts with 20000000, pays 10000 (bet), wins 110000, then pays 110000 (bet), wins 0 + BOOST_CHECK_EQUAL( get_balance( ath.paula_id, asset_id_type() ), ath.paula_ppy - 10000 + 110000 - 110000 + 0 ); + // no wins -> no affiliate payouts + + rake_value = (-100000 - 110000 + 220000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + // penny starts with 30000000, pays 100000 (bet), wins 0, then pays 110000 (bet), wins 220000 + BOOST_CHECK_EQUAL( get_balance( ath.penny_id, asset_id_type() ), ath.penny_ppy - 100000 + 0 - 110000 + 220000 - rake_value ); + // penny wins 10000 net, rake is 3% of that (=300) + // Affiliate pay is 20% of rake (=60). Of this, 60%=36 go to Alice, 40%=24 go to Ann + BOOST_CHECK_EQUAL( ath.alice_ppy + 36, get_balance( ath.alice_id, asset_id_type() ) ); + BOOST_CHECK_EQUAL( ath.ann_ppy + 24, get_balance( ath.ann_id, asset_id_type() ) ); + BOOST_CHECK_EQUAL( ath.audrey_ppy + 0, get_balance( ath.audrey_id, asset_id_type() ) ); + + { + test::set_expiration( db, trx ); + issue_uia( ath.paula_id, asset( 1000000, btc_id ) ); + issue_uia( ath.petra_id, asset( 1000000, btc_id ) ); + + create_event({{"en", "Washington Capitals/Chicago Blackhawks"}, {"zh_Hans", "華盛頓首都隊/芝加哥黑鷹"}, {"ja", "ワシントン・キャピタルズ/シカゴ・ブラックホークス"}}, {{"en", "2016-17"}}, nhl.id); \ + generate_blocks(1); \ + const event_object& capitals_vs_blackhawks2 = *db.get_index_type().indices().get().rbegin(); \ + create_betting_market_group({{"en", "Moneyline"}}, capitals_vs_blackhawks2.id, betting_market_rules.id, btc_id, false, 0); + generate_blocks(1); + const betting_market_group_object& moneyline_betting_markets2 = *db.get_index_type().indices().get().rbegin(); + create_betting_market(moneyline_betting_markets2.id, {{"en", "Washington Capitals win"}}); + generate_blocks(1); + const betting_market_object& capitals_win_market2 = *db.get_index_type().indices().get().rbegin(); + create_betting_market(moneyline_betting_markets2.id, {{"en", "Chicago Blackhawks win"}}); + generate_blocks(1); + const betting_market_object& blackhawks_win_market2 = *db.get_index_type().indices().get().rbegin(); + + // place bets at 10:1 + place_bet(ath.paula_id, capitals_win_market2.id, bet_type::back, asset(10000, btc_id), 11 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(ath.petra_id, capitals_win_market2.id, bet_type::lay, asset(100000, btc_id), 11 * GRAPHENE_BETTING_ODDS_PRECISION); + + // reverse positions at 1:1 + place_bet(ath.paula_id, capitals_win_market2.id, bet_type::lay, asset(110000, btc_id), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + place_bet(ath.petra_id, capitals_win_market2.id, bet_type::back, asset(110000, btc_id), 2 * GRAPHENE_BETTING_ODDS_PRECISION); + + update_betting_market_group(moneyline_betting_markets2.id, graphene::chain::keywords::_status = betting_market_group_status::closed); + resolve_betting_market_group(moneyline_betting_markets2.id, + {{capitals_win_market2.id, betting_market_resolution_type::not_win}, + {blackhawks_win_market2.id, betting_market_resolution_type::win}}); + generate_block(); + + rake_value = (-10000 + 0 - 110000 + 220000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + // paula starts with 1000000, pays 10000 (bet), wins 0, then pays 110000 (bet), wins 220000 + BOOST_CHECK_EQUAL( get_balance( ath.paula_id, btc_id ), 1000000 - 10000 + 0 - 110000 + 220000 - rake_value ); + // net win 100000, 3% rake = 3000, 20% of that is 600, 100% of that goes to Alice + BOOST_CHECK_EQUAL( 600, get_balance( ath.alice_id, btc_id ) ); + BOOST_CHECK_EQUAL( 0, get_balance( ath.ann_id, btc_id ) ); + BOOST_CHECK_EQUAL( 0, get_balance( ath.audrey_id, btc_id ) ); + + // rake_value = (-100000 + 110000 - 110000) * rake_fee_percentage / GRAPHENE_1_PERCENT / 100; + rake_value = 0; + // petra starts with 1000000, pays 100000 (bet), wins 110000, then pays 110000 (bet), wins 0 + BOOST_CHECK_EQUAL( get_balance( ath.petra_id, btc_id ), 1000000 - 100000 + 110000 - 110000 + 0 - rake_value ); + // petra wins nothing -> no payout + } + +} FC_LOG_AND_RETHROW() } + + +BOOST_AUTO_TEST_CASE( statistics_test ) +{ try { + + INVOKE(rps_tournament_payout_test); + + affiliate_test_helper ath( *this ); + + transfer( ath.audrey_id, ath.alice_id, asset( 10 ), asset(0) ); + transfer( ath.audrey_id, ath.ann_id, asset( 10 ), asset(0) ); + + INVOKE(bookie_payout_test); + + const asset_id_type btc_id = get_asset( "BTC" ).id; + + transfer( ath.alice_id, ath.ann_id, asset( 100, btc_id ), asset(0) ); + transfer( ath.alice_id, ath.audrey_id, asset( 100, btc_id ), asset(0) ); + + generate_block(); + + { + const auto& idx = db.get_index_type().indices().get(); + BOOST_CHECK_EQUAL( 2, idx.size() ); // penny 216+60 CORE, paula 600 BTC + } + + { + const auto& idx = db.get_index_type().indices().get(); + BOOST_CHECK_EQUAL( 3, idx.size() ); // rps 216 CORE, bookie 60 CORE + 600 BTC + } + + graphene::affiliate_stats::affiliate_stats_api stats( app ); + + { + std::vector refs = stats.list_historic_referral_rewards( ath.alice_id, operation_history_id_type() ); + BOOST_CHECK_EQUAL( 3, refs.size() ); + BOOST_REQUIRE_LE( 1, refs.size() ); + BOOST_CHECK_EQUAL( app_tag::rps, refs[0].tag ); + BOOST_CHECK_EQUAL( 43, refs[0].payout.amount.value ); + BOOST_CHECK_EQUAL( 0, refs[0].payout.asset_id.instance.value ); + BOOST_REQUIRE_LE( 2, refs.size() ); + BOOST_CHECK_EQUAL( app_tag::bookie, refs[1].tag ); + BOOST_CHECK_EQUAL( 36, refs[1].payout.amount.value ); + BOOST_CHECK_EQUAL( 0, refs[1].payout.asset_id.instance.value ); + BOOST_REQUIRE_LE( 3, refs.size() ); + BOOST_CHECK_EQUAL( app_tag::bookie, refs[2].tag ); + BOOST_CHECK_EQUAL( 600, refs[2].payout.amount.value ); + BOOST_CHECK_EQUAL( btc_id.instance.value, refs[2].payout.asset_id.instance.value ); + + BOOST_CHECK_EQUAL( 3, stats.list_historic_referral_rewards( ath.alice_id, refs[0].id ).size() ); + BOOST_CHECK_EQUAL( 2, stats.list_historic_referral_rewards( ath.alice_id, refs[1].id ).size() ); + BOOST_CHECK_EQUAL( 1, stats.list_historic_referral_rewards( ath.alice_id, refs[2].id ).size() ); + + refs = stats.list_historic_referral_rewards( ath.ann_id, operation_history_id_type() ); + BOOST_CHECK_EQUAL( 2, refs.size() ); + BOOST_REQUIRE_LE( 1, refs.size() ); + BOOST_CHECK_EQUAL( app_tag::rps, refs[0].tag ); + BOOST_CHECK_EQUAL( 64, refs[0].payout.amount.value ); + BOOST_CHECK_EQUAL( 0, refs[0].payout.asset_id.instance.value ); + BOOST_REQUIRE_LE( 2, refs.size() ); + BOOST_CHECK_EQUAL( app_tag::bookie, refs[1].tag ); + BOOST_CHECK_EQUAL( 24, refs[1].payout.amount.value ); + BOOST_CHECK_EQUAL( 0, refs[1].payout.asset_id.instance.value ); + + BOOST_CHECK_EQUAL( 2, stats.list_historic_referral_rewards( ath.ann_id, refs[0].id ).size() ); + BOOST_CHECK_EQUAL( 1, stats.list_historic_referral_rewards( ath.ann_id, refs[1].id ).size() ); + + refs = stats.list_historic_referral_rewards( ath.audrey_id, operation_history_id_type() ); + BOOST_CHECK_EQUAL( 1, refs.size() ); + BOOST_REQUIRE_LE( 1, refs.size() ); + BOOST_CHECK_EQUAL( app_tag::rps, refs[0].tag ); + BOOST_CHECK_EQUAL( 109, refs[0].payout.amount.value ); + BOOST_CHECK_EQUAL( 0, refs[0].payout.asset_id.instance.value ); + + BOOST_CHECK_EQUAL( 0, stats.list_historic_referral_rewards( ath.alice_id, operation_history_id_type(9990) ).size() ); + BOOST_CHECK_EQUAL( 1, stats.list_historic_referral_rewards( ath.alice_id, operation_history_id_type(), 1 ).size() ); + BOOST_CHECK_EQUAL( 0, stats.list_historic_referral_rewards( ath.paula_id, operation_history_id_type() ).size() ); + BOOST_CHECK_EQUAL( 0, stats.list_historic_referral_rewards( ath.penny_id, operation_history_id_type() ).size() ); + BOOST_CHECK_EQUAL( 0, stats.list_historic_referral_rewards( ath.petra_id, operation_history_id_type() ).size() ); + } + + { + std::vector refs = stats.list_top_referred_accounts( asset_id_type() ); + BOOST_CHECK_EQUAL( 1, refs.size() ); + BOOST_REQUIRE_LE( 1, refs.size() ); + BOOST_CHECK_EQUAL( ath.penny_id.instance.value, refs[0].referral.instance.value ); + BOOST_CHECK_EQUAL( 276, refs[0].total_payout.amount.value ); + BOOST_CHECK_EQUAL( 0, refs[0].total_payout.asset_id.instance.value ); + + refs = stats.list_top_referred_accounts( btc_id ); + BOOST_CHECK_EQUAL( 1, refs.size() ); + BOOST_REQUIRE_LE( 1, refs.size() ); + BOOST_CHECK_EQUAL( ath.paula_id.instance.value, refs[0].referral.instance.value ); + BOOST_CHECK_EQUAL( 600, refs[0].total_payout.amount.value ); + BOOST_CHECK_EQUAL( btc_id.instance.value, refs[0].total_payout.asset_id.instance.value ); + + BOOST_CHECK_EQUAL( 1, stats.list_top_referred_accounts( btc_id, 1 ).size() ); + BOOST_CHECK_EQUAL( 0, stats.list_top_referred_accounts( btc_id, 0 ).size() ); + BOOST_CHECK_EQUAL( 0, stats.list_top_referred_accounts( asset_id_type(9999) ).size() ); + } + + { + std::vector top = stats.list_top_rewards_per_app( asset_id_type() ); + BOOST_CHECK_EQUAL( 2, top.size() ); + BOOST_REQUIRE_LE( 1, top.size() ); + BOOST_CHECK_EQUAL( app_tag::rps, top[0].app ); + BOOST_CHECK_EQUAL( 216, top[0].total_payout.amount.value ); + BOOST_CHECK_EQUAL( 0, top[0].total_payout.asset_id.instance.value ); + BOOST_REQUIRE_LE( 2, top.size() ); + BOOST_CHECK_EQUAL( app_tag::bookie, top[1].app ); + BOOST_CHECK_EQUAL( 60, top[1].total_payout.amount.value ); + BOOST_CHECK_EQUAL( 0, top[1].total_payout.asset_id.instance.value ); + + top = stats.list_top_rewards_per_app( btc_id ); + BOOST_CHECK_EQUAL( 1, top.size() ); + BOOST_REQUIRE_LE( 1, top.size() ); + BOOST_CHECK_EQUAL( app_tag::bookie, top[0].app ); + BOOST_CHECK_EQUAL( 600, top[0].total_payout.amount.value ); + BOOST_CHECK_EQUAL( btc_id.instance.value, top[0].total_payout.asset_id.instance.value ); + + BOOST_CHECK_EQUAL( 1, stats.list_top_rewards_per_app( asset_id_type(), 1 ).size() ); + BOOST_CHECK_EQUAL( 0, stats.list_top_referred_accounts( btc_id, 0 ).size() ); + BOOST_CHECK_EQUAL( 0, stats.list_top_rewards_per_app( asset_id_type(9999) ).size() ); + } + +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/tournament/tournament_tests.cpp b/tests/tournament/tournament_tests.cpp index 6b2f2f64..8aa88479 100644 --- a/tests/tournament/tournament_tests.cpp +++ b/tests/tournament/tournament_tests.cpp @@ -28,7 +28,7 @@ #include #include #include -#include "../common/database_fixture.hpp" +#include "../common/tournament_helper.hpp" #include #include #include @@ -44,421 +44,6 @@ using namespace graphene::chain; BOOST_AUTO_TEST_SUITE(tournament_tests) -// class performing operations necessary for creating tournaments, -// having players join the tournaments and playing tournaments to completion. -class tournaments_helper -{ -public: - - tournaments_helper(database_fixture& df) : df(df) - { - assets.insert(asset_id_type()); - current_asset_idx = 0; - optional dividend_account = get_asset_dividend_account(asset_id_type()); - if (dividend_account.valid()) - players.insert(*dividend_account); - } - - const std::set& list_tournaments() - { - return tournaments; - } - - std::map> list_players_balances() - { - std::map> result; - for (account_id_type player_id: players) - { - for( asset_id_type asset_id: assets) - { - asset a = df.db.get_balance(player_id, asset_id); - result[player_id][a.asset_id] = a.amount; - } - } - return result; - } - - std::map> get_players_fees() - { - return players_fees; - } - - void reset_players_fees() - { - for (account_id_type player_id: players) - { - for( asset_id_type asset_id: assets) - { - players_fees[player_id][asset_id] = 0; - } - } - } - - void create_asset(const account_id_type& issuer_account_id, - const string& symbol, - uint8_t precision, - asset_options& common, - const fc::ecc::private_key& sig_priv_key) - { - graphene::chain::database& db = df.db; - const chain_parameters& params = db.get_global_properties().parameters; - signed_transaction tx; - asset_create_operation op; - op.issuer = issuer_account_id; - op.symbol = symbol; - op.precision = precision; - op.common_options = common; - - tx.operations = {op}; - for( auto& op : tx.operations ) - db.current_fee_schedule().set_fee(op); - tx.validate(); - tx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3)); - df.sign(tx, sig_priv_key); - PUSH_TX(db, tx); - - assets.insert(asset_id_type(++current_asset_idx)); - } - - void update_dividend_asset(const asset_id_type asset_to_update_id, - dividend_asset_options new_options, - const fc::ecc::private_key& sig_priv_key) - { - graphene::chain::database& db = df.db; - const chain_parameters& params = db.get_global_properties().parameters; - signed_transaction tx; - asset_update_dividend_operation update_op; - - update_op.issuer = asset_to_update_id(db).issuer; - update_op.asset_to_update = asset_to_update_id; - update_op.new_options = new_options; - - tx.operations = {update_op}; - for( auto& op : tx.operations ) - db.current_fee_schedule().set_fee(op); - tx.validate(); - tx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3)); - df.sign(tx, sig_priv_key); - PUSH_TX(db, tx); - - optional dividend_account = get_asset_dividend_account(asset_to_update_id); - if (dividend_account.valid()) - players.insert(*dividend_account); - } - - optional get_asset_dividend_account(const asset_id_type& asset_id) - { - graphene::chain::database& db = df.db; - optional result; - const asset_object& asset_obj = asset_id(db); - - if (asset_obj.dividend_data_id.valid()) - { - const asset_dividend_data_object& dividend_data = (*asset_obj.dividend_data_id)(db); - result = dividend_data.dividend_distribution_account; - } - return result; - } - - const tournament_id_type create_tournament (const account_id_type& creator, - const fc::ecc::private_key& sig_priv_key, - asset buy_in, - uint32_t number_of_players = 2, - uint32_t time_per_commit_move = 3, - uint32_t time_per_reveal_move = 1, - uint32_t number_of_wins = 3, - int32_t registration_deadline = 3600, - uint32_t start_delay = 3, - uint32_t round_delay = 3, - bool insurance_enabled = false, - uint32_t number_of_gestures = 3, - uint32_t start_time = 0, - fc::optional > whitelist = fc::optional >() - ) - { - if (current_tournament_idx.valid()) - current_tournament_idx = *current_tournament_idx + 1; - else - current_tournament_idx = 0; - - graphene::chain::database& db = df.db; - const chain_parameters& params = db.get_global_properties().parameters; - signed_transaction trx; - tournament_options options; - tournament_create_operation op; - rock_paper_scissors_game_options& game_options = options.game_options.get(); - - game_options.number_of_gestures = number_of_gestures; - game_options.time_per_commit_move = time_per_commit_move; - game_options.time_per_reveal_move = time_per_reveal_move; - game_options.insurance_enabled = insurance_enabled; - - options.registration_deadline = db.head_block_time() + fc::seconds(registration_deadline + *current_tournament_idx); - options.buy_in = buy_in; - options.number_of_players = number_of_players; - if (start_delay) - options.start_delay = start_delay; - if (start_time) - options.start_time = db.head_block_time() + fc::seconds(start_time); - options.round_delay = round_delay; - options.number_of_wins = number_of_wins; - if (whitelist.valid()) - options.whitelist = *whitelist; - - op.creator = creator; - op.options = options; - 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)); - df.sign(trx, sig_priv_key); - PUSH_TX(db, trx); - - tournament_id_type tournament_id = tournament_id_type(*current_tournament_idx); - tournaments.insert(tournament_id); - return tournament_id; - } - - void join_tournament(const tournament_id_type & tournament_id, - const account_id_type& player_id, - const account_id_type& payer_id, - const fc::ecc::private_key& sig_priv_key, - asset buy_in - ) - { - graphene::chain::database& db = df.db; - const chain_parameters& params = db.get_global_properties().parameters; - signed_transaction tx; - tournament_join_operation op; - - op.payer_account_id = payer_id; - op.buy_in = buy_in; - op.player_account_id = player_id; - op.tournament_id = tournament_id; - tx.operations = {op}; - for( auto& op : tx.operations ) - db.current_fee_schedule().set_fee(op); - tx.validate(); - tx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3)); - df.sign(tx, sig_priv_key); - PUSH_TX(db, tx); - - players.insert(player_id); - players_keys[player_id] = sig_priv_key; - } - - void leave_tournament(const tournament_id_type & tournament_id, - const account_id_type& player_id, - const account_id_type& canceling_account_id, - const fc::ecc::private_key& sig_priv_key - ) - { - graphene::chain::database& db = df.db; - const chain_parameters& params = db.get_global_properties().parameters; - signed_transaction tx; - tournament_leave_operation op; - - op.canceling_account_id = canceling_account_id; - op.player_account_id = player_id; - op.tournament_id = tournament_id; - tx.operations = {op}; - for( auto& op : tx.operations ) - db.current_fee_schedule().set_fee(op); - tx.validate(); - tx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3)); - df.sign(tx, sig_priv_key); - PUSH_TX(db, tx); - - //players.erase(player_id); - } - - - - // stolen from cli_wallet - void rps_throw(const game_id_type& game_id, - const account_id_type& player_id, - rock_paper_scissors_gesture gesture, - const fc::ecc::private_key& sig_priv_key - ) - { - - graphene::chain::database& db = df.db; - const chain_parameters& params = db.get_global_properties().parameters; - - // check whether the gesture is appropriate for the game we're playing - game_object game_obj = game_id(db); - match_object match_obj = game_obj.match_id(db); - tournament_object tournament_obj = match_obj.tournament_id(db); - rock_paper_scissors_game_options game_options = tournament_obj.options.game_options.get(); - assert((int)gesture < game_options.number_of_gestures); - - account_object player_account_obj = player_id(db); - - // construct the complete throw, the commit, and reveal - rock_paper_scissors_throw full_throw; - rand_bytes((char*)&full_throw.nonce1, sizeof(full_throw.nonce1)); - rand_bytes((char*)&full_throw.nonce2, sizeof(full_throw.nonce2)); - full_throw.gesture = gesture; - - rock_paper_scissors_throw_commit commit_throw; - commit_throw.nonce1 = full_throw.nonce1; - std::vector full_throw_packed(fc::raw::pack(full_throw)); - commit_throw.throw_hash = fc::sha256::hash(full_throw_packed.data(), full_throw_packed.size()); - - rock_paper_scissors_throw_reveal reveal_throw; - reveal_throw.nonce2 = full_throw.nonce2; - reveal_throw.gesture = full_throw.gesture; - - // store off the reveal for applying after both players commit - committed_game_moves[commit_throw] = reveal_throw; - - signed_transaction tx; - game_move_operation move_operation; - move_operation.game_id = game_id; - move_operation.player_account_id = player_account_obj.id; - move_operation.move = commit_throw; - tx.operations = {move_operation}; - for( operation& op : tx.operations ) - { - asset f = db.current_fee_schedule().set_fee(op); - players_fees[player_id][f.asset_id] -= f.amount; - } - tx.validate(); - tx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3)); - df.sign(tx, sig_priv_key); - if (/*match_obj.match_winners.empty() &&*/ game_obj.get_state() == game_state::expecting_commit_moves) // checking again - PUSH_TX(db, tx); - } - - // spaghetti programming - // walking through all tournaments, matches and games and throwing random moves - // optionaly skip generting randomly selected moves - // every_move_is >= 0 : every game is tie - void play_games(unsigned skip_some_commits = 0, unsigned skip_some_reveals = 0, int every_move_is = -1) - { - //try - //{ - graphene::chain::database& db = df.db; - const chain_parameters& params = db.get_global_properties().parameters; - - for(const auto& tournament_id: tournaments) - { - const tournament_object& tournament = tournament_id(db); - const tournament_details_object& tournament_details = tournament.tournament_details_id(db); - rock_paper_scissors_game_options game_options = tournament.options.game_options.get(); - for(const auto& match_id: tournament_details.matches) - { - const match_object& match = match_id(db); - for(const auto& game_id: match.games ) - { - const game_object& game = game_id(db); - const rock_paper_scissors_game_details& rps_details = game.game_details.get(); - if (game.get_state() == game_state::expecting_commit_moves) - { - for(const auto& player_id: game.players) - { - if ( players_keys.find(player_id) != players_keys.end()) - { - if (!skip_some_commits || player_id.instance.value % skip_some_commits != game_id.instance.value % skip_some_commits) - { - auto iter = std::find(game.players.begin(), game.players.end(), player_id); - unsigned player_index = std::distance(game.players.begin(), iter); - if (!rps_details.commit_moves.at(player_index)) - rps_throw(game_id, player_id, - (rock_paper_scissors_gesture) (every_move_is >= 0 ? every_move_is : (std::rand() % game_options.number_of_gestures)), players_keys[player_id]); - } - } - } - } - else if (game.get_state() == game_state::expecting_reveal_moves) - { - for (unsigned i = 0; i < 2; ++i) - { - if (rps_details.commit_moves.at(i) && - !rps_details.reveal_moves.at(i)) - { - const account_id_type& player_id = game.players[i]; - if (players_keys.find(player_id) != players_keys.end()) - { - { - - auto iter = committed_game_moves.find(*rps_details.commit_moves.at(i)); - if (iter != committed_game_moves.end()) - { - if (!skip_some_reveals || player_id.instance.value % skip_some_reveals != game_id.instance.value % skip_some_reveals) - { - const rock_paper_scissors_throw_reveal& reveal = iter->second; - - game_move_operation move_operation; - move_operation.game_id = game.id; - move_operation.player_account_id = player_id; - move_operation.move = reveal; - - signed_transaction tx; - tx.operations = {move_operation}; - for( auto& op : tx.operations ) - { - asset f = db.current_fee_schedule().set_fee(op); - players_fees[player_id][f.asset_id] -= f.amount; - } - tx.validate(); - tx.set_expiration(db.head_block_time() + fc::seconds( params.block_interval * (params.maintenance_skip_slots + 1) * 3)); - df.sign(tx, players_keys[player_id]); - if (game.get_state() == game_state::expecting_reveal_moves) // check again - PUSH_TX(db, tx); - } - } - } - } - } - } - } - } - } - } - //} - //catch (fc::exception& e) - //{ - // edump((e.to_detail_string())); - // throw; - //} - } - -private: - database_fixture& df; - // index of last created tournament - fc::optional current_tournament_idx; - // index of last asset - uint64_t current_asset_idx; - // assets : core and maybe others - std::set assets; - // tournaments to be played - std::set tournaments; - // all players registered in tournaments - std::set players; - // players' private keys - std::map players_keys; - // total charges for moves made by every player - std::map> players_fees; - // store of commits and reveals - std::map committed_game_moves; - - // taken from rand.cpp - void rand_bytes(char* buf, int count) - { - fc::init_openssl(); - - int result = RAND_bytes((unsigned char*)buf, count); - if (result != 1) - FC_THROW("Error calling OpenSSL's RAND_bytes(): ${code}", ("code", (uint32_t)ERR_get_error())); - } -}; - -/// Expecting assertion - -// creating tournament - BOOST_FIXTURE_TEST_CASE( Registration_deadline_has_already, database_fixture ) { try