// Copyright (c) 2014-2025 Zano Project
// Copyright (c) 2014-2018 The Louisdor Project
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include "chaingen.h"
#include "multiassets_test.h"
#include "wallet_test_core_proxy.h"

#include "random_helper.h"
#include "wallet/wallet_debug_events_definitions.h"
#include "wallet/wallet_rpc_server.h"
#include "wallet/wallet_public_structs_defs.h"
using namespace currency;

//------------------------------------------------------------------------------

#define  AMOUNT_TO_TRANSFER_MULTIASSETS_BASIC (TESTS_DEFAULT_FEE)
#define  AMOUNT_ASSETS_TO_TRANSFER_MULTIASSETS_BASIC  500000000000000000

uint64_t multiassets_basic_test::ts_starter = 0;
multiassets_basic_test::multiassets_basic_test()
{
  // TODO: remove the following line
  //LOG_PRINT_MAGENTA("STARTER TS: " << ts_starter, LOG_LEVEL_0);
  //random_state_test_restorer::reset_random(ts_starter);

  REGISTER_CALLBACK_METHOD(multiassets_basic_test, c1);
}

bool multiassets_basic_test::generate(std::vector<test_event_entry>& events) const
{
  uint64_t ts = test_core_time::get_time();
  m_accounts.resize(ALICE_ACC_IDX+1);
  account_base& miner_acc = m_accounts[MINER_ACC_IDX]; miner_acc.generate(); miner_acc.set_createtime(ts);
  account_base& alice_acc = m_accounts[ALICE_ACC_IDX]; alice_acc.generate(); alice_acc.set_createtime(ts);
  miner_acc.generate();

  MAKE_GENESIS_BLOCK(events, blk_0, miner_acc, ts);
  DO_CALLBACK(events, "configure_core"); // default configure_core callback will initialize core runtime config with m_hardforks
  //TODO: Need to make sure REWIND_BLOCKS_N and other coretests codebase are capable of following hardfork4 rules
  //in this test hardfork4 moment moved to runtime section
  //REWIND_BLOCKS_N_WITH_TIME(events, blk_2_inital, blk_0, alice_acc, 2);
  REWIND_BLOCKS_N_WITH_TIME(events, blk_0r, blk_0, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW + 3);

  DO_CALLBACK(events, "c1");

  return true;
}

bool multiassets_basic_test::c1(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events)
{
  bool r = false;
  std::shared_ptr<debug_wallet2> miner_wlt = init_playtime_test_wallet_t<debug_wallet2>(events, c, MINER_ACC_IDX);
  miner_wlt->get_account().set_createtime(0);
  miner_wlt->refresh();

  std::shared_ptr<tools::wallet2> alice_wlt = init_playtime_test_wallet(events, c, ALICE_ACC_IDX);
  alice_wlt->get_account().set_createtime(0);

  asset_descriptor_base adb = AUTO_VAL_INIT(adb);
  adb.total_max_supply = 10000000000000000000ULL; //1M coins
  adb.full_name = "Test coins";
  adb.ticker = "TCT";
  adb.decimal_point = 12;

  std::vector<currency::tx_destination_entry> destinations(2);
  destinations[0].addr.push_back(miner_wlt->get_account().get_public_address());
  destinations[0].amount = AMOUNT_ASSETS_TO_TRANSFER_MULTIASSETS_BASIC;
  destinations[0].asset_id = currency::null_pkey;
  destinations[1].addr.push_back(alice_wlt->get_account().get_public_address());
  destinations[1].amount = AMOUNT_ASSETS_TO_TRANSFER_MULTIASSETS_BASIC;
  destinations[1].asset_id = currency::null_pkey;
  
  LOG_PRINT_MAGENTA("destinations[0].asset_id:" << destinations[0].asset_id, LOG_LEVEL_0);
  LOG_PRINT_MAGENTA("destinations[1].asset_id:" << destinations[1].asset_id, LOG_LEVEL_0);
  LOG_PRINT_MAGENTA("currency::null_pkey:     " << currency::null_pkey, LOG_LEVEL_0);

  currency::transaction tx = AUTO_VAL_INIT(tx);
  crypto::public_key asset_id = currency::null_pkey;
  miner_wlt->deploy_new_asset(adb, destinations, tx, asset_id);
  LOG_PRINT_L0("Deployed new asset: " << asset_id << ", tx_id: " << currency::get_transaction_hash(tx));

  //pass over hardfork
  r = mine_next_pow_blocks_in_playtime(miner_wlt->get_account().get_public_address(), c, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_blocks_in_playtime failed");

  miner_wlt->refresh();
  alice_wlt->refresh();
  CHECK_AND_ASSERT_MES(miner_wlt->balance(asset_id) == AMOUNT_ASSETS_TO_TRANSFER_MULTIASSETS_BASIC, false, "Failed to find needed asset in result balances");
  CHECK_AND_ASSERT_MES(miner_wlt->balance() == uint64_t(17517225990000000000ULL), false, "Failed to find needed asset in result balances");

  CHECK_AND_ASSERT_MES(alice_wlt->balance() == 0, false, "Failed to find needed asset in result balances");
  CHECK_AND_ASSERT_MES(alice_wlt->balance(asset_id) == AMOUNT_ASSETS_TO_TRANSFER_MULTIASSETS_BASIC, false, "Failed to find needed asset in result balances");
  
  miner_wlt->transfer(AMOUNT_ASSETS_TO_TRANSFER_MULTIASSETS_BASIC/2, alice_wlt->get_account().get_public_address(), asset_id);
  //pass over hardfork
  r = mine_next_pow_blocks_in_playtime(miner_wlt->get_account().get_public_address(), c, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_blocks_in_playtime failed");

  alice_wlt->refresh();
  uint64_t last_alice_balances = alice_wlt->balance(asset_id);
  CHECK_AND_ASSERT_MES(last_alice_balances == AMOUNT_ASSETS_TO_TRANSFER_MULTIASSETS_BASIC + AMOUNT_ASSETS_TO_TRANSFER_MULTIASSETS_BASIC/2, false, "Failed to find needed asset in result balances");

  {
    try {

      miner_wlt->transfer(AMOUNT_ASSETS_TO_TRANSFER_MULTIASSETS_BASIC / 2, alice_wlt->get_account().get_public_address(), asset_id);
      //pass over hardfork
      CHECK_AND_ASSERT_MES(false, false, "Transfer with 0 Zano worked(fail)");
    }
    catch (...)
    {
      LOG_PRINT_L0("Transfer failed as planned");
      //return true;
    }
  }

  miner_wlt->refresh();
  uint64_t last_miner_balance = miner_wlt->balance(asset_id);


  asset_descriptor_base asset_info = AUTO_VAL_INIT(asset_info);
  /*
    adb.total_max_supply = 1000000000000000000; //1M coins
    adb.full_name = "Test coins";
    adb.ticker = "TCT";
    adb.decimal_point = 12 
  */
  r = c.get_blockchain_storage().get_asset_info(asset_id, asset_info);
  CHECK_AND_ASSERT_MES(r, false, "Failed to get_asset_info");

  CHECK_AND_ASSERT_MES(asset_info.current_supply == AMOUNT_ASSETS_TO_TRANSFER_MULTIASSETS_BASIC*2, false, "Failed to find needed asset in result balances");

  //test update function
  asset_info.meta_info = "{\"some\": \"info\"}";
  miner_wlt->update_asset(asset_id, asset_info, tx);
  r = mine_next_pow_blocks_in_playtime(alice_wlt->get_account().get_public_address(), c, 2);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_blocks_in_playtime failed");

  asset_descriptor_base asset_info2 = AUTO_VAL_INIT(asset_info2);
  r = c.get_blockchain_storage().get_asset_info(asset_id, asset_info2);
  CHECK_AND_ASSERT_MES(r, false, "Failed to get_asset_info");

  CHECK_AND_ASSERT_MES(asset_info2.meta_info == asset_info.meta_info, false, "Failed to find needed asset in result balances");

  //test emit function
  //use same destinations as we used before
  miner_wlt->emit_asset(asset_id, destinations, tx);
  r = mine_next_pow_blocks_in_playtime(miner_wlt->get_account().get_public_address(), c, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_blocks_in_playtime failed");

  miner_wlt->refresh();
  alice_wlt->refresh();
  CHECK_AND_ASSERT_MES(miner_wlt->balance(asset_id) == last_miner_balance + destinations[0].amount, false, "Miner balance wrong");
  CHECK_AND_ASSERT_MES(alice_wlt->balance(asset_id) == last_alice_balances + destinations[1].amount, false, "Alice balance wrong");

  asset_descriptor_base asset_info3 = AUTO_VAL_INIT(asset_info3);
  r = c.get_blockchain_storage().get_asset_info(asset_id, asset_info3);
  CHECK_AND_ASSERT_MES(r, false, "Failed to get_asset_info");
  CHECK_AND_ASSERT_MES(asset_info3.current_supply == asset_info2.current_supply + destinations[1].amount + destinations[0].amount, false, "Failed to find needed asset in result balances");



  miner_wlt->burn_asset(asset_id, last_miner_balance, tx);
  r = mine_next_pow_blocks_in_playtime(miner_wlt->get_account().get_public_address(), c, 1);

  miner_wlt->refresh();
  CHECK_AND_ASSERT_MES(miner_wlt->balance(asset_id) == destinations[0].amount, false, "Miner balance wrong");

  asset_descriptor_base asset_info4 = AUTO_VAL_INIT(asset_info4);
  r = c.get_blockchain_storage().get_asset_info(asset_id, asset_info4);
  CHECK_AND_ASSERT_MES(r, false, "Failed to get_asset_info");
  CHECK_AND_ASSERT_MES(asset_info4.current_supply == asset_info3.current_supply - last_miner_balance, false, "Failed to find needed asset in result balances");


  //------------------- tests that trying to break stuff  -------------------
  //tests that trying to break stuff
  miner_wlt->get_debug_events_dispatcher().SUBSCIRBE_DEBUG_EVENT<wde_construct_tx_after_asset_ownership_proof_generated>([&](const wde_construct_tx_after_asset_ownership_proof_generated& o)
  {
    //crypto::signature s = currency::null_sig;
    o.pownership_proof->gss = crypto::generic_schnorr_sig_s{};
  });


  //test update function with broken ownership
  r = c.get_blockchain_storage().get_asset_info(asset_id, asset_info);
  CHECK_AND_ASSERT_MES(r, false, "Failed to get_asset_info");

  asset_info.meta_info = "{\"some2\": \"info2\"}";
  r = false;
  try
  {
    miner_wlt->update_asset(asset_id, asset_info, tx);
  }
  catch(tools::error::tx_rejected&)
  {
    r = true;
  }
  CHECK_AND_ASSERT_MES(r, false, "Test failed, broken ownership passed");
  
  c.get_tx_pool().unsecure_disable_tx_validation_on_addition(true);
  miner_wlt->update_asset(asset_id, asset_info, tx);
  c.get_tx_pool().unsecure_disable_tx_validation_on_addition(false);
  r = mine_next_pow_blocks_in_playtime(miner_wlt->get_account().get_public_address(), c, 2);
  CHECK_AND_ASSERT_MES(!r, false, "Test failed, broken ownership passed");
  c.get_tx_pool().purge_transactions();
  miner_wlt->refresh();

  miner_wlt->get_debug_events_dispatcher().UNSUBSCRIBE_ALL();

  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  //------------------- tests that trying to break stuff  -------------------
  // check update_asset() with modified 'ticker'
  r = c.get_blockchain_storage().get_asset_info(asset_id, asset_info);
  CHECK_AND_ASSERT_MES(r, false, "Failed to get_asset_info");

  asset_info.ticker = "XXX";
  r = false;
  try
  {
    miner_wlt->update_asset(asset_id, asset_info, tx);
  }
  catch(tools::error::tx_rejected&)
  {
    r = true;
  }
  CHECK_AND_ASSERT_MES(r, false, "update_asset succeeded, but this shouldn't happened");

  c.get_tx_pool().unsecure_disable_tx_validation_on_addition(true);
  miner_wlt->update_asset(asset_id, asset_info, tx);
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());
  r = mine_next_pow_block_in_playtime(miner_wlt->get_account().get_public_address(), c);
  CHECK_AND_ASSERT_MES(!r, false, "block with a bad tx was unexpectedly mined");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count()); // make sure tx was not confirmed
  c.get_tx_pool().unsecure_disable_tx_validation_on_addition(false);
  c.get_tx_pool().purge_transactions();

  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count()); // make sure tx was not added

  
  // check update_asset() with modified 'full_name'
  r = c.get_blockchain_storage().get_asset_info(asset_id, asset_info);
  CHECK_AND_ASSERT_MES(r, false, "Failed to get_asset_info");

  asset_info.full_name = "XXX";
  r = false;
  try
  {
    miner_wlt->update_asset(asset_id, asset_info, tx);
  }
  catch(tools::error::tx_rejected&)
  {
    r = true;
  }
  CHECK_AND_ASSERT_MES(r, false, "update_asset succeeded, but this shouldn't happened");

  c.get_tx_pool().unsecure_disable_tx_validation_on_addition(true);
  miner_wlt->update_asset(asset_id, asset_info, tx);
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());
  r = mine_next_pow_block_in_playtime(miner_wlt->get_account().get_public_address(), c);
  CHECK_AND_ASSERT_MES(!r, false, "block with a bad tx was unexpectedly mined");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count()); // make sure tx was not confirmed
  c.get_tx_pool().unsecure_disable_tx_validation_on_addition(false);
  c.get_tx_pool().purge_transactions();
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count()); // make sure tx was not added
  miner_wlt->refresh();

  
  // check update_asset() with modified 'decimal_point'
  r = c.get_blockchain_storage().get_asset_info(asset_id, asset_info);
  CHECK_AND_ASSERT_MES(r, false, "Failed to get_asset_info");

  asset_info.decimal_point = 3;
  r = false;
  try
  {
    miner_wlt->update_asset(asset_id, asset_info, tx);
  }
  catch(tools::error::tx_rejected&)
  {
    r = true;
  }
  CHECK_AND_ASSERT_MES(r, false, "update_asset succeeded, but this shouldn't happened");

  c.get_tx_pool().unsecure_disable_tx_validation_on_addition(true);
  miner_wlt->update_asset(asset_id, asset_info, tx);
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());
  r = mine_next_pow_block_in_playtime(miner_wlt->get_account().get_public_address(), c);
  CHECK_AND_ASSERT_MES(!r, false, "block with a bad tx was unexpectedly mined");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count()); // make sure tx was not confirmed
  c.get_tx_pool().unsecure_disable_tx_validation_on_addition(false);
  c.get_tx_pool().purge_transactions();
  miner_wlt->refresh();
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count()); // make sure tx was not added


  // check update_asset() with modified 'owner'
  //r = c.get_blockchain_storage().get_asset_info(asset_id, asset_info);
  //CHECK_AND_ASSERT_MES(r, false, "Failed to get_asset_info");

  //asset_info.owner = currency::keypair::generate().pub;
  //miner_wlt->update_asset(asset_id, asset_info, tx);
  //CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());
  //r = mine_next_pow_block_in_playtime(miner_wlt->get_account().get_public_address(), c);
  //CHECK_AND_ASSERT_MES(!r, false, "block with a bad tx was unexpectedly mined");
  //CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count()); // make sure tx was not confirmed
  //c.get_tx_pool().purge_transactions();
  //miner_wlt->refresh();



  // check emit_asset() with modified 'current_supply'
  miner_wlt->get_debug_events_dispatcher().SUBSCIRBE_DEBUG_EVENT<wde_construct_tx_handle_asset_descriptor_operation_before_seal>([&](const wde_construct_tx_handle_asset_descriptor_operation_before_seal& o)
  {
    if(o.pado->version < ASSET_DESCRIPTOR_OPERATION_HF5_VER)
    {
      //hf4
      o.pado->opt_descriptor->current_supply += 1000000;
    }
    else
    {
      *o.pado->opt_amount += 1000000;
    }
  });
  //test emit function but re-adjust current_supply to wrong amount
  r = false;
  try
  {
    miner_wlt->emit_asset(asset_id, destinations, tx);
  }
  catch(tools::error::tx_rejected&)
  {
    r = true;
  }
  CHECK_AND_ASSERT_MES(r, false, "emit_asset succeeded, but this shouldn't happened");

  c.get_tx_pool().unsecure_disable_tx_validation_on_addition(true);
  miner_wlt->emit_asset(asset_id, destinations, tx);
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());
  r = mine_next_pow_block_in_playtime(miner_wlt->get_account().get_public_address(), c);
  CHECK_AND_ASSERT_MES(!r, false, "block with a bad tx was unexpectedly mined");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count()); // make sure tx was not confirmed
  c.get_tx_pool().unsecure_disable_tx_validation_on_addition(false);
  c.get_tx_pool().purge_transactions();
  miner_wlt->refresh();

  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count()); // make sure tx was not added

  //------------------- tests that trying to break stuff  -------------------
  //test burn that burns more than tx has
  miner_wlt->get_debug_events_dispatcher().UNSUBSCRIBE_ALL();

  miner_wlt->get_debug_events_dispatcher().SUBSCIRBE_DEBUG_EVENT<wde_construct_tx_handle_asset_descriptor_operation_before_burn>([&](const wde_construct_tx_handle_asset_descriptor_operation_before_burn& o)
  {
    if (o.pado->version < ASSET_DESCRIPTOR_OPERATION_HF5_VER)
    {
      //hf4
      o.pado->opt_descriptor->current_supply -= 1000000;
    }
    else
    {
      *o.pado->opt_amount -= 1000000;
    }
  });


  r = false;
  try
  {
    miner_wlt->burn_asset(asset_id, 10000000000000, tx);
  }
  catch(tools::error::tx_rejected&)
  {
    r = true;
  }
  CHECK_AND_ASSERT_MES(r, false, "burn_asset succeeded, but this shouldn't happened");

  c.get_tx_pool().unsecure_disable_tx_validation_on_addition(true);
  miner_wlt->burn_asset(asset_id, 10000000000000, tx);
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());
  r = mine_next_pow_block_in_playtime(miner_wlt->get_account().get_public_address(), c);
  CHECK_AND_ASSERT_MES(!r, false, "block with a bad tx was unexpectedly mined");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count()); // make sure tx was not confirmed
  c.get_tx_pool().unsecure_disable_tx_validation_on_addition(false);
  c.get_tx_pool().purge_transactions();
  miner_wlt->refresh();
  miner_wlt->get_debug_events_dispatcher().UNSUBSCRIBE_ALL();

  //
  miner_wlt->transfer_asset_ownership(asset_id, alice_wlt->get_account().get_public_address().spend_public_key, tx);
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());
  r = mine_next_pow_block_in_playtime(miner_wlt->get_account().get_public_address(), c);
  CHECK_AND_ASSERT_MES(r, false, "block with a bad tx was unexpectedly mined");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count()); // make sure tx was not confirmed

  miner_wlt->refresh();
  alice_wlt->refresh();

  auto miner_own_assets = miner_wlt->get_own_assets();
  auto alice_own_assets = alice_wlt->get_own_assets();
  CHECK_AND_ASSERT_MES(miner_own_assets.size() == 0, false, "Miner wlt still think he own asset");
  CHECK_AND_ASSERT_MES(alice_own_assets.size() == 1, false, "Alice still don't know she own asset");

  //uint64_t balance_alice_native = alice_wlt->balance();
  //uint64_t balance_miner_native = miner_wlt->balance();
  uint64_t balance_alice_asset = alice_wlt->balance(asset_id);
  uint64_t balance_miner_asset = miner_wlt->balance(asset_id);

  alice_wlt->emit_asset(asset_id, destinations, tx);
  r = mine_next_pow_blocks_in_playtime(miner_wlt->get_account().get_public_address(), c, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_blocks_in_playtime failed");

  miner_wlt->refresh();
  alice_wlt->refresh();

  CHECK_AND_ASSERT_MES(miner_wlt->balance(asset_id) == balance_miner_asset + destinations[0].amount, false, "Miner balance wrong");
  CHECK_AND_ASSERT_MES(alice_wlt->balance(asset_id) == balance_alice_asset + destinations[1].amount, false, "Alice balance wrong");

  //TODO: attempt to emmmit from old key, attempt to emit from more then max supply

  return true;
}

