diff --git a/libraries/chain/CMakeLists.txt b/libraries/chain/CMakeLists.txt index 8fda8a15..c50eadaf 100644 --- a/libraries/chain/CMakeLists.txt +++ b/libraries/chain/CMakeLists.txt @@ -126,6 +126,9 @@ add_library( graphene_chain protocol/nft.cpp protocol/account_role.cpp account_role_evaluator.cpp + protocol/nft_lottery.cpp + nft_lottery_evaluator.cpp + nft_lottery_object.cpp ${HEADERS} ${PROTOCOL_HEADERS} diff --git a/libraries/chain/asset_object.cpp b/libraries/chain/asset_object.cpp index 79ad88ad..055769a8 100644 --- a/libraries/chain/asset_object.cpp +++ b/libraries/chain/asset_object.cpp @@ -252,7 +252,7 @@ map< account_id_type, vector< uint16_t > > asset_object::distribute_winners_part structurized_participants.emplace( holder, vector< uint16_t >() ); } uint64_t jackpot = get_id()( db ).dynamic_data( db ).current_supply.value * lottery_options->ticket_price.amount.value; - auto winner_numbers = db.get_winner_numbers( get_id(), holders.size(), lottery_options->winning_tickets.size() ); + auto winner_numbers = db.get_winner_numbers( get_id().instance.value, holders.size(), lottery_options->winning_tickets.size() ); auto& tickets( lottery_options->winning_tickets ); diff --git a/libraries/chain/db_block.cpp b/libraries/chain/db_block.cpp index e2fc9aab..a9c52618 100644 --- a/libraries/chain/db_block.cpp +++ b/libraries/chain/db_block.cpp @@ -710,6 +710,7 @@ void database::_apply_block( const signed_block& next_block ) perform_chain_maintenance(next_block, global_props); check_ending_lotteries(); + check_ending_nft_lotteries(); create_block_summary(next_block); place_delayed_bets(); // must happen after update_global_dynamic_data() updates the time diff --git a/libraries/chain/db_getter.cpp b/libraries/chain/db_getter.cpp index ccdfa7ad..32fa9b3a 100644 --- a/libraries/chain/db_getter.cpp +++ b/libraries/chain/db_getter.cpp @@ -109,17 +109,17 @@ uint32_t database::last_non_undoable_block_num() const return head_block_num() - _undo_db.size(); } -std::vector database::get_seeds(asset_id_type for_asset, uint8_t count_winners) const +std::vector database::get_seeds( uint32_t instance_value, uint8_t count_winners ) const { FC_ASSERT( count_winners <= 64 ); - std::string salted_string = std::string(_random_number_generator._seed) + std::to_string(for_asset.instance.value); + std::string salted_string = std::string(_random_number_generator._seed) + std::to_string(instance_value); uint32_t* seeds = (uint32_t*)(fc::sha256::hash(salted_string)._hash); std::vector result; result.reserve(64); for( int s = 0; s < 8; ++s ) { - uint32_t* sub_seeds = ( uint32_t* ) fc::sha256::hash( std::to_string( seeds[s] ) + std::to_string( for_asset.instance.value ) )._hash; + uint32_t* sub_seeds = ( uint32_t* ) fc::sha256::hash( std::to_string( seeds[s] ) + std::to_string( instance_value ) )._hash; for( int ss = 0; ss < 8; ++ss ) { result.push_back(sub_seeds[ss]); } @@ -127,14 +127,14 @@ std::vector database::get_seeds(asset_id_type for_asset, uint8_t count return result; } -const std::vector database::get_winner_numbers( asset_id_type for_asset, uint32_t count_members, uint8_t count_winners ) const +const std::vector database::get_winner_numbers( uint32_t instance_value, uint32_t count_members, uint8_t count_winners ) const { std::vector result; if( count_members < count_winners ) count_winners = count_members; if( count_winners == 0 ) return result; result.reserve(count_winners); - auto seeds = get_seeds(for_asset, count_winners); + auto seeds = get_seeds(instance_value, count_winners); for (auto current_seed = seeds.begin(); current_seed != seeds.end(); ++current_seed) { uint8_t winner_num = *current_seed % count_members; diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index 84203e4a..5fc7f341 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -87,6 +87,7 @@ #include #include #include +#include #include @@ -283,6 +284,9 @@ void database::initialize_evaluators() register_evaluator(); register_evaluator(); register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); } void database::initialize_indexes() diff --git a/libraries/chain/db_management.cpp b/libraries/chain/db_management.cpp index c6380b8c..e8175c60 100644 --- a/libraries/chain/db_management.cpp +++ b/libraries/chain/db_management.cpp @@ -306,6 +306,24 @@ void database::check_ending_lotteries() } catch( ... ) {} } +void database::check_ending_nft_lotteries() +{ + try { + const auto &nft_lotteries_idx = get_index_type().indices().get(); + for (auto checking_token : nft_lotteries_idx) + { + FC_ASSERT(checking_token.is_lottery()); + const auto &lottery_options = checking_token.lottery_data->lottery_options; + FC_ASSERT(lottery_options.is_active); + // Check the current supply of lottery tokens + auto current_supply = checking_token.get_token_current_supply(*this); + if ((lottery_options.ending_on_soldout && (current_supply == checking_token.max_supply)) || + (lottery_options.end_date != time_point_sec() && (lottery_options.end_date <= head_block_time()))) + checking_token.end_lottery(*this); + } + } catch( ... ) {} +} + void database::check_lottery_end_by_participants( asset_id_type asset_id ) { try { diff --git a/libraries/chain/db_notify.cpp b/libraries/chain/db_notify.cpp index 6300e685..20700dc8 100644 --- a/libraries/chain/db_notify.cpp +++ b/libraries/chain/db_notify.cpp @@ -353,6 +353,13 @@ struct get_impacted_account_visitor void operator()( const account_role_delete_operation& op ){ _impacted.insert( op.owner ); } + void operator()( const nft_lottery_token_purchase_operation& op ){ + _impacted.insert( op.buyer ); + } + void operator()( const nft_lottery_reward_operation& op ) { + _impacted.insert( op.winner ); + } + void operator()( const nft_lottery_end_operation& op ) {} }; void graphene::chain::operation_get_impacted_accounts( const operation& op, flat_set& result ) diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index b513ac7c..caefbc67 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -266,6 +266,7 @@ namespace graphene { namespace chain { void check_lottery_end_by_participants( asset_id_type asset_id ); void check_ending_lotteries(); + void check_ending_nft_lotteries(); //////////////////// db_getter.cpp //////////////////// @@ -278,8 +279,8 @@ namespace graphene { namespace chain { const node_property_object& get_node_properties()const; const fee_schedule& current_fee_schedule()const; const account_statistics_object& get_account_stats_by_owner( account_id_type owner )const; - const std::vector get_winner_numbers( asset_id_type for_asset, uint32_t count_members, uint8_t count_winners ) const; - std::vector get_seeds( asset_id_type for_asset, uint8_t count_winners )const; + const std::vector get_winner_numbers( uint32_t instance_value, uint32_t count_members, uint8_t count_winners ) const; + std::vector get_seeds( uint32_t instance_value, uint8_t count_winners )const; uint64_t get_random_bits( uint64_t bound ); const witness_schedule_object& get_witness_schedule_object()const; bool item_locked(const nft_id_type& item)const; diff --git a/libraries/chain/include/graphene/chain/nft_lottery_evaluator.hpp b/libraries/chain/include/graphene/chain/nft_lottery_evaluator.hpp new file mode 100644 index 00000000..0839cbbd --- /dev/null +++ b/libraries/chain/include/graphene/chain/nft_lottery_evaluator.hpp @@ -0,0 +1,39 @@ +#pragma once +#include +#include +#include + +namespace graphene +{ + namespace chain + { + + class nft_lottery_token_purchase_evaluator : public evaluator + { + public: + typedef nft_lottery_token_purchase_operation operation_type; + + void_result do_evaluate(const nft_lottery_token_purchase_operation &o); + object_id_type do_apply(const nft_lottery_token_purchase_operation &o); + }; + + class nft_lottery_reward_evaluator : public evaluator + { + public: + typedef nft_lottery_reward_operation operation_type; + + void_result do_evaluate(const nft_lottery_reward_operation &o); + void_result do_apply(const nft_lottery_reward_operation &o); + }; + + class nft_lottery_end_evaluator : public evaluator + { + public: + typedef nft_lottery_end_operation operation_type; + + void_result do_evaluate(const nft_lottery_end_operation &o); + void_result do_apply(const nft_lottery_end_operation &o); + }; + + } // namespace chain +} // namespace graphene diff --git a/libraries/chain/include/graphene/chain/nft_object.hpp b/libraries/chain/include/graphene/chain/nft_object.hpp index 6a150852..dc7f2b77 100644 --- a/libraries/chain/include/graphene/chain/nft_object.hpp +++ b/libraries/chain/include/graphene/chain/nft_object.hpp @@ -6,6 +6,16 @@ namespace graphene { namespace chain { using namespace graphene::db; + struct nft_lottery_data + { + nft_lottery_data() {} + nft_lottery_data(const nft_lottery_options &options) + : lottery_options(options), jackpot(asset(0, options.ticket_price.asset_id)) {} + nft_lottery_options lottery_options; + asset jackpot; + share_type sweeps_tickets_sold; + }; + class nft_metadata_object : public abstract_object { public: @@ -21,6 +31,21 @@ namespace graphene { namespace chain { bool is_transferable = false; bool is_sellable = true; optional account_role; + share_type max_supply = GRAPHENE_MAX_SHARE_SUPPLY; + optional lottery_data; + + nft_metadata_id_type get_id() const { return id; } + bool is_lottery() const { return lottery_data.valid(); } + uint32_t get_owner_num() const { return owner.instance.value; } + time_point_sec get_lottery_expiration() const; + asset get_lottery_jackpot() const; + share_type get_token_current_supply(database &db) const; + vector get_holders(database &db) const; + vector get_ticket_ids(database &db) const; + void distribute_benefactors_part(database &db); + map> distribute_winners_part(database &db); + void distribute_sweeps_holders_part(database &db); + void end_lottery(database &db); }; class nft_object : public abstract_object @@ -36,8 +61,23 @@ namespace graphene { namespace chain { std::string token_uri; }; + struct nft_lottery_comparer + { + bool operator()(const nft_metadata_object& lhs, const nft_metadata_object& rhs) const + { + if ( !lhs.is_lottery() ) return false; + if ( !lhs.lottery_data->lottery_options.is_active && !rhs.is_lottery()) return true; // not active lotteries first, just assets then + if ( !lhs.lottery_data->lottery_options.is_active ) return false; + if ( lhs.lottery_data->lottery_options.is_active && ( !rhs.is_lottery() || !rhs.lottery_data->lottery_options.is_active ) ) return true; + return lhs.get_lottery_expiration() > rhs.get_lottery_expiration(); + } + }; + struct by_name; struct by_symbol; + struct active_nft_lotteries; + struct by_nft_lottery; + struct by_nft_lottery_owner; using nft_metadata_multi_index_type = multi_index_container< nft_metadata_object, indexed_by< @@ -49,6 +89,34 @@ namespace graphene { namespace chain { >, ordered_unique< tag, member + >, + ordered_non_unique< tag, + identity< nft_metadata_object >, + nft_lottery_comparer + >, + ordered_unique< tag, + composite_key< + nft_metadata_object, + const_mem_fun, + member + >, + composite_key_compare< + std::greater< bool >, + std::greater< object_id_type > + > + >, + ordered_unique< tag, + composite_key< + nft_metadata_object, + const_mem_fun, + const_mem_fun, + member + >, + composite_key_compare< + std::greater< bool >, + std::greater< uint32_t >, + std::greater< object_id_type > + > > > >; @@ -88,6 +156,8 @@ namespace graphene { namespace chain { } } // graphene::chain +FC_REFLECT( graphene::chain::nft_lottery_data, (lottery_options)(jackpot)(sweeps_tickets_sold) ) + FC_REFLECT_DERIVED( graphene::chain::nft_metadata_object, (graphene::db::object), (owner) (name) @@ -97,7 +167,9 @@ FC_REFLECT_DERIVED( graphene::chain::nft_metadata_object, (graphene::db::object) (revenue_split) (is_transferable) (is_sellable) - (account_role) ) + (account_role) + (max_supply) + (lottery_data) ) FC_REFLECT_DERIVED( graphene::chain::nft_object, (graphene::db::object), (nft_metadata_id) diff --git a/libraries/chain/include/graphene/chain/protocol/nft_lottery.hpp b/libraries/chain/include/graphene/chain/protocol/nft_lottery.hpp new file mode 100644 index 00000000..0c8ea855 --- /dev/null +++ b/libraries/chain/include/graphene/chain/protocol/nft_lottery.hpp @@ -0,0 +1,86 @@ +#pragma once +#include +#include + +namespace graphene +{ + namespace chain + { + struct nft_lottery_token_purchase_operation : public base_operation + { + struct fee_parameters_type + { + uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; + }; + asset fee; + // Lottery NFT Metadata + nft_metadata_id_type lottery_id; + // Buyer purchasing lottery tickets + account_id_type buyer; + // count of tickets to buy + uint64_t tickets_to_buy; + // amount that can spent + asset amount; + + extensions_type extensions; + + account_id_type fee_payer() const { return buyer; } + void validate() const; + share_type calculate_fee(const fee_parameters_type &k) const; + }; + + struct nft_lottery_reward_operation : public base_operation + { + struct fee_parameters_type + { + uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; + }; + + asset fee; + // Lottery NFT Metadata + nft_metadata_id_type lottery_id; + // winner account + account_id_type winner; + // amount that won + asset amount; + // percentage of jackpot that user won + uint16_t win_percentage; + // true if recieved from benefators section of lottery; false otherwise + bool is_benefactor_reward; + + uint64_t winner_ticket_id; + + extensions_type extensions; + + account_id_type fee_payer() const { return account_id_type(); } + void validate() const {}; + share_type calculate_fee(const fee_parameters_type &k) const { return k.fee; }; + }; + + struct nft_lottery_end_operation : public base_operation + { + struct fee_parameters_type + { + uint64_t fee = GRAPHENE_BLOCKCHAIN_PRECISION; + }; + + asset fee; + // Lottery NFT Metadata + nft_metadata_id_type lottery_id; + + extensions_type extensions; + + account_id_type fee_payer() const { return account_id_type(); } + void validate() const {} + share_type calculate_fee(const fee_parameters_type &k) const { return k.fee; } + }; + + } // namespace chain +} // namespace graphene + +FC_REFLECT(graphene::chain::nft_lottery_token_purchase_operation::fee_parameters_type, (fee)) +FC_REFLECT(graphene::chain::nft_lottery_reward_operation::fee_parameters_type, (fee)) +FC_REFLECT(graphene::chain::nft_lottery_end_operation::fee_parameters_type, (fee)) +FC_REFLECT(graphene::chain::nft_lottery_token_purchase_operation, (fee)(lottery_id)(buyer)(tickets_to_buy)(amount)(extensions)) +FC_REFLECT(graphene::chain::nft_lottery_reward_operation, (fee)(lottery_id)(winner)(amount)(win_percentage)(is_benefactor_reward)(winner_ticket_id)(extensions)) +FC_REFLECT(graphene::chain::nft_lottery_end_operation, (fee)(lottery_id)(extensions)) \ No newline at end of file diff --git a/libraries/chain/include/graphene/chain/protocol/nft_ops.hpp b/libraries/chain/include/graphene/chain/protocol/nft_ops.hpp index 843df403..3809b9e3 100644 --- a/libraries/chain/include/graphene/chain/protocol/nft_ops.hpp +++ b/libraries/chain/include/graphene/chain/protocol/nft_ops.hpp @@ -4,6 +4,27 @@ namespace graphene { namespace chain { + struct nft_lottery_benefactor { + account_id_type id; + uint16_t share; // percent * GRAPHENE_1_PERCENT + nft_lottery_benefactor() = default; + nft_lottery_benefactor( const nft_lottery_benefactor & ) = default; + nft_lottery_benefactor( account_id_type _id, uint16_t _share ) : id( _id ), share( _share ) {} + }; + + struct nft_lottery_options + { + std::vector benefactors; + // specifying winning tickets as shares that will be issued + std::vector winning_tickets; + asset ticket_price; + time_point_sec end_date; + bool ending_on_soldout; + bool is_active; + + void validate() const; + }; + struct nft_metadata_create_operation : public base_operation { struct fee_parameters_type @@ -23,6 +44,10 @@ namespace graphene { namespace chain { bool is_sellable = true; // Accounts Role optional account_role; + // Max number of NFTs that can be minted from the metadata + optional max_supply; + // Lottery configuration + optional lottery_options; extensions_type extensions; account_id_type fee_payer()const { return owner; } @@ -133,6 +158,9 @@ namespace graphene { namespace chain { } } // graphene::chain +FC_REFLECT( graphene::chain::nft_lottery_benefactor, (id)(share) ) +FC_REFLECT( graphene::chain::nft_lottery_options, (benefactors)(winning_tickets)(ticket_price)(end_date)(ending_on_soldout)(is_active) ) + FC_REFLECT( graphene::chain::nft_metadata_create_operation::fee_parameters_type, (fee) (price_per_kbyte) ) FC_REFLECT( graphene::chain::nft_metadata_update_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::nft_mint_operation::fee_parameters_type, (fee) (price_per_kbyte) ) @@ -140,7 +168,7 @@ FC_REFLECT( graphene::chain::nft_safe_transfer_from_operation::fee_parameters_ty FC_REFLECT( graphene::chain::nft_approve_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::nft_set_approval_for_all_operation::fee_parameters_type, (fee) ) -FC_REFLECT( graphene::chain::nft_metadata_create_operation, (fee) (owner) (name) (symbol) (base_uri) (revenue_partner) (revenue_split) (is_transferable) (is_sellable) (account_role) (extensions) ) +FC_REFLECT( graphene::chain::nft_metadata_create_operation, (fee) (owner) (name) (symbol) (base_uri) (revenue_partner) (revenue_split) (is_transferable) (is_sellable) (account_role) (max_supply) (lottery_options) (extensions) ) FC_REFLECT( graphene::chain::nft_metadata_update_operation, (fee) (owner) (nft_metadata_id) (name) (symbol) (base_uri) (revenue_partner) (revenue_split) (is_transferable) (is_sellable) (account_role) (extensions) ) FC_REFLECT( graphene::chain::nft_mint_operation, (fee) (payer) (nft_metadata_id) (owner) (approved) (approved_operators) (token_uri) (extensions) ) FC_REFLECT( graphene::chain::nft_safe_transfer_from_operation, (fee) (operator_) (from) (to) (token_id) (data) (extensions) ) diff --git a/libraries/chain/include/graphene/chain/protocol/operations.hpp b/libraries/chain/include/graphene/chain/protocol/operations.hpp index a8c51781..c5695f8b 100644 --- a/libraries/chain/include/graphene/chain/protocol/operations.hpp +++ b/libraries/chain/include/graphene/chain/protocol/operations.hpp @@ -50,6 +50,7 @@ #include #include #include +#include namespace graphene { namespace chain { @@ -159,7 +160,10 @@ namespace graphene { namespace chain { nft_set_approval_for_all_operation, account_role_create_operation, account_role_update_operation, - account_role_delete_operation + account_role_delete_operation, + nft_lottery_token_purchase_operation, + nft_lottery_reward_operation, + nft_lottery_end_operation > operation; /// @} // operations group diff --git a/libraries/chain/include/graphene/chain/rbac_hardfork_visitor.hpp b/libraries/chain/include/graphene/chain/rbac_hardfork_visitor.hpp index 6851c4e7..11ffe0e4 100644 --- a/libraries/chain/include/graphene/chain/rbac_hardfork_visitor.hpp +++ b/libraries/chain/include/graphene/chain/rbac_hardfork_visitor.hpp @@ -36,6 +36,9 @@ namespace graphene case operation::tag::value: case operation::tag::value: case operation::tag::value: + case operation::tag::value: + case operation::tag::value: + case operation::tag::value: FC_ASSERT(block_time >= HARDFORK_NFT_TIME, "Custom permissions and roles not allowed on this operation yet!"); break; default: diff --git a/libraries/chain/nft_evaluator.cpp b/libraries/chain/nft_evaluator.cpp index f7f007ff..68209126 100644 --- a/libraries/chain/nft_evaluator.cpp +++ b/libraries/chain/nft_evaluator.cpp @@ -24,6 +24,15 @@ void_result nft_metadata_create_evaluator::do_evaluate( const nft_metadata_creat const auto& ar_obj = (*op.account_role)(db()); FC_ASSERT(ar_obj.owner == op.owner, "Only the Account Role created by the owner can be attached"); } + + // Lottery Related + if (!op.lottery_options) { + return void_result(); + } + FC_ASSERT((*op.lottery_options).end_date > now || (*op.lottery_options).end_date == time_point_sec()); + if (op.max_supply) { + FC_ASSERT(*op.max_supply >= 5); + } return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } @@ -39,6 +48,12 @@ object_id_type nft_metadata_create_evaluator::do_apply( const nft_metadata_creat obj.is_transferable = op.is_transferable; obj.is_sellable = op.is_sellable; obj.account_role = op.account_role; + if (op.max_supply) { + obj.max_supply = *op.max_supply; + } + if (op.lottery_options) { + obj.lottery_data = nft_lottery_data(*op.lottery_options); + } }); return new_nft_metadata_object.id; } FC_CAPTURE_AND_RETHROW( (op) ) } @@ -110,6 +125,7 @@ void_result nft_mint_evaluator::do_evaluate( const nft_mint_operation& op ) FC_ASSERT( itr_nft_md != idx_nft_md.end(), "NFT metadata not found" ); FC_ASSERT( itr_nft_md->owner == op.payer, "Only metadata owner can mint NFT" ); + FC_ASSERT(itr_nft_md->get_token_current_supply(db()) < itr_nft_md->max_supply, "NFTs can't be minted more than max_supply"); return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } diff --git a/libraries/chain/nft_lottery_evaluator.cpp b/libraries/chain/nft_lottery_evaluator.cpp new file mode 100644 index 00000000..c208c731 --- /dev/null +++ b/libraries/chain/nft_lottery_evaluator.cpp @@ -0,0 +1,130 @@ +#include +#include +#include +#include +#include + +namespace graphene +{ + namespace chain + { + void_result nft_lottery_token_purchase_evaluator::do_evaluate(const nft_lottery_token_purchase_operation &op) + { + try + { + const database &d = db(); + auto now = d.head_block_time(); + FC_ASSERT(now >= HARDFORK_NFT_TIME, "Not allowed until NFT HF"); + op.buyer(d); + const auto &lottery_md_obj = op.lottery_id(d); + FC_ASSERT(lottery_md_obj.is_lottery(), "Not a lottery type"); + if (lottery_md_obj.account_role) + { + const auto &ar_idx = d.get_index_type().indices().get(); + auto ar_itr = ar_idx.find(*lottery_md_obj.account_role); + if (ar_itr != ar_idx.end()) + { + FC_ASSERT(d.account_role_valid(*ar_itr, op.buyer, get_type()), "Account role not valid"); + } + } + + auto lottery_options = lottery_md_obj.lottery_data->lottery_options; + FC_ASSERT(lottery_options.ticket_price.asset_id == op.amount.asset_id); + FC_ASSERT((double)op.amount.amount.value / lottery_options.ticket_price.amount.value == (double)op.tickets_to_buy); + return void_result(); + } + FC_CAPTURE_AND_RETHROW((op)) + } + + object_id_type nft_lottery_token_purchase_evaluator::do_apply(const nft_lottery_token_purchase_operation &op) + { + try + { + transaction_evaluation_state nft_mint_context(&db()); + nft_mint_context.skip_fee_schedule_check = true; + const auto &lottery_md_obj = op.lottery_id(db()); + nft_id_type nft_id; + for (size_t i = 0; i < op.tickets_to_buy; i++) + { + nft_mint_operation mint_op; + mint_op.payer = lottery_md_obj.owner; + mint_op.nft_metadata_id = lottery_md_obj.id; + mint_op.owner = op.buyer; + nft_id = db().apply_operation(nft_mint_context, mint_op).get(); + } + db().adjust_balance(op.buyer, -op.amount); + db().modify(lottery_md_obj, [&](nft_metadata_object &obj) { + obj.lottery_data->jackpot += op.amount; + }); + return nft_id; + } + FC_CAPTURE_AND_RETHROW((op)) + } + + void_result nft_lottery_reward_evaluator::do_evaluate(const nft_lottery_reward_operation &op) + { + try + { + const database &d = db(); + auto now = d.head_block_time(); + FC_ASSERT(now >= HARDFORK_NFT_TIME, "Not allowed until NFT HF"); + op.winner(d); + + const auto &lottery_md_obj = op.lottery_id(d); + FC_ASSERT(lottery_md_obj.is_lottery()); + + const auto &lottery_options = lottery_md_obj.lottery_data->lottery_options; + FC_ASSERT(lottery_options.is_active); + FC_ASSERT(lottery_md_obj.get_lottery_jackpot() >= op.amount); + return void_result(); + } + FC_CAPTURE_AND_RETHROW((op)) + } + + void_result nft_lottery_reward_evaluator::do_apply(const nft_lottery_reward_operation &op) + { + try + { + const auto &lottery_md_obj = op.lottery_id(db()); + db().adjust_balance(op.winner, op.amount); + db().modify(lottery_md_obj, [&](nft_metadata_object &obj) { + obj.lottery_data->jackpot -= op.amount; + }); + return void_result(); + } + FC_CAPTURE_AND_RETHROW((op)) + } + + void_result nft_lottery_end_evaluator::do_evaluate(const nft_lottery_end_operation &op) + { + try + { + const database &d = db(); + auto now = d.head_block_time(); + FC_ASSERT(now >= HARDFORK_NFT_TIME, "Not allowed until NFT HF"); + const auto &lottery_md_obj = op.lottery_id(d); + FC_ASSERT(lottery_md_obj.is_lottery()); + + const auto &lottery_options = lottery_md_obj.lottery_data->lottery_options; + FC_ASSERT(lottery_options.is_active); + FC_ASSERT(lottery_md_obj.get_lottery_jackpot().amount == 0); + return void_result(); + } + FC_CAPTURE_AND_RETHROW((op)) + } + + void_result nft_lottery_end_evaluator::do_apply(const nft_lottery_end_operation &op) + { + try + { + const auto &lottery_md_obj = op.lottery_id(db()); + db().modify(lottery_md_obj, [&](nft_metadata_object &obj) { + obj.lottery_data->sweeps_tickets_sold = obj.get_token_current_supply(db()); + obj.lottery_data->lottery_options.is_active = false; + }); + return void_result(); + } + FC_CAPTURE_AND_RETHROW((op)) + } + } // namespace chain +} // namespace graphene \ No newline at end of file diff --git a/libraries/chain/nft_lottery_object.cpp b/libraries/chain/nft_lottery_object.cpp new file mode 100644 index 00000000..58261bd7 --- /dev/null +++ b/libraries/chain/nft_lottery_object.cpp @@ -0,0 +1,170 @@ +#include +#include + +namespace graphene +{ + namespace chain + { + time_point_sec nft_metadata_object::get_lottery_expiration() const + { + if (lottery_data) + return lottery_data->lottery_options.end_date; + return time_point_sec(); + } + + asset nft_metadata_object::get_lottery_jackpot() const + { + if (lottery_data) + return lottery_data->jackpot; + return asset(); + } + + share_type nft_metadata_object::get_token_current_supply(database &db) const + { + share_type current_supply; + const auto &idx_lottery_by_md = db.get_index_type().indices().get(); + auto lottery_range = idx_lottery_by_md.equal_range(id); + current_supply = std::distance(lottery_range.first, lottery_range.second); + return current_supply; + } + + vector nft_metadata_object::get_holders(database &db) const + { + const auto &idx_lottery_by_md = db.get_index_type().indices().get(); + auto lottery_range = idx_lottery_by_md.equal_range(id); + vector holders; + holders.reserve(std::distance(lottery_range.first, lottery_range.second)); + std::for_each(lottery_range.first, lottery_range.second, + [&](const nft_object &ticket) { + holders.emplace_back(ticket.owner); + }); + return holders; + } + + vector nft_metadata_object::get_ticket_ids(database &db) const + { + const auto &idx_lottery_by_md = db.get_index_type().indices().get(); + auto lottery_range = idx_lottery_by_md.equal_range(id); + vector tickets; + tickets.reserve(std::distance(lottery_range.first, lottery_range.second)); + std::for_each(lottery_range.first, lottery_range.second, + [&](const nft_object &ticket) { + tickets.emplace_back(ticket.id.instance()); + }); + return tickets; + } + + void nft_metadata_object::distribute_benefactors_part(database &db) + { + transaction_evaluation_state eval(&db); + const auto &lottery_options = lottery_data->lottery_options; + share_type jackpot = lottery_options.ticket_price.amount * get_token_current_supply(db).value; + + for (auto benefactor : lottery_options.benefactors) + { + nft_lottery_reward_operation reward_op; + reward_op.lottery_id = id; + reward_op.winner = benefactor.id; + reward_op.is_benefactor_reward = true; + reward_op.win_percentage = benefactor.share; + reward_op.amount = asset(jackpot.value * benefactor.share / GRAPHENE_100_PERCENT, lottery_options.ticket_price.asset_id); + db.apply_operation(eval, reward_op); + } + } + + map> nft_metadata_object::distribute_winners_part(database &db) + { + transaction_evaluation_state eval(&db); + auto current_supply = get_token_current_supply(db); + auto &lottery_options = lottery_data->lottery_options; + + auto holders = get_holders(db); + vector ticket_ids = get_ticket_ids(db); + FC_ASSERT(current_supply.value == (int64_t)holders.size()); + FC_ASSERT(lottery_data->jackpot.amount.value == current_supply.value * lottery_options.ticket_price.amount.value); + map> structurized_participants; + for (account_id_type holder : holders) + { + if (!structurized_participants.count(holder)) + structurized_participants.emplace(holder, vector()); + } + uint64_t jackpot = lottery_data->jackpot.amount.value; + auto winner_numbers = db.get_winner_numbers(get_id().instance.value, holders.size(), lottery_options.winning_tickets.size()); + + auto &tickets(lottery_options.winning_tickets); + + if (holders.size() < tickets.size()) + { + uint16_t percents_to_distribute = 0; + for (auto i = tickets.begin() + holders.size(); i != tickets.end();) + { + percents_to_distribute += *i; + i = tickets.erase(i); + } + for (auto t = tickets.begin(); t != tickets.begin() + holders.size(); ++t) + *t += percents_to_distribute / holders.size(); + } + auto sweeps_distribution_percentage = db.get_global_properties().parameters.sweeps_distribution_percentage(); + for (size_t c = 0; c < winner_numbers.size(); ++c) + { + auto winner_num = winner_numbers[c]; + nft_lottery_reward_operation reward_op; + reward_op.lottery_id = id; + reward_op.is_benefactor_reward = false; + reward_op.winner = holders[winner_num]; + if (ticket_ids.size() > winner_num) + { + reward_op.winner_ticket_id = ticket_ids[winner_num]; + } + reward_op.win_percentage = tickets[c]; + reward_op.amount = asset(jackpot * tickets[c] * (1. - sweeps_distribution_percentage / (double)GRAPHENE_100_PERCENT) / GRAPHENE_100_PERCENT, lottery_options.ticket_price.asset_id); + db.apply_operation(eval, reward_op); + + structurized_participants[holders[winner_num]].push_back(tickets[c]); + } + return structurized_participants; + } + + void nft_metadata_object::distribute_sweeps_holders_part(database &db) + { + transaction_evaluation_state eval(&db); + auto &asset_bal_idx = db.get_index_type().indices().get(); + auto sweeps_params = db.get_global_properties().parameters; + uint64_t distribution_asset_supply = sweeps_params.sweeps_distribution_asset()(db).dynamic_data(db).current_supply.value; + const auto range = asset_bal_idx.equal_range(boost::make_tuple(sweeps_params.sweeps_distribution_asset())); + asset remaining_jackpot = get_id()(db).lottery_data->jackpot; + uint64_t holders_sum = 0; + for (const account_balance_object &holder_balance : boost::make_iterator_range(range.first, range.second)) + { + int64_t holder_part = remaining_jackpot.amount.value / (double)distribution_asset_supply * holder_balance.balance.value * SWEEPS_VESTING_BALANCE_MULTIPLIER; + db.adjust_sweeps_vesting_balance(holder_balance.owner, holder_part); + holders_sum += holder_part; + } + uint64_t balance_rest = remaining_jackpot.amount.value * SWEEPS_VESTING_BALANCE_MULTIPLIER - holders_sum; + db.adjust_sweeps_vesting_balance(sweeps_params.sweeps_vesting_accumulator_account(), balance_rest); + db.modify(get_id()(db), [&](nft_metadata_object &obj) { + obj.lottery_data->jackpot -= remaining_jackpot; + }); + } + + void nft_metadata_object::end_lottery(database &db) + { + transaction_evaluation_state eval(&db); + const auto &lottery_options = lottery_data->lottery_options; + + FC_ASSERT(is_lottery()); + FC_ASSERT(lottery_options.is_active && (lottery_options.end_date <= db.head_block_time() || lottery_options.ending_on_soldout)); + + auto participants = distribute_winners_part(db); + if (participants.size() > 0) + { + distribute_benefactors_part(db); + distribute_sweeps_holders_part(db); + } + + nft_lottery_end_operation end_op; + end_op.lottery_id = get_id(); + db.apply_operation(eval, end_op); + } + } // namespace chain +} // namespace graphene \ No newline at end of file diff --git a/libraries/chain/proposal_evaluator.cpp b/libraries/chain/proposal_evaluator.cpp index ce2045e6..254b3b5b 100644 --- a/libraries/chain/proposal_evaluator.cpp +++ b/libraries/chain/proposal_evaluator.cpp @@ -208,6 +208,17 @@ struct proposal_operation_hardfork_visitor FC_ASSERT( block_time >= HARDFORK_NFT_TIME, "account_role_delete_operation not allowed yet!" ); } + void operator()(const nft_lottery_token_purchase_operation &v) const { + FC_ASSERT( block_time >= HARDFORK_NFT_TIME, "nft_lottery_token_purchase_operation not allowed yet!" ); + } + + void operator()(const nft_lottery_reward_operation &v) const { + FC_ASSERT( block_time >= HARDFORK_NFT_TIME, "nft_lottery_reward_operation not allowed yet!" ); + } + + void operator()(const nft_lottery_end_operation &v) const { + FC_ASSERT( block_time >= HARDFORK_NFT_TIME, "nft_lottery_end_operation not allowed yet!" ); + } // loop and self visit in proposals void operator()(const proposal_create_operation &v) const { diff --git a/libraries/chain/protocol/nft.cpp b/libraries/chain/protocol/nft.cpp index 4a66f330..ca7fe816 100644 --- a/libraries/chain/protocol/nft.cpp +++ b/libraries/chain/protocol/nft.cpp @@ -45,6 +45,10 @@ void nft_metadata_create_operation::validate() const FC_ASSERT(fee.amount >= 0, "Fee must not be negative"); FC_ASSERT(is_valid_nft_token_name(name), "Invalid NFT name provided"); FC_ASSERT(is_valid_nft_token_name(symbol), "Invalid NFT symbol provided"); + if (lottery_options) + { + (*lottery_options).validate(); + } } void nft_metadata_update_operation::validate() const diff --git a/libraries/chain/protocol/nft_lottery.cpp b/libraries/chain/protocol/nft_lottery.cpp new file mode 100644 index 00000000..16454962 --- /dev/null +++ b/libraries/chain/protocol/nft_lottery.cpp @@ -0,0 +1,38 @@ +#include +#include +#include + +namespace graphene +{ + namespace chain + { + + void nft_lottery_options::validate() const + { + FC_ASSERT(winning_tickets.size() <= 64); + FC_ASSERT(ticket_price.amount >= 1); + uint16_t total = 0; + for (auto benefactor : benefactors) + { + total += benefactor.share; + } + for (auto share : winning_tickets) + { + total += share; + } + FC_ASSERT(total == GRAPHENE_100_PERCENT, "distribution amount not equals GRAPHENE_100_PERCENT"); + FC_ASSERT(ending_on_soldout == true || end_date != time_point_sec(), "lottery may not end"); + } + + share_type nft_lottery_token_purchase_operation::calculate_fee(const fee_parameters_type &k) const + { + return k.fee; + } + + void nft_lottery_token_purchase_operation::validate() const + { + FC_ASSERT(fee.amount >= 0, "Fee must not be negative"); + FC_ASSERT(tickets_to_buy > 0); + } + } // namespace chain +} // namespace graphene \ No newline at end of file diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index 3a381585..8a3609dd 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include @@ -323,6 +324,14 @@ void database_fixture::verify_asset_supplies( const database& db ) } } + for (const nft_metadata_object &o : db.get_index_type().indices()) + { + if (o.lottery_data) + { + total_balances[o.lottery_data->jackpot.asset_id] += o.lottery_data->jackpot.amount; + } + } + uint64_t sweeps_vestings = 0; for( const sweeps_vesting_balance_object& svbo: db.get_index_type< sweeps_vesting_balance_index >().indices() ) sweeps_vestings += svbo.balance;