diff --git a/libs/Helpers/src/Helpers/conversions.h b/libs/Helpers/src/Helpers/conversions.h index 07ef25bbc4..6807a6678b 100644 --- a/libs/Helpers/src/Helpers/conversions.h +++ b/libs/Helpers/src/Helpers/conversions.h @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -97,7 +98,24 @@ struct adl_serializer> static void from_json(const json& j, std::optional& opt) { - opt.emplace(j.get()); + if(j.is_null()) + opt = std::nullopt; + else + opt.emplace(j.get()); + } +}; + +template <> +struct adl_serializer +{ + 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()); } }; diff --git a/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp b/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp index 23dfbde4ec..100205e452 100644 --- a/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp +++ b/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp @@ -10,6 +10,8 @@ #include +#include + std::optional 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); diff --git a/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.cpp b/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.cpp index d074c72b4f..d12f85d516 100644 --- a/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.cpp +++ b/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.cpp @@ -35,12 +35,12 @@ ScopedTestAccount::ScopedTestAccount(const std::string& tempTestSubfolderName, char* args[] = {appName.data()}; m_app = std::make_unique(argc, reinterpret_cast(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(); - 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 diff --git a/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.h b/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.h index e50df2b017..16844e0b3f 100644 --- a/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.h +++ b/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.h @@ -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 m_fusedTestFolder; std::unique_ptr m_app; - std::filesystem::path m_testFolderPath; + std::filesystem::path m_dataDirPath; std::shared_ptr m_onboarding; std::function m_checkIfShouldContinue; diff --git a/libs/Onboarding/tests/test_OnboardingModule.cpp b/libs/Onboarding/tests/test_OnboardingModule.cpp index 47b34dd2f3..9e2939b5c7 100644 --- a/libs/Onboarding/tests/test_OnboardingModule.cpp +++ b/libs/Onboarding/tests/test_OnboardingModule.cpp @@ -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(); - auto result = accountsService->init(testAccount.fusedTestFolder()); + auto result = accountsService->init(testAccount.testDataDir()); ASSERT_TRUE(result); auto onboarding = std::make_shared(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(); - auto result = accountsService->init(testAccount.fusedTestFolder()); + auto result = accountsService->init(testAccount.testDataDir()); ASSERT_TRUE(result); auto onboarding = std::make_shared(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); diff --git a/libs/StatusGoQt/CMakeLists.txt b/libs/StatusGoQt/CMakeLists.txt index fd82f0e572..cd03eaae12 100644 --- a/libs/StatusGoQt/CMakeLists.txt +++ b/libs/StatusGoQt/CMakeLists.txt @@ -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 ) diff --git a/libs/StatusGoQt/src/StatusGo/Metadata/api_response.h b/libs/StatusGoQt/src/StatusGo/Metadata/api_response.h index f929222da0..75717be085 100644 --- a/libs/StatusGoQt/src/StatusGo/Metadata/api_response.h +++ b/libs/StatusGoQt/src/StatusGo/Metadata/api_response.h @@ -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)) { } diff --git a/libs/StatusGoQt/src/StatusGo/SignalsManager.cpp b/libs/StatusGoQt/src/StatusGo/SignalsManager.cpp index 88ee183c63..5735b8fc4b 100644 --- a/libs/StatusGoQt/src/StatusGo/SignalsManager.cpp +++ b/libs/StatusGoQt/src/StatusGo/SignalsManager.cpp @@ -1,9 +1,15 @@ #include "SignalsManager.h" +#include "StatusGoEvent.h" #include #include +#include +#include + +using json = nlohmann::json; + using namespace std::string_literals; namespace Status::StatusGo @@ -11,6 +17,11 @@ namespace Status::StatusGo std::map 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,61 +32,74 @@ 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}, - {"node.started"s, SignalType::NodeStarted}, - {"node.stopped"s, SignalType::NodeStopped}, - {"node.login"s, SignalType::NodeLogin}, - {"node.crashed"s, SignalType::NodeCrashed}, + signalMap = { + {"node.ready"s, SignalType::NodeReady}, + {"node.started"s, SignalType::NodeStarted}, + {"node.stopped"s, SignalType::NodeStopped}, + {"node.login"s, SignalType::NodeLogin}, + {"node.crashed"s, SignalType::NodeCrashed}, - {"discovery.started"s, SignalType::DiscoveryStarted}, - {"discovery.stopped"s, SignalType::DiscoveryStopped}, - {"discovery.summary"s, SignalType::DiscoverySummary}, + {"discovery.started"s, SignalType::DiscoveryStarted}, + {"discovery.stopped"s, SignalType::DiscoveryStopped}, + {"discovery.summary"s, SignalType::DiscoverySummary}, - {"mailserver.changed"s, SignalType::MailserverChanged}, - {"mailserver.available"s, SignalType::MailserverAvailable}, + {"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.started"s, SignalType::HistoryRequestStarted}, + {"history.request.batch.processed"s, SignalType::HistoryRequestBatchProcessed}, + {"history.request.completed"s, SignalType::HistoryRequestCompleted}, + + {"wallet"s, SignalType::WalletEvent}, + }; } SignalsManager::~SignalsManager() { } -void SignalsManager::processSignal(const QString& statusSignal) +void SignalsManager::processSignal(const char* statusSignalData) { - try - { - QJsonParseError json_error; - const QJsonDocument signalEventDoc(QJsonDocument::fromJson(statusSignal.toUtf8(), &json_error)); - if(json_error.error != QJsonParseError::NoError) + // TODO: overkill, use some kind of message broker + using namespace std::chrono_literals; + auto dataStrPtr = std::make_shared(statusSignalData); + m_threadPool.start(QRunnable::create([dataStrPtr, this]() { + try { - qWarning() << "Invalid signal received"; - return; + StatusGoEvent event = json::parse(*dataStrPtr); + if(event.error != std::nullopt) + { + qWarning() << "Error in signal" << event.type.c_str() << "; error" << event.error.value(); + // TODO report event error + return; + } + + QString signalError; + if(event.event.contains("error")) signalError = event.event["error"].get(); + + dispatch(event.type, std::move(event.event), signalError); } - decode(signalEventDoc.object()); - } - catch(const std::exception& e) - { - qWarning() << "Error decoding signal, err: ", e.what(); - return; - } + catch(const std::exception& e) + { + 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(data); - QFuture future = - QtConcurrent::run([dataStrPtr]() { SignalsManager::instance()->processSignal(*dataStrPtr); }); + SignalsManager::instance()->processSignal(data); } } // namespace Status::StatusGo diff --git a/libs/StatusGoQt/src/StatusGo/SignalsManager.h b/libs/StatusGoQt/src/StatusGo/SignalsManager.h index c4db6ecd28..89746268be 100644 --- a/libs/StatusGoQt/src/StatusGo/SignalsManager.h +++ b/libs/StatusGoQt/src/StatusGo/SignalsManager.h @@ -1,6 +1,9 @@ #pragma once #include +#include + +#include 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; + /*! \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 eventData); + private: explicit SignalsManager(); ~SignalsManager(); @@ -65,7 +94,10 @@ private: private: static std::map 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 diff --git a/libs/StatusGoQt/src/StatusGo/StatusGoEvent.cpp b/libs/StatusGoQt/src/StatusGo/StatusGoEvent.cpp new file mode 100644 index 0000000000..8e15b41a4e --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/StatusGoEvent.cpp @@ -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); +} + +} \ No newline at end of file diff --git a/libs/StatusGoQt/src/StatusGo/StatusGoEvent.h b/libs/StatusGoQt/src/StatusGo/StatusGoEvent.h new file mode 100644 index 0000000000..b52d2c5d3a --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/StatusGoEvent.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include +#include + +#include + +#include + +using json = nlohmann::json; + +namespace Status::StatusGo +{ + +/// \see status-go's EventType@events.go in services/wallet/transfer module +using StatusGoEventType = Helpers::NamedType; + +/// \see status-go's Event@events.go in services/wallet/transfer module +struct StatusGoEvent +{ + std::string type; + json event; + std::optional error; +}; + +void to_json(json& j, const StatusGoEvent& d); +void from_json(const json& j, StatusGoEvent& d); + +} // namespace Status::StatusGo diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/BigInt.h b/libs/StatusGoQt/src/StatusGo/Wallet/BigInt.h index ac6e75a031..da8d7ee08a 100644 --- a/libs/StatusGoQt/src/StatusGo/Wallet/BigInt.h +++ b/libs/StatusGoQt/src/StatusGo/Wallet/BigInt.h @@ -48,7 +48,10 @@ struct adl_serializer static void from_json(const json& j, GoWallet::BigInt& num) { - num = GoWallet::BigInt(j.get()); + if(j.is_number()) + num = GoWallet::BigInt(j.get()); + else + num = GoWallet::BigInt(j.get()); } }; diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/Transfer/Event.cpp b/libs/StatusGoQt/src/StatusGo/Wallet/Transfer/Event.cpp new file mode 100644 index 0000000000..03fd7ff848 --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/Wallet/Transfer/Event.cpp @@ -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 diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/Transfer/Event.h b/libs/StatusGoQt/src/StatusGo/Wallet/Transfer/Event.h new file mode 100644 index 0000000000..0efba539b5 --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/Wallet/Transfer/Event.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include +#include + +#include + +#include + +namespace Status::StatusGo::Wallet::Transfer +{ + +/// \see status-go's EventType@events.go in services/wallet/transfer module +using EventType = Helpers::NamedType; + +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 blockNumber; + /// Accounts are \c null in case of error. In the error case message contains the error message + std::optional> accounts; + QString message; +}; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Event, type, blockNumber, accounts, message); + +} // namespace Status diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.cpp b/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.cpp index 94b2ca24a4..94c3a35b4e 100644 --- a/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.cpp +++ b/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.cpp @@ -83,8 +83,8 @@ Tokens getTokens(const ChainID& chainId) } TokenBalances getTokensBalancesForChainIDs(const std::vector& chainIds, - const std::vector accounts, - const std::vector tokens) + const std::vector& accounts, + const std::vector& tokens) { std::vector params = {chainIds, accounts, tokens}; json inputJson = {{"jsonrpc", "2.0"}, {"method", "wallet_getTokensBalancesForChainIDs"}, {"params", params}}; @@ -108,4 +108,26 @@ TokenBalances getTokensBalancesForChainIDs(const std::vector& chainIds, return resultData; } +std::vector +getBalanceHistoryOnChain(Accounts::EOAddress account, const std::chrono::seconds& secondsToNow, int sampleCount) +{ + std::vector 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().result; +} + +void checkRecentHistory(const std::vector& accounts) +{ + std::vector 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 diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.h b/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.h index b3b8fea8bf..5a671a603a 100644 --- a/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.h +++ b/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.h @@ -11,8 +11,12 @@ #include "Types.h" +#include + #include +#include + 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>; -/// \note status-go's @api.go -> @.go +/// \note status-go's API -> GetTokensBalancesForChainIDs<@api.go /// \throws \c CallPrivateRpcError TokenBalances getTokensBalancesForChainIDs(const std::vector& chainIds, - const std::vector accounts, - const std::vector tokens); + const std::vector& accounts, + const std::vector& 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 +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); + } // namespace Status::StatusGo::Wallet diff --git a/src/app/modules/main/wallet_section/all_tokens/historical_balance.md b/src/app/modules/main/wallet_section/all_tokens/historical_balance.md new file mode 100644 index 0000000000..9244ee22e1 --- /dev/null +++ b/src/app/modules/main/wallet_section/all_tokens/historical_balance.md @@ -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 \ No newline at end of file diff --git a/test/libs/StatusGoQt/test_messaging.cpp b/test/libs/StatusGoQt/test_messaging.cpp index d85da02d5d..e6cb0899a3 100644 --- a/test/libs/StatusGoQt/test_messaging.cpp +++ b/test/libs/StatusGoQt/test_messaging.cpp @@ -18,14 +18,13 @@ 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; + } + }); ScopedTestAccount testAccount(test_info_->name()); diff --git a/test/libs/StatusGoQt/test_wallet.cpp b/test/libs/StatusGoQt/test_wallet.cpp index c8e93a0b06..f893ca59b1 100644 --- a/test/libs/StatusGoQt/test_wallet.cpp +++ b/test/libs/StatusGoQt/test_wallet.cpp @@ -1,17 +1,22 @@ +#include + #include +#include #include +#include #include -#include #include +#include #include #include #include #include +#include #include 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(); - auto result = accountsService->init(testAccount.fusedTestFolder()); + auto result = accountsService->init(testAccount.testDataDir()); ASSERT_TRUE(result); auto onboarding = std::make_shared(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) { - accountLoggedInError = true; - qDebug() << "Failed logging in in test" << test_info_->name() << "with error:" << 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) { - return a.name == newTestAccountName; - }); + 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 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(1h), + std::chrono::round(std::chrono::days(1)), + std::chrono::round(std::chrono::days(7)), + std::chrono::round(std::chrono::months(1)), + std::chrono::round(std::chrono::months(6)), + std::chrono::round(std::chrono::years(1)), + std::chrono::round(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() + (qSzabos.convert_to() / ((weiD / szaboD).convert_to())); + }; + + 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