From d5afd6beacc44d4bb862015c9b4499873d55c3d0 Mon Sep 17 00:00:00 2001 From: Stefan Date: Fri, 15 Jul 2022 09:30:16 +0200 Subject: [PATCH] chore(CPP): Create new wallet accounts - POC UI The UI is for demo purposes. Also architecture decisions are open for change Closes: #6321 --- CMakeLists.txt | 1 + app/CMakeLists.txt | 3 +- .../Status/Application/MainView/MainView.qml | 12 +- .../MainView/StatusApplicationSections.qml | 40 +-- .../Wallet/WalletNavBarSection.qml | 20 -- .../Application/Navigation/CMakeLists.txt | 4 +- .../Navigation/NavigationBarButton.qml | 37 +++ .../Navigation/SimpleNavBarSection.qml | 29 +++ .../Navigation/StatusNavigationBar.qml | 29 --- .../Navigation/StatusNavigationButton.qml | 6 - .../Status/Application/StatusContentView.qml | 3 +- app/src/Application/ApplicationController.cpp | 17 ++ app/src/Application/ApplicationController.h | 17 +- libs/Helpers/CMakeLists.txt | 1 + libs/Helpers/src/Helpers/QObjectVectorModel.h | 98 ++++++++ .../qml/Status/Onboarding/OnboardingView.qml | 7 +- .../qml/Status/Onboarding/WelcomeView.qml | 13 +- .../Onboarding/Accounts/AccountsService.cpp | 30 ++- .../src/Onboarding/Common/Constants.h | 12 +- libs/Onboarding/src/Onboarding/TODO.md | 9 - .../Onboarding/src/Onboarding/UserAccount.cpp | 1 - .../src/Onboarding/UserAccountsModel.h | 5 +- .../ScopedTestAccount.cpp | 11 +- .../OnboardingTestHelpers/ScopedTestAccount.h | 10 +- .../Onboarding/tests/qml_tests/CMakeLists.txt | 4 +- .../tests/test_OnboardingModule.cpp | 56 ++++- .../src/StatusGo/Accounts/AccountsAPI.cpp | 1 + .../src/StatusGo/Accounts/AccountsAPI.h | 8 +- .../StatusGo/Accounts/ChatOrWalletAccount.h | 20 +- .../src/StatusGo/Wallet/DerivedAddress.h | 2 +- .../qml/Status/Controls/CMakeLists.txt | 1 + .../Navigation/ApplicationContentView.qml | 4 +- .../Navigation/ApplicationSection.qml | 12 +- .../Status/Controls/Navigation/CMakeLists.txt | 3 +- .../Controls/Navigation/MacTrafficLights.qml | 1 + .../Controls/Navigation/NavigationBar.qml | 54 +++- .../Navigation/NavigationBarButton.qml | 8 - .../Navigation/NavigationBarSection.qml | 8 +- .../Navigation/PanelAndContentBase.qml | 5 + libs/StatusQ/tests/CMakeLists.txt | 23 +- libs/StatusQ/tests/main.cpp | 27 +- libs/StatusQ/tests/tst_Controls.qml | 69 +++++ libs/Wallet/CMakeLists.txt | 76 ++++++ .../Status/Wallet/DerivedWalletAddress.h | 33 +++ .../Wallet/NewWalletAccountController.h | 95 +++++++ .../include/Status/Wallet/WalletAccount.h | 37 +++ .../include/Status/Wallet/WalletController.h | 58 +++++ libs/Wallet/qml/Status/Wallet/AssetView.qml | 14 ++ libs/Wallet/qml/Status/Wallet/AssetsPanel.qml | 167 ++++++++++++ .../NewAccount/NewWalletAccountView.qml | 238 ++++++++++++++++++ .../qml/Status/Wallet/WalletContentView.qml | 70 ++++++ libs/Wallet/qml/Status/Wallet/WalletView.qml | 42 ++++ libs/Wallet/src/DerivedWalletAddress.cpp | 21 ++ .../Wallet/src/NewWalletAccountController.cpp | 187 ++++++++++++++ libs/Wallet/src/WalletAccount.cpp | 26 ++ libs/Wallet/src/WalletController.cpp | 74 ++++++ test/libs/StatusGoQt/test_accounts.cpp | 119 +++++---- test/libs/StatusGoQt/test_onboarding.cpp | 1 - test/libs/StatusGoQt/test_wallet.cpp | 145 +++++++++-- 59 files changed, 1841 insertions(+), 283 deletions(-) delete mode 100644 app/qml/Status/Application/MainView/StatusApplicationSections/Wallet/WalletNavBarSection.qml create mode 100644 app/qml/Status/Application/Navigation/NavigationBarButton.qml create mode 100644 app/qml/Status/Application/Navigation/SimpleNavBarSection.qml delete mode 100644 app/qml/Status/Application/Navigation/StatusNavigationBar.qml delete mode 100644 app/qml/Status/Application/Navigation/StatusNavigationButton.qml create mode 100644 libs/Helpers/src/Helpers/QObjectVectorModel.h delete mode 100644 libs/Onboarding/src/Onboarding/TODO.md delete mode 100644 libs/StatusQ/qml/Status/Controls/Navigation/NavigationBarButton.qml create mode 100644 libs/StatusQ/qml/Status/Controls/Navigation/PanelAndContentBase.qml create mode 100644 libs/StatusQ/tests/tst_Controls.qml create mode 100644 libs/Wallet/CMakeLists.txt create mode 100644 libs/Wallet/include/Status/Wallet/DerivedWalletAddress.h create mode 100644 libs/Wallet/include/Status/Wallet/NewWalletAccountController.h create mode 100644 libs/Wallet/include/Status/Wallet/WalletAccount.h create mode 100644 libs/Wallet/include/Status/Wallet/WalletController.h create mode 100644 libs/Wallet/qml/Status/Wallet/AssetView.qml create mode 100644 libs/Wallet/qml/Status/Wallet/AssetsPanel.qml create mode 100644 libs/Wallet/qml/Status/Wallet/NewAccount/NewWalletAccountView.qml create mode 100644 libs/Wallet/qml/Status/Wallet/WalletContentView.qml create mode 100644 libs/Wallet/qml/Status/Wallet/WalletView.qml create mode 100644 libs/Wallet/src/DerivedWalletAddress.cpp create mode 100644 libs/Wallet/src/NewWalletAccountController.cpp create mode 100644 libs/Wallet/src/WalletAccount.cpp create mode 100644 libs/Wallet/src/WalletController.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5bcbaf5892..bb76465370 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,7 @@ add_subdirectory(libs/ApplicationCore) add_subdirectory(libs/Assets) add_subdirectory(libs/Helpers) add_subdirectory(libs/Onboarding) +add_subdirectory(libs/Wallet) add_subdirectory(libs/StatusGoQt) add_subdirectory(libs/StatusQ) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 49c026cb5f..05fbd1d7d7 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -20,8 +20,6 @@ qt6_add_qml_module(${PROJECT_NAME} qml/Status/Application/MainView/MainView.qml qml/Status/Application/MainView/StatusApplicationSections.qml - qml/Status/Application/MainView/StatusApplicationSections/Wallet/WalletNavBarSection.qml - qml/Status/Application/Settings/ApplicationSettings.qml qml/Status/Application/System/StatusTrayIcon.qml @@ -70,6 +68,7 @@ target_link_libraries(${PROJECT_NAME} Status::ApplicationCore Status::Helpers Status::Onboarding + Status::Wallet Status::Assets Status::StatusQ Status::StatusGoQt diff --git a/app/qml/Status/Application/MainView/MainView.qml b/app/qml/Status/Application/MainView/MainView.qml index 566ad412ab..d663e1e6cb 100644 --- a/app/qml/Status/Application/MainView/MainView.qml +++ b/app/qml/Status/Application/MainView/MainView.qml @@ -7,7 +7,7 @@ import Status.Application import Status.Containers import Status.Controls -import Status.Application.Navigation +import Status.Controls.Navigation /// Responsible for setup of user workflows after onboarding Item { @@ -28,12 +28,12 @@ Item { anchors.fill: parent - StatusNavigationBar { + NavigationBar { id: navBar Layout.fillHeight: true - sections: appSections.sectionsList + sections: appSections.sections } ColumnLayout { @@ -46,17 +46,15 @@ Item { visible: false // TODO: appController.bannerController.visible } Loader { - id: mainLoader - Layout.fillWidth: true Layout.fillHeight: true + + sourceComponent: navBar.currentSection } } } StatusApplicationSections { id: appSections - // Chat ... - // Wallet ... } } diff --git a/app/qml/Status/Application/MainView/StatusApplicationSections.qml b/app/qml/Status/Application/MainView/StatusApplicationSections.qml index 1f59c38fa2..8fc06d92a1 100644 --- a/app/qml/Status/Application/MainView/StatusApplicationSections.qml +++ b/app/qml/Status/Application/MainView/StatusApplicationSections.qml @@ -1,25 +1,37 @@ import QtQml +import QtQuick +import QtQuick.Controls +import Status.Application.Navigation import Status.Controls.Navigation +import Status.Wallet -QtObject { - readonly property var sectionsList: [wallet, settings] - readonly property ApplicationSection wallet: ApplicationSection { - navButton: WalletButtonComponent - content: WalletContentComponent +Item { + property var sections: [walletSection, settingsSection] - component WalletButtonComponent: NavigationBarButton { - } - component WalletContentComponent: ApplicationContentView { - } + ButtonGroup { + id: oneSectionSelectedGroup } - readonly property ApplicationSection settings: ApplicationSection { - navButton: SettingsButtonComponent - content: SettingsContentComponent - component SettingsButtonComponent: NavigationBarButton { + ApplicationSection { + id: walletSection + navigationSection: SimpleNavBarSection { + name: "Wallet" + mutuallyExclusiveGroup: oneSectionSelectedGroup } - component SettingsContentComponent: ApplicationContentView { + content: WalletView {} + } + ApplicationSection { + id: settingsSection + navigationSection: SimpleNavBarSection { + name: "Settings" + mutuallyExclusiveGroup: oneSectionSelectedGroup + } + content: ApplicationContentView { + Label { + anchors.centerIn: parent + text: "TODO Settings" + } } } } diff --git a/app/qml/Status/Application/MainView/StatusApplicationSections/Wallet/WalletNavBarSection.qml b/app/qml/Status/Application/MainView/StatusApplicationSections/Wallet/WalletNavBarSection.qml deleted file mode 100644 index 19788aa828..0000000000 --- a/app/qml/Status/Application/MainView/StatusApplicationSections/Wallet/WalletNavBarSection.qml +++ /dev/null @@ -1,20 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls - -import Status.Application.Navigation -import Status.Controls.Navigation - -NavigationBarSection { - id: root - - implicitHeight: walletButton.implicitHeight - - StatusNavigationButton { - id: walletButton - - anchors.fill: parent - - // TODO: icon, tooltip ... - } -} diff --git a/app/qml/Status/Application/Navigation/CMakeLists.txt b/app/qml/Status/Application/Navigation/CMakeLists.txt index 37f49e9602..e885b77e2c 100644 --- a/app/qml/Status/Application/Navigation/CMakeLists.txt +++ b/app/qml/Status/Application/Navigation/CMakeLists.txt @@ -15,8 +15,8 @@ qt6_add_qml_module(${PROJECT_NAME} VERSION 1.0 QML_FILES - StatusNavigationBar.qml - StatusNavigationButton.qml + NavigationBarButton.qml + SimpleNavBarSection.qml # Required to suppress "qmllint may not work" warning OUTPUT_DIRECTORY diff --git a/app/qml/Status/Application/Navigation/NavigationBarButton.qml b/app/qml/Status/Application/Navigation/NavigationBarButton.qml new file mode 100644 index 0000000000..32c5fa3d46 --- /dev/null +++ b/app/qml/Status/Application/Navigation/NavigationBarButton.qml @@ -0,0 +1,37 @@ +import QtQuick +import QtQuick.Controls + +/// The control must be squared. User must set the \c width only, height will follow. +Item { + required property string name + property alias selected: iconButton.checked + property ButtonGroup mutuallyExclusiveGroup: null + + implicitWidth: iconButton.implicitWidth + implicitHeight: iconButton.implicitWidth + height: width + + Button { + id: iconButton + + anchors.fill: parent + + text: name.length ? name.charAt(0) : "" + + flat: true + + checkable: true + hoverEnabled: true + + autoExclusive: true + ButtonGroup.group: mutuallyExclusiveGroup + + background: Rectangle { + radius: width/2 + border.width: 1 + + color: "#4360DF" + opacity: iconButton.checked ? 0.1 : iconButton.hovered ? 0.05 : 0 + } + } +} diff --git a/app/qml/Status/Application/Navigation/SimpleNavBarSection.qml b/app/qml/Status/Application/Navigation/SimpleNavBarSection.qml new file mode 100644 index 0000000000..687d4df334 --- /dev/null +++ b/app/qml/Status/Application/Navigation/SimpleNavBarSection.qml @@ -0,0 +1,29 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +import Status.Application.Navigation +import Status.Controls.Navigation + +/// Only one button, squared +NavigationBarSection { + id: root + + property alias name: button.name + property alias mutuallyExclusiveGroup: button.mutuallyExclusiveGroup + + // Size of the current button + implicitHeight: implicitWidth + + NavigationBarButton { + id: button + + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: root.sideMargin + anchors.rightMargin: root.sideMargin + + selected: root.selected + onSelectedChanged: root.selected = selected + } +} diff --git a/app/qml/Status/Application/Navigation/StatusNavigationBar.qml b/app/qml/Status/Application/Navigation/StatusNavigationBar.qml deleted file mode 100644 index b734613961..0000000000 --- a/app/qml/Status/Application/Navigation/StatusNavigationBar.qml +++ /dev/null @@ -1,29 +0,0 @@ -import QtQml -import QtQuick -import QtQuick.Layouts - -import Status.Controls.Navigation - -NavigationBar { - implicitHeight: mainLayout.implicitHeight - - required property var sections - - ColumnLayout { - id: mainLayout - - MacTrafficLights { - Layout.margins: 13 - } - - Repeater { - model: sections - - Loader { - Layout.fillWidth: true - - sourceComponent: modelData.navButton - } - } - } -} diff --git a/app/qml/Status/Application/Navigation/StatusNavigationButton.qml b/app/qml/Status/Application/Navigation/StatusNavigationButton.qml deleted file mode 100644 index 9a8aef20bf..0000000000 --- a/app/qml/Status/Application/Navigation/StatusNavigationButton.qml +++ /dev/null @@ -1,6 +0,0 @@ -import QtQml - -import Status.Controls.Navigation - -NavigationBarButton { -} diff --git a/app/qml/Status/Application/StatusContentView.qml b/app/qml/Status/Application/StatusContentView.qml index f6ae1fd653..4c943d1b50 100644 --- a/app/qml/Status/Application/StatusContentView.qml +++ b/app/qml/Status/Application/StatusContentView.qml @@ -29,8 +29,9 @@ Item { id: onboardingViewComponent OnboardingView { - onUserLoggedIn: { + onUserLoggedIn: function (statusAccount) { splashScreenPopup.open() + //appController.statusAccount = statusAccount contentLoader.sourceComponent = mainViewComponent } } diff --git a/app/src/Application/ApplicationController.cpp b/app/src/Application/ApplicationController.cpp index 4cf3bd047d..28cae09ace 100644 --- a/app/src/Application/ApplicationController.cpp +++ b/app/src/Application/ApplicationController.cpp @@ -1,7 +1,24 @@ #include "ApplicationController.h" +namespace Status::Application { + ApplicationController::ApplicationController(QObject *parent) : QObject{parent} { } + +QObject *ApplicationController::statusAccount() const +{ + return m_statusAccount; +} + +void ApplicationController::setStatusAccount(QObject *newStatusAccount) +{ + if (m_statusAccount == newStatusAccount) + return; + m_statusAccount = newStatusAccount; + emit statusAccountChanged(); +} + +} diff --git a/app/src/Application/ApplicationController.h b/app/src/Application/ApplicationController.h index 91c65cdef5..0fb1780658 100644 --- a/app/src/Application/ApplicationController.h +++ b/app/src/Application/ApplicationController.h @@ -4,6 +4,11 @@ #include #include +// TODO: investigate. This line breaks qobject_cast in OnboardingController::login +//#include + +namespace Status::Application { + /** * @brief Responsible for providing general information and utility components */ @@ -11,9 +16,19 @@ class ApplicationController : public QObject { Q_OBJECT QML_ELEMENT + + Q_PROPERTY(QObject* statusAccount READ statusAccount WRITE setStatusAccount NOTIFY statusAccountChanged) public: explicit ApplicationController(QObject *parent = nullptr); -signals: + QObject *statusAccount() const; + void setStatusAccount(QObject *newStatusAccount); +signals: + void statusAccountChanged(); + +private: + QObject* m_statusAccount{}; }; + +} diff --git a/libs/Helpers/CMakeLists.txt b/libs/Helpers/CMakeLists.txt index b62a364ab4..74243fd4fb 100644 --- a/libs/Helpers/CMakeLists.txt +++ b/libs/Helpers/CMakeLists.txt @@ -72,5 +72,6 @@ target_sources(Helpers ${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/logs.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/logs.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/NamedType.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/QObjectVectorModel.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/Singleton.h ) diff --git a/libs/Helpers/src/Helpers/QObjectVectorModel.h b/libs/Helpers/src/Helpers/QObjectVectorModel.h new file mode 100644 index 0000000000..4803a17eb4 --- /dev/null +++ b/libs/Helpers/src/Helpers/QObjectVectorModel.h @@ -0,0 +1,98 @@ +#pragma once +#include +#include + +namespace Status::Helpers { + +/// Generic typed QObject provider model +/// +/// Supports: source model update +/// \todo rename it to SharedQObjectVectorModel +/// \todo consider "separating class template interface and implementation: move impl to .hpp file and include it at the end of .h file. That's not affect compilation time, but it better to read" propsed by @MishkaRogachev +template +class QObjectVectorModel final : public QAbstractListModel +{ + static_assert(std::is_base_of::value, "Template parameter (T) not a QObject"); + +public: + + using ObjectContainer = std::vector>; + + explicit QObjectVectorModel(ObjectContainer initialObjects, const char* objectRoleName, QObject* parent = nullptr) + : QAbstractListModel(parent) + , m_objects(std::move(initialObjects)) + , m_roleName(objectRoleName) + { + } + explicit QObjectVectorModel(const char* objectRoleName, QObject* parent = nullptr) + : QObjectVectorModel(ObjectContainer{}, objectRoleName, parent) + {} + ~QObjectVectorModel() {}; + + QHash roleNames() const override { + return {{ObjectRole, m_roleName}}; + }; + + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override { + Q_UNUSED(parent) + return m_objects.size(); + } + + virtual QVariant data(const QModelIndex& index, int role) const override { + if(!QAbstractItemModel::checkIndex(index) || role != ObjectRole) + return QVariant(); + + return QVariant::fromValue(m_objects[index.row()].get()); + } + + const T* at(size_t pos) const { + return m_objects.at(pos).get(); + }; + + std::shared_ptr get(size_t pos) { + return m_objects.at(pos); + }; + + size_t size() const { + return m_objects.size(); + }; + + void clear() { + m_objects.clear(); + }; + + void push_back(const std::shared_ptr newValue) { + beginInsertRows(QModelIndex(), m_objects.size(), m_objects.size()); + m_objects.push_back(newValue); + endInsertRows(); + }; + + void resize(size_t count) { + if(count > m_objects.size()) { + beginInsertRows(QModelIndex(), m_objects.size(), count - 1); + m_objects.resize(count); + endInsertRows(); + } + else if(count < m_objects.size()) { + beginRemoveRows(QModelIndex(), count, m_objects.size() - 1); + m_objects.resize(count); + endRemoveRows(); + } + }; + + void set(size_t row, const std::shared_ptr newVal) { + m_objects.at(row) = newVal; + emit dataChanged(index(row), index(row), {}); + }; + + const ObjectContainer &objects() const { return m_objects; }; + +private: + ObjectContainer m_objects; + + const QByteArray m_roleName; + + constexpr static auto ObjectRole = Qt::UserRole + 1; +}; + +} diff --git a/libs/Onboarding/qml/Status/Onboarding/OnboardingView.qml b/libs/Onboarding/qml/Status/Onboarding/OnboardingView.qml index f67220594f..ea8e5e1a9b 100644 --- a/libs/Onboarding/qml/Status/Onboarding/OnboardingView.qml +++ b/libs/Onboarding/qml/Status/Onboarding/OnboardingView.qml @@ -17,7 +17,8 @@ import Status.ApplicationCore Item { id: root - signal userLoggedIn() + /// \param statusAccount \c UserAccount + signal userLoggedIn(var statusAccount) implicitWidth: 1232 implicitHeight: 770 @@ -47,7 +48,9 @@ Item { initialItem: WelcomeView { onboardingController: onboardingModule.controller onSetupNewAccount: stackView.push(setupNewProfileViewComponent) - onAccountLoggedIn: root.userLoggedIn() + onAccountLoggedIn: function (statusAccount) { + root.userLoggedIn(statusAccount) + } } } diff --git a/libs/Onboarding/qml/Status/Onboarding/WelcomeView.qml b/libs/Onboarding/qml/Status/Onboarding/WelcomeView.qml index e25db72d2f..babcee1823 100644 --- a/libs/Onboarding/qml/Status/Onboarding/WelcomeView.qml +++ b/libs/Onboarding/qml/Status/Onboarding/WelcomeView.qml @@ -2,6 +2,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Status.Onboarding + import Status.Containers import "base" @@ -12,7 +14,8 @@ OnboardingPageBase { required property var onboardingController // OnboardingController signal setupNewAccount() - signal accountLoggedIn() + /// \param statusAccount \c UserAccount + signal accountLoggedIn(var statusAccount) backAvailable: false @@ -51,10 +54,16 @@ OnboardingPageBase { // TODO: remove dev helper text: "1234567890" + Timer { + interval: 100 + running: loginButton.enabled && accountsComboBox.count + onTriggered: loginButton.clicked() + } // END dev } Button { + id: loginButton text: qsTr("Login") enabled: passwordInput.text.length >= 10 onClicked: { @@ -80,7 +89,7 @@ OnboardingPageBase { target: onboardingController function onAccountLoggedIn() { - root.accountLoggedIn() + root.accountLoggedIn(accountsComboBox.currentValue) } function onAccountLoginError(error) { console.warn(`Error logging in "${error}"`) diff --git a/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp b/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp index 726b598234..32321bcf3f 100644 --- a/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp +++ b/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp @@ -81,8 +81,8 @@ const std::vector& AccountsService::generatedAccounts() c bool AccountsService::setupAccountAndLogin(const QString &accountId, const QString &password, const QString &displayName) { - QString installationId(QUuid::createUuid().toString(QUuid::WithoutBraces)); - QJsonObject accountData(getAccountDataForAccountId(accountId, displayName)); + const QString installationId(QUuid::createUuid().toString(QUuid::WithoutBraces)); + const QJsonObject accountData(getAccountDataForAccountId(accountId, displayName)); if(!setKeyStoreDir(accountData.value("key-uid").toString())) return false; @@ -123,7 +123,7 @@ bool AccountsService::isFirstTimeAccountLogin() const bool AccountsService::setKeyStoreDir(const QString &key) { m_keyStoreDir = m_statusgoDataDir / m_keyStoreDirName / key.toStdString(); - auto response = StatusGo::General::initKeystore(m_keyStoreDir.c_str()); + const auto response = StatusGo::General::initKeystore(m_keyStoreDir.c_str()); return !response.containsError(); } @@ -137,12 +137,16 @@ QString AccountsService::login(MultiAccount account, const QString& password) if(StatusGo::Accounts::openAccounts(m_statusgoDataDir.c_str()).containsError()) return QString("Failed to open accounts before logging in"); - auto hashedPassword(Utils::hashPassword(password)); + const auto hashedPassword(Utils::hashPassword(password)); - QString thumbnailImage; - QString largeImage; - auto response = StatusGo::Accounts::login(account.name, account.keyUid, hashedPassword, - thumbnailImage, largeImage); + const QString installationId(QUuid::createUuid().toString(QUuid::WithoutBraces)); + const QJsonObject nodeConfig(getDefaultNodeConfig(installationId)); + + const QString thumbnailImage; + const QString largeImage; + // TODO DEV + const auto response = StatusGo::Accounts::login(account.name, account.keyUid, hashedPassword, + thumbnailImage, largeImage/*, nodeConfig*/); if(response.containsError()) { qWarning() << response.error.message; @@ -164,7 +168,7 @@ void AccountsService::clear() QString AccountsService::generateAlias(const QString& publicKey) { - auto response = StatusGo::Accounts::generateAlias(publicKey); + const auto response = StatusGo::Accounts::generateAlias(publicKey); if(response.containsError()) { qWarning() << response.error.message; @@ -182,7 +186,7 @@ void AccountsService::deleteMultiAccount(const MultiAccount &account) DerivedAccounts AccountsService::storeDerivedAccounts(const QString& accountId, const StatusGo::HashedPassword& password, const std::vector &paths) { - auto response = StatusGo::Accounts::storeDerivedAccounts(accountId, password, paths); + const auto response = StatusGo::Accounts::storeDerivedAccounts(accountId, password, paths); if(response.containsError()) { qWarning() << response.error.message; @@ -193,7 +197,7 @@ DerivedAccounts AccountsService::storeDerivedAccounts(const QString& accountId, StoredMultiAccount AccountsService::storeAccount(const QString& accountId, const StatusGo::HashedPassword& password) { - auto response = StatusGo::Accounts::storeAccount(accountId, password); + const auto response = StatusGo::Accounts::storeAccount(accountId, password); if(response.containsError()) { qWarning() << response.error.message; @@ -308,7 +312,7 @@ QJsonObject AccountsService::prepareAccountSettingsJsonObject(const GeneratedMul { try { auto templateDefaultNetworksJson = getDataFromFile(":/Status/StaticConfig/default-networks.json").value(); - auto infuraKey = getDataFromFile(":/Status/StaticConfig/infura_key").value(); + const auto infuraKey = getDataFromFile(":/Status/StaticConfig/infura_key").value(); QString defaultNetworksContent = templateDefaultNetworksJson.replace("%INFURA_KEY%", infuraKey); QJsonArray defaultNetworksJson = QJsonDocument::fromJson(defaultNetworksContent.toUtf8()).array(); @@ -370,7 +374,7 @@ QJsonObject AccountsService::getAccountSettings(const QString& accountId, const QJsonArray getNodes(const QJsonObject& fleet, const QString& nodeType) { - auto nodes = fleet[nodeType].toObject(); + const auto nodes = fleet[nodeType].toObject(); QJsonArray result; for(auto it = nodes.begin(); it != nodes.end(); ++it) result << *it; diff --git a/libs/Onboarding/src/Onboarding/Common/Constants.h b/libs/Onboarding/src/Onboarding/Common/Constants.h index 6597150bda..46f657bf93 100644 --- a/libs/Onboarding/src/Onboarding/Common/Constants.h +++ b/libs/Onboarding/src/Onboarding/Common/Constants.h @@ -5,7 +5,7 @@ #include #include -namespace Accounts = Status::StatusGo::Accounts; +namespace GoAccounts = Status::StatusGo::Accounts; namespace Status::Constants { @@ -38,15 +38,15 @@ namespace General inline const auto ZeroAddress = u"0x0000000000000000000000000000000000000000"_qs; - inline const Accounts::DerivationPath PathWalletRoot{u"m/44'/60'/0'/0"_qs}; + inline const GoAccounts::DerivationPath PathWalletRoot{u"m/44'/60'/0'/0"_qs}; // EIP1581 Root Key, the extended key from which any whisper key/encryption key can be derived - inline const Accounts::DerivationPath PathEIP1581{u"m/43'/60'/1581'"_qs}; + inline const GoAccounts::DerivationPath PathEIP1581{u"m/43'/60'/1581'"_qs}; // BIP44-0 Wallet key, the default wallet key - inline const Accounts::DerivationPath PathDefaultWallet{PathWalletRoot.get() + u"/0"_qs}; + inline const GoAccounts::DerivationPath PathDefaultWallet{PathWalletRoot.get() + u"/0"_qs}; // EIP1581 Chat Key 0, the default whisper key - inline const Accounts::DerivationPath PathWhisper{PathEIP1581.get() + u"/0'/0"_qs}; + inline const GoAccounts::DerivationPath PathWhisper{PathEIP1581.get() + u"/0'/0"_qs}; - inline const std::vector AccountDefaultPaths {PathWalletRoot, PathEIP1581, PathWhisper, PathDefaultWallet}; + inline const std::vector AccountDefaultPaths {PathWalletRoot, PathEIP1581, PathWhisper, PathDefaultWallet}; } } diff --git a/libs/Onboarding/src/Onboarding/TODO.md b/libs/Onboarding/src/Onboarding/TODO.md deleted file mode 100644 index b7d823e68b..0000000000 --- a/libs/Onboarding/src/Onboarding/TODO.md +++ /dev/null @@ -1,9 +0,0 @@ -# Onboarding refactoring - -TODO - -- [ ] Consider moving path requirements, into `StatusGoQt` or unify them as module requirement through abstraction -- [ ] Refactor to use typed IDs across Account and Login services instead of plain strings. - - A quick workaround would be to add a generic NamedType and convert strings at status-go APIs boundaries -- [ ] Bring uniformity to namespace: `Status::`. Don't go too deep, not deeper than two domain-related namespaces -- [ ] Consider RAII for controllers, remove `init` diff --git a/libs/Onboarding/src/Onboarding/UserAccount.cpp b/libs/Onboarding/src/Onboarding/UserAccount.cpp index 7bc6d9f62d..51cce627d1 100644 --- a/libs/Onboarding/src/Onboarding/UserAccount.cpp +++ b/libs/Onboarding/src/Onboarding/UserAccount.cpp @@ -9,7 +9,6 @@ UserAccount::UserAccount(std::unique_ptr data) : QObject() , m_data(std::move(data)) { - } const QString &UserAccount::name() const diff --git a/libs/Onboarding/src/Onboarding/UserAccountsModel.h b/libs/Onboarding/src/Onboarding/UserAccountsModel.h index b1f21593b3..c105831c40 100644 --- a/libs/Onboarding/src/Onboarding/UserAccountsModel.h +++ b/libs/Onboarding/src/Onboarding/UserAccountsModel.h @@ -3,13 +3,10 @@ #include "UserAccount.h" #include -#include namespace Status::Onboarding { -/*! - * \brief Available UserAccount elements - */ +/// \todo Replace it with \c QObjectVectorModel class UserAccountsModel : public QAbstractListModel { Q_OBJECT diff --git a/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.cpp b/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.cpp index b8a1a1024f..dded536f4b 100644 --- a/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.cpp +++ b/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.cpp @@ -22,7 +22,9 @@ namespace fs = std::filesystem; namespace Status::Testing { -ScopedTestAccount::ScopedTestAccount(const std::string &tempTestSubfolderName, const QString &accountName, const QString &accountPassword, bool ignorePreviousState) +ScopedTestAccount::ScopedTestAccount(const std::string &tempTestSubfolderName, + const QString &accountName, + const QString &accountPassword) : m_fusedTestFolder{std::make_unique(tempTestSubfolderName)} , m_accountName(accountName) , m_accountPassword(accountPassword) @@ -48,7 +50,7 @@ ScopedTestAccount::ScopedTestAccount(const std::string &tempTestSubfolderName, c // Beware, smartpointer is a requirement m_onboarding = std::make_shared(accountsService); - if(m_onboarding->getOpenedAccounts().size() != 0 && !ignorePreviousState) + if(m_onboarding->getOpenedAccounts().size() != 0) throw std::runtime_error("ScopedTestAccount - already have opened account"); int accountLoggedInCount = 0; @@ -132,6 +134,11 @@ Accounts::ChatOrWalletAccount ScopedTestAccount::firstWalletAccount() return *walletIt; } +const Onboarding::MultiAccount &ScopedTestAccount::loggedInAccount() const +{ + return m_onboarding->accountsService()->getLoggedInAccount(); +} + 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 5ddeed7e32..620e427870 100644 --- a/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.h +++ b/libs/Onboarding/tests/OnboardingTestHelpers/ScopedTestAccount.h @@ -2,6 +2,8 @@ #include +#include + #include #include @@ -11,10 +13,12 @@ class QCoreApplication; namespace Status::Onboarding { class OnboardingController; + class MultiAccount; } namespace Wallet = Status::StatusGo::Wallet; namespace Accounts = Status::StatusGo::Accounts; +namespace GoUtils = Status::StatusGo::Utils; namespace Status::Testing { @@ -29,8 +33,7 @@ public: */ explicit ScopedTestAccount(const std::string &tempTestSubfolderName, const QString &accountName = defaultAccountName, - const QString &accountPassword = defaultAccountPassword, - bool ignorePreviousState = false /*workaround to status-go persisting state*/); + const QString &accountPassword = defaultAccountPassword); ~ScopedTestAccount(); void processMessages(size_t millis, std::function shouldWaitUntilTimeout); @@ -38,8 +41,11 @@ public: static Accounts::ChatOrWalletAccount firstChatAccount(); static Accounts::ChatOrWalletAccount firstWalletAccount(); + /// Root account + const Status::Onboarding::MultiAccount &loggedInAccount() const; QString password() const { return m_accountPassword; }; + StatusGo::HashedPassword hashedPassword() const { return GoUtils::hashPassword(m_accountPassword); }; Status::Onboarding::OnboardingController* onboardingController() const; diff --git a/libs/Onboarding/tests/qml_tests/CMakeLists.txt b/libs/Onboarding/tests/qml_tests/CMakeLists.txt index 17a6bba634..4d5a6bd560 100644 --- a/libs/Onboarding/tests/qml_tests/CMakeLists.txt +++ b/libs/Onboarding/tests/qml_tests/CMakeLists.txt @@ -18,8 +18,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # no need to copy around qml test files for shadow builds - just set the respective define add_compile_definitions(QUICK_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") -add_test(NAME TestOnboardingQml WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${CMAKE_CURRENT_BINARY_DIR}/TestOnboardingQml -input "${CMAKE_CURRENT_SOURCE_DIR}") -add_custom_target("Run_TestOnboardingQml" COMMAND ${CMAKE_CTEST_COMMAND} --test-dir "${CMAKE_CURRENT_BINARY_DIR}") +add_test(NAME TestOnboardingQml WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} COMMAND ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/TestOnboardingQml -input "${CMAKE_CURRENT_SOURCE_DIR}") +add_custom_target("Run_TestOnboardingQml" COMMAND ${CMAKE_CTEST_COMMAND} --test-dir "${CMAKE_CURRENT_SOURCE_DIR}") add_dependencies("Run_TestOnboardingQml" TestOnboardingQml) target_link_libraries(TestOnboardingQml PRIVATE diff --git a/libs/Onboarding/tests/test_OnboardingModule.cpp b/libs/Onboarding/tests/test_OnboardingModule.cpp index 38890312e5..4fb03b6697 100644 --- a/libs/Onboarding/tests/test_OnboardingModule.cpp +++ b/libs/Onboarding/tests/test_OnboardingModule.cpp @@ -120,8 +120,7 @@ TEST(OnboardingModule, TestLoginEndToEnd) }); constexpr auto accountName = "TestLoginAccountName"; - constexpr auto accountPassword = "1234567890"; - ScopedTestAccount testAccount(test_info_->name(), accountName, accountPassword, true); + ScopedTestAccount testAccount(test_info_->name(), accountName); testAccount.processMessages(1000, [createAndLogin]() { return !createAndLogin; }); @@ -139,10 +138,9 @@ TEST(OnboardingModule, TestLoginEndToEnd) auto onboarding = std::make_shared(accountsService); // We don't have a way yet to simulate status-go process exit - //EXPECT_EQ(onboarding->getOpenedAccounts().count(), 0); + EXPECT_EQ(onboarding->getOpenedAccounts().size(), 1); auto accounts = accountsService->openAndListAccounts(); - //ASSERT_EQ(accounts.size(), 1); ASSERT_GT(accounts.size(), 0); int accountLoggedInCount = 0; @@ -150,13 +148,15 @@ TEST(OnboardingModule, TestLoginEndToEnd) accountLoggedInCount++; }); bool accountLoggedInError = false; - QObject::connect(onboarding.get(), &Onboarding::OnboardingController::accountLoginError, [&accountLoggedInError]() { - accountLoggedInError = true; - }); + QObject::connect(onboarding.get(), &Onboarding::OnboardingController::accountLoginError, + [&accountLoggedInError](const QString& error) { + accountLoggedInError = true; + qDebug() << "Failed logging in in test" << test_info_->name() << "with error:" << error; + } + ); - // 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); + auto errorString = accountsService->login(*ourAccountRes, testAccount.password()); ASSERT_EQ(errorString.length(), 0); testAccount.processMessages(1000, [accountLoggedInCount, accountLoggedInError]() { @@ -166,4 +166,42 @@ TEST(OnboardingModule, TestLoginEndToEnd) ASSERT_EQ(accountLoggedInError, 0); } +TEST(OnboardingModule, TestLoginEndToEnd_WrongPassword) +{ + constexpr auto testRootAccountName = "test-login_wrong_pass-name"; + ScopedTestAccount testAccount(test_info_->name(), testRootAccountName); + + testAccount.logOut(); + + auto accountsService = std::make_shared(); + auto result = accountsService->init(testAccount.fusedTestFolder()); + ASSERT_TRUE(result); + auto onboarding = std::make_shared(accountsService); + auto accounts = accountsService->openAndListAccounts(); + ASSERT_GT(accounts.size(), 0); + + int accountLoggedInCount = 0; + QObject::connect(onboarding.get(), &Onboarding::OnboardingController::accountLoggedIn, [&accountLoggedInCount]() { + accountLoggedInCount++; + }); + bool accountLoggedInError = false; + QString loginErrorMessage; + QObject::connect(onboarding.get(), &Onboarding::OnboardingController::accountLoginError, + [&loginErrorMessage, &accountLoggedInError](const QString& error) { + accountLoggedInError = true; + loginErrorMessage = error; + }); + + auto ourAccountRes = std::find_if(accounts.begin(), accounts.end(), [testRootAccountName](const auto &a) { return a.name == testRootAccountName; }); + auto errorString = accountsService->login(*ourAccountRes, testAccount.password() + "extra"); + ASSERT_EQ(errorString.length(), 0); + + testAccount.processMessages(1000, [accountLoggedInCount, accountLoggedInError]() { + return accountLoggedInCount == 0 && !accountLoggedInError; + }); + ASSERT_EQ(accountLoggedInError, 1); + ASSERT_EQ(accountLoggedInCount, 0); + ASSERT_EQ(loginErrorMessage, "file is not a database"); +} + } // namespace diff --git a/libs/StatusGoQt/src/StatusGo/Accounts/AccountsAPI.cpp b/libs/StatusGoQt/src/StatusGo/Accounts/AccountsAPI.cpp index 33964fa0f1..2774635c02 100644 --- a/libs/StatusGoQt/src/StatusGo/Accounts/AccountsAPI.cpp +++ b/libs/StatusGoQt/src/StatusGo/Accounts/AccountsAPI.cpp @@ -42,6 +42,7 @@ void generateAccountWithDerivedPath(const HashedPassword &password, const QStrin auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str()); auto resultJson = json::parse(result); checkPrivateRpcCallResultAndReportError(resultJson); + } void addAccountWithMnemonicAndPath(const QString &mnemonic, const HashedPassword &password, const QString &name, diff --git a/libs/StatusGoQt/src/StatusGo/Accounts/AccountsAPI.h b/libs/StatusGoQt/src/StatusGo/Accounts/AccountsAPI.h index 1686f04d70..08df16d640 100644 --- a/libs/StatusGoQt/src/StatusGo/Accounts/AccountsAPI.h +++ b/libs/StatusGoQt/src/StatusGo/Accounts/AccountsAPI.h @@ -20,8 +20,12 @@ namespace Status::StatusGo::Accounts /// \throws \c CallPrivateRpcError Accounts::ChatOrWalletAccounts getAccounts(); -/// \brief Generate a new account -/// \note the underlying status-go api, SaveAccounts@accounts.go, returns `nil` for \c CallPrivateRpcResponse.result +/// \brief Generate a new account for the specified derivation path +/// +/// \note if the account for the \c path exists it will fail with +/// CallPrivateRpcError.errorResponse().error.message="account already exists" +/// \note increment the last path index in consequent calls to generate multiple accounts for \c derivedFrom +/// \note the underlying status-go API, SaveAccounts@accounts.go, returns `nil` for \c CallPrivateRpcResponse.result /// \see \c getAccounts /// \throws \c CallPrivateRpcError void generateAccountWithDerivedPath(const HashedPassword &password, const QString &name, diff --git a/libs/StatusGoQt/src/StatusGo/Accounts/ChatOrWalletAccount.h b/libs/StatusGoQt/src/StatusGo/Accounts/ChatOrWalletAccount.h index 8c6fb73354..cccc4989d0 100644 --- a/libs/StatusGoQt/src/StatusGo/Accounts/ChatOrWalletAccount.h +++ b/libs/StatusGoQt/src/StatusGo/Accounts/ChatOrWalletAccount.h @@ -19,19 +19,19 @@ namespace Status::StatusGo::Accounts { /// \note equivalent of status-go's accounts.Account@multiaccounts/accounts/database.go struct ChatOrWalletAccount { - EOAddress address; - bool isChat = false; - int clock = -1; - QColor color; - std::optional derivedFrom; - QString emoji; - bool isHidden = false; - QString mixedcaseAddress; QString name; + EOAddress address; + bool isChat{false}; + bool isWallet{false}; + QColor color; + QString emoji; + std::optional derivedFrom; DerivationPath path; + int clock{-1}; + bool isHidden{false}; + bool isRemoved{false}; QString publicKey; - bool isRemoved = false; - bool isWallet = false; + QString mixedcaseAddress; }; using ChatOrWalletAccounts = std::vector; diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/DerivedAddress.h b/libs/StatusGoQt/src/StatusGo/Wallet/DerivedAddress.h index 2fb25920df..81c6b7bc75 100644 --- a/libs/StatusGoQt/src/StatusGo/Wallet/DerivedAddress.h +++ b/libs/StatusGoQt/src/StatusGo/Wallet/DerivedAddress.h @@ -23,7 +23,7 @@ namespace Status::StatusGo::Wallet { */ struct DerivedAddress { - Accounts::EOAddress address; + Accounts::EOAddress address; Accounts::DerivationPath path; bool hasActivity = false; bool alreadyCreated = false; diff --git a/libs/StatusQ/qml/Status/Controls/CMakeLists.txt b/libs/StatusQ/qml/Status/Controls/CMakeLists.txt index c55103dedb..22ffc8dbba 100644 --- a/libs/StatusQ/qml/Status/Controls/CMakeLists.txt +++ b/libs/StatusQ/qml/Status/Controls/CMakeLists.txt @@ -11,6 +11,7 @@ qt6_add_qml_module(${PROJECT_NAME} VERSION 1.0 QML_FILES + StatusBanner.qml # Required to suppress "qmllint may not work" warning diff --git a/libs/StatusQ/qml/Status/Controls/Navigation/ApplicationContentView.qml b/libs/StatusQ/qml/Status/Controls/Navigation/ApplicationContentView.qml index f829e25d86..117b28bdc7 100644 --- a/libs/StatusQ/qml/Status/Controls/Navigation/ApplicationContentView.qml +++ b/libs/StatusQ/qml/Status/Controls/Navigation/ApplicationContentView.qml @@ -1,7 +1,5 @@ import QtQuick -/*! - Template for application section content - */ +/// Template for application section content Item { } diff --git a/libs/StatusQ/qml/Status/Controls/Navigation/ApplicationSection.qml b/libs/StatusQ/qml/Status/Controls/Navigation/ApplicationSection.qml index 37c5dc9dd3..cdffc7bd2f 100644 --- a/libs/StatusQ/qml/Status/Controls/Navigation/ApplicationSection.qml +++ b/libs/StatusQ/qml/Status/Controls/Navigation/ApplicationSection.qml @@ -1,12 +1,10 @@ import QtQml -/*! - An application section with button and content view - */ +/// An application section with button and content view QtObject { - required property NavigationBarButtonComponent navButton - required property ApplicationContentView content + /// \c NavigationBarSection + required property Component navigationSection - component NavigationBarButtonComponent: NavigationBarButton {} - component ApplicationContentViewComponent: ApplicationContentView {} + /// \c ApplicationContentView + required property Component content } diff --git a/libs/StatusQ/qml/Status/Controls/Navigation/CMakeLists.txt b/libs/StatusQ/qml/Status/Controls/Navigation/CMakeLists.txt index b5efdfd882..fdb7469dd0 100644 --- a/libs/StatusQ/qml/Status/Controls/Navigation/CMakeLists.txt +++ b/libs/StatusQ/qml/Status/Controls/Navigation/CMakeLists.txt @@ -20,7 +20,8 @@ qt6_add_qml_module(${PROJECT_NAME} ApplicationState.qml MacTrafficLights.qml NavigationBar.qml - NavigationBarButton.qml + NavigationBarSection.qml + PanelAndContentBase.qml # Required to suppress "qmllint may not work" warning OUTPUT_DIRECTORY diff --git a/libs/StatusQ/qml/Status/Controls/Navigation/MacTrafficLights.qml b/libs/StatusQ/qml/Status/Controls/Navigation/MacTrafficLights.qml index f85377479b..cff3066383 100644 --- a/libs/StatusQ/qml/Status/Controls/Navigation/MacTrafficLights.qml +++ b/libs/StatusQ/qml/Status/Controls/Navigation/MacTrafficLights.qml @@ -4,6 +4,7 @@ import QtQuick.Controls import Status.Core.Theme import Status.Assets +/// MacOS window decoration for QML. To be used when the title-bar is hidden. Item { id: root diff --git a/libs/StatusQ/qml/Status/Controls/Navigation/NavigationBar.qml b/libs/StatusQ/qml/Status/Controls/Navigation/NavigationBar.qml index 4ce0c94843..099c6cb9b2 100644 --- a/libs/StatusQ/qml/Status/Controls/Navigation/NavigationBar.qml +++ b/libs/StatusQ/qml/Status/Controls/Navigation/NavigationBar.qml @@ -1,11 +1,57 @@ +import QtQml import QtQuick import QtQuick.Layouts -/*! - Template for side NavigationBar +import Status.Controls.Navigation - The width is given, the rest of the controls have to adapt to the width - */ + +/// Template for side NavigationBar +/// +/// The width is given, the rest of the controls have to adapt to the width +/// Contains a list of Item { + id: root + implicitWidth: 78 + implicitHeight: mainLayout.implicitHeight + + readonly property Component currentSection: listView.currentItem.content + + required property var sections + + ColumnLayout { + id: mainLayout + + anchors.fill: parent + + MacTrafficLights { + Layout.margins: 13 + } + + ListView { + id: listView + + Layout.fillWidth: true + Layout.fillHeight: true + + model: root.sections + + // TODO: sync with user settings + currentIndex: 0 + + onCurrentItemChanged: currentItem.item.selected = true + + // Each delegate is a section + delegate: Loader { + property var content: modelData.content + sourceComponent: modelData.navigationSection + Connections { + target: item + function onSelectedChanged() { + listView.currentIndex = index + } + } + } + } + } } diff --git a/libs/StatusQ/qml/Status/Controls/Navigation/NavigationBarButton.qml b/libs/StatusQ/qml/Status/Controls/Navigation/NavigationBarButton.qml deleted file mode 100644 index ab69f331dd..0000000000 --- a/libs/StatusQ/qml/Status/Controls/Navigation/NavigationBarButton.qml +++ /dev/null @@ -1,8 +0,0 @@ -import QtQuick - -/*! - Template for a NavigationBar square button - */ -Item { - height: width -} diff --git a/libs/StatusQ/qml/Status/Controls/Navigation/NavigationBarSection.qml b/libs/StatusQ/qml/Status/Controls/Navigation/NavigationBarSection.qml index 0827159594..5d744ecf32 100644 --- a/libs/StatusQ/qml/Status/Controls/Navigation/NavigationBarSection.qml +++ b/libs/StatusQ/qml/Status/Controls/Navigation/NavigationBarSection.qml @@ -1,7 +1,9 @@ import QtQuick -/*! - Template for a Navigation Bar section - */ +/// Template for a navigation bar section Item { + readonly property int sideMargin: 18 + property bool selected: false + + implicitWidth: 78 } diff --git a/libs/StatusQ/qml/Status/Controls/Navigation/PanelAndContentBase.qml b/libs/StatusQ/qml/Status/Controls/Navigation/PanelAndContentBase.qml new file mode 100644 index 0000000000..d121427bb4 --- /dev/null +++ b/libs/StatusQ/qml/Status/Controls/Navigation/PanelAndContentBase.qml @@ -0,0 +1,5 @@ +import QtQuick + +ApplicationContentView { + readonly property int panelWidth: 304 +} diff --git a/libs/StatusQ/tests/CMakeLists.txt b/libs/StatusQ/tests/CMakeLists.txt index 6091b1264f..8fb02f510c 100644 --- a/libs/StatusQ/tests/CMakeLists.txt +++ b/libs/StatusQ/tests/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.21) project(TestStatusQ LANGUAGES CXX) @@ -7,17 +7,9 @@ enable_testing(true) set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true) find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml QuickTest REQUIRED) -qt6_standard_project_setup() -qt6_add_qml_module(${PROJECT_NAME} - URI Status.TestHelpers - VERSION 1.0 - - QML_FILES - - # Required to suppress "qmllint may not work" warning - OUTPUT_DIRECTORY - ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Status/TestHelpers +add_executable(TestStatusQ + "main.cpp" ) set(CMAKE_CXX_STANDARD 20) @@ -26,9 +18,10 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # no need to copy around qml test files for shadow builds - just set the respective define add_definitions(-DQUICK_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") -add_test(NAME ${PROJECT_NAME} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME} -input "${CMAKE_CURRENT_SOURCE_DIR}") -add_custom_target("Run_${PROJECT_NAME}" COMMAND ${CMAKE_CTEST_COMMAND} --test-dir "${CMAKE_CURRENT_BINARY_DIR}") -add_dependencies("Run_${PROJECT_NAME}" ${PROJECT_NAME}) + +add_test(NAME TestStatusQ WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} COMMAND ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/TestStatusQ -input "${CMAKE_CURRENT_SOURCE_DIR}") +add_custom_target("Run_TestStatusQ" COMMAND ${CMAKE_CTEST_COMMAND} --test-dir "${CMAKE_CURRENT_SOURCE_DIR}") +add_dependencies("Run_TestStatusQ" TestStatusQ) target_include_directories(${PROJECT_NAME} PUBLIC @@ -37,7 +30,7 @@ target_include_directories(${PROJECT_NAME} add_subdirectory(TestHelpers) -target_link_libraries(${PROJECT_NAME} PRIVATE +target_link_libraries(TestStatusQ PRIVATE Qt6::QuickTest Qt6::Qml Qt6::Quick diff --git a/libs/StatusQ/tests/main.cpp b/libs/StatusQ/tests/main.cpp index 924f9a2603..36d927a734 100644 --- a/libs/StatusQ/tests/main.cpp +++ b/libs/StatusQ/tests/main.cpp @@ -1,26 +1,3 @@ -#include -#include +#include -#include "TestHelpers/MonitorQtOutput.h" - -class TestSetup : public QObject -{ - Q_OBJECT - -public: - TestSetup() {} - -public slots: - void qmlEngineAvailable(QQmlEngine *engine) - { - // TODO: Workaround until we make StatusQ a CMake library - engine->addImportPath("../src/"); - engine->addImportPath("./qml/"); - // TODO: Alternative to not yet supported QML_ELEMENT - qmlRegisterType("StatusQ.TestHelpers", 0, 1, "MonitorQtOutput"); - } -}; - -QUICK_TEST_MAIN_WITH_SETUP(TestControls, TestSetup) - -#include "main.moc" +QUICK_TEST_MAIN(TestOnboardingQml) diff --git a/libs/StatusQ/tests/tst_Controls.qml b/libs/StatusQ/tests/tst_Controls.qml new file mode 100644 index 0000000000..8ae5877ec5 --- /dev/null +++ b/libs/StatusQ/tests/tst_Controls.qml @@ -0,0 +1,69 @@ +import QtQuick +import QtQml +import QtTest + +import Status.Controls.Navigation + +import Status.TestHelpers + +/// \todo use mocked values +Item { + id: root + width: 400 + height: 300 + + Component { + id: macTrafficLightsComponent + + Item { + MacTrafficLights { + anchors.left: parent.left + anchors.margins: 13 + anchors.top: parent.top + z: parent.z + 1 + } + } + } + + Loader { + id: testLoader + + anchors.fill: parent + active: false + } + + TestCase { + id: qmlWarningsTest + + name: "TestQmlWarnings" + + when: windowShown + + // + // Test guards + + function init() { + qtOuput.restartCapturing() + } + + function cleanup() { + testLoader.active = false + } + + // + // Tests + + /// \todo check if data driven testing is possible for checking all the controls with its defaults + function test_macTrafficLightsInitialization() { + testLoader.sourceComponent = macTrafficLightsComponent + testLoader.active = true + verify(waitForRendering(testLoader.item)) + testLoader.active = false + verify(qtOuput.qtOuput().length === 0, `No output expected. Found:\n"${qtOuput.qtOuput()}"\n`) + } + } + + MonitorQtOutput { + id: qtOuput + } +} diff --git a/libs/Wallet/CMakeLists.txt b/libs/Wallet/CMakeLists.txt new file mode 100644 index 0000000000..a7cc8c9349 --- /dev/null +++ b/libs/Wallet/CMakeLists.txt @@ -0,0 +1,76 @@ +# Wallet Module build definition +# +cmake_minimum_required(VERSION 3.21) + +project(Wallet + VERSION 0.1.0 + LANGUAGES CXX) + +set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true) + +find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml Concurrent REQUIRED) +qt6_standard_project_setup() + +qt6_add_qml_module(Wallet + URI Status.Wallet + VERSION 1.0 + + QML_FILES + qml/Status/Wallet/NewAccount/NewWalletAccountView.qml + qml/Status/Wallet/AssetsPanel.qml + qml/Status/Wallet/AssetView.qml + qml/Status/Wallet/WalletContentView.qml + qml/Status/Wallet/WalletView.qml + + # Required to suppress "qmllint may not work" warning + OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Status/Wallet/ +) +add_library(Status::Wallet ALIAS Wallet) + +target_include_directories(Wallet + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + + # Workaround to Qt6's *_qmltyperegistrations.cpp + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include/Status/Wallet/ + + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +target_link_libraries(Wallet + PRIVATE + Qt6::Quick + Qt6::Qml + Qt6::Concurrent + + Status::ApplicationCore + Status::Onboarding + + Status::StatusGoQt + Status::StatusGoConfig +) + +# QtCreator needs this +set(QML_IMPORT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/qml;${QML_IMPORT_PATH} CACHE STRING "For QtCreator" FORCE) +list(REMOVE_DUPLICATES QML_IMPORT_PATH) + +install( + TARGETS + Wallet + RUNTIME +) + +target_sources(Wallet + PRIVATE + include/Status/Wallet/DerivedWalletAddress.h + src/DerivedWalletAddress.cpp + include/Status/Wallet/NewWalletAccountController.h + src/NewWalletAccountController.cpp + include/Status/Wallet/WalletAccount.h + # Move to Accounts module + src/WalletAccount.cpp + include/Status/Wallet/WalletController.h + src/WalletController.cpp +) diff --git a/libs/Wallet/include/Status/Wallet/DerivedWalletAddress.h b/libs/Wallet/include/Status/Wallet/DerivedWalletAddress.h new file mode 100644 index 0000000000..f7164aa0fe --- /dev/null +++ b/libs/Wallet/include/Status/Wallet/DerivedWalletAddress.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include + +namespace GoWallet = Status::StatusGo::Wallet; + +namespace Status::Wallet { + +class DerivedWalletAddress : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString address READ address CONSTANT) + Q_PROPERTY(bool alreadyCreated READ alreadyCreated CONSTANT) + +public: + explicit DerivedWalletAddress(GoWallet::DerivedAddress address, QObject *parent = nullptr); + + QString address() const; + + const GoWallet::DerivedAddress &data() const { return m_derivedAddress; }; + + bool alreadyCreated() const; + +private: + const GoWallet::DerivedAddress m_derivedAddress; +}; + +using DerivedWalletAddressPtr = std::shared_ptr; + +} diff --git a/libs/Wallet/include/Status/Wallet/NewWalletAccountController.h b/libs/Wallet/include/Status/Wallet/NewWalletAccountController.h new file mode 100644 index 0000000000..fc43d029e3 --- /dev/null +++ b/libs/Wallet/include/Status/Wallet/NewWalletAccountController.h @@ -0,0 +1,95 @@ +#pragma once + +#include "Status/Wallet/WalletAccount.h" +#include "Status/Wallet/DerivedWalletAddress.h" + +#include + +#include +#include + +class QQmlEngine; +class QJSEngine; + +namespace Status::Wallet { + +/// \note the folowing values are kept in sync \c selectedDerivedAddress, \c derivedAddressIndex and \c derivationPath +/// and \c customDerivationPath; \see connascence.io/value +class NewWalletAccountController: public QObject +{ + Q_OBJECT + QML_UNCREATABLE("C++ only") + + Q_PROPERTY(QAbstractListModel* mainAccountsModel READ mainAccountsModel CONSTANT) + + Q_PROPERTY(QAbstractItemModel* currentDerivedAddressModel READ currentDerivedAddressModel CONSTANT) + Q_PROPERTY(DerivedWalletAddress* selectedDerivedAddress READ selectedDerivedAddress WRITE setSelectedDerivedAddress NOTIFY selectedDerivedAddressChanged) + Q_PROPERTY(int derivedAddressIndex MEMBER m_derivedAddressIndex NOTIFY selectedDerivedAddressChanged) + + Q_PROPERTY(QString derivationPath READ derivationPath WRITE setDerivationPath NOTIFY derivationPathChanged) + Q_PROPERTY(bool customDerivationPath MEMBER m_customDerivationPath NOTIFY customDerivationPathChanged) + +public: + using AccountsModel = Helpers::QObjectVectorModel; + + /// \note On account creation \c accounts are updated with the newly created wallet account + NewWalletAccountController(std::shared_ptr accounts); + ~NewWalletAccountController(); + + /// Called by QML engine to register the instance. QML takes ownership of the instance + static NewWalletAccountController *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine); + + + QAbstractListModel *mainAccountsModel(); + QAbstractItemModel *currentDerivedAddressModel(); + + QString derivationPath() const; + void setDerivationPath(const QString &newDerivationPath); + + /// \see \c accountCreatedStatus for async result + Q_INVOKABLE void createAccountAsync(const QString &password, const QString &name, + const QColor &color, const QString &path, + const Status::Wallet::WalletAccount *derivedFrom); + + + /// \returns \c false if fails (due to incomplete user input) + Q_INVOKABLE bool retrieveAndUpdateDerivedAddresses(const QString &password, + const Status::Wallet::WalletAccount *derivedFrom); + Q_INVOKABLE void clearDerivedAddresses(); + + DerivedWalletAddress *selectedDerivedAddress() const; + void setSelectedDerivedAddress(DerivedWalletAddress *newSelectedDerivedAddress); + +signals: + void accountCreatedStatus(bool createdSuccessfully); + + void selectedDerivedAddressChanged(); + + void derivationPathChanged(); + + void customDerivationPathChanged(); + +private: + void updateSelectedDerivedAddress(int index, std::shared_ptr newEntry); + + std::tuple searchDerivationPath(const GoAccounts::DerivationPath &derivationPath); + + WalletAccountPtr findMissingAccount(); + + AccountsModel::ObjectContainer filterMainAccounts(const AccountsModel &accounts); + + std::shared_ptr m_accounts; + /// \todo make it a proxy filter on top of \c m_accounts + AccountsModel m_mainAccounts; + + Helpers::QObjectVectorModel m_derivedAddress; + int m_derivedAddressIndex{0}; + DerivedWalletAddressPtr m_selectedDerivedAddress; + GoAccounts::DerivationPath m_derivationPath; + bool m_customDerivationPath{}; + + static constexpr int m_derivedAddressesPageSize{15}; + static constexpr int m_maxDerivedAddresses{5 * m_derivedAddressesPageSize}; +}; + +} // namespace Status::Wallet diff --git a/libs/Wallet/include/Status/Wallet/WalletAccount.h b/libs/Wallet/include/Status/Wallet/WalletAccount.h new file mode 100644 index 0000000000..ff1418c1e7 --- /dev/null +++ b/libs/Wallet/include/Status/Wallet/WalletAccount.h @@ -0,0 +1,37 @@ +#pragma once + +#include "Accounts/ChatOrWalletAccount.h" + +#include + +namespace GoAccounts = Status::StatusGo::Accounts; + +namespace Status::Wallet { + +class WalletAccount: public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString name READ name CONSTANT) + Q_PROPERTY(QString address READ address CONSTANT) + Q_PROPERTY(QColor color READ color CONSTANT) + +public: + explicit WalletAccount(const GoAccounts::ChatOrWalletAccount rawAccount, QObject *parent = nullptr); + + const QString &name() const; + + const QString &address() const; + + QColor color() const; + + const GoAccounts::ChatOrWalletAccount &data() const { return m_data; }; + +private: + const GoAccounts::ChatOrWalletAccount m_data; +}; + +using WalletAccountPtr = std::shared_ptr; +using WalletAccounts = std::vector; + +} diff --git a/libs/Wallet/include/Status/Wallet/WalletController.h b/libs/Wallet/include/Status/Wallet/WalletController.h new file mode 100644 index 0000000000..b0285c3455 --- /dev/null +++ b/libs/Wallet/include/Status/Wallet/WalletController.h @@ -0,0 +1,58 @@ +#pragma once + +#include "Status/Wallet/WalletAccount.h" +#include "Status/Wallet/DerivedWalletAddress.h" + +#include + +#include +#include + +#include + +class QQmlEngine; +class QJSEngine; + +namespace Status::Wallet { + +class NewWalletAccountController; + +/// \todo move account creation to its own controller +class WalletController: public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(QAbstractListModel* accountsModel READ accountsModel CONSTANT) + Q_PROPERTY(WalletAccount* currentAccount READ currentAccount NOTIFY currentAccountChanged) + +public: + WalletController(); + + /// Called by QML engine to register the instance. QML takes ownership of the instance + [[nodiscard]] static WalletController *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine); + + /// To be used in the new wallet account workflow + /// \note caller (QML) takes ownership of the returned object + /// \todo consider if complex approach of keeping ownership here and enforcing a unique instance + /// or not reusing the account list and make it singleton are better options + Q_INVOKABLE [[nodiscard]] Status::Wallet::NewWalletAccountController* createNewWalletAccountController() const; + + QAbstractListModel *accountsModel() const; + + WalletAccount *currentAccount() const; + Q_INVOKABLE void setCurrentAccountIndex(int index); + +signals: + void currentAccountChanged(); + +private: + std::vector getWalletAccounts(bool rootWalletAccountsOnly = false) const; + + using AccountsModel = Helpers::QObjectVectorModel; + std::shared_ptr m_accounts; + WalletAccountPtr m_currentAccount; +}; + +} // namespace Status::Wallet diff --git a/libs/Wallet/qml/Status/Wallet/AssetView.qml b/libs/Wallet/qml/Status/Wallet/AssetView.qml new file mode 100644 index 0000000000..53d9cac085 --- /dev/null +++ b/libs/Wallet/qml/Status/Wallet/AssetView.qml @@ -0,0 +1,14 @@ +import QtQuick +import QtQuick.Controls + +Item { + id: root + + /// WalletAccount + required property var asset + + Label { + anchors.centerIn: parent + text: "$$$$$" + } +} diff --git a/libs/Wallet/qml/Status/Wallet/AssetsPanel.qml b/libs/Wallet/qml/Status/Wallet/AssetsPanel.qml new file mode 100644 index 0000000000..6bfb083507 --- /dev/null +++ b/libs/Wallet/qml/Status/Wallet/AssetsPanel.qml @@ -0,0 +1,167 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Status.Wallet + +import Status.Onboarding + +import Status.Containers + +Item { + id: root + + /// WalletController + required property WalletController controller + + ColumnLayout { + anchors.left: leftLine.right + anchors.top: parent.top + anchors.right: rightLine.left + anchors.bottom: parent.bottom + + Label { + text: qsTr("Wallet") + } + Label { + id: totalValueLabel + text: "" // TODO: Aggregate or API!? + } + Label { + text: qsTr("Total value") + } + + LayoutSpacer { + Layout.fillHeight: false + Layout.preferredHeight: 10 + } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + + model: controller.accountsModel + + onCurrentIndexChanged: controller.setCurrentAccountIndex(currentIndex) + + clip: true + + delegate: ItemDelegate { + highlighted: ListView.isCurrentItem + + width: ListView.view.width + + onClicked: ListView.view.currentIndex = index + + contentItem: ColumnLayout { + spacing: 2 + + RowLayout { + Rectangle { + Layout.preferredWidth: 15 + Layout.preferredHeight: Layout.preferredWidth + Layout.leftMargin: 5 + Layout.alignment: Qt.AlignVCenter + + radius: width/2 + color: account.color + } + Label { + Layout.leftMargin: 10 + Layout.topMargin: 5 + Layout.rightMargin: 10 + Layout.alignment: Qt.AlignVCenter + + text: account.name + + verticalAlignment: Qt.AlignVCenter + + elide: Label.ElideRight + } + } + Label { + Layout.leftMargin: 10 + Layout.rightMargin: 10 + Layout.bottomMargin: 5 + + text: "$" + color: "grey" + + verticalAlignment: Qt.AlignVCenter + + elide: Label.ElideRight + } + } + } + } + + LayoutSpacer { + Layout.fillHeight: false + Layout.preferredHeight: 20 + } + + Button { + text: "+" + + Layout.fillWidth: true + visible: !(newAccountLoader.active || errorLayout.visible) + + onClicked: newAccountLoader.active = true + } + + Loader { + id: newAccountLoader + + Layout.fillWidth: true + + visible: !errorLayout.visible && active + active: false + + sourceComponent: Component { + NewWalletAccountView { + controller: root.controller.createNewWalletAccountController() + + onCancel: newAccountLoader.active = false + onAccountCreated: newAccountLoader.active = false + + Connections { + target: controller + function onAccountCreatedStatus(createdSuccessfully) { + if(createdSuccessfully) + newAccountLoader.active = false + else + errorLayout.visible = true + } + } + } + } + } + + ColumnLayout { + id: errorLayout + + visible: false + + Label { + text: qsTr("Account creation failed!") + color: "red" + Layout.margins: 5 + } + Button { + text: qsTr("OK") + Layout.margins: 5 + onClicked: errorLayout.visible = false + } + } + } + + SideLine { id: leftLine; anchors.left: parent.left } + SideLine { id: rightLine; anchors.right: parent.right } + + component SideLine: Rectangle { + color: "black" + width: 1 + anchors.top: parent.top + anchors.bottom: parent.bottom + } +} diff --git a/libs/Wallet/qml/Status/Wallet/NewAccount/NewWalletAccountView.qml b/libs/Wallet/qml/Status/Wallet/NewAccount/NewWalletAccountView.qml new file mode 100644 index 0000000000..3a1d0ae778 --- /dev/null +++ b/libs/Wallet/qml/Status/Wallet/NewAccount/NewWalletAccountView.qml @@ -0,0 +1,238 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +import Status.Wallet +import Status.Onboarding +import Status.Containers + +Item { + id: root + + /// NewWalletAccountController + required property var controller + + signal accountCreated() + signal cancel() + + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + + QtObject { + id: d + + property bool errorRetrievingDerivationAddresses: false + + function updateDerivedAddresses() { + errorRetrievingDerivationAddresses = !root.controller.retrieveAndUpdateDerivedAddresses(passwordInput.text, derivedFromCombo.currentValue) + } + } + + Component.onCompleted: d.updateDerivedAddresses() + + ColumnLayout { + id: mainLayout + + anchors.fill: parent + + Rectangle { + color: "blue" + + Layout.fillWidth: true + Layout.preferredHeight: 2 + Layout.margins: 5 + } + + Label { + text: "Name" + + Layout.margins: 5 + } + TempTextInput { + id: nameInput + + text: "Test Account" + + Layout.fillWidth: true + Layout.margins: 5 + } + + Label { + text: "Password" + Layout.margins: 5 + } + TempTextInput { + id: passwordInput + + text: "1234567890" + + Layout.fillWidth: true + Layout.margins: 5 + + onTextChanged: d.updateDerivedAddresses() + } + + Label { + text: "Color" + Layout.margins: 5 + } + + ComboBox { + id: colorCombo + + model: ListModel { + ListElement { colorText: "Red"; colorValue: "red" } + ListElement { colorText: "Green"; colorValue: "green" } + ListElement { colorText: "Blue"; colorValue: "blue" } + ListElement { colorText: "Orange"; colorValue: "orange" } + ListElement { colorText: "Pink"; colorValue: "pink" } + ListElement { colorText: "Fuchsia"; colorValue: "fuchsia" } + } + textRole: "colorText" + valueRole: "colorValue" + + currentIndex: 0 + + Layout.fillWidth: true + Layout.margins: 5 + + delegate: ItemDelegate { + required property string colorText + required property color colorValue + required property int index + + width: colorCombo.width + contentItem: Text { + text: colorText + color: colorValue + font: colorCombo.font + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + highlighted: colorCombo.highlightedIndex === index + } + } + + Label { + text: "Derivation Path" + Layout.margins: 5 + } + TempTextInput { + id: pathInput + text: root.controller.derivationPath + onTextChanged: root.controller.derivationPath = text + + Layout.fillWidth: true + Layout.margins: 5 + } + + Label { + text: "Account" + Layout.margins: 5 + } + + ColumnLayout { + Layout.margins: 5 + + Label { + id: derivationPathsError + text: qsTr("") + visible: d.errorRetrievingDerivationAddresses + } + + ColumnLayout { + ComboBox { + id: derivedAddressCombo + + visible: !root.controller.customDerivationPath && !d.errorRetrievingDerivationAddresses + + model: root.controller.currentDerivedAddressModel + textRole: "derivedAddress.address" + valueRole: "derivedAddress" + onCurrentValueChanged: root.controller.selectedDerivedAddress = currentValue + + currentIndex: root.controller.derivedAddressIndex + + Layout.fillWidth: true + Layout.margins: 5 + + clip: true + + delegate: ItemDelegate { + width: derivedAddressCombo.width + enabled: !derivedAddress.alreadyCreated + contentItem: Text { + text: derivedAddress.address + color: derivedAddress.alreadyCreated + ? "blue" + : (derivedAddress === root.controller.selectedDerivedAddress) ? "green" : "black" + font: derivedAddressCombo.font + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + highlighted: derivedAddressCombo.highlightedIndex === index + + required property var derivedAddress + required property int index + } + } + Label { + text: qsTr("Custom Derivation Path") + visible: root.controller.customDerivationPath + } + } + } + + Label { + text: "Origin" + Layout.margins: 5 + } + + ComboBox { + id: derivedFromCombo + + model: root.controller.mainAccountsModel + textRole: "account.name" + valueRole: "account" + + currentIndex: 0 + + Layout.fillWidth: true + Layout.margins: 5 + + delegate: ItemDelegate { + width: derivedFromCombo.width + contentItem: Text { + text: account.name + color: account.color + font: derivedFromCombo.font + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + highlighted: derivedFromCombo.highlightedIndex === index + + required property var account + required property int index + } + } + + RowLayout { + Button { + text: qsTr("Create") + + enabled: nameInput.text.length > 5 && passwordInput.length > 5 + && pathInput.length > 0 + + onClicked: root.controller.createAccountAsync(passwordInput.text, nameInput.text, + colorCombo.currentValue, pathInput.text, + derivedFromCombo.currentValue); + Layout.margins: 5 + } + Button { + text: qsTr("V") + onClicked: root.cancel() + Layout.margins: 5 + } + } + } +} diff --git a/libs/Wallet/qml/Status/Wallet/WalletContentView.qml b/libs/Wallet/qml/Status/Wallet/WalletContentView.qml new file mode 100644 index 0000000000..de90b4d2ca --- /dev/null +++ b/libs/Wallet/qml/Status/Wallet/WalletContentView.qml @@ -0,0 +1,70 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQml.Models + +import Status.Wallet + +Item { + id: root + + /// WalletAccount + required property var asset + + ColumnLayout { + anchors.fill: parent + + Label { + text: asset.name + } + Label { + text: asset.address + } + TabBar { + id: tabBar + width: parent.width + + TabButton { + text: qsTr("Assets") + } + TabButton { + text: qsTr("Positions") + } + } + + SwipeView { + id: swipeView + + Layout.fillWidth: true + Layout.fillHeight: true + + currentIndex: tabBar.currentIndex + + interactive: false + clip: true + + Loader { + active: SwipeView.isCurrentItem + sourceComponent: AssetView { + Layout.fillWidth: true + Layout.fillHeight: true + + asset: root.asset + } + } + + Loader { + active: SwipeView.isCurrentItem + sourceComponent: Item { + Layout.fillWidth: true + Layout.fillHeight: true + + Label { + anchors.centerIn: parent + text: "TODO" + } + } + } + } + } +} diff --git a/libs/Wallet/qml/Status/Wallet/WalletView.qml b/libs/Wallet/qml/Status/Wallet/WalletView.qml new file mode 100644 index 0000000000..8724a68c41 --- /dev/null +++ b/libs/Wallet/qml/Status/Wallet/WalletView.qml @@ -0,0 +1,42 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +import QtQml + +import Qt.labs.platform + +import Status.Wallet + +import Status.Containers +import Status.Controls.Navigation + +/// Drives the wallet workflow +PanelAndContentBase { + id: root + + implicitWidth: 1232 + implicitHeight: 770 + + RowLayout { + id: mainLayout + + anchors.fill: parent + + AssetsPanel { + id: panel + + Layout.preferredWidth: root.panelWidth + Layout.fillHeight: true + + controller: WalletController + } + + WalletContentView { + Layout.fillWidth: true + Layout.fillHeight: true + + asset: WalletController.currentAccount + } + } +} diff --git a/libs/Wallet/src/DerivedWalletAddress.cpp b/libs/Wallet/src/DerivedWalletAddress.cpp new file mode 100644 index 0000000000..a3c2948d23 --- /dev/null +++ b/libs/Wallet/src/DerivedWalletAddress.cpp @@ -0,0 +1,21 @@ +#include "DerivedWalletAddress.h" + +namespace Status::Wallet { + +DerivedWalletAddress::DerivedWalletAddress(GoWallet::DerivedAddress address, QObject *parent) + : QObject{parent} + , m_derivedAddress{std::move(address)} +{ +} + +QString DerivedWalletAddress::address() const +{ + return m_derivedAddress.address.get(); +} + +bool DerivedWalletAddress::alreadyCreated() const +{ + return m_derivedAddress.alreadyCreated; +} + +} diff --git a/libs/Wallet/src/NewWalletAccountController.cpp b/libs/Wallet/src/NewWalletAccountController.cpp new file mode 100644 index 0000000000..24144f4b9c --- /dev/null +++ b/libs/Wallet/src/NewWalletAccountController.cpp @@ -0,0 +1,187 @@ +#include "Status/Wallet/NewWalletAccountController.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include + +namespace GoAccounts = Status::StatusGo::Accounts; +namespace GoWallet = Status::StatusGo::Wallet; +namespace UtilsSG = Status::StatusGo::Utils; +namespace StatusGo = Status::StatusGo; + +namespace Status::Wallet { + +NewWalletAccountController::NewWalletAccountController(std::shared_ptr> accounts) + : m_accounts(accounts) + , m_mainAccounts(std::move(filterMainAccounts(*accounts)), "account") + , m_derivedAddress("derivedAddress") + , m_derivationPath(Status::Constants::General::PathWalletRoot) +{ +} + +NewWalletAccountController::~NewWalletAccountController() +{ +} + +QAbstractListModel* NewWalletAccountController::mainAccountsModel() +{ + return &m_mainAccounts; +} + +QAbstractItemModel *NewWalletAccountController::currentDerivedAddressModel() +{ + return &m_derivedAddress; +} + +QString NewWalletAccountController::derivationPath() const +{ + return m_derivationPath.get(); +} + +void NewWalletAccountController::setDerivationPath(const QString &newDerivationPath) +{ + if (m_derivationPath.get() == newDerivationPath) + return; + m_derivationPath = GoAccounts::DerivationPath(newDerivationPath); + emit derivationPathChanged(); + + auto oldCustom = m_customDerivationPath; + auto found = searchDerivationPath(m_derivationPath); + m_customDerivationPath = std::get<0>(found) == nullptr; + if(!m_customDerivationPath && !std::get<0>(found).get()->alreadyCreated()) + updateSelectedDerivedAddress(std::get<1>(found), std::get<0>(found)); + + if(m_customDerivationPath != oldCustom) + emit customDerivationPathChanged(); +} + +void NewWalletAccountController::createAccountAsync(const QString &password, const QString &name, + const QColor &color, const QString &path, + const WalletAccount *derivedFrom) +{ + try { + GoAccounts::generateAccountWithDerivedPath(StatusGo::HashedPassword(UtilsSG::hashPassword(password)), + name, color, "", GoAccounts::DerivationPath(path), + derivedFrom->data().derivedFrom.value()); + auto found = findMissingAccount(); + if(found) + m_accounts->push_back(found); + else + qWarning() << "Failed to create account. No new account found by this->findMissingAccount"; + + emit accountCreatedStatus(found != nullptr); + } + catch(const StatusGo::CallPrivateRpcError& e) { + qWarning() << "StatusGoQt.generateAccountWithDerivedPath error: " << e.errorResponse().error.message.c_str(); + emit accountCreatedStatus(false); + } +} + +bool NewWalletAccountController::retrieveAndUpdateDerivedAddresses(const QString &password, + const WalletAccount *derivedFrom) +{ + assert(derivedFrom->data().derivedFrom.has_value()); + try { + int currentPage = 1; + int foundIndex = -1; + int currentIndex = 0; + auto maxPageCount = static_cast(std::ceil(static_cast(m_maxDerivedAddresses)/static_cast(m_derivedAddressesPageSize))); + std::shared_ptr foundEntry; + while(currentPage <= maxPageCount && foundIndex < 0) { + auto all = GoWallet::getDerivedAddressesForPath(StatusGo::HashedPassword(UtilsSG::hashPassword(password)), + derivedFrom->data().derivedFrom.value(), + Status::Constants::General::PathWalletRoot, + m_derivedAddressesPageSize, currentPage); + if((currentIndex + all.size()) > m_derivedAddress.size()) + m_derivedAddress.resize(currentIndex + all.size()); + + for(auto newDerived : all) { + auto newEntry = std::make_shared(std::move(newDerived)); + m_derivedAddress.set(currentIndex, newEntry); + if(foundIndex < 0 && !newEntry->data().alreadyCreated) { + foundIndex = currentIndex; + foundEntry = newEntry; + } + currentIndex++; + } + currentPage++; + } + if(foundIndex > 0) + updateSelectedDerivedAddress(foundIndex, foundEntry); + + return true; + } catch(const StatusGo::CallPrivateRpcError &e) { + return false; + } +} + +void NewWalletAccountController::clearDerivedAddresses() +{ + m_derivedAddress.clear(); +} + +WalletAccountPtr NewWalletAccountController::findMissingAccount() +{ + auto accounts = GoAccounts::getAccounts(); + // TODO: consider using a QObjectSetModel and a proxy sort model on top instead + auto it = std::find_if(accounts.begin(), accounts.end(), [this](const auto &a) { + return std::none_of(m_accounts->objects().begin(), m_accounts->objects().end(), + [&a](const auto &eA) { return a.address == eA->data().address; }); + }); + return it != accounts.end() ? std:: make_shared(*it) : nullptr; +} + +NewWalletAccountController::AccountsModel::ObjectContainer +NewWalletAccountController::filterMainAccounts(const AccountsModel &accounts) +{ + AccountsModel::ObjectContainer out; + const auto &c = accounts.objects(); + std::copy_if(c.begin(), c.end(), std::back_inserter(out), [](const auto &a){ return a->data().isWallet; }); + return out; +} + +DerivedWalletAddress *NewWalletAccountController::selectedDerivedAddress() const +{ + return m_selectedDerivedAddress.get(); +} + +void NewWalletAccountController::setSelectedDerivedAddress(DerivedWalletAddress *newSelectedDerivedAddress) +{ + if (m_selectedDerivedAddress.get() == newSelectedDerivedAddress) + return; + auto &objs = m_derivedAddress.objects(); + auto foundIt = std::find_if(objs.begin(), objs.end(), [newSelectedDerivedAddress](const auto &a) { return a.get() == newSelectedDerivedAddress; }); + updateSelectedDerivedAddress(std::distance(objs.begin(), foundIt), *foundIt); +} + +void NewWalletAccountController::updateSelectedDerivedAddress(int index, std::shared_ptr newEntry) { + m_derivedAddressIndex = index; + m_selectedDerivedAddress = newEntry; + emit selectedDerivedAddressChanged(); + if(m_derivationPath != newEntry->data().path) { + m_derivationPath = newEntry->data().path; + emit derivationPathChanged(); + } +} + +std::tuple NewWalletAccountController::searchDerivationPath(const GoAccounts::DerivationPath &derivationPath) { + const auto &c = m_derivedAddress.objects(); + auto foundIt = find_if(c.begin(), c.end(), [&derivationPath](const auto &a) { return a->data().path == derivationPath; }); + if(foundIt != c.end()) + return {*foundIt, std::distance(c.begin(), foundIt)}; + return {nullptr, -1}; +} + +} diff --git a/libs/Wallet/src/WalletAccount.cpp b/libs/Wallet/src/WalletAccount.cpp new file mode 100644 index 0000000000..1852e0a8e2 --- /dev/null +++ b/libs/Wallet/src/WalletAccount.cpp @@ -0,0 +1,26 @@ +#include "Status/Wallet/WalletAccount.h" + +namespace Status::Wallet { + +WalletAccount::WalletAccount(const GoAccounts::ChatOrWalletAccount rawAccount, QObject *parent) + : QObject(parent) + , m_data(std::move(rawAccount)) +{ +} + +const QString &WalletAccount::name() const +{ + return m_data.name; +} + +const QString &WalletAccount::address() const +{ + return m_data.address.get(); +} + +QColor WalletAccount::color() const +{ + return m_data.color; +} + +} diff --git a/libs/Wallet/src/WalletController.cpp b/libs/Wallet/src/WalletController.cpp new file mode 100644 index 0000000000..7cffc4a788 --- /dev/null +++ b/libs/Wallet/src/WalletController.cpp @@ -0,0 +1,74 @@ +#include "Status/Wallet/WalletController.h" +#include "NewWalletAccountController.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace GoAccounts = Status::StatusGo::Accounts; +namespace GoWallet = Status::StatusGo::Wallet; +namespace UtilsSG = Status::StatusGo::Utils; +namespace StatusGo = Status::StatusGo; + +namespace Status::Wallet { + +WalletController::WalletController() + : m_accounts(std::make_shared(std::move(getWalletAccounts()), "account")) + , m_currentAccount(m_accounts->get(0)) +{ +} + +WalletController *WalletController::create(QQmlEngine *qmlEngine, QJSEngine *jsEngine) +{ + return new WalletController(); +} + +NewWalletAccountController* WalletController::createNewWalletAccountController() const +{ + return new NewWalletAccountController(m_accounts); +} + +QAbstractListModel* WalletController::accountsModel() const +{ + return m_accounts.get(); +} + +WalletAccount *WalletController::currentAccount() const +{ + return m_currentAccount.get(); +} + +void WalletController::setCurrentAccountIndex(int index) +{ + assert(index >= 0 && index < m_accounts->size()); + + auto newCurrentAccount = m_accounts->get(index); + if (m_currentAccount == newCurrentAccount) + return; + + m_currentAccount = newCurrentAccount; + emit currentAccountChanged(); +} + +std::vector WalletController::getWalletAccounts(bool rootWalletAccountsOnly) const +{ + auto all = GoAccounts::getAccounts(); + std::vector result; + for(auto account : all) { + if(!account.isChat && (!rootWalletAccountsOnly || account.isWallet)) + result.push_back(std::make_shared(std::move(account))); + } + return result; +} + +} diff --git a/test/libs/StatusGoQt/test_accounts.cpp b/test/libs/StatusGoQt/test_accounts.cpp index 0cbae6090d..e435c359a8 100644 --- a/test/libs/StatusGoQt/test_accounts.cpp +++ b/test/libs/StatusGoQt/test_accounts.cpp @@ -3,9 +3,12 @@ #include #include +#include #include #include +#include + #include #include @@ -25,12 +28,10 @@ namespace Status::Testing { TEST(AccountsAPI, TestGetAccounts) { constexpr auto testAccountName = "test_get_accounts_name"; - constexpr auto testAccountPassword = "password*"; - ScopedTestAccount testAccount(test_info_->name(), testAccountName, testAccountPassword, true); + ScopedTestAccount testAccount(test_info_->name(), testAccountName); const auto accounts = Accounts::getAccounts(); - // TODO: enable after calling reset to status-go - //ASSERT_EQ(accounts.size(), 2); + 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()); @@ -47,47 +48,79 @@ TEST(AccountsAPI, TestGetAccounts) ASSERT_TRUE(walletAccount.derivedFrom.has_value()); } -TEST(Accounts, TestGenerateAccountWithDerivedPath) +TEST(Accounts, TestGenerateAccountWithDerivedPath_GenerateTwoAccounts) { constexpr auto testRootAccountName = "test-generate_account_with_derived_path-name"; - constexpr auto testAccountPassword = "password*"; - ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true); + ScopedTestAccount testAccount(test_info_->name(), testRootAccountName); - auto password{Utils::hashPassword(testAccountPassword)}; - const auto newTestAccountName = u"test_generated_new_account-name"_qs; + const auto newTestWalletAccountNameBase = u"test_generated_new_wallet_account-name"_qs; const auto newTestAccountColor = QColor("fuchsia"); const auto newTestAccountEmoji = u""_qs; - const auto newTestAccountPath = Status::Constants::General::PathWalletRoot; + const auto walletAccount = testAccount.firstWalletAccount(); + for(int i = 1; i < 3; ++i) { + const auto newTestAccountPath = GoAccounts::DerivationPath(Status::Constants::General::PathWalletRoot.get() + "/" + QString::number(i)); + const auto newTestWalletAccountName = newTestWalletAccountNameBase + QString::number(i); + Accounts::generateAccountWithDerivedPath(testAccount.hashedPassword(), + newTestWalletAccountName, + newTestAccountColor, newTestAccountEmoji, + newTestAccountPath, + walletAccount.derivedFrom.value()); + } - const auto chatAccount = testAccount.firstChatAccount(); - Accounts::generateAccountWithDerivedPath(password, newTestAccountName, - newTestAccountColor, newTestAccountEmoji, - newTestAccountPath, chatAccount.address); const auto updatedAccounts = Accounts::getAccounts(); - ASSERT_EQ(updatedAccounts.size(), 3); + ASSERT_EQ(updatedAccounts.size(), 4); + + for(int i = 1; i < 3; ++i) { + const auto newTestWalletAccountName = newTestWalletAccountNameBase + QString::number(i); + const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(), + [&newTestWalletAccountName](const auto& a) { + return a.name == newTestWalletAccountName; + }); + ASSERT_NE(newAccountIt, updatedAccounts.end()); + const auto &acc = newAccountIt; + ASSERT_FALSE(acc->derivedFrom.has_value()); + ASSERT_FALSE(acc->isWallet); + ASSERT_FALSE(acc->isChat); + const auto newTestAccountPath = GoAccounts::DerivationPath(Status::Constants::General::PathWalletRoot.get() + "/" + QString::number(i)); + ASSERT_EQ(newTestAccountPath, acc->path); + } +} + +TEST(Accounts, TestGenerateAccountWithDerivedPath_FailsWithAlreadyExists) +{ + constexpr auto testRootAccountName = "test-generate_account_with_derived_path-name"; + ScopedTestAccount testAccount(test_info_->name(), testRootAccountName); + + const auto newTestWalletAccountName = u"test_generated_new_account-name"_qs; + const auto newTestAccountColor = QColor("fuchsia"); + const auto newTestAccountEmoji = u""_qs; + const auto newTestAccountPath = Status::Constants::General::PathDefaultWallet; + + const auto walletAccount = testAccount.firstWalletAccount(); + try { + Accounts::generateAccountWithDerivedPath(testAccount.hashedPassword(), newTestWalletAccountName, + newTestAccountColor, newTestAccountEmoji, + newTestAccountPath, walletAccount.derivedFrom.value()); + FAIL() << "The first wallet account already exists from the logged in multi-account"; + } catch(const StatusGo::CallPrivateRpcError& e) { + ASSERT_EQ(e.errorResponse().error.message, "account already exists"); + ASSERT_EQ(e.errorResponse().error.code, -32000); + } + + const auto updatedAccounts = Accounts::getAccounts(); + ASSERT_EQ(updatedAccounts.size(), 2); const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(), - [newTestAccountName = std::as_const(newTestAccountName)](const auto& a) { - return a.name == newTestAccountName; + [&newTestWalletAccountName](const auto& a) { + return a.name == newTestWalletAccountName; }); - ASSERT_NE(newAccountIt, updatedAccounts.end()); - const auto &newAccount = *newAccountIt; - ASSERT_FALSE(newAccount.address.get().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.get().toUpper()); - ASSERT_EQ(newAccount.path, newTestAccountPath); - ASSERT_FALSE(newAccount.publicKey.isEmpty()); + ASSERT_EQ(newAccountIt, updatedAccounts.end()); } 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); + ScopedTestAccount testAccount(test_info_->name(), testRootAccountName); const auto chatAccount = testAccount.firstChatAccount(); try { @@ -108,23 +141,21 @@ TEST(AccountsAPI, TestGenerateAccountWithDerivedPath_WrongPassword) TEST(AccountsAPI, TestAddAccountWithMnemonicAndPath) { constexpr auto testRootAccountName = "test_root_account-name"; - constexpr auto testAccountPassword = "password*"; - ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true); + ScopedTestAccount testAccount(test_info_->name(), testRootAccountName); - auto password{Utils::hashPassword(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", - password, newTestAccountName, newTestAccountColor, newTestAccountEmoji, + testAccount.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) { + [&newTestAccountName](const auto& a) { return a.name == newTestAccountName; }); ASSERT_NE(newAccountIt, updatedAccounts.end()); @@ -144,10 +175,8 @@ TEST(AccountsAPI, TestAddAccountWithMnemonicAndPath) TEST(AccountsAPI, TestAddAccountWithMnemonicAndPath_WrongMnemonicWorks) { constexpr auto testRootAccountName = "test_root_account-name"; - constexpr auto testAccountPassword = "password*"; - ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true); + ScopedTestAccount testAccount(test_info_->name(), testRootAccountName); - auto password{Utils::hashPassword(testAccountPassword)}; const auto newTestAccountName = u"test_import_from_wrong_mnemonic-name"_qs; const auto newTestAccountColor = QColor("fuchsia"); const auto newTestAccountEmoji = u""_qs; @@ -155,14 +184,14 @@ TEST(AccountsAPI, TestAddAccountWithMnemonicAndPath_WrongMnemonicWorks) // Added an inexistent word. The mnemonic is not checked. Accounts::addAccountWithMnemonicAndPath("october control quarter husband dish throw couch depth stadium cigar waku", - password, newTestAccountName, newTestAccountColor, newTestAccountEmoji, + testAccount.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) { + [&newTestAccountName](const auto& a) { return a.name == newTestAccountName; }); @@ -182,8 +211,7 @@ TEST(AccountsAPI, TestAddAccountWithMnemonicAndPath_WrongMnemonicWorks) TEST(AccountsAPI, TestAddAccountWatch) { constexpr auto testRootAccountName = "test_root_account-name"; - constexpr auto testAccountPassword = "password*"; - ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true); + ScopedTestAccount testAccount(test_info_->name(), testRootAccountName); const auto newTestAccountName = u"test_watch_only-name"_qs; const auto newTestAccountColor = QColor("fuchsia"); @@ -194,7 +222,7 @@ TEST(AccountsAPI, TestAddAccountWatch) ASSERT_EQ(updatedAccounts.size(), 3); const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(), - [newTestAccountName = std::as_const(newTestAccountName)](const auto& a) { + [&newTestAccountName](const auto& a) { return a.name == newTestAccountName; }); ASSERT_NE(newAccountIt, updatedAccounts.end()); @@ -213,8 +241,7 @@ TEST(AccountsAPI, TestAddAccountWatch) TEST(AccountsAPI, TestDeleteAccount) { constexpr auto testRootAccountName = "test_root_account-name"; - constexpr auto testAccountPassword = "password*"; - ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true); + ScopedTestAccount testAccount(test_info_->name(), testRootAccountName); const auto newTestAccountName = u"test_account_to_delete-name"_qs; const auto newTestAccountColor = QColor("fuchsia"); @@ -225,7 +252,7 @@ TEST(AccountsAPI, TestDeleteAccount) ASSERT_EQ(updatedAccounts.size(), 3); const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(), - [newTestAccountName = std::as_const(newTestAccountName)](const auto& a) { + [&newTestAccountName](const auto& a) { return a.name == newTestAccountName; }); ASSERT_NE(newAccountIt, updatedAccounts.end()); diff --git a/test/libs/StatusGoQt/test_onboarding.cpp b/test/libs/StatusGoQt/test_onboarding.cpp index 55ec7c67af..476a0bb874 100644 --- a/test/libs/StatusGoQt/test_onboarding.cpp +++ b/test/libs/StatusGoQt/test_onboarding.cpp @@ -6,7 +6,6 @@ #include #include -#include #include diff --git a/test/libs/StatusGoQt/test_wallet.cpp b/test/libs/StatusGoQt/test_wallet.cpp index 9033551038..67f2987929 100644 --- a/test/libs/StatusGoQt/test_wallet.cpp +++ b/test/libs/StatusGoQt/test_wallet.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -12,7 +13,9 @@ #include namespace Wallet = Status::StatusGo::Wallet; +namespace Accounts = Status::StatusGo::Accounts; namespace Utils = Status::StatusGo::Utils; +namespace General = Status::Constants::General; namespace fs = std::filesystem; @@ -21,45 +24,141 @@ namespace fs = std::filesystem; /// \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) +TEST(WalletApi, TestGetDerivedAddressesForPath_FromRootAccount) { constexpr auto testRootAccountName = "test_root_account-name"; - constexpr auto testAccountPassword = "password*"; - ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true); + ScopedTestAccount testAccount(test_info_->name(), testRootAccountName); 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 password{Utils::hashPassword(testAccountPassword)}; - const auto testPath = Status::Constants::General::PathWalletRoot; + const auto testPath = General::PathWalletRoot; - // chatAccount.address - const auto chatDerivedAddresses = Wallet::getDerivedAddressesForPath(password, chatAccount.address, testPath, 3, 1); - // Check that no change is done + const auto derivedAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(), + walletAccount.derivedFrom.value(), testPath, 3, 1); + // Check that accounts are generated in memory and none is saved const auto updatedAccounts = Accounts::getAccounts(); ASSERT_EQ(updatedAccounts.size(), 2); - ASSERT_EQ(chatDerivedAddresses.size(), 3); - // all alreadyCreated are false - ASSERT_TRUE(std::none_of(chatDerivedAddresses.begin(), chatDerivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; })); + ASSERT_EQ(derivedAddresses.size(), 3); + auto defaultWalletAccountIt = std::find_if(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; }); + ASSERT_NE(defaultWalletAccountIt, derivedAddresses.end()); + const auto& defaultWalletAccount = *defaultWalletAccountIt; + ASSERT_EQ(defaultWalletAccount.path, General::PathDefaultWallet); + ASSERT_EQ(defaultWalletAccount.address, walletAccount.address); + ASSERT_TRUE(defaultWalletAccount.alreadyCreated); + + ASSERT_EQ(1, std::count_if(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; })); // all hasActivity are false - ASSERT_TRUE(std::none_of(chatDerivedAddresses.begin(), chatDerivedAddresses.end(), [](const auto& a) { return a.hasActivity; })); + ASSERT_TRUE(std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.hasActivity; })); // all address are valid - ASSERT_TRUE(std::none_of(chatDerivedAddresses.begin(), chatDerivedAddresses.end(), [](const auto& a) { return a.address.get().isEmpty(); })); + ASSERT_TRUE(std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.address.get().isEmpty(); })); +} - const auto walletDerivedAddresses = Wallet::getDerivedAddressesForPath(password, walletAccount.address, testPath, 2, 1); - ASSERT_EQ(walletDerivedAddresses.size(), 2); +TEST(Accounts, TestGetDerivedAddressesForPath_AfterLogin) +{ + constexpr auto testRootAccountName = "test-generate_account_with_derived_path-name"; + ScopedTestAccount testAccount(test_info_->name(), testRootAccountName); + + testAccount.logOut(); + + auto accountsService = std::make_shared(); + auto result = accountsService->init(testAccount.fusedTestFolder()); + ASSERT_TRUE(result); + auto onboarding = std::make_shared(accountsService); + EXPECT_EQ(onboarding->getOpenedAccounts().size(), 1); + + auto accounts = accountsService->openAndListAccounts(); + ASSERT_GT(accounts.size(), 0); + + int accountLoggedInCount = 0; + QObject::connect(onboarding.get(), &Onboarding::OnboardingController::accountLoggedIn, [&accountLoggedInCount]() { + accountLoggedInCount++; + }); + bool accountLoggedInError = false; + QObject::connect(onboarding.get(), &Onboarding::OnboardingController::accountLoginError, [&accountLoggedInError](const QString& error) { + accountLoggedInError = true; + qDebug() << "Failed logging in in test" << test_info_->name() << "with error:" << error; + }); + + auto ourAccountRes = std::find_if(accounts.begin(), accounts.end(), [&testRootAccountName](const auto &a) { return a.name == testRootAccountName; }); + auto errorString = accountsService->login(*ourAccountRes, testAccount.password()); + ASSERT_EQ(errorString.length(), 0); + + testAccount.processMessages(1000, [accountLoggedInCount, accountLoggedInError]() { + return accountLoggedInCount == 0 && !accountLoggedInError; + }); + ASSERT_EQ(accountLoggedInCount, 1); + ASSERT_EQ(accountLoggedInError, 0); + + const auto testPath = General::PathWalletRoot; + + const auto walletAccount = testAccount.firstWalletAccount(); + const auto derivedAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(), + walletAccount.derivedFrom.value(), + testPath, 3, 1); + // Check that accounts are generated in memory and none is saved + const auto updatedAccounts = Accounts::getAccounts(); + ASSERT_EQ(updatedAccounts.size(), 2); + + ASSERT_EQ(derivedAddresses.size(), 3); + auto defaultWalletAccountIt = std::find_if(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; }); + ASSERT_NE(defaultWalletAccountIt, derivedAddresses.end()); + const auto& defaultWalletAccount = *defaultWalletAccountIt; + ASSERT_EQ(defaultWalletAccount.path, General::PathDefaultWallet); + ASSERT_EQ(defaultWalletAccount.address, walletAccount.address); + ASSERT_TRUE(defaultWalletAccount.alreadyCreated); + + ASSERT_EQ(1, std::count_if(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; })); + // all hasActivity are false + ASSERT_TRUE(std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.hasActivity; })); + // all address are valid + ASSERT_TRUE(std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.address.get().isEmpty(); })); +} + +/// getDerivedAddresses@api.go fron statys-go has a special case when requesting the 6 path will return only one account +TEST(WalletApi, TestGetDerivedAddressesForPath_FromWalletAccount_FirstLevel_SixPathSpecialCase) +{ + constexpr auto testRootAccountName = "test_root_account-name"; + ScopedTestAccount testAccount(test_info_->name(), testRootAccountName); + + const auto walletAccount = testAccount.firstWalletAccount(); + + const auto testPath = General::PathDefaultWallet; + + const auto derivedAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(), + walletAccount.address, testPath, 4, 1); + ASSERT_EQ(derivedAddresses.size(), 1); + const auto& onlyAccount = derivedAddresses[0]; // all alreadyCreated are false - ASSERT_TRUE(std::none_of(walletDerivedAddresses.begin(), walletDerivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; })); + ASSERT_FALSE(onlyAccount.alreadyCreated); + ASSERT_EQ(onlyAccount.path, General::PathDefaultWallet); +} - const auto rootDerivedAddresses = Wallet::getDerivedAddressesForPath(password, 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); +TEST(WalletApi, TestGetDerivedAddressesForPath_FromWalletAccount_SecondLevel) +{ + constexpr auto testRootAccountName = "test_root_account-name"; + ScopedTestAccount testAccount(test_info_->name(), testRootAccountName); + + const auto walletAccount = testAccount.firstWalletAccount(); + const auto firstLevelPath = General::PathDefaultWallet; + const auto firstLevelAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(), + walletAccount.address, firstLevelPath, 4, 1); + + const auto testPath = Accounts::DerivationPath{General::PathDefaultWallet.get() + u"/0"_qs}; + + const auto derivedAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(), + walletAccount.address, testPath, 4, 1); + ASSERT_EQ(derivedAddresses.size(), 4); + + // all alreadyCreated are false + ASSERT_TRUE(std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; })); + // all hasActivity are false + ASSERT_TRUE(std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.hasActivity; })); + // all address are valid + ASSERT_TRUE(std::none_of(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.address.get().isEmpty(); })); + ASSERT_TRUE(std::all_of(derivedAddresses.begin(), derivedAddresses.end(), [&testPath](const auto& a) { return a.path.get().startsWith(testPath.get()); })); } } // namespace