chore(CPP): Test basic functionality for wallet status-go wrapper API

Considerations

- MultiAccountStoreAccount is required for generating a new account.
  MultiAccountStoreDerivedAccounts won't be enough even that it works
  for creating initial account and login code
- Validate the understanding that `rootAccount.address` is `walletAccount.derivedFrom`

Updates: 6321
This commit is contained in:
Stefan 2022-07-07 20:16:59 +02:00 committed by Stefan Dunca
parent 3bfbe104a4
commit 1983443608
51 changed files with 1159 additions and 189 deletions

View File

@ -29,7 +29,7 @@ endif()
add_subdirectory(vendor) add_subdirectory(vendor)
add_subdirectory(libs) add_subdirectory(libs)
add_subdirectory(app) add_subdirectory(app)
add_subdirectory(test) add_subdirectory(test/libs/StatusGoQt)
# TODO: temporary not to duplicate resources until we switch to c++ app then it can be refactored # TODO: temporary not to duplicate resources until we switch to c++ app then it can be refactored
add_subdirectory(resources) add_subdirectory(resources)
add_subdirectory(ui/imports/assets) add_subdirectory(ui/imports/assets)

View File

@ -21,6 +21,8 @@ target_link_libraries(ApplicationCore
PRIVATE PRIVATE
Qt6::Quick Qt6::Quick
Qt6::Qml Qt6::Qml
Status::Helpers
) )
install( install(
@ -42,8 +44,6 @@ target_include_directories(ApplicationCore
target_sources(ApplicationCore target_sources(ApplicationCore
PRIVATE PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationCore/Conversions.h
${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationCore/Conversions.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationCore/UserConfiguration.h ${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationCore/UserConfiguration.h
${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationCore/UserConfiguration.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ApplicationCore/UserConfiguration.cpp
) )

View File

@ -1,15 +0,0 @@
#include "Conversions.h"
namespace fs = std::filesystem;
namespace Status {
QString toString(const fs::path &path) {
return QString::fromStdString(path.string());
}
fs::path toPath(const QString &pathStr) {
return fs::path(pathStr.toStdString());
}
}

View File

@ -1,12 +0,0 @@
#pragma once
#include <filesystem>
#include <QString>
namespace Status {
QString toString(const std::filesystem::path& path);
std::filesystem::path toPath(const QString& pathStr);
}

View File

@ -1,6 +1,6 @@
#include "UserConfiguration.h" #include "UserConfiguration.h"
#include "Conversions.h" #include "Helpers/conversions.h"
#include <filesystem> #include <filesystem>
@ -21,7 +21,7 @@ UserConfiguration::UserConfiguration(QObject *parent)
const QString UserConfiguration::qmlUserDataFolder() const const QString UserConfiguration::qmlUserDataFolder() const
{ {
return toString(m_userDataFolder.string()); return toQString(m_userDataFolder.string());
} }
const fs::path &UserConfiguration::userDataFolder() const const fs::path &UserConfiguration::userDataFolder() const

View File

@ -6,6 +6,8 @@ project(Helpers
VERSION 0.1.0 VERSION 0.1.0
LANGUAGES CXX) LANGUAGES CXX)
find_package(nlohmann_json 3.10.5 REQUIRED)
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true) set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml REQUIRED) find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml REQUIRED)
@ -48,6 +50,9 @@ target_include_directories(Helpers
) )
target_link_libraries(Helpers target_link_libraries(Helpers
PUBLIC
nlohmann_json::nlohmann_json
PRIVATE PRIVATE
Qt6::Quick Qt6::Quick
Qt6::Qml Qt6::Qml
@ -61,6 +66,8 @@ install(
target_sources(Helpers target_sources(Helpers
PRIVATE PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/conversions.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/conversions.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/helpers.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/helpers.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/logs.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/logs.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/logs.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/logs.cpp

View File

@ -0,0 +1,20 @@
#include "conversions.h"
namespace fs = std::filesystem;
namespace Status {
QString toQString(const std::string &str)
{
return QString::fromStdString(str);
}
QString toQString(const fs::path &path) {
return toQString(path.string());
}
fs::path toPath(const QString &pathStr) {
return fs::path(pathStr.toStdString());
}
}

View File

@ -0,0 +1,45 @@
#pragma once
#include <QString>
#include <QColor>
#include <filesystem>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
namespace Status {
QString toQString(const std::string& str);
QString toQString(const std::filesystem::path& path);
std::filesystem::path toPath(const QString& pathStr);
} // namespace Status
namespace nlohmann {
template<>
struct adl_serializer<QString> {
static void to_json(json& j, const QString& str) {
j = str.toStdString();
}
static void from_json(const json& j, QString& str) {
str = QString::fromStdString(j.get<std::string>());
}
};
template<>
struct adl_serializer<QColor> {
static void to_json(json& j, const QColor& color) {
j = color.name();
}
static void from_json(const json& j, QColor& color) {
color = QColor(Status::toQString(j.get<std::string>()));
}
};
} // namespace nlohmann

View File

@ -43,6 +43,7 @@ target_link_libraries(Onboarding
Qt6::Concurrent Qt6::Concurrent
Status::ApplicationCore Status::ApplicationCore
Status::Helpers
Status::StatusGoQt Status::StatusGoQt
Status::StatusGoConfig Status::StatusGoConfig

View File

@ -11,6 +11,7 @@ SetupNewProfilePageBase {
TempTextInput { TempTextInput {
id: confirmPasswordInput id: confirmPasswordInput
// TODO: remove this developer helper
text: qsTr("1234567890") text: qsTr("1234567890")
width: 416 width: 416

View File

@ -48,6 +48,10 @@ OnboardingPageBase {
id: passwordInput id: passwordInput
Layout.preferredWidth: 328 Layout.preferredWidth: 328
Layout.preferredHeight: 44 Layout.preferredHeight: 44
// TODO: remove dev helper
text: "1234567890"
// END dev
} }
Button { Button {

View File

@ -10,13 +10,17 @@
namespace Status::Onboarding namespace Status::Onboarding
{ {
// TODO: refactor it to MultiAccount
struct AccountDto struct AccountDto
{ {
QString name; QString name;
long timestamp; long timestamp;
QString identicon;
QString keycardPairing; QString keycardPairing;
QString keyUid; QString keyUid;
// TODO images
// TODO colorHash
// TODO colorId
QString address;
bool isValid() const bool isValid() const
{ {
@ -37,9 +41,9 @@ struct AccountDto
if(ok) if(ok)
result.timestamp = t; result.timestamp = t;
} }
result.identicon = Json::getMandatoryProp(jsonObj, "identicon")->toString();
result.keycardPairing = Json::getMandatoryProp(jsonObj, "keycard-pairing")->toString(); result.keycardPairing = Json::getMandatoryProp(jsonObj, "keycard-pairing")->toString();
result.keyUid = Json::getMandatoryProp(jsonObj, "key-uid")->toString(); result.keyUid = Json::getMandatoryProp(jsonObj, "key-uid")->toString();
result.address = Json::getProp(jsonObj, "address")->toString();
/// TODO: investigate unhandled `photo-path` value /// TODO: investigate unhandled `photo-path` value
} }

View File

@ -1,11 +1,12 @@
#include "AccountsService.h" #include "AccountsService.h"
#include <StatusGo/Accounts/Accounts.h> #include <StatusGo/Accounts/Accounts.h>
#include <StatusGo/Accounts/AccountsAPI.h>
#include <StatusGo/General.h> #include <StatusGo/General.h>
#include <StatusGo/Utils.h> #include <StatusGo/Utils.h>
#include <StatusGo/Messenger/Service.h> #include <StatusGo/Messenger/Service.h>
#include <ApplicationCore/Conversions.h> #include <Helpers/conversions.h>
#include <optional> #include <optional>
@ -13,7 +14,7 @@
std::optional<QString> std::optional<QString>
getDataFromFile(const fs::path &path) getDataFromFile(const fs::path &path)
{ {
QFile jsonFile{Status::toString(path)}; QFile jsonFile{Status::toQString(path)};
if(!jsonFile.open(QIODevice::ReadOnly)) if(!jsonFile.open(QIODevice::ReadOnly))
{ {
qDebug() << "unable to open" << path.filename().c_str() << " for reading"; qDebug() << "unable to open" << path.filename().c_str() << " for reading";
@ -50,7 +51,6 @@ bool AccountsService::init(const fs::path& statusgoDataDir)
{ {
auto gAcc = GeneratedAccountDto::toGeneratedAccountDto(genAddressObj.toObject()); auto gAcc = GeneratedAccountDto::toGeneratedAccountDto(genAddressObj.toObject());
gAcc.alias = generateAlias(gAcc.derivedAccounts.whisper.publicKey); gAcc.alias = generateAlias(gAcc.derivedAccounts.whisper.publicKey);
gAcc.identicon = generateIdenticon(gAcc.derivedAccounts.whisper.publicKey);
m_generatedAccounts.push_back(std::move(gAcc)); m_generatedAccounts.push_back(std::move(gAcc));
} }
return true; return true;
@ -97,6 +97,7 @@ bool AccountsService::setupAccountAndLogin(const QString &accountId, const QStri
if(StatusGo::Accounts::openAccounts(m_statusgoDataDir.c_str()).containsError()) if(StatusGo::Accounts::openAccounts(m_statusgoDataDir.c_str()).containsError())
return false; return false;
AccountsService::storeAccount(accountId, hashedPassword);
AccountsService::storeDerivedAccounts(accountId, hashedPassword, Constants::General::AccountDefaultPaths); AccountsService::storeDerivedAccounts(accountId, hashedPassword, Constants::General::AccountDefaultPaths);
m_loggedInAccount = saveAccountAndLogin(hashedPassword, accountData, subAccountData, settings, nodeConfig); m_loggedInAccount = saveAccountAndLogin(hashedPassword, accountData, subAccountData, settings, nodeConfig);
@ -121,8 +122,8 @@ bool AccountsService::isFirstTimeAccountLogin() const
bool AccountsService::setKeyStoreDir(const QString &key) bool AccountsService::setKeyStoreDir(const QString &key)
{ {
auto keyStoreDir = m_statusgoDataDir / m_keyStoreDirName / key.toStdString(); m_keyStoreDir = m_statusgoDataDir / m_keyStoreDirName / key.toStdString();
auto response = StatusGo::General::initKeystore(keyStoreDir.c_str()); auto response = StatusGo::General::initKeystore(m_keyStoreDir.c_str());
return !response.containsError(); return !response.containsError();
} }
@ -140,7 +141,7 @@ QString AccountsService::login(AccountDto account, const QString& password)
QString thumbnailImage; QString thumbnailImage;
QString largeImage; QString largeImage;
auto response = StatusGo::Accounts::login(account.name, account.keyUid, hashedPassword, account.identicon, auto response = StatusGo::Accounts::login(account.name, account.keyUid, hashedPassword,
thumbnailImage, largeImage); thumbnailImage, largeImage);
if(response.containsError()) if(response.containsError())
{ {
@ -173,16 +174,9 @@ QString AccountsService::generateAlias(const QString& publicKey)
return response.result; return response.result;
} }
QString AccountsService::generateIdenticon(const QString& publicKey) void AccountsService::deleteMultiAccount(const AccountDto &account)
{ {
auto response = StatusGo::Accounts::generateIdenticon(publicKey); StatusGo::Accounts::deleteMultiaccount(account.keyUid, m_keyStoreDir);
if(response.containsError())
{
qWarning() << response.error.message;
return QString();
}
return response.result;
} }
DerivedAccounts AccountsService::storeDerivedAccounts(const QString& accountId, const QString& hashedPassword, DerivedAccounts AccountsService::storeDerivedAccounts(const QString& accountId, const QString& hashedPassword,
@ -225,15 +219,12 @@ QJsonObject AccountsService::prepareAccountJsonObject(const GeneratedAccountDto&
{ {
return QJsonObject{{"name", displayName.isEmpty() ? account.alias : displayName}, return QJsonObject{{"name", displayName.isEmpty() ? account.alias : displayName},
{"address", account.address}, {"address", account.address},
{"photo-path", account.identicon},
{"identicon", account.identicon},
{"key-uid", account.keyUid}, {"key-uid", account.keyUid},
{"keycard-pairing", QJsonValue()}}; {"keycard-pairing", QJsonValue()}};
} }
QJsonObject AccountsService::getAccountDataForAccountId(const QString &accountId, const QString &displayName) const QJsonObject AccountsService::getAccountDataForAccountId(const QString &accountId, const QString &displayName) const
{ {
for(const GeneratedAccountDto &acc : m_generatedAccounts) for(const GeneratedAccountDto &acc : m_generatedAccounts)
{ {
if(acc.id == accountId) if(acc.id == accountId)
@ -263,15 +254,16 @@ QJsonArray AccountsService::prepareSubaccountJsonObject(const GeneratedAccountDt
{"color", "#4360df"}, {"color", "#4360df"},
{"wallet", true}, {"wallet", true},
{"path", Constants::General::PathDefaultWallet}, {"path", Constants::General::PathDefaultWallet},
{"name", "Status account"} {"name", "Status account"},
{"derived-from", account.address}
}, },
QJsonObject{ QJsonObject{
{"public-key", account.derivedAccounts.whisper.publicKey}, {"public-key", account.derivedAccounts.whisper.publicKey},
{"address", account.derivedAccounts.whisper.address}, {"address", account.derivedAccounts.whisper.address},
{"path", Constants::General::PathWhisper},
{"name", displayName.isEmpty() ? account.alias : displayName}, {"name", displayName.isEmpty() ? account.alias : displayName},
{"identicon", account.identicon}, {"path", Constants::General::PathWhisper},
{"chat", true} {"chat", true},
{"derived-from", ""}
} }
}; };
} }
@ -331,20 +323,22 @@ QJsonObject AccountsService::prepareAccountSettingsJsonObject(const GeneratedAcc
{"eip1581-address", account.derivedAccounts.eip1581.address}, {"eip1581-address", account.derivedAccounts.eip1581.address},
{"dapps-address", account.derivedAccounts.defaultWallet.address}, {"dapps-address", account.derivedAccounts.defaultWallet.address},
{"wallet-root-address", account.derivedAccounts.walletRoot.address}, {"wallet-root-address", account.derivedAccounts.walletRoot.address},
{"preview-privacy", true}, {"preview-privacy?", true},
{"signing-phrase", generateSigningPhrase(3)}, {"signing-phrase", generateSigningPhrase(3)},
{"log-level", "INFO"}, {"log-level", "INFO"},
{"latest-derived-path", 0}, {"latest-derived-path", 0},
{"networks/networks", defaultNetworksJson},
{"currency", "usd"}, {"currency", "usd"},
{"identicon", account.identicon}, //{"networks/networks", defaultNetworksJson},
{"networks/networks", QJsonArray()},
//{"networks/current-network", Constants::General::DefaultNetworkName},
{"networks/current-network", ""},
{"wallet/visible-tokens", QJsonObject()},
//{"wallet/visible-tokens", {
// {Constants::General::DefaultNetworkName, QJsonArray{"SNT"}}
// }
//},
{"waku-enabled", true}, {"waku-enabled", true},
{"wallet/visible-tokens", {
{Constants::General::DefaultNetworkName, QJsonArray{"SNT"}}
}
},
{"appearance", 0}, {"appearance", 0},
{"networks/current-network", Constants::General::DefaultNetworkName},
{"installation-id", installationId} {"installation-id", installationId}
}; };
} catch (std::bad_optional_access) { } catch (std::bad_optional_access) {
@ -410,6 +404,7 @@ QJsonObject AccountsService::getDefaultNodeConfig(const QString& installationId)
nodeConfigJson["ClusterConfig"] = clusterConfig; nodeConfigJson["ClusterConfig"] = clusterConfig;
nodeConfigJson["KeyStoreDir"] = toQString(fs::relative(m_keyStoreDir, m_statusgoDataDir));
return nodeConfigJson; return nodeConfigJson;
} catch (std::bad_optional_access) { } catch (std::bad_optional_access) {
return QJsonObject(); return QJsonObject();

View File

@ -50,7 +50,7 @@ public:
QString generateAlias(const QString& publicKey) override; QString generateAlias(const QString& publicKey) override;
QString generateIdenticon(const QString& publicKey) override; void deleteMultiAccount(const AccountDto &account) override;
private: private:
QJsonObject prepareAccountJsonObject(const GeneratedAccountDto& account, const QString& displayName) const; QJsonObject prepareAccountJsonObject(const GeneratedAccountDto& account, const QString& displayName) const;
@ -83,6 +83,7 @@ private:
std::vector<GeneratedAccountDto> m_generatedAccounts; std::vector<GeneratedAccountDto> m_generatedAccounts;
fs::path m_statusgoDataDir; fs::path m_statusgoDataDir;
fs::path m_keyStoreDir;
bool m_isFirstTimeAccountLogin; bool m_isFirstTimeAccountLogin;
// TODO: don't see the need for this state here // TODO: don't see the need for this state here
AccountDto m_loggedInAccount; AccountDto m_loggedInAccount;

View File

@ -45,7 +45,9 @@ public:
virtual QString generateAlias(const QString& publicKey) = 0; virtual QString generateAlias(const QString& publicKey) = 0;
virtual QString generateIdenticon(const QString& publicKey) = 0; virtual void deleteMultiAccount(const AccountDto &account) = 0;
}; };
using AccountsServiceInterfacePtr = std::shared_ptr<AccountsServiceInterface>;
} }

View File

@ -102,9 +102,8 @@ struct GeneratedAccountDto
QString mnemonic; QString mnemonic;
DerivedAccounts derivedAccounts; DerivedAccounts derivedAccounts;
// The following two are set additionally. // set additionally.
QString alias; QString alias;
QString identicon;
bool isValid() const bool isValid() const
{ {

View File

@ -11,16 +11,8 @@ namespace Status::Onboarding
namespace StatusGo = Status::StatusGo; namespace StatusGo = Status::StatusGo;
NewAccountController::NewAccountController(std::shared_ptr<AccountsServiceInterface> accountsService, QObject *parent) NewAccountController::NewAccountController(AccountsServiceInterfacePtr accountsService, QObject* parent)
// TODO: remove dev dev setup after the final implementation : m_accountsService(accountsService)
: m_name("TestAccount")
, m_nameIsValid(true)
, m_password("1234567890")
, m_passwordIsValid(true)
, m_confirmationPassword("1234567890")
, m_confirmationPasswordIsValid(true)
// END dev setup
, m_accountsService(accountsService)
{ {
connect(this, &NewAccountController::passwordChanged, this, &NewAccountController::checkAndUpdateDataValidity); connect(this, &NewAccountController::passwordChanged, this, &NewAccountController::checkAndUpdateDataValidity);
connect(this, &NewAccountController::confirmationPasswordChanged, this, &NewAccountController::checkAndUpdateDataValidity); connect(this, &NewAccountController::confirmationPasswordChanged, this, &NewAccountController::checkAndUpdateDataValidity);

View File

@ -1,5 +1,6 @@
#include "OnboardingController.h" #include "OnboardingController.h"
#include "Accounts/AccountsServiceInterface.h"
#include "NewAccountController.h" #include "NewAccountController.h"
#include "UserAccount.h" #include "UserAccount.h"
@ -9,7 +10,7 @@ namespace Status::Onboarding {
namespace StatusGo = Status::StatusGo; namespace StatusGo = Status::StatusGo;
OnboardingController::OnboardingController(std::shared_ptr<AccountsServiceInterface> accountsService) OnboardingController::OnboardingController(AccountsServiceInterfacePtr accountsService)
: QObject(nullptr) : QObject(nullptr)
, m_accountsService(std::move(accountsService)) , m_accountsService(std::move(accountsService))
{ {
@ -74,4 +75,9 @@ NewAccountController *OnboardingController::newAccountController() const
return m_newAccountController.get(); return m_newAccountController.get();
} }
AccountsServiceInterfacePtr OnboardingController::accountsService() const
{
return m_accountsService;
}
} }

View File

@ -2,7 +2,6 @@
#include "UserAccountsModel.h" #include "UserAccountsModel.h"
#include "Accounts/AccountsServiceInterface.h"
#include "Accounts/AccountDto.h" #include "Accounts/AccountDto.h"
#include <QQmlEngine> #include <QQmlEngine>
@ -14,7 +13,7 @@ namespace Status::Onboarding
{ {
class UserAccount; class UserAccount;
class AccountsServiceInterface;
class NewAccountController; class NewAccountController;
/*! /*!
@ -50,6 +49,7 @@ public:
Q_INVOKABLE NewAccountController *initNewAccountController(); Q_INVOKABLE NewAccountController *initNewAccountController();
Q_INVOKABLE void terminateNewAccountController(); Q_INVOKABLE void terminateNewAccountController();
NewAccountController *newAccountController() const; NewAccountController *newAccountController() const;
std::shared_ptr<AccountsServiceInterface> accountsService() const;
signals: signals:
void accountLoggedIn(); void accountLoggedIn();

View File

@ -8,6 +8,7 @@
#include <Onboarding/OnboardingController.h> #include <Onboarding/OnboardingController.h>
#include <StatusGo/Accounts/Accounts.h> #include <StatusGo/Accounts/Accounts.h>
#include <StatusGo/Accounts/AccountsAPI.h>
#include <QCoreApplication> #include <QCoreApplication>
@ -15,6 +16,7 @@
namespace Testing = Status::Testing; namespace Testing = Status::Testing;
namespace Onboarding = Status::Onboarding; namespace Onboarding = Status::Onboarding;
namespace Accounts = Status::StatusGo::Accounts;
namespace fs = std::filesystem; namespace fs = std::filesystem;
@ -86,6 +88,8 @@ ScopedTestAccount::ScopedTestAccount(const std::string &tempTestSubfolderName, c
ScopedTestAccount::~ScopedTestAccount() ScopedTestAccount::~ScopedTestAccount()
{ {
const auto rootAccount = m_onboarding->accountsService()->getLoggedInAccount();
m_onboarding->accountsService()->deleteMultiAccount(rootAccount);
} }
void ScopedTestAccount::processMessages(size_t maxWaitTimeMillis, std::function<bool()> shouldWaitUntilTimeout) { void ScopedTestAccount::processMessages(size_t maxWaitTimeMillis, std::function<bool()> shouldWaitUntilTimeout) {
@ -106,6 +110,28 @@ void ScopedTestAccount::logOut()
throw std::runtime_error("ScopedTestAccount - failed logging out"); throw std::runtime_error("ScopedTestAccount - failed logging out");
} }
Accounts::MultiAccount ScopedTestAccount::firstChatAccount()
{
auto accounts = Accounts::getAccounts();
auto chatIt = std::find_if(accounts.begin(), accounts.end(), [](const auto& a) {
return a.isChat;
});
if(chatIt == accounts.end())
throw std::runtime_error("ScopedTestAccount::chatAccount: account not found");
return *chatIt;
}
Accounts::MultiAccount ScopedTestAccount::firstWalletAccount()
{
auto accounts = Accounts::getAccounts();
auto walletIt = std::find_if(accounts.begin(), accounts.end(), [](const auto& a) {
return a.isWallet;
});
if(walletIt == accounts.end())
throw std::runtime_error("ScopedTestAccount::firstWalletAccount: account not found");
return *walletIt;
}
Onboarding::OnboardingController *ScopedTestAccount::onboardingController() const Onboarding::OnboardingController *ScopedTestAccount::onboardingController() const
{ {
return m_onboarding.get(); return m_onboarding.get();

View File

@ -1,5 +1,7 @@
#pragma once #pragma once
#include <Wallet/WalletApi.h>
#include <string> #include <string>
#include <filesystem> #include <filesystem>
@ -11,6 +13,9 @@ namespace Status::Onboarding {
class OnboardingController; class OnboardingController;
} }
namespace Wallet = Status::StatusGo::Wallet;
namespace Accounts = Status::StatusGo::Accounts;
namespace Status::Testing { namespace Status::Testing {
class AutoCleanTempTestDir; class AutoCleanTempTestDir;
@ -31,9 +36,14 @@ public:
void processMessages(size_t millis, std::function<bool()> shouldWaitUntilTimeout); void processMessages(size_t millis, std::function<bool()> shouldWaitUntilTimeout);
void logOut(); void logOut();
static Accounts::MultiAccount firstChatAccount();
static Accounts::MultiAccount firstWalletAccount();
QString password() const { return m_accountPassword; };
Status::Onboarding::OnboardingController* onboardingController() const; Status::Onboarding::OnboardingController* onboardingController() const;
const std::filesystem::path& fusedTestFolder() const;; const std::filesystem::path& fusedTestFolder() const;
private: private:
std::unique_ptr<AutoCleanTempTestDir> m_fusedTestFolder; std::unique_ptr<AutoCleanTempTestDir> m_fusedTestFolder;

View File

@ -31,7 +31,7 @@ public:
MOCK_METHOD(QString, login, (Onboarding::AccountDto, const QString&), (override)); MOCK_METHOD(QString, login, (Onboarding::AccountDto, const QString&), (override));
MOCK_METHOD(void, clear, (), (override)); MOCK_METHOD(void, clear, (), (override));
MOCK_METHOD(QString, generateAlias, (const QString&), (override)); MOCK_METHOD(QString, generateAlias, (const QString&), (override));
MOCK_METHOD(QString, generateIdenticon, (const QString&), (override)); MOCK_METHOD(void, deleteMultiAccount, (const Onboarding::AccountDto&), (override));
}; };
} }

View File

@ -3,7 +3,10 @@
#include <IOTestHelpers.h> #include <IOTestHelpers.h>
#include <Constants.h> #include <Constants.h>
#include <StatusGo/Accounts/Accounts.h>
#include <Onboarding/Accounts/AccountsService.h> #include <Onboarding/Accounts/AccountsService.h>
#include <Onboarding/Common/Constants.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
@ -14,14 +17,14 @@ namespace fs = std::filesystem;
namespace Status::Testing { namespace Status::Testing {
class AccountsServicesTest : public ::testing::Test class AccountsService : public ::testing::Test
{ {
protected: protected:
std::unique_ptr<Onboarding::AccountsService> m_accountsService; std::unique_ptr<Onboarding::AccountsService> m_accountsService;
std::unique_ptr<Testing::AutoCleanTempTestDir> m_fusedTestFolder; std::unique_ptr<Testing::AutoCleanTempTestDir> m_fusedTestFolder;
void SetUp() override { void SetUp() override {
m_fusedTestFolder = std::make_unique<Testing::AutoCleanTempTestDir>("AccountsServicesTest"); m_fusedTestFolder = std::make_unique<Testing::AutoCleanTempTestDir>("TestAccountsService");
m_accountsService = std::make_unique<Onboarding::AccountsService>(); m_accountsService = std::make_unique<Onboarding::AccountsService>();
m_accountsService->init(m_fusedTestFolder->tempFolder() / Constants::statusGoDataDirName); m_accountsService->init(m_fusedTestFolder->tempFolder() / Constants::statusGoDataDirName);
} }
@ -33,7 +36,7 @@ protected:
}; };
TEST_F(AccountsServicesTest, GeneratedAccounts) TEST_F(AccountsService, GeneratedAccounts)
{ {
auto genAccounts = m_accountsService->generatedAccounts(); auto genAccounts = m_accountsService->generatedAccounts();
@ -48,7 +51,7 @@ TEST_F(AccountsServicesTest, GeneratedAccounts)
} }
} }
TEST_F(AccountsServicesTest, DISABLED_GenerateAlias) // temporary disabled till we see what's happening on the status-go side since it doesn't return aliases for any pk TEST_F(AccountsService, DISABLED_GenerateAlias) // temporary disabled till we see what's happening on the status-go side since it doesn't return aliases for any pk
{ {
QString testPubKey = "0x04487f44bac3e90825bfa9720148308cb64835bebb7e888f519cebc127223187067629f8b70d0661a35d4af6516b225286"; QString testPubKey = "0x04487f44bac3e90825bfa9720148308cb64835bebb7e888f519cebc127223187067629f8b70d0661a35d4af6516b225286";

View File

@ -154,7 +154,6 @@ TEST(OnboardingModule, TestLoginEndToEnd)
accountLoggedInError = true; accountLoggedInError = true;
}); });
//auto errorString = accountsService->login(accounts[0], accountPassword);
// Workaround until we reset the status-go state // Workaround until we reset the status-go state
auto ourAccountRes = std::find_if(accounts.begin(), accounts.end(), [accountName](const auto &a) { return a.name == accountName; }); auto ourAccountRes = std::find_if(accounts.begin(), accounts.end(), [accountName](const auto &a) { return a.name == accountName; });
auto errorString = accountsService->login(*ourAccountRes, accountPassword); auto errorString = accountsService->login(*ourAccountRes, accountPassword);

View File

@ -8,7 +8,9 @@ project(StatusGoQt
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true) set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Core Concurrent REQUIRED) find_package(nlohmann_json 3.10.5 REQUIRED)
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Core Concurrent Gui REQUIRED)
qt6_standard_project_setup() qt6_standard_project_setup()
add_library(${PROJECT_NAME} SHARED) add_library(${PROJECT_NAME} SHARED)
@ -16,18 +18,35 @@ add_library(${PROJECT_NAME} SHARED)
# Use by linker only # Use by linker only
set_property(GLOBAL PROPERTY DEBUG_CONFIGURATIONS Debug) set_property(GLOBAL PROPERTY DEBUG_CONFIGURATIONS Debug)
add_subdirectory(src) # TODO: consider adding a private header for parsing and keep json dependency away!?
target_link_libraries(${PROJECT_NAME} target_link_libraries(${PROJECT_NAME}
PUBLIC PUBLIC
Status::Helpers
PRIVATE
Qt6::Gui
Qt6::Core Qt6::Core
Qt6::Concurrent Qt6::Concurrent
PRIVATE nlohmann_json::nlohmann_json
statusgo_shared statusgo_shared
) )
add_library(Status::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) add_library(Status::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
target_include_directories(${PROJECT_NAME}
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo
# TODO: Workaround to QML_ELEMENT Qt6
INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src
)
add_subdirectory(tests)
# Copy status-go lib close to the executable # Copy status-go lib close to the executable
# Temporary workaround; TODO: see a better alternative that doesn't depend on target order (dedicated dependencies dir?) # Temporary workaround; TODO: see a better alternative that doesn't depend on target order (dedicated dependencies dir?)
# and current directory (on mac). Use bundle or set rpath relative to executable # and current directory (on mac). Use bundle or set rpath relative to executable
@ -46,3 +65,32 @@ install(
IMPORTED_RUNTIME_ARTIFACTS IMPORTED_RUNTIME_ARTIFACTS
statusgo_shared statusgo_shared
) )
target_sources(${PROJECT_NAME}
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/General.h
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/General.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Types.h
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Utils.h
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Utils.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Accounts/Accounts.h
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Accounts/Accounts.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Accounts/AccountsAPI.h
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Accounts/AccountsAPI.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Accounts/MultiAccount.h
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Accounts/MultiAccount.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Messenger/Service.h
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Messenger/Service.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Metadata/api_response.h
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Metadata/api_response.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/SignalsManager.h
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/SignalsManager.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Wallet/DerivedAddress.h
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Wallet/WalletApi.h
${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Wallet/WalletApi.cpp
)

View File

@ -1,14 +0,0 @@
# Internally we use includes directly
# External clients have to explicitly use the module name
add_subdirectory(StatusGo)
target_include_directories(${PROJECT_NAME}
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/StatusGo
# TODO: Workaround to QML_ELEMENT Qt6
INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/StatusGo
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)

View File

@ -1,7 +1,8 @@
#include "Accounts.h" #include "Accounts.h"
#include "Utils.h" #include "Utils.h"
#include "libstatus.h"
#include <libstatus.h>
const int NUMBER_OF_ADDRESSES_TO_GENERATE = 5; const int NUMBER_OF_ADDRESSES_TO_GENERATE = 5;
const int MNEMONIC_PHRASE_LENGTH = 12; const int MNEMONIC_PHRASE_LENGTH = 12;
@ -43,25 +44,6 @@ RpcResponse<QJsonArray> generateAddresses(const QVector<QString>& paths)
} }
} }
RpcResponse<QString> generateIdenticon(const QString& publicKey)
{
try
{
QString identicon;
if(!publicKey.isEmpty())
{
identicon = Identicon(publicKey.toUtf8().data());
}
return Utils::buildPrivateRPCResponse(identicon);
}
catch (...)
{
auto response = RpcResponse<QString>(QString());
response.error.message = QObject::tr("an error generating identicon occurred");
return response;
}
}
RpcResponse<QString> generateAlias(const QString& publicKey) RpcResponse<QString> generateAlias(const QString& publicKey)
{ {
try try
@ -212,7 +194,7 @@ RpcResponse<QJsonArray> openAccounts(const char* dataDirPath)
} }
RpcResponse<QJsonObject> login(const QString& name, const QString& keyUid, const QString& hashedPassword, RpcResponse<QJsonObject> login(const QString& name, const QString& keyUid, const QString& hashedPassword,
const QString& identicon, const QString& thumbnail, const QString& large) const QString& thumbnail, const QString& large)
{ {
QJsonObject payload{ QJsonObject payload{
{"name", name}, {"name", name},
@ -253,8 +235,7 @@ RpcResponse<QJsonObject> login(const QString& name, const QString& keyUid, const
} }
RpcResponse<QJsonObject> loginWithConfig(const QString& name, const QString& keyUid, const QString& hashedPassword, RpcResponse<QJsonObject> loginWithConfig(const QString& name, const QString& keyUid, const QString& hashedPassword,
const QString& identicon, const QString& thumbnail, const QString& large, const QString& thumbnail, const QString& large, const QJsonObject& nodeConfig)
const QJsonObject& nodeConfig)
{ {
QJsonObject payload{ QJsonObject payload{
{"name", name}, {"name", name},

View File

@ -8,8 +8,6 @@ namespace Status::StatusGo::Accounts
{ {
RpcResponse<QJsonArray> generateAddresses(const QVector<QString>& paths); RpcResponse<QJsonArray> generateAddresses(const QVector<QString>& paths);
RpcResponse<QString> generateIdenticon(const QString& publicKey);
RpcResponse<QString> generateAlias(const QString& publicKey); RpcResponse<QString> generateAlias(const QString& publicKey);
RpcResponse<QJsonObject> storeDerivedAccounts(const QString& accountId, const QString& hashedPassword, RpcResponse<QJsonObject> storeDerivedAccounts(const QString& accountId, const QString& hashedPassword,
@ -24,10 +22,10 @@ namespace Status::StatusGo::Accounts
/// opens database and returns accounts list. /// opens database and returns accounts list.
RpcResponse<QJsonArray> openAccounts(const char* dataDirPath); RpcResponse<QJsonArray> openAccounts(const char* dataDirPath);
/// TODO harmonise password parameters (hashed or plain)?
RpcResponse<QJsonObject> login(const QString& name, const QString& keyUid, const QString& hashedPassword, RpcResponse<QJsonObject> login(const QString& name, const QString& keyUid, const QString& hashedPassword,
const QString& identicon, const QString& thumbnail, const QString& large); const QString& thumbnail, const QString& large);
RpcResponse<QJsonObject> loginWithConfig(const QString& name, const QString& keyUid, const QString& hashedPassword, RpcResponse<QJsonObject> loginWithConfig(const QString& name, const QString& keyUid, const QString& hashedPassword,
const QString& identicon, const QString& thumbnail, const QString& large, const QString& thumbnail, const QString& large, const QJsonObject& nodeConfig);
const QJsonObject& nodeConfig);
RpcResponse<QJsonObject> logout(); RpcResponse<QJsonObject> logout();
} }

View File

@ -0,0 +1,98 @@
#include "AccountsAPI.h"
#include "Utils.h"
#include "Metadata/api_response.h"
#include <libstatus.h>
#include <nlohmann/json.hpp>
#include <iostream>
using json = nlohmann::json;
namespace Status::StatusGo::Accounts
{
Accounts::MultiAccounts getAccounts() {
// or even nicer with a raw string literal
json inputJson = {
{"jsonrpc", "2.0"},
{"method", "accounts_getAccounts"},
{"params", json::array()}
};
auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str());
auto resultJson = json::parse(result);
checkPrivateRpcCallResultAndReportError(resultJson);
return resultJson.get<CallPrivateRpcResponse>().result;
}
void generateAccountWithDerivedPath(const QString &hashedPassword, const QString &name, const QColor &color, const QString &emoji,
const QString &path, const QString &derivedFrom)
{
std::vector<json> params = {hashedPassword, name, color, emoji, path, derivedFrom};
json inputJson = {
{"jsonrpc", "2.0"},
{"method", "accounts_generateAccountWithDerivedPath"},
{"params", params}
};
auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str());
auto resultJson = json::parse(result);
checkPrivateRpcCallResultAndReportError(resultJson);
}
void addAccountWithMnemonicAndPath(const QString &mnemonic, const QString &hashedPassword, const QString &name,
const QColor &color, const QString &emoji, const QString &path)
{
std::vector<json> params = {mnemonic, hashedPassword, name, color, emoji, path};
json inputJson = {
{"jsonrpc", "2.0"},
{"method", "accounts_addAccountWithMnemonicAndPath"},
{"params", params}
};
auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str());
auto resultJson = json::parse(result);
checkPrivateRpcCallResultAndReportError(resultJson);
}
void addAccountWatch(const QString &address, const QString &name, const QColor &color, const QString &emoji)
{
std::vector<json> params = {address, name, color, emoji};
json inputJson = {
{"jsonrpc", "2.0"},
{"method", "accounts_addAccountWatch"},
{"params", params}
};
auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str());
auto resultJson = json::parse(result);
checkPrivateRpcCallResultAndReportError(resultJson);
}
void deleteAccount(const QString &address)
{
std::vector<json> params = {address};
json inputJson = {
{"jsonrpc", "2.0"},
{"method", "accounts_deleteAccount"},
{"params", params}
};
auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str());
auto resultJson = json::parse(result);
checkPrivateRpcCallResultAndReportError(resultJson);
}
void deleteMultiaccount(const QString &keyUID, const fs::path &keyStoreDir)
{
// We know go bridge won't misbehave with the input arguments
auto result = DeleteMultiaccount(const_cast<char*>(keyUID.toStdString().c_str()), const_cast<char*>(keyStoreDir.string().c_str()));
auto resultJson = json::parse(result);
checkApiError(resultJson);
}
}

View File

@ -0,0 +1,54 @@
#pragma once
#include "Accounts/MultiAccount.h"
#include <vector>
#include <filesystem>
namespace Accounts = Status::StatusGo::Accounts;
namespace fs = std::filesystem;
namespace Status::StatusGo::Accounts
{
/// \brief Retrieve all available accounts Wallet and Chat
/// \note status-go returns accounts in \c CallPrivateRpcResponse.result
/// \throws \c CallPrivateRpcError
Accounts::MultiAccounts getAccounts();
/// \brief Generate a new account
/// \note the underlying status-go api, SaveAccounts@accounts.go, returns `nil` for \c CallPrivateRpcResponse.result
/// \see \c getAccounts
/// \throws \c CallPrivateRpcError
void generateAccountWithDerivedPath(const QString &password, const QString &name,
const QColor &color, const QString &emoji,
const QString &path, const QString &derivedFrom);
/// \brief Add a new account from an existing mnemonic
/// \note the underlying status-go api, SaveAccounts@accounts.go, returns `nil` for \c CallPrivateRpcResponse.result
/// \see \c getAccounts
/// \throws \c CallPrivateRpcError
void addAccountWithMnemonicAndPath(const QString &mnemonic, const QString &hashedPassword, const QString &name,
const QColor &color, const QString &emoji, const QString &path);
/// \brief Add a watch only account
/// \note the underlying status-go api, SaveAccounts@accounts.go, returns `nil` for \c CallPrivateRpcResponse.result
/// \see \c getAccounts
/// \throws \c CallPrivateRpcError
void addAccountWatch(const QString &address, const QString &name, const QColor &color, const QString &emoji);
/// \brief Delete an existing account
/// \note the underlying status-go api, DeleteAccount@accounts.go, returns `os.Remove(keyFile)`
/// \see \c getAccounts
/// \throws \c CallPrivateRpcError
void deleteAccount(const QString &address);
/// \brief Delete an existing account
/// \note the underlying status-go api, DeleteAccount@accounts.go, returns `os.Remove(keyFile)`
/// \see \c getAccounts
/// \throws \c CallPrivateRpcError
void deleteMultiaccount(const QString &keyUID, const fs::path &keyStoreDir);
} // namespaces

View File

@ -0,0 +1,46 @@
#include "MultiAccount.h"
namespace Status::StatusGo::Accounts {
void to_json(json& j, const MultiAccount& d) {
j = {{"address", d.address},
{"chat", d.isChat},
{"clock", d.clock},
{"color", d.color},
{"emoji", d.emoji},
{"hidden", d.isHidden},
{"mixedcase-address", d.mixedcaseAddress},
{"name", d.name},
{"path", d.path},
{"public-key", d.publicKey},
{"removed", d.isRemoved},
{"wallet", d.isWallet},
};
if(d.derivedFrom != std::nullopt)
j["derived-from"] = d.derivedFrom.value();
}
void from_json(const json& j, MultiAccount& d) {
j.at("address").get_to(d.address);
j.at("chat").get_to(d.isChat);
j.at("clock").get_to(d.clock);
j.at("color").get_to(d.color);
j.at("emoji").get_to(d.emoji);
j.at("hidden").get_to(d.isHidden);
j.at("mixedcase-address").get_to(d.mixedcaseAddress);
j.at("name").get_to(d.name);
j.at("removed").get_to(d.isRemoved);
j.at("wallet").get_to(d.isWallet);
constexpr auto pathKey = "path";
if(j.contains(pathKey))
j.at(pathKey).get_to(d.path);
constexpr auto publicKeyKey = "public-key";
if(j.contains(publicKeyKey))
j.at(publicKeyKey).get_to(d.publicKey);
if(d.isWallet && !j.at("derived-from").get<std::string>().empty())
d.derivedFrom = j.at("derived-from").get<QString>();
}
}

View File

@ -0,0 +1,42 @@
#pragma once
#include <Helpers/conversions.h>
#include <QColor>
#include <nlohmann/json.hpp>
#include <vector>
using json = nlohmann::json;
namespace Status::StatusGo::Accounts {
// TODO: rename to MixedAccount
// TODO: create custom types or just named types for all. Also fix APIs after this
/*! \brief Unique wallet account entity
*/
struct MultiAccount
{
QString address;
bool isChat = false;
int clock = -1;
QColor color;
std::optional<QString> derivedFrom;
QString emoji;
bool isHidden = false;
QString mixedcaseAddress;
QString name;
QString path;
QString publicKey;
bool isRemoved = false;
bool isWallet = false;
};
using MultiAccounts = std::vector<MultiAccount>;
void to_json(json& j, const MultiAccount& d);
void from_json(const json& j, MultiAccount& d);
}

View File

@ -1,22 +0,0 @@
target_sources(${PROJECT_NAME}
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/General.h
${CMAKE_CURRENT_SOURCE_DIR}/Utils.h
${CMAKE_CURRENT_SOURCE_DIR}/Accounts/Accounts.h
${CMAKE_CURRENT_SOURCE_DIR}/Messenger/Service.h
${CMAKE_CURRENT_SOURCE_DIR}/SignalsManager.h
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/General.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Types.h
${CMAKE_CURRENT_SOURCE_DIR}/Utils.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Accounts/Accounts.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Messenger/Service.cpp
${CMAKE_CURRENT_SOURCE_DIR}/SignalsManager.cpp
)

View File

@ -1,7 +1,8 @@
#include "General.h" #include "General.h"
#include "Utils.h" #include "Utils.h"
#include "libstatus.h"
#include <libstatus.h>
namespace Status::StatusGo::General namespace Status::StatusGo::General
{ {

View File

@ -0,0 +1,25 @@
#include "api_response.h"
namespace Status::StatusGo {
void checkApiError(const json &response) {
if(response.contains("error")) {
const auto &error = response["error"];
if(error.is_object()) {
const auto apiErr = response["error"].get<ApiErrorResponseWithCode>();
throw CallGenericPrepareJsonError(apiErr);
}
assert(error.is_string());
const auto apiError = response.get<ApiErrorResponse>();
if(!apiError.error.empty())
throw CallGenericMakeJsonError(response.get<ApiErrorResponse>());
}
}
/// \throws \c CallPrivateRpcError, \c nlohmann::exception
void checkPrivateRpcCallResultAndReportError(const json &response) {
if(response.contains("error"))
throw CallPrivateRpcError(response.get<CallPrivateRpcErrorResponse>());
}
} // namespace

View File

@ -0,0 +1,163 @@
#pragma once
#include <nlohmann/json.hpp>
#include <QDebug>
#include <string>
using json = nlohmann::json;
namespace Status::StatusGo {
/*!
* \brief General API response if an internal status-go error occured
*
* \see makeJSONResponse@status.go
* \see APIResponsee@types.go
*
* \note update NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE when changing structure's content
*/
struct ApiErrorResponse {
std::string error;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ApiErrorResponse, error)
/*!
* \brief General API response if an internal status-go error occured
*
* \see prepareJSONResponseWithCode@response.go
*
* \note update NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE when changing structure's content
*/
struct JsonError {
int code{};
std::string message;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(JsonError, code, message)
/*!
* \brief General API response if an internal status-go error occured
*
* \see prepareJSONResponseWithCode@response.go
* \see jsonrpcSuccessfulResponse@response.go
* \note update NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE when changing structure's content
*/
struct ApiErrorResponseWithCode {
JsonError error;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ApiErrorResponseWithCode, error)
/*!
* \brief General API response if no error occured
*
* \see jsonrpcSuccessfulResponse@response.go
* \see jsonrpcSuccessfulResponse@call_raw.go
* \see prepareJSONResponseWithCode@response.go
*
* \note update NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE when changing structure's content
*/
struct ApiResponse {
json result;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ApiResponse, result)
/*!
* \brief Response of status-go's \c CallPrivateRPC
*
* There are multiple stages in calling private RPC and they return the following values
*
* 1. CallPrivateRPC@status.go returns `APIResponse` with the returned error if the underlying implementation failed
* otherwise returns result of CallRaw@call_raw.go, see 2.
* - \see makeJSONResponse@status-go
* 2. CallRaw@call_raw.go returns newErrorResponse@call_raw.go or newSuccessResponse@call_raw.go
*
* \see \c libstatus.h
*/
struct CallPrivateRpcErrorResponse
{
std::string jsonrpc;
int id;
JsonError error;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(CallPrivateRpcErrorResponse, jsonrpc, id, error)
/*!
* \brief Response of status-go's \c CallPrivateRPC
*
* There are multiple stages in calling private RPC and they return the following values
*
* 1. CallPrivateRPC@status.go returns `APIResponse` with the returned error if the underlying implementation failed
* otherwise returns result of CallRaw@call_raw.go, see 2.
* - \see makeJSONResponse@status-go
* 2. CallRaw@call_raw.go returns newErrorResponse@call_raw.go or newSuccessResponse@call_raw.go
*
* \see \c libstatus.h
*/
struct CallPrivateRpcResponse
{
std::string jsonrpc;
int id;
json result;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(CallPrivateRpcResponse, jsonrpc, id, result)
/*!
* \brief Check generic API calls for error
* \throws \c CallGenericMakeJsonError or CallGenericPrepareJsonError in case of error
*/
void checkApiError(const json& response);
constexpr int defaultErrorCode = -32000;
class CallGenericMakeJsonError: public std::runtime_error {
public:
CallGenericMakeJsonError(const ApiErrorResponse error)
: std::runtime_error("CallGenericMakeJsonError@status-go failed")
, m_error(std::move(error))
{}
const ApiErrorResponse &errorResponse() const { return m_error; };
private:
const ApiErrorResponse m_error;
};
class CallGenericPrepareJsonError: public std::runtime_error {
public:
CallGenericPrepareJsonError(const ApiErrorResponseWithCode error)
: std::runtime_error("CallGenericPrepareJsonError@status-go failed")
, m_error(std::move(error))
{}
const ApiErrorResponseWithCode &errorResponse() const { return m_error; };
private:
const ApiErrorResponseWithCode m_error;
};
class CallPrivateRpcError: public std::runtime_error {
public:
CallPrivateRpcError(const CallPrivateRpcErrorResponse error)
: std::runtime_error("CallPrivateRPC@status-go failed")
, m_error(std::move(error))
{}
const CallPrivateRpcErrorResponse &errorResponse() const { return m_error; };
private:
const CallPrivateRpcErrorResponse m_error;
};
/*!
* \brief check response from \c CallPrivateRPC call
* \param response json api response from \c CallPrivateRPC
* \return true if no error found
* \throws \c CallPrivateRpcError
*/
void checkPrivateRpcCallResultAndReportError(const json& response);
}

View File

@ -2,7 +2,7 @@
#include <QtConcurrent> #include <QtConcurrent>
#include "libstatus.h" #include <libstatus.h>
using namespace std::string_literals; using namespace std::string_literals;

View File

@ -1,6 +1,6 @@
#include "Utils.h" #include "Utils.h"
#include "libstatus.h" #include <libstatus.h>
#include <QtCore> #include <QtCore>
@ -15,8 +15,8 @@ QJsonArray toJsonArray(const QVector<QString>& value)
return array; return array;
} }
const char* statusgoCallPrivateRPC(const char* inputJSON) { const char* statusGoCallPrivateRPC(const char* inputJSON) {
// Evil done here! status-go API doesn't follow the proper so we adapt // Evil done here! status-go API doesn't follow the proper const conventions
return CallPrivateRPC(const_cast<char*>(inputJSON)); return CallPrivateRPC(const_cast<char*>(inputJSON));
} }

View File

@ -80,14 +80,14 @@ RpcResponse<T> buildPrivateRPCResponse(const T& json)
return response; return response;
} }
const char* statusgoCallPrivateRPC(const char* inputJSON); const char* statusGoCallPrivateRPC(const char* inputJSON);
template<class T> template<class T>
RpcResponse<T> callPrivateRpc(const QByteArray& payload) RpcResponse<T> callPrivateRpc(const QByteArray& payload)
{ {
try try
{ {
auto result = statusgoCallPrivateRPC(payload.data()); auto result = statusGoCallPrivateRPC(payload.data());
T jsonResult; T jsonResult;
if(!Utils::checkReceivedResponse(result, jsonResult)) if(!Utils::checkReceivedResponse(result, jsonResult))
{ {

View File

@ -0,0 +1,34 @@
#pragma once
#include <Helpers/conversions.h>
#include <QColor>
#include <nlohmann/json.hpp>
#include <vector>
using json = nlohmann::json;
namespace Status::StatusGo::Wallet {
/*!
* \brief Define a derived address as returned by the corresponding API
* \note equivalent of status-go's DerivedAddress@api.go
* \see \c getDerivedAddressesForPath
*/
struct DerivedAddress
{
// TODO create and Address type represents the 20 byte address of an Ethereum account. See https://pkg.go.dev/github.com/ethereum/go-ethereum/common?utm_source=gopls#Address
QString address;
// TODO: create an Path named type
QString path;
bool hasActivity = false;
bool alreadyCreated = false;
};
using DerivedAddresses = std::vector<DerivedAddress>;
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(DerivedAddress, address, path, hasActivity, alreadyCreated);
}

View File

@ -0,0 +1,33 @@
#include "WalletApi.h"
#include "Utils.h"
#include "Metadata/api_response.h"
#include <libstatus.h>
#include <nlohmann/json.hpp>
#include <iostream>
using json = nlohmann::json;
namespace Status::StatusGo::Wallet
{
DerivedAddresses getDerivedAddressesForPath(const QString &hashedPassword, const QString &derivedFrom, const QString &path, int pageSize, int pageNumber)
{
std::vector<json> params = {hashedPassword, derivedFrom, path, pageSize, pageNumber};
json inputJson = {
{"jsonrpc", "2.0"},
{"method", "wallet_getDerivedAddressesForPath"},
{"params", params}
};
auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str());
const auto resultJson = json::parse(result);
checkPrivateRpcCallResultAndReportError(resultJson);
return resultJson.get<CallPrivateRpcResponse>().result;
}
} // namespaces

View File

@ -0,0 +1,18 @@
#pragma once
#include "Accounts/MultiAccount.h"
#include "DerivedAddress.h"
#include <vector>
namespace Accounts = Status::StatusGo::Accounts;
namespace Status::StatusGo::Wallet
{
/// \brief Retrieve a list of derived account addresses
/// \see \c generateAccountWithDerivedPath
/// \throws \c CallPrivateRpcError
DerivedAddresses getDerivedAddressesForPath(const QString &password, const QString &derivedFrom, const QString &path, int pageSize, int pageNumber);
} // namespaces

View File

@ -0,0 +1,40 @@
# Unit tests for StatusGoQt
cmake_minimum_required(VERSION 3.21)
project(TestStatusGoQt VERSION 0.1.0 LANGUAGES CXX)
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Core REQUIRED)
qt6_standard_project_setup()
find_package(GTest REQUIRED)
enable_testing()
add_executable(TestStatusGoQt
test_StatusGo.cpp
)
target_include_directories(TestStatusGoQt
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(TestStatusGoQt
PRIVATE
Qt6::Core
GTest::gtest
GTest::gmock
GTest::gtest_main
Status::StatusGoQt
)
include(GoogleTest)
gtest_add_tests(
TARGET TestStatusGoQt
WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
)

View File

@ -0,0 +1,31 @@
#include <gtest/gtest.h>
#include <StatusGo/Metadata/api_response.h>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
namespace StatusGo = Status::StatusGo;
namespace Status::Testing {
TEST(StatusGoQt, TestJsonParsing)
{
auto callRawRPCJsonStr = R"({"jsonrpc":"2.0","id":42,"error":{"code":-32601,"message":"Method not found"}})";
auto callRawRPCJson = json::parse(callRawRPCJsonStr).get<StatusGo::CallPrivateRpcErrorResponse>();
ASSERT_EQ(callRawRPCJson.jsonrpc, "2.0");
ASSERT_EQ(callRawRPCJson.id, 42);
StatusGo::JsonError expectedJsonError = {-32601, "Method not found"};
ASSERT_EQ(callRawRPCJson.error.code, expectedJsonError.code);
ASSERT_EQ(callRawRPCJson.error.message, expectedJsonError.message);
auto callRawRPCBadJsonKeyStr = R"({"unknown":"2.0","id":42,"error":{"code":-32601,"message":"Method not found"}})";
ASSERT_THROW(json::parse(callRawRPCBadJsonKeyStr).get<StatusGo::CallPrivateRpcErrorResponse>(), nlohmann::detail::out_of_range);
auto callRawRPCBadJsonValStr = R"({"jsonrpc":"2.0","id":42,"error":23})";
ASSERT_THROW(json::parse(callRawRPCBadJsonValStr).get<StatusGo::CallPrivateRpcErrorResponse>(), nlohmann::detail::type_error);
auto statusGoWithResultJsonStr = R"({"result":"0x123"})";
auto statusGoWithResultJson = json::parse(statusGoWithResultJsonStr).get<StatusGo::ApiResponse>();
ASSERT_EQ(statusGoWithResultJson.result, "0x123");
}
} // namespace

View File

@ -1 +0,0 @@
add_subdirectory(libs)

View File

@ -1,3 +0,0 @@
# Libs integration tests
#
add_subdirectory(StatusGoQt)

View File

@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.21) cmake_minimum_required(VERSION 3.21)
project(TestStatusGoQt VERSION 0.1.0 LANGUAGES CXX) project(TestStatusGoQtModule VERSION 0.1.0 LANGUAGES CXX)
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true) set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
@ -11,12 +11,14 @@ find_package(GTest REQUIRED)
enable_testing() enable_testing()
add_executable(${PROJECT_NAME} add_executable(TestStatusGoQtModule
test_accounts.cpp test_accounts.cpp
test_messaging.cpp test_messaging.cpp
test_onboarding.cpp
test_wallet.cpp
) )
target_link_libraries(${PROJECT_NAME} target_link_libraries(TestStatusGoQtModule
PRIVATE PRIVATE
Qt6::Core Qt6::Core
@ -34,6 +36,6 @@ target_link_libraries(${PROJECT_NAME}
include(GoogleTest) include(GoogleTest)
gtest_add_tests( gtest_add_tests(
TARGET ${PROJECT_NAME} TARGET TestStatusGoQtModule
WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
) )

View File

@ -1,34 +1,240 @@
#include <gtest/gtest.h> #include <StatusGo/Accounts/AccountsAPI.h>
#include <StatusGo/Metadata/api_response.h>
#include <StatusGo/Accounts/Accounts.h> #include <StatusGo/Accounts/Accounts.h>
#include <Onboarding/Common/Constants.h>
#include <Onboarding/OnboardingController.h>
#include <StatusGo/Utils.h>
#include <IOTestHelpers.h> #include <IOTestHelpers.h>
#include <ScopedTestAccount.h>
#include <gtest/gtest.h>
namespace Accounts = Status::StatusGo::Accounts; namespace Accounts = Status::StatusGo::Accounts;
namespace StatusGo = Status::StatusGo; namespace StatusGo = Status::StatusGo;
namespace Utils = Status::StatusGo::Utils;
namespace fs = std::filesystem; namespace fs = std::filesystem;
namespace Status::Testing { namespace Status::Testing {
TEST(Onboarding, TestOpenAccountsNoDataFails) { /// \todo fin a way to test the integration within a test environment. Also how about reusing an existing account
AutoCleanTempTestDir fusedTestFolder{test_info_->name()}; TEST(AccountsAPI, TestGetAccounts)
{
constexpr auto testAccountName = "test_get_accounts_name";
constexpr auto testAccountPassword = "password*";
ScopedTestAccount testAccount(test_info_->name(), testAccountName, testAccountPassword, true);
auto response = Accounts::openAccounts(fusedTestFolder.tempFolder().c_str()); const auto accounts = Accounts::getAccounts();
EXPECT_FALSE(response.containsError()); // TODO: enable after calling reset to status-go
EXPECT_EQ(response.result.count(), 0); //ASSERT_EQ(accounts.size(), 2);
const auto chatIt = std::find_if(accounts.begin(), accounts.end(), [](const auto& a) { return a.isChat; });
ASSERT_NE(chatIt, accounts.end());
const auto &chatAccount = *chatIt;
ASSERT_EQ(chatAccount.name, testAccountName);
ASSERT_FALSE(chatAccount.path.isEmpty());
ASSERT_FALSE(chatAccount.derivedFrom.has_value());
const auto walletIt = std::find_if(accounts.begin(), accounts.end(), [](const auto& a) { return a.isWallet; });
ASSERT_NE(walletIt, accounts.end());
const auto &walletAccount = *walletIt;
ASSERT_NE(walletAccount.name, testAccountName);
ASSERT_FALSE(walletAccount.path.isEmpty());
ASSERT_TRUE(walletAccount.derivedFrom.has_value());
} }
TEST(Onboarding, TestOpenAccountsNoDataDoesNotCreateFiles) { TEST(Accounts, TestGenerateAccountWithDerivedPath)
AutoCleanTempTestDir fusedTestFolder{test_info_->name()}; {
constexpr auto testRootAccountName = "test-generate_account_with_derived_path-name";
constexpr auto testAccountPassword = "password*";
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true);
auto response = Accounts::openAccounts(fusedTestFolder.tempFolder().c_str()); auto hashedPassword{Utils::hashString(testAccountPassword)};
EXPECT_FALSE(response.containsError()); const auto newTestAccountName = u"test_generated_new_account-name"_qs;
const auto newTestAccountColor = QColor("fuchsia");
const auto newTestAccountEmoji = u""_qs;
const auto newTestAccountPath = Status::Constants::General::PathWalletRoot;
int fileCount = 0; const auto chatAccount = testAccount.firstChatAccount();
for (const auto & file : fs::directory_iterator(fusedTestFolder.tempFolder())) Accounts::generateAccountWithDerivedPath(hashedPassword, newTestAccountName,
fileCount++; newTestAccountColor, newTestAccountEmoji,
EXPECT_EQ(fileCount, 0); newTestAccountPath, chatAccount.address);
const auto updatedAccounts = Accounts::getAccounts();
ASSERT_EQ(updatedAccounts.size(), 3);
const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(),
[newTestAccountName = std::as_const(newTestAccountName)](const auto& a) {
return a.name == newTestAccountName;
});
ASSERT_NE(newAccountIt, updatedAccounts.end());
const auto &newAccount = *newAccountIt;
ASSERT_FALSE(newAccount.address.isEmpty());
ASSERT_FALSE(newAccount.isChat);
ASSERT_FALSE(newAccount.isWallet);
ASSERT_EQ(newAccount.color, newTestAccountColor);
ASSERT_FALSE(newAccount.derivedFrom.has_value());
ASSERT_EQ(newAccount.emoji, newTestAccountEmoji);
ASSERT_EQ(newAccount.mixedcaseAddress.toUpper(), newAccount.address.toUpper());
ASSERT_EQ(newAccount.path, newTestAccountPath);
ASSERT_FALSE(newAccount.publicKey.isEmpty());
} }
TEST(AccountsAPI, TestGenerateAccountWithDerivedPath_WrongPassword)
{
constexpr auto testRootAccountName = "test-generate_account_with_derived_path-name";
constexpr auto testAccountPassword = "password*";
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true);
const auto chatAccount = testAccount.firstChatAccount();
try {
Accounts::generateAccountWithDerivedPath(Utils::hashString("WrongPassword"), u"test_wrong_pass-name"_qs,
QColor("fuchsia"), "", Status::Constants::General::PathWalletRoot,
chatAccount.address);
FAIL();
} catch(const StatusGo::CallPrivateRpcError &exception) {
const auto &err = exception.errorResponse();
ASSERT_EQ(err.error.code, StatusGo::defaultErrorCode);
ASSERT_EQ(err.error.message, "could not decrypt key with given password");
}
const auto updatedAccounts = Accounts::getAccounts();
ASSERT_EQ(updatedAccounts.size(), 2);
}
TEST(AccountsAPI, TestAddAccountWithMnemonicAndPath)
{
constexpr auto testRootAccountName = "test_root_account-name";
constexpr auto testAccountPassword = "password*";
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true);
auto hashedPassword{Utils::hashString(testAccountPassword)};
const auto newTestAccountName = u"test_import_from_mnemonic-name"_qs;
const auto newTestAccountColor = QColor("fuchsia");
const auto newTestAccountEmoji = u""_qs;
const auto newTestAccountPath = Status::Constants::General::PathWalletRoot;
Accounts::addAccountWithMnemonicAndPath("festival october control quarter husband dish throw couch depth stadium cigar whisper",
hashedPassword, newTestAccountName, newTestAccountColor, newTestAccountEmoji,
newTestAccountPath);
const auto updatedAccounts = Accounts::getAccounts();
ASSERT_EQ(updatedAccounts.size(), 3);
const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(),
[newTestAccountName = std::as_const(newTestAccountName)](const auto& a) {
return a.name == newTestAccountName;
});
ASSERT_NE(newAccountIt, updatedAccounts.end());
const auto &newAccount = *newAccountIt;
ASSERT_FALSE(newAccount.address.isEmpty());
ASSERT_FALSE(newAccount.isChat);
ASSERT_FALSE(newAccount.isWallet);
ASSERT_EQ(newAccount.color, newTestAccountColor);
ASSERT_FALSE(newAccount.derivedFrom.has_value());
ASSERT_EQ(newAccount.emoji, newTestAccountEmoji);
ASSERT_EQ(newAccount.mixedcaseAddress.toUpper(), newAccount.address.toUpper());
ASSERT_EQ(newAccount.path, newTestAccountPath);
ASSERT_FALSE(newAccount.publicKey.isEmpty());
}
/// Show that the menmonic is not validated. Client has to validate the user provided mnemonic
TEST(AccountsAPI, TestAddAccountWithMnemonicAndPath_WrongMnemonicWorks)
{
constexpr auto testRootAccountName = "test_root_account-name";
constexpr auto testAccountPassword = "password*";
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true);
auto hashedPassword{Utils::hashString(testAccountPassword)};
const auto newTestAccountName = u"test_import_from_wrong_mnemonic-name"_qs;
const auto newTestAccountColor = QColor("fuchsia");
const auto newTestAccountEmoji = u""_qs;
const auto newTestAccountPath = Status::Constants::General::PathWalletRoot;
// Added an inexistent word. The mnemonic is not checked.
Accounts::addAccountWithMnemonicAndPath("october control quarter husband dish throw couch depth stadium cigar waku",
hashedPassword, newTestAccountName, newTestAccountColor, newTestAccountEmoji,
newTestAccountPath);
const auto updatedAccounts = Accounts::getAccounts();
ASSERT_EQ(updatedAccounts.size(), 3);
const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(),
[newTestAccountName = std::as_const(newTestAccountName)](const auto& a) {
return a.name == newTestAccountName;
});
ASSERT_NE(newAccountIt, updatedAccounts.end());
const auto &newAccount = *newAccountIt;
ASSERT_FALSE(newAccount.address.isEmpty());
ASSERT_FALSE(newAccount.isChat);
ASSERT_FALSE(newAccount.isWallet);
ASSERT_EQ(newAccount.color, newTestAccountColor);
ASSERT_FALSE(newAccount.derivedFrom.has_value());
ASSERT_EQ(newAccount.emoji, newTestAccountEmoji);
ASSERT_EQ(newAccount.mixedcaseAddress.toUpper(), newAccount.address.toUpper());
ASSERT_EQ(newAccount.path, newTestAccountPath);
ASSERT_FALSE(newAccount.publicKey.isEmpty());
}
TEST(AccountsAPI, TestAddAccountWatch)
{
constexpr auto testRootAccountName = "test_root_account-name";
constexpr auto testAccountPassword = "password*";
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true);
const auto newTestAccountName = u"test_watch_only-name"_qs;
const auto newTestAccountColor = QColor("fuchsia");
const auto newTestAccountEmoji = u""_qs;
Accounts::addAccountWatch("0x145b6B821523afFC346774b41ACC7b77A171BbA4", newTestAccountName, newTestAccountColor, newTestAccountEmoji);
const auto updatedAccounts = Accounts::getAccounts();
ASSERT_EQ(updatedAccounts.size(), 3);
const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(),
[newTestAccountName = std::as_const(newTestAccountName)](const auto& a) {
return a.name == newTestAccountName;
});
ASSERT_NE(newAccountIt, updatedAccounts.end());
const auto &newAccount = *newAccountIt;
ASSERT_FALSE(newAccount.address.isEmpty());
ASSERT_FALSE(newAccount.isChat);
ASSERT_FALSE(newAccount.isWallet);
ASSERT_EQ(newAccount.color, newTestAccountColor);
ASSERT_FALSE(newAccount.derivedFrom.has_value());
ASSERT_EQ(newAccount.emoji, newTestAccountEmoji);
ASSERT_EQ(newAccount.mixedcaseAddress.toUpper(), newAccount.address.toUpper());
ASSERT_TRUE(newAccount.path.isEmpty());
ASSERT_TRUE(newAccount.publicKey.isEmpty());
}
TEST(AccountsAPI, TestDeleteAccount)
{
constexpr auto testRootAccountName = "test_root_account-name";
constexpr auto testAccountPassword = "password*";
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true);
const auto newTestAccountName = u"test_account_to_delete-name"_qs;
const auto newTestAccountColor = QColor("fuchsia");
const auto newTestAccountEmoji = u""_qs;
Accounts::addAccountWatch("0x145b6B821523afFC346774b41ACC7b77A171BbA4", newTestAccountName, newTestAccountColor, newTestAccountEmoji);
const auto updatedAccounts = Accounts::getAccounts();
ASSERT_EQ(updatedAccounts.size(), 3);
const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(),
[newTestAccountName = std::as_const(newTestAccountName)](const auto& a) {
return a.name == newTestAccountName;
});
ASSERT_NE(newAccountIt, updatedAccounts.end());
const auto &newAccount = *newAccountIt;
Accounts::deleteAccount(newAccount.address);
const auto updatedDefaultAccounts = Accounts::getAccounts();
ASSERT_EQ(updatedDefaultAccounts.size(), 2);
}
} }

View File

@ -7,8 +7,6 @@
#include <ScopedTestAccount.h> #include <ScopedTestAccount.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <gmock/gmock.h>
namespace fs = std::filesystem; namespace fs = std::filesystem;
@ -17,7 +15,7 @@ namespace Status::Testing {
/// This is an integration test to check that status-go doesn't crash on apple silicon when starting Me /// This is an integration test to check that status-go doesn't crash on apple silicon when starting Me
/// \warning the test depends on IO and it is not deterministic, fast, focused or reliable. It is here for validation only /// \warning the test depends on IO and it is not deterministic, fast, focused or reliable. It is here for validation only
/// \todo fin a way to test the integration within a test environment. Also how about reusing an existing account /// \todo fin a way to test the integration within a test environment. Also how about reusing an existing account
TEST(OnboardingModule, TestStartMessaging) TEST(MessagingApi, TestStartMessaging)
{ {
bool nodeReady = false; bool nodeReady = false;
QObject::connect(StatusGo::SignalsManager::instance(), &StatusGo::SignalsManager::nodeReady, [&nodeReady](const QString& error) { QObject::connect(StatusGo::SignalsManager::instance(), &StatusGo::SignalsManager::nodeReady, [&nodeReady](const QString& error) {

View File

@ -0,0 +1,39 @@
#include <StatusGo/Accounts/AccountsAPI.h>
#include <StatusGo/Metadata/api_response.h>
#include <StatusGo/Accounts/Accounts.h>
#include <Onboarding/Common/Constants.h>
#include <Onboarding/OnboardingController.h>
#include <IOTestHelpers.h>
#include <ScopedTestAccount.h>
#include <gtest/gtest.h>
namespace Accounts = Status::StatusGo::Accounts;
namespace fs = std::filesystem;
namespace Status::Testing {
TEST(OnboardingApi, TestOpenAccountsNoDataFails) {
AutoCleanTempTestDir fusedTestFolder{test_info_->name()};
auto response = Accounts::openAccounts(fusedTestFolder.tempFolder().c_str());
EXPECT_FALSE(response.containsError());
EXPECT_EQ(response.result.count(), 0);
}
TEST(OnboardingApi, TestOpenAccountsNoDataCreatesFiles) {
AutoCleanTempTestDir fusedTestFolder{test_info_->name()};
auto response = Accounts::openAccounts(fusedTestFolder.tempFolder().c_str());
EXPECT_FALSE(response.containsError());
int fileCount = 0;
for (const auto & file : fs::directory_iterator(fusedTestFolder.tempFolder()))
fileCount++;
EXPECT_GT(fileCount, 0);
}
}

View File

@ -0,0 +1,65 @@
#include <StatusGo/Accounts/AccountsAPI.h>
#include <StatusGo/Wallet/WalletApi.h>
#include <StatusGo/Metadata/api_response.h>
#include <Onboarding/Accounts/AccountsServiceInterface.h>
#include <Onboarding/Common/Constants.h>
#include <Onboarding/OnboardingController.h>
#include <ScopedTestAccount.h>
#include <StatusGo/Utils.h>
#include <gtest/gtest.h>
namespace Wallet = Status::StatusGo::Wallet;
namespace Utils = Status::StatusGo::Utils;
namespace fs = std::filesystem;
/// \warning for now this namespace contains integration test to check the basic assumptions of status-go while building the C++ wrapper.
/// \warning the tests depend on IO and are not deterministic, fast, focused or reliable. They are here for validation only
/// \todo after status-go API coverage all the integration tests should go away and only test the thin wrapper code
namespace Status::Testing {
TEST(WalletApi, TestGetDerivedAddressesForPath)
{
constexpr auto testRootAccountName = "test_root_account-name";
constexpr auto testAccountPassword = "password*";
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true);
const auto walletAccount = testAccount.firstWalletAccount();
const auto chatAccount = testAccount.firstChatAccount();
const auto rootAccount = testAccount.onboardingController()->accountsService()->getLoggedInAccount();
ASSERT_EQ(rootAccount.address, walletAccount.derivedFrom.value());
const auto hashedPassword{Utils::hashString(testAccountPassword)};
const auto testPath = Status::Constants::General::PathWalletRoot;
// chatAccount.address
const auto chatDerivedAddresses = Wallet::getDerivedAddressesForPath(hashedPassword, chatAccount.address, testPath, 3, 1);
// Check that no change is done
const auto updatedAccounts = Accounts::getAccounts();
ASSERT_EQ(updatedAccounts.size(), 2);
ASSERT_EQ(chatDerivedAddresses.size(), 3);
// all alreadyCreated are false
ASSERT_TRUE(std::none_of(chatDerivedAddresses.begin(), chatDerivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; }));
// all hasActivity are false
ASSERT_TRUE(std::none_of(chatDerivedAddresses.begin(), chatDerivedAddresses.end(), [](const auto& a) { return a.hasActivity; }));
// all address are valid
ASSERT_TRUE(std::none_of(chatDerivedAddresses.begin(), chatDerivedAddresses.end(), [](const auto& a) { return a.address.isEmpty(); }));
const auto walletDerivedAddresses = Wallet::getDerivedAddressesForPath(hashedPassword, walletAccount.address, testPath, 2, 1);
ASSERT_EQ(walletDerivedAddresses.size(), 2);
// all alreadyCreated are false
ASSERT_TRUE(std::none_of(walletDerivedAddresses.begin(), walletDerivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; }));
const auto rootDerivedAddresses = Wallet::getDerivedAddressesForPath(hashedPassword, rootAccount.address, testPath, 4, 1);
ASSERT_EQ(rootDerivedAddresses.size(), 4);
ASSERT_EQ(std::count_if(rootDerivedAddresses.begin(), rootDerivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; }), 1);
const auto &existingAddress = *std::find_if(rootDerivedAddresses.begin(), rootDerivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; });
ASSERT_EQ(existingAddress.address, walletAccount.address);
ASSERT_FALSE(existingAddress.hasActivity);
}
} // namespace