//------------------------------------------------------------------------------
//@#@  TODO: subject for refactoring: this fill_ado*/fill_adb* are copy/paste clones of wallet's, need to be implemented in one place at some point
//----------------------------------------------------------------------------------------------------
void fill_ado_version_based_onhardfork(currency::asset_descriptor_operation& asset_reg_info, size_t current_latest_hf)
{
  if (current_latest_hf < ZANO_HARDFORK_05)
  {
    asset_reg_info.version = ASSET_DESCRIPTOR_OPERATION_HF4_VER;
  }
  else
  {
    asset_reg_info.version = ASSET_DESCRIPTOR_OPERATION_LAST_VER;
  }
}

void fill_adb_version_based_onhardfork(currency::asset_descriptor_base& asset_base, size_t current_latest_hf)
{
  if (current_latest_hf < ZANO_HARDFORK_05)
  {
    asset_base.version = ASSET_DESCRIPTOR_BASE_HF4_VER;
  }
  else
  {
    asset_base.version = ASSET_DESCRIPTOR_BASE_LAST_VER;
  }
}

//----------------------------------------------------------------------------------------------------


assets_and_explicit_native_coins_in_outs::assets_and_explicit_native_coins_in_outs()
{
  REGISTER_CALLBACK_METHOD(assets_and_explicit_native_coins_in_outs, c1_alice_cannot_deploy_asset);
  REGISTER_CALLBACK_METHOD(assets_and_explicit_native_coins_in_outs, c2_alice_deploys_asset);

  m_hardforks.clear();
  m_hardforks.set_hardfork_height(ZANO_HARDFORK_04_ZARCANUM, CURRENCY_MINED_MONEY_UNLOCK_WINDOW + 1);
}

bool assets_and_explicit_native_coins_in_outs::generate(std::vector<test_event_entry>& events) const
{
  /* Test idea:
  *    1) make sure an asset cannot be deployed if there's no ZC outputs available;
  *    2) make sure an asset emission transaction has hidden asset ids in all outputs;
  *    3) (NOT DONE YET) make sure tx with at least one input with at least one reference to non-explicit native asset id has non-explicit asset ids in outs (TODO: move to separate test)
  *    4) Bob get coins with non-explicit asset id and then tries to register an alias with them (some will be burnt with explicit asset id)
  */

  bool r = false;

  uint64_t ts = test_core_time::get_time();
  m_accounts.resize(TOTAL_ACCS_COUNT);
  account_base& miner_acc = m_accounts[MINER_ACC_IDX]; miner_acc.generate(); miner_acc.set_createtime(ts);
  account_base& alice_acc = m_accounts[ALICE_ACC_IDX]; alice_acc.generate(); alice_acc.set_createtime(ts);
  account_base& bob_acc   = m_accounts[BOB_ACC_IDX];   bob_acc.generate();   bob_acc.set_createtime(ts);
  MAKE_GENESIS_BLOCK(events, blk_0, miner_acc, ts);
  DO_CALLBACK(events, "configure_core"); // necessary to set m_hardforks

  // HF4 requires tests_random_split_strategy (for 2 outputs minimum)
  test_gentime_settings tgts = generator.get_test_gentime_settings();
  tgts.split_strategy = tests_random_split_strategy;
  generator.set_test_gentime_settings(tgts);

  REWIND_BLOCKS_N_WITH_TIME(events, blk_0r, blk_0, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);

  DO_CALLBACK_PARAMS(events, "check_hardfork_inactive", static_cast<size_t>(ZANO_HARDFORK_04_ZARCANUM));

  // tx_0: miner -> Alice
  // make tx_0 before HF4, so Alice will have only bare outs
  m_alice_initial_balance = MK_TEST_COINS(1000) + TESTS_DEFAULT_FEE;
  transaction tx_0{};
  std::vector<tx_source_entry> sources;
  std::vector<tx_destination_entry> destinations;
  r = fill_tx_sources_and_destinations(events, blk_0r, miner_acc, alice_acc, m_alice_initial_balance, TESTS_DEFAULT_FEE, 0, sources, destinations, true /* spends */, false /* unlock time */);
  CHECK_AND_ASSERT_MES(r, false, "fill_tx_sources_and_destinations failed");
  r = construct_tx(miner_acc.get_keys(), sources, destinations, events, this, tx_0);
  CHECK_AND_ASSERT_MES(r, false, "construct_tx failed");
  
  ADD_CUSTOM_EVENT(events, tx_0);
  MAKE_NEXT_BLOCK_TX1(events, blk_1, blk_0r, miner_acc, tx_0);

  // make sure HF4 has been activated
  DO_CALLBACK_PARAMS(events, "check_hardfork_active", static_cast<size_t>(ZANO_HARDFORK_04_ZARCANUM));

  // rewind blocks
  REWIND_BLOCKS_N_WITH_TIME(events, blk_1r, blk_1, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);

  // check Alice's balance and make sure she cannot deploy an asset
  DO_CALLBACK(events, "c1_alice_cannot_deploy_asset");

  // tx_1: Alice -> Alice (all coins) : this will convert two Alice's outputs to ZC outs (since we're at post-HF4 zone)
  MAKE_TX(events, tx_1, alice_acc, alice_acc, m_alice_initial_balance - TESTS_DEFAULT_FEE, blk_1r);
  CHECK_AND_ASSERT_MES(tx_1.vout.size() == 2, false, "unexpected tx_1.vout.size : " << tx_1.vout.size());

  // make sure that all tx_1 outputs have explicit native coin asset id
  for(auto& out : tx_1.vout)
  {
    CHECK_AND_ASSERT_MES(out.type() == typeid(tx_out_zarcanum), false, "invalid out type");
    const tx_out_zarcanum& out_zc = boost::get<tx_out_zarcanum>(out);
    CHECK_AND_ASSERT_MES(out_zc.blinded_asset_id == native_coin_asset_id_1div8, false, "tx_1 has non-explicit asset id in outputs");
  }

  MAKE_NEXT_BLOCK_TX1(events, blk_2, blk_1r, miner_acc, tx_1);

  // rewind blocks
  REWIND_BLOCKS_N_WITH_TIME(events, blk_2r, blk_2, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);

  // check Alice's balance and make sure she CAN deploy an asset now
  DO_CALLBACK(events, "c2_alice_deploys_asset");

  return true;
}

bool assets_and_explicit_native_coins_in_outs::c1_alice_cannot_deploy_asset(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events)
{
  std::shared_ptr<tools::wallet2> alice_wlt = init_playtime_test_wallet(events, c, m_accounts[ALICE_ACC_IDX]);
  alice_wlt->refresh();

  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", m_alice_initial_balance, 0, m_alice_initial_balance, 0, 0), false, "");

  asset_descriptor_base adb{};
  adb.total_max_supply = 10;
  adb.full_name = "it doesn't matter";
  adb.ticker = "really";
  adb.decimal_point = 12;

  std::vector<tx_destination_entry> destinations;
  destinations.emplace_back(adb.total_max_supply, m_accounts[MINER_ACC_IDX].get_public_address(), null_pkey);

  transaction asset_emission_tx{};
  crypto::public_key asset_id = null_pkey;
  bool r = false;
  try
  {
    alice_wlt->deploy_new_asset(adb, destinations, asset_emission_tx, asset_id);
  }
  catch(...)
  {
    r = true;
  }

  CHECK_AND_ASSERT_MES(r, false, "Alice successfully deployed an asset, which is unexpected (she has no ZC outs available)");

  return true;
}

bool assets_and_explicit_native_coins_in_outs::c2_alice_deploys_asset(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events)
{
  bool r = false;
  std::shared_ptr<tools::wallet2> alice_wlt = init_playtime_test_wallet(events, c, m_accounts[ALICE_ACC_IDX]);
  alice_wlt->refresh();

  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", m_alice_initial_balance - TESTS_DEFAULT_FEE, 0, m_alice_initial_balance - TESTS_DEFAULT_FEE, 0, 0), false, "");

  // make sure Alice has two UTXO now
  tools::transfer_container transfers{};
  alice_wlt->get_transfers(transfers);
  size_t unspent_transfers = std::count_if(transfers.begin(), transfers.end(), [](const auto& tr){ return !tr.second.is_spent(); });
  CHECK_AND_ASSERT_MES(unspent_transfers == 2, false, "unexpected number of Alice's unspent transfers: " << unspent_transfers);

  asset_descriptor_base adb{};
  adb.total_max_supply = 100 * 1000000000000;
  adb.full_name = "very confidential asset";
  adb.ticker = "VCA";
  adb.decimal_point = 12;

  std::vector<tx_destination_entry> destinations;
  destinations.emplace_back(adb.total_max_supply / 2, m_accounts[MINER_ACC_IDX].get_public_address(), null_pkey);
  destinations.emplace_back(adb.total_max_supply / 2, m_accounts[MINER_ACC_IDX].get_public_address(), null_pkey);

  transaction asset_emission_tx{};
  crypto::public_key asset_id = null_pkey;

  alice_wlt->deploy_new_asset(adb, destinations, asset_emission_tx, asset_id);


  // make sure the emission tx is correct
  CHECK_AND_ASSERT_MES(asset_emission_tx.vout.size() > 2, false, "Unexpected vout size: " << asset_emission_tx.vout.size());
  for(auto& out : asset_emission_tx.vout)
  {
    CHECK_AND_ASSERT_MES(out.type() == typeid(tx_out_zarcanum), false, "invalid out type");
    const tx_out_zarcanum& out_zc = boost::get<tx_out_zarcanum>(out);
    // as soon as this is the asset emmiting transaction, no output has an obvious asset id
    // make sure it is so
    CHECK_AND_ASSERT_MES(out_zc.blinded_asset_id != native_coin_asset_id_1div8, false, "One of outputs has explicit native asset id, which is unexpected");
  }

  // get this tx confirmed
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  r = mine_next_pow_blocks_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c, CURRENCY_MINED_MONEY_UNLOCK_WINDOW + 1);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_block_in_playtime failed");

  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  // check Alice balance, make sure all native coins are unlocked
  alice_wlt->refresh();
  uint64_t alice_balance = m_alice_initial_balance - TESTS_DEFAULT_FEE * 2;
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", alice_balance, 0, alice_balance, 0, 0), false, "");

  // now Alice only has UTXO with non explicit asset id
  // Transfer all of them to Bob and check asset ids of outputs
  transaction tx_2{};
  alice_wlt->transfer(alice_balance - TESTS_DEFAULT_FEE, m_accounts[BOB_ACC_IDX].get_public_address(), tx_2, native_coin_asset_id);

  CHECK_AND_ASSERT_MES(tx_2.vout.size() == 2, false, "unexpected tx_2.vout.size : " << tx_2.vout.size());
  for(auto& out : tx_2.vout)
  {
    CHECK_AND_ASSERT_MES(out.type() == typeid(tx_out_zarcanum), false, "invalid out type");
    const tx_out_zarcanum& out_zc = boost::get<tx_out_zarcanum>(out);
    CHECK_AND_ASSERT_MES(out_zc.blinded_asset_id != native_coin_asset_id_1div8, false, "One of outputs has explicit native asset id, which is unexpected");
  }

  // finally, get this tx confirmed
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  r = mine_next_pow_blocks_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_block_in_playtime failed");

  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  std::shared_ptr<tools::wallet2> bob_wlt = init_playtime_test_wallet(events, c, m_accounts[BOB_ACC_IDX]);
  bob_wlt->refresh();
  uint64_t bob_balance = alice_balance - TESTS_DEFAULT_FEE;
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*bob_wlt, "Bob", bob_balance, 0, bob_balance, 0, 0), false, "");

  extra_alias_entry ae{};
  ae.m_alias = "kris.kelvin";
  ae.m_address = m_accounts[BOB_ACC_IDX].get_public_address();
  transaction tx_3{};
  bob_wlt->request_alias_registration(ae, tx_3, TESTS_DEFAULT_FEE);

  // finally, get this tx confirmed
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());
  r = mine_next_pow_block_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_block_in_playtime failed");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  return true;
}

//------------------------------------------------------------------------------

asset_depoyment_and_few_zc_utxos::asset_depoyment_and_few_zc_utxos()
{
  REGISTER_CALLBACK_METHOD(asset_depoyment_and_few_zc_utxos, c1);

  m_hardforks.clear();
  m_hardforks.set_hardfork_height(ZANO_HARDFORK_04_ZARCANUM, CURRENCY_MINED_MONEY_UNLOCK_WINDOW + 1);
}

bool asset_depoyment_and_few_zc_utxos::generate(std::vector<test_event_entry>& events) const
{
  bool r = false;

  uint64_t ts = test_core_time::get_time();
  m_accounts.resize(TOTAL_ACCS_COUNT);
  account_base& miner_acc = m_accounts[MINER_ACC_IDX]; miner_acc.generate(); miner_acc.set_createtime(ts);
  account_base& alice_acc = m_accounts[ALICE_ACC_IDX]; alice_acc.generate(); alice_acc.set_createtime(ts);
  MAKE_GENESIS_BLOCK(events, blk_0, miner_acc, ts);
  DO_CALLBACK(events, "configure_core"); // necessary to set m_hardforks

  // HF4 requires tests_random_split_strategy (for 2 outputs minimum)
  test_gentime_settings tgts = generator.get_test_gentime_settings();
  tgts.split_strategy = tests_random_split_strategy;
  generator.set_test_gentime_settings(tgts);

  REWIND_BLOCKS_N_WITH_TIME(events, blk_0r, blk_0, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);

  DO_CALLBACK_PARAMS(events, "check_hardfork_inactive", static_cast<size_t>(ZANO_HARDFORK_04_ZARCANUM));

  // tx_0: miner -> Alice
  // make tx_0 before HF4, so Alice will have only bare outs
  transaction tx_0{};
  std::vector<tx_source_entry> sources;
  std::vector<tx_destination_entry> destinations;
  for(size_t i = 0; i < 100; ++i)
    destinations.emplace_back(TESTS_DEFAULT_FEE, m_accounts[ALICE_ACC_IDX].get_public_address());
  m_alice_initial_balance = TESTS_DEFAULT_FEE * 100;
  r = fill_tx_sources(sources, events, blk_0r, miner_acc.get_keys(), m_alice_initial_balance + TESTS_DEFAULT_FEE, 0);
  CHECK_AND_ASSERT_MES(r, false, "fill_tx_sources failed");
  r = construct_tx(miner_acc.get_keys(), sources, destinations, events, this, tx_0);
  CHECK_AND_ASSERT_MES(r, false, "construct_tx failed");

  ADD_CUSTOM_EVENT(events, tx_0);
  MAKE_NEXT_BLOCK_TX1(events, blk_1, blk_0r, miner_acc, tx_0);

  // make sure HF4 has been activated
  DO_CALLBACK_PARAMS(events, "check_hardfork_active", static_cast<size_t>(ZANO_HARDFORK_04_ZARCANUM));

  // tx_1: miner -> Alice
  // send less than min fee. This output will be the only ZC UTXO in Alice's wallet
  //destinations.clear();

  MAKE_TX(events, tx_1, miner_acc, alice_acc, TESTS_DEFAULT_FEE * 0.5, blk_1);
  m_alice_initial_balance += TESTS_DEFAULT_FEE * 0.5;
  MAKE_NEXT_BLOCK_TX1(events, blk_2, blk_1, miner_acc, tx_1);

  // rewind blocks
  REWIND_BLOCKS_N_WITH_TIME(events, blk_2r, blk_2, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);

  // check Alice's balance and make sure she can deploy an asset
  DO_CALLBACK(events, "c1");

  return true;
}

