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:
parent
cc9f83650c
commit
5450384a34
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
|
#include <QDateTime>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
|
@ -97,7 +98,24 @@ struct adl_serializer<std::optional<T>>
|
||||||
|
|
||||||
static void from_json(const json& j, std::optional<T>& opt)
|
static void from_json(const json& j, std::optional<T>& opt)
|
||||||
{
|
{
|
||||||
opt.emplace(j.get<T>());
|
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>());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
std::optional<QString> getDataFromFile(const fs::path& path)
|
std::optional<QString> getDataFromFile(const fs::path& path)
|
||||||
{
|
{
|
||||||
QFile jsonFile{Status::toQString(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 templateNodeConfigJsonStr = getDataFromFile(":/Status/StaticConfig/node-config.json").value();
|
||||||
auto fleetJson = getDataFromFile(":/Status/StaticConfig/fleets.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();
|
auto templateDefaultNetworksJson = getDataFromFile(":/Status/StaticConfig/default-networks.json").value();
|
||||||
QString defaultNetworksContent = templateDefaultNetworksJson.replace("%INFURA_TOKEN_RESOLVED%", infuraKey);
|
QString defaultNetworksContent = templateDefaultNetworksJson.replace("%INFURA_TOKEN_RESOLVED%", infuraKey);
|
||||||
|
|
|
@ -35,12 +35,12 @@ ScopedTestAccount::ScopedTestAccount(const std::string& tempTestSubfolderName,
|
||||||
char* args[] = {appName.data()};
|
char* args[] = {appName.data()};
|
||||||
m_app = std::make_unique<QCoreApplication>(argc, reinterpret_cast<char**>(args));
|
m_app = std::make_unique<QCoreApplication>(argc, reinterpret_cast<char**>(args));
|
||||||
|
|
||||||
m_testFolderPath = m_fusedTestFolder->tempFolder() / Constants::statusGoDataDirName;
|
m_dataDirPath = m_fusedTestFolder->tempFolder() / Constants::statusGoDataDirName;
|
||||||
fs::create_directory(m_testFolderPath);
|
fs::create_directory(m_dataDirPath);
|
||||||
|
|
||||||
// Setup accounts
|
// Setup accounts
|
||||||
auto accountsService = std::make_shared<Onboarding::AccountsService>();
|
auto accountsService = std::make_shared<Onboarding::AccountsService>();
|
||||||
auto result = accountsService->init(m_testFolderPath);
|
auto result = accountsService->init(m_dataDirPath);
|
||||||
if(!result)
|
if(!result)
|
||||||
{
|
{
|
||||||
throw std::runtime_error("ScopedTestAccount - Failed to create temporary test account");
|
throw std::runtime_error("ScopedTestAccount - Failed to create temporary test account");
|
||||||
|
@ -59,12 +59,14 @@ ScopedTestAccount::ScopedTestAccount(const std::string& tempTestSubfolderName,
|
||||||
}
|
}
|
||||||
|
|
||||||
int accountLoggedInCount = 0;
|
int accountLoggedInCount = 0;
|
||||||
QObject::connect(m_onboarding.get(), &Onboarding::OnboardingController::accountLoggedIn, [&accountLoggedInCount]() {
|
QObject::connect(m_onboarding.get(),
|
||||||
accountLoggedInCount++;
|
&Onboarding::OnboardingController::accountLoggedIn,
|
||||||
});
|
m_app.get(),
|
||||||
|
[&accountLoggedInCount]() { accountLoggedInCount++; });
|
||||||
bool accountLoggedInError = false;
|
bool accountLoggedInError = false;
|
||||||
QObject::connect(m_onboarding.get(),
|
QObject::connect(m_onboarding.get(),
|
||||||
&Onboarding::OnboardingController::accountLoginError,
|
&Onboarding::OnboardingController::accountLoginError,
|
||||||
|
m_app.get(),
|
||||||
[&accountLoggedInError]() { accountLoggedInError = true; });
|
[&accountLoggedInError]() { accountLoggedInError = true; });
|
||||||
|
|
||||||
// Create Accounts
|
// Create Accounts
|
||||||
|
@ -99,14 +101,15 @@ ScopedTestAccount::ScopedTestAccount(const std::string& tempTestSubfolderName,
|
||||||
|
|
||||||
processMessages(2000, [accountLoggedInCount]() { return accountLoggedInCount == 0; });
|
processMessages(2000, [accountLoggedInCount]() { return accountLoggedInCount == 0; });
|
||||||
|
|
||||||
if(accountLoggedInCount != 1)
|
|
||||||
{
|
|
||||||
throw std::runtime_error("ScopedTestAccount - missing confirmation of account creation");
|
|
||||||
}
|
|
||||||
if(accountLoggedInError)
|
if(accountLoggedInError)
|
||||||
{
|
{
|
||||||
throw std::runtime_error("ScopedTestAccount - account loggedin error");
|
throw std::runtime_error("ScopedTestAccount - account loggedin error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(accountLoggedInCount != 1)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("ScopedTestAccount - missing confirmation of account creation");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScopedTestAccount::~ScopedTestAccount()
|
ScopedTestAccount::~ScopedTestAccount()
|
||||||
|
@ -123,10 +126,12 @@ void ScopedTestAccount::processMessages(size_t maxWaitTimeMillis, std::function<
|
||||||
auto remainingIterations = maxWaitTime / iterationSleepTime;
|
auto remainingIterations = maxWaitTime / iterationSleepTime;
|
||||||
while(remainingIterations-- > 0 && shouldWaitUntilTimeout())
|
while(remainingIterations-- > 0 && shouldWaitUntilTimeout())
|
||||||
{
|
{
|
||||||
std::this_thread::sleep_for(iterationSleepTime);
|
|
||||||
|
|
||||||
QCoreApplication::sendPostedEvents();
|
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()
|
void ScopedTestAccount::logOut()
|
||||||
|
@ -171,7 +176,12 @@ Onboarding::OnboardingController* ScopedTestAccount::onboardingController() cons
|
||||||
|
|
||||||
const std::filesystem::path& ScopedTestAccount::fusedTestFolder() const
|
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
|
} // namespace Status::Testing
|
||||||
|
|
|
@ -58,12 +58,19 @@ public:
|
||||||
|
|
||||||
Status::Onboarding::OnboardingController* onboardingController() const;
|
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& fusedTestFolder() const;
|
||||||
|
const std::filesystem::path& testDataDir() const;
|
||||||
|
|
||||||
|
QCoreApplication* app()
|
||||||
|
{
|
||||||
|
return m_app.get();
|
||||||
|
};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<AutoCleanTempTestDir> m_fusedTestFolder;
|
std::unique_ptr<AutoCleanTempTestDir> m_fusedTestFolder;
|
||||||
std::unique_ptr<QCoreApplication> m_app;
|
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::shared_ptr<Status::Onboarding::OnboardingController> m_onboarding;
|
||||||
std::function<bool()> m_checkIfShouldContinue;
|
std::function<bool()> m_checkIfShouldContinue;
|
||||||
|
|
||||||
|
|
|
@ -93,9 +93,9 @@ TEST(OnboardingModule, TestCreateAndLoginAccountEndToEnd)
|
||||||
auto remainingIterations = maxWaitTime / iterationSleepTime;
|
auto remainingIterations = maxWaitTime / iterationSleepTime;
|
||||||
while(remainingIterations-- > 0 && accountLoggedInCount == 0)
|
while(remainingIterations-- > 0 && accountLoggedInCount == 0)
|
||||||
{
|
{
|
||||||
std::this_thread::sleep_for(iterationSleepTime);
|
|
||||||
|
|
||||||
QCoreApplication::sendPostedEvents();
|
QCoreApplication::sendPostedEvents();
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(iterationSleepTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
EXPECT_EQ(accountLoggedInCount, 1);
|
EXPECT_EQ(accountLoggedInCount, 1);
|
||||||
|
@ -115,20 +115,12 @@ TEST(OnboardingModule, TestLoginEndToEnd)
|
||||||
QObject::connect(StatusGo::SignalsManager::instance(),
|
QObject::connect(StatusGo::SignalsManager::instance(),
|
||||||
&StatusGo::SignalsManager::nodeLogin,
|
&StatusGo::SignalsManager::nodeLogin,
|
||||||
[&createAndLogin](const QString& error) {
|
[&createAndLogin](const QString& error) {
|
||||||
if(error.isEmpty())
|
if(error.isEmpty()) createAndLogin = !createAndLogin;
|
||||||
{
|
|
||||||
if(createAndLogin)
|
|
||||||
{
|
|
||||||
createAndLogin = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
createAndLogin = true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
constexpr auto accountName = "TestLoginAccountName";
|
constexpr auto accountName = "TestLoginAccountName";
|
||||||
ScopedTestAccount testAccount(test_info_->name(), accountName);
|
ScopedTestAccount testAccount(test_info_->name(), accountName);
|
||||||
testAccount.processMessages(1000, [createAndLogin]() { return !createAndLogin; });
|
testAccount.processMessages(1000, [&createAndLogin]() { return !createAndLogin; });
|
||||||
ASSERT_TRUE(createAndLogin);
|
ASSERT_TRUE(createAndLogin);
|
||||||
|
|
||||||
testAccount.logOut();
|
testAccount.logOut();
|
||||||
|
@ -138,7 +130,7 @@ TEST(OnboardingModule, TestLoginEndToEnd)
|
||||||
|
|
||||||
// Setup accounts
|
// Setup accounts
|
||||||
auto accountsService = std::make_shared<Onboarding::AccountsService>();
|
auto accountsService = std::make_shared<Onboarding::AccountsService>();
|
||||||
auto result = accountsService->init(testAccount.fusedTestFolder());
|
auto result = accountsService->init(testAccount.testDataDir());
|
||||||
ASSERT_TRUE(result);
|
ASSERT_TRUE(result);
|
||||||
|
|
||||||
auto onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
|
auto onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
|
||||||
|
@ -156,8 +148,8 @@ TEST(OnboardingModule, TestLoginEndToEnd)
|
||||||
QObject::connect(onboarding.get(),
|
QObject::connect(onboarding.get(),
|
||||||
&Onboarding::OnboardingController::accountLoginError,
|
&Onboarding::OnboardingController::accountLoginError,
|
||||||
[&accountLoggedInError](const QString& error) {
|
[&accountLoggedInError](const QString& error) {
|
||||||
accountLoggedInError = true;
|
|
||||||
qDebug() << "Failed logging in in test" << test_info_->name() << "with error:" << error;
|
qDebug() << "Failed logging in in test" << test_info_->name() << "with error:" << error;
|
||||||
|
accountLoggedInError = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
auto ourAccountRes =
|
auto ourAccountRes =
|
||||||
|
@ -165,11 +157,11 @@ TEST(OnboardingModule, TestLoginEndToEnd)
|
||||||
auto errorString = accountsService->login(*ourAccountRes, testAccount.password());
|
auto errorString = accountsService->login(*ourAccountRes, testAccount.password());
|
||||||
ASSERT_EQ(errorString.length(), 0);
|
ASSERT_EQ(errorString.length(), 0);
|
||||||
|
|
||||||
testAccount.processMessages(1000, [accountLoggedInCount, accountLoggedInError]() {
|
testAccount.processMessages(1000, [&accountLoggedInCount, &accountLoggedInError]() {
|
||||||
return accountLoggedInCount == 0 && !accountLoggedInError;
|
return accountLoggedInCount == 0 && !accountLoggedInError;
|
||||||
});
|
});
|
||||||
ASSERT_EQ(accountLoggedInCount, 1);
|
|
||||||
ASSERT_EQ(accountLoggedInError, 0);
|
ASSERT_EQ(accountLoggedInError, 0);
|
||||||
|
ASSERT_EQ(accountLoggedInCount, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(OnboardingModule, TestLoginEndToEnd_WrongPassword)
|
TEST(OnboardingModule, TestLoginEndToEnd_WrongPassword)
|
||||||
|
@ -180,7 +172,7 @@ TEST(OnboardingModule, TestLoginEndToEnd_WrongPassword)
|
||||||
testAccount.logOut();
|
testAccount.logOut();
|
||||||
|
|
||||||
auto accountsService = std::make_shared<Onboarding::AccountsService>();
|
auto accountsService = std::make_shared<Onboarding::AccountsService>();
|
||||||
auto result = accountsService->init(testAccount.fusedTestFolder());
|
auto result = accountsService->init(testAccount.testDataDir());
|
||||||
ASSERT_TRUE(result);
|
ASSERT_TRUE(result);
|
||||||
auto onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
|
auto onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
|
||||||
auto accounts = accountsService->openAndListAccounts();
|
auto accounts = accountsService->openAndListAccounts();
|
||||||
|
@ -205,7 +197,7 @@ TEST(OnboardingModule, TestLoginEndToEnd_WrongPassword)
|
||||||
auto errorString = accountsService->login(*ourAccountRes, testAccount.password() + "extra");
|
auto errorString = accountsService->login(*ourAccountRes, testAccount.password() + "extra");
|
||||||
ASSERT_EQ(errorString.length(), 0);
|
ASSERT_EQ(errorString.length(), 0);
|
||||||
|
|
||||||
testAccount.processMessages(1000, [accountLoggedInCount, accountLoggedInError]() {
|
testAccount.processMessages(1000, [&accountLoggedInCount, &accountLoggedInError]() {
|
||||||
return accountLoggedInCount == 0 && !accountLoggedInError;
|
return accountLoggedInCount == 0 && !accountLoggedInError;
|
||||||
});
|
});
|
||||||
ASSERT_EQ(accountLoggedInError, 1);
|
ASSERT_EQ(accountLoggedInError, 1);
|
||||||
|
|
|
@ -114,6 +114,8 @@ target_sources(${PROJECT_NAME}
|
||||||
|
|
||||||
src/StatusGo/SignalsManager.h
|
src/StatusGo/SignalsManager.h
|
||||||
src/StatusGo/SignalsManager.cpp
|
src/StatusGo/SignalsManager.cpp
|
||||||
|
src/StatusGo/StatusGoEvent.h
|
||||||
|
src/StatusGo/StatusGoEvent.cpp
|
||||||
|
|
||||||
src/StatusGo/Settings/SettingsAPI.h
|
src/StatusGo/Settings/SettingsAPI.h
|
||||||
src/StatusGo/Settings/SettingsAPI.cpp
|
src/StatusGo/Settings/SettingsAPI.cpp
|
||||||
|
@ -129,4 +131,7 @@ target_sources(${PROJECT_NAME}
|
||||||
src/StatusGo/Wallet/wallet_types.h
|
src/StatusGo/Wallet/wallet_types.h
|
||||||
src/StatusGo/Wallet/WalletApi.h
|
src/StatusGo/Wallet/WalletApi.h
|
||||||
src/StatusGo/Wallet/WalletApi.cpp
|
src/StatusGo/Wallet/WalletApi.cpp
|
||||||
|
|
||||||
|
src/StatusGo/Wallet/Transfer/Event.h
|
||||||
|
src/StatusGo/Wallet/Transfer/Event.cpp
|
||||||
)
|
)
|
||||||
|
|
|
@ -159,7 +159,8 @@ class CallPrivateRpcError : public std::runtime_error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
CallPrivateRpcError(const CallPrivateRpcErrorResponse error)
|
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))
|
, m_error(std::move(error))
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
#include "SignalsManager.h"
|
#include "SignalsManager.h"
|
||||||
|
#include "StatusGoEvent.h"
|
||||||
|
|
||||||
#include <QtConcurrent>
|
#include <QtConcurrent>
|
||||||
|
|
||||||
#include <libstatus.h>
|
#include <libstatus.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
using namespace std::string_literals;
|
using namespace std::string_literals;
|
||||||
|
|
||||||
namespace Status::StatusGo
|
namespace Status::StatusGo
|
||||||
|
@ -11,6 +17,11 @@ namespace Status::StatusGo
|
||||||
|
|
||||||
std::map<std::string, SignalType> SignalsManager::signalMap;
|
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
|
// TODO: make me thread safe or better refactor into broadcasting mechanism
|
||||||
SignalsManager* SignalsManager::instance()
|
SignalsManager* SignalsManager::instance()
|
||||||
{
|
{
|
||||||
|
@ -21,61 +32,74 @@ SignalsManager* SignalsManager::instance()
|
||||||
SignalsManager::SignalsManager()
|
SignalsManager::SignalsManager()
|
||||||
: QObject(nullptr)
|
: 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);
|
SetSignalEventCallback((void*)&SignalsManager::signalCallback);
|
||||||
|
|
||||||
signalMap = {{"node.ready"s, SignalType::NodeReady},
|
signalMap = {
|
||||||
{"node.started"s, SignalType::NodeStarted},
|
{"node.ready"s, SignalType::NodeReady},
|
||||||
{"node.stopped"s, SignalType::NodeStopped},
|
{"node.started"s, SignalType::NodeStarted},
|
||||||
{"node.login"s, SignalType::NodeLogin},
|
{"node.stopped"s, SignalType::NodeStopped},
|
||||||
{"node.crashed"s, SignalType::NodeCrashed},
|
{"node.login"s, SignalType::NodeLogin},
|
||||||
|
{"node.crashed"s, SignalType::NodeCrashed},
|
||||||
|
|
||||||
{"discovery.started"s, SignalType::DiscoveryStarted},
|
{"discovery.started"s, SignalType::DiscoveryStarted},
|
||||||
{"discovery.stopped"s, SignalType::DiscoveryStopped},
|
{"discovery.stopped"s, SignalType::DiscoveryStopped},
|
||||||
{"discovery.summary"s, SignalType::DiscoverySummary},
|
{"discovery.summary"s, SignalType::DiscoverySummary},
|
||||||
|
|
||||||
{"mailserver.changed"s, SignalType::MailserverChanged},
|
{"mediaserver.started"s, SignalType::MailserverStarted},
|
||||||
{"mailserver.available"s, SignalType::MailserverAvailable},
|
{"mailserver.changed"s, SignalType::MailserverChanged},
|
||||||
|
{"mailserver.available"s, SignalType::MailserverAvailable},
|
||||||
|
|
||||||
{"history.request.started"s, SignalType::HistoryRequestStarted},
|
{"history.request.started"s, SignalType::HistoryRequestStarted},
|
||||||
{"history.request.batch.processed"s, SignalType::HistoryRequestBatchProcessed},
|
{"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() { }
|
SignalsManager::~SignalsManager() { }
|
||||||
|
|
||||||
void SignalsManager::processSignal(const QString& statusSignal)
|
void SignalsManager::processSignal(const char* statusSignalData)
|
||||||
{
|
{
|
||||||
try
|
// TODO: overkill, use some kind of message broker
|
||||||
{
|
using namespace std::chrono_literals;
|
||||||
QJsonParseError json_error;
|
auto dataStrPtr = std::make_shared<std::string>(statusSignalData);
|
||||||
const QJsonDocument signalEventDoc(QJsonDocument::fromJson(statusSignal.toUtf8(), &json_error));
|
m_threadPool.start(QRunnable::create([dataStrPtr, this]() {
|
||||||
if(json_error.error != QJsonParseError::NoError)
|
try
|
||||||
{
|
{
|
||||||
qWarning() << "Invalid signal received";
|
StatusGoEvent event = json::parse(*dataStrPtr);
|
||||||
return;
|
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<QString>();
|
||||||
|
|
||||||
|
dispatch(event.type, std::move(event.event), signalError);
|
||||||
}
|
}
|
||||||
decode(signalEventDoc.object());
|
catch(const std::exception& e)
|
||||||
}
|
{
|
||||||
catch(const std::exception& e)
|
qWarning() << "Error decoding signal, err: " << e.what() << "; signal data: " << dataStrPtr->c_str();
|
||||||
{
|
}
|
||||||
qWarning() << "Error decoding signal, err: ", e.what();
|
}));
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SignalsManager::decode(const QJsonObject& signalEvent)
|
void SignalsManager::dispatch(const std::string& type, json signalEvent, const QString& signalError)
|
||||||
{
|
{
|
||||||
SignalType signalType(Unknown);
|
SignalType signalType(Unknown);
|
||||||
auto signalName = signalEvent["type"].toString().toStdString();
|
if(!signalMap.contains(type))
|
||||||
if(!signalMap.contains(signalName))
|
|
||||||
{
|
{
|
||||||
qWarning() << "Unknown signal received: " << signalName.c_str();
|
qWarning() << "Unknown signal received: " << type.c_str();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
signalType = signalMap[signalName];
|
signalType = signalMap[type];
|
||||||
auto signalError = signalEvent["event"]["error"].toString();
|
|
||||||
|
|
||||||
switch(signalType)
|
switch(signalType)
|
||||||
{
|
{
|
||||||
// TODO: create extractor functions like in nim
|
// TODO: create extractor functions like in nim
|
||||||
|
@ -89,22 +113,21 @@ void SignalsManager::decode(const QJsonObject& signalEvent)
|
||||||
break;
|
break;
|
||||||
case DiscoveryStarted: emit discoveryStarted(signalError); break;
|
case DiscoveryStarted: emit discoveryStarted(signalError); break;
|
||||||
case DiscoveryStopped: emit discoveryStopped(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 MailserverChanged: emit mailserverChanged(signalError); break;
|
||||||
case MailserverAvailable: emit mailserverAvailable(signalError); break;
|
case MailserverAvailable: emit mailserverAvailable(signalError); break;
|
||||||
case HistoryRequestStarted: emit historyRequestStarted(signalError); break;
|
case HistoryRequestStarted: emit historyRequestStarted(signalError); break;
|
||||||
case HistoryRequestBatchProcessed: emit historyRequestBatchProcessed(signalError); break;
|
case HistoryRequestBatchProcessed: emit historyRequestBatchProcessed(signalError); break;
|
||||||
case HistoryRequestCompleted: emit historyRequestCompleted(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;
|
case Unknown: assert(false); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SignalsManager::signalCallback(const char* data)
|
void SignalsManager::signalCallback(const char* data)
|
||||||
{
|
{
|
||||||
// TODO: overkill, use some kind of message broker
|
SignalsManager::instance()->processSignal(data);
|
||||||
auto dataStrPtr = std::make_shared<QString>(data);
|
|
||||||
QFuture<void> future =
|
|
||||||
QtConcurrent::run([dataStrPtr]() { SignalsManager::instance()->processSignal(*dataStrPtr); });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Status::StatusGo
|
} // namespace Status::StatusGo
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QThreadPool>
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
namespace Status::StatusGo
|
namespace Status::StatusGo
|
||||||
{
|
{
|
||||||
|
@ -18,14 +21,36 @@ enum SignalType
|
||||||
DiscoveryStopped,
|
DiscoveryStopped,
|
||||||
DiscoverySummary,
|
DiscoverySummary,
|
||||||
|
|
||||||
|
MailserverStarted,
|
||||||
MailserverChanged,
|
MailserverChanged,
|
||||||
MailserverAvailable,
|
MailserverAvailable,
|
||||||
|
|
||||||
HistoryRequestStarted,
|
HistoryRequestStarted,
|
||||||
HistoryRequestBatchProcessed,
|
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
|
\todo refactor into a message broker helper to be used by specific service APIs to deliver signals
|
||||||
as part of the specific StatusGoAPI service
|
as part of the specific StatusGoAPI service
|
||||||
|
@ -38,9 +63,10 @@ class SignalsManager final : public QObject
|
||||||
public:
|
public:
|
||||||
static SignalsManager* instance();
|
static SignalsManager* instance();
|
||||||
|
|
||||||
void processSignal(const QString& ev);
|
void processSignal(const char* statusSignalData);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
// TODO: move all signals to deliver EventData, distributing this way data processing to the consumer
|
||||||
void nodeReady(const QString& error);
|
void nodeReady(const QString& error);
|
||||||
void nodeStarted(const QString& error);
|
void nodeStarted(const QString& error);
|
||||||
void nodeStopped(const QString& error);
|
void nodeStopped(const QString& error);
|
||||||
|
@ -51,6 +77,7 @@ signals:
|
||||||
void discoveryStopped(const QString& error);
|
void discoveryStopped(const QString& error);
|
||||||
void discoverySummary(size_t nodeCount, const QString& error);
|
void discoverySummary(size_t nodeCount, const QString& error);
|
||||||
|
|
||||||
|
void mailserverStarted(const QString& error);
|
||||||
void mailserverChanged(const QString& error);
|
void mailserverChanged(const QString& error);
|
||||||
void mailserverAvailable(const QString& error);
|
void mailserverAvailable(const QString& error);
|
||||||
|
|
||||||
|
@ -58,6 +85,8 @@ signals:
|
||||||
void historyRequestBatchProcessed(const QString& error);
|
void historyRequestBatchProcessed(const QString& error);
|
||||||
void historyRequestCompleted(const QString& error);
|
void historyRequestCompleted(const QString& error);
|
||||||
|
|
||||||
|
void wallet(QSharedPointer<Status::StatusGo::EventData> eventData);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
explicit SignalsManager();
|
explicit SignalsManager();
|
||||||
~SignalsManager();
|
~SignalsManager();
|
||||||
|
@ -65,7 +94,10 @@ private:
|
||||||
private:
|
private:
|
||||||
static std::map<std::string, SignalType> signalMap;
|
static std::map<std::string, SignalType> signalMap;
|
||||||
static void signalCallback(const char* data);
|
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
|
} // namespace Status::StatusGo
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
|
@ -48,7 +48,10 @@ struct adl_serializer<GoWallet::BigInt>
|
||||||
|
|
||||||
static void from_json(const json& j, GoWallet::BigInt& num)
|
static void from_json(const json& j, GoWallet::BigInt& num)
|
||||||
{
|
{
|
||||||
num = GoWallet::BigInt(j.get<std::string>());
|
if(j.is_number())
|
||||||
|
num = GoWallet::BigInt(j.get<long long>());
|
||||||
|
else
|
||||||
|
num = GoWallet::BigInt(j.get<std::string>());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -83,8 +83,8 @@ Tokens getTokens(const ChainID& chainId)
|
||||||
}
|
}
|
||||||
|
|
||||||
TokenBalances getTokensBalancesForChainIDs(const std::vector<ChainID>& chainIds,
|
TokenBalances getTokensBalancesForChainIDs(const std::vector<ChainID>& chainIds,
|
||||||
const std::vector<Accounts::EOAddress> accounts,
|
const std::vector<Accounts::EOAddress>& accounts,
|
||||||
const std::vector<Accounts::EOAddress> tokens)
|
const std::vector<Accounts::EOAddress>& tokens)
|
||||||
{
|
{
|
||||||
std::vector<json> params = {chainIds, accounts, tokens};
|
std::vector<json> params = {chainIds, accounts, tokens};
|
||||||
json inputJson = {{"jsonrpc", "2.0"}, {"method", "wallet_getTokensBalancesForChainIDs"}, {"params", params}};
|
json inputJson = {{"jsonrpc", "2.0"}, {"method", "wallet_getTokensBalancesForChainIDs"}, {"params", params}};
|
||||||
|
@ -108,4 +108,26 @@ TokenBalances getTokensBalancesForChainIDs(const std::vector<ChainID>& chainIds,
|
||||||
return resultData;
|
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
|
} // namespace Status::StatusGo::Wallet
|
||||||
|
|
|
@ -11,8 +11,12 @@
|
||||||
|
|
||||||
#include "Types.h"
|
#include "Types.h"
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
|
||||||
namespace Accounts = Status::StatusGo::Accounts;
|
namespace Accounts = Status::StatusGo::Accounts;
|
||||||
|
|
||||||
namespace Status::StatusGo::Wallet
|
namespace Status::StatusGo::Wallet
|
||||||
|
@ -52,9 +56,31 @@ NetworkConfigurations getEthereumChains(bool onlyEnabled);
|
||||||
Tokens getTokens(const ChainID& chainId);
|
Tokens getTokens(const ChainID& chainId);
|
||||||
|
|
||||||
using TokenBalances = std::map<Accounts::EOAddress, std::map<Accounts::EOAddress, BigInt>>;
|
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
|
/// \throws \c CallPrivateRpcError
|
||||||
TokenBalances getTokensBalancesForChainIDs(const std::vector<ChainID>& chainIds,
|
TokenBalances getTokensBalancesForChainIDs(const std::vector<ChainID>& chainIds,
|
||||||
const std::vector<Accounts::EOAddress> accounts,
|
const std::vector<Accounts::EOAddress>& accounts,
|
||||||
const std::vector<Accounts::EOAddress> tokens);
|
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
|
} // namespace Status::StatusGo::Wallet
|
||||||
|
|
|
@ -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
|
|
@ -18,14 +18,13 @@ namespace Status::Testing {
|
||||||
TEST(MessagingApi, TestStartMessaging)
|
TEST(MessagingApi, TestStartMessaging)
|
||||||
{
|
{
|
||||||
bool nodeReady = false;
|
bool nodeReady = false;
|
||||||
QObject::connect(StatusGo::SignalsManager::instance(), &StatusGo::SignalsManager::nodeReady, [&nodeReady](const QString& error) {
|
QObject::connect(
|
||||||
if(error.isEmpty()) {
|
StatusGo::SignalsManager::instance(), &StatusGo::SignalsManager::nodeReady, [&nodeReady](const QString& error) {
|
||||||
if(nodeReady) {
|
if(error.isEmpty())
|
||||||
nodeReady = false;
|
{
|
||||||
} else
|
nodeReady = !nodeReady;
|
||||||
nodeReady = true;
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
ScopedTestAccount testAccount(test_info_->name());
|
ScopedTestAccount testAccount(test_info_->name());
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
|
#include <StatusGo/SignalsManager.h>
|
||||||
|
|
||||||
#include <StatusGo/Accounts/AccountsAPI.h>
|
#include <StatusGo/Accounts/AccountsAPI.h>
|
||||||
|
|
||||||
|
#include <StatusGo/Wallet/Transfer/Event.h>
|
||||||
#include <StatusGo/Wallet/WalletApi.h>
|
#include <StatusGo/Wallet/WalletApi.h>
|
||||||
|
#include <StatusGo/Wallet/wallet_types.h>
|
||||||
|
|
||||||
#include <StatusGo/Metadata/api_response.h>
|
#include <StatusGo/Metadata/api_response.h>
|
||||||
|
|
||||||
#include <Onboarding/Accounts/AccountsServiceInterface.h>
|
|
||||||
#include <Onboarding/Accounts/AccountsService.h>
|
#include <Onboarding/Accounts/AccountsService.h>
|
||||||
|
#include <Onboarding/Accounts/AccountsServiceInterface.h>
|
||||||
#include <Onboarding/Common/Constants.h>
|
#include <Onboarding/Common/Constants.h>
|
||||||
#include <Onboarding/OnboardingController.h>
|
#include <Onboarding/OnboardingController.h>
|
||||||
|
|
||||||
#include <ScopedTestAccount.h>
|
#include <ScopedTestAccount.h>
|
||||||
#include <StatusGo/Utils.h>
|
#include <StatusGo/Utils.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
namespace Wallet = Status::StatusGo::Wallet;
|
namespace Wallet = Status::StatusGo::Wallet;
|
||||||
|
@ -21,10 +26,13 @@ namespace General = Status::Constants::General;
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
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 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
|
/// \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
|
/// \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)
|
TEST(WalletApi, TestGetDerivedAddressesForPath_FromRootAccount)
|
||||||
{
|
{
|
||||||
|
@ -37,25 +45,30 @@ TEST(WalletApi, TestGetDerivedAddressesForPath_FromRootAccount)
|
||||||
|
|
||||||
const auto testPath = General::PathWalletRoot;
|
const auto testPath = General::PathWalletRoot;
|
||||||
|
|
||||||
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(),
|
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(
|
||||||
walletAccount.derivedFrom.value(), testPath, 3, 1);
|
testAccount.hashedPassword(), walletAccount.derivedFrom.value(), testPath, 3, 1);
|
||||||
// Check that accounts are generated in memory and none is saved
|
// Check that accounts are generated in memory and none is saved
|
||||||
const auto updatedAccounts = Accounts::getAccounts();
|
const auto updatedAccounts = Accounts::getAccounts();
|
||||||
ASSERT_EQ(updatedAccounts.size(), 2);
|
ASSERT_EQ(updatedAccounts.size(), 2);
|
||||||
|
|
||||||
ASSERT_EQ(derivedAddresses.size(), 3);
|
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());
|
ASSERT_NE(defaultWalletAccountIt, derivedAddresses.end());
|
||||||
const auto& defaultWalletAccount = *defaultWalletAccountIt;
|
const auto& defaultWalletAccount = *defaultWalletAccountIt;
|
||||||
ASSERT_EQ(defaultWalletAccount.path, General::PathDefaultWallet);
|
ASSERT_EQ(defaultWalletAccount.path, General::PathDefaultWallet);
|
||||||
ASSERT_EQ(defaultWalletAccount.address, walletAccount.address);
|
ASSERT_EQ(defaultWalletAccount.address, walletAccount.address);
|
||||||
ASSERT_TRUE(defaultWalletAccount.alreadyCreated);
|
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
|
// 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
|
// 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)
|
TEST(Accounts, TestGetDerivedAddressesForPath_AfterLogin)
|
||||||
|
@ -66,7 +79,7 @@ TEST(Accounts, TestGetDerivedAddressesForPath_AfterLogin)
|
||||||
testAccount.logOut();
|
testAccount.logOut();
|
||||||
|
|
||||||
auto accountsService = std::make_shared<Onboarding::AccountsService>();
|
auto accountsService = std::make_shared<Onboarding::AccountsService>();
|
||||||
auto result = accountsService->init(testAccount.fusedTestFolder());
|
auto result = accountsService->init(testAccount.testDataDir());
|
||||||
ASSERT_TRUE(result);
|
ASSERT_TRUE(result);
|
||||||
auto onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
|
auto onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
|
||||||
EXPECT_EQ(onboarding->getOpenedAccounts().size(), 1);
|
EXPECT_EQ(onboarding->getOpenedAccounts().size(), 1);
|
||||||
|
@ -79,16 +92,20 @@ TEST(Accounts, TestGetDerivedAddressesForPath_AfterLogin)
|
||||||
accountLoggedInCount++;
|
accountLoggedInCount++;
|
||||||
});
|
});
|
||||||
bool accountLoggedInError = false;
|
bool accountLoggedInError = false;
|
||||||
QObject::connect(onboarding.get(), &Onboarding::OnboardingController::accountLoginError, [&accountLoggedInError](const QString& error) {
|
QObject::connect(onboarding.get(),
|
||||||
accountLoggedInError = true;
|
&Onboarding::OnboardingController::accountLoginError,
|
||||||
qDebug() << "Failed logging in in test" << test_info_->name() << "with error:" << error;
|
[&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());
|
auto errorString = accountsService->login(*ourAccountRes, testAccount.password());
|
||||||
ASSERT_EQ(errorString.length(), 0);
|
ASSERT_EQ(errorString.length(), 0);
|
||||||
|
|
||||||
testAccount.processMessages(1000, [accountLoggedInCount, accountLoggedInError]() {
|
testAccount.processMessages(1000, [&accountLoggedInCount, &accountLoggedInError]() {
|
||||||
return accountLoggedInCount == 0 && !accountLoggedInError;
|
return accountLoggedInCount == 0 && !accountLoggedInError;
|
||||||
});
|
});
|
||||||
ASSERT_EQ(accountLoggedInCount, 1);
|
ASSERT_EQ(accountLoggedInCount, 1);
|
||||||
|
@ -97,26 +114,30 @@ TEST(Accounts, TestGetDerivedAddressesForPath_AfterLogin)
|
||||||
const auto testPath = General::PathWalletRoot;
|
const auto testPath = General::PathWalletRoot;
|
||||||
|
|
||||||
const auto walletAccount = testAccount.firstWalletAccount();
|
const auto walletAccount = testAccount.firstWalletAccount();
|
||||||
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(),
|
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(
|
||||||
walletAccount.derivedFrom.value(),
|
testAccount.hashedPassword(), walletAccount.derivedFrom.value(), testPath, 3, 1);
|
||||||
testPath, 3, 1);
|
|
||||||
// Check that accounts are generated in memory and none is saved
|
// Check that accounts are generated in memory and none is saved
|
||||||
const auto updatedAccounts = Accounts::getAccounts();
|
const auto updatedAccounts = Accounts::getAccounts();
|
||||||
ASSERT_EQ(updatedAccounts.size(), 2);
|
ASSERT_EQ(updatedAccounts.size(), 2);
|
||||||
|
|
||||||
ASSERT_EQ(derivedAddresses.size(), 3);
|
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());
|
ASSERT_NE(defaultWalletAccountIt, derivedAddresses.end());
|
||||||
const auto& defaultWalletAccount = *defaultWalletAccountIt;
|
const auto& defaultWalletAccount = *defaultWalletAccountIt;
|
||||||
ASSERT_EQ(defaultWalletAccount.path, General::PathDefaultWallet);
|
ASSERT_EQ(defaultWalletAccount.path, General::PathDefaultWallet);
|
||||||
ASSERT_EQ(defaultWalletAccount.address, walletAccount.address);
|
ASSERT_EQ(defaultWalletAccount.address, walletAccount.address);
|
||||||
ASSERT_TRUE(defaultWalletAccount.alreadyCreated);
|
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
|
// 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
|
// 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
|
/// 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 testPath = General::PathDefaultWallet;
|
||||||
|
|
||||||
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(),
|
const auto derivedAddresses =
|
||||||
walletAccount.address, testPath, 4, 1);
|
Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(), walletAccount.address, testPath, 4, 1);
|
||||||
ASSERT_EQ(derivedAddresses.size(), 1);
|
ASSERT_EQ(derivedAddresses.size(), 1);
|
||||||
const auto& onlyAccount = derivedAddresses[0];
|
const auto& onlyAccount = derivedAddresses[0];
|
||||||
// all alreadyCreated are false
|
// all alreadyCreated are false
|
||||||
|
@ -145,22 +166,27 @@ TEST(WalletApi, TestGetDerivedAddressesForPath_FromWalletAccount_SecondLevel)
|
||||||
|
|
||||||
const auto walletAccount = testAccount.firstWalletAccount();
|
const auto walletAccount = testAccount.firstWalletAccount();
|
||||||
const auto firstLevelPath = General::PathDefaultWallet;
|
const auto firstLevelPath = General::PathDefaultWallet;
|
||||||
const auto firstLevelAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(),
|
const auto firstLevelAddresses =
|
||||||
walletAccount.address, firstLevelPath, 4, 1);
|
Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(), walletAccount.address, firstLevelPath, 4, 1);
|
||||||
|
|
||||||
const auto testPath = Accounts::DerivationPath{General::PathDefaultWallet.get() + u"/0"_qs};
|
const auto testPath = Accounts::DerivationPath{General::PathDefaultWallet.get() + u"/0"_qs};
|
||||||
|
|
||||||
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(),
|
const auto derivedAddresses =
|
||||||
walletAccount.address, testPath, 4, 1);
|
Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(), walletAccount.address, testPath, 4, 1);
|
||||||
ASSERT_EQ(derivedAddresses.size(), 4);
|
ASSERT_EQ(derivedAddresses.size(), 4);
|
||||||
|
|
||||||
// all alreadyCreated are false
|
// 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
|
// 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
|
// 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(
|
||||||
ASSERT_TRUE(std::all_of(derivedAddresses.begin(), derivedAddresses.end(), [&testPath](const auto& a) { return a.path.get().startsWith(testPath.get()); }));
|
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)
|
TEST(WalletApi, TestGetEthereumChains)
|
||||||
|
@ -169,7 +195,7 @@ TEST(WalletApi, TestGetEthereumChains)
|
||||||
|
|
||||||
auto networks = Wallet::getEthereumChains(false);
|
auto networks = Wallet::getEthereumChains(false);
|
||||||
ASSERT_GT(networks.size(), 0);
|
ASSERT_GT(networks.size(), 0);
|
||||||
const auto &network = networks[0];
|
const auto& network = networks[0];
|
||||||
ASSERT_FALSE(network.chainName.isEmpty());
|
ASSERT_FALSE(network.chainName.isEmpty());
|
||||||
ASSERT_TRUE(network.rpcUrl.isValid());
|
ASSERT_TRUE(network.rpcUrl.isValid());
|
||||||
}
|
}
|
||||||
|
@ -180,15 +206,16 @@ TEST(WalletApi, TestGetTokens)
|
||||||
|
|
||||||
auto networks = Wallet::getEthereumChains(false);
|
auto networks = Wallet::getEthereumChains(false);
|
||||||
ASSERT_GT(networks.size(), 0);
|
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());
|
ASSERT_NE(mainNetIt, networks.end());
|
||||||
const auto &mainNet = *mainNetIt;
|
const auto& mainNet = *mainNetIt;
|
||||||
|
|
||||||
auto tokens = Wallet::getTokens(mainNet.chainId);
|
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());
|
ASSERT_NE(sntIt, tokens.end());
|
||||||
const auto &snt = *sntIt;
|
const auto& snt = *sntIt;
|
||||||
ASSERT_EQ(snt.chainId, mainNet.chainId);
|
ASSERT_EQ(snt.chainId, mainNet.chainId);
|
||||||
ASSERT_TRUE(snt.color.isValid());
|
ASSERT_TRUE(snt.color.isValid());
|
||||||
}
|
}
|
||||||
|
@ -200,32 +227,35 @@ TEST(WalletApi, TestGetTokensBalancesForChainIDs)
|
||||||
auto networks = Wallet::getEthereumChains(false);
|
auto networks = Wallet::getEthereumChains(false);
|
||||||
ASSERT_GT(networks.size(), 1);
|
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());
|
ASSERT_NE(mainNetIt, networks.end());
|
||||||
const auto &mainNet = *mainNetIt;
|
const auto& mainNet = *mainNetIt;
|
||||||
|
|
||||||
auto mainTokens = Wallet::getTokens(mainNet.chainId);
|
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());
|
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());
|
ASSERT_NE(testNetIt, networks.end());
|
||||||
const auto &testNet = *testNetIt;
|
const auto& testNet = *testNetIt;
|
||||||
|
|
||||||
auto testTokens = Wallet::getTokens(testNet.chainId);
|
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());
|
ASSERT_NE(sntTestIt, testTokens.end());
|
||||||
const auto &sntTest = *sntTestIt;
|
const auto& sntTest = *sntTestIt;
|
||||||
|
|
||||||
auto testAddress = testAccount.firstWalletAccount().address;
|
auto testAddress = testAccount.firstWalletAccount().address;
|
||||||
auto balances = Wallet::getTokensBalancesForChainIDs({mainNet.chainId, testNet.chainId},
|
auto balances = Wallet::getTokensBalancesForChainIDs(
|
||||||
{testAddress},
|
{mainNet.chainId, testNet.chainId}, {testAddress}, {sntMain.address, sntTest.address});
|
||||||
{sntMain.address, sntTest.address});
|
|
||||||
ASSERT_GT(balances.size(), 0);
|
ASSERT_GT(balances.size(), 0);
|
||||||
|
|
||||||
ASSERT_TRUE(balances.contains(testAddress));
|
ASSERT_TRUE(balances.contains(testAddress));
|
||||||
const auto &addressBalance = balances[testAddress];
|
const auto& addressBalance = balances[testAddress];
|
||||||
ASSERT_GT(addressBalance.size(), 0);
|
ASSERT_GT(addressBalance.size(), 0);
|
||||||
|
|
||||||
ASSERT_TRUE(addressBalance.contains(sntMain.address));
|
ASSERT_TRUE(addressBalance.contains(sntMain.address));
|
||||||
|
@ -240,43 +270,148 @@ TEST(WalletApi, TestGetTokensBalancesForChainIDs_WatchOnlyAccount)
|
||||||
ScopedTestAccount testAccount(test_info_->name());
|
ScopedTestAccount testAccount(test_info_->name());
|
||||||
|
|
||||||
const auto newTestAccountName = u"test_watch_only-name"_qs;
|
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();
|
const auto updatedAccounts = Accounts::getAccounts();
|
||||||
ASSERT_EQ(updatedAccounts.size(), 3);
|
ASSERT_EQ(updatedAccounts.size(), 3);
|
||||||
|
|
||||||
const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(),
|
const auto newAccountIt =
|
||||||
[&newTestAccountName](const auto& a) {
|
std::find_if(updatedAccounts.begin(), updatedAccounts.end(), [&newTestAccountName](const auto& a) {
|
||||||
return a.name == newTestAccountName;
|
return a.name == newTestAccountName;
|
||||||
});
|
});
|
||||||
ASSERT_NE(newAccountIt, updatedAccounts.end());
|
ASSERT_NE(newAccountIt, updatedAccounts.end());
|
||||||
const auto &newAccount = *newAccountIt;
|
const auto& newAccount = *newAccountIt;
|
||||||
|
|
||||||
auto networks = Wallet::getEthereumChains(false);
|
auto networks = Wallet::getEthereumChains(false);
|
||||||
ASSERT_GT(networks.size(), 1);
|
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());
|
ASSERT_NE(mainNetIt, networks.end());
|
||||||
const auto &mainNet = *mainNetIt;
|
const auto& mainNet = *mainNetIt;
|
||||||
|
|
||||||
auto mainTokens = Wallet::getTokens(mainNet.chainId);
|
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());
|
ASSERT_NE(sntMainIt, mainTokens.end());
|
||||||
const auto &sntMain = *sntMainIt;
|
const auto& sntMain = *sntMainIt;
|
||||||
|
|
||||||
auto balances = Wallet::getTokensBalancesForChainIDs({mainNet.chainId},
|
auto balances = Wallet::getTokensBalancesForChainIDs({mainNet.chainId}, {newAccount.address}, {sntMain.address});
|
||||||
{newAccount.address},
|
|
||||||
{sntMain.address});
|
|
||||||
ASSERT_GT(balances.size(), 0);
|
ASSERT_GT(balances.size(), 0);
|
||||||
|
|
||||||
ASSERT_TRUE(balances.contains(newAccount.address));
|
ASSERT_TRUE(balances.contains(newAccount.address));
|
||||||
const auto &addressBalance = balances[newAccount.address];
|
const auto& addressBalance = balances[newAccount.address];
|
||||||
ASSERT_GT(addressBalance.size(), 0);
|
ASSERT_GT(addressBalance.size(), 0);
|
||||||
|
|
||||||
ASSERT_TRUE(addressBalance.contains(sntMain.address));
|
ASSERT_TRUE(addressBalance.contains(sntMain.address));
|
||||||
ASSERT_GT(addressBalance.at(sntMain.address), 0);
|
ASSERT_GT(addressBalance.at(sntMain.address), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO: this is a debugging test. Augment it with local Ganache environment to have a reliable test
|
||||||
// "{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"no tokens for this network"}}"
|
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
|
||||||
|
|
Loading…
Reference in New Issue