text: // Copyright (c) 2011-2017 The Cryptonote developers
  // Copyright (c) 2017-2018 The Circle Foundation & invert Devs
  // Copyright (c) 2018-2019 invert Network & invert Devs
  // Distributed under the MIT/X11 software license, see the accompanying
  // file COPYING or http://www.opensource.org/licenses/mit-license.php.
  
  #include "../IntegrationTestLib/BaseFunctionalTests.h"
  
  #include <thread>
  #include <Logging/ConsoleLogger.h>
  
  #include "CryptoNoteCore/Account.h"
  #include "WalletLegacyObserver.h"
  
  #undef ERROR
  
  using namespace CryptoNote;
  using namespace Logging;
  
  inline std::string shortAddress(const std::string &addr)
  {
    return addr.substr(0, 6);
  }
  
  class MultiVersionTest : Tests::Common::BaseFunctionalTests
  {
  public:
    MultiVersionTest(const CryptoNote::Currency &currency, System::Dispatcher &d, const Tests::Common::BaseFunctionalTestsConfig &config, Logging::ILogger &log) : BaseFunctionalTests(currency, d, config), m_config(config), m_nodeCount(config.daemons.size()), logger(log, "MultiVersion") {}
  
    void run()
    {
      if (m_config.daemons.empty())
      {
        logger(ERROR, BRIGHT_RED) << "No daemons configured, exiting";
        return;
      }
  
      launchTestnet(m_nodeCount, Tests::Common::BaseFunctionalTests::Line);
  
      createWallets();
  
      miningTest();
  
      // create some address for mining
      CryptoNote::AccountBase stashAddress;
      stashAddress.generate();
      auto stashAddressStr = m_currency.accountAddressAsString(stashAddress);
  
      unlockMoney(stashAddressStr);
  
      std::vector<uint64_t> balances;
      for (auto &o : m_observers)
      {
        balances.push_back(o->totalBalance());
      }
  
      printBalances();
  
      // transfer money from each wallet to each other
      for (size_t i = 0; i < m_nodeCount; ++i)
      {
        auto &srcWallet = *m_wallets[i];
        for (size_t wi = 0; wi < m_nodeCount; ++wi)
        {
          if (i != wi)
          {
            CryptoNote::WalletLegacyTransfer transfer;
            transfer.address = m_wallets[wi]->getAddress();
            transfer.amount = (i * 1000 + wi * 100) * m_currency.coin();
            logger(INFO, BRIGHT_YELLOW) << "Sending from " << shortAddress(srcWallet.getAddress()) << " to " << shortAddress(transfer.address) << " amount = " << m_currency.formatAmount(transfer.amount);
            std::vector<CryptoNote::TransactionMessage> messages;
            std::string extraString;
            uint64_t fee = CryptoNote::parameters::MINIMUM_FEE_V2;
            uint64_t mixIn = 0;
            uint64_t unlockTimestamp = 0;
            uint64_t ttl = 0;
            Crypto::SecretKey transactionSK;
            auto txid = srcWallet.sendTransaction(transactionSK, transfer, fee, extraString, mixIn, unlockTimestamp, messages, ttl);
  
            balances[i] -= transfer.amount + m_currency.minimumFee();
            balances[wi] += transfer.amount;
  
            auto res = m_observers[i]->waitSendResult(txid);
  
            if (res)
            {
              logger(ERROR, BRIGHT_RED) << "Failed to send transaction: " << res.message();
              throw std::runtime_error("Failed to send transaction: " + res.message());
            }
  
            logger(INFO) << "Sent successfully";
          }
        }
      }
  
      nodeDaemons[0]->startMining(1, stashAddressStr);
  
      for (size_t i = 0; i < m_nodeCount; ++i)
      {
        uint64_t total;
        logger(INFO) << i << " Expected target balance: " << m_currency.formatAmount(balances[i]);
  
        while ((total = m_wallets[i]->pendingBalance() + m_wallets[i]->actualBalance()) != balances[i])
        {
          logger(INFO) << i << " - total: " << m_currency.formatAmount(total) << ", waiting";
          m_observers[i]->waitTotalBalanceChange();
        }
      }
  
      nodeDaemons[0]->stopMining();
  
      printBalances();
    }
  
    void miningTest()
    {
      auto prevHeight = nodeDaemons[0]->getLocalHeight();
  
      // mine block from each node to each wallet
      for (size_t i = 0; i < m_nodeCount; ++i)
      {
        for (size_t shift = 0; shift < m_nodeCount; ++shift)
        {
          logger(INFO, BRIGHT_YELLOW) << "Starting mining from node " << i << " -> wallet at node " << shift;
  
          while (nodeDaemons[i]->getLocalHeight() != prevHeight)
          {
            std::this_thread::sleep_for(std::chrono::seconds(1));
          }
  
          logger(INFO) << "Starting mining at height " << prevHeight;
          nodeDaemons[i]->startMining(1, m_wallets[shift]->getAddress());
  
          uint64_t newHeight = 0;
  
          while ((newHeight = nodeDaemons[i]->getLocalHeight()) == prevHeight)
          {
            std::this_thread::sleep_for(std::chrono::seconds(1));
          }
  
          nodeDaemons[i]->stopMining();
          logger(INFO) << "Block mined, new height = " << newHeight;
  
          prevHeight = nodeDaemons[i]->getLocalHeight();
  
          logger(INFO, BRIGHT_YELLOW) << "Waiting for balance to change";
          auto res = m_observers[shift]->waitPendingBalanceChangeFor(std::chrono::seconds(m_currency.difficultyTarget() * 5));
  
          if (!res.first)
          {
            logger(ERROR, BRIGHT_RED) << "Timeout waiting for balance to change!";
            throw std::runtime_error("Timeout");
          }
        }
      }
    }
  
    void unlockMoney(const std::string &miningAddress)
    {
      logger(INFO, BRIGHT_YELLOW) << "Starting to mine blocks to unlock money";
  
      // unlock money
      nodeDaemons[0]->startMining(1, miningAddress);
      for (auto &o : m_observers)
      {
        o->waitActualBalanceChange();
      }
      nodeDaemons[0]->stopMining();
      logger(INFO, BRIGHT_YELLOW) << "Unlocked all, waiting for all daemons to sync blockchain";
  
      auto minerHeight = nodeDaemons[0]->getLocalHeight();
      logger(INFO) << "Miner height: " << minerHeight;
  
      bool unsynced = true;
  
      while (unsynced)
      {
        unsynced = false;
        for (auto &o : m_observers)
        {
          if (o->getCurrentHeight() < minerHeight)
          {
            unsynced = true;
            break;
          }
        }
  
        if (unsynced)
        {
          std::this_thread::sleep_for(std::chrono::seconds(1));
        }
      }
  
      logger(INFO) << "OK";
    }
  
    void printBalances()
    {
      for (auto &w : m_wallets)
      {
        auto pending = w->pendingBalance();
        auto actual = w->actualBalance();
  
        logger(INFO, BRIGHT_GREEN) << "Wallet " << shortAddress(w->getAddress()) << ": " << m_currency.formatAmount(actual) << " / " << m_currency.formatAmount(pending) << " total = " << m_currency.formatAmount(pending + actual);
      }
    }
  
    void createWallets()
    {
      for (auto &daemon : nodeDaemons)
      {
        std::unique_ptr<INode> node;
        std::unique_ptr<IWalletLegacy> wallet;
  
        daemon->makeINode(node);
        makeWallet(wallet, node);
  
        std::unique_ptr<WalletLegacyObserver> observer(new WalletLegacyObserver);
  
        wallet->addObserver(observer.get());
  
        m_nodes.push_back(std::move(node));
        m_wallets.push_back(std::move(wallet));
        m_observers.push_back(std::move(observer));
      }
    }
  
    void startShiftedMining(size_t shift)
    {
      for (size_t i = 0; i < m_nodeCount; ++i)
      {
        nodeDaemons[i]->startMining(1, m_wallets[(i + shift) % m_nodeCount]->getAddress());
      }
    }
  
    void waitAllPendingBalancesChange()
    {
      for (auto &o : m_observers)
      {
        o->waitPendingBalanceChange();
      }
    }
  
    void stopAllMining()
    {
      for (auto &n : nodeDaemons)
      {
        n->stopMining();
      }
    }
  
  private:
    const size_t m_nodeCount;
    const Tests::Common::BaseFunctionalTestsConfig &m_config;
  
    std::vector<std::unique_ptr<INode>> m_nodes;
    std::vector<std::unique_ptr<IWalletLegacy>> m_wallets;
    std::vector<std::unique_ptr<WalletLegacyObserver>> m_observers;
  
    Logging::LoggerRef logger;
  };
  
  void testMultiVersion(const CryptoNote::Currency &currency, System::Dispatcher &d, const Tests::Common::BaseFunctionalTestsConfig &config)
  {
    Logging::ConsoleLogger log;
    MultiVersionTest test(currency, d, config, log);
    test.run();
  }
  