bool asset_depoyment_and_few_zc_utxos::c1(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events)
{
  bool r = false;
  std::shared_ptr<tools::wallet2> alice_wlt = init_playtime_test_wallet(events, c, m_accounts[ALICE_ACC_IDX]);
  alice_wlt->refresh();

  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", m_alice_initial_balance, 0, m_alice_initial_balance, 0, 0), false, "");

  // make sure Alice has correct UTXO wallet structure
  tools::transfer_container transfers;
  alice_wlt->get_transfers(transfers);
  size_t zc_unspent_outs = 0, unspent_outs = 0;
  for(auto& td : transfers)
  {
    if (!td.second.is_spent())
    {
      ++unspent_outs;
      if (td.second.is_zc())
        ++zc_unspent_outs;
    }
  }
  CHECK_AND_ASSERT_MES(unspent_outs == 101 && zc_unspent_outs == 1, false, "incorrect UTXO structure: " << unspent_outs << ", " << zc_unspent_outs);

  asset_descriptor_base adb{};
  adb.total_max_supply = 100 * 1000000000000;
  adb.full_name = "very confidential asset";
  adb.ticker = "VCA";
  adb.decimal_point = 12;

  std::vector<tx_destination_entry> destinations;
  destinations.emplace_back(adb.total_max_supply, m_accounts[MINER_ACC_IDX].get_public_address(), null_pkey);
  destinations.emplace_back(adb.total_max_supply / 2, m_accounts[MINER_ACC_IDX].get_public_address(), null_pkey);

  transaction asset_emission_tx{};
  crypto::public_key asset_id = null_pkey;

  alice_wlt->deploy_new_asset(adb, destinations, asset_emission_tx, asset_id);

  // make sure the emission tx is correct
  CHECK_AND_ASSERT_MES(asset_emission_tx.vout.size() > 2, false, "Unexpected vout size: " << asset_emission_tx.vout.size());
  for(auto& out : asset_emission_tx.vout)
  {
    CHECK_AND_ASSERT_MES(out.type() == typeid(tx_out_zarcanum), false, "invalid out type");
    const tx_out_zarcanum& out_zc = boost::get<tx_out_zarcanum>(out);
    // as soon as this is the asset emmiting transaction, no output has an obvious asset id
    // make sure it is so
    CHECK_AND_ASSERT_MES(out_zc.blinded_asset_id != native_coin_asset_id_1div8, false, "One of outputs has explicit native asset id, which is unexpected");
  }

  // get this tx confirmed
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  r = mine_next_pow_blocks_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c, CURRENCY_MINED_MONEY_UNLOCK_WINDOW + 1);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_block_in_playtime failed");

  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  return true;
}

//------------------------------------------------------------------------------

assets_and_pos_mining::assets_and_pos_mining()
{
  REGISTER_CALLBACK_METHOD(assets_and_pos_mining, c1);
}

bool assets_and_pos_mining::generate(std::vector<test_event_entry>& events) const
{
  // Test idea: ensure that post-HF4 Zarcanum staking functions correctly with outputs that have a nonzero asset id blinding mask (i.e., outputs with a non-explicit asset id)

  uint64_t ts = test_core_time::get_time();
  m_accounts.resize(TOTAL_ACCS_COUNT);
  account_base& miner_acc = m_accounts[MINER_ACC_IDX]; miner_acc.generate(); miner_acc.set_createtime(ts);
  account_base& alice_acc = m_accounts[ALICE_ACC_IDX]; alice_acc.generate(); alice_acc.set_createtime(ts);
  MAKE_GENESIS_BLOCK(events, blk_0, miner_acc, ts);
  DO_CALLBACK(events, "configure_core"); // necessary to set m_hardforks

  REWIND_BLOCKS_N_WITH_TIME(events, blk_0r, blk_0, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW + 1);

  DO_CALLBACK_PARAMS(events, "check_hardfork_active", static_cast<size_t>(ZANO_HARDFORK_04_ZARCANUM));

  DO_CALLBACK(events, "c1");
  return true;
}

bool assets_and_pos_mining::c1(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events)
{
  bool r = false;
  std::shared_ptr<tools::wallet2> miner_wlt = init_playtime_test_wallet(events, c, MINER_ACC_IDX);
  miner_wlt->refresh();

  asset_descriptor_base adb{};
  adb.total_max_supply = 10*COIN;
  adb.full_name = "test";
  adb.ticker = "TEST";

  std::vector<currency::tx_destination_entry> destinations;
  destinations.emplace_back(adb.total_max_supply, m_accounts[ALICE_ACC_IDX].get_public_address(), null_pkey);
  destinations.emplace_back(MK_TEST_COINS(2), m_accounts[ALICE_ACC_IDX].get_public_address());
  destinations.emplace_back(MK_TEST_COINS(2), m_accounts[ALICE_ACC_IDX].get_public_address());

  currency::transaction tx{};
  crypto::public_key asset_id = currency::null_pkey;
  miner_wlt->deploy_new_asset(adb, destinations, tx, asset_id);
  LOG_PRINT_L0("Deployed new asset: " << asset_id << ", tx_id: " << currency::get_transaction_hash(tx));

  CHECK_AND_ASSERT_MES(tx.vout.size() >= 3, false, "Unexpected vout size: " << tx.vout.size());
  for(auto& out : tx.vout)
  {
    CHECK_AND_ASSERT_MES(out.type() == typeid(tx_out_zarcanum), false, "invalid out type");
    CHECK_AND_ASSERT_MES(boost::get<tx_out_zarcanum>(out).blinded_asset_id != native_coin_asset_id_1div8, false, "One of outputs has explicit native asset id, which is unexpected");
  }

  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());
  r = mine_next_pow_blocks_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_block_in_playtime failed");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  std::shared_ptr<tools::wallet2> alice_wlt = init_playtime_test_wallet(events, c, ALICE_ACC_IDX);
  alice_wlt->refresh();
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", MK_TEST_COINS(4), 0, MK_TEST_COINS(4), 0, 0), false, "");

  size_t current_blockchain_size = c.get_current_blockchain_size();

  r = alice_wlt->try_mint_pos();
  CHECK_AND_ASSERT_MES(r, false, "try_mint_pos failed");
  CHECK_AND_ASSERT_MES(c.get_current_blockchain_size() == current_blockchain_size + 1, false, "incorrect blockchain size: " << c.get_current_blockchain_size());

  r = alice_wlt->try_mint_pos();
  CHECK_AND_ASSERT_MES(r, false, "try_mint_pos failed");
  CHECK_AND_ASSERT_MES(c.get_current_blockchain_size() == current_blockchain_size + 2, false, "incorrect blockchain size: " << c.get_current_blockchain_size());

  // the following attempt should fail because Alice has only two outputs eligiable for staking
  r = alice_wlt->try_mint_pos();
  CHECK_AND_ASSERT_MES(r, false, "try_mint_pos failed");
  CHECK_AND_ASSERT_MES(c.get_current_blockchain_size() == current_blockchain_size + 2, false, "incorrect blockchain size: " << c.get_current_blockchain_size()); // the same height

  return true;
}

//------------------------------------------------------------------------------

asset_emission_and_unconfirmed_balance::asset_emission_and_unconfirmed_balance()
{
  REGISTER_CALLBACK_METHOD(asset_emission_and_unconfirmed_balance, c1);
}

bool asset_emission_and_unconfirmed_balance::generate(std::vector<test_event_entry>& events) const
{
  uint64_t ts = test_core_time::get_time();
  m_accounts.resize(TOTAL_ACCS_COUNT);
  account_base& miner_acc = m_accounts[MINER_ACC_IDX]; miner_acc.generate(); miner_acc.set_createtime(ts);
  //account_base& alice_acc = m_accounts[ALICE_ACC_IDX]; alice_acc.generate(); alice_acc.set_createtime(ts);
  miner_acc.generate();

  MAKE_GENESIS_BLOCK(events, blk_0, miner_acc, ts);
  DO_CALLBACK(events, "configure_core"); // default configure_core callback will initialize core runtime config with m_hardforks
  REWIND_BLOCKS_N_WITH_TIME(events, blk_0r, blk_0, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW + 3);

  DO_CALLBACK(events, "c1");

  return true;
}

bool asset_emission_and_unconfirmed_balance::c1(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events)
{
  bool r = false;
  std::shared_ptr<tools::wallet2> miner_wlt = init_playtime_test_wallet(events, c, MINER_ACC_IDX);
  miner_wlt->refresh();

  asset_descriptor_base adb{};
  adb.total_max_supply = UINT64_MAX;
  adb.full_name = "2 xx 64";
  adb.ticker = "2POWER64";

  std::vector<currency::tx_destination_entry> destinations;
  destinations.emplace_back(adb.total_max_supply, m_accounts[MINER_ACC_IDX].get_public_address(), null_pkey);

  currency::transaction tx{};
  crypto::public_key asset_id = currency::null_pkey;
  miner_wlt->deploy_new_asset(adb, destinations, tx, asset_id);
  LOG_PRINT_L0("Deployed new asset: " << asset_id << ", tx_id: " << currency::get_transaction_hash(tx));

  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  bool stub_bool = 0;
  miner_wlt->refresh();
  miner_wlt->scan_tx_pool(stub_bool);
  uint64_t total, unlocked, awaiting_in, awaiting_out, mined;
  balance_via_wallet(*miner_wlt, asset_id, &total, &unlocked, &awaiting_in, &awaiting_out, &mined);
  CHECK_AND_ASSERT_EQ(total,        UINT64_MAX);
  CHECK_AND_ASSERT_EQ(unlocked,     0);
  CHECK_AND_ASSERT_EQ(awaiting_in,  UINT64_MAX);
  CHECK_AND_ASSERT_EQ(awaiting_out, 0);
  //CHECK_AND_ASSERT_EQ(mined,        0);

  r = mine_next_pow_blocks_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_block_in_playtime failed");

  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  return true;
}

//------------------------------------------------------------------------------

asset_operation_and_hardfork_checks::asset_operation_and_hardfork_checks()
{
  m_adb_hello.version = ASSET_DESCRIPTOR_BASE_HF4_VER;
  m_adb_hello.total_max_supply = 1'000'000'000'000'000'000;
  m_adb_hello.current_supply = 1'000'000'000'000'000'000;
  m_adb_hello.ticker = "HLO";
  m_adb_hello.full_name = "HELLO_WORLD";
  m_adb_hello.meta_info = "Hello, world!";
  m_adb_hello.hidden_supply = false;

  m_ado_hello.operation_type = ASSET_DESCRIPTOR_OPERATION_REGISTER;
  m_ado_hello.opt_asset_id = currency::null_pkey;
  m_ado_hello.version = ASSET_DESCRIPTOR_OPERATION_HF4_VER;

  m_adb_bye.total_max_supply = 1'000'000'000'000'000'000;
  m_adb_bye.current_supply = 1'000'000'000'000'000'000;
  m_adb_bye.ticker = "BYE";
  m_adb_bye.full_name = "BYE_WORLD";
  m_adb_bye.meta_info = "Bye, world!";
  m_adb_bye.hidden_supply = false;

  m_ado_bye.operation_type = ASSET_DESCRIPTOR_OPERATION_REGISTER;
  m_ado_hello.opt_asset_id = currency::null_pkey;
  m_ado_hello.version = ASSET_DESCRIPTOR_OPERATION_HF4_VER;

  REGISTER_CALLBACK_METHOD(asset_operation_and_hardfork_checks, c1);
  REGISTER_CALLBACK_METHOD(asset_operation_and_hardfork_checks, c2);
}

bool asset_operation_and_hardfork_checks::generate(
  std::vector<test_event_entry>& events) const
{
  /*
    0           10     11           21     22
  ( 0) - ... - (0r) - ( 1) - ... - (1r) - ( 2)
                       \                   \
                        [tx_0]              [tx_1]
  */

  bool success{false};
  std::vector<tx_source_entry> sources{};
  std::vector<tx_destination_entry> destinations{};
  transaction tx_0{}, tx_1{}, tx_2{}, tx_3{}, tx_4{};
  uint64_t tx_version{};
  crypto::secret_key stub{};

  m_accounts.resize(2);
  account_base& miner{m_accounts[MINER_ACC_IDX]};
  miner.generate();
  account_base& alice{m_accounts[ALICE_ACC_IDX]};
  alice.generate();

  m_adb_hello.owner = alice.get_public_address().spend_public_key;
  m_ado_hello.opt_descriptor = m_adb_hello;

  m_adb_bye.owner = alice.get_public_address().spend_public_key;
  m_ado_bye.opt_descriptor = m_adb_bye;

  MAKE_GENESIS_BLOCK(events,
                     blk_0,
                     miner,
                     test_core_time::get_time());

  DO_CALLBACK(events, "configure_core");

  REWIND_BLOCKS_N_WITH_TIME(events,
                            blk_0r,
                            blk_0,
                            miner,
                            CURRENCY_MINED_MONEY_UNLOCK_WINDOW);

  success = fill_tx_sources_and_destinations(events,
                                             /* head = */ blk_0r,
                                             /* from = */ miner.get_keys(),
                                             /* to = */ alice.get_public_address(),
                                             /* amount = */ MK_TEST_COINS(12),
                                             /* fee = */ TESTS_DEFAULT_FEE,
                                             /* nmix = */ 0,
                                             sources,
                                             destinations);

  CHECK_AND_ASSERT_MES(success, false, "fail to fill sources, destinations");

  size_t tx_hardfork_id{};
  tx_version = get_tx_version_and_hardfork_id(get_block_height(blk_0r), m_hardforks, tx_hardfork_id);

  success = construct_tx(miner.get_keys(), sources, destinations, empty_attachment, tx_0, tx_version, tx_hardfork_id, 0);

  CHECK_AND_ASSERT_MES(success, false, "fail to construct tx_0");

  ADD_CUSTOM_EVENT(events, tx_0);
  MAKE_NEXT_BLOCK_TX1(events, blk_1, blk_0r, miner, tx_0);

  REWIND_BLOCKS_N_WITH_TIME(events,
                            blk_1r,
                            blk_1,
                            miner,
                            CURRENCY_MINED_MONEY_UNLOCK_WINDOW);

  sources.clear();
  destinations.clear();

  success = fill_tx_sources_and_destinations(events,
                                             /* head = */ blk_1r,
                                             /* from = */ alice,
                                             /* to = */ alice,
                                             /* amount = */ MK_TEST_COINS(5),
                                             /* fee = */ TESTS_DEFAULT_FEE,
                                             /* nmix = */ 0,
                                             sources,
                                             destinations);

  CHECK_AND_ASSERT_MES(success, false, "fail to fill sources, destinations");

  destinations.emplace_back(/* amount = */ 1'000'000'000'000'000'000,
                            /* to = */ alice.get_public_address(),
                            /* asset_id = */ currency::null_pkey);

  tx_version = get_tx_version_and_hardfork_id(get_block_height(blk_1r), m_hardforks, tx_hardfork_id);
  size_t hf_n = m_hardforks.get_the_most_recent_hardfork_id_for_height(get_block_height(blk_1r));
  fill_ado_version_based_onhardfork(m_ado_hello, hf_n);
  fill_adb_version_based_onhardfork(*m_ado_hello.opt_descriptor, hf_n);
  success = construct_tx(alice.get_keys(),
                         sources,
                         destinations,
                         /* extra = */ {m_ado_hello},
                         empty_attachment,
                         tx_1,
                         tx_version,
                         tx_hardfork_id,
                         stub,
                         0);

  CHECK_AND_ASSERT_MES(success, false, "fail to construct tx_1");
  ADD_CUSTOM_EVENT(events, tx_1);

  MAKE_NEXT_BLOCK_TX1(events,
                      blk_2,
                      blk_1r,
                      alice,
                      tx_1);

  REWIND_BLOCKS_N_WITH_TIME(events,
                            blk_2r,
                            blk_2,
                            miner,
                            CURRENCY_MINED_MONEY_UNLOCK_WINDOW);

  /* A transaction that has at least 2 registration operation descriptors in its
  extra is rejected by the core. */

  sources.clear();
  destinations.clear();

  success = fill_tx_sources_and_destinations(events,
                                             /* head = */ blk_2r,
                                             /* from = */ alice,
                                             /* to = */ alice,
                                             /* amount = */ MK_TEST_COINS(2),
                                             /* fee = */ TESTS_DEFAULT_FEE,
                                             /* nmix = */ 0,
                                             sources,
                                             destinations);

  CHECK_AND_ASSERT_MES(success, false, "fail to fill sources, destinations");

  tx_version = get_tx_version_and_hardfork_id(get_block_height(blk_2r), m_hardforks, tx_hardfork_id);

  hf_n = m_hardforks.get_the_most_recent_hardfork_id_for_height(get_block_height(blk_2r));
  fill_ado_version_based_onhardfork(m_ado_hello, hf_n);
  fill_adb_version_based_onhardfork(*m_ado_hello.opt_descriptor, hf_n);
  success = construct_tx(alice.get_keys(),
                         sources,
                         destinations,
                         /* extra = */ {m_ado_hello, m_ado_hello},
                         empty_attachment,
                         tx_2,
                         tx_version,
                         tx_hardfork_id,
                         stub,
                         0);

  CHECK_AND_ASSERT_MES(success, false, "fail to construct tx_2");

  DO_CALLBACK(events, "mark_invalid_tx");
  ADD_CUSTOM_EVENT(events, tx_2);

  sources.clear();
  destinations.clear();

  /* A transaction that contains a registration operation descriptor in its
  attachement, but extra is empty, is valid, but doesn't register the asset. The
  fact that the asset is not registered is checked in the assertions in the
  callback с2. */

  success = fill_tx_sources_and_destinations(events,
                                             /* head = */ blk_2r,
                                             /* from = */ alice,
                                             /* to = */ alice,
                                             /* amount = */ MK_TEST_COINS(2),
                                             /* fee = */ TESTS_DEFAULT_FEE,
                                             /* nmix = */ 0,
                                             sources,
                                             destinations);

  CHECK_AND_ASSERT_MES(success, false, "fail to fill sources, destinations");

  tx_version = get_tx_version(get_block_height(blk_2r),
                              m_hardforks);

  hf_n = m_hardforks.get_the_most_recent_hardfork_id_for_height(get_block_height(blk_2r));
  fill_ado_version_based_onhardfork(m_ado_bye, hf_n);
  fill_adb_version_based_onhardfork(*m_ado_bye.opt_descriptor, hf_n);

  success = construct_tx(alice.get_keys(),
                         sources,
                         destinations,
                         /* attachments = */ {m_ado_bye},
                         tx_3,
                         tx_version,
                         hf_n,
                         0);

  CHECK_AND_ASSERT_MES(success, false, "fail to construct tx_3");

  ADD_CUSTOM_EVENT(events, tx_3);
  DO_CALLBACK(events, "c2");

  sources.clear();
  destinations.clear();

  /* A transaction that contains a registration operation descriptor in its
  attachement, but extra is empty, is valid, but doesn't register the asset. In
  this case a different definition of the function construct_tx is used. */

  success = fill_tx_sources_and_destinations(events,
                                             /* head = */ blk_2r,
                                             /* from = */ alice,
                                             /* to = */ alice,
                                             /* amount = */ MK_TEST_COINS(2),
                                             /* fee = */ TESTS_DEFAULT_FEE,
                                             /* nmix = */ 0,
                                             sources,
                                             destinations);

  CHECK_AND_ASSERT_MES(success, false, "fail to fill sources, destinations");

  tx_version = get_tx_version(get_block_height(blk_2r),
                              m_hardforks);

  hf_n = m_hardforks.get_the_most_recent_hardfork_id_for_height(get_block_height(blk_2r));
  fill_ado_version_based_onhardfork(m_ado_bye, hf_n);
  fill_adb_version_based_onhardfork(*m_ado_bye.opt_descriptor, hf_n);

  success = construct_tx(alice.get_keys(),
                         sources,
                         destinations,
                         empty_extra,
                         /* attachments = */ {m_ado_bye},
                         tx_4,
                         tx_version,
                         hf_n,
                         stub,
                         0);

  CHECK_AND_ASSERT_MES(success, false, "fail to construct tx_4");

  ADD_CUSTOM_EVENT(events, tx_4);
  DO_CALLBACK(events, "c2");
  DO_CALLBACK(events, "c1");

  return true;
}

