From c0da3a2e847b72d0cf4f345cb35f7b63a0cceff3 Mon Sep 17 00:00:00 2001 From: sierra19XX <15652887+sierra19XX@users.noreply.github.com> Date: Tue, 7 Jul 2020 18:40:58 +0000 Subject: [PATCH] ppy marketplace 5 - Add revenue split --- .../include/graphene/chain/nft_object.hpp | 6 ++- .../graphene/chain/protocol/nft_ops.hpp | 8 ++- libraries/chain/nft_evaluator.cpp | 17 ++++++- libraries/chain/offer_evaluator.cpp | 51 +++++++++++++++---- .../wallet/include/graphene/wallet/wallet.hpp | 8 +++ libraries/wallet/wallet.cpp | 26 ++++++++++ tests/tests/marketplace_tests.cpp | 30 +++++++++-- 7 files changed, 129 insertions(+), 17 deletions(-) diff --git a/libraries/chain/include/graphene/chain/nft_object.hpp b/libraries/chain/include/graphene/chain/nft_object.hpp index 34d31bf4..4300baa9 100644 --- a/libraries/chain/include/graphene/chain/nft_object.hpp +++ b/libraries/chain/include/graphene/chain/nft_object.hpp @@ -16,6 +16,8 @@ namespace graphene { namespace chain { std::string name; std::string symbol; std::string base_uri; + optional revenue_partner; + optional revenue_split; }; class nft_object : public abstract_object @@ -87,7 +89,9 @@ FC_REFLECT_DERIVED( graphene::chain::nft_metadata_object, (graphene::db::object) (owner) (name) (symbol) - (base_uri) ) + (base_uri) + (revenue_partner) + (revenue_split) ) FC_REFLECT_DERIVED( graphene::chain::nft_object, (graphene::db::object), (nft_metadata_id) diff --git a/libraries/chain/include/graphene/chain/protocol/nft_ops.hpp b/libraries/chain/include/graphene/chain/protocol/nft_ops.hpp index 678d2bf3..f1893c29 100644 --- a/libraries/chain/include/graphene/chain/protocol/nft_ops.hpp +++ b/libraries/chain/include/graphene/chain/protocol/nft_ops.hpp @@ -13,6 +13,8 @@ namespace graphene { namespace chain { std::string name; std::string symbol; std::string base_uri; + optional revenue_partner; + optional revenue_split; account_id_type fee_payer()const { return owner; } }; @@ -27,6 +29,8 @@ namespace graphene { namespace chain { optional name; optional symbol; optional base_uri; + optional revenue_partner; + optional revenue_split; account_id_type fee_payer()const { return owner; } }; @@ -97,8 +101,8 @@ 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) ) -FC_REFLECT( graphene::chain::nft_metadata_update_operation, (fee) (owner) (nft_metadata_id) (name) (symbol) (base_uri) ) +FC_REFLECT( graphene::chain::nft_metadata_create_operation, (fee) (owner) (name) (symbol) (base_uri) (revenue_partner) (revenue_split) ) +FC_REFLECT( graphene::chain::nft_metadata_update_operation, (fee) (owner) (nft_metadata_id) (name) (symbol) (base_uri) (revenue_partner) (revenue_split) ) FC_REFLECT( graphene::chain::nft_mint_operation, (fee) (nft_metadata_id) (owner) (approved) (approved_operators) (token_uri) ) FC_REFLECT( graphene::chain::nft_safe_transfer_from_operation, (fee) (operator_) (from) (to) (token_id) (data) ) FC_REFLECT( graphene::chain::nft_approve_operation, (fee) (operator_) (approved) (token_id) ) diff --git a/libraries/chain/nft_evaluator.cpp b/libraries/chain/nft_evaluator.cpp index 26bb9b18..d0bdf000 100644 --- a/libraries/chain/nft_evaluator.cpp +++ b/libraries/chain/nft_evaluator.cpp @@ -9,6 +9,11 @@ void_result nft_metadata_create_evaluator::do_evaluate( const nft_metadata_creat FC_ASSERT( idx_nft_md_by_name.find(op.name) == idx_nft_md_by_name.end(), "NFT name already in use" ); const auto& idx_nft_md_by_symbol = db().get_index_type().indices().get(); FC_ASSERT( idx_nft_md_by_symbol.find(op.symbol) == idx_nft_md_by_symbol.end(), "NFT symbol already in use" ); + FC_ASSERT( (op.revenue_partner && op.revenue_split) || (!op.revenue_partner && !op.revenue_split), "NFT revenue partner info invalid" ); + if(op.revenue_partner) { + (*op.revenue_partner)(db()); + FC_ASSERT( *op.revenue_split >= 0.0 && *op.revenue_split <= 1.0, "Revenue split percent invalid" ); + } return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } @@ -19,6 +24,8 @@ object_id_type nft_metadata_create_evaluator::do_apply( const nft_metadata_creat obj.name = op.name; obj.symbol = op.symbol; obj.base_uri = op.base_uri; + obj.revenue_partner = op.revenue_partner; + obj.revenue_split = op.revenue_split; }); return new_nft_metadata_object.id; } FC_CAPTURE_AND_RETHROW( (op) ) } @@ -30,7 +37,11 @@ void_result nft_metadata_update_evaluator::do_evaluate( const nft_metadata_updat auto itr_nft_md = idx_nft_md.find(op.nft_metadata_id); FC_ASSERT( itr_nft_md != idx_nft_md.end(), "NFT metadata not found" ); FC_ASSERT( itr_nft_md->owner == op.owner, "Only owner can modify NFT metadata" ); - + FC_ASSERT( (op.revenue_partner && op.revenue_split) || (!op.revenue_partner && !op.revenue_split), "NFT revenue partner info invalid" ); + if(op.revenue_partner) { + (*op.revenue_partner)(db()); + FC_ASSERT( *op.revenue_split >= 0.0 && *op.revenue_split <= 1.0, "Revenue split percent invalid" ); + } return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } @@ -43,6 +54,10 @@ void_result nft_metadata_update_evaluator::do_apply( const nft_metadata_update_o obj.symbol = *op.symbol; if( op.base_uri.valid() ) obj.base_uri = *op.base_uri; + if( op.revenue_partner.valid() ) + obj.revenue_partner = op.revenue_partner; + if( op.revenue_split.valid() ) + obj.revenue_split = op.revenue_split; }); return void_result(); } FC_CAPTURE_AND_RETHROW( (op) ) } diff --git a/libraries/chain/offer_evaluator.cpp b/libraries/chain/offer_evaluator.cpp index b43d353a..f6fd3138 100644 --- a/libraries/chain/offer_evaluator.cpp +++ b/libraries/chain/offer_evaluator.cpp @@ -11,7 +11,6 @@ namespace graphene { namespace chain { - void_result offer_evaluator::do_evaluate(const offer_operation &op) { try @@ -33,7 +32,7 @@ namespace graphene } else { - FC_ASSERT(is_owner || is_approved || is_approved_operator, "Issuer has no authority to sell the item"); + FC_ASSERT(is_owner, "Issuer has no authority to sell the item"); } } FC_ASSERT(op.offer_expiration_date > d.head_block_time(), "Expiration should be in future"); @@ -87,7 +86,7 @@ namespace graphene bool is_approved_operator = (std::find(nft_obj.approved_operators.begin(), nft_obj.approved_operators.end(), op.bidder) != nft_obj.approved_operators.end()); if (offer.buying_item) { - FC_ASSERT(is_owner || is_approved || is_approved_operator, "Bidder has no authority to sell the item"); + FC_ASSERT(is_owner, "Bidder has no authority to sell the item"); } else { @@ -165,7 +164,6 @@ namespace graphene FC_THROW_EXCEPTION(fc::assert_exception, "finalize_offer_operation: unknown result type."); break; } - return void_result(); } FC_CAPTURE_AND_RETHROW((op)) @@ -176,25 +174,61 @@ namespace graphene try { database &d = db(); - offer_object offer = op.offer_id(d); vector xfer_ops; + // Calculate the fees for all revenue partners of the items + auto calc_fee = [&](int64_t &tot_fees) { + map fee_map; + for (const auto &item : offer.item_ids) + { + const auto &nft_obj = item(d); + const auto &nft_meta_obj = nft_obj.nft_metadata_id(d); + if (nft_meta_obj.revenue_partner && *nft_meta_obj.revenue_split > 0.0) + { + const auto &rev_partner = *nft_meta_obj.revenue_partner; + const auto &rev_split = *nft_meta_obj.revenue_split; + int64_t item_fee = static_cast((rev_split * (*offer.bid_price).amount.value) / offer.item_ids.size()); + const auto& fee_asset = asset(item_fee, (*offer.bid_price).asset_id); + auto ret_val = fee_map.insert({rev_partner, fee_asset}); + if ( ret_val.second == false ) { + fee_map[rev_partner] += fee_asset; + } + tot_fees += item_fee; + } + } + return fee_map; + }; if (op.result != result_type::ExpiredNoBid) { + int64_t tot_fees = 0; + auto &&fee_map = calc_fee(tot_fees); + // Transfer all the fee + for (const auto &fee_itr : fee_map) + { + auto &acc = fee_itr.first; + auto &acc_fee = fee_itr.second; + d.adjust_balance(acc, acc_fee); + } + // Calculate the remaining seller amount after the fee is deducted + auto &&seller_amount = *offer.bid_price - asset(tot_fees, (*offer.bid_price).asset_id); + // Buy Offer if (offer.buying_item) { - d.adjust_balance(*offer.bidder, *offer.bid_price); + // Send the seller his amount + d.adjust_balance(*offer.bidder, seller_amount); if (offer.bid_price < offer.maximum_price) { + // Send the buyer the delta d.adjust_balance(offer.issuer, offer.maximum_price - *offer.bid_price); } } else { - d.adjust_balance(offer.issuer, *offer.bid_price); + // Sell Offer, send the seller his amount + d.adjust_balance(offer.issuer, seller_amount); } - + // Safely tranfer the NFTs with the ops for (const auto &item : offer.item_ids) { const auto &nft_obj = item(d); @@ -244,7 +278,6 @@ namespace graphene d.apply_operation(xfer_context, xfer_op); } } - return void_result(); } FC_CAPTURE_AND_RETHROW((op)) diff --git a/libraries/wallet/include/graphene/wallet/wallet.hpp b/libraries/wallet/include/graphene/wallet/wallet.hpp index 76da2ac9..9183ddb0 100644 --- a/libraries/wallet/include/graphene/wallet/wallet.hpp +++ b/libraries/wallet/include/graphene/wallet/wallet.hpp @@ -1904,6 +1904,8 @@ class wallet_api * @param name Name of the token group * @param symbol Symbol of the token group * @param base_uri Base URI for token URI + * @param revenue_partner revenue partner for this type of Token + * @param revenue_split revenue split for the sale * @param broadcast true to broadcast transaction to the network * @return Signed transaction transfering the funds */ @@ -1911,6 +1913,8 @@ class wallet_api string name, string symbol, string base_uri, + optional revenue_partner, + optional revenue_split, bool broadcast); /** @@ -1919,6 +1923,8 @@ class wallet_api * @param name Name of the token group * @param symbol Symbol of the token group * @param base_uri Base URI for token URI + * @param revenue_partner revenue partner for this type of Token + * @param revenue_split revenue split for the sale * @param broadcast true to broadcast transaction to the network * @return Signed transaction transfering the funds */ @@ -1926,6 +1932,8 @@ class wallet_api string name, string symbol, string base_uri, + optional revenue_partner, + optional revenue_split, bool broadcast); /** diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index 86d28100..5fe247ad 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -6161,6 +6161,8 @@ signed_transaction wallet_api::nft_metadata_create(string owner_account_id_or_na string name, string symbol, string base_uri, + optional revenue_partner, + optional revenue_split, bool broadcast) { account_object owner_account = my->get_account(owner_account_id_or_name); @@ -6170,6 +6172,17 @@ signed_transaction wallet_api::nft_metadata_create(string owner_account_id_or_na op.name = name; op.symbol = symbol; op.base_uri = base_uri; + if( revenue_partner ) + { + account_object partner_account = my->get_account(*revenue_partner); + op.revenue_partner = partner_account.id; + double rev_split = 0.0; + if( revenue_split ) + { + rev_split = *revenue_split; + } + op.revenue_split = rev_split; + } signed_transaction trx; trx.operations.push_back(op); @@ -6183,6 +6196,8 @@ signed_transaction wallet_api::nft_metadata_update(string owner_account_id_or_na string name, string symbol, string base_uri, + optional revenue_partner, + optional revenue_split, bool broadcast) { account_object owner_account = my->get_account(owner_account_id_or_name); @@ -6192,6 +6207,17 @@ signed_transaction wallet_api::nft_metadata_update(string owner_account_id_or_na op.name = name; op.symbol = symbol; op.base_uri = base_uri; + if( revenue_partner ) + { + account_object partner_account = my->get_account(*revenue_partner); + op.revenue_partner = partner_account.id; + double rev_split = 0.0; + if( revenue_split ) + { + rev_split = *revenue_split; + } + op.revenue_split = rev_split; + } signed_transaction trx; trx.operations.push_back(op); diff --git a/tests/tests/marketplace_tests.cpp b/tests/tests/marketplace_tests.cpp index 0327da58..b73d5dc7 100644 --- a/tests/tests/marketplace_tests.cpp +++ b/tests/tests/marketplace_tests.cpp @@ -33,6 +33,8 @@ BOOST_AUTO_TEST_CASE(nft_metadata_create_test) op.name = "NFT Test"; op.symbol = "NFT"; op.base_uri = "http://nft.example.com"; + op.revenue_partner = mdowner_id; + op.revenue_split = 0.1; trx.operations.push_back(op); sign(trx, mdowner_private_key); @@ -337,10 +339,12 @@ BOOST_AUTO_TEST_CASE(best_buy_bid_for_sell_offer) GET_ACTOR(bob); GET_ACTOR(charlie); GET_ACTOR(operator1); + GET_ACTOR(mdowner); int64_t bob_balance = get_balance(bob_id(db), asset_id_type()(db)); int64_t alice_balance = get_balance(alice_id(db), asset_id_type()(db)); int64_t charlie_balance = get_balance(charlie_id(db), asset_id_type()(db)); + int64_t mdowner_balance = get_balance(mdowner_id(db), asset_id_type()(db)); const auto &offer_obj = sell_offer(db); bid_operation bid_op; @@ -375,8 +379,11 @@ BOOST_AUTO_TEST_CASE(best_buy_bid_for_sell_offer) auto cached_offer_obj = offer_obj; // Generate a block and offer should be finalized with bid generate_block(); + int64_t partner_fee = 2 * static_cast((0.1 * (*cached_offer_obj.bid_price).amount.value)/2); BOOST_CHECK_EQUAL(get_balance(alice_id(db), asset_id_type()(db)), - (alice_balance + cached_offer_obj.maximum_price.amount).value); + (alice_balance + cached_offer_obj.maximum_price.amount).value - partner_fee); + BOOST_CHECK_EQUAL(get_balance(mdowner_id(db), asset_id_type()(db)), + mdowner_balance + partner_fee); const auto &oidx = db.get_index_type().indices().get(); const auto &ohidx = db.get_index_type().indices().get(); BOOST_REQUIRE(oidx.size() == 0); @@ -408,12 +415,17 @@ BOOST_AUTO_TEST_CASE(expire_with_bid_for_sell_offer_test) INVOKE(second_buy_bid_for_sell_offer_test); GET_ACTOR(alice); GET_ACTOR(charlie); + GET_ACTOR(mdowner); int64_t alice_balance = get_balance(alice_id(db), asset_id_type()(db)); + int64_t mdowner_balance = get_balance(mdowner_id(db), asset_id_type()(db)); const auto &offer_obj = sell_offer(db); auto cached_offer_obj = offer_obj; generate_blocks(5); + int64_t partner_fee = 2 * static_cast((0.1 * (*cached_offer_obj.bid_price).amount.value)/2); + BOOST_CHECK_EQUAL(get_balance(mdowner_id(db), asset_id_type()(db)), + mdowner_balance + partner_fee); BOOST_CHECK_EQUAL(get_balance(alice_id(db), asset_id_type()(db)), - (alice_balance + (*cached_offer_obj.bid_price).amount).value); + (alice_balance + (*cached_offer_obj.bid_price).amount).value - partner_fee); const auto &oidx = db.get_index_type().indices().get(); const auto &ohidx = db.get_index_type().indices().get(); BOOST_REQUIRE(oidx.size() == 0); @@ -624,9 +636,11 @@ BOOST_AUTO_TEST_CASE(best_sell_bid_for_buy_offer) INVOKE(second_sell_bid_for_buy_offer_test); GET_ACTOR(alice); GET_ACTOR(bob); + GET_ACTOR(mdowner); int64_t bob_balance = get_balance(bob_id(db), asset_id_type()(db)); int64_t alice_balance = get_balance(alice_id(db), asset_id_type()(db)); + int64_t mdowner_balance = get_balance(mdowner_id(db), asset_id_type()(db)); const auto &offer_obj = buy_offer(db); bid_operation bid_op; @@ -658,8 +672,11 @@ BOOST_AUTO_TEST_CASE(best_sell_bid_for_buy_offer) // Generate a block and offer should be finalized with bid generate_block(); // Check balances + int64_t partner_fee = 2 * static_cast((0.1 * (*cached_offer_obj.bid_price).amount.value)/2); + BOOST_CHECK_EQUAL(get_balance(mdowner_id(db), asset_id_type()(db)), + mdowner_balance + partner_fee); BOOST_CHECK_EQUAL(get_balance(bob_id(db), asset_id_type()(db)), - (bob_balance + exp_delta_bidder1.amount).value); + (bob_balance + exp_delta_bidder1.amount).value - partner_fee); BOOST_CHECK_EQUAL(get_balance(alice_id(db), asset_id_type()(db)), (alice_balance + exp_delta_bidder2.amount).value); const auto &oidx = db.get_index_type().indices().get(); @@ -693,15 +710,20 @@ BOOST_AUTO_TEST_CASE(expire_with_bid_for_buy_offer_test) INVOKE(second_sell_bid_for_buy_offer_test); GET_ACTOR(alice); GET_ACTOR(bob); + GET_ACTOR(mdowner); int64_t alice_balance = get_balance(alice_id(db), asset_id_type()(db)); int64_t bob_balance = get_balance(bob_id(db), asset_id_type()(db)); + int64_t mdowner_balance = get_balance(mdowner_id(db), asset_id_type()(db)); const auto &offer_obj = buy_offer(db); auto cached_offer_obj = offer_obj; generate_blocks(5); + int64_t partner_fee = 2 * static_cast((0.1 * (*cached_offer_obj.bid_price).amount.value)/2); + BOOST_CHECK_EQUAL(get_balance(mdowner_id(db), asset_id_type()(db)), + mdowner_balance + partner_fee); BOOST_CHECK_EQUAL(get_balance(alice_id(db), asset_id_type()(db)), (alice_balance + cached_offer_obj.maximum_price.amount - (*cached_offer_obj.bid_price).amount).value); BOOST_CHECK_EQUAL(get_balance(bob_id(db), asset_id_type()(db)), - (bob_balance + (*cached_offer_obj.bid_price).amount).value); + (bob_balance + (*cached_offer_obj.bid_price).amount).value - partner_fee); const auto &oidx = db.get_index_type().indices().get(); const auto &ohidx = db.get_index_type().indices().get(); BOOST_REQUIRE(oidx.size() == 0);