From b9304caffa8b737dfb055a2991c9dfc6d668316d Mon Sep 17 00:00:00 2001 From: Eric Frias Date: Sun, 26 Jun 2016 15:41:07 -0400 Subject: [PATCH] Cherry-picked commit abc7853. Initial work on dividend-paying assets. Basic functionality works in simple cases. --- libraries/app/impacted.cpp | 1 + libraries/chain/asset_evaluator.cpp | 61 ++++ libraries/chain/db_init.cpp | 5 +- libraries/chain/db_maint.cpp | 287 ++++++++++++++++++ .../include/graphene/chain/account_object.hpp | 55 ++++ .../graphene/chain/asset_evaluator.hpp | 12 + .../include/graphene/chain/asset_object.hpp | 86 ++++++ .../graphene/chain/protocol/asset_ops.hpp | 68 +++++ .../graphene/chain/protocol/operations.hpp | 3 +- .../include/graphene/chain/protocol/types.hpp | 12 +- libraries/chain/protocol/asset_ops.cpp | 10 + libraries/wallet/wallet.cpp | 25 +- tests/common/database_fixture.cpp | 18 ++ tests/common/database_fixture.hpp | 3 + tests/tests/operation_tests.cpp | 222 ++++++++++++++ 15 files changed, 863 insertions(+), 5 deletions(-) diff --git a/libraries/app/impacted.cpp b/libraries/app/impacted.cpp index 7a5370dc..b5dbd979 100644 --- a/libraries/app/impacted.cpp +++ b/libraries/app/impacted.cpp @@ -90,6 +90,7 @@ struct get_impacted_account_visitor } void operator()( const asset_update_bitasset_operation& op ) {} + void operator()( const asset_update_dividend_operation& op ) {} void operator()( const asset_update_feed_producers_operation& op ) {} void operator()( const asset_issue_operation& op ) diff --git a/libraries/chain/asset_evaluator.cpp b/libraries/chain/asset_evaluator.cpp index 1f2826a0..16b6ee18 100644 --- a/libraries/chain/asset_evaluator.cpp +++ b/libraries/chain/asset_evaluator.cpp @@ -32,6 +32,8 @@ #include +#include + namespace graphene { namespace chain { void_result asset_create_evaluator::do_evaluate( const asset_create_operation& op ) @@ -366,6 +368,65 @@ void_result asset_update_bitasset_evaluator::do_apply(const asset_update_bitasse return void_result(); } FC_CAPTURE_AND_RETHROW( (o) ) } +void_result asset_update_dividend_evaluator::do_evaluate(const asset_update_dividend_operation& o) +{ try { + database& d = db(); + + const asset_object& a = o.asset_to_update(d); + asset_to_update = &a; + + FC_ASSERT( o.issuer == a.issuer, "", ("o.issuer", o.issuer)("a.issuer", a.issuer) ); + auto& params = db().get_global_properties().parameters; + if (o.new_options.payout_interval && + *o.new_options.payout_interval < params.maintenance_interval) + FC_THROW("New payout interval may not be less than the maintenance interval", + ("new_payout_interval", o.new_options.payout_interval)("maintenance_interval", params.maintenance_interval)); + return void_result(); +} FC_CAPTURE_AND_RETHROW( (o) ) } + +void_result asset_update_dividend_evaluator::do_apply( const asset_update_dividend_operation& op ) +{ try { + database& d = db(); + if (!asset_to_update->dividend_data_id) + { + // this was not a dividend-paying asset, we're converting it to a dividend-paying asset + std::string dividend_distribution_account_name(boost::to_lower_copy(asset_to_update->symbol) + "-dividend-distribution"); + + const auto& new_acnt_object = db().create( [&]( account_object& obj ){ + obj.registrar = op.issuer; + obj.referrer = op.issuer; + obj.lifetime_referrer = op.issuer(db()).lifetime_referrer; + + auto& params = db().get_global_properties().parameters; + obj.network_fee_percentage = GRAPHENE_DEFAULT_NETWORK_PERCENT_OF_FEE; + obj.lifetime_referrer_fee_percentage = GRAPHENE_DEFAULT_LIFETIME_REFERRER_PERCENT_OF_FEE; + obj.referrer_rewards_percentage = GRAPHENE_DEFAULT_LIFETIME_REFERRER_PERCENT_OF_FEE; + + obj.name = dividend_distribution_account_name; + obj.owner.weight_threshold = 1; + obj.active.weight_threshold = 1; + obj.statistics = db().create([&](account_statistics_object& s){s.owner = obj.id;}).id; + }); + + const asset_dividend_data_object& dividend_data = d.create( [&]( asset_dividend_data_object& dividend_data_obj ) { + dividend_data_obj.options = op.new_options; + dividend_data_obj.dividend_distribution_account = new_acnt_object.id; + }); + + d.modify(*asset_to_update, [&](asset_object& a) { + a.dividend_data_id = dividend_data.id; + }); + } + else + { + const asset_dividend_data_object& dividend_data = asset_to_update->dividend_data(d); + d.modify(dividend_data, [&]( asset_dividend_data_object& dividend_data_obj ) { + dividend_data_obj.options = op.new_options; + }); + } + return void_result(); +} FC_CAPTURE_AND_RETHROW( (op) ) } + void_result asset_update_feed_producers_evaluator::do_evaluate(const asset_update_feed_producers_evaluator::operation_type& o) { try { database& d = db(); diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index a57af12d..d19ce204 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -183,6 +183,7 @@ void database::initialize_evaluators() register_evaluator(); register_evaluator(); register_evaluator(); + register_evaluator(); register_evaluator(); register_evaluator(); register_evaluator(); @@ -259,6 +260,7 @@ void database::initialize_indexes() add_index< primary_index >(); add_index< primary_index >(); add_index< primary_index >(); + add_index< primary_index >(); add_index< primary_index> >(); add_index< primary_index> >(); add_index< primary_index> >(); @@ -269,10 +271,11 @@ void database::initialize_indexes() add_index< primary_index > >(); add_index< primary_index< special_authority_index > >(); add_index< primary_index< buyback_index > >(); - add_index< primary_index< simple_index< fba_accumulator_object > > >(); add_index< primary_index< betting_market_position_index > >(); add_index< primary_index< global_betting_statistics_object_index > >(); + add_index< primary_index >(); + add_index< primary_index >(); } void database::init_genesis(const genesis_state_type& genesis_state) diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index d515a961..6bef6051 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -716,6 +716,291 @@ void deprecate_annual_members( database& db ) return; } +void schedule_pending_dividend_balances(database& db, + const asset_object& dividend_holder_asset_obj, + const asset_dividend_data_object& dividend_data, + const account_balance_index& balance_index, + const distributed_dividend_balance_object_index& distributed_dividend_balance_index, + const pending_dividend_payout_balance_object_index& pending_payout_balance_index) +{ + dlog("Processing dividend payments for dividend holder asset asset type ${holder_asset}", ("holder_asset", dividend_holder_asset_obj.symbol)); + auto current_distribution_account_balance_range = + balance_index.indices().get().equal_range(boost::make_tuple(dividend_data.dividend_distribution_account)); + auto previous_distribution_account_balance_range = + distributed_dividend_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id)); + // the current range is now all current balances for the distribution account, sorted by asset_type + // the previous range is now all previous balances for this account, sorted by asset type + + auto current_distribution_account_balance_iter = current_distribution_account_balance_range.first; + auto previous_distribution_account_balance_iter = previous_distribution_account_balance_range.first; + dlog("Current balances in distribution account: ${current}, Previous balances: ${previous}", + ("current", std::distance(current_distribution_account_balance_range.first, current_distribution_account_balance_range.second)) + ("previous", std::distance(previous_distribution_account_balance_range.first, previous_distribution_account_balance_range.second))); + + while (current_distribution_account_balance_iter != current_distribution_account_balance_range.second || + previous_distribution_account_balance_iter != previous_distribution_account_balance_range.second) + { + share_type current_balance; + share_type previous_balance; + asset_id_type payout_asset_type; + + if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || + current_distribution_account_balance_iter->asset_type < previous_distribution_account_balance_iter->dividend_payout_asset_type) + { + payout_asset_type = current_distribution_account_balance_iter->asset_type; + current_balance = current_distribution_account_balance_iter->balance; + idump((payout_asset_type)(current_balance)); + } + else if (current_distribution_account_balance_iter == current_distribution_account_balance_range.second || + previous_distribution_account_balance_iter->dividend_payout_asset_type < current_distribution_account_balance_iter->asset_type) + { + payout_asset_type = previous_distribution_account_balance_iter->dividend_payout_asset_type; + previous_balance = previous_distribution_account_balance_iter->balance_at_last_maintenance_interval; + idump((payout_asset_type)(previous_balance)); + } + else + { + payout_asset_type = current_distribution_account_balance_iter->asset_type; + current_balance = current_distribution_account_balance_iter->balance; + previous_balance = previous_distribution_account_balance_iter->balance_at_last_maintenance_interval; + idump((payout_asset_type)(current_balance)(previous_balance)); + } + + share_type delta_balance = current_balance - previous_balance; + dlog("Processing dividend payments of asset type ${payout_asset_type}, delta balance is ${delta_balance}", ("payout_asset_type", payout_asset_type(db).symbol)("delta_balance", delta_balance)); + if (delta_balance > 0) + { + // we need to pay out delta_balance to shareholders proportional to their stake + auto holder_account_balance_range = + balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id)); + share_type total_balance_of_dividend_asset; + for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_account_balance_range.first, holder_account_balance_range.second)) + if (holder_balance_object.owner != dividend_data.dividend_distribution_account) + // TODO: if holder_balance_object.owner is able to accept payout_asset_type + total_balance_of_dividend_asset += holder_balance_object.balance; + + dlog("There are ${count} holders of the dividend-paying asset, with a total balance of ${total}", + ("count", std::distance(holder_account_balance_range.first, holder_account_balance_range.second)) + ("total", total_balance_of_dividend_asset)); + share_type remaining_amount_to_distribute = delta_balance; + share_type remaining_balance_of_dividend_asset = total_balance_of_dividend_asset; + + for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_account_balance_range.first, holder_account_balance_range.second)) + if (holder_balance_object.owner != dividend_data.dividend_distribution_account) + { + // TODO: if holder_balance_object.owner is able to accept payout_asset_type + fc::uint128_t amount_to_credit(remaining_amount_to_distribute.value); + amount_to_credit *= holder_balance_object.balance.value; + amount_to_credit /= remaining_balance_of_dividend_asset.value; + share_type shares_to_credit((int64_t)amount_to_credit.to_uint64()); + + remaining_amount_to_distribute -= shares_to_credit; + remaining_balance_of_dividend_asset -= holder_balance_object.balance; + + dlog("Crediting account ${account} with ${amount}", ("account", holder_balance_object.owner(db).name)("amount", amount_to_credit)); + auto pending_payout_iter = + pending_payout_balance_index.indices().get().find(boost::make_tuple(dividend_holder_asset_obj.id, payout_asset_type, holder_balance_object.owner)); + if (pending_payout_iter == pending_payout_balance_index.indices().get().end()) + db.create( [&]( pending_dividend_payout_balance_object& obj ){ + obj.owner = holder_balance_object.owner; + obj.dividend_holder_asset_type = dividend_holder_asset_obj.id; + obj.dividend_payout_asset_type = payout_asset_type; + obj.pending_balance = shares_to_credit; + }); + else + db.modify(*pending_payout_iter, [&]( pending_dividend_payout_balance_object& pending_balance ){ + pending_balance.pending_balance += shares_to_credit; + }); + } + + for (const auto& pending_payout : pending_payout_balance_index.indices()) + { + dlog("Pending payout: ${account_name} -> ${amount}", ("account_name", pending_payout.owner(db).name)("amount", pending_payout.pending_balance)); + } + + share_type distributed_amount = current_balance - remaining_amount_to_distribute; + if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || + previous_distribution_account_balance_iter->dividend_payout_asset_type != payout_asset_type) + db.create( [&]( distributed_dividend_balance_object& obj ){ + obj.dividend_holder_asset_type = dividend_holder_asset_obj.id; + obj.dividend_payout_asset_type = payout_asset_type; + obj.balance_at_last_maintenance_interval = distributed_amount; + }); + else + db.modify(*previous_distribution_account_balance_iter, [&]( distributed_dividend_balance_object& obj ){ + obj.balance_at_last_maintenance_interval = distributed_amount; + }); + } + else if (delta_balance < 0) + { + // some amount of the asset has been withdrawn from the dividend_distribution_account, + // meaning the current pending payout balances will add up to more than our current balance. + // This should be extremely rare. + // Reduce all pending payouts proportionally + share_type total_pending_balances; + auto pending_payouts_range = + pending_payout_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id, payout_asset_type)); + + for (const pending_dividend_payout_balance_object& pending_balance_object : boost::make_iterator_range(pending_payouts_range.first, pending_payouts_range.second)) + total_pending_balances += pending_balance_object.pending_balance; + + share_type remaining_amount_to_recover = -delta_balance; + share_type remaining_pending_balances = total_pending_balances; + for (const pending_dividend_payout_balance_object& pending_balance_object : boost::make_iterator_range(pending_payouts_range.first, pending_payouts_range.second)) + { + fc::uint128_t amount_to_debit(remaining_amount_to_recover.value); + amount_to_debit *= pending_balance_object.pending_balance.value; + amount_to_debit /= remaining_pending_balances.value; + share_type shares_to_debit((int64_t)amount_to_debit.to_uint64()); + + remaining_amount_to_recover -= shares_to_debit; + remaining_pending_balances -= pending_balance_object.pending_balance; + + db.modify(pending_balance_object, [&]( pending_dividend_payout_balance_object& pending_balance ){ + pending_balance.pending_balance -= shares_to_debit; + }); + } + + if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || + previous_distribution_account_balance_iter->dividend_payout_asset_type != payout_asset_type) + db.create( [&]( distributed_dividend_balance_object& obj ){ + obj.dividend_holder_asset_type = dividend_holder_asset_obj.id; + obj.dividend_payout_asset_type = payout_asset_type; + obj.balance_at_last_maintenance_interval = 0; + }); + else + db.modify(*previous_distribution_account_balance_iter, [&]( distributed_dividend_balance_object& obj ){ + obj.balance_at_last_maintenance_interval = 0; + }); + } + + // iterate + if (previous_distribution_account_balance_iter == previous_distribution_account_balance_range.second || + current_distribution_account_balance_iter->asset_type < previous_distribution_account_balance_iter->dividend_payout_asset_type) + ++current_distribution_account_balance_iter; + else if (current_distribution_account_balance_iter == current_distribution_account_balance_range.second || + previous_distribution_account_balance_iter->dividend_payout_asset_type < current_distribution_account_balance_iter->asset_type) + ++previous_distribution_account_balance_iter; + else + { + ++current_distribution_account_balance_iter; + ++previous_distribution_account_balance_iter; + } + } +} + +void process_dividend_assets(database& db) +{ + ilog("In process_dividend_assets time ${time}", ("time", db.head_block_time())); + + const account_balance_index& balance_index = db.get_index_type(); + const distributed_dividend_balance_object_index& distributed_dividend_balance_index = db.get_index_type(); + const pending_dividend_payout_balance_object_index& pending_payout_balance_index = db.get_index_type(); + + // TODO: switch to iterating over only dividend assets (generalize the by_type index) + for( const asset_object& dividend_holder_asset_obj : db.get_index_type().indices() ) + if (dividend_holder_asset_obj.dividend_data_id) + { + const asset_dividend_data_object& dividend_data = dividend_holder_asset_obj.dividend_data(db); + schedule_pending_dividend_balances(db, dividend_holder_asset_obj, dividend_data, + balance_index, distributed_dividend_balance_index, pending_payout_balance_index); + fc::time_point_sec current_head_block_time = db.head_block_time(); + if (dividend_data.options.next_payout_time && + db.head_block_time() >= *dividend_data.options.next_payout_time) + { + dlog("Dividend payout time has arrived for asset ${holder_asset}", + ("holder_asset", dividend_holder_asset_obj.symbol)); + +#ifndef NDEBUG + // dump balances before the payouts for debugging + const auto& balance_idx = db.get_index_type().indices().get(); + auto holder_account_balance_range = balance_idx.equal_range(boost::make_tuple(dividend_data.dividend_distribution_account)); + for (const account_balance_object& holder_balance_object : boost::make_iterator_range(holder_account_balance_range.first, holder_account_balance_range.second)) + ilog(" Current balance: ${asset}", ("asset", asset(holder_balance_object.balance, holder_balance_object.asset_type))); +#endif + + // when we do the payouts, we first increase the balances in all of the receiving accounts + // and use this map to keep track of the total amount of each asset paid out. + // Afterwards, we decrease the distribution account's balance by the total amount paid out, + // and modify the distributed_balances accordingly +#ifndef NDEBUG + // for debugging, sum up our payouts here + std::map amounts_paid_out_by_asset; +#endif + auto pending_payouts_range = + pending_payout_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id)); + for (auto pending_balance_object_iter = pending_payouts_range.first; pending_balance_object_iter != pending_payouts_range.second; ) + { + const pending_dividend_payout_balance_object& pending_balance_object = *pending_balance_object_iter; + ilog("Processing payout of ${asset} to account ${account}", + ("asset", asset(pending_balance_object.pending_balance, pending_balance_object.dividend_payout_asset_type)) + ("account", pending_balance_object.owner(db).name)); + + db.adjust_balance(pending_balance_object.owner, + asset(pending_balance_object.pending_balance, + pending_balance_object.dividend_payout_asset_type)); +#ifndef NDEBUG + amounts_paid_out_by_asset[pending_balance_object.dividend_payout_asset_type] += pending_balance_object.pending_balance; +#endif + + ++pending_balance_object_iter; + db.remove(pending_balance_object); + } + + // now debit the total amount of dividends paid out from the distribution account + auto distributed_balance_range = + distributed_dividend_balance_index.indices().get().equal_range(boost::make_tuple(dividend_holder_asset_obj.id)); + +#ifndef NDEBUG + // validate that we actually paid out exactly as much as we had planned to + assert(amounts_paid_out_by_asset.size() == std::distance(distributed_balance_range.first, distributed_balance_range.second)); + if (amounts_paid_out_by_asset.size() == std::distance(distributed_balance_range.first, distributed_balance_range.second)) + { + auto distributed_balance_object_iter = distributed_balance_range.first; + for (const auto& asset_and_amount_paid_out : amounts_paid_out_by_asset) + { + assert(distributed_balance_object_iter->dividend_payout_asset_type == asset_and_amount_paid_out.first); + assert(distributed_balance_object_iter->balance_at_last_maintenance_interval == asset_and_amount_paid_out.second); + ++distributed_balance_object_iter; + } + } +#endif + + for (auto distributed_balance_object_iter = distributed_balance_range.first; distributed_balance_object_iter != distributed_balance_range.second; ) + { + const distributed_dividend_balance_object& distributed_balance_object = *distributed_balance_object_iter; + db.adjust_balance(dividend_data.dividend_distribution_account, + asset(-distributed_balance_object.balance_at_last_maintenance_interval, + distributed_balance_object.dividend_payout_asset_type)); + ++distributed_balance_object_iter; + db.remove(distributed_balance_object); // now they've been paid out, reset to zero + } + + // now schedule the next payout time + db.modify(dividend_data, [current_head_block_time](asset_dividend_data_object& dividend_data_obj) { + dlog("Updating dividend payout time, new values are:"); + dividend_data_obj.last_scheduled_payout_time = dividend_data_obj.options.next_payout_time; + dividend_data_obj.last_payout_time = current_head_block_time; + fc::optional next_payout_time; + if (dividend_data_obj.options.payout_interval) + { + // if there was a previous payout, make our next payment one interval + uint32_t current_time_sec = current_head_block_time.sec_since_epoch(); + fc::time_point_sec reference_time = *dividend_data_obj.last_scheduled_payout_time; + uint32_t next_possible_time_sec = dividend_data_obj.last_scheduled_payout_time->sec_since_epoch(); + do + next_possible_time_sec += *dividend_data_obj.options.payout_interval; + while (next_possible_time_sec <= current_time_sec); + + next_payout_time = next_possible_time_sec; + } + dividend_data_obj.options.next_payout_time = next_payout_time; + idump((dividend_data_obj.last_scheduled_payout_time)(dividend_data_obj.last_payout_time)(dividend_data_obj.options.next_payout_time)); + }); + } + } +} + void database::perform_chain_maintenance(const signed_block& next_block, const global_property_object& global_props) { const auto& gpo = get_global_properties(); @@ -723,6 +1008,8 @@ void database::perform_chain_maintenance(const signed_block& next_block, const g distribute_fba_balances(*this); create_buyback_orders(*this); + process_dividend_assets(*this); + struct vote_tally_helper { database& d; const global_property_object& props; diff --git a/libraries/chain/include/graphene/chain/account_object.hpp b/libraries/chain/include/graphene/chain/account_object.hpp index 522cb7bc..d7ba8b34 100644 --- a/libraries/chain/include/graphene/chain/account_object.hpp +++ b/libraries/chain/include/graphene/chain/account_object.hpp @@ -311,6 +311,31 @@ namespace graphene { namespace chain { /** maps the referrer to the set of accounts that they have referred */ map< account_id_type, set > referred_by; }; + + /** + * @brief Tracks a pending payout of a single dividend payout asset + * from a single dividend holder asset to a holder's account. + * + * Each maintenance interval, this will be adjusted to account for + * any new transfers to the dividend distribution account. + * @ingroup object + * + */ + class pending_dividend_payout_balance_object : public abstract_object + { + public: + static const uint8_t space_id = implementation_ids; + static const uint8_t type_id = impl_pending_dividend_payout_balance_object_type; + + account_id_type owner; + asset_id_type dividend_holder_asset_type; + asset_id_type dividend_payout_asset_type; + share_type pending_balance; + + asset get_pending_balance()const { return asset(pending_balance, dividend_payout_asset_type); } + void adjust_balance(const asset& delta); + }; + struct by_account_asset; struct by_asset_balance; @@ -367,6 +392,31 @@ namespace graphene { namespace chain { */ typedef generic_index account_index; + struct by_dividend_asset_account_asset{}; + + /** + * @ingroup object_index + */ + typedef multi_index_container< + pending_dividend_payout_balance_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > >, + ordered_unique< tag, + composite_key< + pending_dividend_payout_balance_object, + member, + member, + member + > + > + > + > pending_dividend_payout_balance_object_multi_index_type; + + /** + * @ingroup object_index + */ + typedef generic_index pending_dividend_payout_balance_object_index; + }} FC_REFLECT_DERIVED( graphene::chain::account_object, @@ -395,3 +445,8 @@ FC_REFLECT_DERIVED( graphene::chain::account_statistics_object, (pending_fees)(pending_vested_fees) ) +FC_REFLECT_DERIVED( graphene::chain::pending_dividend_payout_balance_object, + (graphene::db::object), + (owner)(dividend_holder_asset_type)(dividend_payout_asset_type)(pending_balance) ) + + diff --git a/libraries/chain/include/graphene/chain/asset_evaluator.hpp b/libraries/chain/include/graphene/chain/asset_evaluator.hpp index eb8d5789..234a60d7 100644 --- a/libraries/chain/include/graphene/chain/asset_evaluator.hpp +++ b/libraries/chain/include/graphene/chain/asset_evaluator.hpp @@ -82,6 +82,18 @@ namespace graphene { namespace chain { const asset_bitasset_data_object* bitasset_to_update = nullptr; }; + class asset_update_dividend_evaluator : public evaluator + { + public: + typedef asset_update_dividend_operation operation_type; + + void_result do_evaluate( const asset_update_dividend_operation& o ); + void_result do_apply( const asset_update_dividend_operation& o ); + + const asset_object* asset_to_update = nullptr; + const asset_dividend_data_object* asset_dividend_data_to_update = nullptr; + }; + class asset_update_feed_producers_evaluator : public evaluator { public: diff --git a/libraries/chain/include/graphene/chain/asset_object.hpp b/libraries/chain/include/graphene/chain/asset_object.hpp index d93993fe..f41afbe1 100644 --- a/libraries/chain/include/graphene/chain/asset_object.hpp +++ b/libraries/chain/include/graphene/chain/asset_object.hpp @@ -132,6 +132,9 @@ namespace graphene { namespace chain { optional buyback_account; + /// Extra data associated with dividend-paying assets. + optional dividend_data_id; + asset_id_type get_id()const { return id; } void validate()const @@ -148,6 +151,10 @@ namespace graphene { namespace chain { const asset_bitasset_data_object& bitasset_data(const DB& db)const { assert(bitasset_data_id); return db.get(*bitasset_data_id); } + template + const asset_dividend_data_object& dividend_data(const DB& db)const + { assert(dividend_data_id); return db.get(*dividend_data_id); } + template const asset_dynamic_data_object& dynamic_data(const DB& db)const { return db.get(dynamic_asset_data_id); } @@ -249,6 +256,72 @@ namespace graphene { namespace chain { > asset_object_multi_index_type; typedef generic_index asset_index; + /** + * @brief contains properties that only apply to dividend-paying assets + * + * @ingroup object + * @ingroup implementation + */ + class asset_dividend_data_object : public abstract_object + { + public: + static const uint8_t space_id = implementation_ids; + static const uint8_t type_id = impl_asset_dividend_data_type; + + /// The tunable options for Dividend-paying assets are stored in this field. + dividend_asset_options options; + + /// The time payouts on this asset were scheduled to be processed last + fc::optional last_scheduled_payout_time; + + /// The time payouts on this asset were last processed + /// (this should be the maintenance interval at or after last_scheduled_payout_time) + fc::optional last_payout_time; + + /// The account which collects pending payouts + account_id_type dividend_distribution_account; + }; + typedef multi_index_container< + asset_dividend_data_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > > + > + > asset_dividend_data_object_multi_index_type; + typedef generic_index asset_dividend_data_object_index; + + + // This tracks the balances in a dividend distribution account at the last time + // pending dividend payouts were calculated (last maintenance interval). + // At each maintenance interval, we will compare the current balance to the + // balance stored here to see how much was deposited during that interval. + class distributed_dividend_balance_object : public abstract_object + { + public: + static const uint8_t space_id = implementation_ids; + static const uint8_t type_id = impl_distributed_dividend_balance_data_type; + + asset_id_type dividend_holder_asset_type; + asset_id_type dividend_payout_asset_type; + share_type balance_at_last_maintenance_interval; + }; + struct by_dividend_payout_asset{}; + typedef multi_index_container< + distributed_dividend_balance_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > >, + ordered_unique< tag, + composite_key< + distributed_dividend_balance_object, + member, + member + > + > + > + > distributed_dividend_balance_object_multi_index_type; + typedef generic_index distributed_dividend_balance_object_index; + + + } } // graphene::chain FC_REFLECT_DERIVED( graphene::chain::asset_dynamic_data_object, (graphene::db::object), @@ -264,6 +337,19 @@ FC_REFLECT_DERIVED( graphene::chain::asset_bitasset_data_object, (graphene::db:: (settlement_price) (settlement_fund) ) + +FC_REFLECT_DERIVED( graphene::chain::asset_dividend_data_object, (graphene::db::object), + (options) + (last_scheduled_payout_time) + (last_payout_time ) + (dividend_distribution_account) + ) + +FC_REFLECT_DERIVED( graphene::chain::distributed_dividend_balance_object, (graphene::db::object), + (dividend_holder_asset_type) + (dividend_payout_asset_type) + (balance_at_last_maintenance_interval) + ) FC_REFLECT_DERIVED( graphene::chain::asset_object, (graphene::db::object), (symbol) diff --git a/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp b/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp index 3f5ede19..eab3ae38 100644 --- a/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp +++ b/libraries/chain/include/graphene/chain/protocol/asset_ops.hpp @@ -112,6 +112,30 @@ namespace graphene { namespace chain { void validate()const; }; + /** + * @brief The dividend_asset_options struct contains configurable options available only to dividend-paying assets. + * + * @note Changes to this struct will break protocol compatibility + */ + struct dividend_asset_options { + /// Time when the next payout should occur. + /// The payouts will happen on the maintenance interval at or after this time + /// If this is set to null, there will be no payouts. + fc::optional next_payout_time; + /// If payouts happen on a fixed schedule, this specifies the interval between + /// payouts in seconds. After each payout, the next payout time will be incremented by + /// this amount. + /// If payout_interval is not set, the next payout (if any) will be the last until + /// the options are updated again. + fc::optional payout_interval; + + extensions_type extensions; + + /// Perform internal consistency checks. + /// @throws fc::exception if any check fails + void validate()const; + }; + /** * @ingroup operations @@ -319,6 +343,35 @@ namespace graphene { namespace chain { void validate()const; }; + /** + * @brief Update options specific to dividend-paying assets + * @ingroup operations + * + * Dividend-paying assets have some options which are not relevant to other asset types. + * This operation is used to update those options an an existing dividend-paying asset. + * This can also be used to convert a non-dividend-paying asset into a dividend-paying + * asset. + * + * @pre @ref issuer MUST be an existing account and MUST match asset_object::issuer on @ref asset_to_update + * @pre @ref fee MUST be nonnegative, and @ref issuer MUST have a sufficient balance to pay it + * @pre @ref new_options SHALL be internally consistent, as verified by @ref validate() + * @post @ref asset_to_update will have dividend-specific options matching those of new_options + */ + struct asset_update_dividend_operation : public base_operation + { + struct fee_parameters_type { uint64_t fee = 500 * GRAPHENE_BLOCKCHAIN_PRECISION; }; + + asset fee; + account_id_type issuer; + asset_id_type asset_to_update; + + dividend_asset_options new_options; + extensions_type extensions; + + account_id_type fee_payer()const { return issuer; } + void validate()const; + }; + /** * @brief Update the set of feed-producing accounts for a BitAsset * @ingroup operations @@ -462,6 +515,13 @@ FC_REFLECT( graphene::chain::asset_options, (description) (extensions) ) + +FC_REFLECT( graphene::chain::dividend_asset_options, + (next_payout_time) + (payout_interval) + (extensions) + ) + FC_REFLECT( graphene::chain::bitasset_options, (feed_lifetime_sec) (minimum_feeds) @@ -480,6 +540,7 @@ FC_REFLECT( graphene::chain::asset_settle_cancel_operation::fee_parameters_type, FC_REFLECT( graphene::chain::asset_fund_fee_pool_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::asset_update_operation::fee_parameters_type, (fee)(price_per_kbyte) ) FC_REFLECT( graphene::chain::asset_update_bitasset_operation::fee_parameters_type, (fee) ) +FC_REFLECT( graphene::chain::asset_update_dividend_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::asset_update_feed_producers_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::asset_publish_feed_operation::fee_parameters_type, (fee) ) FC_REFLECT( graphene::chain::asset_issue_operation::fee_parameters_type, (fee)(price_per_kbyte) ) @@ -511,6 +572,13 @@ FC_REFLECT( graphene::chain::asset_update_bitasset_operation, (new_options) (extensions) ) +FC_REFLECT( graphene::chain::asset_update_dividend_operation, + (fee) + (issuer) + (asset_to_update) + (new_options) + (extensions) + ) FC_REFLECT( graphene::chain::asset_update_feed_producers_operation, (fee)(issuer)(asset_to_update)(new_feed_producers)(extensions) ) diff --git a/libraries/chain/include/graphene/chain/protocol/operations.hpp b/libraries/chain/include/graphene/chain/protocol/operations.hpp index 097ad854..f9dc0099 100644 --- a/libraries/chain/include/graphene/chain/protocol/operations.hpp +++ b/libraries/chain/include/graphene/chain/protocol/operations.hpp @@ -108,7 +108,8 @@ namespace graphene { namespace chain { betting_market_group_resolved_operation, // VIRTUAL bet_matched_operation, // VIRTUAL bet_cancel_operation, - bet_canceled_operation // VIRTUAL + bet_canceled_operation, // VIRTUAL + asset_update_dividend_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 b4d42d47..1d15ea88 100644 --- a/libraries/chain/include/graphene/chain/protocol/types.hpp +++ b/libraries/chain/include/graphene/chain/protocol/types.hpp @@ -164,7 +164,10 @@ namespace graphene { namespace chain { impl_buyback_object_type, impl_fba_accumulator_object_type, impl_betting_market_position_object_type, - impl_global_betting_statistics_object_type + impl_global_betting_statistics_object_type, + impl_asset_dividend_data_type, + impl_pending_dividend_payout_balance_object_type, + impl_distributed_dividend_balance_data_type }; //typedef fc::unsigned_int object_id_type; @@ -232,11 +235,15 @@ namespace graphene { namespace chain { class fba_accumulator_object; class betting_market_position_object; class global_betting_statistics_object; + class asset_dividend_data_object; + class pending_dividend_payout_balance_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; typedef object_id< implementation_ids, impl_asset_dynamic_data_type, asset_dynamic_data_object> asset_dynamic_data_id_type; typedef object_id< implementation_ids, impl_asset_bitasset_data_type, asset_bitasset_data_object> asset_bitasset_data_id_type; + typedef object_id< implementation_ids, impl_asset_dividend_data_type, asset_dividend_data_object> asset_dividend_data_id_type; + typedef object_id< implementation_ids, impl_pending_dividend_payout_balance_object_type, pending_dividend_payout_balance_object> pending_dividend_payout_balance_object_type; typedef object_id< implementation_ids, impl_account_balance_object_type, account_balance_object> account_balance_id_type; typedef object_id< implementation_ids, impl_account_statistics_object_type,account_statistics_object> account_statistics_id_type; typedef object_id< implementation_ids, impl_transaction_object_type, transaction_object> transaction_obj_id_type; @@ -400,6 +407,9 @@ FC_REFLECT_ENUM( graphene::chain::impl_object_type, (impl_fba_accumulator_object_type) (impl_betting_market_position_object_type) (impl_global_betting_statistics_object_type) + (impl_asset_dividend_data_type) + (impl_pending_dividend_payout_balance_object_type) + (impl_distributed_dividend_balance_data_type) ) FC_REFLECT_TYPENAME( graphene::chain::share_type ) diff --git a/libraries/chain/protocol/asset_ops.cpp b/libraries/chain/protocol/asset_ops.cpp index fd1c11be..fdf153a3 100644 --- a/libraries/chain/protocol/asset_ops.cpp +++ b/libraries/chain/protocol/asset_ops.cpp @@ -183,6 +183,12 @@ void asset_update_bitasset_operation::validate() const new_options.validate(); } +void asset_update_dividend_operation::validate() const +{ + FC_ASSERT( fee.amount >= 0 ); + new_options.validate(); +} + void asset_update_feed_producers_operation::validate() const { FC_ASSERT( fee.amount >= 0 ); @@ -201,6 +207,10 @@ void bitasset_options::validate() const FC_ASSERT(maximum_force_settlement_volume <= GRAPHENE_100_PERCENT); } +void dividend_asset_options::validate() const +{ +} + void asset_options::validate()const { FC_ASSERT( max_supply > 0 ); diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index adadf868..c9348ece 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -1199,6 +1199,27 @@ public: return sign_transaction( tx, broadcast ); } FC_CAPTURE_AND_RETHROW( (symbol)(new_options)(broadcast) ) } + signed_transaction update_dividend_asset(string symbol, + dividend_asset_options new_options, + bool broadcast /* = false */) + { try { + optional asset_to_update = find_asset(symbol); + if (!asset_to_update) + FC_THROW("No asset with that symbol exists!"); + + asset_update_dividend_operation update_op; + update_op.issuer = asset_to_update->issuer; + update_op.asset_to_update = asset_to_update->id; + update_op.new_options = new_options; + + signed_transaction tx; + tx.operations.push_back( update_op ); + set_operation_fees( tx, _remote_db->get_global_properties().parameters.current_fees); + tx.validate(); + + return sign_transaction( tx, broadcast ); + } FC_CAPTURE_AND_RETHROW( (symbol)(new_options)(broadcast) ) } + signed_transaction update_asset_feed_producers(string symbol, flat_set new_feed_producers, bool broadcast /* = false */) @@ -2224,7 +2245,7 @@ public: << "\n=====================================================================================" << "|=====================================================================================\n"; - for (int i = 0; i < bids.size() || i < asks.size() ; i++) + for (unsigned i = 0; i < bids.size() || i < asks.size() ; i++) { if ( i < bids.size() ) { @@ -2827,7 +2848,7 @@ vector wallet_api::get_account_history(string name, int limit) auto memo = o.op.visit(detail::operation_printer(ss, *my, o.result)); result.push_back( operation_detail{ memo, ss.str(), o } ); } - if( current.size() < std::min(100,limit) ) + if( (int)current.size() < std::min(100,limit) ) break; limit -= current.size(); } diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index 04fb62ec..b3c11c07 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -1063,6 +1063,24 @@ int64_t database_fixture::get_balance( const account_object& account, const asse return db.get_balance(account.get_id(), a.get_id()).amount.value; } +int64_t database_fixture::get_dividend_pending_payout_balance(asset_id_type dividend_holder_asset_type, + account_id_type dividend_holder_account_id, + asset_id_type dividend_payout_asset_type) const +{ + const pending_dividend_payout_balance_object_index& pending_payout_balance_index = + db.get_index_type(); + dlog("searching ${a}", ("a", dividend_holder_asset_type(db).symbol)); + dlog("searching ${b}", ("b", dividend_payout_asset_type(db).symbol)); + dlog("searching ${c}", ("c", dividend_holder_account_id(db).name)); + dlog("searching ${a} ${b} ${c}", ("a", dividend_holder_asset_type(db).symbol)("b", dividend_payout_asset_type(db).symbol)("c", dividend_holder_account_id(db).name)); + auto pending_payout_iter = + pending_payout_balance_index.indices().get().find(boost::make_tuple(dividend_holder_asset_type, dividend_payout_asset_type, dividend_holder_account_id)); + if (pending_payout_iter == pending_payout_balance_index.indices().get().end()) + return 0; + else + return pending_payout_iter->pending_balance.value; +} + vector< operation_history_object > database_fixture::get_operation_history( account_id_type account_id )const { vector< operation_history_object > result; diff --git a/tests/common/database_fixture.hpp b/tests/common/database_fixture.hpp index de800fe0..92ffb1b3 100644 --- a/tests/common/database_fixture.hpp +++ b/tests/common/database_fixture.hpp @@ -280,6 +280,9 @@ struct database_fixture { void print_joint_market( const string& syma, const string& symb )const; int64_t get_balance( account_id_type account, asset_id_type a )const; int64_t get_balance( const account_object& account, const asset_object& a )const; + int64_t get_dividend_pending_payout_balance(asset_id_type dividend_holder_asset_type, + account_id_type dividend_holder_account_id, + asset_id_type dividend_payout_asset_type) const; vector< operation_history_object > get_operation_history( account_id_type account_id )const; void process_operation_by_witnesses(operation op); const sport_object& create_sport(internationalized_string_type name); diff --git a/tests/tests/operation_tests.cpp b/tests/tests/operation_tests.cpp index 7b3867d7..fc9bdefa 100644 --- a/tests/tests/operation_tests.cpp +++ b/tests/tests/operation_tests.cpp @@ -1111,6 +1111,228 @@ BOOST_AUTO_TEST_CASE( uia_fees ) } } +BOOST_AUTO_TEST_CASE( create_dividend_uia ) +{ + using namespace graphene; + try { + BOOST_TEST_MESSAGE("Creating dividend holder asset"); + { + asset_create_operation creator; + creator.issuer = account_id_type(); + creator.fee = asset(); + creator.symbol = "DIVIDEND"; + creator.common_options.max_supply = 100000000; + creator.precision = 2; + creator.common_options.market_fee_percent = GRAPHENE_MAX_MARKET_FEE_PERCENT/100; /*1%*/ + creator.common_options.issuer_permissions = UIA_ASSET_ISSUER_PERMISSION_MASK; + creator.common_options.flags = charge_market_fee; + creator.common_options.core_exchange_rate = price({asset(2),asset(1,asset_id_type(1))}); + trx.operations.push_back(std::move(creator)); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + } + generate_block(); + + // it should not yet be a divdend asset + const auto& dividend_holder_asset_object = get_asset("DIVIDEND"); + BOOST_CHECK(!dividend_holder_asset_object.dividend_data_id); + + BOOST_TEST_MESSAGE("Converting the new asset to a dividend holder asset"); + { + asset_update_dividend_operation op; + op.issuer = dividend_holder_asset_object.issuer; + op.asset_to_update = dividend_holder_asset_object.id; + op.new_options.next_payout_time = fc::time_point::now() + fc::minutes(1); + op.new_options.payout_interval = 60 * 60 * 24 * 7; // one week + + trx.operations.push_back(op); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + } + generate_block(); + + //const auto& test_readback = get_asset("TEST"); + //BOOST_REQUIRE(test_readback.dividend_data_id); + BOOST_TEST_MESSAGE("Verifying the dividend holder asset options"); + BOOST_REQUIRE(dividend_holder_asset_object.dividend_data_id); + const auto& dividend_data = dividend_holder_asset_object.dividend_data(db); + { + BOOST_REQUIRE(dividend_data.options.payout_interval); + BOOST_CHECK_EQUAL(*dividend_data.options.payout_interval, 60 * 60 * 24 * 7); + } + + BOOST_TEST_MESSAGE("Updating the payout interval"); + { + asset_update_dividend_operation op; + op.issuer = dividend_holder_asset_object.issuer; + op.asset_to_update = dividend_holder_asset_object.id; + op.new_options.next_payout_time = fc::time_point::now() + fc::minutes(1); + op.new_options.payout_interval = 60 * 60 * 24; // one day + trx.operations.push_back(op); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + } + generate_block(); + + BOOST_TEST_MESSAGE("Verifying the updated dividend holder asset options"); + { + BOOST_REQUIRE(dividend_data.options.payout_interval); + BOOST_CHECK_EQUAL(*dividend_data.options.payout_interval, 60 * 60 * 24); + } + + const account_object& dividend_distribution_account = dividend_data.dividend_distribution_account(db); + BOOST_CHECK_EQUAL(dividend_distribution_account.name, "dividend-dividend-distribution"); + + + BOOST_TEST_MESSAGE("Creating test accounts"); + create_account("alice"); + create_account("bob"); + create_account("carol"); + create_account("dave"); + create_account("frank"); + generate_block(); + const account_object& alice = get_account("alice"); + const account_object& bob = get_account("bob"); + const account_object& carol = get_account("carol"); + const account_object& dave = get_account("dave"); + const account_object& frank = get_account("frank"); + + BOOST_TEST_MESSAGE("Creating test asset"); + { + asset_create_operation creator; + creator.issuer = account_id_type(); + creator.fee = asset(); + creator.symbol = "TEST"; + creator.common_options.max_supply = 100000000; + creator.precision = 2; + creator.common_options.market_fee_percent = GRAPHENE_MAX_MARKET_FEE_PERCENT/100; /*1%*/ + creator.common_options.issuer_permissions = UIA_ASSET_ISSUER_PERMISSION_MASK; + creator.common_options.flags = charge_market_fee; + creator.common_options.core_exchange_rate = price({asset(2),asset(1,asset_id_type(1))}); + trx.operations.push_back(std::move(creator)); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + } + generate_block(); + + // it should not yet be a divdend asset + const auto& test_asset_object = get_asset("TEST"); + + auto issue_asset_to_account = [&](const asset_object& asset_to_issue, const account_object& destination_account, int64_t amount_to_issue) + { + asset_issue_operation op; + op.issuer = asset_to_issue.issuer; + op.asset_to_issue = asset(amount_to_issue, asset_to_issue.id); + op.issue_to_account = destination_account.id; + trx.operations.push_back( op ); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + }; + + // Set up the first test, issue alice, bob, and carol each 100 DIVIDEND. + // Then deposit 300 TEST in the distribution account, and see that they + // each are credited 100 TEST. + issue_asset_to_account(dividend_holder_asset_object, alice, 100000); + issue_asset_to_account(dividend_holder_asset_object, bob, 100000); + issue_asset_to_account(dividend_holder_asset_object, carol, 100000); + + BOOST_TEST_MESSAGE("Issuing 300 TEST to the dividend account"); + issue_asset_to_account(test_asset_object, dividend_distribution_account, 30000); + + generate_block(); // get the maintenance skip slots out of the way + + BOOST_TEST_MESSAGE( "Generating blocks until next maintenance interval" ); + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + + // for (const auto& pending_payout : db.get_index_type().indices()) + // dlog("In test, pending payout: ${account_name} -> ${amount}", ("account_name", pending_payout.owner(db).name)("amount", pending_payout.pending_balance)); + + dlog("Test asset object symbol is ${symbol}", ("symbol", test_asset_object.get_id()(db).symbol)); + auto verify_pending_balance = [&](const account_object& holder_account_obj, const asset_object& payout_asset_obj, int64_t expected_balance) { + int64_t pending_balance = get_dividend_pending_payout_balance(dividend_holder_asset_object.id, + holder_account_obj.id, + payout_asset_obj.id); + BOOST_CHECK_EQUAL(pending_balance, expected_balance); + }; + verify_pending_balance(alice, test_asset_object, 10000); + verify_pending_balance(bob, test_asset_object, 10000); + verify_pending_balance(carol, test_asset_object, 10000); + + // For the second test, issue carol more than the other two, so it's + // alice: 100 DIVIDND, bob: 100 DIVIDEND, carol: 200 DIVIDEND + // Then deposit 400 TEST in the distribution account, and see that alice + // and bob are credited with 100 TEST, and carol gets 200 TEST + BOOST_TEST_MESSAGE("Issuing carol twice as much of the holder asset"); + issue_asset_to_account(dividend_holder_asset_object, carol, 100000); // one thousand at two digits of precision + issue_asset_to_account(test_asset_object, dividend_distribution_account, 40000); // one thousand at two digits of precision + BOOST_TEST_MESSAGE( "Generating blocks until next maintenance interval" ); + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + verify_pending_balance(alice, test_asset_object, 20000); + verify_pending_balance(bob, test_asset_object, 20000); + verify_pending_balance(carol, test_asset_object, 30000); + + auto advance_to_next_payout_time = [&]() { + // Advance to the next upcoming payout time + BOOST_REQUIRE(dividend_data.options.next_payout_time); + fc::time_point_sec next_payout_scheduled_time = *dividend_data.options.next_payout_time; + // generate blocks up to the next scheduled time + generate_blocks(next_payout_scheduled_time); + // if the scheduled time fell on a maintenance interval, then we should have paid out. + // if not, we need to advance to the next maintenance interval to trigger the payout + BOOST_REQUIRE(dividend_data.options.next_payout_time); + if (*dividend_data.options.next_payout_time == next_payout_scheduled_time) + generate_blocks(db.get_dynamic_global_properties().next_maintenance_time); + generate_block(); // get the maintenance skip slots out of the way + }; + + fc::time_point_sec old_next_payout_scheduled_time = *dividend_data.options.next_payout_time; + advance_to_next_payout_time(); + + + BOOST_REQUIRE_MESSAGE(dividend_data.options.next_payout_time, "No new payout was scheduled"); + BOOST_CHECK_MESSAGE(old_next_payout_scheduled_time != *dividend_data.options.next_payout_time, + "New payout was scheduled for the same time as the last payout"); + BOOST_CHECK_MESSAGE(old_next_payout_scheduled_time + *dividend_data.options.payout_interval == *dividend_data.options.next_payout_time, + "New payout was not scheduled for the expected time"); + + BOOST_CHECK_EQUAL(get_balance(alice, test_asset_object), 20000); + verify_pending_balance(alice, test_asset_object, 0); + BOOST_CHECK_EQUAL(get_balance(bob, test_asset_object), 20000); + verify_pending_balance(bob, test_asset_object, 0); + BOOST_CHECK_EQUAL(get_balance(carol, test_asset_object), 30000); + verify_pending_balance(carol, test_asset_object, 0); + + + BOOST_TEST_MESSAGE("Removing the payout interval"); + { + asset_update_dividend_operation op; + op.issuer = dividend_holder_asset_object.issuer; + op.asset_to_update = dividend_holder_asset_object.id; + op.new_options.next_payout_time = dividend_data.options.next_payout_time; + op.new_options.payout_interval = fc::optional(); + trx.operations.push_back(op); + set_expiration(db, trx); + PUSH_TX( db, trx, ~0 ); + trx.operations.clear(); + } + generate_block(); + BOOST_CHECK(!dividend_data.options.payout_interval); + advance_to_next_payout_time(); + BOOST_REQUIRE_MESSAGE(!dividend_data.options.next_payout_time, "A new payout was scheduled, but none should have been"); + + } catch(fc::exception& e) { + edump((e.to_detail_string())); + throw; + } +} + BOOST_AUTO_TEST_CASE( cancel_limit_order_test ) { try { INVOKE( issue_uia );