bool asset_operation_and_hardfork_checks::c1(
  currency::core& c,
  size_t ev_index,
  const std::vector<test_event_entry>& events)
{
  std::shared_ptr<tools::wallet2> wallet{
    init_playtime_test_wallet_t<tools::wallet2>(events, c, ALICE_ACC_IDX)
  };

  wallet->refresh();

  crypto::point_t asset_id_point{};
  crypto::public_key asset_id_public_key{};
  currency::get_or_calculate_asset_id(m_ado_hello,
                                      &asset_id_point,
                                      &asset_id_public_key);

  CHECK_AND_ASSERT_MES(
    check_balance_via_wallet(*wallet,
                             /* name = */ "Alice",
                             /* expected_total = */ 1'000'000'000'000'000'000,
                             /* expected_mined = */ INVALID_BALANCE_VAL,
                             /* expected_unlocked = */ 1'000'000'000'000'000'000,
                             /* expected_awaiting_in = */ INVALID_BALANCE_VAL,
                             /* expected_awaiting_out = */ INVALID_BALANCE_VAL,
                             asset_id_public_key),
    false,
    "balance check failed");

  return true;
}

bool asset_operation_and_hardfork_checks::c2(
  currency::core& c,
  size_t ev_index,
  const std::vector<test_event_entry>& events)
{
  crypto::point_t asset_id_pt{};
  crypto::public_key asset_id{};
  currency::asset_descriptor_base stub{};

  CHECK_AND_ASSERT_MES(
    get_or_calculate_asset_id(m_ado_bye,
                              &asset_id_pt,
                              &asset_id),
    false,
    "fail to calculate asset id");

  CHECK_AND_ASSERT_MES(
    !c.get_blockchain_storage().get_asset_info(asset_id,
                                               stub),
    false,
    "unregistered asset has info");

  return true;
}

asset_operation_in_consolidated_tx::asset_operation_in_consolidated_tx()
{
  REGISTER_CALLBACK_METHOD(asset_operation_in_consolidated_tx, assert_balances);
  REGISTER_CALLBACK_METHOD(asset_operation_in_consolidated_tx, assert_alice_currency_not_registered);
}

bool asset_operation_in_consolidated_tx::generate(std::vector<test_event_entry>& events) const
{
  // Test idea: make sure that the core rule prohibiting operations with assets in TX_FLAG_SIGNATURE_MODE_SEPARATE transactions works.
  bool success{};
  transaction tx_2{};
  uint64_t tx_version{};
  crypto::secret_key one_time{};
  tx_generation_context context_tx_2{};
  asset_descriptor_base adb_alice_currency{};
  asset_descriptor_operation ado_alice_currency{};
  GENERATE_ACCOUNT(miner);
  GENERATE_ACCOUNT(alice);
  GENERATE_ACCOUNT(bob);

  m_accounts.push_back(miner);
  m_accounts.push_back(alice);
  m_accounts.push_back(bob);

  adb_alice_currency.version = ASSET_DESCRIPTOR_BASE_HF4_VER;
  adb_alice_currency.total_max_supply = 1'000'000'000'000'000'000;
  adb_alice_currency.current_supply = 1'000'000'000'000'000'000;
  adb_alice_currency.ticker = "ALC";
  adb_alice_currency.full_name = "ALICE";
  adb_alice_currency.meta_info = "Currency created by Alice";
  adb_alice_currency.hidden_supply = false;
  adb_alice_currency.owner = m_accounts.at(ALICE_ACC_IDX).get_public_address().spend_public_key;
  ado_alice_currency.opt_descriptor = adb_alice_currency;
  ado_alice_currency.operation_type = ASSET_DESCRIPTOR_OPERATION_REGISTER;
  ado_alice_currency.opt_asset_id = currency::null_pkey;
  ado_alice_currency.version = ASSET_DESCRIPTOR_OPERATION_HF4_VER;

  MAKE_GENESIS_BLOCK(events, blk_0, miner, test_core_time::get_time());
  DO_CALLBACK(events, "configure_core");
  REWIND_BLOCKS_N(events, blk_0r, blk_0, miner, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);
  MAKE_TX(events, tx_0, miner, bob, MK_TEST_COINS(10), blk_0r);
  MAKE_NEXT_BLOCK_TX1(events, blk_1, blk_0r, miner, tx_0);
  REWIND_BLOCKS_N(events, blk_1r, blk_1, miner, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);
  MAKE_TX(events, tx_1, miner, alice, MK_TEST_COINS(10), blk_1r);
  MAKE_NEXT_BLOCK_TX1(events, blk_2, blk_1r, miner, tx_1);
  REWIND_BLOCKS_N(events, blk_2r, blk_2, miner, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);
  // Miner sent 10 coins to Alice, 10 coins to Bob.
  DO_CALLBACK(events, "assert_balances");

  {
    std::vector<tx_source_entry> sources{};
    std::vector<tx_destination_entry> destinations{};

    success = fill_tx_sources(sources, events, blk_2r, alice.get_keys(), MK_TEST_COINS(5), 1);
    CHECK_AND_ASSERT_MES(success, false, "failed to fill transaction sources on step 1");
    destinations.emplace_back(MK_TEST_COINS(5), bob.get_public_address());
    destinations.emplace_back(MK_TEST_COINS(/* 10 - 5 - 1 = */ 4), alice.get_public_address());
    tx_version = get_tx_version(get_block_height(blk_2r), m_hardforks);
    success = construct_tx(alice.get_keys(), sources, destinations, empty_extra, empty_attachment, tx_2, tx_version, one_time, 0, 0, 0, true, TX_FLAG_SIGNATURE_MODE_SEPARATE, TESTS_DEFAULT_FEE,
      context_tx_2);
    CHECK_AND_ASSERT_MES(success, false, "failed to construct transaction tx_2 on step 1");
  }

  // Transaction tx_2 hasn't been constructed completely yet. Core rejects tx_2.
  DO_CALLBACK(events, "mark_invalid_tx");
  ADD_CUSTOM_EVENT(events, tx_2);

  {
    std::vector<tx_source_entry> sources{};
    std::vector<tx_destination_entry> destinations{};

    success = fill_tx_sources(sources, events, blk_2r, bob.get_keys(), MK_TEST_COINS(5), 0);
    CHECK_AND_ASSERT_MES(success, false, "failed to fill transaction sources on step 2");

    for (tx_source_entry& source : sources)
    {
      source.separately_signed_tx_complete = true;
    }

    destinations.emplace_back(MK_TEST_COINS(5), alice.get_public_address());
    destinations.emplace_back(MK_TEST_COINS(/* 10 - 5 - 0 = */ 5), bob.get_public_address());
    destinations.emplace_back(adb_alice_currency.current_supply, alice.get_public_address(), null_pkey);
    tx_version = get_tx_version(get_block_height(blk_2r), m_hardforks);
    size_t hf_n = m_hardforks.get_the_most_recent_hardfork_id_for_height(get_block_height(blk_2r));
    fill_ado_version_based_onhardfork(ado_alice_currency, hf_n);
    fill_adb_version_based_onhardfork(*ado_alice_currency.opt_descriptor, hf_n);
    success = construct_tx(bob.get_keys(), sources, destinations, {ado_alice_currency}, empty_attachment, tx_2, tx_version, one_time, 0, 0, 0, true, TX_FLAG_SIGNATURE_MODE_SEPARATE,
      /* fee = */ 0, context_tx_2);
    CHECK_AND_ASSERT_MES(success, false, "failed to construct transaction tx_2 on step 2");
  }

  DO_CALLBACK(events, "mark_invalid_tx");
  ADD_CUSTOM_EVENT(events, tx_2);
  // Core rejects transaction tx_2. The balances of Alice, Bob haven't changed: Alice has 10 coins, Bob has 10 coins.
  DO_CALLBACK(events, "assert_balances");
  // Alice's asset hasn't registered, because transaction tx_2 was rejected.
  DO_CALLBACK_PARAMS_STR(events, "assert_alice_currency_not_registered", t_serializable_object_to_blob(ado_alice_currency));

  return true;
}

bool asset_operation_in_consolidated_tx::assert_balances(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events) const
{
  const auto alice_wallet{init_playtime_test_wallet_t<tools::wallet2>(events, c, ALICE_ACC_IDX)};
  const auto bob_wallet{init_playtime_test_wallet_t<tools::wallet2>(events, c, BOB_ACC_IDX)};

  alice_wallet->refresh();
  bob_wallet->refresh();

  CHECK_AND_ASSERT_EQ(alice_wallet->balance(currency::native_coin_asset_id), MK_TEST_COINS(10));
  CHECK_AND_ASSERT_EQ(bob_wallet->balance(currency::native_coin_asset_id), MK_TEST_COINS(10));

  return true;
}

bool asset_operation_in_consolidated_tx::assert_alice_currency_not_registered(const currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events) const
{
  crypto::public_key asset_id{};
  asset_descriptor_operation ado{};

  {
    const auto serialized_ado{boost::get<callback_entry>(events.at(ev_index)).callback_params};

    CHECK_AND_ASSERT_MES(t_unserializable_object_from_blob(ado, serialized_ado), false, "ADO deserialization failed");
  }

  {
    crypto::point_t point_asset_id{};

    CHECK_AND_ASSERT_MES(get_or_calculate_asset_id(ado, &point_asset_id, &asset_id), false, "fail to calculate asset id");
  }

  {
    currency::asset_descriptor_base adb_stub{};

    CHECK_AND_ASSERT_MES(!c.get_blockchain_storage().get_asset_info(asset_id, adb_stub), false, "unregistered asset has info");
  }

  return true;
}

//------------------------------------------------------------------------------

eth_signed_asset_basics::eth_signed_asset_basics()
{
  REGISTER_CALLBACK_METHOD(eth_signed_asset_basics, c1);
}

bool eth_signed_asset_basics::generate(std::vector<test_event_entry>& events) const
{
  //
  // Test idea: register an asset, then emit, public burn, transfer ownership, and then emit it once again.
  // Update (ownership transferring) and emit operations are done using third-party external asset operation signing (ETH signature)
  // Public burn operation is done by an entity, who isn't controlling the asset.
  //

  uint64_t ts = test_core_time::get_time();
  m_accounts.resize(TOTAL_ACCS_COUNT);
  account_base& miner_acc = m_accounts[MINER_ACC_IDX]; miner_acc.generate(); miner_acc.set_createtime(ts);
  account_base& alice_acc = m_accounts[ALICE_ACC_IDX]; alice_acc.generate(); alice_acc.set_createtime(ts);

  MAKE_GENESIS_BLOCK(events, blk_0, miner_acc, ts);
  DO_CALLBACK(events, "configure_core"); // default configure_core callback will initialize core runtime config with m_hardforks
  REWIND_BLOCKS_N_WITH_TIME(events, blk_0r, blk_0, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW + 3);

  DO_CALLBACK(events, "c1");

  return true;
}

bool eth_signed_asset_basics::c1(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events)
{
  bool r = false;

  crypto::eth_secret_key eth_sk{};
  crypto::eth_public_key eth_pk{};
  r = crypto::generate_eth_key_pair(eth_sk, eth_pk);
  CHECK_AND_ASSERT_MES(r, false, "generate_eth_key_pair failed");

  std::shared_ptr<tools::wallet2> miner_wlt = init_playtime_test_wallet(events, c, MINER_ACC_IDX);
  miner_wlt->refresh();
  std::shared_ptr<tools::wallet2> alice_wlt = init_playtime_test_wallet(events, c, ALICE_ACC_IDX);
  alice_wlt->refresh();

  //
  // register
  //
  asset_descriptor_base adb{};
  adb.decimal_point = 2;
  adb.total_max_supply = 10000;
  adb.full_name = "Either";
  adb.ticker = "EITH";
  adb.owner_eth_pub_key = eth_pk; // note setting owner eth pub key here

  uint64_t initial_emit_amount = adb.total_max_supply / 2;

  std::vector<tx_destination_entry> destinations{tx_destination_entry{initial_emit_amount, m_accounts[ALICE_ACC_IDX].get_public_address(), null_pkey}};
  finalized_tx ft{};
  crypto::public_key asset_id = currency::null_pkey;
  miner_wlt->deploy_new_asset(adb, destinations, ft, asset_id);

  LOG_PRINT_L0("Deployed new asset: " << asset_id << ", tx_id: " << ft.tx_id);

  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());
  r = mine_next_pow_block_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_block_in_playtime failed");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  miner_wlt->refresh();

  //
  // emit
  //
  destinations.clear();
  destinations.emplace_back(initial_emit_amount, m_accounts[ALICE_ACC_IDX].get_public_address(), null_pkey); // asset
  destinations.emplace_back(MK_TEST_COINS(100), m_accounts[ALICE_ACC_IDX].get_public_address()); // some native coins for fee
  uint64_t emit_amount = adb.total_max_supply - initial_emit_amount;
  ft = finalized_tx{};
  miner_wlt->emit_asset(asset_id, destinations, ft);

  // make sure the transaction didn't get into tx pool (as it's not fully signed)
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  // make sure emit transaction cannot be added to the transaction pool (it's not fully signed atm)
  tx_verification_context tvc{};
  r = c.get_tx_pool().add_tx(ft.tx, tvc, false);
  CHECK_AND_ASSERT_MES(!r, false, "unsigned emit tx was able to be added to the pool");

  // sign asset emit operation with ETH signature
  crypto::eth_signature eth_sig{};
  r = crypto::generate_eth_signature(ft.tx_id, eth_sk, eth_sig);
  CHECK_AND_ASSERT_MES(r, false, "generate_eth_signature failed");
  r = crypto::verify_eth_signature(ft.tx_id, eth_pk, eth_sig);
  CHECK_AND_ASSERT_MES(r, false, "generate_eth_signature self validation failed");
   
  transaction emit_tx{};
  bool transfers_unlocked = false;
  miner_wlt->submit_externally_signed_asset_tx(ft, eth_sig, true, emit_tx, transfers_unlocked);

  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());
  r = mine_next_pow_block_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_block_in_playtime failed");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  // Alice checks her asset balance
  size_t blocks_fetched = 0;
  alice_wlt->refresh(blocks_fetched);
  CHECK_AND_ASSERT_EQ(blocks_fetched, 2);
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", adb.total_max_supply, asset_id, adb.decimal_point), false, "");
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", MK_TEST_COINS(100)), false, "");


  //
  // public burn (by Alice, as anyone can do public burn)
  //
  uint64_t amount_to_burn = initial_emit_amount / 2;
  uint64_t total_amount_expected = adb.total_max_supply - amount_to_burn;

  r = mine_next_pow_blocks_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_block_in_playtime failed");
  miner_wlt->refresh();
  alice_wlt->refresh();

  ft = finalized_tx{};
  alice_wlt->burn_asset(asset_id, amount_to_burn, ft);

  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());
  r = mine_next_pow_block_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_block_in_playtime failed");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  asset_descriptor_base adb_recent{};
  r = c.get_blockchain_storage().get_asset_info(asset_id, adb_recent);
  CHECK_AND_ASSERT_MES(r, false, "get_asset_info failed");

  // make sure the current supply of the asset did change accordingly
  CHECK_AND_ASSERT_EQ(adb_recent.current_supply, total_amount_expected);


  //
  // ownership transfer (to another ETH key)
  //
  crypto::eth_secret_key eth_sk_2{};
  crypto::eth_public_key eth_pk_2{};
  r = crypto::generate_eth_key_pair(eth_sk_2, eth_pk_2);
  CHECK_AND_ASSERT_MES(r, false, "generate_eth_key_pair failed");

  ft = finalized_tx{};
  miner_wlt->transfer_asset_ownership(asset_id, eth_pk_2, ft);

  // make sure the transaction didn't get into tx pool (as it's not fully signed)
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  // make sure ownership transaction cannot be added to the transaction pool (it's not fully signed atm)
  tvc = tx_verification_context{};
  r = c.get_tx_pool().add_tx(ft.tx, tvc, false);
  CHECK_AND_ASSERT_MES(!r, false, "unsigned tx was able to be added to the pool");

  // sign asset emit operation with ETH signature
  eth_sig = crypto::eth_signature{};
  r = crypto::generate_eth_signature(ft.tx_id, eth_sk, eth_sig); // signing with old eth key
  CHECK_AND_ASSERT_MES(r, false, "generate_eth_signature failed");

  transaction to_tx{};
  transfers_unlocked = false;
  miner_wlt->submit_externally_signed_asset_tx(ft, eth_sig, true, to_tx, transfers_unlocked);

  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());
  r = mine_next_pow_block_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_block_in_playtime failed");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  miner_wlt->refresh();

  //
  // emit #2
  //
  emit_amount = adb.total_max_supply - total_amount_expected; // up to max
  destinations.clear();
  destinations.emplace_back(emit_amount, m_accounts[ALICE_ACC_IDX].get_public_address(), null_pkey); // asset
  ft = finalized_tx{};
  miner_wlt->emit_asset(asset_id, destinations, ft);

  // make sure the transaction didn't get into tx pool (as it's not fully signed)
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  // make sure emit transaction cannot be added to the transaction pool (it's not fully signed atm)
  r = c.get_tx_pool().add_tx(ft.tx, tvc, false);
  CHECK_AND_ASSERT_MES(!r, false, "unsigned emit tx was able to be added to the pool");

  // sign asset emit operation with ETH signature
  eth_sig = crypto::eth_signature{};
  r = crypto::generate_eth_signature(ft.tx_id, eth_sk_2, eth_sig); // note using ETH key #2 here
  CHECK_AND_ASSERT_MES(r, false, "generate_eth_signature failed");

  emit_tx = transaction{};
  transfers_unlocked = false;
  miner_wlt->submit_externally_signed_asset_tx(ft, eth_sig, true, emit_tx, transfers_unlocked);

  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());
  r = mine_next_pow_block_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_block_in_playtime failed");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  // Alice checks her asset balance
  alice_wlt->refresh(blocks_fetched);
  CHECK_AND_ASSERT_EQ(blocks_fetched, 3);
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", adb.total_max_supply, asset_id, adb.decimal_point), false, "");
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", MK_TEST_COINS(100) - TESTS_DEFAULT_FEE), false, "");

  return true;
}

