Stefan d0389a6305 feat(Wallet) show balance cache for chain native token
Quick integration of fetching balance in the current chart view.

The proper implementation requires refactoring the QML views to separate
price chart, that depends only on the token and chain, from balance that
depends on token, chain and address.

Closes: #7662
2022-11-16 13:07:46 +01:00

432 lines
19 KiB
C++

#include <StatusGo/SignalsManager.h>
#include <StatusGo/Accounts/AccountsAPI.h>
#include <StatusGo/Wallet/Transfer/Event.h>
#include <StatusGo/Wallet/WalletApi.h>
#include <StatusGo/Wallet/wallet_types.h>
#include <StatusGo/Metadata/api_response.h>
#include <Onboarding/Accounts/AccountsService.h>
#include <Onboarding/Accounts/AccountsServiceInterface.h>
#include <Onboarding/Common/Constants.h>
#include <Onboarding/OnboardingController.h>
#include <ScopedTestAccount.h>
#include <StatusGo/Utils.h>
#include <chrono>
#include <gtest/gtest.h>
namespace Wallet = Status::StatusGo::Wallet;
namespace Accounts = Status::StatusGo::Accounts;
namespace Utils = Status::StatusGo::Utils;
namespace General = Status::Constants::General;
namespace fs = std::filesystem;
using namespace std::chrono_literals;
/// \warning for now this namespace contains integration test to check the basic assumptions of status-go while building the C++ wrapper.
/// \warning the tests depend on IO and are not deterministic, fast, focused or reliable. They are here for validation only
/// \todo after status-go API coverage all the integration tests should go away and only test the thin wrapper code
namespace Status::Testing
{
TEST(WalletApi, TestGetDerivedAddressesForPath_FromRootAccount)
{
constexpr auto testRootAccountName = "test_root_account-name";
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName);
const auto walletAccount = testAccount.firstWalletAccount();
const auto rootAccount = testAccount.onboardingController()->accountsService()->getLoggedInAccount();
ASSERT_EQ(rootAccount.address, walletAccount.derivedFrom.value());
const auto testPath = General::PathWalletRoot;
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(
testAccount.hashedPassword(), walletAccount.derivedFrom.value(), testPath, 3, 1);
// Check that accounts are generated in memory and none is saved
const auto updatedAccounts = Accounts::getAccounts();
ASSERT_EQ(updatedAccounts.size(), 2);
ASSERT_EQ(derivedAddresses.size(), 3);
auto defaultWalletAccountIt =
std::find_if(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; });
ASSERT_NE(defaultWalletAccountIt, derivedAddresses.end());
const auto& defaultWalletAccount = *defaultWalletAccountIt;
ASSERT_EQ(defaultWalletAccount.path, General::PathDefaultWallet);
ASSERT_EQ(defaultWalletAccount.address, walletAccount.address);
ASSERT_TRUE(defaultWalletAccount.alreadyCreated);
ASSERT_EQ(1, std::count_if(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) {
return a.alreadyCreated;
}));
// all hasActivity are false
ASSERT_TRUE(
std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.hasActivity; }));
// all address are valid
ASSERT_TRUE(std::none_of(
derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.address.get().isEmpty(); }));
}
TEST(Accounts, TestGetDerivedAddressesForPath_AfterLogin)
{
constexpr auto testRootAccountName = "test-generate_account_with_derived_path-name";
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName);
testAccount.logOut();
auto accountsService = std::make_shared<Onboarding::AccountsService>();
auto result = accountsService->init(testAccount.testDataDir());
ASSERT_TRUE(result);
auto onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
EXPECT_EQ(onboarding->getOpenedAccounts().size(), 1);
auto accounts = accountsService->openAndListAccounts();
ASSERT_GT(accounts.size(), 0);
int accountLoggedInCount = 0;
QObject::connect(onboarding.get(), &Onboarding::OnboardingController::accountLoggedIn, [&accountLoggedInCount]() {
accountLoggedInCount++;
});
bool accountLoggedInError = false;
QObject::connect(onboarding.get(),
&Onboarding::OnboardingController::accountLoginError,
[&accountLoggedInError](const QString& error) {
accountLoggedInError = true;
qDebug() << "Failed logging in in test" << test_info_->name() << "with error:" << error;
});
auto ourAccountRes = std::find_if(accounts.begin(), accounts.end(), [testRootAccountName](const auto& a) {
return a.name == testRootAccountName;
});
auto errorString = accountsService->login(*ourAccountRes, testAccount.password());
ASSERT_EQ(errorString.length(), 0);
testAccount.processMessages(1000, [&accountLoggedInCount, &accountLoggedInError]() {
return accountLoggedInCount == 0 && !accountLoggedInError;
});
ASSERT_EQ(accountLoggedInCount, 1);
ASSERT_EQ(accountLoggedInError, 0);
const auto testPath = General::PathWalletRoot;
const auto walletAccount = testAccount.firstWalletAccount();
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(
testAccount.hashedPassword(), walletAccount.derivedFrom.value(), testPath, 3, 1);
// Check that accounts are generated in memory and none is saved
const auto updatedAccounts = Accounts::getAccounts();
ASSERT_EQ(updatedAccounts.size(), 2);
ASSERT_EQ(derivedAddresses.size(), 3);
auto defaultWalletAccountIt =
std::find_if(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; });
ASSERT_NE(defaultWalletAccountIt, derivedAddresses.end());
const auto& defaultWalletAccount = *defaultWalletAccountIt;
ASSERT_EQ(defaultWalletAccount.path, General::PathDefaultWallet);
ASSERT_EQ(defaultWalletAccount.address, walletAccount.address);
ASSERT_TRUE(defaultWalletAccount.alreadyCreated);
ASSERT_EQ(1, std::count_if(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) {
return a.alreadyCreated;
}));
// all hasActivity are false
ASSERT_TRUE(
std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.hasActivity; }));
// all address are valid
ASSERT_TRUE(std::none_of(
derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.address.get().isEmpty(); }));
}
/// getDerivedAddresses@api.go fron statys-go has a special case when requesting the 6 path will return only one account
TEST(WalletApi, TestGetDerivedAddressesForPath_FromWalletAccount_FirstLevel_SixPathSpecialCase)
{
constexpr auto testRootAccountName = "test_root_account-name";
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName);
const auto walletAccount = testAccount.firstWalletAccount();
const auto testPath = General::PathDefaultWallet;
const auto derivedAddresses =
Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(), walletAccount.address, testPath, 4, 1);
ASSERT_EQ(derivedAddresses.size(), 1);
const auto& onlyAccount = derivedAddresses[0];
// all alreadyCreated are false
ASSERT_FALSE(onlyAccount.alreadyCreated);
ASSERT_EQ(onlyAccount.path, General::PathDefaultWallet);
}
TEST(WalletApi, TestGetDerivedAddressesForPath_FromWalletAccount_SecondLevel)
{
constexpr auto testRootAccountName = "test_root_account-name";
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName);
const auto walletAccount = testAccount.firstWalletAccount();
const auto firstLevelPath = General::PathDefaultWallet;
const auto firstLevelAddresses =
Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(), walletAccount.address, firstLevelPath, 4, 1);
const auto testPath = Accounts::DerivationPath{General::PathDefaultWallet.get() + u"/0"_qs};
const auto derivedAddresses =
Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(), walletAccount.address, testPath, 4, 1);
ASSERT_EQ(derivedAddresses.size(), 4);
// all alreadyCreated are false
ASSERT_TRUE(
std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; }));
// all hasActivity are false
ASSERT_TRUE(
std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.hasActivity; }));
// all address are valid
ASSERT_TRUE(std::none_of(
derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.address.get().isEmpty(); }));
ASSERT_TRUE(std::all_of(derivedAddresses.begin(), derivedAddresses.end(), [testPath](const auto& a) {
return a.path.get().startsWith(testPath.get());
}));
}
TEST(WalletApi, TestGetEthereumChains)
{
ScopedTestAccount testAccount(test_info_->name());
auto networks = Wallet::getEthereumChains(false);
ASSERT_GT(networks.size(), 0);
const auto& network = networks[0];
ASSERT_FALSE(network.chainName.isEmpty());
ASSERT_TRUE(network.rpcUrl.isValid());
}
TEST(WalletApi, TestGetTokens)
{
ScopedTestAccount testAccount(test_info_->name());
auto networks = Wallet::getEthereumChains(false);
ASSERT_GT(networks.size(), 0);
auto mainNetIt =
std::find_if(networks.begin(), networks.end(), [](const auto& n) { return n.chainName == "Mainnet"; });
ASSERT_NE(mainNetIt, networks.end());
const auto& mainNet = *mainNetIt;
auto tokens = Wallet::getTokens(mainNet.chainId);
auto sntIt = std::find_if(tokens.begin(), tokens.end(), [](const auto& t) { return t.symbol == "SNT"; });
ASSERT_NE(sntIt, tokens.end());
const auto& snt = *sntIt;
ASSERT_EQ(snt.chainId, mainNet.chainId);
ASSERT_TRUE(snt.color.isValid());
}
TEST(WalletApi, TestGetTokensBalancesForChainIDs)
{
ScopedTestAccount testAccount(test_info_->name());
auto networks = Wallet::getEthereumChains(false);
ASSERT_GT(networks.size(), 1);
auto mainNetIt =
std::find_if(networks.begin(), networks.end(), [](const auto& n) { return n.chainName == "Mainnet"; });
ASSERT_NE(mainNetIt, networks.end());
const auto& mainNet = *mainNetIt;
auto mainTokens = Wallet::getTokens(mainNet.chainId);
auto sntMainIt =
std::find_if(mainTokens.begin(), mainTokens.end(), [](const auto& t) { return t.symbol == "SNT"; });
ASSERT_NE(sntMainIt, mainTokens.end());
const auto& sntMain = *sntMainIt;
auto testNetIt =
std::find_if(networks.begin(), networks.end(), [](const auto& n) { return n.chainName == "Ropsten"; });
ASSERT_NE(testNetIt, networks.end());
const auto& testNet = *testNetIt;
auto testTokens = Wallet::getTokens(testNet.chainId);
auto sntTestIt =
std::find_if(testTokens.begin(), testTokens.end(), [](const auto& t) { return t.symbol == "STT"; });
ASSERT_NE(sntTestIt, testTokens.end());
const auto& sntTest = *sntTestIt;
auto testAddress = testAccount.firstWalletAccount().address;
auto balances = Wallet::getTokensBalancesForChainIDs(
{mainNet.chainId, testNet.chainId}, {testAddress}, {sntMain.address, sntTest.address});
ASSERT_GT(balances.size(), 0);
ASSERT_TRUE(balances.contains(testAddress));
const auto& addressBalance = balances[testAddress];
ASSERT_GT(addressBalance.size(), 0);
ASSERT_TRUE(addressBalance.contains(sntMain.address));
ASSERT_EQ(toQString(addressBalance.at(sntMain.address)), "0");
ASSERT_TRUE(addressBalance.contains(sntTest.address));
ASSERT_EQ(toQString(addressBalance.at(sntTest.address)), "0");
}
TEST(WalletApi, TestGetTokensBalancesForChainIDs_WatchOnlyAccount)
{
ScopedTestAccount testAccount(test_info_->name());
const auto newTestAccountName = u"test_watch_only-name"_qs;
Accounts::addAccountWatch(Accounts::EOAddress("0xdb5ac1a559b02e12f29fc0ec0e37be8e046def49"),
newTestAccountName,
QColor("fuchsia"),
u""_qs);
const auto updatedAccounts = Accounts::getAccounts();
ASSERT_EQ(updatedAccounts.size(), 3);
const auto newAccountIt =
std::find_if(updatedAccounts.begin(), updatedAccounts.end(), [&newTestAccountName](const auto& a) {
return a.name == newTestAccountName;
});
ASSERT_NE(newAccountIt, updatedAccounts.end());
const auto& newAccount = *newAccountIt;
auto networks = Wallet::getEthereumChains(false);
ASSERT_GT(networks.size(), 1);
auto mainNetIt =
std::find_if(networks.begin(), networks.end(), [](const auto& n) { return n.chainName == "Mainnet"; });
ASSERT_NE(mainNetIt, networks.end());
const auto& mainNet = *mainNetIt;
auto mainTokens = Wallet::getTokens(mainNet.chainId);
auto sntMainIt =
std::find_if(mainTokens.begin(), mainTokens.end(), [](const auto& t) { return t.symbol == "SNT"; });
ASSERT_NE(sntMainIt, mainTokens.end());
const auto& sntMain = *sntMainIt;
auto balances = Wallet::getTokensBalancesForChainIDs({mainNet.chainId}, {newAccount.address}, {sntMain.address});
ASSERT_GT(balances.size(), 0);
ASSERT_TRUE(balances.contains(newAccount.address));
const auto& addressBalance = balances[newAccount.address];
ASSERT_GT(addressBalance.size(), 0);
ASSERT_TRUE(addressBalance.contains(sntMain.address));
ASSERT_GT(addressBalance.at(sntMain.address), 0);
}
// TODO: this is a debugging test. Augment it with local Ganache environment to have a reliable test
TEST(WalletApi, TestCheckRecentHistory)
{
ScopedTestAccount testAccount(test_info_->name());
// Add watch account
const auto newTestAccountName = u"test_watch_only-name"_qs;
Accounts::addAccountWatch(Accounts::EOAddress("0xe74E17D586227691Cb7b64ed78b1b7B14828B034"),
newTestAccountName,
QColor("fuchsia"),
u""_qs);
const auto updatedAccounts = Accounts::getAccounts();
ASSERT_EQ(updatedAccounts.size(), 3);
const auto newAccountIt =
std::find_if(updatedAccounts.begin(), updatedAccounts.end(), [newTestAccountName](const auto& a) {
return a.name == newTestAccountName;
});
ASSERT_NE(newAccountIt, updatedAccounts.end());
const auto& newAccount = *newAccountIt;
bool startedTransferFetching = false;
bool historyReady = false;
QObject::connect(StatusGo::SignalsManager::instance(),
&StatusGo::SignalsManager::wallet,
testAccount.app(),
[&startedTransferFetching, &historyReady](QSharedPointer<StatusGo::EventData> data) {
Wallet::Transfer::Event event = data->eventInfo();
if(event.type == Wallet::Transfer::Events::FetchingRecentHistory)
startedTransferFetching = true;
else if(event.type == Wallet::Transfer::Events::RecentHistoryReady)
historyReady = true;
// Wallet::Transfer::Events::NewTransfers might not be emitted if there is no intermediate transfers
});
Wallet::checkRecentHistory({newAccount.address});
testAccount.processMessages(50000, [&historyReady]() { return !historyReady; });
ASSERT_TRUE(startedTransferFetching);
ASSERT_TRUE(historyReady);
}
// TODO: this is a debugging test. Augment it with local Ganache environment to have a reliable test
TEST(WalletApi, TestGetBalanceHistory)
{
ScopedTestAccount testAccount(test_info_->name());
// Add watch account
const auto newTestAccountName = u"test_watch_only-name"_qs;
Accounts::addAccountWatch(Accounts::EOAddress("0x473780deAF4a2Ac070BBbA936B0cdefe7F267dFc"),
newTestAccountName,
QColor("fuchsia"),
u""_qs);
const auto updatedAccounts = Accounts::getAccounts();
ASSERT_EQ(updatedAccounts.size(), 3);
auto networks = Wallet::getEthereumChains(false);
ASSERT_GT(networks.size(), 0);
auto mainNetIt =
std::find_if(networks.begin(), networks.end(), [](const auto& n) { return n.chainName == "Mainnet"; });
ASSERT_NE(mainNetIt, networks.end());
const auto& mainNet = *mainNetIt;
auto tokens = Wallet::getTokens(mainNet.chainId);
const auto newAccountIt =
std::find_if(updatedAccounts.begin(), updatedAccounts.end(), [newTestAccountName](const auto& a) {
return a.name == newTestAccountName;
});
ASSERT_NE(newAccountIt, updatedAccounts.end());
const auto& newAccount = *newAccountIt;
auto testIntervals = {Wallet::BalanceHistoryTimeInterval::BalanceHistory7Hours,
Wallet::BalanceHistoryTimeInterval::BalanceHistory1Month,
Wallet::BalanceHistoryTimeInterval::BalanceHistory6Months,
Wallet::BalanceHistoryTimeInterval::BalanceHistory1Year,
Wallet::BalanceHistoryTimeInterval::BalanceHistoryAllTime};
std::map<Wallet::BalanceHistoryTimeInterval, QString> testIntervalsStrs{
{Wallet::BalanceHistoryTimeInterval::BalanceHistory7Hours, "7H"},
{Wallet::BalanceHistoryTimeInterval::BalanceHistory1Month, "1M"},
{Wallet::BalanceHistoryTimeInterval::BalanceHistory6Months, "6M"},
{Wallet::BalanceHistoryTimeInterval::BalanceHistory1Year, "1Y"},
{Wallet::BalanceHistoryTimeInterval::BalanceHistoryAllTime, "All"}};
for(const auto& historyInterval : testIntervals)
{
// TODO: next `mainNet.nativeCurrencySymbol`, later `tokens.symbol`
auto balanceHistory = Wallet::getBalanceHistory(mainNet.chainId, newAccount.address, historyInterval);
ASSERT_TRUE(balanceHistory.size() > 0);
auto weiToEth = [](const StatusGo::Wallet::BigInt& wei) -> double {
StatusGo::Wallet::BigInt q; // wei / eth
StatusGo::Wallet::BigInt r; // wei % eth
auto weiD = StatusGo::Wallet::BigInt("1000000000000000000");
boost::multiprecision::divide_qr(wei, weiD, q, r);
StatusGo::Wallet::BigInt rSzabos; // r / szaboD
StatusGo::Wallet::BigInt qSzabos; // r % szaboD
auto szaboD = StatusGo::Wallet::BigInt("1000000000000");
boost::multiprecision::divide_qr(r, szaboD, qSzabos, rSzabos);
return q.convert_to<double>() + (qSzabos.convert_to<double>() / ((weiD / szaboD).convert_to<double>()));
};
QFile file(QString("/tmp/balance_history-%s.csv").arg(testIntervalsStrs[historyInterval]));
if(file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QTextStream out(&file);
out << "Balance, Timestamp" << Qt::endl;
for(int i = 0; i < balanceHistory.size(); ++i)
{
out << weiToEth(balanceHistory[i].value) << "," << balanceHistory[i].time.toSecsSinceEpoch()
<< Qt::endl;
}
}
file.close();
}
}
} // namespace Status::Testing