chore(CPP) add debugging test for token balance history

Also

- added debugging test for `checkRecentHistory` from the attempt to
use transactions to restore balance
- small improvements that might clarify better the issues reported about
running under linux issues (didn't test them)
- fix issues found while reviewing the code.
- add support for custom infura token to be used in development

Updates #7662
This commit is contained in:
Stefan 2022-10-14 13:38:41 +03:00 committed by Stefan Dunca
parent cc9f83650c
commit 5450384a34
19 changed files with 693 additions and 158 deletions

View File

@ -4,6 +4,7 @@
#include <QByteArray>
#include <QColor>
#include <QDateTime>
#include <QString>
#include <QUrl>
@ -97,8 +98,25 @@ struct adl_serializer<std::optional<T>>
static void from_json(const json& j, std::optional<T>& opt)
{
if(j.is_null())
opt = std::nullopt;
else
opt.emplace(j.get<T>());
}
};
template <>
struct adl_serializer<QDateTime>
{
static void to_json(json& j, const QDateTime& dt)
{
j = dt.toSecsSinceEpoch();
}
static void from_json(const json& j, QDateTime& dt)
{
dt = QDateTime::fromSecsSinceEpoch(j.get<qint64>());
}
};
} // namespace nlohmann

View File

@ -10,6 +10,8 @@
#include <optional>
#include <QtGlobal>
std::optional<QString> getDataFromFile(const fs::path& path)
{
QFile jsonFile{Status::toQString(path)};
@ -363,7 +365,9 @@ QJsonObject AccountsService::getDefaultNodeConfig(const QString& installationId)
{
auto templateNodeConfigJsonStr = getDataFromFile(":/Status/StaticConfig/node-config.json").value();
auto fleetJson = getDataFromFile(":/Status/StaticConfig/fleets.json").value();
auto infuraKey = getDataFromFile(":/Status/StaticConfig/infura_key").value();
auto envInfuraKey = qEnvironmentVariable("INFURA_TOKEN");
auto infuraKey =
envInfuraKey.isEmpty() ? getDataFromFile(":/Status/StaticConfig/infura_key").value() : envInfuraKey;
auto templateDefaultNetworksJson = getDataFromFile(":/Status/StaticConfig/default-networks.json").value();
QString defaultNetworksContent = templateDefaultNetworksJson.replace("%INFURA_TOKEN_RESOLVED%", infuraKey);

View File

@ -35,12 +35,12 @@ ScopedTestAccount::ScopedTestAccount(const std::string& tempTestSubfolderName,
char* args[] = {appName.data()};
m_app = std::make_unique<QCoreApplication>(argc, reinterpret_cast<char**>(args));
m_testFolderPath = m_fusedTestFolder->tempFolder() / Constants::statusGoDataDirName;
fs::create_directory(m_testFolderPath);
m_dataDirPath = m_fusedTestFolder->tempFolder() / Constants::statusGoDataDirName;
fs::create_directory(m_dataDirPath);
// Setup accounts
auto accountsService = std::make_shared<Onboarding::AccountsService>();
auto result = accountsService->init(m_testFolderPath);
auto result = accountsService->init(m_dataDirPath);
if(!result)
{
throw std::runtime_error("ScopedTestAccount - Failed to create temporary test account");
@ -59,12 +59,14 @@ ScopedTestAccount::ScopedTestAccount(const std::string& tempTestSubfolderName,
}
int accountLoggedInCount = 0;
QObject::connect(m_onboarding.get(), &Onboarding::OnboardingController::accountLoggedIn, [&accountLoggedInCount]() {
accountLoggedInCount++;
});
QObject::connect(m_onboarding.get(),
&Onboarding::OnboardingController::accountLoggedIn,
m_app.get(),
[&accountLoggedInCount]() { accountLoggedInCount++; });
bool accountLoggedInError = false;
QObject::connect(m_onboarding.get(),
&Onboarding::OnboardingController::accountLoginError,
m_app.get(),
[&accountLoggedInError]() { accountLoggedInError = true; });
// Create Accounts
@ -99,14 +101,15 @@ ScopedTestAccount::ScopedTestAccount(const std::string& tempTestSubfolderName,
processMessages(2000, [accountLoggedInCount]() { return accountLoggedInCount == 0; });
if(accountLoggedInCount != 1)
{
throw std::runtime_error("ScopedTestAccount - missing confirmation of account creation");
}
if(accountLoggedInError)
{
throw std::runtime_error("ScopedTestAccount - account loggedin error");
}
if(accountLoggedInCount != 1)
{
throw std::runtime_error("ScopedTestAccount - missing confirmation of account creation");
}
}
ScopedTestAccount::~ScopedTestAccount()
@ -123,10 +126,12 @@ void ScopedTestAccount::processMessages(size_t maxWaitTimeMillis, std::function<
auto remainingIterations = maxWaitTime / iterationSleepTime;
while(remainingIterations-- > 0 && shouldWaitUntilTimeout())
{
std::this_thread::sleep_for(iterationSleepTime);
QCoreApplication::sendPostedEvents();
std::this_thread::sleep_for(iterationSleepTime);
}
// Provide chance to exit slot processing after we set the condition that might trigger shouldWaitUntilTimeout to false
std::this_thread::sleep_for(iterationSleepTime);
}
void ScopedTestAccount::logOut()
@ -171,7 +176,12 @@ Onboarding::OnboardingController* ScopedTestAccount::onboardingController() cons
const std::filesystem::path& ScopedTestAccount::fusedTestFolder() const
{
return m_testFolderPath;
return m_fusedTestFolder->tempFolder();
}
const std::filesystem::path& ScopedTestAccount::testDataDir() const
{
return m_dataDirPath;
}
} // namespace Status::Testing

View File

@ -58,12 +58,19 @@ public:
Status::Onboarding::OnboardingController* onboardingController() const;
/// Temporary test folder that is deleted when class instance goes out of scope
const std::filesystem::path& fusedTestFolder() const;
const std::filesystem::path& testDataDir() const;
QCoreApplication* app()
{
return m_app.get();
};
private:
std::unique_ptr<AutoCleanTempTestDir> m_fusedTestFolder;
std::unique_ptr<QCoreApplication> m_app;
std::filesystem::path m_testFolderPath;
std::filesystem::path m_dataDirPath;
std::shared_ptr<Status::Onboarding::OnboardingController> m_onboarding;
std::function<bool()> m_checkIfShouldContinue;

View File

@ -93,9 +93,9 @@ TEST(OnboardingModule, TestCreateAndLoginAccountEndToEnd)
auto remainingIterations = maxWaitTime / iterationSleepTime;
while(remainingIterations-- > 0 && accountLoggedInCount == 0)
{
std::this_thread::sleep_for(iterationSleepTime);
QCoreApplication::sendPostedEvents();
std::this_thread::sleep_for(iterationSleepTime);
}
EXPECT_EQ(accountLoggedInCount, 1);
@ -115,20 +115,12 @@ TEST(OnboardingModule, TestLoginEndToEnd)
QObject::connect(StatusGo::SignalsManager::instance(),
&StatusGo::SignalsManager::nodeLogin,
[&createAndLogin](const QString& error) {
if(error.isEmpty())
{
if(createAndLogin)
{
createAndLogin = false;
}
else
createAndLogin = true;
}
if(error.isEmpty()) createAndLogin = !createAndLogin;
});
constexpr auto accountName = "TestLoginAccountName";
ScopedTestAccount testAccount(test_info_->name(), accountName);
testAccount.processMessages(1000, [createAndLogin]() { return !createAndLogin; });
testAccount.processMessages(1000, [&createAndLogin]() { return !createAndLogin; });
ASSERT_TRUE(createAndLogin);
testAccount.logOut();
@ -138,7 +130,7 @@ TEST(OnboardingModule, TestLoginEndToEnd)
// Setup accounts
auto accountsService = std::make_shared<Onboarding::AccountsService>();
auto result = accountsService->init(testAccount.fusedTestFolder());
auto result = accountsService->init(testAccount.testDataDir());
ASSERT_TRUE(result);
auto onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
@ -156,8 +148,8 @@ TEST(OnboardingModule, TestLoginEndToEnd)
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;
accountLoggedInError = true;
});
auto ourAccountRes =
@ -165,11 +157,11 @@ TEST(OnboardingModule, TestLoginEndToEnd)
auto errorString = accountsService->login(*ourAccountRes, testAccount.password());
ASSERT_EQ(errorString.length(), 0);
testAccount.processMessages(1000, [accountLoggedInCount, accountLoggedInError]() {
testAccount.processMessages(1000, [&accountLoggedInCount, &accountLoggedInError]() {
return accountLoggedInCount == 0 && !accountLoggedInError;
});
ASSERT_EQ(accountLoggedInCount, 1);
ASSERT_EQ(accountLoggedInError, 0);
ASSERT_EQ(accountLoggedInCount, 1);
}
TEST(OnboardingModule, TestLoginEndToEnd_WrongPassword)
@ -180,7 +172,7 @@ TEST(OnboardingModule, TestLoginEndToEnd_WrongPassword)
testAccount.logOut();
auto accountsService = std::make_shared<Onboarding::AccountsService>();
auto result = accountsService->init(testAccount.fusedTestFolder());
auto result = accountsService->init(testAccount.testDataDir());
ASSERT_TRUE(result);
auto onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
auto accounts = accountsService->openAndListAccounts();
@ -205,7 +197,7 @@ TEST(OnboardingModule, TestLoginEndToEnd_WrongPassword)
auto errorString = accountsService->login(*ourAccountRes, testAccount.password() + "extra");
ASSERT_EQ(errorString.length(), 0);
testAccount.processMessages(1000, [accountLoggedInCount, accountLoggedInError]() {
testAccount.processMessages(1000, [&accountLoggedInCount, &accountLoggedInError]() {
return accountLoggedInCount == 0 && !accountLoggedInError;
});
ASSERT_EQ(accountLoggedInError, 1);

View File

@ -114,6 +114,8 @@ target_sources(${PROJECT_NAME}
src/StatusGo/SignalsManager.h
src/StatusGo/SignalsManager.cpp
src/StatusGo/StatusGoEvent.h
src/StatusGo/StatusGoEvent.cpp
src/StatusGo/Settings/SettingsAPI.h
src/StatusGo/Settings/SettingsAPI.cpp
@ -129,4 +131,7 @@ target_sources(${PROJECT_NAME}
src/StatusGo/Wallet/wallet_types.h
src/StatusGo/Wallet/WalletApi.h
src/StatusGo/Wallet/WalletApi.cpp
src/StatusGo/Wallet/Transfer/Event.h
src/StatusGo/Wallet/Transfer/Event.cpp
)

View File

@ -159,7 +159,8 @@ class CallPrivateRpcError : public std::runtime_error
{
public:
CallPrivateRpcError(const CallPrivateRpcErrorResponse error)
: std::runtime_error("CallPrivateRPC@status-go failed")
: std::runtime_error("CallPrivateRPC@status-go failed - [" + std::to_string(error.error.code) +
"]: " + error.error.message)
, m_error(std::move(error))
{ }

View File

@ -1,9 +1,15 @@
#include "SignalsManager.h"
#include "StatusGoEvent.h"
#include <QtConcurrent>
#include <libstatus.h>
#include <chrono>
#include <thread>
using json = nlohmann::json;
using namespace std::string_literals;
namespace Status::StatusGo
@ -11,6 +17,11 @@ namespace Status::StatusGo
std::map<std::string, SignalType> SignalsManager::signalMap;
EventData::EventData(nlohmann::json eventInfo, bool error)
: m_eventInfo(std::move(eventInfo))
, m_hasError(error)
{ }
// TODO: make me thread safe or better refactor into broadcasting mechanism
SignalsManager* SignalsManager::instance()
{
@ -21,9 +32,13 @@ SignalsManager* SignalsManager::instance()
SignalsManager::SignalsManager()
: QObject(nullptr)
{
// Don't allow async signal processing in attept to debug the the linux running tests issue
m_threadPool.setMaxThreadCount(1);
SetSignalEventCallback((void*)&SignalsManager::signalCallback);
signalMap = {{"node.ready"s, SignalType::NodeReady},
signalMap = {
{"node.ready"s, SignalType::NodeReady},
{"node.started"s, SignalType::NodeStarted},
{"node.stopped"s, SignalType::NodeStopped},
{"node.login"s, SignalType::NodeLogin},
@ -33,49 +48,58 @@ SignalsManager::SignalsManager()
{"discovery.stopped"s, SignalType::DiscoveryStopped},
{"discovery.summary"s, SignalType::DiscoverySummary},
{"mediaserver.started"s, SignalType::MailserverStarted},
{"mailserver.changed"s, SignalType::MailserverChanged},
{"mailserver.available"s, SignalType::MailserverAvailable},
{"history.request.started"s, SignalType::HistoryRequestStarted},
{"history.request.batch.processed"s, SignalType::HistoryRequestBatchProcessed},
{"history.request.completed"s, SignalType::HistoryRequestCompleted}};
{"history.request.completed"s, SignalType::HistoryRequestCompleted},
{"wallet"s, SignalType::WalletEvent},
};
}
SignalsManager::~SignalsManager() { }
void SignalsManager::processSignal(const QString& statusSignal)
void SignalsManager::processSignal(const char* statusSignalData)
{
// TODO: overkill, use some kind of message broker
using namespace std::chrono_literals;
auto dataStrPtr = std::make_shared<std::string>(statusSignalData);
m_threadPool.start(QRunnable::create([dataStrPtr, this]() {
try
{
QJsonParseError json_error;
const QJsonDocument signalEventDoc(QJsonDocument::fromJson(statusSignal.toUtf8(), &json_error));
if(json_error.error != QJsonParseError::NoError)
StatusGoEvent event = json::parse(*dataStrPtr);
if(event.error != std::nullopt)
{
qWarning() << "Invalid signal received";
qWarning() << "Error in signal" << event.type.c_str() << "; error" << event.error.value();
// TODO report event error
return;
}
decode(signalEventDoc.object());
QString signalError;
if(event.event.contains("error")) signalError = event.event["error"].get<QString>();
dispatch(event.type, std::move(event.event), signalError);
}
catch(const std::exception& e)
{
qWarning() << "Error decoding signal, err: ", e.what();
return;
qWarning() << "Error decoding signal, err: " << e.what() << "; signal data: " << dataStrPtr->c_str();
}
}));
}
void SignalsManager::decode(const QJsonObject& signalEvent)
void SignalsManager::dispatch(const std::string& type, json signalEvent, const QString& signalError)
{
SignalType signalType(Unknown);
auto signalName = signalEvent["type"].toString().toStdString();
if(!signalMap.contains(signalName))
if(!signalMap.contains(type))
{
qWarning() << "Unknown signal received: " << signalName.c_str();
qWarning() << "Unknown signal received: " << type.c_str();
return;
}
signalType = signalMap[signalName];
auto signalError = signalEvent["event"]["error"].toString();
signalType = signalMap[type];
switch(signalType)
{
// TODO: create extractor functions like in nim
@ -89,22 +113,21 @@ void SignalsManager::decode(const QJsonObject& signalEvent)
break;
case DiscoveryStarted: emit discoveryStarted(signalError); break;
case DiscoveryStopped: emit discoveryStopped(signalError); break;
case DiscoverySummary: emit discoverySummary(signalEvent["event"].toArray().count(), signalError); break;
case DiscoverySummary: emit discoverySummary(signalEvent.array().size(), signalError); break;
case MailserverStarted: emit mailserverStarted(signalError); break;
case MailserverChanged: emit mailserverChanged(signalError); break;
case MailserverAvailable: emit mailserverAvailable(signalError); break;
case HistoryRequestStarted: emit historyRequestStarted(signalError); break;
case HistoryRequestBatchProcessed: emit historyRequestBatchProcessed(signalError); break;
case HistoryRequestCompleted: emit historyRequestCompleted(signalError); break;
case WalletEvent: emit wallet(EventDataQPtr(new EventData(std::move(signalEvent), false))); break;
case Unknown: assert(false); break;
}
}
void SignalsManager::signalCallback(const char* data)
{
// TODO: overkill, use some kind of message broker
auto dataStrPtr = std::make_shared<QString>(data);
QFuture<void> future =
QtConcurrent::run([dataStrPtr]() { SignalsManager::instance()->processSignal(*dataStrPtr); });
SignalsManager::instance()->processSignal(data);
}
} // namespace Status::StatusGo

View File

@ -1,6 +1,9 @@
#pragma once
#include <QObject>
#include <QThreadPool>
#include <nlohmann/json.hpp>
namespace Status::StatusGo
{
@ -18,14 +21,36 @@ enum SignalType
DiscoveryStopped,
DiscoverySummary,
MailserverStarted,
MailserverChanged,
MailserverAvailable,
HistoryRequestStarted,
HistoryRequestBatchProcessed,
HistoryRequestCompleted
HistoryRequestCompleted,
WalletEvent
};
class EventData final : public QObject
{
Q_OBJECT
public:
explicit EventData(nlohmann::json eventInfo, bool error);
const nlohmann::json& eventInfo() const
{
return m_eventInfo;
};
private:
nlohmann::json m_eventInfo;
bool m_hasError;
};
using EventDataQPtr = QSharedPointer<Status::StatusGo::EventData>;
/*!
\todo refactor into a message broker helper to be used by specific service APIs to deliver signals
as part of the specific StatusGoAPI service
@ -38,9 +63,10 @@ class SignalsManager final : public QObject
public:
static SignalsManager* instance();
void processSignal(const QString& ev);
void processSignal(const char* statusSignalData);
signals:
// TODO: move all signals to deliver EventData, distributing this way data processing to the consumer
void nodeReady(const QString& error);
void nodeStarted(const QString& error);
void nodeStopped(const QString& error);
@ -51,6 +77,7 @@ signals:
void discoveryStopped(const QString& error);
void discoverySummary(size_t nodeCount, const QString& error);
void mailserverStarted(const QString& error);
void mailserverChanged(const QString& error);
void mailserverAvailable(const QString& error);
@ -58,6 +85,8 @@ signals:
void historyRequestBatchProcessed(const QString& error);
void historyRequestCompleted(const QString& error);
void wallet(QSharedPointer<Status::StatusGo::EventData> eventData);
private:
explicit SignalsManager();
~SignalsManager();
@ -65,7 +94,10 @@ private:
private:
static std::map<std::string, SignalType> signalMap;
static void signalCallback(const char* data);
void decode(const QJsonObject& signalEvent);
void dispatch(const std::string& type, nlohmann::json signalEvent, const QString& signalError);
QThreadPool m_threadPool;
};
} // namespace Status::StatusGo

View File

@ -0,0 +1,23 @@
#include "StatusGoEvent.h"
namespace Status::StatusGo
{
constexpr auto statusGoEventErrorKey = "error";
void to_json(json& j, const StatusGoEvent& d)
{
j = {{"type", d.type}, {"event", d.event}};
if(d.error != std::nullopt) j[statusGoEventErrorKey] = d.error.value();
}
void from_json(const json& j, StatusGoEvent& d)
{
j.at("type").get_to(d.type);
j.at("event").get_to(d.event);
if(j.contains(statusGoEventErrorKey)) j.at(statusGoEventErrorKey).get_to(d.error);
}
}

View File

@ -0,0 +1,31 @@
#pragma once
#include <Accounts/accounts_types.h>
#include <Helpers/NamedType.h>
#include <Helpers/conversions.h>
#include <Wallet/BigInt.h>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
namespace Status::StatusGo
{
/// \see status-go's EventType@events.go in services/wallet/transfer module
using StatusGoEventType = Helpers::NamedType<QString, struct StatusGoEventTypeTag>;
/// \see status-go's Event@events.go in services/wallet/transfer module
struct StatusGoEvent
{
std::string type;
json event;
std::optional<QString> error;
};
void to_json(json& j, const StatusGoEvent& d);
void from_json(const json& j, StatusGoEvent& d);
} // namespace Status::StatusGo

View File

@ -48,6 +48,9 @@ struct adl_serializer<GoWallet::BigInt>
static void from_json(const json& j, GoWallet::BigInt& num)
{
if(j.is_number())
num = GoWallet::BigInt(j.get<long long>());
else
num = GoWallet::BigInt(j.get<std::string>());
}
};

View File

@ -0,0 +1,12 @@
#include "Event.h"
namespace Status::StatusGo::Wallet::Transfer
{
const EventType Events::NewTransfers{"new-transfers"};
const EventType Events::FetchingRecentHistory{"recent-history-fetching"};
const EventType Events::RecentHistoryReady{"recent-history-ready"};
const EventType Events::FetchingHistoryError{"fetching-history-error"};
const EventType Events::NonArchivalNodeDetected{"non-archival-node-detected"};
} // namespace Status::StatusGo::Wallet

View File

@ -0,0 +1,39 @@
#pragma once
#include <Accounts/accounts_types.h>
#include <Helpers/NamedType.h>
#include <Helpers/conversions.h>
#include <Wallet/BigInt.h>
#include <nlohmann/json.hpp>
namespace Status::StatusGo::Wallet::Transfer
{
/// \see status-go's EventType@events.go in services/wallet/transfer module
using EventType = Helpers::NamedType<QString, struct TransferEventTypeTag>;
struct Events
{
static const EventType NewTransfers;
static const EventType FetchingRecentHistory;
static const EventType RecentHistoryReady;
static const EventType FetchingHistoryError;
static const EventType NonArchivalNodeDetected;
};
/// \see status-go's Event@events.go in services/wallet/transfer module
struct Event
{
EventType type;
std::optional<StatusGo::Wallet::BigInt> blockNumber;
/// Accounts are \c null in case of error. In the error case message contains the error message
std::optional<std::vector<StatusGo::Accounts::EOAddress>> accounts;
QString message;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Event, type, blockNumber, accounts, message);
} // namespace Status

View File

@ -83,8 +83,8 @@ Tokens getTokens(const ChainID& chainId)
}
TokenBalances getTokensBalancesForChainIDs(const std::vector<ChainID>& chainIds,
const std::vector<Accounts::EOAddress> accounts,
const std::vector<Accounts::EOAddress> tokens)
const std::vector<Accounts::EOAddress>& accounts,
const std::vector<Accounts::EOAddress>& tokens)
{
std::vector<json> params = {chainIds, accounts, tokens};
json inputJson = {{"jsonrpc", "2.0"}, {"method", "wallet_getTokensBalancesForChainIDs"}, {"params", params}};
@ -108,4 +108,26 @@ TokenBalances getTokensBalancesForChainIDs(const std::vector<ChainID>& chainIds,
return resultData;
}
std::vector<TokenBalanceHistory>
getBalanceHistoryOnChain(Accounts::EOAddress account, const std::chrono::seconds& secondsToNow, int sampleCount)
{
std::vector<json> params = {account, secondsToNow.count(), sampleCount};
json inputJson = {{"jsonrpc", "2.0"}, {"method", "wallet_getBalanceHistoryOnChain"}, {"params", params}};
auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str());
const auto resultJson = json::parse(result);
checkPrivateRpcCallResultAndReportError(resultJson);
return resultJson.get<CallPrivateRpcResponse>().result;
}
void checkRecentHistory(const std::vector<Accounts::EOAddress>& accounts)
{
std::vector<json> params = {accounts};
json inputJson = {{"jsonrpc", "2.0"}, {"method", "wallet_checkRecentHistory"}, {"params", params}};
auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str());
const auto resultJson = json::parse(result);
checkPrivateRpcCallResultAndReportError(resultJson);
}
} // namespace Status::StatusGo::Wallet

View File

@ -11,8 +11,12 @@
#include "Types.h"
#include <nlohmann/json.hpp>
#include <vector>
#include <QDateTime>
namespace Accounts = Status::StatusGo::Accounts;
namespace Status::StatusGo::Wallet
@ -52,9 +56,31 @@ NetworkConfigurations getEthereumChains(bool onlyEnabled);
Tokens getTokens(const ChainID& chainId);
using TokenBalances = std::map<Accounts::EOAddress, std::map<Accounts::EOAddress, BigInt>>;
/// \note status-go's @api.go -> <xx>@<xx>.go
/// \note status-go's API -> GetTokensBalancesForChainIDs<@api.go
/// \throws \c CallPrivateRpcError
TokenBalances getTokensBalancesForChainIDs(const std::vector<ChainID>& chainIds,
const std::vector<Accounts::EOAddress> accounts,
const std::vector<Accounts::EOAddress> tokens);
const std::vector<Accounts::EOAddress>& accounts,
const std::vector<Accounts::EOAddress>& tokens);
struct TokenBalanceHistory
{
BigInt value;
QDateTime time;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(TokenBalanceHistory, value, time)
/// \warning it relies on the stored transaction data fetched by calling \c checkRecentHistory
/// \todo reconsider \c checkRecentHistory dependency
///
/// \see checkRecentHistory
/// \note status-go's API -> GetBalanceHistoryOnChain@api.go
/// \throws \c CallPrivateRpcError
std::vector<TokenBalanceHistory>
getBalanceHistoryOnChain(Accounts::EOAddress account, const std::chrono::seconds& secondsToNow, int sampleCount);
/// \note status-go's API -> CheckRecentHistory@api.go
/// \throws \c CallPrivateRpcError
void checkRecentHistory(const std::vector<Accounts::EOAddress>& accounts);
} // namespace Status::StatusGo::Wallet

View File

@ -0,0 +1,153 @@
# Implement historical balance
Task [#7662](https://github.com/status-im/status-desktop/issues/7662)
## Summary
User story: as a user I want to see the historical balance of a specific token in the Asset view Balance tab
UI design [Figma](https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?node-id=6770%3A76490)
## Considerations
Technical requirements
- New UI View
- Time interval tabs
- [ ] Save last selected tab? Which is the default?
- The price history uses All Time as default
- Show the graph for the selected tab
- New Controller fetch API entry and async Response
- `status-go` API call to fetch token historical balance
### Assumptions
Data source
- The balance history is unique for each address and token.
- It is represented by the blockchain transactions (in/out) for a specific address and token
- The original data resides in blocks on blockchain as transactions
- Direct data fetch is not possible so using infura APIs that caches the data is the alternative
- We already fetch part of the data when displaying `Activity` as a list of transactions
- Constructing the data forward (from the block 0) is not practical.
- Given that we have to show increasing period of activity starting from 1H to All time best is to fetch the data backwards
- Start going from current balance to the past and inverse the operations while going back
- Q
- [ ] What information do we get with each transaction request?
Caching the data
- It is a requirement to reduce the number of requests and improve future performance
- Q
- [ ] How do we match the data points not to duplicate?
- [ ] What unique key should we use as primary key in the `balance_cache` DB?
- How about `chainID` + `address` + `token`?
- [x] Is our sqlite DB the best time series data store we have at hand?
- Yes, great integration
- [x] Do we adjust the data points later on or just add forever?
- For start we just add. We might prune old data later on based on age and granularity
- [ ] What do we already cache with transaction history API?
- [x] What granularity do we cache?
- All we fetch for now. Can't see yet how to manage gaps beside the current merging blocks implementation
- [x] What is the cache eviction policy?
- None yet, always growing. Can't see concerns of cache size yet due to scarce transactions for regular users
- Can be done later as an optimization
View
- Q
- [ ] How do we represent data? Each transaction is a point and interpolate in between?
- [x] Are "Overview" and "Activity" tabs affected somehow?
- It seems not, quite isolated and no plan to update transactions for now. Can be done later as an optimization
### Dependencies
Infura API
### Constraints
- Data points granularity should be optimal
- In theory it doesn't make sense to show more than 1 data point per pixel
- In practice it make no sense to show more than 1 data point per few pixels
- Q
- [ ] What is the optimal granularity? Time range based?
- [ ] How do we prune the data points to match the optimal granularity? Do we need this in practice? Can we just show all the data points?
- [ ] How about having 10 transactions in a day and showing the ALL time plot
- Q
- [ ] What is the interval for Showing All Time case? Starting from the first transaction? Or from the first block?
- It seems the current price history start from 2010
## Development
Data scattered in `transfers` DB table and `balanceCache`
Q:
- [ ] Unify it, update? Is there a relation in between these two sources?
- [ ] How to fill the gaps?
### Reference
Token historical price PRs
- status-desktop [#7599](https://github.com/status-im/status-desktop/pull/7599/files)
- See `TokenMarketValuesStore` for converting from raw data to QML friendly JS data: `ui/imports/shared/stores/TokenMarketValuesStore.qml`
- status-go [#2882](https://github.com/status-im/status-go/pull/2882/files)
Building blocks
- status-go
- Transaction history, common data: `CheckRecentHistory` - `services/wallet/transfer/controller.go`
- setup a running task that fetches balances for chainIDs and accounts
- caches balances in memory see `balanceCache`
- Q
- [x] Queries block ranges?
- Retrieve all "old" block ranges (from block - to block) in table `blocks_ranges` to match the specified network and address. Also order by block from
- See `BlocksRange {from, to *big.Int}` - `services/wallet/transfer/block.go`
- Merge them in continuous blocks simplifying the ranges
- See `TestGetNewRanges` - `services/wallet/transfer/block_test.go`
- Deletes the simplified and add new ranges to the table
- [x] When are block ranges fragmented?
- `setInitialBlocksRange` for all the accounts and watched addresses added initially from block 0 to latest - `services/wallet/transfer/block.go`
- Q:
- [ ] What does latest means? Latest block in the chain?
- `eth_getBlockByNumber` in `HeaderByNumber` - `services/wallet/chain/client.go`
- Event `EventFetchingRecentHistory` processes the history and updates the block ranges via `ProcessBlocks`-> `upsertRange` - `services/wallet/transfer/commands.go`
- [x] Reactor loops?
- Reactor listens to new blocks and stores transfers into the database.
- `ERC20TransfersDownloader`, `ETHDownloader`
- Updates **in memory cache of balances** maps (an address to a map of a block number and the balance of this particular address)
- `balanceCache` - `services/wallet/transfer/balance_cache.go`
- [x] Why `watchAccountsChanges`?
- To update the reactor when list of accounts is updated (added/removed)
- [ ] How do we identify a balance cache entry?
- NIM
- QML
- `RootStore` - `ui/imports/shared/stores/RootStore.qml`
- Entry point for accessing underlying NIM model
- `RightTabView` - `ui/app/AppLayouts/Wallet/views/RightTabView.qml`
- Contains the "Asset" tab from which user selects the token to see history for
- StatusQ's `StatusChartPanel` - `ui/StatusQ/src/StatusQ/Components/StatusChartPanel.qml`
- `Chart` - `ui/StatusQ/src/StatusQ/Components/private/chart/Chart.qml`
- Canvas based drawing using `Chart.js`
- `HistoryView` calls `RootStore.getTransfersByAddress` to retrieve list of transfers which are delivered as async signals
- `getTransfersByAddress` will call `status-go`'s `GetTransfersByAddress` which will update `blocks` and `transfers` DB tables
- Q:
- [ ] How is this overlapping with the check recent history?
### TODO
- [x] New `status-go` API to fetch balance history from existing cache using rules (granularity, time range)
- ~~In the meantime use fetch transactions to populate the cache for testing purposes~~
- This doesn't work because of the internal transactions that don't show up in the transaction history
- [ ] Sample blocks using `eth_getBalance`
- [ ] Extend Controller to Expose balance history API
- [ ] Implement UI View to use the new API and display the graph of existing data
- [ ] Extend `status-go`
- [ ] Control fetching of new balances for history purpose
- [ ] Premature optimization?
- DB cache eviction policy
- DB cache granularity control
- [ ] Add balance cache DB and sync it with in memory cache
- [ ] Retrieve from the DB if missing exists optimization
- [ ] Extend UI View with controls for time range

View File

@ -18,12 +18,11 @@ namespace Status::Testing {
TEST(MessagingApi, TestStartMessaging)
{
bool nodeReady = false;
QObject::connect(StatusGo::SignalsManager::instance(), &StatusGo::SignalsManager::nodeReady, [&nodeReady](const QString& error) {
if(error.isEmpty()) {
if(nodeReady) {
nodeReady = false;
} else
nodeReady = true;
QObject::connect(
StatusGo::SignalsManager::instance(), &StatusGo::SignalsManager::nodeReady, [&nodeReady](const QString& error) {
if(error.isEmpty())
{
nodeReady = !nodeReady;
}
});

View File

@ -1,17 +1,22 @@
#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/AccountsServiceInterface.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;
@ -21,10 +26,13 @@ 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 {
namespace Status::Testing
{
TEST(WalletApi, TestGetDerivedAddressesForPath_FromRootAccount)
{
@ -37,25 +45,30 @@ TEST(WalletApi, TestGetDerivedAddressesForPath_FromRootAccount)
const auto testPath = General::PathWalletRoot;
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(),
walletAccount.derivedFrom.value(), testPath, 3, 1);
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; });
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; }));
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; }));
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::none_of(
derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.address.get().isEmpty(); }));
}
TEST(Accounts, TestGetDerivedAddressesForPath_AfterLogin)
@ -66,7 +79,7 @@ TEST(Accounts, TestGetDerivedAddressesForPath_AfterLogin)
testAccount.logOut();
auto accountsService = std::make_shared<Onboarding::AccountsService>();
auto result = accountsService->init(testAccount.fusedTestFolder());
auto result = accountsService->init(testAccount.testDataDir());
ASSERT_TRUE(result);
auto onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
EXPECT_EQ(onboarding->getOpenedAccounts().size(), 1);
@ -79,16 +92,20 @@ TEST(Accounts, TestGetDerivedAddressesForPath_AfterLogin)
accountLoggedInCount++;
});
bool accountLoggedInError = false;
QObject::connect(onboarding.get(), &Onboarding::OnboardingController::accountLoginError, [&accountLoggedInError](const QString& error) {
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 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]() {
testAccount.processMessages(1000, [&accountLoggedInCount, &accountLoggedInError]() {
return accountLoggedInCount == 0 && !accountLoggedInError;
});
ASSERT_EQ(accountLoggedInCount, 1);
@ -97,26 +114,30 @@ TEST(Accounts, TestGetDerivedAddressesForPath_AfterLogin)
const auto testPath = General::PathWalletRoot;
const auto walletAccount = testAccount.firstWalletAccount();
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(),
walletAccount.derivedFrom.value(),
testPath, 3, 1);
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; });
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; }));
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; }));
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::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
@ -129,8 +150,8 @@ TEST(WalletApi, TestGetDerivedAddressesForPath_FromWalletAccount_FirstLevel_SixP
const auto testPath = General::PathDefaultWallet;
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(),
walletAccount.address, testPath, 4, 1);
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
@ -145,22 +166,27 @@ TEST(WalletApi, TestGetDerivedAddressesForPath_FromWalletAccount_SecondLevel)
const auto walletAccount = testAccount.firstWalletAccount();
const auto firstLevelPath = General::PathDefaultWallet;
const auto firstLevelAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(),
walletAccount.address, firstLevelPath, 4, 1);
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);
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; }));
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; }));
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()); }));
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)
@ -169,7 +195,7 @@ TEST(WalletApi, TestGetEthereumChains)
auto networks = Wallet::getEthereumChains(false);
ASSERT_GT(networks.size(), 0);
const auto &network = networks[0];
const auto& network = networks[0];
ASSERT_FALSE(network.chainName.isEmpty());
ASSERT_TRUE(network.rpcUrl.isValid());
}
@ -180,15 +206,16 @@ TEST(WalletApi, TestGetTokens)
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"; });
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;
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"; });
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;
const auto& snt = *sntIt;
ASSERT_EQ(snt.chainId, mainNet.chainId);
ASSERT_TRUE(snt.color.isValid());
}
@ -200,32 +227,35 @@ TEST(WalletApi, TestGetTokensBalancesForChainIDs)
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"; });
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;
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"; });
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;
const auto& sntMain = *sntMainIt;
auto testNetIt = std::find_if(networks.begin(), networks.end(), [](const auto &n){ return n.chainName == "Ropsten"; });
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;
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"; });
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;
const auto& sntTest = *sntTestIt;
auto testAddress = testAccount.firstWalletAccount().address;
auto balances = Wallet::getTokensBalancesForChainIDs({mainNet.chainId, testNet.chainId},
{testAddress},
{sntMain.address, sntTest.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];
const auto& addressBalance = balances[testAddress];
ASSERT_GT(addressBalance.size(), 0);
ASSERT_TRUE(addressBalance.contains(sntMain.address));
@ -240,43 +270,148 @@ 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);
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) {
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;
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"; });
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;
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"; });
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;
const auto& sntMain = *sntMainIt;
auto balances = Wallet::getTokensBalancesForChainIDs({mainNet.chainId},
{newAccount.address},
{sntMain.address});
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];
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
// "{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"no tokens for this network"}}"
// 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());
} // namespace
// 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, TestGetBalanceHistoryOnChain)
{
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);
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 = {std::chrono::round<std::chrono::seconds>(1h),
std::chrono::round<std::chrono::seconds>(std::chrono::days(1)),
std::chrono::round<std::chrono::seconds>(std::chrono::days(7)),
std::chrono::round<std::chrono::seconds>(std::chrono::months(1)),
std::chrono::round<std::chrono::seconds>(std::chrono::months(6)),
std::chrono::round<std::chrono::seconds>(std::chrono::years(1)),
std::chrono::round<std::chrono::seconds>(std::chrono::years(100))};
auto sampleCount = 10;
for(const auto& historyDuration : testIntervals)
{
auto balanceHistory = Wallet::getBalanceHistoryOnChain(newAccount.address, historyDuration, sampleCount);
ASSERT_TRUE(balanceHistory.size() > 0); // TODO: we get one extra, match sample size
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-%1s.csv").arg(historyDuration.count()));
if(file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QTextStream out(&file);
out << "Balance, Timestamp" << Qt::endl;
for(int i = balanceHistory.size() - 1; i >= 0; --i)
{
out << weiToEth(balanceHistory[i].value) << "," << balanceHistory[i].time.toSecsSinceEpoch()
<< Qt::endl;
}
}
file.close();
sampleCount += 10;
}
}
} // namespace Status::Testing