//------------------------------------------------------------------------------

eth_signed_asset_via_rpc::eth_signed_asset_via_rpc()
{
  REGISTER_CALLBACK_METHOD(eth_signed_asset_via_rpc, c1);
}

bool eth_signed_asset_via_rpc::generate(std::vector<test_event_entry>& events) const
{
  //
  // Test idea: make sure register, emit, and ownership transfer operations with external signing (ETH signature)
  // can be done entirely using JSON RPC calls (both, the core RPC and the wallet RPC)
  //
  uint64_t ts = test_core_time::get_time();
  m_accounts.resize(TOTAL_ACCS_COUNT);
  account_base& miner_acc = m_accounts[MINER_ACC_IDX]; miner_acc.generate(); miner_acc.set_createtime(ts);
  account_base& alice_acc = m_accounts[ALICE_ACC_IDX]; alice_acc.generate(); alice_acc.set_createtime(ts);
  account_base& bob_acc   = m_accounts[BOB_ACC_IDX];   bob_acc.generate();   bob_acc.set_createtime(ts);

  MAKE_GENESIS_BLOCK(events, blk_0, miner_acc, ts);
  DO_CALLBACK(events, "configure_core"); // default configure_core callback will initialize core runtime config with m_hardforks
  REWIND_BLOCKS_N_WITH_TIME(events, blk_0r, blk_0, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW + 3);

  DO_CALLBACK(events, "c1");

  return true;
}

bool eth_signed_asset_via_rpc::c1(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events)
{
  bool r = false;

  crypto::eth_secret_key eth_sk{};
  crypto::eth_public_key eth_pk{};
  r = crypto::generate_eth_key_pair(eth_sk, eth_pk);
  CHECK_AND_ASSERT_MES(r, false, "generate_eth_key_pair failed");

  std::shared_ptr<tools::wallet2> miner_wlt = init_playtime_test_wallet(events, c, MINER_ACC_IDX);
  std::shared_ptr<tools::wallet2> alice_wlt = init_playtime_test_wallet(events, c, ALICE_ACC_IDX);
  miner_wlt->refresh();
  alice_wlt->refresh();

  // wallet RPC server
  tools::wallet_rpc_server miner_wlt_rpc(miner_wlt);
  epee::json_rpc::error jerr{};
  tools::wallet_rpc_server::connection_context ctx{};

  // core RPC server
  currency::t_currency_protocol_handler<currency::core> m_cprotocol(c, nullptr);
  nodetool::node_server<currency::t_currency_protocol_handler<currency::core> > p2p(m_cprotocol);
  bc_services::bc_offers_service of(nullptr);
  currency::core_rpc_server core_rpc_wrapper(c, p2p, of);
  core_rpc_wrapper.set_ignore_connectivity_status(true);

  // asset description
  asset_descriptor_base adb{};
  adb.decimal_point = 2;
  adb.total_max_supply = 45000;
  adb.full_name = "P450";
  adb.ticker = "P450";
  adb.owner_eth_pub_key = eth_pk; // note setting owner eth pub key here

  uint64_t initial_register_amount = 10000;

  // 1. Miner deploys initial amount of the asset (all go to Alice)
  // deploy operation don't require eth proof and therefore the corresponding tx will be generated and added to the tx pool as usual

  tools::wallet_public::COMMAND_ASSETS_DEPLOY::request deploy_req{};
  deploy_req.asset_descriptor = adb;
  deploy_req.destinations.push_back(tools::wallet_public::transfer_destination{initial_register_amount, m_accounts[ALICE_ACC_IDX].get_public_address_str(), null_pkey});
  deploy_req.do_not_split_destinations = false;
  tools::wallet_public::COMMAND_ASSETS_DEPLOY::response deploy_resp{};
  r = miner_wlt_rpc.on_asset_deploy(deploy_req, deploy_resp, jerr, ctx);
  CHECK_AND_ASSERT_MES(r, false, "RPC on_asset_deploy failed");

  const crypto::public_key asset_id = deploy_resp.new_asset_id;
  LOG_PRINT_GREEN_L0("Asset " << asset_id << " was successfully deployed with tx " << deploy_resp.tx_id);

  // make sure tx was added to the pool
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  // confirm the tx with a block
  CHECK_AND_ASSERT_MES(mine_next_pow_block_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c), false, "");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  size_t blocks_fetched = 0;
  miner_wlt->refresh(blocks_fetched);
  CHECK_AND_ASSERT_EQ(blocks_fetched, 1);

  // Alice checks her asset balance
  alice_wlt->refresh(blocks_fetched);
  CHECK_AND_ASSERT_EQ(blocks_fetched, 1);
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", initial_register_amount, asset_id, adb.decimal_point), false, "");


  //
  // 2. Miner emits additional amount of the asset and transfers it to Alice
  //
  uint64_t additional_emit_amount = 20000;
  uint64_t total_asset_amount = initial_register_amount + additional_emit_amount;

  tools::wallet_public::COMMAND_ASSETS_EMIT::request emit_req{};
  emit_req.asset_id = asset_id;
  emit_req.destinations.push_back(tools::wallet_public::transfer_destination{additional_emit_amount, m_accounts[ALICE_ACC_IDX].get_public_address_str(), asset_id});
  emit_req.do_not_split_destinations = false;

  tools::wallet_public::COMMAND_ASSETS_EMIT::response emit_resp{};
  r = miner_wlt_rpc.on_asset_emit(emit_req, emit_resp, jerr, ctx);
  CHECK_AND_ASSERT_MES(r, false, "RPC on_asset_emit failed: " << jerr.message);

  // make sure tx was NOT added to the pool (because it's only partially signed)
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  // unserialize transaction that we got from RPC
  transaction emit_tx{};
  CHECK_AND_ASSERT_MES(emit_resp.data_for_external_signing.has_value(), false, "data_for_external_signing has no value");
  r = t_unserializable_object_from_blob(emit_tx, emit_resp.data_for_external_signing->unsigned_tx);
  CHECK_AND_ASSERT_MES(r, false, "t_unserializable_object_from_blob failed");
  CHECK_AND_ASSERT_EQ(get_transaction_hash(emit_tx), emit_resp.tx_id);

  // make sure emit transaction cannot be added to the transaction pool (it's not fully signed atm)
  tx_verification_context tvc{};
  r = c.get_tx_pool().add_tx(emit_tx, tvc, false);
  CHECK_AND_ASSERT_MES(!r, false, "emit tx was able to be added to the pool");

  //
  // decrypt emission transaction outputs prior to ETH signing to make sure it's valid
  //
  currency::COMMAND_RPC_DECRYPT_TX_DETAILS::request decrypt_req{};
  decrypt_req.tx_secret_key = emit_resp.data_for_external_signing->tx_secret_key;
  decrypt_req.tx_blob = epee::string_encoding::base64_encode(emit_resp.data_for_external_signing->unsigned_tx);
  // note: decrypt_req.outputs_addresses can be populated using emit_resp.data_for_external_signing->outputs_addresses but we fill it manually here
  decrypt_req.outputs_addresses.push_back(m_accounts[MINER_ACC_IDX].get_public_address_str()); // we expect that the first output is the cashback and addressed to miner
  for(size_t i = 0, size = emit_tx.vout.size() - 1; i < size; ++i)
    decrypt_req.outputs_addresses.push_back(m_accounts[ALICE_ACC_IDX].get_public_address_str()); // we expect all other outputs are asset emission and addresses to Alice
  currency::COMMAND_RPC_DECRYPT_TX_DETAILS::response decrypt_resp{};
  r = core_rpc_wrapper.on_decrypt_tx_details(decrypt_req, decrypt_resp, jerr, ctx);
  CHECK_AND_ASSERT_MES(r, false, "RPC on_decrypt_tx_details failed: " << jerr.message);
  // make sure that verified_tx_id is the one we expect
  CHECK_AND_ASSERT_EQ(decrypt_resp.verified_tx_id, emit_resp.tx_id);

  // after a successfull tx outputs decryption, examine them
  CHECK_AND_ASSERT_EQ(decrypt_resp.decoded_outputs.size(), emit_tx.vout.size());
  uint64_t decrypted_emission_sum = 0;
  for(auto& el : decrypt_resp.decoded_outputs)
  {
    if (el.asset_id == asset_id)
      decrypted_emission_sum += el.amount;
    else
      CHECK_AND_ASSERT_EQ(el.asset_id, native_coin_asset_id);
  }
  // make sure the transaction emits the expected amount of asset
  CHECK_AND_ASSERT_EQ(decrypted_emission_sum, additional_emit_amount);

  LOG_PRINT_YELLOW(decrypt_resp.tx_in_json, LOG_LEVEL_0); // tx with still missing ownership proof

  //
  // as everything is allright, sign emit_tx with ETH signature.
  //
  crypto::eth_signature eth_sig{};
  crypto::generate_eth_signature(emit_resp.tx_id, eth_sk, eth_sig);
  // instant verification, just in case
  r = crypto::verify_eth_signature(emit_resp.tx_id, eth_pk, eth_sig);
  CHECK_AND_ASSERT_MES(r, false, "verify_eth_signature failed");

  //
  // send ETH signature alogn with all previous data to a wallet RPC call for final tx assembling and broadcasting
  //
  tools::wallet_public::COMMAND_ASSET_SEND_EXT_SIGNED_TX::request send_signed_req{};
  send_signed_req.unsigned_tx     = emit_resp.data_for_external_signing->unsigned_tx;
  send_signed_req.eth_sig         = eth_sig;
  send_signed_req.expected_tx_id  = decrypt_resp.verified_tx_id;
  send_signed_req.finalized_tx    = emit_resp.data_for_external_signing->finalized_tx;
  send_signed_req.unlock_transfers_on_fail = true;
  tools::wallet_public::COMMAND_ASSET_SEND_EXT_SIGNED_TX::response send_signed_resp{};
  r = miner_wlt_rpc.on_asset_send_ext_signed_tx(send_signed_req, send_signed_resp, jerr, ctx);
  CHECK_AND_ASSERT_MES(r, false, "RPC send_ext_signed_asset_tx failed: " << jerr.message);

  // make sure tx was broadcasted
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  // confirm the tx with a block
  CHECK_AND_ASSERT_MES(mine_next_pow_block_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c), false, "");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  // Alice checks her asset balance
  alice_wlt->refresh(blocks_fetched);
  CHECK_AND_ASSERT_EQ(blocks_fetched, 1);
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", total_asset_amount, asset_id, adb.decimal_point), false, "");


  //
  // 3. Miner transfer ownership of the asset to another ETH key (#2)
  //
  crypto::eth_secret_key eth_sk_2{};
  crypto::eth_public_key eth_pk_2{};
  r = crypto::generate_eth_key_pair(eth_sk_2, eth_pk_2);
  CHECK_AND_ASSERT_MES(r, false, "generate_eth_key_pair failed");

  tools::wallet_public::COMMAND_ASSETS_UPDATE::request to_req{}; // 'to' means transfer of ownership
  to_req.asset_id = asset_id;
  {
    // request the most recent asset info from core to correctly fill to_req
    currency::COMMAND_RPC_GET_ASSET_INFO::request req{};
    req.asset_id = asset_id;
    currency::COMMAND_RPC_GET_ASSET_INFO::response resp{};
    CHECK_AND_ASSERT_MES(core_rpc_wrapper.on_get_asset_info(req, resp, ctx), false, "on_get_asset_info failed");
    to_req.asset_descriptor = resp.asset_descriptor;
  }
  to_req.asset_descriptor.owner_eth_pub_key = eth_pk_2; // new owner, note using another ETH pub key
  to_req.asset_descriptor.meta_info = "owner: eth_pk_2"; // it's also possible to change meta_info with this update operation
  tools::wallet_public::COMMAND_ASSETS_UPDATE::response to_resp{};
  r = miner_wlt_rpc.on_asset_update(to_req, to_resp, jerr, ctx);
  CHECK_AND_ASSERT_MES(r, false, "RPC send_ext_signed_asset_tx failed: " << jerr.message);

  // make sure tx was NOT added to the pool (because it's only partially signed)
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  CHECK_AND_ASSERT_MES(to_resp.data_for_external_signing.has_value(), false, "data_for_external_signing has no value");
  transaction to_tx{};
  r = t_unserializable_object_from_blob(to_tx, to_resp.data_for_external_signing->unsigned_tx);
  CHECK_AND_ASSERT_MES(r, false, "t_unserializable_object_from_blob failed");
  CHECK_AND_ASSERT_EQ(get_transaction_hash(to_tx), to_resp.tx_id);

  // make sure ownership transferring transaction cannot be added to the transaction pool (it's not fully signed atm)
  tvc = tx_verification_context{};
  r = c.get_tx_pool().add_tx(to_tx, tvc, false);
  CHECK_AND_ASSERT_MES(!r, false, "ownership transferring tx was able to be added to the pool");

  //
  // decrypt ownership transfer transaction prior to ETH signing to make sure it's valid
  //
  decrypt_req = currency::COMMAND_RPC_DECRYPT_TX_DETAILS::request{};
  decrypt_req.tx_secret_key = to_resp.data_for_external_signing->tx_secret_key;
  decrypt_req.tx_blob = epee::string_encoding::base64_encode(to_resp.data_for_external_signing->unsigned_tx);
  // note: decrypt_req.outputs_addresses can be populated using to_resp.data_for_external_signing->outputs_addresses but we fill it manually here
  for(size_t i = 0, size = to_tx.vout.size(); i < size; ++i)
    decrypt_req.outputs_addresses.push_back(m_accounts[MINER_ACC_IDX].get_public_address_str()); // we expect all outputs goes to Miner
  decrypt_resp = currency::COMMAND_RPC_DECRYPT_TX_DETAILS::response{};
  r = core_rpc_wrapper.on_decrypt_tx_details(decrypt_req, decrypt_resp, jerr, ctx);
  CHECK_AND_ASSERT_MES(r, false, "RPC on_decrypt_tx_details failed: " << jerr.message);
  // make sure that verified_tx_id is the one we expect
  CHECK_AND_ASSERT_EQ(decrypt_resp.verified_tx_id, to_resp.tx_id);

  // after a successfull tx outputs decryption, examine them
  CHECK_AND_ASSERT_EQ(decrypt_resp.decoded_outputs.size(), to_tx.vout.size());
  uint64_t asset_sum = 0;
  for(auto& el : decrypt_resp.decoded_outputs)
  {
    if (el.asset_id != native_coin_asset_id)
      asset_sum += el.amount;
  }
  // make sure the transaction don't send assets
  CHECK_AND_ASSERT_EQ(asset_sum, 0);

  // make sure this is an ownership transfer transaction and the ownership is correctly transferred:
  // Note: this check could also be done by examination of decrypt_resp.tx_in_json
  asset_descriptor_operation* pado = get_type_in_variant_container<asset_descriptor_operation>(to_tx.extra);
  CHECK_AND_ASSERT_NEQ(pado, 0);
  CHECK_AND_ASSERT_EQ(pado->operation_type, ASSET_DESCRIPTOR_OPERATION_UPDATE);
  CHECK_AND_ASSERT_EQ(pado->opt_asset_id.has_value(), true);
  CHECK_AND_ASSERT_EQ(pado->opt_asset_id.get(), asset_id);
  CHECK_AND_ASSERT_EQ(pado->opt_descriptor->owner_eth_pub_key.has_value(), true);
  CHECK_AND_ASSERT_EQ(pado->opt_descriptor->owner_eth_pub_key.get(), eth_pk_2); // the most important condition for an ownership transfer
  // other fileds of pado->descriptor may also be checked here

  //
  // as everything is allright, sign to_tx with ETH signature.
  //
  crypto::eth_signature to_eth_sig{};
  crypto::generate_eth_signature(to_resp.tx_id, eth_sk, to_eth_sig); // note using old ETH secret key here, because this tx must be signed with the original owner
  // instant verification, just in case
  r = crypto::verify_eth_signature(to_resp.tx_id, eth_pk, to_eth_sig);
  CHECK_AND_ASSERT_MES(r, false, "verify_eth_signature failed");

  //
  // send ETH signature along with all previous data to a wallet RPC call for final tx assembling and broadcasting
  //
  send_signed_req = tools::wallet_public::COMMAND_ASSET_SEND_EXT_SIGNED_TX::request{};
  send_signed_req.unsigned_tx     = to_resp.data_for_external_signing->unsigned_tx;
  send_signed_req.eth_sig         = to_eth_sig;
  send_signed_req.expected_tx_id  = decrypt_resp.verified_tx_id;
  send_signed_req.finalized_tx    = to_resp.data_for_external_signing->finalized_tx;
  send_signed_req.unlock_transfers_on_fail = true;
  send_signed_resp = tools::wallet_public::COMMAND_ASSET_SEND_EXT_SIGNED_TX::response{};
  r = miner_wlt_rpc.on_asset_send_ext_signed_tx(send_signed_req, send_signed_resp, jerr, ctx);
  CHECK_AND_ASSERT_MES(r, false, "RPC send_ext_signed_asset_tx failed: " << jerr.message);

  // make sure tx was broadcasted
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  // confirm the tx with a block
  CHECK_AND_ASSERT_MES(mine_next_pow_block_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c), false, "");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  // Alice checks her asset balance, it shouldn't change
  alice_wlt->refresh(blocks_fetched);
  CHECK_AND_ASSERT_EQ(blocks_fetched, 1);
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", total_asset_amount, asset_id, adb.decimal_point), false, "");

  miner_wlt->refresh();

  //
  // 4. Miner emits additional amount of the asset (signing it with ETH key #2) and transfers it to Bob
  //
  additional_emit_amount = 15000;
  uint64_t alice_amount = total_asset_amount;
  total_asset_amount += additional_emit_amount;

  emit_req = tools::wallet_public::COMMAND_ASSETS_EMIT::request{};
  emit_req.asset_id = asset_id;
  emit_req.destinations.push_back(tools::wallet_public::transfer_destination{additional_emit_amount, m_accounts[BOB_ACC_IDX].get_public_address_str(), asset_id});
  emit_req.do_not_split_destinations = false;

  emit_resp = tools::wallet_public::COMMAND_ASSETS_EMIT::response{};
  r = miner_wlt_rpc.on_asset_emit(emit_req, emit_resp, jerr, ctx);
  CHECK_AND_ASSERT_MES(r, false, "RPC on_asset_emit failed: " << jerr.message);

  // make sure tx was NOT added to the pool (because it's only partially signed)
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  // unserialize transaction that we got from RPC
  emit_tx = transaction{};
  CHECK_AND_ASSERT_MES(emit_resp.data_for_external_signing.has_value(), false, "data_for_external_signing has no value");
  r = t_unserializable_object_from_blob(emit_tx, emit_resp.data_for_external_signing->unsigned_tx);
  CHECK_AND_ASSERT_MES(r, false, "t_unserializable_object_from_blob failed");
  CHECK_AND_ASSERT_EQ(get_transaction_hash(emit_tx), emit_resp.tx_id);

  // make sure emit transaction cannot be added to the transaction pool (it's not fully signed atm)
  tvc = tx_verification_context{};
  r = c.get_tx_pool().add_tx(emit_tx, tvc, false);
  CHECK_AND_ASSERT_MES(!r, false, "emit tx was able to be added to the pool");

  //
  // decrypt emission transaction outputs prior to ETH signing to make sure it's valid
  //
  decrypt_req = currency::COMMAND_RPC_DECRYPT_TX_DETAILS::request{};
  decrypt_req.tx_secret_key = emit_resp.data_for_external_signing->tx_secret_key;
  decrypt_req.tx_blob = epee::string_encoding::base64_encode(emit_resp.data_for_external_signing->unsigned_tx);
  // note: decrypt_req.outputs_addresses can be populated using emit_resp.data_for_external_signing->outputs_addresses but we fill it manually here
  decrypt_req.outputs_addresses.push_back(m_accounts[MINER_ACC_IDX].get_public_address_str()); // we expect that the first output is the cashback and addressed to miner
  for(size_t i = 0, size = emit_tx.vout.size() - 1; i < size; ++i)
    decrypt_req.outputs_addresses.push_back(m_accounts[BOB_ACC_IDX].get_public_address_str()); // we expect all other outputs are asset emission and addresses to Bob
  decrypt_resp = currency::COMMAND_RPC_DECRYPT_TX_DETAILS::response{};
  r = core_rpc_wrapper.on_decrypt_tx_details(decrypt_req, decrypt_resp, jerr, ctx);
  CHECK_AND_ASSERT_MES(r, false, "RPC on_decrypt_tx_details failed: " << jerr.message);
  // make sure that verified_tx_id is the one we expect
  CHECK_AND_ASSERT_EQ(decrypt_resp.verified_tx_id, emit_resp.tx_id);

  // after a successfull tx outputs decryption, examine them
  CHECK_AND_ASSERT_EQ(decrypt_resp.decoded_outputs.size(), emit_tx.vout.size());
  decrypted_emission_sum = 0;
  for(auto& el : decrypt_resp.decoded_outputs)
  {
    if (el.asset_id == asset_id)
      decrypted_emission_sum += el.amount;
    else
      CHECK_AND_ASSERT_EQ(el.asset_id, native_coin_asset_id);
  }
  // make sure the transaction emits the expected amount of asset
  CHECK_AND_ASSERT_EQ(decrypted_emission_sum, additional_emit_amount);

  //
  // as everything is allright, sign emit_tx with ETH signature.
  //
  eth_sig = crypto::eth_signature{};
  crypto::generate_eth_signature(emit_resp.tx_id, eth_sk_2, eth_sig); // note using ETH key #2
  // instant verification, just in case
  r = crypto::verify_eth_signature(emit_resp.tx_id, eth_pk_2, eth_sig);
  CHECK_AND_ASSERT_MES(r, false, "verify_eth_signature failed");

  //
  // send ETH signature alogn with all previous data to a wallet RPC call for final tx assembling and broadcasting
  //
  send_signed_req = tools::wallet_public::COMMAND_ASSET_SEND_EXT_SIGNED_TX::request{};
  send_signed_req.unsigned_tx     = emit_resp.data_for_external_signing->unsigned_tx;
  send_signed_req.eth_sig         = eth_sig;
  send_signed_req.expected_tx_id  = decrypt_resp.verified_tx_id;
  send_signed_req.finalized_tx    = emit_resp.data_for_external_signing->finalized_tx;
  send_signed_req.unlock_transfers_on_fail = true;
  send_signed_resp = tools::wallet_public::COMMAND_ASSET_SEND_EXT_SIGNED_TX::response{};
  r = miner_wlt_rpc.on_asset_send_ext_signed_tx(send_signed_req, send_signed_resp, jerr, ctx);
  CHECK_AND_ASSERT_MES(r, false, "RPC send_ext_signed_asset_tx failed: " << jerr.message);

  // make sure tx was broadcasted
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  // confirm the tx with a block
  CHECK_AND_ASSERT_MES(mine_next_pow_block_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c), false, "");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  // Alice checks her asset balance, it shouldn't change
  alice_wlt->refresh(blocks_fetched);
  CHECK_AND_ASSERT_EQ(blocks_fetched, 1);
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", alice_amount, asset_id, adb.decimal_point), false, "");

  // Bob checks his asset balance
  std::shared_ptr<tools::wallet2> bob_wlt = init_playtime_test_wallet(events, c, BOB_ACC_IDX);
  bob_wlt->refresh();
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*bob_wlt, "Bob", additional_emit_amount, asset_id, adb.decimal_point), false, "");

  // finally, check asset's current supply
  asset_descriptor_base adb_temp{};
  r = c.get_blockchain_storage().get_asset_info(asset_id, adb_temp);
  CHECK_AND_ASSERT_MES(r, false, "get_asset_info failed");
  CHECK_AND_ASSERT_EQ(adb_temp.current_supply, total_asset_amount);

  return true;
}

