From 3032185b5c2dd7ad00ec82579dd74bce1e62c299 Mon Sep 17 00:00:00 2001 From: sierra19XX <15652887+sierra19XX@users.noreply.github.com> Date: Mon, 15 Jun 2020 12:12:56 +0000 Subject: [PATCH] ppy marketplace 1 - add evaluators and objects --- libraries/chain/CMakeLists.txt | 3 + libraries/chain/db_block.cpp | 1 + libraries/chain/db_init.cpp | 12 + libraries/chain/db_notify.cpp | 13 + libraries/chain/db_update.cpp | 26 + .../chain/include/graphene/chain/database.hpp | 1 + .../graphene/chain/offer_evaluator.hpp | 34 ++ .../include/graphene/chain/offer_object.hpp | 117 ++++ .../include/graphene/chain/protocol/offer.hpp | 106 ++++ .../graphene/chain/protocol/operations.hpp | 6 +- .../include/graphene/chain/protocol/types.hpp | 12 +- libraries/chain/offer_evaluator.cpp | 173 ++++++ libraries/chain/protocol/offer.cpp | 46 ++ programs/js_operation_serializer/main.cpp | 1 + tests/tests/operation_tests.cpp | 508 ++++++++++++++++++ 15 files changed, 1057 insertions(+), 2 deletions(-) create mode 100644 libraries/chain/include/graphene/chain/offer_evaluator.hpp create mode 100644 libraries/chain/include/graphene/chain/offer_object.hpp create mode 100644 libraries/chain/include/graphene/chain/protocol/offer.hpp create mode 100644 libraries/chain/offer_evaluator.cpp create mode 100644 libraries/chain/protocol/offer.cpp diff --git a/libraries/chain/CMakeLists.txt b/libraries/chain/CMakeLists.txt index 07f1ea0a..d9c0e042 100644 --- a/libraries/chain/CMakeLists.txt +++ b/libraries/chain/CMakeLists.txt @@ -61,6 +61,7 @@ add_library( graphene_chain protocol/vote.cpp protocol/tournament.cpp protocol/small_ops.cpp + protocol/offer.cpp genesis_state.cpp get_config.cpp @@ -115,6 +116,8 @@ add_library( graphene_chain affiliate_payout.cpp + offer_evaluator.cpp + ${HEADERS} ${PROTOCOL_HEADERS} "${CMAKE_CURRENT_BINARY_DIR}/include/graphene/chain/hardfork.hpp" diff --git a/libraries/chain/db_block.cpp b/libraries/chain/db_block.cpp index b45d922b..b9938386 100644 --- a/libraries/chain/db_block.cpp +++ b/libraries/chain/db_block.cpp @@ -721,6 +721,7 @@ void database::_apply_block( const signed_block& next_block ) update_withdraw_permissions(); update_tournaments(); update_betting_markets(next_block.timestamp); + finalize_expired_offers(); // n.b., update_maintenance_flag() happens this late // because get_slot_time() / get_slot_at_time() is needed above diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index 4e30029b..65b589ff 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -49,6 +49,7 @@ #include #include #include +#include #include @@ -77,6 +78,7 @@ #include #include #include +#include #include @@ -169,6 +171,11 @@ const uint8_t betting_market_position_object::type_id; const uint8_t global_betting_statistics_object::space_id; const uint8_t global_betting_statistics_object::type_id; +const uint8_t offer_object::space_id; +const uint8_t offer_object::type_id; + +const uint8_t offer_history_object::space_id; +const uint8_t offer_history_object::type_id; void database::initialize_evaluators() { @@ -243,6 +250,9 @@ void database::initialize_evaluators() register_evaluator(); register_evaluator(); register_evaluator(); + register_evaluator(); + register_evaluator(); + register_evaluator(); } void database::initialize_indexes() @@ -284,6 +294,7 @@ void database::initialize_indexes() tournament_details_idx->add_secondary_index(); add_index< primary_index >(); add_index< primary_index >(); + add_index< primary_index >(); //Implementation object indexes add_index< primary_index >(); @@ -313,6 +324,7 @@ void database::initialize_indexes() add_index< primary_index >(); add_index< primary_index >(); + add_index< primary_index >(); } diff --git a/libraries/chain/db_notify.cpp b/libraries/chain/db_notify.cpp index e91eaa6b..db4050ca 100644 --- a/libraries/chain/db_notify.cpp +++ b/libraries/chain/db_notify.cpp @@ -293,6 +293,19 @@ struct get_impacted_account_visitor void operator()( const sweeps_vesting_claim_operation& op ) { _impacted.insert( op.account ); } + + void operator()( const offer_operation& op ) + { + _impacted.insert( op.issuer ); + } + void operator()( const bid_operation& op ) + { + _impacted.insert( op.bidder ); + } + void operator()( const finalize_offer_operation& op ) + { + _impacted.insert( op.fee_paying_account ); + } }; void graphene::chain::operation_get_impacted_accounts( const operation& op, flat_set& result ) diff --git a/libraries/chain/db_update.cpp b/libraries/chain/db_update.cpp index 5c0fbfc9..a08c33fd 100644 --- a/libraries/chain/db_update.cpp +++ b/libraries/chain/db_update.cpp @@ -705,4 +705,30 @@ void database::update_betting_markets(fc::time_point_sec current_block_time) remove_completed_events(); } +void database::finalize_expired_offers(){ + try { + detail::with_skip_flags( *this, + get_node_properties().skip_flags | skip_authority_check, [&](){ + transaction_evaluation_state cancel_context(this); + + //Cancel expired limit orders + auto& limit_index = get_index_type().indices().get(); + auto itr = limit_index.begin(); + while( itr != limit_index.end() && itr->offer_expiration_date <= head_block_time() ) + { + const offer_object& offer = *itr; + ++itr; + + finalize_offer_operation finalize; + finalize.fee_paying_account = offer.transfer_agent_id; + finalize.offer_id = offer.id; + finalize.fee = current_fee_schedule().calculate_fee( finalize ); + finalize.result = offer.bidder ? result_type::Expired : result_type::ExpiredNoBid; + + cancel_context.skip_fee_schedule_check = true; + apply_operation(cancel_context, finalize); + } + }); +} FC_CAPTURE_AND_RETHROW()} + } } diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index e697b797..b297f477 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -519,6 +519,7 @@ namespace graphene { namespace chain { void update_betting_markets(fc::time_point_sec current_block_time); bool check_for_blackswan( const asset_object& mia, bool enable_black_swan = true, const asset_bitasset_data_object* bitasset_ptr = nullptr ); + void finalize_expired_offers(); ///Steps performed only at maintenance intervals ///@{ diff --git a/libraries/chain/include/graphene/chain/offer_evaluator.hpp b/libraries/chain/include/graphene/chain/offer_evaluator.hpp new file mode 100644 index 00000000..141b8bfa --- /dev/null +++ b/libraries/chain/include/graphene/chain/offer_evaluator.hpp @@ -0,0 +1,34 @@ +#include +#include +#include + +namespace graphene { namespace chain { + + class offer_evaluator : public evaluator + { + public: + typedef offer_operation operation_type; + + void_result do_evaluate( const offer_operation& o ); + object_id_type do_apply( const offer_operation& o ); + }; + + class bid_evaluator : public evaluator + { + public: + typedef bid_operation operation_type; + + void_result do_evaluate( const bid_operation& o ); + void_result do_apply( const bid_operation& o ); + }; + + class finalize_offer_evaluator : public evaluator + { + public: + typedef finalize_offer_operation operation_type; + + void_result do_evaluate( const finalize_offer_operation& op ); + void_result do_apply( const finalize_offer_operation& op ); + }; + +}} diff --git a/libraries/chain/include/graphene/chain/offer_object.hpp b/libraries/chain/include/graphene/chain/offer_object.hpp new file mode 100644 index 00000000..1c3ee7ba --- /dev/null +++ b/libraries/chain/include/graphene/chain/offer_object.hpp @@ -0,0 +1,117 @@ +#pragma once +#include +#include + +namespace graphene { namespace chain { + class database; + + /** + * @brief This class represents an offer on the object graph + * @ingroup object + * @ingroup protocol + * + */ + struct by_expiration_date{}; + class offer_object : public graphene::db::abstract_object + { + public: + static const uint8_t space_id = protocol_ids; + static const uint8_t type_id = offer_object_type; + + account_id_type issuer; + + account_id_type transfer_agent_id; + uint32_t item_id; + optional bidder; + optional bid_price; + asset minimum_price; + asset maximum_price; + + bool buying_item; + fc::time_point_sec offer_expiration_date; + + offer_id_type get_id()const { return id; } + }; + + class offer_history_object : public graphene::db::abstract_object + { + public: + static const uint8_t space_id = implementation_ids; + static const uint8_t type_id = impl_offer_history_object_type; + + account_id_type issuer; + + account_id_type transfer_agent_id; + uint32_t item_id; + optional bidder; + optional bid_price; + asset minimum_price; + asset maximum_price; + + bool buying_item; + fc::time_point_sec offer_expiration_date; + + offer_history_id_type get_id()const { return id; } + }; + + struct compare_by_expiration_date { + bool operator()( const fc::time_point_sec& o1, const fc::time_point_sec& o2 ) const { + return o1 > o2; + } + }; + + /** + * @ingroup object_index + */ + typedef multi_index_container< + offer_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > >, + ordered_non_unique< + tag, + member< offer_object, fc::time_point_sec, &offer_object::offer_expiration_date >, + compare_by_expiration_date + > + > + > offer_multi_index_type; + + typedef multi_index_container< + offer_history_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > >, + ordered_non_unique< + tag, + member< offer_history_object, fc::time_point_sec, &offer_history_object::offer_expiration_date >, + compare_by_expiration_date + > + > + > offer_history_multi_index_type; + + /** + * @ingroup object_index + */ + typedef generic_index offer_index; + + typedef generic_index offer_history_index; + +}} + +FC_REFLECT_DERIVED( graphene::chain::offer_object, + (graphene::db::object), + (issuer) + (transfer_agent_id)(item_id) + (bidder)(bid_price) + (minimum_price)(maximum_price) + (buying_item) + (offer_expiration_date) + ) + +FC_REFLECT_DERIVED( graphene::chain::offer_history_object, + (graphene::db::object), + (issuer) + (transfer_agent_id)(item_id) + (bidder)(bid_price) + (minimum_price)(maximum_price) + (buying_item) + (offer_expiration_date) + ) diff --git a/libraries/chain/include/graphene/chain/protocol/offer.hpp b/libraries/chain/include/graphene/chain/protocol/offer.hpp new file mode 100644 index 00000000..5d982318 --- /dev/null +++ b/libraries/chain/include/graphene/chain/protocol/offer.hpp @@ -0,0 +1,106 @@ +#pragma once +#include +#include + +namespace graphene { namespace chain { + + /* + * @class offer_operation + * @brief To place an offer to buy or sell an item, a user broadcasts a proposed transaction + * @ingroup operations + * @pre amount.asset_id->issuer == issuer + * @pre issuer != from because this is pointless, use a normal transfer operation + */ + struct offer_operation : public base_operation + { + struct fee_parameters_type { + uint64_t fee = 20 * GRAPHENE_BLOCKCHAIN_PRECISION; + uint32_t price_per_kbyte = 10 * GRAPHENE_BLOCKCHAIN_PRECISION; /// only required for large memos. + }; + + account_id_type transfer_agent_id; + uint32_t item_id; + + // /** + // * minimum_price.asset_id == maximum_price.asset_id. + // * to set fixed price without auction minimum_price == maximum_price + // * If buying_item is true, and minimum_price != maximum_price, the user is proposing a “reverse auction” + // * where bidders can offer to sell the item for progressively lower prices. + // * In this case, minimum_price functions as the sell-it-now price for the reverse auction + // */ + + asset fee; + account_id_type issuer; + + /// minimum_price is minimum bid price. 0 if no minimum_price required + asset minimum_price; + /// buy_it_now price. 0 if no maximum price + asset maximum_price; + /// true means user wants to buy item, false mean user is selling item + bool buying_item; + /// not transaction expiration date + fc::time_point_sec offer_expiration_date; + + /// User provided data encrypted to the memo key of the "to" account + optional memo; + extensions_type extensions; + + account_id_type fee_payer()const { return transfer_agent_id; } + void validate()const; + share_type calculate_fee(const fee_parameters_type& k)const; + }; + + struct bid_operation : public base_operation + { + struct fee_parameters_type { + uint64_t fee = 20 * GRAPHENE_BLOCKCHAIN_PRECISION; + uint32_t price_per_kbyte = 10 * GRAPHENE_BLOCKCHAIN_PRECISION; /// only required for large memos. + }; + + asset fee; + account_id_type bidder; + + asset bid_price; + offer_id_type offer_id; + + extensions_type extensions; + + account_id_type fee_payer()const { return bidder; } + void validate()const; + share_type calculate_fee(const fee_parameters_type& k)const; + }; + + enum class result_type {Expired = 0, ExpiredNoBid = 1}; + + struct finalize_offer_operation : public base_operation + { + struct fee_parameters_type { + uint64_t fee = 20 * GRAPHENE_BLOCKCHAIN_PRECISION; + uint32_t price_per_kbyte = 10 * GRAPHENE_BLOCKCHAIN_PRECISION; /// only required for large memos. + }; + + asset fee; + account_id_type fee_paying_account; + + offer_id_type offer_id; + + fc::enum_type result; + + extensions_type extensions; + + account_id_type fee_payer()const { return fee_paying_account; } + void validate()const; + share_type calculate_fee(const fee_parameters_type& k)const; + }; + +}} // graphene::chain + +FC_REFLECT( graphene::chain::offer_operation::fee_parameters_type, (fee)(price_per_kbyte) ); +FC_REFLECT( graphene::chain::offer_operation, (fee)(issuer)(memo)(minimum_price)(maximum_price)(buying_item)(transfer_agent_id)(item_id)(offer_expiration_date)(extensions) ); + +FC_REFLECT( graphene::chain::bid_operation::fee_parameters_type, (fee)(price_per_kbyte) ); +FC_REFLECT( graphene::chain::bid_operation, (fee)(bidder)(bid_price)(offer_id)(extensions) ); + +FC_REFLECT_ENUM( graphene::chain::result_type, (Expired)(ExpiredNoBid) ); +FC_REFLECT( graphene::chain::finalize_offer_operation::fee_parameters_type, (fee)(price_per_kbyte) ); +FC_REFLECT( graphene::chain::finalize_offer_operation, (fee)(fee_paying_account)(offer_id)(result)(extensions) ); diff --git a/libraries/chain/include/graphene/chain/protocol/operations.hpp b/libraries/chain/include/graphene/chain/protocol/operations.hpp index cb9a83a1..2751bd03 100644 --- a/libraries/chain/include/graphene/chain/protocol/operations.hpp +++ b/libraries/chain/include/graphene/chain/protocol/operations.hpp @@ -45,6 +45,7 @@ #include #include #include +#include namespace graphene { namespace chain { @@ -135,7 +136,10 @@ namespace graphene { namespace chain { ticket_purchase_operation, lottery_reward_operation, lottery_end_operation, - sweeps_vesting_claim_operation + sweeps_vesting_claim_operation, + offer_operation, + bid_operation, + finalize_offer_operation > operation; /// @} // operations group diff --git a/libraries/chain/include/graphene/chain/protocol/types.hpp b/libraries/chain/include/graphene/chain/protocol/types.hpp index 8ea3a8af..2d883bbb 100644 --- a/libraries/chain/include/graphene/chain/protocol/types.hpp +++ b/libraries/chain/include/graphene/chain/protocol/types.hpp @@ -171,6 +171,7 @@ namespace graphene { namespace chain { betting_market_group_object_type, betting_market_object_type, bet_object_type, + offer_object_type, OBJECT_TYPE_COUNT ///< Sentry value which contains the number of different object types }; @@ -199,7 +200,8 @@ namespace graphene { namespace chain { impl_betting_market_position_object_type, impl_global_betting_statistics_object_type, impl_lottery_balance_object_type, - impl_sweeps_vesting_balance_object_type + impl_sweeps_vesting_balance_object_type, + impl_offer_history_object_type }; //typedef fc::unsigned_int object_id_type; @@ -230,6 +232,7 @@ namespace graphene { namespace chain { class betting_market_group_object; class betting_market_object; class bet_object; + class offer_object; typedef object_id< protocol_ids, account_object_type, account_object> account_id_type; typedef object_id< protocol_ids, asset_object_type, asset_object> asset_id_type; @@ -256,6 +259,7 @@ namespace graphene { namespace chain { typedef object_id< protocol_ids, betting_market_group_object_type, betting_market_group_object> betting_market_group_id_type; typedef object_id< protocol_ids, betting_market_object_type, betting_market_object> betting_market_id_type; typedef object_id< protocol_ids, bet_object_type, bet_object> bet_id_type; + typedef object_id< protocol_ids, offer_object_type, offer_object> offer_id_type; // implementation types class global_property_object; @@ -279,6 +283,7 @@ namespace graphene { namespace chain { class global_betting_statistics_object; class lottery_balance_object; class sweeps_vesting_balance_object; + class offer_history_object; typedef object_id< implementation_ids, impl_global_property_object_type, global_property_object> global_property_id_type; typedef object_id< implementation_ids, impl_dynamic_global_property_object_type, dynamic_global_property_object> dynamic_global_property_id_type; @@ -307,6 +312,7 @@ namespace graphene { namespace chain { typedef object_id< implementation_ids, impl_global_betting_statistics_object_type, global_betting_statistics_object > global_betting_statistics_id_type; typedef object_id< implementation_ids, impl_lottery_balance_object_type, lottery_balance_object > lottery_balance_id_type; typedef object_id< implementation_ids, impl_sweeps_vesting_balance_object_type, sweeps_vesting_balance_object> sweeps_vesting_balance_id_type; + typedef object_id< implementation_ids, impl_offer_history_object_type, offer_history_object> offer_history_id_type; typedef fc::array symbol_type; typedef fc::ripemd160 block_id_type; @@ -436,6 +442,7 @@ FC_REFLECT_ENUM( graphene::chain::object_type, (betting_market_group_object_type) (betting_market_object_type) (bet_object_type) + (offer_object_type) (OBJECT_TYPE_COUNT) ) FC_REFLECT_ENUM( graphene::chain::impl_object_type, @@ -463,6 +470,7 @@ FC_REFLECT_ENUM( graphene::chain::impl_object_type, (impl_global_betting_statistics_object_type) (impl_lottery_balance_object_type) (impl_sweeps_vesting_balance_object_type) + (impl_offer_history_object_type) ) FC_REFLECT_TYPENAME( graphene::chain::share_type ) @@ -489,6 +497,7 @@ FC_REFLECT_TYPENAME( graphene::chain::betting_market_group_id_type ) FC_REFLECT_TYPENAME( graphene::chain::betting_market_id_type ) FC_REFLECT_TYPENAME( graphene::chain::bet_id_type ) FC_REFLECT_TYPENAME( graphene::chain::tournament_id_type ) +FC_REFLECT_TYPENAME( graphene::chain::offer_id_type ) FC_REFLECT_TYPENAME( graphene::chain::global_property_id_type ) FC_REFLECT_TYPENAME( graphene::chain::dynamic_global_property_id_type ) FC_REFLECT_TYPENAME( graphene::chain::asset_dynamic_data_id_type ) @@ -505,6 +514,7 @@ FC_REFLECT_TYPENAME( graphene::chain::fba_accumulator_id_type ) FC_REFLECT_TYPENAME( graphene::chain::betting_market_position_id_type ) FC_REFLECT_TYPENAME( graphene::chain::global_betting_statistics_id_type ) FC_REFLECT_TYPENAME( graphene::chain::tournament_details_id_type ) +FC_REFLECT_TYPENAME( graphene::chain::offer_history_id_type ) FC_REFLECT( graphene::chain::void_t, ) diff --git a/libraries/chain/offer_evaluator.cpp b/libraries/chain/offer_evaluator.cpp new file mode 100644 index 00000000..192c60e7 --- /dev/null +++ b/libraries/chain/offer_evaluator.cpp @@ -0,0 +1,173 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace graphene { namespace chain { + +void_result offer_evaluator::do_evaluate( const offer_operation& op ) +{ try { + database& d = db(); + + FC_ASSERT( op.offer_expiration_date > d.head_block_time() ); + FC_ASSERT( op.item_id > 0 ); + FC_ASSERT( op.fee.amount >= 0 ); + FC_ASSERT( op.minimum_price.amount >= 0 && op.maximum_price.amount > 0); + FC_ASSERT( op.minimum_price.asset_id == op.maximum_price.asset_id ); + FC_ASSERT( op.maximum_price >= op.minimum_price ); + + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +object_id_type offer_evaluator::do_apply( const offer_operation& o ) +{ try { + database& d = db(); + + if (o.buying_item) + { + d.adjust_balance( o.issuer, -o.maximum_price); + d.adjust_balance( o.transfer_agent_id, o.maximum_price ); + } + + const auto& offer_obj = db().create( [&]( offer_object& obj ){ + obj.issuer = o.issuer; + + obj.transfer_agent_id = o.transfer_agent_id; + obj.item_id = o.item_id; + + obj.minimum_price = o.minimum_price; + obj.maximum_price = o.maximum_price; + + obj.buying_item = o.buying_item; + obj.offer_expiration_date = o.offer_expiration_date; + auto& idx = d.get_index_type().indices().get(); + auto acc = idx.find(o.issuer); + FC_ASSERT(acc != idx.end()); + + }); + + return offer_obj.id; +} FC_CAPTURE_AND_RETHROW((o)) } + +void_result bid_evaluator::do_evaluate( const bid_operation& op ) +{ try { + database& d = db(); + offer_object offer = op.offer_id(d); + + FC_ASSERT( op.bid_price.asset_id == offer.minimum_price.asset_id); + FC_ASSERT( offer.transfer_agent_id(d).whitelisted_accounts.find(op.bidder) != offer.transfer_agent_id(d).whitelisted_accounts.end() ); + FC_ASSERT( offer.minimum_price.amount == 0 || op.bid_price >= offer.minimum_price); + FC_ASSERT( offer.maximum_price.amount == 0 || op.bid_price <= offer.maximum_price); + if (offer.bidder) { + FC_ASSERT((offer.buying_item && op.bid_price < *offer.bid_price) || (!offer.buying_item && op.bid_price > *offer.bid_price)); + } + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result bid_evaluator::do_apply( const bid_operation& op ) +{ try { + database& d = db(); + + offer_object offer = op.offer_id(d); + + if (!offer.buying_item) + { + if(offer.bidder) + { + d.adjust_balance( *offer.bidder, *offer.bid_price ); + d.adjust_balance( offer.transfer_agent_id, -(*offer.bid_price )); + } + d.adjust_balance( op.bidder, -op.bid_price); + d.adjust_balance( offer.transfer_agent_id, op.bid_price ); + } + d.modify( op.offer_id(d), [&]( offer_object& o ) + { + if (op.bid_price == ( offer.buying_item ? offer.minimum_price : offer.maximum_price ) ) { + o.offer_expiration_date = d.head_block_time(); + } + o.bidder = op.bidder; + o.bid_price = op.bid_price; + }); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result finalize_offer_evaluator::do_evaluate(const finalize_offer_operation& op) +{ try { + database& d = db(); + offer_object offer = op.offer_id(d); + + // FC_ASSERT(*offer.bid_price == (offer.buying_item ? offer.minimum_price : offer.maximum_price)); + + if (op.result != result_type::ExpiredNoBid) + { + FC_ASSERT(offer.bidder); + FC_ASSERT((*offer.bid_price).amount >= 0); + } else + { + FC_ASSERT(!offer.bidder); + } + + switch (op.result) { + case result_type::Expired: + case result_type::ExpiredNoBid: + FC_ASSERT(offer.offer_expiration_date <= d.head_block_time()); + break; + default: + FC_THROW_EXCEPTION( fc::assert_exception, "finalize_offer_operation: unknown result type." ); + break; + } + + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + +void_result finalize_offer_evaluator::do_apply(const finalize_offer_operation& op) { + database& d = db(); + + offer_object offer = op.offer_id(d); + + if (op.result != result_type::ExpiredNoBid) + { + if (offer.buying_item) + { + d.adjust_balance(*offer.bidder, *offer.bid_price); + d.adjust_balance(offer.transfer_agent_id, -offer.maximum_price); + if (offer.bid_price < offer.maximum_price) + { + d.adjust_balance(offer.issuer, offer.maximum_price - *offer.bid_price); + } + } else + { + d.adjust_balance(offer.transfer_agent_id, -(*offer.bid_price)); + d.adjust_balance(offer.issuer, *offer.bid_price); + } + } else + { + if (offer.buying_item) + { + d.adjust_balance(offer.issuer, offer.maximum_price); + d.adjust_balance(offer.transfer_agent_id, -offer.maximum_price); + } + } + d.create( [&]( offer_history_object& obj ) { + obj.issuer = offer.issuer; + + obj.transfer_agent_id = offer.transfer_agent_id; + obj.item_id = offer.item_id; + obj.bidder = offer.bidder; + obj.bid_price = offer.bid_price; + obj.minimum_price = offer.minimum_price; + obj.maximum_price = offer.maximum_price; + + obj.buying_item = offer.buying_item; + obj.offer_expiration_date = offer.offer_expiration_date; + std::cout << "get id" << uint64_t(obj.get_id()) << std::endl; + }); + d.remove(op.offer_id(d)); + return void_result(); +} + +} +} \ No newline at end of file diff --git a/libraries/chain/protocol/offer.cpp b/libraries/chain/protocol/offer.cpp new file mode 100644 index 00000000..0f8afa18 --- /dev/null +++ b/libraries/chain/protocol/offer.cpp @@ -0,0 +1,46 @@ +#include + +namespace graphene { namespace chain { +share_type offer_operation::calculate_fee( const fee_parameters_type& schedule )const +{ + share_type core_fee_required = schedule.fee; + return core_fee_required; +} + + +void offer_operation::validate()const +{ + FC_ASSERT( item_id > 0 ); + FC_ASSERT( fee.amount >= 0 ); + FC_ASSERT( minimum_price.asset_id == maximum_price.asset_id ); + FC_ASSERT( minimum_price.amount >= 0 && maximum_price.amount > 0); + FC_ASSERT( maximum_price >= minimum_price ); + +// FC_ASSERT( offer_expiration_date >= fc::time_point::now() ); +} + +share_type bid_operation::calculate_fee( const fee_parameters_type& schedule )const +{ + share_type core_fee_required = schedule.fee; + return core_fee_required; +} + + +void bid_operation::validate()const +{ + FC_ASSERT( fee.amount.value >= 0 ); + FC_ASSERT( bid_price.amount.value >= 0 ); +} + +void finalize_offer_operation::validate() const { + FC_ASSERT ( offer_id != offer_id_type() ); + FC_ASSERT( fee.amount.value >= 0 ); +} + +share_type finalize_offer_operation::calculate_fee( const fee_parameters_type& k) const { + share_type core_fee_required = k.fee; + return core_fee_required; +} + +} +} \ No newline at end of file diff --git a/programs/js_operation_serializer/main.cpp b/programs/js_operation_serializer/main.cpp index 8994b36b..e469c885 100644 --- a/programs/js_operation_serializer/main.cpp +++ b/programs/js_operation_serializer/main.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include diff --git a/tests/tests/operation_tests.cpp b/tests/tests/operation_tests.cpp index 3093f0b0..ed656a8b 100644 --- a/tests/tests/operation_tests.cpp +++ b/tests/tests/operation_tests.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include @@ -1821,6 +1822,513 @@ BOOST_AUTO_TEST_CASE( vesting_balance_withdraw_test ) // TODO: Test with non-core asset and Bob account } FC_LOG_AND_RETHROW() } +offer_id_type buy_offer; +offer_id_type sell_offer; + +BOOST_AUTO_TEST_CASE(create_offer_test) +{ + try + { + + fc::ecc::private_key agent_pk = generate_private_key("agent"); + fc::ecc::private_key issuer_pk = generate_private_key("issuer"); + + const asset_object &bitusd = create_bitasset("STUB"); + const account_object &agent = create_account("agent", agent_pk.get_public_key()); + const account_object &issuer = create_account("issuer", issuer_pk.get_public_key()); + + transfer(committee_account(db), issuer, asset(10000)); + transfer(committee_account(db), agent, asset(10000)); + upgrade_to_lifetime_member(agent); + + bool params[] = {true, false}; + std::for_each(params, params + 1, [&](bool is_buying_offer) { + offer_operation offer_op; + + offer_op.item_id = 13; + offer_op.issuer = issuer.id; + offer_op.buying_item = is_buying_offer; + offer_op.maximum_price = asset(100); + offer_op.minimum_price = asset(10); + offer_op.transfer_agent_id = agent.id; + // +1 second + offer_op.offer_expiration_date = db.head_block_time() + fc::microseconds(3000000); + + trx.operations.push_back(offer_op); + offer_operation &op = trx.operations.back().get(); + + //REQUIRE_THROW_WITH_VALUE(op, offer_expiration_date, db.head_block_time()); + + // //positive prices + REQUIRE_OP_VALIDATION_FAILURE(op, minimum_price, asset(-1)); + REQUIRE_OP_VALIDATION_FAILURE(op, maximum_price, asset(-1)); + + REQUIRE_OP_VALIDATION_FAILURE(op, fee, asset(-1)); + + // // min price > max price check + REQUIRE_OP_VALIDATION_FAILURE(op, maximum_price, asset(1)); + + // different asset for min/max + REQUIRE_OP_VALIDATION_FAILURE(op, minimum_price, asset(1, bitusd.id)); + + offer_id_type offer_id = db.get_index_type>>().get_next_id(); + + PUSH_TX(db, trx, ~0); + + const offer_object &d = offer_id(db); + + BOOST_CHECK(d.space_id == protocol_ids); + BOOST_CHECK(d.type_id == offer_object_type); + + //empty bid + BOOST_CHECK(!d.bid_price); + BOOST_CHECK(!d.bidder); + + // data integrity + BOOST_CHECK(d.issuer == issuer.id); + BOOST_CHECK(d.maximum_price == asset(100)); + BOOST_CHECK(d.minimum_price == asset(10)); + BOOST_CHECK(d.transfer_agent_id == agent.id); + BOOST_CHECK(d.buying_item == is_buying_offer); + + if (is_buying_offer) + { + buy_offer = offer_id; + } + else + { + sell_offer = offer_id; + } + }); + } + catch (fc::exception &e) + { + edump((e.to_detail_string())); + throw; + } +} + +BOOST_AUTO_TEST_CASE(bid_test) +{ + try + { + INVOKE(create_offer_test); + const account_object &agent = get_account("agent"); + const account_object &issuer = get_account("issuer"); + const account_object &bidder = create_account("bidder", generate_private_key("bidder").get_public_key()); + transfer(committee_account(db), bidder, asset(10000)); + + account_id_type bidder_id = bidder.id; + account_id_type agent_id = agent.id; + account_id_type issuer_id = issuer.id; + + const offer_object &offer_obj = buy_offer(db); + + bid_operation bid_op; + bid_op.offer_id = offer_obj.id; + bid_op.bid_price = asset(offer_obj.minimum_price.amount + 1, offer_obj.minimum_price.asset_id); + bid_op.bidder = bidder.id; + trx.operations.push_back(bid_op); + + // exception due to empty whitelist + GRAPHENE_REQUIRE_THROW(PUSH_TX(db, trx, ~0), fc::exception); + + trx.operations.clear(); + + //adding whitelist + account_whitelist_operation wop; + wop.authorizing_account = agent.id; + wop.account_to_list = bidder.id; + wop.new_listing = account_whitelist_operation::white_listed; + trx.operations.clear(); + trx.operations.push_back(wop); + PUSH_TX(db, trx, ~0); + + trx.operations.clear(); + + bool params[] = {true, false}; + std::for_each(params, params + 1, [&](bool is_buying_offer) { + asset bid; + asset exp_delta_agent; + asset exp_delta_bidder; + + int64_t agent_balance = get_balance(agent_id(db), asset_id_type()(db)); + int64_t issuer_balance = get_balance(issuer_id(db), asset_id_type()(db)); + int64_t bidder_balance = get_balance(bidder_id(db), asset_id_type()(db)); + + const offer_object &offer_obj = is_buying_offer ? buy_offer(db) : sell_offer(db); + + if (is_buying_offer) + { + bid = asset(offer_obj.maximum_price.amount - 1, asset_id_type()); + exp_delta_agent = asset(0); + exp_delta_bidder = asset(0); + } + else + { + bid = asset(offer_obj.minimum_price.amount + 1, asset_id_type()); + exp_delta_agent = bid; + exp_delta_bidder = -bid; + } + + bid_op.offer_id = offer_obj.id; + + bid_op.bid_price = bid; + bid_op.bidder = bidder.id; + + trx.operations.push_back(bid_op); + bid_operation &op = trx.operations.back().get(); + + //REQUIRE_THROW_WITH_VALUE(op, bid_price, asset(-1, asset_id_type())); + //REQUIRE_THROW_WITH_VALUE(op, bid_price, asset(offer_obj.maximum_price.amount + 1, offer_obj.minimum_price.asset_id)); + // REQUIRE_THROW_WITH_VALUE(op, offer_id, offer_id_type()); + + PUSH_TX(db, trx, ~0); + + BOOST_CHECK_EQUAL(get_balance(agent_id(db), asset_id_type()(db)), + (agent_balance + exp_delta_agent.amount).value); + BOOST_CHECK_EQUAL(get_balance(bidder_id(db), asset_id_type()(db)), + (bidder_balance + exp_delta_bidder.amount).value); + + //not empty bid + BOOST_CHECK(offer_obj.bid_price); + BOOST_CHECK(offer_obj.bidder); + + // data integrity + BOOST_CHECK(offer_obj.bidder == bidder.id); + BOOST_CHECK(offer_obj.issuer == issuer.id); + BOOST_CHECK(offer_obj.transfer_agent_id == agent.id); + BOOST_CHECK(offer_obj.maximum_price == asset(100)); + BOOST_CHECK(offer_obj.minimum_price == asset(10)); + BOOST_CHECK(offer_obj.bid_price == bid); + }); + } + catch (fc::exception &e) + { + edump((e.to_detail_string())); + throw; + } +} + +BOOST_AUTO_TEST_CASE(second_bid_test) +{ + try + { + INVOKE(bid_test); + const account_object &agent = get_account("agent"); + const account_object &bidder = get_account("bidder"); + const account_object &bidder2 = create_account("bidder2", generate_private_key("bidder2").get_public_key()); + + //adding whitelist + account_whitelist_operation wop; + wop.authorizing_account = agent.id; + wop.account_to_list = bidder2.id; + wop.new_listing = account_whitelist_operation::white_listed; + trx.operations.push_back(wop); + PUSH_TX(db, trx, ~0); + + account_id_type agent_id = agent.id; + account_id_type bidder_id = bidder.id; + account_id_type bidder2_id = bidder2.id; + + transfer(committee_account(db), bidder2, asset(10000)); + + bool params[] = {true, false}; + std::for_each(params, params + 1, [&](bool is_buying_offer) { + asset bid; + asset exp_delta_agent; + asset exp_delta_bidder; + asset exp_delta_bidder2; + + int64_t agent_balance = get_balance(agent_id(db), asset_id_type()(db)); + int64_t bidder_balance = get_balance(bidder_id(db), asset_id_type()(db)); + int64_t bidder2_balance = get_balance(bidder2_id(db), asset_id_type()(db)); + + const offer_object &offer_obj = is_buying_offer ? buy_offer(db) : sell_offer(db); + + if (is_buying_offer) + { + bid = asset((*offer_obj.bid_price).amount - 1, asset_id_type()); + exp_delta_agent = asset(0); + exp_delta_bidder = asset(0); + exp_delta_bidder2 = asset(0); + } + else + { + bid = asset((*offer_obj.bid_price).amount + 1, asset_id_type()); + exp_delta_agent = bid - (*offer_obj.bid_price); + exp_delta_bidder = *offer_obj.bid_price; + exp_delta_bidder2 = -bid; + } + + bid_operation bid_op; + trx.operations.push_back(bid_op); + bid_op.offer_id = offer_obj.id; + bid_op.bid_price = bid; + bid_op.bidder = bidder2.id; + + trx.operations.clear(); + trx.operations.push_back(bid_op); + + PUSH_TX(db, trx, ~0); + + BOOST_CHECK_EQUAL(get_balance(agent_id(db), asset_id_type()(db)), + (agent_balance + exp_delta_agent.amount).value); + BOOST_CHECK_EQUAL(get_balance(bidder_id(db), asset_id_type()(db)), + (bidder_balance + exp_delta_bidder.amount).value); + BOOST_CHECK_EQUAL(get_balance(bidder2_id(db), asset_id_type()(db)), + (bidder2_balance + exp_delta_bidder2.amount).value); + + //not empty bid + BOOST_CHECK(offer_obj.bid_price); + BOOST_CHECK(offer_obj.bidder); + + // data integrity + BOOST_CHECK(offer_obj.bidder == bidder2.id); + BOOST_CHECK(offer_obj.transfer_agent_id == agent.id); + BOOST_CHECK(offer_obj.maximum_price == asset(100)); + BOOST_CHECK(offer_obj.minimum_price == asset(10)); + BOOST_CHECK(offer_obj.bid_price == bid); + }); + + const offer_object &offer_obj = *db.get_index_type().indices().get().begin(); + } + catch (fc::exception &e) + { + edump((e.to_detail_string())); + throw; + } +} + +BOOST_AUTO_TEST_CASE(buyout_offer_test) +{ + try + { + INVOKE(create_offer_test); + const account_object &agent = get_account("agent"); + const account_object &issuer = get_account("issuer"); + const account_object &bidder = create_account("bidder", generate_private_key("bidder").get_public_key()); + + account_id_type agent_id = agent.id; + account_id_type issuer_id = issuer.id; + account_id_type bidder_id = bidder.id; + + //adding whitelist + account_whitelist_operation wop; + wop.authorizing_account = agent.id; + wop.account_to_list = bidder.id; + wop.new_listing = account_whitelist_operation::white_listed; + trx.operations.push_back(wop); + PUSH_TX(db, trx, ~0); + + transfer(committee_account(db), bidder, asset(10000)); + + bool params[] = {true, false}; + std::for_each(params, params + 1, [&](bool is_buying_offer) { + const offer_object &offer_obj = is_buying_offer ? buy_offer(db) : sell_offer(db); + + asset bid; + asset exp_delta_agent; + asset exp_delta_bidder; + asset exp_delta_issuer; + + int64_t agent_balance = get_balance(agent_id(db), asset_id_type()(db)); + int64_t bidder_balance = get_balance(bidder_id(db), asset_id_type()(db)); + int64_t issuer_balance = get_balance(issuer_id(db), asset_id_type()(db)); + + if (is_buying_offer) + { + bid = asset(offer_obj.minimum_price.amount, asset_id_type()); + exp_delta_agent = -offer_obj.maximum_price; + exp_delta_bidder = offer_obj.minimum_price; + exp_delta_issuer = offer_obj.maximum_price - offer_obj.minimum_price; + } + else + { + bid = asset(offer_obj.maximum_price.amount, asset_id_type()); + exp_delta_agent = asset(0); + exp_delta_bidder = -offer_obj.maximum_price.amount; + exp_delta_issuer = (offer_obj.maximum_price).amount; + } + + bid_operation bid_op; + trx.operations.push_back(bid_op); + bid_op.offer_id = offer_obj.id; + bid_op.bid_price = bid; + bid_op.bidder = bidder.id; + + trx.operations.clear(); + trx.operations.push_back(bid_op); + + PUSH_TX(db, trx, ~0); + + offer_object cached_offer_obj = offer_obj; + + offer_history_id_type history_id = db.get_index().get_next_id(); + + generate_block(); + + BOOST_CHECK_EQUAL(get_balance(agent_id(db), asset_id_type()(db)), + (agent_balance + exp_delta_agent.amount).value); + BOOST_CHECK_EQUAL(get_balance(bidder_id(db), asset_id_type()(db)), + (bidder_balance + exp_delta_bidder.amount).value); + BOOST_CHECK_EQUAL(get_balance(issuer_id(db), asset_id_type()(db)), + (issuer_balance + exp_delta_issuer.amount).value); + + BOOST_CHECK(!db.find(buy_offer)); + BOOST_CHECK(!db.find(sell_offer)); + + BOOST_CHECK(db.find(history_id)); + + offer_history_object history_obj = history_id(db); + + BOOST_CHECK(cached_offer_obj.bid_price == history_obj.bid_price); + BOOST_CHECK(cached_offer_obj.bidder == history_obj.bidder); + BOOST_CHECK(cached_offer_obj.buying_item == history_obj.buying_item); + BOOST_CHECK(cached_offer_obj.issuer == history_obj.issuer); + BOOST_CHECK(cached_offer_obj.maximum_price == history_obj.maximum_price); + BOOST_CHECK(cached_offer_obj.minimum_price == history_obj.minimum_price); + BOOST_CHECK(cached_offer_obj.offer_expiration_date == history_obj.offer_expiration_date); + BOOST_CHECK(cached_offer_obj.transfer_agent_id == history_obj.transfer_agent_id); + BOOST_CHECK(cached_offer_obj.item_id == history_obj.item_id); + }); + } + catch (fc::exception &e) + { + edump((e.to_detail_string())); + throw; + } +} + +BOOST_AUTO_TEST_CASE(expire_with_bid_offer_test) +{ + + bool params[] = {true, false}; + + std::for_each(params, params + 1, [&](bool is_buying_offer) { + INVOKE(bid_test); + const offer_object &offer_obj = is_buying_offer ? buy_offer(db) : sell_offer(db); + const account_object &agent = get_account("agent"); + const account_object &issuer = get_account("issuer"); + const account_object &bidder = get_account("bidder"); + + account_id_type agent_id = agent.id; + account_id_type issuer_id = issuer.id; + account_id_type bidder_id = bidder.id; + + int64_t agent_balance = get_balance(agent_id(db), asset_id_type()(db)); + int64_t issuer_balance = get_balance(issuer_id(db), asset_id_type()(db)); + int64_t bidder_balance = get_balance(bidder_id(db), asset_id_type()(db)); + + asset exp_delta_agent; + asset exp_delta_issuer; + asset exp_delta_bidder; + + if (is_buying_offer) + { + exp_delta_agent = -offer_obj.maximum_price; + exp_delta_issuer = offer_obj.maximum_price - *offer_obj.bid_price; + exp_delta_bidder = *offer_obj.bid_price; + } + else + { + exp_delta_agent = -(*offer_obj.bid_price); + exp_delta_issuer = *offer_obj.bid_price; + exp_delta_bidder = asset(0); + } + + offer_object cached_offer_obj = offer_obj; + + offer_history_id_type history_id = db.get_index().get_next_id(); + + generate_blocks(5); + + BOOST_CHECK_EQUAL(get_balance(agent_id(db), asset_id_type()(db)), + (agent_balance + exp_delta_agent.amount).value); + BOOST_CHECK_EQUAL(get_balance(issuer_id(db), asset_id_type()(db)), + (issuer_balance + exp_delta_issuer.amount).value); + BOOST_CHECK_EQUAL(get_balance(bidder_id(db), asset_id_type()(db)), + (bidder_balance + exp_delta_bidder.amount).value); + + BOOST_CHECK(!db.find(buy_offer)); + BOOST_CHECK(!db.find(sell_offer)); + + BOOST_CHECK(db.find(history_id)); + + offer_history_object history_obj = history_id(db); + + BOOST_CHECK(cached_offer_obj.bid_price == history_obj.bid_price); + BOOST_CHECK(cached_offer_obj.bidder == history_obj.bidder); + BOOST_CHECK(cached_offer_obj.buying_item == history_obj.buying_item); + BOOST_CHECK(cached_offer_obj.issuer == history_obj.issuer); + BOOST_CHECK(cached_offer_obj.maximum_price == history_obj.maximum_price); + BOOST_CHECK(cached_offer_obj.minimum_price == history_obj.minimum_price); + BOOST_CHECK(cached_offer_obj.offer_expiration_date == history_obj.offer_expiration_date); + BOOST_CHECK(cached_offer_obj.transfer_agent_id == history_obj.transfer_agent_id); + BOOST_CHECK(cached_offer_obj.item_id == history_obj.item_id); + }); +} + +BOOST_AUTO_TEST_CASE(expire_no_bid_offer_test) +{ + + bool params[] = {true, false}; + + std::for_each(params, params + 1, [&](bool is_buying_offer) { + INVOKE(create_offer_test); + const offer_object &offer_obj = is_buying_offer ? buy_offer(db) : sell_offer(db); + const account_object &agent = get_account("agent"); + const account_object &issuer = get_account("issuer"); + + account_id_type agent_id = agent.id; + account_id_type issuer_id = issuer.id; + + int64_t agent_balance = get_balance(agent_id(db), asset_id_type()(db)); + int64_t issuer_balance = get_balance(issuer_id(db), asset_id_type()(db)); + + asset exp_delta_agent; + asset exp_delta_issuer; + + if (is_buying_offer) + { + exp_delta_agent = -offer_obj.maximum_price; + exp_delta_issuer = offer_obj.maximum_price; + } + else + { + exp_delta_agent = asset(0); + exp_delta_issuer = asset(0); + } + + offer_object cached_offer_obj = offer_obj; + + offer_history_id_type history_id = db.get_index().get_next_id(); + + generate_blocks(5); + + BOOST_CHECK_EQUAL(get_balance(agent_id(db), asset_id_type()(db)), + (agent_balance + exp_delta_agent.amount).value); + BOOST_CHECK_EQUAL(get_balance(issuer_id(db), asset_id_type()(db)), + (issuer_balance + exp_delta_issuer.amount).value); + + BOOST_CHECK(!db.find(buy_offer)); + BOOST_CHECK(!db.find(sell_offer)); + + BOOST_CHECK(db.find(history_id)); + + offer_history_object history_obj = history_id(db); + + BOOST_CHECK(cached_offer_obj.bid_price == history_obj.bid_price); + BOOST_CHECK(cached_offer_obj.bidder == history_obj.bidder); + BOOST_CHECK(cached_offer_obj.buying_item == history_obj.buying_item); + BOOST_CHECK(cached_offer_obj.issuer == history_obj.issuer); + BOOST_CHECK(cached_offer_obj.maximum_price == history_obj.maximum_price); + BOOST_CHECK(cached_offer_obj.minimum_price == history_obj.minimum_price); + BOOST_CHECK(cached_offer_obj.offer_expiration_date == history_obj.offer_expiration_date); + BOOST_CHECK(cached_offer_obj.transfer_agent_id == history_obj.transfer_agent_id); + BOOST_CHECK(cached_offer_obj.item_id == history_obj.item_id); + }); +} // TODO: Write linear VBO tests BOOST_AUTO_TEST_SUITE_END()