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(libs)
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
add_subdirectory(resources)
add_subdirectory(ui/imports/assets)

View File

@ -21,6 +21,8 @@ target_link_libraries(ApplicationCore
PRIVATE
Qt6::Quick
Qt6::Qml
Status::Helpers
)
install(
@ -42,8 +44,6 @@ target_include_directories(ApplicationCore
target_sources(ApplicationCore
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.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 "Conversions.h"
#include "Helpers/conversions.h"
#include <filesystem>
@ -21,7 +21,7 @@ UserConfiguration::UserConfiguration(QObject *parent)
const QString UserConfiguration::qmlUserDataFolder() const
{
return toString(m_userDataFolder.string());
return toQString(m_userDataFolder.string());
}
const fs::path &UserConfiguration::userDataFolder() const

View File

@ -6,6 +6,8 @@ project(Helpers
VERSION 0.1.0
LANGUAGES CXX)
find_package(nlohmann_json 3.10.5 REQUIRED)
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml REQUIRED)
@ -48,6 +50,9 @@ target_include_directories(Helpers
)
target_link_libraries(Helpers
PUBLIC
nlohmann_json::nlohmann_json
PRIVATE
Qt6::Quick
Qt6::Qml
@ -61,6 +66,8 @@ install(
target_sources(Helpers
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/logs.h
${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
Status::ApplicationCore
Status::Helpers
Status::StatusGoQt
Status::StatusGoConfig

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -45,7 +45,9 @@ public:
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;
DerivedAccounts derivedAccounts;
// The following two are set additionally.
// set additionally.
QString alias;
QString identicon;
bool isValid() const
{

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@
#include <Onboarding/OnboardingController.h>
#include <StatusGo/Accounts/Accounts.h>
#include <StatusGo/Accounts/AccountsAPI.h>
#include <QCoreApplication>
@ -15,6 +16,7 @@
namespace Testing = Status::Testing;
namespace Onboarding = Status::Onboarding;
namespace Accounts = Status::StatusGo::Accounts;
namespace fs = std::filesystem;
@ -86,6 +88,8 @@ ScopedTestAccount::ScopedTestAccount(const std::string &tempTestSubfolderName, c
ScopedTestAccount::~ScopedTestAccount()
{
const auto rootAccount = m_onboarding->accountsService()->getLoggedInAccount();
m_onboarding->accountsService()->deleteMultiAccount(rootAccount);
}
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");
}
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
{
return m_onboarding.get();

View File

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

View File

@ -31,7 +31,7 @@ public:
MOCK_METHOD(QString, login, (Onboarding::AccountDto, const QString&), (override));
MOCK_METHOD(void, clear, (), (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 <Constants.h>
#include <StatusGo/Accounts/Accounts.h>
#include <Onboarding/Accounts/AccountsService.h>
#include <Onboarding/Common/Constants.h>
#include <gtest/gtest.h>
@ -14,14 +17,14 @@ namespace fs = std::filesystem;
namespace Status::Testing {
class AccountsServicesTest : public ::testing::Test
class AccountsService : public ::testing::Test
{
protected:
std::unique_ptr<Onboarding::AccountsService> m_accountsService;
std::unique_ptr<Testing::AutoCleanTempTestDir> m_fusedTestFolder;
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->init(m_fusedTestFolder->tempFolder() / Constants::statusGoDataDirName);
}
@ -33,7 +36,7 @@ protected:
};
TEST_F(AccountsServicesTest, GeneratedAccounts)
TEST_F(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";

View File

@ -154,7 +154,6 @@ TEST(OnboardingModule, TestLoginEndToEnd)
accountLoggedInError = true;
});
//auto errorString = accountsService->login(accounts[0], accountPassword);
// 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 errorString = accountsService->login(*ourAccountRes, accountPassword);

View File

@ -8,7 +8,9 @@ project(StatusGoQt
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()
add_library(${PROJECT_NAME} SHARED)
@ -16,18 +18,35 @@ add_library(${PROJECT_NAME} SHARED)
# Use by linker only
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}
PUBLIC
Status::Helpers
PRIVATE
Qt6::Gui
Qt6::Core
Qt6::Concurrent
PRIVATE
nlohmann_json::nlohmann_json
statusgo_shared
)
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
# 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
@ -46,3 +65,32 @@ install(
IMPORTED_RUNTIME_ARTIFACTS
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 "Utils.h"
#include "libstatus.h"
#include <libstatus.h>
const int NUMBER_OF_ADDRESSES_TO_GENERATE = 5;
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)
{
try
@ -212,7 +194,7 @@ RpcResponse<QJsonArray> openAccounts(const char* dataDirPath)
}
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{
{"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,
const QString& identicon, const QString& thumbnail, const QString& large,
const QJsonObject& nodeConfig)
const QString& thumbnail, const QString& large, const QJsonObject& nodeConfig)
{
QJsonObject payload{
{"name", name},

View File

@ -8,8 +8,6 @@ namespace Status::StatusGo::Accounts
{
RpcResponse<QJsonArray> generateAddresses(const QVector<QString>& paths);
RpcResponse<QString> generateIdenticon(const QString& publicKey);
RpcResponse<QString> generateAlias(const QString& publicKey);
RpcResponse<QJsonObject> storeDerivedAccounts(const QString& accountId, const QString& hashedPassword,
@ -24,10 +22,10 @@ namespace Status::StatusGo::Accounts
/// opens database and returns accounts list.
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,
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,
const QString& identicon, const QString& thumbnail, const QString& large,
const QJsonObject& nodeConfig);
const QString& thumbnail, const QString& large, const QJsonObject& nodeConfig);
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 "Utils.h"
#include "libstatus.h"
#include <libstatus.h>
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 "libstatus.h"
#include <libstatus.h>
using namespace std::string_literals;

View File

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

View File

@ -80,14 +80,14 @@ RpcResponse<T> buildPrivateRPCResponse(const T& json)
return response;
}
const char* statusgoCallPrivateRPC(const char* inputJSON);
const char* statusGoCallPrivateRPC(const char* inputJSON);
template<class T>
RpcResponse<T> callPrivateRpc(const QByteArray& payload)
{
try
{
auto result = statusgoCallPrivateRPC(payload.data());
auto result = statusGoCallPrivateRPC(payload.data());
T 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)
project(TestStatusGoQt VERSION 0.1.0 LANGUAGES CXX)
project(TestStatusGoQtModule VERSION 0.1.0 LANGUAGES CXX)
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
@ -11,12 +11,14 @@ find_package(GTest REQUIRED)
enable_testing()
add_executable(${PROJECT_NAME}
add_executable(TestStatusGoQtModule
test_accounts.cpp
test_messaging.cpp
test_onboarding.cpp
test_wallet.cpp
)
target_link_libraries(${PROJECT_NAME}
target_link_libraries(TestStatusGoQtModule
PRIVATE
Qt6::Core
@ -34,6 +36,6 @@ target_link_libraries(${PROJECT_NAME}
include(GoogleTest)
gtest_add_tests(
TARGET ${PROJECT_NAME}
TARGET TestStatusGoQtModule
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 <Onboarding/Common/Constants.h>
#include <Onboarding/OnboardingController.h>
#include <StatusGo/Utils.h>
#include <IOTestHelpers.h>
#include <ScopedTestAccount.h>
#include <gtest/gtest.h>
namespace Accounts = Status::StatusGo::Accounts;
namespace StatusGo = Status::StatusGo;
namespace Utils = Status::StatusGo::Utils;
namespace fs = std::filesystem;
namespace Status::Testing {
TEST(Onboarding, TestOpenAccountsNoDataFails) {
AutoCleanTempTestDir fusedTestFolder{test_info_->name()};
/// \todo fin a way to test the integration within a test environment. Also how about reusing an existing account
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());
EXPECT_FALSE(response.containsError());
EXPECT_EQ(response.result.count(), 0);
const auto accounts = Accounts::getAccounts();
// TODO: enable after calling reset to status-go
//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) {
AutoCleanTempTestDir fusedTestFolder{test_info_->name()};
TEST(Accounts, TestGenerateAccountWithDerivedPath)
{
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());
EXPECT_FALSE(response.containsError());
auto hashedPassword{Utils::hashString(testAccountPassword)};
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;
for (const auto & file : fs::directory_iterator(fusedTestFolder.tempFolder()))
fileCount++;
EXPECT_EQ(fileCount, 0);
const auto chatAccount = testAccount.firstChatAccount();
Accounts::generateAccountWithDerivedPath(hashedPassword, newTestAccountName,
newTestAccountColor, newTestAccountEmoji,
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 <gtest/gtest.h>
#include <gmock/gmock.h>
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
/// \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
TEST(OnboardingModule, TestStartMessaging)
TEST(MessagingApi, TestStartMessaging)
{
bool nodeReady = false;
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