asset_current_and_total_supplies_comparative_constraints::asset_current_and_total_supplies_comparative_constraints()
{
  {
    auto& adb{m_adbs.at(asset_position::alpha)};

    adb.full_name = "Alpha";
    adb.ticker = "ALPH";
    adb.current_supply = 1;
    adb.total_max_supply = 0;
  }

  {
    auto& adb{m_adbs.at(asset_position::beta)};

    adb.full_name = "Beta";
    adb.ticker = "BETA";
    adb.current_supply = adb.total_max_supply = 1;
  }

  {
    auto &adb{m_adbs.at(asset_position::gamma)};

    adb.full_name = "Gamma";
    adb.ticker = "GAMM";
    adb.current_supply = adb.total_max_supply = 0;
  }

  m_ados_register.at(asset_position::alpha).operation_type = m_ados_register.at(asset_position::beta).operation_type = m_ados_register.at(asset_position::gamma).operation_type =
    ASSET_DESCRIPTOR_OPERATION_REGISTER;

  m_ado_emit.operation_type = ASSET_DESCRIPTOR_OPERATION_EMIT;

  REGISTER_CALLBACK_METHOD(asset_current_and_total_supplies_comparative_constraints, assert_asset_gamma_registered);
  REGISTER_CALLBACK_METHOD(asset_current_and_total_supplies_comparative_constraints, assert_asset_alpha_not_registered);
  REGISTER_CALLBACK_METHOD(asset_current_and_total_supplies_comparative_constraints, assert_asset_beta_registered);
  REGISTER_CALLBACK_METHOD(asset_current_and_total_supplies_comparative_constraints, emit_asset_beta_with_incorrect_supply);
  REGISTER_CALLBACK_METHOD(asset_current_and_total_supplies_comparative_constraints, assert_asset_beta_not_emitted);
  REGISTER_CALLBACK_METHOD(asset_current_and_total_supplies_comparative_constraints, public_burn_asset_beta_with_incorrect_supply);
}

bool asset_current_and_total_supplies_comparative_constraints::generate(std::vector<test_event_entry>& events) const
{
  /* Test ideas:
  * ensure that it's possible to register asset with .current_supply = .total_max_supply = 0;
  * ensure that asset operations in which .current_supply is greater than .total_max_supply are not performed. */

  bool success{};
  GENERATE_ACCOUNT(miner);
  GENERATE_ACCOUNT(alice);
  transaction tx_0{}, tx_1{}, tx_2{}, tx_3{}, tx_4{};
  size_t tx_hardfork_id{};

  m_accounts.push_back(miner);
  m_accounts.push_back(alice);
  m_adbs.at(asset_position::alpha).owner = m_adbs.at(asset_position::beta).owner = m_adbs.at(asset_position::gamma).owner = alice.get_public_address().spend_public_key;
  m_ados_register.at(asset_position::alpha).opt_descriptor = m_adbs.at(asset_position::alpha);
  m_ados_register.at(asset_position::beta).opt_descriptor = m_ado_emit.opt_descriptor = m_adbs.at(asset_position::beta);
  m_ados_register.at(asset_position::gamma).opt_descriptor = m_adbs.at(asset_position::gamma);

  CHECK_AND_ASSERT((*m_ado_emit.opt_descriptor).current_supply <= (*m_ado_emit.opt_descriptor).total_max_supply, false);
  ++(m_ado_emit.opt_descriptor->current_supply);
  CHECK_AND_ASSERT(m_ado_emit.opt_descriptor->current_supply > m_ado_emit.opt_descriptor->total_max_supply, false);

  MAKE_GENESIS_BLOCK(events, blk_0, miner, test_core_time::get_time());
  DO_CALLBACK(events, "configure_core");
  REWIND_BLOCKS_N(events, blk_0r, blk_0, miner, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);

  {
    const auto& top{blk_0r};
    std::vector<tx_source_entry> sources{};
    std::vector<tx_destination_entry> destinations{};

    success = fill_tx_sources_and_destinations(events, top, miner.get_keys(), alice.get_public_address(), MK_TEST_COINS(8), TESTS_DEFAULT_FEE, 0, sources, destinations);
    CHECK_AND_ASSERT_EQ(success, true);
    success = construct_tx(miner.get_keys(), sources, destinations, empty_attachment, tx_0, get_tx_version_and_hardfork_id(get_block_height(top), m_hardforks, tx_hardfork_id), tx_hardfork_id, 0);
    CHECK_AND_ASSERT_EQ(success, true);
  }

  ADD_CUSTOM_EVENT(events, tx_0);
  MAKE_NEXT_BLOCK_TX1(events, blk_1, blk_0r, miner, tx_0);
  REWIND_BLOCKS_N(events, blk_1r, blk_1, miner, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);

  // Alice registers the asset GAMM with .current_supply = 0, .total_max_supply = 0.
  {
    const auto& top{blk_1r};
    std::vector<tx_source_entry> sources{};
    std::vector<tx_destination_entry> destinations{};
    const auto& ado{m_ados_register.at(asset_position::gamma)};
    crypto::secret_key one_time{};
    //size_t hf_n = m_hardforks.get_the_most_recent_hardfork_id_for_height(get_block_height(blk_1r));
    //fill_ado_version_based_onhardfork(ado, hf_n);
    //fill_adb_version_based_onhardfork(*ado.opt_descriptor, hf_n);

    success = fill_tx_sources_and_destinations(events, top, alice.get_keys(), alice.get_public_address(), MK_TEST_COINS(2), TESTS_DEFAULT_FEE, 0, sources, destinations);
    CHECK_AND_ASSERT_EQ(success, true);
    destinations.emplace_back(ado.opt_descriptor->current_supply, alice.get_public_address(), null_pkey);
    CHECK_AND_ASSERT_EQ(ado.opt_descriptor->total_max_supply, 0);
    CHECK_AND_ASSERT_EQ(ado.opt_descriptor->total_max_supply, ado.opt_descriptor->current_supply);
    success = construct_tx(alice.get_keys(), sources, destinations, {ado}, empty_attachment, tx_1, get_tx_version_and_hardfork_id(get_block_height(top), m_hardforks, tx_hardfork_id), tx_hardfork_id, one_time, 0);
    CHECK_AND_ASSERT_EQ(success, true);
  }

  // tx_1 is valid and must be accepted.
  ADD_CUSTOM_EVENT(events, tx_1);
  MAKE_NEXT_BLOCK_TX1(events, blk_2, blk_1r, alice, tx_1);
  REWIND_BLOCKS_N(events, blk_2r, blk_2, alice, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);
  DO_CALLBACK(events, "assert_asset_gamma_registered");

  // Alice registers asset ALPH. Transaction is invalid, because .current_supply > .total_max_supply in the asset base descriptor.
  {
    const auto& top{blk_2r};
    std::vector<tx_source_entry> sources{};
    std::vector<tx_destination_entry> destinations{};
    crypto::secret_key one_time{};
    const auto& ado{m_ados_register.at(asset_position::alpha)};
    //size_t hf_n = m_hardforks.get_the_most_recent_hardfork_id_for_height(get_block_height(blk_2r));
    //fill_ado_version_based_onhardfork(ado, hf_n);
    //fill_adb_version_based_onhardfork(*ado.opt_descriptor, hf_n);

    success = fill_tx_sources_and_destinations(events, top, alice.get_keys(), alice.get_public_address(), MK_TEST_COINS(2), TESTS_DEFAULT_FEE, 0, sources, destinations);
    CHECK_AND_ASSERT_EQ(success, true);
    destinations.emplace_back(ado.opt_descriptor->current_supply, alice.get_public_address(), null_pkey);
    CHECK_AND_ASSERT_MES(ado.opt_descriptor->current_supply > ado.opt_descriptor->total_max_supply, false, "current_supply <= total_max_supply");
    success = construct_tx(alice.get_keys(), sources, destinations, {ado}, empty_attachment, tx_2, get_tx_version_and_hardfork_id(get_block_height(top), m_hardforks, tx_hardfork_id), tx_hardfork_id, one_time, 0);
    CHECK_AND_ASSERT_EQ(success, true);
  }

  /* TODO: tx_1 is invalid and mustn't be accepted.
  DO_CALLBACK(events, "mark_invalid_tx");
  ADD_CUSTOM_EVENT(events, tx_2);
  DO_CALLBACK(events, "assert_asset_alpha_not_registered"); */

  // Alice registers asset BETA. In the asset base descriptor .current_supply <= .total_max_supply.
  {
    const auto& top{blk_2r};

    std::vector<tx_source_entry> sources{};
    std::vector<tx_destination_entry> destinations{};
    crypto::secret_key one_time{};
    const auto& ado{m_ados_register.at(asset_position::beta)};
    //size_t hf_n = m_hardforks.get_the_most_recent_hardfork_id_for_height(get_block_height(blk_2r));
    //fill_ado_version_based_onhardfork(ado, hf_n);
    //fill_adb_version_based_onhardfork(*ado.opt_descriptor, hf_n);

    success = fill_tx_sources_and_destinations(events, top, alice.get_keys(), alice.get_public_address(), MK_TEST_COINS(2), TESTS_DEFAULT_FEE, 0, sources, destinations);
    CHECK_AND_ASSERT_EQ(success, true);
    destinations.emplace_back(ado.opt_descriptor->current_supply, alice.get_public_address(), null_pkey);
    CHECK_AND_ASSERT(ado.opt_descriptor->current_supply <= ado.opt_descriptor->total_max_supply, false);
    success = construct_tx(alice.get_keys(), sources, destinations, {ado}, empty_attachment, tx_3, get_tx_version_and_hardfork_id(get_block_height(top), m_hardforks, tx_hardfork_id), tx_hardfork_id, one_time, 0);
    CHECK_AND_ASSERT_EQ(success, true);
  }

  // tx_3 is valid and must be accepted.
  ADD_CUSTOM_EVENT(events, tx_3);
  MAKE_NEXT_BLOCK_TX1(events, blk_3, blk_2r, alice, tx_3);
  REWIND_BLOCKS_N(events, blk_3r, blk_3, alice, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);
  DO_CALLBACK(events, "assert_asset_beta_registered");

  {
    crypto::public_key beta_asset_id{};
    crypto::point_t point_beta_asset_id{};

    CHECK_AND_ASSERT_MES(get_or_calculate_asset_id(m_ados_register.at(asset_position::beta), &point_beta_asset_id, &beta_asset_id), false, "failed to calculate asset id");
    m_ado_emit.opt_asset_id = beta_asset_id;
  }

  CHECK_AND_ASSERT_GREATER(m_ado_emit.opt_descriptor->current_supply, m_ado_emit.opt_descriptor->total_max_supply);
  // Alice emits asset BETA. The emission is performed through the wallet object. There is no emission, because .current_supply > .total_max_supply in the asset base descriptor.
  DO_CALLBACK(events, "emit_asset_beta_with_incorrect_supply");

  /* Alice emits asset BETA. A transaction is constructed through finalize_tx_param object. This is low-level transaction construction. Transaction muse be rejected by the core, because
  .current_supply > .total_max_supply in the asset base descriptor. */
  {
    const auto& top{blk_3r};
    const auto& ado_register{m_ados_register.at(asset_position::beta)};
    std::vector<tx_source_entry> sources{};
    std::vector<tx_destination_entry> destinations{};
    tx_source_entry source{};
    finalize_tx_param ftp{};
    finalized_tx ftx{};
    
    size_t hf_n = m_hardforks.get_the_most_recent_hardfork_id_for_height(get_block_height(blk_3r));
    //fill_ado_version_based_onhardfork(ado_register, hf_n);
    //fill_adb_version_based_onhardfork(*ado_register.opt_descriptor, hf_n);

    success = fill_tx_sources_and_destinations(events, top, alice.get_keys(), alice.get_public_address(), MK_TEST_COINS(2), TESTS_DEFAULT_FEE, 0, sources, destinations);
    CHECK_AND_ASSERT_EQ(success, true);
    CHECK_AND_ASSERT_GREATER(m_ado_emit.opt_descriptor->current_supply, ado_register.opt_descriptor->current_supply);
    destinations.emplace_back(m_ado_emit.opt_descriptor->current_supply - ado_register.opt_descriptor->current_supply, alice.get_public_address(), null_pkey);

    fill_ado_version_based_onhardfork(m_ado_emit, hf_n);
    if (m_ado_emit.opt_descriptor.has_value())  fill_adb_version_based_onhardfork(*m_ado_emit.opt_descriptor, hf_n);

    ftp.sources = sources;
    ftp.prepared_destinations = destinations;
    ftp.tx_version = get_tx_version(get_block_height(top), m_hardforks);
    ftp.extra = {m_ado_emit};
    ftp.shuffle = true;
    CHECK_AND_ASSERT_GREATER(m_ado_emit.opt_descriptor->current_supply, m_ado_emit.opt_descriptor->total_max_supply);
    success = construct_tx(alice.get_keys(), ftp, ftx);
    CHECK_AND_ASSERT_EQ(success, true);
    tx_4 = ftx.tx;
  }

  DO_CALLBACK(events, "mark_invalid_tx");
  ADD_CUSTOM_EVENT(events, tx_4);
  DO_CALLBACK(events, "assert_asset_beta_not_emitted");
  // Alice burns asset BETA. The public burn is performed through the wallet object. Burn mustn't be performed, because .current_supply > .total_max_supply in the asset base descriptor.
  DO_CALLBACK(events, "public_burn_asset_beta_with_incorrect_supply");

  return true;
}

