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 <QColor>
|
||||
#include <QDateTime>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
|
@ -97,8 +98,25 @@ struct adl_serializer<std::optional<T>>
|
|||
|
||||
static void from_json(const json& j, std::optional<T>& opt)
|
||||
{
|
||||
if(j.is_null())
|
||||
opt = std::nullopt;
|
||||
else
|
||||
opt.emplace(j.get<T>());
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct adl_serializer<QDateTime>
|
||||
{
|
||||
static void to_json(json& j, const QDateTime& dt)
|
||||
{
|
||||
j = dt.toSecsSinceEpoch();
|
||||
}
|
||||
|
||||
static void from_json(const json& j, QDateTime& dt)
|
||||
{
|
||||
dt = QDateTime::fromSecsSinceEpoch(j.get<qint64>());
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace nlohmann
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
|
||||
#include <optional>
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
std::optional<QString> getDataFromFile(const fs::path& path)
|
||||
{
|
||||
QFile jsonFile{Status::toQString(path)};
|
||||
|
@ -363,7 +365,9 @@ QJsonObject AccountsService::getDefaultNodeConfig(const QString& installationId)
|
|||
{
|
||||
auto templateNodeConfigJsonStr = getDataFromFile(":/Status/StaticConfig/node-config.json").value();
|
||||
auto fleetJson = getDataFromFile(":/Status/StaticConfig/fleets.json").value();
|
||||
auto infuraKey = getDataFromFile(":/Status/StaticConfig/infura_key").value();
|
||||
auto envInfuraKey = qEnvironmentVariable("INFURA_TOKEN");
|
||||
auto infuraKey =
|
||||
envInfuraKey.isEmpty() ? getDataFromFile(":/Status/StaticConfig/infura_key").value() : envInfuraKey;
|
||||
|
||||
auto templateDefaultNetworksJson = getDataFromFile(":/Status/StaticConfig/default-networks.json").value();
|
||||
QString defaultNetworksContent = templateDefaultNetworksJson.replace("%INFURA_TOKEN_RESOLVED%", infuraKey);
|
||||
|
|
|
@ -35,12 +35,12 @@ ScopedTestAccount::ScopedTestAccount(const std::string& tempTestSubfolderName,
|
|||
char* args[] = {appName.data()};
|
||||
m_app = std::make_unique<QCoreApplication>(argc, reinterpret_cast<char**>(args));
|
||||
|
||||
m_testFolderPath = m_fusedTestFolder->tempFolder() / Constants::statusGoDataDirName;
|
||||
fs::create_directory(m_testFolderPath);
|
||||
m_dataDirPath = m_fusedTestFolder->tempFolder() / Constants::statusGoDataDirName;
|
||||
fs::create_directory(m_dataDirPath);
|
||||
|
||||
// Setup accounts
|
||||
auto accountsService = std::make_shared<Onboarding::AccountsService>();
|
||||
auto result = accountsService->init(m_testFolderPath);
|
||||
auto result = accountsService->init(m_dataDirPath);
|
||||
if(!result)
|
||||
{
|
||||
throw std::runtime_error("ScopedTestAccount - Failed to create temporary test account");
|
||||
|
@ -59,12 +59,14 @@ ScopedTestAccount::ScopedTestAccount(const std::string& tempTestSubfolderName,
|
|||
}
|
||||
|
||||
int accountLoggedInCount = 0;
|
||||
QObject::connect(m_onboarding.get(), &Onboarding::OnboardingController::accountLoggedIn, [&accountLoggedInCount]() {
|
||||
accountLoggedInCount++;
|
||||
});
|
||||
QObject::connect(m_onboarding.get(),
|
||||
&Onboarding::OnboardingController::accountLoggedIn,
|
||||
m_app.get(),
|
||||
[&accountLoggedInCount]() { accountLoggedInCount++; });
|
||||
bool accountLoggedInError = false;
|
||||
QObject::connect(m_onboarding.get(),
|
||||
&Onboarding::OnboardingController::accountLoginError,
|
||||
m_app.get(),
|
||||
[&accountLoggedInError]() { accountLoggedInError = true; });
|
||||
|
||||
// Create Accounts
|
||||
|
@ -99,14 +101,15 @@ ScopedTestAccount::ScopedTestAccount(const std::string& tempTestSubfolderName,
|
|||
|
||||
processMessages(2000, [accountLoggedInCount]() { return accountLoggedInCount == 0; });
|
||||
|
||||
if(accountLoggedInCount != 1)
|
||||
{
|
||||
throw std::runtime_error("ScopedTestAccount - missing confirmation of account creation");
|
||||
}
|
||||
if(accountLoggedInError)
|
||||
{
|
||||
throw std::runtime_error("ScopedTestAccount - account loggedin error");
|
||||
}
|
||||
|
||||
if(accountLoggedInCount != 1)
|
||||
{
|
||||
throw std::runtime_error("ScopedTestAccount - missing confirmation of account creation");
|
||||
}
|
||||
}
|
||||
|
||||
ScopedTestAccount::~ScopedTestAccount()
|
||||
|
@ -123,10 +126,12 @@ void ScopedTestAccount::processMessages(size_t maxWaitTimeMillis, std::function<
|
|||
auto remainingIterations = maxWaitTime / iterationSleepTime;
|
||||
while(remainingIterations-- > 0 && shouldWaitUntilTimeout())
|
||||
{
|
||||
std::this_thread::sleep_for(iterationSleepTime);
|
||||
|
||||
QCoreApplication::sendPostedEvents();
|
||||
|
||||
std::this_thread::sleep_for(iterationSleepTime);
|
||||
}
|
||||
// Provide chance to exit slot processing after we set the condition that might trigger shouldWaitUntilTimeout to false
|
||||
std::this_thread::sleep_for(iterationSleepTime);
|
||||
}
|
||||
|
||||
void ScopedTestAccount::logOut()
|
||||
|
@ -171,7 +176,12 @@ Onboarding::OnboardingController* ScopedTestAccount::onboardingController() cons
|
|||
|
||||
const std::filesystem::path& ScopedTestAccount::fusedTestFolder() const
|
||||
{
|
||||
return m_testFolderPath;
|
||||
return m_fusedTestFolder->tempFolder();
|
||||
}
|
||||
|
||||
const std::filesystem::path& ScopedTestAccount::testDataDir() const
|
||||
{
|
||||
return m_dataDirPath;
|
||||
}
|
||||
|
||||
} // namespace Status::Testing
|
||||
|
|
|
@ -58,12 +58,19 @@ public:
|
|||
|
||||
Status::Onboarding::OnboardingController* onboardingController() const;
|
||||
|
||||
/// Temporary test folder that is deleted when class instance goes out of scope
|
||||
const std::filesystem::path& fusedTestFolder() const;
|
||||
const std::filesystem::path& testDataDir() const;
|
||||
|
||||
QCoreApplication* app()
|
||||
{
|
||||
return m_app.get();
|
||||
};
|
||||
|
||||
private:
|
||||
std::unique_ptr<AutoCleanTempTestDir> m_fusedTestFolder;
|
||||
std::unique_ptr<QCoreApplication> m_app;
|
||||
std::filesystem::path m_testFolderPath;
|
||||
std::filesystem::path m_dataDirPath;
|
||||
std::shared_ptr<Status::Onboarding::OnboardingController> m_onboarding;
|
||||
std::function<bool()> m_checkIfShouldContinue;
|
||||
|
||||
|
|
|
@ -93,9 +93,9 @@ TEST(OnboardingModule, TestCreateAndLoginAccountEndToEnd)
|
|||
auto remainingIterations = maxWaitTime / iterationSleepTime;
|
||||
while(remainingIterations-- > 0 && accountLoggedInCount == 0)
|
||||
{
|
||||
std::this_thread::sleep_for(iterationSleepTime);
|
||||
|
||||
QCoreApplication::sendPostedEvents();
|
||||
|
||||
std::this_thread::sleep_for(iterationSleepTime);
|
||||
}
|
||||
|
||||
EXPECT_EQ(accountLoggedInCount, 1);
|
||||
|
@ -115,20 +115,12 @@ TEST(OnboardingModule, TestLoginEndToEnd)
|
|||
QObject::connect(StatusGo::SignalsManager::instance(),
|
||||
&StatusGo::SignalsManager::nodeLogin,
|
||||
[&createAndLogin](const QString& error) {
|
||||
if(error.isEmpty())
|
||||
{
|
||||
if(createAndLogin)
|
||||
{
|
||||
createAndLogin = false;
|
||||
}
|
||||
else
|
||||
createAndLogin = true;
|
||||
}
|
||||
if(error.isEmpty()) createAndLogin = !createAndLogin;
|
||||
});
|
||||
|
||||
constexpr auto accountName = "TestLoginAccountName";
|
||||
ScopedTestAccount testAccount(test_info_->name(), accountName);
|
||||
testAccount.processMessages(1000, [createAndLogin]() { return !createAndLogin; });
|
||||
testAccount.processMessages(1000, [&createAndLogin]() { return !createAndLogin; });
|
||||
ASSERT_TRUE(createAndLogin);
|
||||
|
||||
testAccount.logOut();
|
||||
|
@ -138,7 +130,7 @@ TEST(OnboardingModule, TestLoginEndToEnd)
|
|||
|
||||
// Setup accounts
|
||||
auto accountsService = std::make_shared<Onboarding::AccountsService>();
|
||||
auto result = accountsService->init(testAccount.fusedTestFolder());
|
||||
auto result = accountsService->init(testAccount.testDataDir());
|
||||
ASSERT_TRUE(result);
|
||||
|
||||
auto onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
|
||||
|
@ -156,8 +148,8 @@ TEST(OnboardingModule, TestLoginEndToEnd)
|
|||
QObject::connect(onboarding.get(),
|
||||
&Onboarding::OnboardingController::accountLoginError,
|
||||
[&accountLoggedInError](const QString& error) {
|
||||
accountLoggedInError = true;
|
||||
qDebug() << "Failed logging in in test" << test_info_->name() << "with error:" << error;
|
||||
accountLoggedInError = true;
|
||||
});
|
||||
|
||||
auto ourAccountRes =
|
||||
|
@ -165,11 +157,11 @@ TEST(OnboardingModule, TestLoginEndToEnd)
|
|||
auto errorString = accountsService->login(*ourAccountRes, testAccount.password());
|
||||
ASSERT_EQ(errorString.length(), 0);
|
||||
|
||||
testAccount.processMessages(1000, [accountLoggedInCount, accountLoggedInError]() {
|
||||
testAccount.processMessages(1000, [&accountLoggedInCount, &accountLoggedInError]() {
|
||||
return accountLoggedInCount == 0 && !accountLoggedInError;
|
||||
});
|
||||
ASSERT_EQ(accountLoggedInCount, 1);
|
||||
ASSERT_EQ(accountLoggedInError, 0);
|
||||
ASSERT_EQ(accountLoggedInCount, 1);
|
||||
}
|
||||
|
||||
TEST(OnboardingModule, TestLoginEndToEnd_WrongPassword)
|
||||
|
@ -180,7 +172,7 @@ TEST(OnboardingModule, TestLoginEndToEnd_WrongPassword)
|
|||
testAccount.logOut();
|
||||
|
||||
auto accountsService = std::make_shared<Onboarding::AccountsService>();
|
||||
auto result = accountsService->init(testAccount.fusedTestFolder());
|
||||
auto result = accountsService->init(testAccount.testDataDir());
|
||||
ASSERT_TRUE(result);
|
||||
auto onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
|
||||
auto accounts = accountsService->openAndListAccounts();
|
||||
|
@ -205,7 +197,7 @@ TEST(OnboardingModule, TestLoginEndToEnd_WrongPassword)
|
|||
auto errorString = accountsService->login(*ourAccountRes, testAccount.password() + "extra");
|
||||
ASSERT_EQ(errorString.length(), 0);
|
||||
|
||||
testAccount.processMessages(1000, [accountLoggedInCount, accountLoggedInError]() {
|
||||
testAccount.processMessages(1000, [&accountLoggedInCount, &accountLoggedInError]() {
|
||||
return accountLoggedInCount == 0 && !accountLoggedInError;
|
||||
});
|
||||
ASSERT_EQ(accountLoggedInError, 1);
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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))
|
||||
{ }
|
||||
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
#include "SignalsManager.h"
|
||||
#include "StatusGoEvent.h"
|
||||
|
||||
#include <QtConcurrent>
|
||||
|
||||
#include <libstatus.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
using namespace std::string_literals;
|
||||
|
||||
namespace Status::StatusGo
|
||||
|
@ -11,6 +17,11 @@ namespace Status::StatusGo
|
|||
|
||||
std::map<std::string, SignalType> SignalsManager::signalMap;
|
||||
|
||||
EventData::EventData(nlohmann::json eventInfo, bool error)
|
||||
: m_eventInfo(std::move(eventInfo))
|
||||
, m_hasError(error)
|
||||
{ }
|
||||
|
||||
// TODO: make me thread safe or better refactor into broadcasting mechanism
|
||||
SignalsManager* SignalsManager::instance()
|
||||
{
|
||||
|
@ -21,9 +32,13 @@ SignalsManager* SignalsManager::instance()
|
|||
SignalsManager::SignalsManager()
|
||||
: QObject(nullptr)
|
||||
{
|
||||
// Don't allow async signal processing in attept to debug the the linux running tests issue
|
||||
m_threadPool.setMaxThreadCount(1);
|
||||
|
||||
SetSignalEventCallback((void*)&SignalsManager::signalCallback);
|
||||
|
||||
signalMap = {{"node.ready"s, SignalType::NodeReady},
|
||||
signalMap = {
|
||||
{"node.ready"s, SignalType::NodeReady},
|
||||
{"node.started"s, SignalType::NodeStarted},
|
||||
{"node.stopped"s, SignalType::NodeStopped},
|
||||
{"node.login"s, SignalType::NodeLogin},
|
||||
|
@ -33,49 +48,58 @@ SignalsManager::SignalsManager()
|
|||
{"discovery.stopped"s, SignalType::DiscoveryStopped},
|
||||
{"discovery.summary"s, SignalType::DiscoverySummary},
|
||||
|
||||
{"mediaserver.started"s, SignalType::MailserverStarted},
|
||||
{"mailserver.changed"s, SignalType::MailserverChanged},
|
||||
{"mailserver.available"s, SignalType::MailserverAvailable},
|
||||
|
||||
{"history.request.started"s, SignalType::HistoryRequestStarted},
|
||||
{"history.request.batch.processed"s, SignalType::HistoryRequestBatchProcessed},
|
||||
{"history.request.completed"s, SignalType::HistoryRequestCompleted}};
|
||||
{"history.request.completed"s, SignalType::HistoryRequestCompleted},
|
||||
|
||||
{"wallet"s, SignalType::WalletEvent},
|
||||
};
|
||||
}
|
||||
|
||||
SignalsManager::~SignalsManager() { }
|
||||
|
||||
void SignalsManager::processSignal(const QString& statusSignal)
|
||||
void SignalsManager::processSignal(const char* statusSignalData)
|
||||
{
|
||||
// TODO: overkill, use some kind of message broker
|
||||
using namespace std::chrono_literals;
|
||||
auto dataStrPtr = std::make_shared<std::string>(statusSignalData);
|
||||
m_threadPool.start(QRunnable::create([dataStrPtr, this]() {
|
||||
try
|
||||
{
|
||||
QJsonParseError json_error;
|
||||
const QJsonDocument signalEventDoc(QJsonDocument::fromJson(statusSignal.toUtf8(), &json_error));
|
||||
if(json_error.error != QJsonParseError::NoError)
|
||||
StatusGoEvent event = json::parse(*dataStrPtr);
|
||||
if(event.error != std::nullopt)
|
||||
{
|
||||
qWarning() << "Invalid signal received";
|
||||
qWarning() << "Error in signal" << event.type.c_str() << "; error" << event.error.value();
|
||||
// TODO report event error
|
||||
return;
|
||||
}
|
||||
decode(signalEventDoc.object());
|
||||
|
||||
QString signalError;
|
||||
if(event.event.contains("error")) signalError = event.event["error"].get<QString>();
|
||||
|
||||
dispatch(event.type, std::move(event.event), signalError);
|
||||
}
|
||||
catch(const std::exception& e)
|
||||
{
|
||||
qWarning() << "Error decoding signal, err: ", e.what();
|
||||
return;
|
||||
qWarning() << "Error decoding signal, err: " << e.what() << "; signal data: " << dataStrPtr->c_str();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void SignalsManager::decode(const QJsonObject& signalEvent)
|
||||
void SignalsManager::dispatch(const std::string& type, json signalEvent, const QString& signalError)
|
||||
{
|
||||
SignalType signalType(Unknown);
|
||||
auto signalName = signalEvent["type"].toString().toStdString();
|
||||
if(!signalMap.contains(signalName))
|
||||
if(!signalMap.contains(type))
|
||||
{
|
||||
qWarning() << "Unknown signal received: " << signalName.c_str();
|
||||
qWarning() << "Unknown signal received: " << type.c_str();
|
||||
return;
|
||||
}
|
||||
|
||||
signalType = signalMap[signalName];
|
||||
auto signalError = signalEvent["event"]["error"].toString();
|
||||
|
||||
signalType = signalMap[type];
|
||||
switch(signalType)
|
||||
{
|
||||
// TODO: create extractor functions like in nim
|
||||
|
@ -89,22 +113,21 @@ void SignalsManager::decode(const QJsonObject& signalEvent)
|
|||
break;
|
||||
case DiscoveryStarted: emit discoveryStarted(signalError); break;
|
||||
case DiscoveryStopped: emit discoveryStopped(signalError); break;
|
||||
case DiscoverySummary: emit discoverySummary(signalEvent["event"].toArray().count(), signalError); break;
|
||||
case DiscoverySummary: emit discoverySummary(signalEvent.array().size(), signalError); break;
|
||||
case MailserverStarted: emit mailserverStarted(signalError); break;
|
||||
case MailserverChanged: emit mailserverChanged(signalError); break;
|
||||
case MailserverAvailable: emit mailserverAvailable(signalError); break;
|
||||
case HistoryRequestStarted: emit historyRequestStarted(signalError); break;
|
||||
case HistoryRequestBatchProcessed: emit historyRequestBatchProcessed(signalError); break;
|
||||
case HistoryRequestCompleted: emit historyRequestCompleted(signalError); break;
|
||||
case WalletEvent: emit wallet(EventDataQPtr(new EventData(std::move(signalEvent), false))); break;
|
||||
case Unknown: assert(false); break;
|
||||
}
|
||||
}
|
||||
|
||||
void SignalsManager::signalCallback(const char* data)
|
||||
{
|
||||
// TODO: overkill, use some kind of message broker
|
||||
auto dataStrPtr = std::make_shared<QString>(data);
|
||||
QFuture<void> future =
|
||||
QtConcurrent::run([dataStrPtr]() { SignalsManager::instance()->processSignal(*dataStrPtr); });
|
||||
SignalsManager::instance()->processSignal(data);
|
||||
}
|
||||
|
||||
} // namespace Status::StatusGo
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QThreadPool>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace Status::StatusGo
|
||||
{
|
||||
|
@ -18,14 +21,36 @@ enum SignalType
|
|||
DiscoveryStopped,
|
||||
DiscoverySummary,
|
||||
|
||||
MailserverStarted,
|
||||
MailserverChanged,
|
||||
MailserverAvailable,
|
||||
|
||||
HistoryRequestStarted,
|
||||
HistoryRequestBatchProcessed,
|
||||
HistoryRequestCompleted
|
||||
HistoryRequestCompleted,
|
||||
|
||||
WalletEvent
|
||||
};
|
||||
|
||||
class EventData final : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit EventData(nlohmann::json eventInfo, bool error);
|
||||
|
||||
const nlohmann::json& eventInfo() const
|
||||
{
|
||||
return m_eventInfo;
|
||||
};
|
||||
|
||||
private:
|
||||
nlohmann::json m_eventInfo;
|
||||
bool m_hasError;
|
||||
};
|
||||
|
||||
using EventDataQPtr = QSharedPointer<Status::StatusGo::EventData>;
|
||||
|
||||
/*!
|
||||
\todo refactor into a message broker helper to be used by specific service APIs to deliver signals
|
||||
as part of the specific StatusGoAPI service
|
||||
|
@ -38,9 +63,10 @@ class SignalsManager final : public QObject
|
|||
public:
|
||||
static SignalsManager* instance();
|
||||
|
||||
void processSignal(const QString& ev);
|
||||
void processSignal(const char* statusSignalData);
|
||||
|
||||
signals:
|
||||
// TODO: move all signals to deliver EventData, distributing this way data processing to the consumer
|
||||
void nodeReady(const QString& error);
|
||||
void nodeStarted(const QString& error);
|
||||
void nodeStopped(const QString& error);
|
||||
|
@ -51,6 +77,7 @@ signals:
|
|||
void discoveryStopped(const QString& error);
|
||||
void discoverySummary(size_t nodeCount, const QString& error);
|
||||
|
||||
void mailserverStarted(const QString& error);
|
||||
void mailserverChanged(const QString& error);
|
||||
void mailserverAvailable(const QString& error);
|
||||
|
||||
|
@ -58,6 +85,8 @@ signals:
|
|||
void historyRequestBatchProcessed(const QString& error);
|
||||
void historyRequestCompleted(const QString& error);
|
||||
|
||||
void wallet(QSharedPointer<Status::StatusGo::EventData> eventData);
|
||||
|
||||
private:
|
||||
explicit SignalsManager();
|
||||
~SignalsManager();
|
||||
|
@ -65,7 +94,10 @@ private:
|
|||
private:
|
||||
static std::map<std::string, SignalType> signalMap;
|
||||
static void signalCallback(const char* data);
|
||||
void decode(const QJsonObject& signalEvent);
|
||||
|
||||
void dispatch(const std::string& type, nlohmann::json signalEvent, const QString& signalError);
|
||||
|
||||
QThreadPool m_threadPool;
|
||||
};
|
||||
|
||||
} // namespace Status::StatusGo
|
||||
|
|
|
@ -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,6 +48,9 @@ struct adl_serializer<GoWallet::BigInt>
|
|||
|
||||
static void from_json(const json& j, GoWallet::BigInt& num)
|
||||
{
|
||||
if(j.is_number())
|
||||
num = GoWallet::BigInt(j.get<long long>());
|
||||
else
|
||||
num = GoWallet::BigInt(j.get<std::string>());
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
const std::vector<Accounts::EOAddress> accounts,
|
||||
const std::vector<Accounts::EOAddress> tokens)
|
||||
const std::vector<Accounts::EOAddress>& accounts,
|
||||
const std::vector<Accounts::EOAddress>& tokens)
|
||||
{
|
||||
std::vector<json> params = {chainIds, accounts, tokens};
|
||||
json inputJson = {{"jsonrpc", "2.0"}, {"method", "wallet_getTokensBalancesForChainIDs"}, {"params", params}};
|
||||
|
@ -108,4 +108,26 @@ TokenBalances getTokensBalancesForChainIDs(const std::vector<ChainID>& chainIds,
|
|||
return resultData;
|
||||
}
|
||||
|
||||
std::vector<TokenBalanceHistory>
|
||||
getBalanceHistoryOnChain(Accounts::EOAddress account, const std::chrono::seconds& secondsToNow, int sampleCount)
|
||||
{
|
||||
std::vector<json> params = {account, secondsToNow.count(), sampleCount};
|
||||
json inputJson = {{"jsonrpc", "2.0"}, {"method", "wallet_getBalanceHistoryOnChain"}, {"params", params}};
|
||||
|
||||
auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str());
|
||||
const auto resultJson = json::parse(result);
|
||||
checkPrivateRpcCallResultAndReportError(resultJson);
|
||||
|
||||
return resultJson.get<CallPrivateRpcResponse>().result;
|
||||
}
|
||||
|
||||
void checkRecentHistory(const std::vector<Accounts::EOAddress>& accounts)
|
||||
{
|
||||
std::vector<json> params = {accounts};
|
||||
json inputJson = {{"jsonrpc", "2.0"}, {"method", "wallet_checkRecentHistory"}, {"params", params}};
|
||||
auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str());
|
||||
const auto resultJson = json::parse(result);
|
||||
checkPrivateRpcCallResultAndReportError(resultJson);
|
||||
}
|
||||
|
||||
} // namespace Status::StatusGo::Wallet
|
||||
|
|
|
@ -11,8 +11,12 @@
|
|||
|
||||
#include "Types.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QDateTime>
|
||||
|
||||
namespace Accounts = Status::StatusGo::Accounts;
|
||||
|
||||
namespace Status::StatusGo::Wallet
|
||||
|
@ -52,9 +56,31 @@ NetworkConfigurations getEthereumChains(bool onlyEnabled);
|
|||
Tokens getTokens(const ChainID& chainId);
|
||||
|
||||
using TokenBalances = std::map<Accounts::EOAddress, std::map<Accounts::EOAddress, BigInt>>;
|
||||
/// \note status-go's @api.go -> <xx>@<xx>.go
|
||||
/// \note status-go's API -> GetTokensBalancesForChainIDs<@api.go
|
||||
/// \throws \c CallPrivateRpcError
|
||||
TokenBalances getTokensBalancesForChainIDs(const std::vector<ChainID>& chainIds,
|
||||
const std::vector<Accounts::EOAddress> accounts,
|
||||
const std::vector<Accounts::EOAddress> tokens);
|
||||
const std::vector<Accounts::EOAddress>& accounts,
|
||||
const std::vector<Accounts::EOAddress>& tokens);
|
||||
|
||||
struct TokenBalanceHistory
|
||||
{
|
||||
BigInt value;
|
||||
QDateTime time;
|
||||
};
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(TokenBalanceHistory, value, time)
|
||||
|
||||
/// \warning it relies on the stored transaction data fetched by calling \c checkRecentHistory
|
||||
/// \todo reconsider \c checkRecentHistory dependency
|
||||
///
|
||||
/// \see checkRecentHistory
|
||||
/// \note status-go's API -> GetBalanceHistoryOnChain@api.go
|
||||
/// \throws \c CallPrivateRpcError
|
||||
std::vector<TokenBalanceHistory>
|
||||
getBalanceHistoryOnChain(Accounts::EOAddress account, const std::chrono::seconds& secondsToNow, int sampleCount);
|
||||
|
||||
/// \note status-go's API -> CheckRecentHistory@api.go
|
||||
/// \throws \c CallPrivateRpcError
|
||||
void checkRecentHistory(const std::vector<Accounts::EOAddress>& accounts);
|
||||
|
||||
} // namespace Status::StatusGo::Wallet
|
||||
|
|
|
@ -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,12 +18,11 @@ namespace Status::Testing {
|
|||
TEST(MessagingApi, TestStartMessaging)
|
||||
{
|
||||
bool nodeReady = false;
|
||||
QObject::connect(StatusGo::SignalsManager::instance(), &StatusGo::SignalsManager::nodeReady, [&nodeReady](const QString& error) {
|
||||
if(error.isEmpty()) {
|
||||
if(nodeReady) {
|
||||
nodeReady = false;
|
||||
} else
|
||||
nodeReady = true;
|
||||
QObject::connect(
|
||||
StatusGo::SignalsManager::instance(), &StatusGo::SignalsManager::nodeReady, [&nodeReady](const QString& error) {
|
||||
if(error.isEmpty())
|
||||
{
|
||||
nodeReady = !nodeReady;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
#include <StatusGo/SignalsManager.h>
|
||||
|
||||
#include <StatusGo/Accounts/AccountsAPI.h>
|
||||
|
||||
#include <StatusGo/Wallet/Transfer/Event.h>
|
||||
#include <StatusGo/Wallet/WalletApi.h>
|
||||
#include <StatusGo/Wallet/wallet_types.h>
|
||||
|
||||
#include <StatusGo/Metadata/api_response.h>
|
||||
|
||||
#include <Onboarding/Accounts/AccountsServiceInterface.h>
|
||||
#include <Onboarding/Accounts/AccountsService.h>
|
||||
#include <Onboarding/Accounts/AccountsServiceInterface.h>
|
||||
#include <Onboarding/Common/Constants.h>
|
||||
#include <Onboarding/OnboardingController.h>
|
||||
|
||||
#include <ScopedTestAccount.h>
|
||||
#include <StatusGo/Utils.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace Wallet = Status::StatusGo::Wallet;
|
||||
|
@ -21,10 +26,13 @@ namespace General = Status::Constants::General;
|
|||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
/// \warning for now this namespace contains integration test to check the basic assumptions of status-go while building the C++ wrapper.
|
||||
/// \warning the tests depend on IO and are not deterministic, fast, focused or reliable. They are here for validation only
|
||||
/// \todo after status-go API coverage all the integration tests should go away and only test the thin wrapper code
|
||||
namespace Status::Testing {
|
||||
namespace Status::Testing
|
||||
{
|
||||
|
||||
TEST(WalletApi, TestGetDerivedAddressesForPath_FromRootAccount)
|
||||
{
|
||||
|
@ -37,25 +45,30 @@ TEST(WalletApi, TestGetDerivedAddressesForPath_FromRootAccount)
|
|||
|
||||
const auto testPath = General::PathWalletRoot;
|
||||
|
||||
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(),
|
||||
walletAccount.derivedFrom.value(), testPath, 3, 1);
|
||||
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(
|
||||
testAccount.hashedPassword(), walletAccount.derivedFrom.value(), testPath, 3, 1);
|
||||
// Check that accounts are generated in memory and none is saved
|
||||
const auto updatedAccounts = Accounts::getAccounts();
|
||||
ASSERT_EQ(updatedAccounts.size(), 2);
|
||||
|
||||
ASSERT_EQ(derivedAddresses.size(), 3);
|
||||
auto defaultWalletAccountIt = std::find_if(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; });
|
||||
auto defaultWalletAccountIt =
|
||||
std::find_if(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; });
|
||||
ASSERT_NE(defaultWalletAccountIt, derivedAddresses.end());
|
||||
const auto& defaultWalletAccount = *defaultWalletAccountIt;
|
||||
ASSERT_EQ(defaultWalletAccount.path, General::PathDefaultWallet);
|
||||
ASSERT_EQ(defaultWalletAccount.address, walletAccount.address);
|
||||
ASSERT_TRUE(defaultWalletAccount.alreadyCreated);
|
||||
|
||||
ASSERT_EQ(1, std::count_if(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; }));
|
||||
ASSERT_EQ(1, std::count_if(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) {
|
||||
return a.alreadyCreated;
|
||||
}));
|
||||
// all hasActivity are false
|
||||
ASSERT_TRUE(std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.hasActivity; }));
|
||||
ASSERT_TRUE(
|
||||
std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.hasActivity; }));
|
||||
// all address are valid
|
||||
ASSERT_TRUE(std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.address.get().isEmpty(); }));
|
||||
ASSERT_TRUE(std::none_of(
|
||||
derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.address.get().isEmpty(); }));
|
||||
}
|
||||
|
||||
TEST(Accounts, TestGetDerivedAddressesForPath_AfterLogin)
|
||||
|
@ -66,7 +79,7 @@ TEST(Accounts, TestGetDerivedAddressesForPath_AfterLogin)
|
|||
testAccount.logOut();
|
||||
|
||||
auto accountsService = std::make_shared<Onboarding::AccountsService>();
|
||||
auto result = accountsService->init(testAccount.fusedTestFolder());
|
||||
auto result = accountsService->init(testAccount.testDataDir());
|
||||
ASSERT_TRUE(result);
|
||||
auto onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
|
||||
EXPECT_EQ(onboarding->getOpenedAccounts().size(), 1);
|
||||
|
@ -79,16 +92,20 @@ TEST(Accounts, TestGetDerivedAddressesForPath_AfterLogin)
|
|||
accountLoggedInCount++;
|
||||
});
|
||||
bool accountLoggedInError = false;
|
||||
QObject::connect(onboarding.get(), &Onboarding::OnboardingController::accountLoginError, [&accountLoggedInError](const QString& error) {
|
||||
QObject::connect(onboarding.get(),
|
||||
&Onboarding::OnboardingController::accountLoginError,
|
||||
[&accountLoggedInError](const QString& error) {
|
||||
accountLoggedInError = true;
|
||||
qDebug() << "Failed logging in in test" << test_info_->name() << "with error:" << error;
|
||||
});
|
||||
|
||||
auto ourAccountRes = std::find_if(accounts.begin(), accounts.end(), [&testRootAccountName](const auto &a) { return a.name == testRootAccountName; });
|
||||
auto ourAccountRes = std::find_if(accounts.begin(), accounts.end(), [testRootAccountName](const auto& a) {
|
||||
return a.name == testRootAccountName;
|
||||
});
|
||||
auto errorString = accountsService->login(*ourAccountRes, testAccount.password());
|
||||
ASSERT_EQ(errorString.length(), 0);
|
||||
|
||||
testAccount.processMessages(1000, [accountLoggedInCount, accountLoggedInError]() {
|
||||
testAccount.processMessages(1000, [&accountLoggedInCount, &accountLoggedInError]() {
|
||||
return accountLoggedInCount == 0 && !accountLoggedInError;
|
||||
});
|
||||
ASSERT_EQ(accountLoggedInCount, 1);
|
||||
|
@ -97,26 +114,30 @@ TEST(Accounts, TestGetDerivedAddressesForPath_AfterLogin)
|
|||
const auto testPath = General::PathWalletRoot;
|
||||
|
||||
const auto walletAccount = testAccount.firstWalletAccount();
|
||||
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(),
|
||||
walletAccount.derivedFrom.value(),
|
||||
testPath, 3, 1);
|
||||
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(
|
||||
testAccount.hashedPassword(), walletAccount.derivedFrom.value(), testPath, 3, 1);
|
||||
// Check that accounts are generated in memory and none is saved
|
||||
const auto updatedAccounts = Accounts::getAccounts();
|
||||
ASSERT_EQ(updatedAccounts.size(), 2);
|
||||
|
||||
ASSERT_EQ(derivedAddresses.size(), 3);
|
||||
auto defaultWalletAccountIt = std::find_if(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; });
|
||||
auto defaultWalletAccountIt =
|
||||
std::find_if(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; });
|
||||
ASSERT_NE(defaultWalletAccountIt, derivedAddresses.end());
|
||||
const auto& defaultWalletAccount = *defaultWalletAccountIt;
|
||||
ASSERT_EQ(defaultWalletAccount.path, General::PathDefaultWallet);
|
||||
ASSERT_EQ(defaultWalletAccount.address, walletAccount.address);
|
||||
ASSERT_TRUE(defaultWalletAccount.alreadyCreated);
|
||||
|
||||
ASSERT_EQ(1, std::count_if(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; }));
|
||||
ASSERT_EQ(1, std::count_if(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) {
|
||||
return a.alreadyCreated;
|
||||
}));
|
||||
// all hasActivity are false
|
||||
ASSERT_TRUE(std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.hasActivity; }));
|
||||
ASSERT_TRUE(
|
||||
std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.hasActivity; }));
|
||||
// all address are valid
|
||||
ASSERT_TRUE(std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.address.get().isEmpty(); }));
|
||||
ASSERT_TRUE(std::none_of(
|
||||
derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.address.get().isEmpty(); }));
|
||||
}
|
||||
|
||||
/// getDerivedAddresses@api.go fron statys-go has a special case when requesting the 6 path will return only one account
|
||||
|
@ -129,8 +150,8 @@ TEST(WalletApi, TestGetDerivedAddressesForPath_FromWalletAccount_FirstLevel_SixP
|
|||
|
||||
const auto testPath = General::PathDefaultWallet;
|
||||
|
||||
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(),
|
||||
walletAccount.address, testPath, 4, 1);
|
||||
const auto derivedAddresses =
|
||||
Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(), walletAccount.address, testPath, 4, 1);
|
||||
ASSERT_EQ(derivedAddresses.size(), 1);
|
||||
const auto& onlyAccount = derivedAddresses[0];
|
||||
// all alreadyCreated are false
|
||||
|
@ -145,22 +166,27 @@ TEST(WalletApi, TestGetDerivedAddressesForPath_FromWalletAccount_SecondLevel)
|
|||
|
||||
const auto walletAccount = testAccount.firstWalletAccount();
|
||||
const auto firstLevelPath = General::PathDefaultWallet;
|
||||
const auto firstLevelAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(),
|
||||
walletAccount.address, firstLevelPath, 4, 1);
|
||||
const auto firstLevelAddresses =
|
||||
Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(), walletAccount.address, firstLevelPath, 4, 1);
|
||||
|
||||
const auto testPath = Accounts::DerivationPath{General::PathDefaultWallet.get() + u"/0"_qs};
|
||||
|
||||
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(),
|
||||
walletAccount.address, testPath, 4, 1);
|
||||
const auto derivedAddresses =
|
||||
Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(), walletAccount.address, testPath, 4, 1);
|
||||
ASSERT_EQ(derivedAddresses.size(), 4);
|
||||
|
||||
// all alreadyCreated are false
|
||||
ASSERT_TRUE(std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; }));
|
||||
ASSERT_TRUE(
|
||||
std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; }));
|
||||
// all hasActivity are false
|
||||
ASSERT_TRUE(std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.hasActivity; }));
|
||||
ASSERT_TRUE(
|
||||
std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.hasActivity; }));
|
||||
// all address are valid
|
||||
ASSERT_TRUE(std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.address.get().isEmpty(); }));
|
||||
ASSERT_TRUE(std::all_of(derivedAddresses.begin(), derivedAddresses.end(), [&testPath](const auto& a) { return a.path.get().startsWith(testPath.get()); }));
|
||||
ASSERT_TRUE(std::none_of(
|
||||
derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.address.get().isEmpty(); }));
|
||||
ASSERT_TRUE(std::all_of(derivedAddresses.begin(), derivedAddresses.end(), [testPath](const auto& a) {
|
||||
return a.path.get().startsWith(testPath.get());
|
||||
}));
|
||||
}
|
||||
|
||||
TEST(WalletApi, TestGetEthereumChains)
|
||||
|
@ -180,7 +206,8 @@ 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;
|
||||
|
||||
|
@ -200,28 +227,31 @@ 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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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));
|
||||
|
@ -240,12 +270,15 @@ TEST(WalletApi, TestGetTokensBalancesForChainIDs_WatchOnlyAccount)
|
|||
ScopedTestAccount testAccount(test_info_->name());
|
||||
|
||||
const auto newTestAccountName = u"test_watch_only-name"_qs;
|
||||
Accounts::addAccountWatch(Accounts::EOAddress("0xdb5ac1a559b02e12f29fc0ec0e37be8e046def49"), newTestAccountName, QColor("fuchsia"), u""_qs);
|
||||
Accounts::addAccountWatch(Accounts::EOAddress("0xdb5ac1a559b02e12f29fc0ec0e37be8e046def49"),
|
||||
newTestAccountName,
|
||||
QColor("fuchsia"),
|
||||
u""_qs);
|
||||
const auto updatedAccounts = Accounts::getAccounts();
|
||||
ASSERT_EQ(updatedAccounts.size(), 3);
|
||||
|
||||
const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(),
|
||||
[&newTestAccountName](const auto& a) {
|
||||
const auto newAccountIt =
|
||||
std::find_if(updatedAccounts.begin(), updatedAccounts.end(), [&newTestAccountName](const auto& a) {
|
||||
return a.name == newTestAccountName;
|
||||
});
|
||||
ASSERT_NE(newAccountIt, updatedAccounts.end());
|
||||
|
@ -254,18 +287,18 @@ TEST(WalletApi, TestGetTokensBalancesForChainIDs_WatchOnlyAccount)
|
|||
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;
|
||||
|
||||
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;
|
||||
|
||||
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));
|
||||
|
@ -276,7 +309,109 @@ TEST(WalletApi, TestGetTokensBalancesForChainIDs_WatchOnlyAccount)
|
|||
ASSERT_GT(addressBalance.at(sntMain.address), 0);
|
||||
}
|
||||
|
||||
// TODO
|
||||
// "{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"no tokens for this network"}}"
|
||||
// TODO: this is a debugging test. Augment it with local Ganache environment to have a reliable test
|
||||
TEST(WalletApi, TestCheckRecentHistory)
|
||||
{
|
||||
ScopedTestAccount testAccount(test_info_->name());
|
||||
|
||||
} // namespace
|
||||
// Add watch account
|
||||
const auto newTestAccountName = u"test_watch_only-name"_qs;
|
||||
Accounts::addAccountWatch(Accounts::EOAddress("0xe74E17D586227691Cb7b64ed78b1b7B14828B034"),
|
||||
newTestAccountName,
|
||||
QColor("fuchsia"),
|
||||
u""_qs);
|
||||
const auto updatedAccounts = Accounts::getAccounts();
|
||||
ASSERT_EQ(updatedAccounts.size(), 3);
|
||||
|
||||
const auto newAccountIt =
|
||||
std::find_if(updatedAccounts.begin(), updatedAccounts.end(), [newTestAccountName](const auto& a) {
|
||||
return a.name == newTestAccountName;
|
||||
});
|
||||
ASSERT_NE(newAccountIt, updatedAccounts.end());
|
||||
const auto& newAccount = *newAccountIt;
|
||||
|
||||
bool startedTransferFetching = false;
|
||||
bool historyReady = false;
|
||||
QObject::connect(StatusGo::SignalsManager::instance(),
|
||||
&StatusGo::SignalsManager::wallet,
|
||||
testAccount.app(),
|
||||
[&startedTransferFetching, &historyReady](QSharedPointer<StatusGo::EventData> data) {
|
||||
Wallet::Transfer::Event event = data->eventInfo();
|
||||
if(event.type == Wallet::Transfer::Events::FetchingRecentHistory)
|
||||
startedTransferFetching = true;
|
||||
else if(event.type == Wallet::Transfer::Events::RecentHistoryReady)
|
||||
historyReady = true;
|
||||
// Wallet::Transfer::Events::NewTransfers might not be emitted if there is no intermediate transfers
|
||||
});
|
||||
|
||||
Wallet::checkRecentHistory({newAccount.address});
|
||||
|
||||
testAccount.processMessages(50000, [&historyReady]() { return !historyReady; });
|
||||
|
||||
ASSERT_TRUE(startedTransferFetching);
|
||||
ASSERT_TRUE(historyReady);
|
||||
}
|
||||
|
||||
// TODO: this is a debugging test. Augment it with local Ganache environment to have a reliable test
|
||||
TEST(WalletApi, TestGetBalanceHistoryOnChain)
|
||||
{
|
||||
ScopedTestAccount testAccount(test_info_->name());
|
||||
|
||||
// Add watch account
|
||||
const auto newTestAccountName = u"test_watch_only-name"_qs;
|
||||
Accounts::addAccountWatch(Accounts::EOAddress("0x473780deAF4a2Ac070BBbA936B0cdefe7F267dFc"),
|
||||
newTestAccountName,
|
||||
QColor("fuchsia"),
|
||||
u""_qs);
|
||||
const auto updatedAccounts = Accounts::getAccounts();
|
||||
ASSERT_EQ(updatedAccounts.size(), 3);
|
||||
|
||||
const auto newAccountIt =
|
||||
std::find_if(updatedAccounts.begin(), updatedAccounts.end(), [newTestAccountName](const auto& a) {
|
||||
return a.name == newTestAccountName;
|
||||
});
|
||||
ASSERT_NE(newAccountIt, updatedAccounts.end());
|
||||
const auto& newAccount = *newAccountIt;
|
||||
|
||||
auto testIntervals = {std::chrono::round<std::chrono::seconds>(1h),
|
||||
std::chrono::round<std::chrono::seconds>(std::chrono::days(1)),
|
||||
std::chrono::round<std::chrono::seconds>(std::chrono::days(7)),
|
||||
std::chrono::round<std::chrono::seconds>(std::chrono::months(1)),
|
||||
std::chrono::round<std::chrono::seconds>(std::chrono::months(6)),
|
||||
std::chrono::round<std::chrono::seconds>(std::chrono::years(1)),
|
||||
std::chrono::round<std::chrono::seconds>(std::chrono::years(100))};
|
||||
auto sampleCount = 10;
|
||||
for(const auto& historyDuration : testIntervals)
|
||||
{
|
||||
auto balanceHistory = Wallet::getBalanceHistoryOnChain(newAccount.address, historyDuration, sampleCount);
|
||||
ASSERT_TRUE(balanceHistory.size() > 0); // TODO: we get one extra, match sample size
|
||||
|
||||
auto weiToEth = [](const StatusGo::Wallet::BigInt& wei) -> double {
|
||||
StatusGo::Wallet::BigInt q; // wei / eth
|
||||
StatusGo::Wallet::BigInt r; // wei % eth
|
||||
auto weiD = StatusGo::Wallet::BigInt("1000000000000000000");
|
||||
boost::multiprecision::divide_qr(wei, weiD, q, r);
|
||||
StatusGo::Wallet::BigInt rSzabos; // r / szaboD
|
||||
StatusGo::Wallet::BigInt qSzabos; // r % szaboD
|
||||
auto szaboD = StatusGo::Wallet::BigInt("1000000000000");
|
||||
boost::multiprecision::divide_qr(r, szaboD, qSzabos, rSzabos);
|
||||
return q.convert_to<double>() + (qSzabos.convert_to<double>() / ((weiD / szaboD).convert_to<double>()));
|
||||
};
|
||||
|
||||
QFile file(QString("/tmp/balance_history-%1s.csv").arg(historyDuration.count()));
|
||||
if(file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
{
|
||||
QTextStream out(&file);
|
||||
out << "Balance, Timestamp" << Qt::endl;
|
||||
for(int i = balanceHistory.size() - 1; i >= 0; --i)
|
||||
{
|
||||
out << weiToEth(balanceHistory[i].value) << "," << balanceHistory[i].time.toSecsSinceEpoch()
|
||||
<< Qt::endl;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
sampleCount += 10;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Status::Testing
|
||||
|
|
Loading…
Reference in New Issue