diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d200641bd..ca95540b5f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/libs/ApplicationCore/CMakeLists.txt b/libs/ApplicationCore/CMakeLists.txt index bd656c7222..e329717956 100644 --- a/libs/ApplicationCore/CMakeLists.txt +++ b/libs/ApplicationCore/CMakeLists.txt @@ -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 ) diff --git a/libs/ApplicationCore/src/ApplicationCore/Conversions.cpp b/libs/ApplicationCore/src/ApplicationCore/Conversions.cpp deleted file mode 100644 index f7d0caad5f..0000000000 --- a/libs/ApplicationCore/src/ApplicationCore/Conversions.cpp +++ /dev/null @@ -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()); -} - -} diff --git a/libs/ApplicationCore/src/ApplicationCore/Conversions.h b/libs/ApplicationCore/src/ApplicationCore/Conversions.h deleted file mode 100644 index a76fa8bb52..0000000000 --- a/libs/ApplicationCore/src/ApplicationCore/Conversions.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include - -#include - -namespace Status { - -QString toString(const std::filesystem::path& path); -std::filesystem::path toPath(const QString& pathStr); - -} diff --git a/libs/ApplicationCore/src/ApplicationCore/UserConfiguration.cpp b/libs/ApplicationCore/src/ApplicationCore/UserConfiguration.cpp index b08b0ad403..268e04ea3a 100644 --- a/libs/ApplicationCore/src/ApplicationCore/UserConfiguration.cpp +++ b/libs/ApplicationCore/src/ApplicationCore/UserConfiguration.cpp @@ -1,6 +1,6 @@ #include "UserConfiguration.h" -#include "Conversions.h" +#include "Helpers/conversions.h" #include @@ -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 diff --git a/libs/Helpers/CMakeLists.txt b/libs/Helpers/CMakeLists.txt index 0e821e6dff..eb12f1df92 100644 --- a/libs/Helpers/CMakeLists.txt +++ b/libs/Helpers/CMakeLists.txt @@ -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 diff --git a/libs/Helpers/src/Helpers/conversions.cpp b/libs/Helpers/src/Helpers/conversions.cpp new file mode 100644 index 0000000000..d9c3fd97af --- /dev/null +++ b/libs/Helpers/src/Helpers/conversions.cpp @@ -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()); +} + +} diff --git a/libs/Helpers/src/Helpers/conversions.h b/libs/Helpers/src/Helpers/conversions.h new file mode 100644 index 0000000000..cf0ca95f62 --- /dev/null +++ b/libs/Helpers/src/Helpers/conversions.h @@ -0,0 +1,45 @@ +#pragma once + + +#include +#include + +#include + +#include + +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 { + 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()); + } +}; + +template<> +struct adl_serializer { + 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())); + } +}; + +} // namespace nlohmann diff --git a/libs/Onboarding/CMakeLists.txt b/libs/Onboarding/CMakeLists.txt index 6914b7c12d..111d47336d 100644 --- a/libs/Onboarding/CMakeLists.txt +++ b/libs/Onboarding/CMakeLists.txt @@ -43,6 +43,7 @@ target_link_libraries(Onboarding Qt6::Concurrent Status::ApplicationCore + Status::Helpers Status::StatusGoQt Status::StatusGoConfig diff --git a/libs/Onboarding/qml/Status/Onboarding/ConfirmPasswordPage.qml b/libs/Onboarding/qml/Status/Onboarding/ConfirmPasswordPage.qml index 8cce496543..9d13257f0d 100644 --- a/libs/Onboarding/qml/Status/Onboarding/ConfirmPasswordPage.qml +++ b/libs/Onboarding/qml/Status/Onboarding/ConfirmPasswordPage.qml @@ -11,6 +11,7 @@ SetupNewProfilePageBase { TempTextInput { id: confirmPasswordInput + // TODO: remove this developer helper text: qsTr("1234567890") width: 416 diff --git a/libs/Onboarding/qml/Status/Onboarding/WelcomeView.qml b/libs/Onboarding/qml/Status/Onboarding/WelcomeView.qml index 1a11cd17a4..e25db72d2f 100644 --- a/libs/Onboarding/qml/Status/Onboarding/WelcomeView.qml +++ b/libs/Onboarding/qml/Status/Onboarding/WelcomeView.qml @@ -48,6 +48,10 @@ OnboardingPageBase { id: passwordInput Layout.preferredWidth: 328 Layout.preferredHeight: 44 + + // TODO: remove dev helper + text: "1234567890" + // END dev } Button { diff --git a/libs/Onboarding/src/Onboarding/Accounts/AccountDto.h b/libs/Onboarding/src/Onboarding/Accounts/AccountDto.h index 6ff62a82e7..69fe1df6ac 100644 --- a/libs/Onboarding/src/Onboarding/Accounts/AccountDto.h +++ b/libs/Onboarding/src/Onboarding/Accounts/AccountDto.h @@ -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 } diff --git a/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp b/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp index 1be33ad817..392c666c76 100644 --- a/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp +++ b/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp @@ -1,11 +1,12 @@ #include "AccountsService.h" #include +#include #include #include #include -#include +#include #include @@ -13,7 +14,7 @@ std::optional 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(); diff --git a/libs/Onboarding/src/Onboarding/Accounts/AccountsService.h b/libs/Onboarding/src/Onboarding/Accounts/AccountsService.h index dc6b2d31b5..6642052a01 100644 --- a/libs/Onboarding/src/Onboarding/Accounts/AccountsService.h +++ b/libs/Onboarding/src/Onboarding/Accounts/AccountsService.h @@ -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 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; diff --git a/libs/Onboarding/src/Onboarding/Accounts/AccountsServiceInterface.h b/libs/Onboarding/src/Onboarding/Accounts/AccountsServiceInterface.h index 9ac2d454fe..fbc6093961 100644 --- a/libs/Onboarding/src/Onboarding/Accounts/AccountsServiceInterface.h +++ b/libs/Onboarding/src/Onboarding/Accounts/AccountsServiceInterface.h @@ -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; + } diff --git a/libs/Onboarding/src/Onboarding/Accounts/GeneratedAccountDto.h b/libs/Onboarding/src/Onboarding/Accounts/GeneratedAccountDto.h index 12c468bbfe..37aa541862 100644 --- a/libs/Onboarding/src/Onboarding/Accounts/GeneratedAccountDto.h +++ b/libs/Onboarding/src/Onboarding/Accounts/GeneratedAccountDto.h @@ -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 { diff --git a/libs/Onboarding/src/Onboarding/NewAccountController.cpp b/libs/Onboarding/src/Onboarding/NewAccountController.cpp index 3a89393d64..333d96dd3e 100644 --- a/libs/Onboarding/src/Onboarding/NewAccountController.cpp +++ b/libs/Onboarding/src/Onboarding/NewAccountController.cpp @@ -11,16 +11,8 @@ namespace Status::Onboarding namespace StatusGo = Status::StatusGo; -NewAccountController::NewAccountController(std::shared_ptr 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); diff --git a/libs/Onboarding/src/Onboarding/OnboardingController.cpp b/libs/Onboarding/src/Onboarding/OnboardingController.cpp index 2a35642d12..addbc4d582 100644 --- a/libs/Onboarding/src/Onboarding/OnboardingController.cpp +++ b/libs/Onboarding/src/Onboarding/OnboardingController.cpp @@ -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 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; +} + } diff --git a/libs/Onboarding/src/Onboarding/OnboardingController.h b/libs/Onboarding/src/Onboarding/OnboardingController.h index 87bd66e4ce..665ad187b0 100644 --- a/libs/Onboarding/src/Onboarding/OnboardingController.h +++ b/libs/Onboarding/src/Onboarding/OnboardingController.h @@ -2,7 +2,6 @@ #include "UserAccountsModel.h" -#include "Accounts/AccountsServiceInterface.h" #include "Accounts/AccountDto.h" #include @@ -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 accountsService() const; signals: void accountLoggedIn(); diff --git a/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.cpp b/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.cpp index b8cb7e777c..76c37a4984 100644 --- a/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.cpp +++ b/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.cpp @@ -8,6 +8,7 @@ #include #include +#include #include @@ -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 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(); diff --git a/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.h b/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.h index 3186439a1f..015acca96d 100644 --- a/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.h +++ b/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -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 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 m_fusedTestFolder; diff --git a/libs/Onboarding/tests/ServiceMock.h b/libs/Onboarding/tests/ServiceMock.h index 40f37ad73b..1a3fd5a10a 100644 --- a/libs/Onboarding/tests/ServiceMock.h +++ b/libs/Onboarding/tests/ServiceMock.h @@ -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)); }; } diff --git a/libs/Onboarding/tests/test_AccountService.cpp b/libs/Onboarding/tests/test_AccountService.cpp index c19e7b8c1d..40d0241491 100644 --- a/libs/Onboarding/tests/test_AccountService.cpp +++ b/libs/Onboarding/tests/test_AccountService.cpp @@ -3,7 +3,10 @@ #include #include +#include + #include +#include #include @@ -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 m_accountsService; std::unique_ptr m_fusedTestFolder; void SetUp() override { - m_fusedTestFolder = std::make_unique("AccountsServicesTest"); + m_fusedTestFolder = std::make_unique("TestAccountsService"); m_accountsService = std::make_unique(); 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"; diff --git a/libs/Onboarding/tests/test_OnboardingModule.cpp b/libs/Onboarding/tests/test_OnboardingModule.cpp index cfba04263f..38890312e5 100644 --- a/libs/Onboarding/tests/test_OnboardingModule.cpp +++ b/libs/Onboarding/tests/test_OnboardingModule.cpp @@ -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); diff --git a/libs/StatusGoQt/CMakeLists.txt b/libs/StatusGoQt/CMakeLists.txt index 81de8e39c9..89efbd78e9 100644 --- a/libs/StatusGoQt/CMakeLists.txt +++ b/libs/StatusGoQt/CMakeLists.txt @@ -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 +) diff --git a/libs/StatusGoQt/src/CMakeLists.txt b/libs/StatusGoQt/src/CMakeLists.txt deleted file mode 100644 index 7e7b61d0b6..0000000000 --- a/libs/StatusGoQt/src/CMakeLists.txt +++ /dev/null @@ -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} -) diff --git a/libs/StatusGoQt/src/StatusGo/Accounts/Accounts.cpp b/libs/StatusGoQt/src/StatusGo/Accounts/Accounts.cpp index 4d745a0cd9..8c725fea7c 100644 --- a/libs/StatusGoQt/src/StatusGo/Accounts/Accounts.cpp +++ b/libs/StatusGoQt/src/StatusGo/Accounts/Accounts.cpp @@ -1,7 +1,8 @@ #include "Accounts.h" #include "Utils.h" -#include "libstatus.h" + +#include const int NUMBER_OF_ADDRESSES_TO_GENERATE = 5; const int MNEMONIC_PHRASE_LENGTH = 12; @@ -43,25 +44,6 @@ RpcResponse generateAddresses(const QVector& paths) } } -RpcResponse generateIdenticon(const QString& publicKey) -{ - try - { - QString identicon; - if(!publicKey.isEmpty()) - { - identicon = Identicon(publicKey.toUtf8().data()); - } - return Utils::buildPrivateRPCResponse(identicon); - } - catch (...) - { - auto response = RpcResponse(QString()); - response.error.message = QObject::tr("an error generating identicon occurred"); - return response; - } -} - RpcResponse generateAlias(const QString& publicKey) { try @@ -212,7 +194,7 @@ RpcResponse openAccounts(const char* dataDirPath) } RpcResponse 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 login(const QString& name, const QString& keyUid, const } RpcResponse 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}, diff --git a/libs/StatusGoQt/src/StatusGo/Accounts/Accounts.h b/libs/StatusGoQt/src/StatusGo/Accounts/Accounts.h index c6b24c0b1b..c4c76ece97 100644 --- a/libs/StatusGoQt/src/StatusGo/Accounts/Accounts.h +++ b/libs/StatusGoQt/src/StatusGo/Accounts/Accounts.h @@ -8,8 +8,6 @@ namespace Status::StatusGo::Accounts { RpcResponse generateAddresses(const QVector& paths); - RpcResponse generateIdenticon(const QString& publicKey); - RpcResponse generateAlias(const QString& publicKey); RpcResponse storeDerivedAccounts(const QString& accountId, const QString& hashedPassword, @@ -24,10 +22,10 @@ namespace Status::StatusGo::Accounts /// opens database and returns accounts list. RpcResponse openAccounts(const char* dataDirPath); + /// TODO harmonise password parameters (hashed or plain)? RpcResponse 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 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 logout(); } diff --git a/libs/StatusGoQt/src/StatusGo/Accounts/AccountsAPI.cpp b/libs/StatusGoQt/src/StatusGo/Accounts/AccountsAPI.cpp new file mode 100644 index 0000000000..fc4ed76f45 --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/Accounts/AccountsAPI.cpp @@ -0,0 +1,98 @@ +#include "AccountsAPI.h" + +#include "Utils.h" +#include "Metadata/api_response.h" + +#include + +#include + +#include + +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().result; +} + +void generateAccountWithDerivedPath(const QString &hashedPassword, const QString &name, const QColor &color, const QString &emoji, + const QString &path, const QString &derivedFrom) +{ + std::vector 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 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 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 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(keyUID.toStdString().c_str()), const_cast(keyStoreDir.string().c_str())); + auto resultJson = json::parse(result); + checkApiError(resultJson); +} + +} diff --git a/libs/StatusGoQt/src/StatusGo/Accounts/AccountsAPI.h b/libs/StatusGoQt/src/StatusGo/Accounts/AccountsAPI.h new file mode 100644 index 0000000000..885192705f --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/Accounts/AccountsAPI.h @@ -0,0 +1,54 @@ +#pragma once + + +#include "Accounts/MultiAccount.h" + +#include +#include + +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 diff --git a/libs/StatusGoQt/src/StatusGo/Accounts/MultiAccount.cpp b/libs/StatusGoQt/src/StatusGo/Accounts/MultiAccount.cpp new file mode 100644 index 0000000000..dc3c2d15d1 --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/Accounts/MultiAccount.cpp @@ -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().empty()) + d.derivedFrom = j.at("derived-from").get(); +} + +} diff --git a/libs/StatusGoQt/src/StatusGo/Accounts/MultiAccount.h b/libs/StatusGoQt/src/StatusGo/Accounts/MultiAccount.h new file mode 100644 index 0000000000..47a0c26bf1 --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/Accounts/MultiAccount.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include + +#include + +#include + +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 derivedFrom; + QString emoji; + bool isHidden = false; + QString mixedcaseAddress; + QString name; + QString path; + QString publicKey; + bool isRemoved = false; + bool isWallet = false; +}; + +using MultiAccounts = std::vector; + +void to_json(json& j, const MultiAccount& d); +void from_json(const json& j, MultiAccount& d); + +} diff --git a/libs/StatusGoQt/src/StatusGo/CMakeLists.txt b/libs/StatusGoQt/src/StatusGo/CMakeLists.txt deleted file mode 100644 index 0393538c50..0000000000 --- a/libs/StatusGoQt/src/StatusGo/CMakeLists.txt +++ /dev/null @@ -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 -) diff --git a/libs/StatusGoQt/src/StatusGo/General.cpp b/libs/StatusGoQt/src/StatusGo/General.cpp index d769a0c791..95855ea038 100644 --- a/libs/StatusGoQt/src/StatusGo/General.cpp +++ b/libs/StatusGoQt/src/StatusGo/General.cpp @@ -1,7 +1,8 @@ #include "General.h" #include "Utils.h" -#include "libstatus.h" + +#include namespace Status::StatusGo::General { diff --git a/libs/StatusGoQt/src/StatusGo/Metadata/api_response.cpp b/libs/StatusGoQt/src/StatusGo/Metadata/api_response.cpp new file mode 100644 index 0000000000..6154c5b5b1 --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/Metadata/api_response.cpp @@ -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(); + throw CallGenericPrepareJsonError(apiErr); + } + assert(error.is_string()); + const auto apiError = response.get(); + if(!apiError.error.empty()) + throw CallGenericMakeJsonError(response.get()); + } +} + +/// \throws \c CallPrivateRpcError, \c nlohmann::exception +void checkPrivateRpcCallResultAndReportError(const json &response) { + if(response.contains("error")) + throw CallPrivateRpcError(response.get()); +} + +} // namespace diff --git a/libs/StatusGoQt/src/StatusGo/Metadata/api_response.h b/libs/StatusGoQt/src/StatusGo/Metadata/api_response.h new file mode 100644 index 0000000000..6f67ae361d --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/Metadata/api_response.h @@ -0,0 +1,163 @@ +#pragma once + +#include + +#include + +#include + +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); + +} diff --git a/libs/StatusGoQt/src/StatusGo/SignalsManager.cpp b/libs/StatusGoQt/src/StatusGo/SignalsManager.cpp index 419cbcd23f..d2ca62af94 100644 --- a/libs/StatusGoQt/src/StatusGo/SignalsManager.cpp +++ b/libs/StatusGoQt/src/StatusGo/SignalsManager.cpp @@ -2,7 +2,7 @@ #include -#include "libstatus.h" +#include using namespace std::string_literals; diff --git a/libs/StatusGoQt/src/StatusGo/Utils.cpp b/libs/StatusGoQt/src/StatusGo/Utils.cpp index e4007dd347..155e6850c5 100644 --- a/libs/StatusGoQt/src/StatusGo/Utils.cpp +++ b/libs/StatusGoQt/src/StatusGo/Utils.cpp @@ -1,6 +1,6 @@ #include "Utils.h" -#include "libstatus.h" +#include #include @@ -15,8 +15,8 @@ QJsonArray toJsonArray(const QVector& 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(inputJSON)); } diff --git a/libs/StatusGoQt/src/StatusGo/Utils.h b/libs/StatusGoQt/src/StatusGo/Utils.h index f39a306fec..75e1810a19 100644 --- a/libs/StatusGoQt/src/StatusGo/Utils.h +++ b/libs/StatusGoQt/src/StatusGo/Utils.h @@ -80,14 +80,14 @@ RpcResponse buildPrivateRPCResponse(const T& json) return response; } -const char* statusgoCallPrivateRPC(const char* inputJSON); +const char* statusGoCallPrivateRPC(const char* inputJSON); template RpcResponse callPrivateRpc(const QByteArray& payload) { try { - auto result = statusgoCallPrivateRPC(payload.data()); + auto result = statusGoCallPrivateRPC(payload.data()); T jsonResult; if(!Utils::checkReceivedResponse(result, jsonResult)) { diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/DerivedAddress.h b/libs/StatusGoQt/src/StatusGo/Wallet/DerivedAddress.h new file mode 100644 index 0000000000..3e0d1de47a --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/Wallet/DerivedAddress.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include + +#include + +#include + +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; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(DerivedAddress, address, path, hasActivity, alreadyCreated); + +} diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.cpp b/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.cpp new file mode 100644 index 0000000000..e438ef3b12 --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.cpp @@ -0,0 +1,33 @@ +#include "WalletApi.h" + +#include "Utils.h" +#include "Metadata/api_response.h" + +#include + +#include + +#include + +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 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().result; +} + +} // namespaces diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.h b/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.h new file mode 100644 index 0000000000..8fe248c0be --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.h @@ -0,0 +1,18 @@ +#pragma once + + +#include "Accounts/MultiAccount.h" +#include "DerivedAddress.h" + +#include + +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 diff --git a/libs/StatusGoQt/tests/CMakeLists.txt b/libs/StatusGoQt/tests/CMakeLists.txt new file mode 100644 index 0000000000..c958d7ceca --- /dev/null +++ b/libs/StatusGoQt/tests/CMakeLists.txt @@ -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} +) diff --git a/libs/StatusGoQt/tests/test_StatusGo.cpp b/libs/StatusGoQt/tests/test_StatusGo.cpp new file mode 100644 index 0000000000..8fc92fa613 --- /dev/null +++ b/libs/StatusGoQt/tests/test_StatusGo.cpp @@ -0,0 +1,31 @@ +#include + +#include + +#include + +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(); + 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(), nlohmann::detail::out_of_range); + auto callRawRPCBadJsonValStr = R"({"jsonrpc":"2.0","id":42,"error":23})"; + ASSERT_THROW(json::parse(callRawRPCBadJsonValStr).get(), nlohmann::detail::type_error); + + auto statusGoWithResultJsonStr = R"({"result":"0x123"})"; + auto statusGoWithResultJson = json::parse(statusGoWithResultJsonStr).get(); + ASSERT_EQ(statusGoWithResultJson.result, "0x123"); +} + +} // namespace diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt deleted file mode 100644 index f7921bdf60..0000000000 --- a/test/CMakeLists.txt +++ /dev/null @@ -1 +0,0 @@ -add_subdirectory(libs) \ No newline at end of file diff --git a/test/libs/CMakeLists.txt b/test/libs/CMakeLists.txt deleted file mode 100644 index fc7b11f4cd..0000000000 --- a/test/libs/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -# Libs integration tests -# -add_subdirectory(StatusGoQt) diff --git a/test/libs/StatusGoQt/CMakeLists.txt b/test/libs/StatusGoQt/CMakeLists.txt index 8b1b3064d3..15d3405aab 100644 --- a/test/libs/StatusGoQt/CMakeLists.txt +++ b/test/libs/StatusGoQt/CMakeLists.txt @@ -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} ) diff --git a/test/libs/StatusGoQt/test_accounts.cpp b/test/libs/StatusGoQt/test_accounts.cpp index c3789c8dca..d3991dd872 100644 --- a/test/libs/StatusGoQt/test_accounts.cpp +++ b/test/libs/StatusGoQt/test_accounts.cpp @@ -1,34 +1,240 @@ -#include - +#include +#include #include +#include +#include +#include + #include +#include + +#include + + 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); +} + + } diff --git a/test/libs/StatusGoQt/test_messaging.cpp b/test/libs/StatusGoQt/test_messaging.cpp index 8927605c74..d85da02d5d 100644 --- a/test/libs/StatusGoQt/test_messaging.cpp +++ b/test/libs/StatusGoQt/test_messaging.cpp @@ -7,8 +7,6 @@ #include #include -#include - 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) { diff --git a/test/libs/StatusGoQt/test_onboarding.cpp b/test/libs/StatusGoQt/test_onboarding.cpp new file mode 100644 index 0000000000..55ec7c67af --- /dev/null +++ b/test/libs/StatusGoQt/test_onboarding.cpp @@ -0,0 +1,39 @@ +#include +#include +#include + +#include +#include + +#include +#include + +#include + +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); +} + +} diff --git a/test/libs/StatusGoQt/test_wallet.cpp b/test/libs/StatusGoQt/test_wallet.cpp new file mode 100644 index 0000000000..0cc4b193bd --- /dev/null +++ b/test/libs/StatusGoQt/test_wallet.cpp @@ -0,0 +1,65 @@ +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include + +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