bool asset_current_and_total_supplies_comparative_constraints::assert_asset_alpha_not_registered(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events) const
{
  const std::shared_ptr alice_wallet{init_playtime_test_wallet_t<tools::wallet2>(events, c, ALICE_ACC_IDX)};
  crypto::public_key alpha_asset_id{};
  const std::string ticker{m_ados_register.at(asset_position::alpha).opt_descriptor->ticker};

  alice_wallet->refresh();

  {
    crypto::point_t point_alpha_asset_id{};

    CHECK_AND_ASSERT_MES(get_or_calculate_asset_id(m_ados_register.at(asset_position::alpha), &point_alpha_asset_id, &alpha_asset_id), false, "failed to calculate asset " + ticker + " id");
  }

  {
    asset_descriptor_base alpha_adb{};

    CHECK_AND_ASSERT_MES(!c.get_blockchain_storage().get_asset_info(alpha_asset_id, alpha_adb), false, "the asset " + ticker + " must not be registered");
  }

  return true;
}

bool asset_current_and_total_supplies_comparative_constraints::assert_asset_beta_registered(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events) const
{
  const std::shared_ptr alice_wallet{init_playtime_test_wallet_t<tools::wallet2>(events, c, ALICE_ACC_IDX)};
  crypto::public_key key_beta_asset_id{};
  const std::string ticker{m_ados_register.at(asset_position::beta).opt_descriptor->ticker};

  alice_wallet->refresh();

  {
    crypto::point_t point_beta_asset_id{};

    CHECK_AND_ASSERT_MES(get_or_calculate_asset_id(m_ados_register.at(asset_position::beta), &point_beta_asset_id, &key_beta_asset_id), false, "failed to calculate asset id");
  }

  {
    asset_descriptor_base beta_adb{};

    CHECK_AND_ASSERT_MES(c.get_blockchain_storage().get_asset_info(key_beta_asset_id, beta_adb), false, "the asset " + ticker + " must not be registered");
  }

  CHECK_AND_ASSERT_MES(alice_wallet->balance(key_beta_asset_id) == 1, false, "Alice has got not exactly 1 " + ticker);

  return true;
}

bool asset_current_and_total_supplies_comparative_constraints::emit_asset_beta_with_incorrect_supply(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events) const
{
  const std::shared_ptr alice_wallet{init_playtime_test_wallet_t<tools::wallet2>(events, c, ALICE_ACC_IDX)};
  std::vector<tx_destination_entry> destinations{};
  crypto::public_key beta_asset_id{};

  {
    asset_descriptor_base adb{};
    CHECK_AND_ASSERT_EQ(c.get_blockchain_storage().get_asset_info(*m_ado_emit.opt_asset_id, adb), true);
  }

  {
    crypto::point_t point_beta_asset_id{};

    CHECK_AND_ASSERT_EQ(get_or_calculate_asset_id(m_ado_emit, &point_beta_asset_id, &beta_asset_id), true);
  }

  CHECK_AND_ASSERT_EQ(*m_ado_emit.opt_asset_id, beta_asset_id);
  alice_wallet->refresh();

  {
    const auto& ado_register{m_ados_register.at(asset_position::beta)};

    CHECK_AND_ASSERT_GREATER(m_ado_emit.opt_descriptor->current_supply, ado_register.opt_descriptor->current_supply);
    destinations.emplace_back(m_ado_emit.opt_descriptor->current_supply - ado_register.opt_descriptor->current_supply, alice_wallet->get_account().get_public_address(), beta_asset_id);
  }

  CHECK_AND_ASSERT_GREATER(m_ado_emit.opt_descriptor->current_supply, m_ado_emit.opt_descriptor->total_max_supply);

  try
  {
    transaction tx{};

    alice_wallet->emit_asset(beta_asset_id, destinations, tx);
  }
  catch (const tools::error::tx_rejected&)
  {
    CHECK_AND_ASSERT_EQ(c.get_pool_transactions_count(), 0);
    return true;
  }
  catch (...)
  {
    return false;
  }

  return false;
}

bool asset_current_and_total_supplies_comparative_constraints::assert_asset_beta_not_emitted(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events) const
{
  const std::shared_ptr alice_wallet{init_playtime_test_wallet_t<tools::wallet2>(events, c, ALICE_ACC_IDX)};
  crypto::public_key beta_asset_id{};
  const auto& register_ado{m_ados_register.at(asset_position::beta)};

  alice_wallet->refresh();

  {
    crypto::point_t point_beta_asset_id{};

    CHECK_AND_ASSERT_MES(get_or_calculate_asset_id(register_ado, &point_beta_asset_id, &beta_asset_id), false, "failed to calculate asset id");
  }

  {
    const uint64_t& current_supply{register_ado.opt_descriptor->current_supply};

    CHECK_AND_ASSERT_MES(alice_wallet->balance(beta_asset_id) == current_supply, false, "Alice has got not exactly " + std::to_string(current_supply) + ' ' + register_ado.opt_descriptor->ticker);
  }
  return true;
}

bool asset_current_and_total_supplies_comparative_constraints::public_burn_asset_beta_with_incorrect_supply(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events) const
{
  const std::shared_ptr alice_wallet{init_playtime_test_wallet_t<tools::wallet2>(events, c, ALICE_ACC_IDX)};
  crypto::public_key beta_asset_id{};

  alice_wallet->refresh();

  {
    crypto::point_t point_beta_asset_id{};

    CHECK_AND_ASSERT_MES(get_or_calculate_asset_id(m_ados_register.at(asset_position::beta), &point_beta_asset_id, &beta_asset_id), false, "failed to calculate asset id");
  }

  try
  {
    transaction tx{};

    alice_wallet->burn_asset(beta_asset_id, m_ado_emit.opt_descriptor->current_supply, tx);
  }
  catch (const std::runtime_error&)
  {
    CHECK_AND_ASSERT_EQ(c.get_pool_transactions_count(), 0);
    return true;
  }

  return false;
}

bool asset_current_and_total_supplies_comparative_constraints::assert_asset_gamma_registered(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events) const
{
  const std::shared_ptr alice_wallet{init_playtime_test_wallet_t<tools::wallet2>(events, c, ALICE_ACC_IDX)};
  crypto::public_key key_gamma_asset_id{};
  const std::string ticker{m_ados_register.at(asset_position::gamma).opt_descriptor->ticker};

  alice_wallet->refresh();

  {
    crypto::point_t point_gamma_asset_id{};

    CHECK_AND_ASSERT_MES(get_or_calculate_asset_id(m_ados_register.at(asset_position::gamma), &point_gamma_asset_id, &key_gamma_asset_id), false, "failed to calculate asset " + ticker + " id");
  }

  {
    asset_descriptor_base gamma_adb{};

    CHECK_AND_ASSERT_MES(c.get_blockchain_storage().get_asset_info(key_gamma_asset_id, gamma_adb), false, "the asset " + ticker + " must be registered");
  }

  CHECK_AND_ASSERT_EQ(alice_wallet->balance(key_gamma_asset_id), m_ados_register.at(asset_position::gamma).opt_descriptor->current_supply);

  return true;
}

//------------------------------------------------------------------------------

several_asset_emit_burn_txs_in_pool::several_asset_emit_burn_txs_in_pool()
{
  REGISTER_CALLBACK_METHOD(several_asset_emit_burn_txs_in_pool, c1);
}

bool several_asset_emit_burn_txs_in_pool::generate(std::vector<test_event_entry>& events) const
{
  //
  // Test idea: make sure two asset emit or two asset burn tx can be added to the pool for the same asset.
  //
  uint64_t ts = test_core_time::get_time();
  m_accounts.resize(TOTAL_ACCS_COUNT);
  account_base& miner_acc = m_accounts[MINER_ACC_IDX]; miner_acc.generate(); miner_acc.set_createtime(ts);
  account_base& alice_acc = m_accounts[ALICE_ACC_IDX]; alice_acc.generate(); alice_acc.set_createtime(ts);

  MAKE_GENESIS_BLOCK(events, blk_0, miner_acc, ts);
  // rebuild genesis miner tx
  std::vector<tx_destination_entry> destinations;
  destinations.emplace_back(MK_TEST_COINS(1), alice_acc.get_public_address());
  destinations.emplace_back(MK_TEST_COINS(1), alice_acc.get_public_address());
  CHECK_AND_ASSERT_MES(replace_coinbase_in_genesis_block(destinations, generator, events, blk_0), false, ""); // leftover amount will be also send to miner

  DO_CALLBACK(events, "configure_core"); // default configure_core callback will initialize core runtime config with m_hardforks
  REWIND_BLOCKS_N_WITH_TIME(events, blk_0r, blk_0, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW + 3);

  DO_CALLBACK(events, "c1");

  return true;
}

bool several_asset_emit_burn_txs_in_pool::c1(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events)
{
  std::shared_ptr<tools::wallet2> miner_wlt = init_playtime_test_wallet(events, c, MINER_ACC_IDX);
  miner_wlt->refresh();
  std::shared_ptr<tools::wallet2> alice_wlt = init_playtime_test_wallet(events, c, ALICE_ACC_IDX);
  alice_wlt->refresh();

  // asset description
  asset_descriptor_base adb{};
  adb.decimal_point = 3;
  adb.total_max_supply = 10'000;
  adb.full_name = "Lets gooo!";
  adb.ticker = "BRNDN";

  uint64_t initial_register_amount = 5'000;

  // 1. Miner registers an asset and sends some initial amount to Alice
  std::vector<tx_destination_entry> destinations;
  destinations.emplace_back(initial_register_amount, m_accounts[ALICE_ACC_IDX].get_public_address(), null_pkey);
  finalized_tx ft{};
  crypto::public_key asset_id{};
  miner_wlt->deploy_new_asset(adb, destinations, ft, asset_id);
  LOG_PRINT_GREEN_L0("Asset " << asset_id << " was successfully deployed with tx " << ft.tx_id);

  // make sure tx was added to the pool, then mine a block to confirm it
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());
  CHECK_AND_ASSERT_MES(mine_next_pow_block_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c), false, "");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  size_t blocks_fetched = 0;
  miner_wlt->refresh(blocks_fetched);
  CHECK_AND_ASSERT_EQ(blocks_fetched, 1);

  // Alice checks her asset balance
  alice_wlt->refresh(blocks_fetched);
  CHECK_AND_ASSERT_EQ(blocks_fetched, 1);
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", initial_register_amount, asset_id, adb.decimal_point), false, "");


  //
  // 2. Miner emits additional amount of the asset in two transactions (all goes to Alice)
  //

  // 2.1 the first emit
  uint64_t additional_emit_amount = 2'500;
  uint64_t total_asset_amount = initial_register_amount + additional_emit_amount;

  destinations.clear();
  destinations.emplace_back(additional_emit_amount, m_accounts[ALICE_ACC_IDX].get_public_address(), null_pkey);
  ft = finalized_tx{};
  miner_wlt->emit_asset(asset_id, destinations, ft);

  // make sure tx was added to the pool
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  // Alice checks her asset balance (including unconfirmed txs)
  alice_wlt->refresh(blocks_fetched);
  CHECK_AND_ASSERT_EQ(blocks_fetched, 0);
  bool stub{};
  alice_wlt->scan_tx_pool(stub);
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", total_asset_amount, asset_id, adb.decimal_point), false, "");

  // 2.2 the second emit
  additional_emit_amount = 2'500;
  total_asset_amount += additional_emit_amount;

  destinations.clear();
  destinations.emplace_back(additional_emit_amount, m_accounts[ALICE_ACC_IDX].get_public_address(), null_pkey);
  ft = finalized_tx{};
  miner_wlt->emit_asset(asset_id, destinations, ft);

  // make sure the second tx was added to the pool
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 2, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  // Alice checks her asset balance (including unconfirmed txs)
  alice_wlt->refresh(blocks_fetched);
  CHECK_AND_ASSERT_EQ(blocks_fetched, 0);
  alice_wlt->scan_tx_pool(stub);
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", total_asset_amount, asset_id, adb.decimal_point), false, "");

  // 2.3
  // mine a block to confirm both txs to make sure everything is alright
  CHECK_AND_ASSERT_MES(mine_next_pow_block_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c), false, "");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  alice_wlt->refresh(blocks_fetched);
  CHECK_AND_ASSERT_EQ(blocks_fetched, 1);

  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", total_asset_amount, asset_id, adb.decimal_point), false, "");


  // make sure these txs are fully confirmed
  CHECK_AND_ASSERT_MES(mine_next_pow_blocks_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c, CURRENCY_MINED_MONEY_UNLOCK_WINDOW), false, "");


  //
  // 3. Alice burns amount of the asset in two transactions
  //
  alice_wlt->refresh();

  // 3.1, the first burn
  uint64_t burn_amount = 2'500;
  total_asset_amount -= burn_amount;

  ft = finalized_tx{};
  alice_wlt->burn_asset(asset_id, burn_amount, ft);

  // make sure tx was added to the pool
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 1, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  alice_wlt->refresh(blocks_fetched);
  CHECK_AND_ASSERT_EQ(blocks_fetched, 0);
  alice_wlt->scan_tx_pool(stub);
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", total_asset_amount, asset_id, adb.decimal_point), false, "");

  // 3.2, the second burn
  burn_amount = 2'500;
  total_asset_amount -= burn_amount;

  ft = finalized_tx{};
  alice_wlt->burn_asset(asset_id, burn_amount, ft);

  // make sure both txs are now in the pool
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 2, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  alice_wlt->refresh(blocks_fetched);
  CHECK_AND_ASSERT_EQ(blocks_fetched, 0);
  alice_wlt->scan_tx_pool(stub);
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", total_asset_amount, asset_id, adb.decimal_point), false, "");

  // 3.3
  // mine a block to confirm both txs to make sure everything is alright
  CHECK_AND_ASSERT_MES(mine_next_pow_block_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c), false, "");
  CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Unexpected number of txs in the pool: " << c.get_pool_transactions_count());

  alice_wlt->refresh(blocks_fetched);
  CHECK_AND_ASSERT_EQ(blocks_fetched, 1);

  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", total_asset_amount, asset_id, adb.decimal_point), false, "");

  // make sure these txs are fully confirmed
  CHECK_AND_ASSERT_MES(mine_next_pow_blocks_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c, CURRENCY_MINED_MONEY_UNLOCK_WINDOW), false, "");

  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", total_asset_amount, asset_id, adb.decimal_point), false, "");

  return true;
}

//------------------------------------------------------------------------------

assets_transfer_with_smallest_amount::assets_transfer_with_smallest_amount()
{
  REGISTER_CALLBACK_METHOD(assets_transfer_with_smallest_amount, c1);
}

bool assets_transfer_with_smallest_amount::generate(std::vector<test_event_entry>& events) const
{
  // 
  // Test idea: make sure wallet correctly handles cases when there's smallest amount being transferred (so it cannot be divided to maintain the mininum required outputs count).
  // Only for HF >= 4
  //

  uint64_t ts = test_core_time::get_time();
  m_accounts.resize(TOTAL_ACCS_COUNT);
  account_base& miner_acc = m_accounts[MINER_ACC_IDX]; miner_acc.generate(); miner_acc.set_createtime(ts);
  account_base& alice_acc = m_accounts[ALICE_ACC_IDX]; alice_acc.generate(); alice_acc.set_createtime(ts);
  MAKE_GENESIS_BLOCK(events, blk_0, miner_acc, ts);
  DO_CALLBACK(events, "configure_core"); // necessary to set m_hardforks

  // HF4 requires tests_random_split_strategy (for 2 outputs minimum)
  test_gentime_settings tgts = generator.get_test_gentime_settings();
  tgts.split_strategy = tests_random_split_strategy;
  generator.set_test_gentime_settings(tgts);

  REWIND_BLOCKS_N_WITH_TIME(events, blk_0r, blk_0, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);

  // make sure HF4 has been activated
  DO_CALLBACK_PARAMS(events, "check_hardfork_active", static_cast<size_t>(ZANO_HARDFORK_04_ZARCANUM));

  // tx_0: miner -> Alice (1 * fee + 2 * fee)
  MAKE_TX(events, tx_0, miner_acc, alice_acc, TESTS_DEFAULT_FEE, blk_0r);
  MAKE_TX(events, tx_1, miner_acc, alice_acc, 2 * TESTS_DEFAULT_FEE, blk_0r);
  MAKE_NEXT_BLOCK_TX_LIST(events, blk_1, blk_0r, miner_acc, std::list<transaction>({tx_0, tx_1}));

  m_alice_initial_balance = TESTS_DEFAULT_FEE + 2 * TESTS_DEFAULT_FEE;

  // rewind blocks
  REWIND_BLOCKS_N_WITH_TIME(events, blk_1r, blk_1, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);

  DO_CALLBACK(events, "c1");

  return true;
}

bool assets_transfer_with_smallest_amount::c1(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events)
{
  bool r = false, stub = false;;
  std::shared_ptr<tools::wallet2> miner_wlt = init_playtime_test_wallet(events, c, m_accounts[MINER_ACC_IDX]);
  miner_wlt->refresh();
  std::shared_ptr<tools::wallet2> alice_wlt = init_playtime_test_wallet(events, c, m_accounts[ALICE_ACC_IDX]);
  alice_wlt->refresh();

  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", m_alice_initial_balance, 0, m_alice_initial_balance, 0, 0), false, "");

  // miner deploys new asset
  asset_descriptor_base adb{};
  adb.total_max_supply = 1;
  adb.full_name = "Patrzcie co spotkalem!";
  adb.ticker = "BOBR";
  adb.decimal_point = 0;
  adb.current_supply = 1;

  std::vector<tx_destination_entry> destinations;
  destinations.emplace_back(adb.total_max_supply, m_accounts[ALICE_ACC_IDX].get_public_address(), null_pkey);

  transaction asset_emission_tx{};
  crypto::public_key asset_id = null_pkey;

  miner_wlt->deploy_new_asset(adb, destinations, asset_emission_tx, asset_id);
  r = mine_next_pow_blocks_in_playtime(miner_wlt->get_account().get_public_address(), c, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_blocks_in_playtime failed");

  alice_wlt->refresh();
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", 1, 0, 1, 0, 0, asset_id), false, "");

  // now Alice has undividable output with amount=1 of the asset
  // and two native outputs: fee (which is exactly what is necessary to a normal transfer) and 2 x fee
  // usually, wallet choose the most apropriate amount for native coins, which is 'fee' for transferring an asset.
  // however, in such a case, asset amount cannot be divided any further, and thus, to maintain the min limit for outputs
  // wallet should choose 2 x fee output instead

  alice_wlt->transfer(1, m_accounts[MINER_ACC_IDX].get_public_address(), asset_id);

  // make sure Alice doesn't have this asset anymore
  alice_wlt->scan_tx_pool(stub);
  alice_wlt->refresh();
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", 0, 0, 0, 0, 1, asset_id), false, "");

  // but miner does
  miner_wlt->scan_tx_pool(stub);
  miner_wlt->refresh();
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*miner_wlt, "Miner", 1, 0, 0, 1, 0, asset_id), false, "");

  // mine some block to unlock
  r = mine_next_pow_blocks_in_playtime(miner_wlt->get_account().get_public_address(), c, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);
  CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_blocks_in_playtime failed");

  // and check balances again
  alice_wlt->scan_tx_pool(stub);
  alice_wlt->refresh();
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", 0, 0, 0, 0, 0, asset_id), false, "");

  miner_wlt->scan_tx_pool(stub);
  miner_wlt->refresh();
  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*miner_wlt, "Miner", 1, 0, 1, 0, 0, asset_id), false, "");

  return true;
}

//------------------------------------------------------------------------------

asset_operations_and_chain_switching::asset_operations_and_chain_switching()
{
  REGISTER_CALLBACK_METHOD(asset_operations_and_chain_switching, c1);
  REGISTER_CALLBACK_METHOD(asset_operations_and_chain_switching, c2);
  REGISTER_CALLBACK_METHOD(asset_operations_and_chain_switching, c3);
}

bool asset_operations_and_chain_switching::generate(std::vector<test_event_entry>& events) const
{
  //
  // Test idea: make shure asset operations are correctly handled during chain switching
  // (test intended for HF4 and later hardforks)
  //

  bool r = false;
  uint64_t ts = test_core_time::get_time();
  m_accounts.resize(TOTAL_ACCS_COUNT);
  account_base& miner_acc = m_accounts[MINER_ACC_IDX]; miner_acc.generate(); miner_acc.set_createtime(ts);
  account_base& alice_acc = m_accounts[ALICE_ACC_IDX]; alice_acc.generate(); alice_acc.set_createtime(ts);
  account_base& bob_acc   = m_accounts[BOB_ACC_IDX];   bob_acc.generate();   bob_acc.set_createtime(ts);
  MAKE_GENESIS_BLOCK(events, blk_0, miner_acc, ts);
  // rebuild genesis miner tx
  //std::vector<tx_destination_entry> destinations;
  //destinations.emplace_back(MK_TEST_COINS(1), alice_acc.get_public_address());
  //destinations.emplace_back(MK_TEST_COINS(1), alice_acc.get_public_address());
  //CHECK_AND_ASSERT_MES(replace_coinbase_in_genesis_block(destinations, generator, events, blk_0), false, ""); // leftover amount will be also send to miner
  DO_CALLBACK(events, "configure_core"); // necessary to set m_hardforks

  // HF4 requires tests_random_split_strategy (for 2 outputs minimum)
  test_gentime_settings tgts = generator.get_test_gentime_settings();
  tgts.split_strategy = tests_random_split_strategy;
  generator.set_test_gentime_settings(tgts);

  REWIND_BLOCKS_N_WITH_TIME(events, blk_0r, blk_0, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);

  // make sure HF4 has been activated
  DO_CALLBACK_PARAMS(events, "check_hardfork_active", static_cast<size_t>(ZANO_HARDFORK_04_ZARCANUM));
  size_t current_hardfork_id = m_hardforks.get_the_most_recent_hardfork_id_for_height(get_block_height(blk_0r));

  // transfer some coins to Alice and Bob so all their UTXO will be ZC
  MAKE_TX(events, tx_a0, miner_acc, alice_acc, MK_TEST_COINS(100), blk_0r);
  MAKE_TX(events, tx_a1, miner_acc, alice_acc, MK_TEST_COINS(100), blk_0r);
  MAKE_TX(events, tx_a2, miner_acc, bob_acc,   MK_TEST_COINS(100), blk_0r);
  MAKE_TX(events, tx_a3, miner_acc, bob_acc,   MK_TEST_COINS(100), blk_0r);
  MAKE_NEXT_BLOCK_TX_LIST(events, blk_1, blk_0r, miner_acc, std::list<transaction>({tx_a0, tx_a1, tx_a2, tx_a3}));

  // add more blocks to unlock Alice and Bob's coins
  REWIND_BLOCKS_N_WITH_TIME(events, blk_1r, blk_1, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);

  // register two assets
  transaction tx_0{};
  asset_descriptor_base adb_0{};
  {
    adb_0.total_max_supply = 911;
    adb_0.full_name = "I identify as an ambulance";
    adb_0.ticker = "WEEWOO";
    adb_0.decimal_point = 0;
    asset_descriptor_operation ado{};
    fill_ado_version_based_onhardfork(ado, current_hardfork_id);
    ado.opt_descriptor = adb_0;
    fill_adb_version_based_onhardfork(*ado.opt_descriptor, current_hardfork_id);
    ado.operation_type = ASSET_DESCRIPTOR_OPERATION_REGISTER;
    if (current_hardfork_id >= ZANO_HARDFORK_05)
      ado.opt_asset_id_salt = crypto::rand<uint32_t>();
    std::vector<tx_destination_entry> destinations;
    destinations.emplace_back(1, m_accounts[ALICE_ACC_IDX].get_public_address(), null_pkey);
    destinations.emplace_back(1, m_accounts[ALICE_ACC_IDX].get_public_address(), null_pkey);
    r = construct_tx_to_key(m_hardforks, events, tx_0, blk_1r, alice_acc, destinations, TESTS_DEFAULT_FEE, 0, 0, std::vector<attachment_v>({ado}));
    CHECK_AND_ASSERT_MES(r, false, "construct_tx_to_key failed");
    // get and store asset id
    r = get_type_in_variant_container(tx_0.extra, ado);
    CHECK_AND_ASSERT_MES(r, false, "get_type_in_variant_container failed");
    r = get_or_calculate_asset_id(ado, nullptr, &m_asset_ids[0]);
    CHECK_AND_ASSERT_MES(r, false, "get_or_calculate_asset_id failed");
  }
  ADD_CUSTOM_EVENT(events, tx_0);

  transaction tx_1{};
  asset_descriptor_base adb_1{};
  {
    adb_1.total_max_supply = 2;
    adb_1.full_name = "I identify as invisible";
    adb_1.ticker = "WHOWHERE";
    adb_1.decimal_point = 0;
    asset_descriptor_operation ado{};
    fill_ado_version_based_onhardfork(ado, current_hardfork_id);
    ado.opt_descriptor = adb_1;
    fill_adb_version_based_onhardfork(*ado.opt_descriptor, current_hardfork_id);
    ado.operation_type = ASSET_DESCRIPTOR_OPERATION_REGISTER;
    if (current_hardfork_id >= ZANO_HARDFORK_05)
      ado.opt_asset_id_salt = crypto::rand<uint32_t>();
    std::vector<tx_destination_entry> destinations;
    destinations.emplace_back(1, m_accounts[BOB_ACC_IDX].get_public_address(), null_pkey);
    destinations.emplace_back(1, m_accounts[BOB_ACC_IDX].get_public_address(), null_pkey);
    r = construct_tx_to_key(m_hardforks, events, tx_1, blk_1r, bob_acc, destinations, TESTS_DEFAULT_FEE, 0, 0, std::vector<attachment_v>({ado}));
    CHECK_AND_ASSERT_MES(r, false, "construct_tx_to_key failed");
    // get and store asset id
    r = get_type_in_variant_container(tx_1.extra, ado);
    CHECK_AND_ASSERT_MES(r, false, "get_type_in_variant_container failed");
    r = get_or_calculate_asset_id(ado, nullptr, &m_asset_ids[1]);
    CHECK_AND_ASSERT_MES(r, false, "get_or_calculate_asset_id failed");
  }
  ADD_CUSTOM_EVENT(events, tx_1);

  // add both asset registering txs to a block
  MAKE_NEXT_BLOCK_TX_LIST(events, blk_2, blk_1r, miner_acc, std::list<transaction>({tx_0, tx_1}));

  // now make an alternative chain from blk_1r and switch to it -- tx_0 and tx_1 should be moved to the pool and both assets unregistered
  MAKE_NEXT_BLOCK(events, blk_2a, blk_1r, miner_acc);
  MAKE_NEXT_BLOCK(events, blk_3a, blk_2a, miner_acc);

  // make sure chain switching occured
  DO_CALLBACK_PARAMS(events, "check_top_block", params_top_block(blk_3a));

  // switch again (rewind blocks from blk_2)
  REWIND_BLOCKS_N_WITH_TIME(events, blk_2r, blk_2, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);
  MAKE_NEXT_BLOCK(events, blk_3, blk_2r, miner_acc);

  // make sure we've succeeded
  DO_CALLBACK_PARAMS(events, "check_top_block", params_top_block(blk_3));

  // check balances
  DO_CALLBACK(events, "c1");

  // emit asset 0
  transaction tx_2{};
  {
    asset_descriptor_operation ado{};
    fill_ado_version_based_onhardfork(ado, current_hardfork_id);
    ado.opt_descriptor = adb_0;  // required only for HF4 (since HF5 descriptor is not required, but it sill can be provided)
    ado.opt_descriptor->current_supply = 2;
    ado.operation_type = ASSET_DESCRIPTOR_OPERATION_EMIT;
    ado.opt_asset_id = m_asset_ids[0];
    std::vector<tx_destination_entry> destinations;
    destinations.emplace_back(900, m_accounts[ALICE_ACC_IDX].get_public_address(), null_pkey);
    r = construct_tx_to_key(m_hardforks, events, tx_2, blk_3, alice_acc, destinations, TESTS_DEFAULT_FEE, 0, 0, std::vector<attachment_v>({ado}));
    CHECK_AND_ASSERT_MES(r, false, "construct_tx_to_key failed");
  }
  ADD_CUSTOM_EVENT(events, tx_2);

  // update asset 1
  transaction tx_3{};
  {
    asset_descriptor_operation ado{};
    fill_ado_version_based_onhardfork(ado, current_hardfork_id);
    adb_1.meta_info = "where is they?";
    adb_1.current_supply = 2;
    ado.opt_descriptor = adb_1;
    ado.operation_type = ASSET_DESCRIPTOR_OPERATION_UPDATE;
    ado.opt_asset_id = m_asset_ids[1];
    std::vector<tx_destination_entry> destinations;
    destinations.emplace_back(TESTS_DEFAULT_FEE, m_accounts[BOB_ACC_IDX].get_public_address());
    r = construct_tx_to_key(m_hardforks, events, tx_3, blk_3, bob_acc, destinations, TESTS_DEFAULT_FEE, 0, 0, std::vector<attachment_v>({ado}));
    CHECK_AND_ASSERT_MES(r, false, "construct_tx_to_key failed");
  }
  ADD_CUSTOM_EVENT(events, tx_3);

  // this time we put tx_2 and tx_3 into an alt block first
  MAKE_NEXT_BLOCK_TX_LIST(events, blk_4a, blk_3, miner_acc, std::list<transaction>({tx_2, tx_3}));
  DO_CALLBACK_PARAMS(events, "check_top_block", params_top_block(blk_4a));

  // now make a main chain from blk_3 and switch to it -- tx_2, tx_3 should be moved to the pool and update/emit operations should be undone
  MAKE_NEXT_BLOCK(events, blk_4, blk_3, miner_acc);
  MAKE_NEXT_BLOCK(events, blk_5, blk_4, miner_acc);

  // make sure chain switching occured
  DO_CALLBACK_PARAMS(events, "check_top_block", params_top_block(blk_5));

  // continue the chain, add a main chain block with tx_2 and tx_3
  MAKE_NEXT_BLOCK_TX_LIST(events, blk_6, blk_5, miner_acc, std::list<transaction>({tx_2, tx_3}));
  REWIND_BLOCKS_N_WITH_TIME(events, blk_6r, blk_6, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW);

  // check balances
  DO_CALLBACK(events, "c2");

  // burn asset 1
  transaction tx_4{};
  {
    asset_descriptor_operation ado{};
    fill_ado_version_based_onhardfork(ado, current_hardfork_id);
    ado.opt_descriptor = adb_1; // only for HF4
    ado.operation_type = ASSET_DESCRIPTOR_OPERATION_PUBLIC_BURN;
    ado.opt_asset_id = m_asset_ids[1];

    // to construct tx for burn operation we need to provide more asset outputs in sources, than in destinations 
    uint64_t asset_burn_amount = 2;
    std::vector<tx_source_entry> sources;
    std::unordered_map<crypto::public_key, uint64_t> amounts;
    amounts[native_coin_asset_id] = TESTS_DEFAULT_FEE;
    amounts[m_asset_ids[1]] = asset_burn_amount;
    r = fill_tx_sources(sources, events, blk_6r, bob_acc.get_keys(), amounts, 0, std::vector<tx_source_entry>(), fts_default, nullptr);
    CHECK_AND_ASSERT_MES(r, false, "fill_tx_sources failed");

    // fill destinations
    std::unordered_map<crypto::public_key, uint64_t> sources_total_amounts;
    get_sources_total_amount(sources, sources_total_amounts);
    std::vector<tx_destination_entry> destinations;
    // add change if necessary
    for(const auto& el : sources_total_amounts)
      if (el.second > amounts[el.first])
        destinations.emplace_back(el.second - amounts[el.first], bob_acc.get_public_address(), el.first);
    // handle minimum outputs limit
    if (destinations.size() < CURRENCY_TX_MIN_ALLOWED_OUTS)
      for(size_t i = 0; i < CURRENCY_TX_MIN_ALLOWED_OUTS - destinations.size(); ++i)
        destinations.emplace_back(0, bob_acc.get_public_address());

    crypto::secret_key sk{};
    size_t tx_hardfork_id{};
    uint64_t tx_version = get_tx_version_and_harfork_id_from_events(events, tx_hardfork_id);
    r = construct_tx(bob_acc.get_keys(), sources, destinations, std::vector<attachment_v>({ado}), empty_attachment, tx_4, tx_version, tx_hardfork_id, sk, 0);
    CHECK_AND_ASSERT_MES(r, false, "construct_tx failed");
  }
  ADD_CUSTOM_EVENT(events, tx_4);

  // tx_4 appears in altchain first
  MAKE_NEXT_BLOCK_TX_LIST(events, blk_7a, blk_6r, miner_acc, std::list<transaction>({tx_4}));
  DO_CALLBACK(events, "check_tx_pool_empty");

  // switch to main chain
  MAKE_NEXT_BLOCK(events, blk_7, blk_6r, miner_acc);
  MAKE_NEXT_BLOCK_TX_LIST(events, blk_8, blk_7, miner_acc, std::list<transaction>({tx_4}));

  // make sure chain switching occured
  DO_CALLBACK_PARAMS(events, "check_top_block", params_top_block(blk_8));

  // check final balances
  DO_CALLBACK(events, "c3");

  return true;
}

bool asset_operations_and_chain_switching::c1(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events)
{
  std::shared_ptr<tools::wallet2> alice_wlt = init_playtime_test_wallet(events, c, m_accounts[ALICE_ACC_IDX]);
  alice_wlt->refresh();
  std::shared_ptr<tools::wallet2> bob_wlt = init_playtime_test_wallet(events, c, m_accounts[BOB_ACC_IDX]);
  bob_wlt->refresh();

  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", 2, 0, 2, 0, 0, m_asset_ids[0], 0), false, "");

  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*bob_wlt, "Bob", 2, 0, 2, 0, 0, m_asset_ids[1], 0), false, "");

  return true;
}

bool asset_operations_and_chain_switching::c2(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events)
{
  bool r = false;
  std::shared_ptr<tools::wallet2> alice_wlt = init_playtime_test_wallet(events, c, m_accounts[ALICE_ACC_IDX]);
  alice_wlt->refresh();
  std::shared_ptr<tools::wallet2> bob_wlt = init_playtime_test_wallet(events, c, m_accounts[BOB_ACC_IDX]);
  bob_wlt->refresh();

  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", 902, 0, 902, 0, 0, m_asset_ids[0], 0), false, "");

  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*bob_wlt, "Bob", 2, 0, 2, 0, 0, m_asset_ids[1], 0), false, "");

  asset_descriptor_base adb{};
  r = c.get_blockchain_storage().get_asset_info(m_asset_ids[1], adb);
  CHECK_AND_ASSERT_MES(r, false, "get_asset_info failed for id " << m_asset_ids[1]);

  // make sure the update operation was successfull
  CHECK_AND_ASSERT_EQ(adb.meta_info, std::string("where is they?"));

  return true;
}

bool asset_operations_and_chain_switching::c3(currency::core& c, size_t ev_index, const std::vector<test_event_entry>& events)
{
  bool r = false;
  std::shared_ptr<tools::wallet2> alice_wlt = init_playtime_test_wallet(events, c, m_accounts[ALICE_ACC_IDX]);
  alice_wlt->refresh();
  std::shared_ptr<tools::wallet2> bob_wlt = init_playtime_test_wallet(events, c, m_accounts[BOB_ACC_IDX]);
  bob_wlt->refresh();

  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*alice_wlt, "Alice", 902, 0, 902, 0, 0, m_asset_ids[0], 0), false, "");

  CHECK_AND_ASSERT_MES(check_balance_via_wallet(*bob_wlt, "Bob", 0, 0, 0, 0, 0, m_asset_ids[1], 0), false, "");

  asset_descriptor_base adb{};
  r = c.get_blockchain_storage().get_asset_info(m_asset_ids[1], adb);
  CHECK_AND_ASSERT_MES(r, false, "get_asset_info failed for id " << m_asset_ids[1]);

  // make sure the update operation was successfull
  CHECK_AND_ASSERT_EQ(adb.meta_info, std::string("where is they?"));

  return true;
}
