chore(CPP): Create new wallet accounts - POC UI

The UI is for demo purposes. Also architecture decisions are open for change

Closes: #6321
This commit is contained in:
Stefan 2022-07-15 09:30:16 +02:00 committed by Stefan Dunca
parent ffc053e0aa
commit d5afd6beac
59 changed files with 1841 additions and 283 deletions

View File

@ -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)

View File

@ -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

View File

@ -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 ...
}
}

View File

@ -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"
}
}
}
}

View File

@ -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 ...
}
}

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}
}

View File

@ -1,6 +0,0 @@
import QtQml
import Status.Controls.Navigation
NavigationBarButton {
}

View File

@ -29,8 +29,9 @@ Item {
id: onboardingViewComponent
OnboardingView {
onUserLoggedIn: {
onUserLoggedIn: function (statusAccount) {
splashScreenPopup.open()
//appController.statusAccount = statusAccount
contentLoader.sourceComponent = mainViewComponent
}
}

View File

@ -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();
}
}

View File

@ -4,6 +4,11 @@
#include <QObject>
#include <QtQml/qqmlregistration.h>
// TODO: investigate. This line breaks qobject_cast in OnboardingController::login
//#include <Onboarding/UserAccount.h>
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{};
};
}

View File

@ -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
)

View File

@ -0,0 +1,98 @@
#pragma once
#include <QAbstractListModel>
#include <QQmlEngine>
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<typename T>
class QObjectVectorModel final : public QAbstractListModel
{
static_assert(std::is_base_of<QObject, T>::value, "Template parameter (T) not a QObject");
public:
using ObjectContainer = std::vector<std::shared_ptr<T>>;
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<int, QByteArray> 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<QObject*>(m_objects[index.row()].get());
}
const T* at(size_t pos) const {
return m_objects.at(pos).get();
};
std::shared_ptr<T> 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<T> 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<T> 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;
};
}

View File

@ -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)
}
}
}

View File

@ -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}"`)

View File

@ -81,8 +81,8 @@ const std::vector<GeneratedMultiAccount>& 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<Accounts::DerivationPath> &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;

View File

@ -5,7 +5,7 @@
#include <QtCore>
#include <QStringLiteral>
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<Accounts::DerivationPath> AccountDefaultPaths {PathWalletRoot, PathEIP1581, PathWhisper, PathDefaultWallet};
inline const std::vector<GoAccounts::DerivationPath> AccountDefaultPaths {PathWalletRoot, PathEIP1581, PathWhisper, PathDefaultWallet};
}
}

View File

@ -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::<domain>`. Don't go too deep, not deeper than two domain-related namespaces
- [ ] Consider RAII for controllers, remove `init`

View File

@ -9,7 +9,6 @@ UserAccount::UserAccount(std::unique_ptr<MultiAccount> data)
: QObject()
, m_data(std::move(data))
{
}
const QString &UserAccount::name() const

View File

@ -3,13 +3,10 @@
#include "UserAccount.h"
#include <QAbstractListModel>
#include <QQmlEngine>
namespace Status::Onboarding {
/*!
* \brief Available UserAccount elements
*/
/// \todo Replace it with \c QObjectVectorModel
class UserAccountsModel : public QAbstractListModel
{
Q_OBJECT

View File

@ -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<AutoCleanTempTestDir>(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<Onboarding::OnboardingController>(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();

View File

@ -2,6 +2,8 @@
#include <Wallet/WalletApi.h>
#include <StatusGo/Utils.h>
#include <string>
#include <filesystem>
@ -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<bool()> 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;

View File

@ -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

View File

@ -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<Onboarding::OnboardingController>(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<Onboarding::AccountsService>();
auto result = accountsService->init(testAccount.fusedTestFolder());
ASSERT_TRUE(result);
auto onboarding = std::make_shared<Onboarding::OnboardingController>(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

View File

@ -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,

View File

@ -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,

View File

@ -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<EOAddress> derivedFrom;
QString emoji;
bool isHidden = false;
QString mixedcaseAddress;
QString name;
EOAddress address;
bool isChat{false};
bool isWallet{false};
QColor color;
QString emoji;
std::optional<EOAddress> 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<ChatOrWalletAccount>;

View File

@ -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;

View File

@ -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

View File

@ -1,7 +1,5 @@
import QtQuick
/*!
Template for application section content
*/
/// Template for application section content
Item {
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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
}
}
}
}
}
}

View File

@ -1,8 +0,0 @@
import QtQuick
/*!
Template for a NavigationBar square button
*/
Item {
height: width
}

View File

@ -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
}

View File

@ -0,0 +1,5 @@
import QtQuick
ApplicationContentView {
readonly property int panelWidth: 304
}

View File

@ -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

View File

@ -1,26 +1,3 @@
#include <QtQuickTest/quicktest.h>
#include <QQmlEngine>
#include <QtQuickTest>
#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<MonitorQtOutput>("StatusQ.TestHelpers", 0, 1, "MonitorQtOutput");
}
};
QUICK_TEST_MAIN_WITH_SETUP(TestControls, TestSetup)
#include "main.moc"
QUICK_TEST_MAIN(TestOnboardingQml)

View File

@ -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
}
}

View File

@ -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
)

View File

@ -0,0 +1,33 @@
#pragma once
#include <StatusGo/Wallet/DerivedAddress.h>
#include <QObject>
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<DerivedWalletAddress>;
}

View File

@ -0,0 +1,95 @@
#pragma once
#include "Status/Wallet/WalletAccount.h"
#include "Status/Wallet/DerivedWalletAddress.h"
#include <Helpers/QObjectVectorModel.h>
#include <QQmlListProperty>
#include <QtQmlIntegration>
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<WalletAccount>;
/// \note On account creation \c accounts are updated with the newly created wallet account
NewWalletAccountController(std::shared_ptr<AccountsModel> 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<DerivedWalletAddress> newEntry);
std::tuple<DerivedWalletAddressPtr, int> searchDerivationPath(const GoAccounts::DerivationPath &derivationPath);
WalletAccountPtr findMissingAccount();
AccountsModel::ObjectContainer filterMainAccounts(const AccountsModel &accounts);
std::shared_ptr<AccountsModel> m_accounts;
/// \todo make it a proxy filter on top of \c m_accounts
AccountsModel m_mainAccounts;
Helpers::QObjectVectorModel<DerivedWalletAddress> 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

View File

@ -0,0 +1,37 @@
#pragma once
#include "Accounts/ChatOrWalletAccount.h"
#include <QObject>
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<WalletAccount>;
using WalletAccounts = std::vector<WalletAccount>;
}

View File

@ -0,0 +1,58 @@
#pragma once
#include "Status/Wallet/WalletAccount.h"
#include "Status/Wallet/DerivedWalletAddress.h"
#include <Helpers/QObjectVectorModel.h>
#include <QQmlListProperty>
#include <QtQmlIntegration>
#include <memory>
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<WalletAccountPtr> getWalletAccounts(bool rootWalletAccountsOnly = false) const;
using AccountsModel = Helpers::QObjectVectorModel<WalletAccount>;
std::shared_ptr<AccountsModel> m_accounts;
WalletAccountPtr m_currentAccount;
};
} // namespace Status::Wallet

View File

@ -0,0 +1,14 @@
import QtQuick
import QtQuick.Controls
Item {
id: root
/// WalletAccount
required property var asset
Label {
anchors.centerIn: parent
text: "$$$$$"
}
}

View File

@ -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
}
}

View File

@ -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("<Check password and path!>")
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
}
}
}
}

View File

@ -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"
}
}
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,187 @@
#include "Status/Wallet/NewWalletAccountController.h"
#include <StatusGo/Wallet/WalletApi.h>
#include <StatusGo/Accounts/AccountsAPI.h>
#include <StatusGo/Accounts/Accounts.h>
#include <StatusGo/Accounts/accounts_types.h>
#include <StatusGo/Metadata/api_response.h>
#include <StatusGo/Utils.h>
#include <StatusGo/Types.h>
#include <Onboarding/Common/Constants.h>
#include <QQmlEngine>
#include <QJSEngine>
#include <ranges>
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<Helpers::QObjectVectorModel<WalletAccount>> 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<int>(std::ceil(static_cast<double>(m_maxDerivedAddresses)/static_cast<double>(m_derivedAddressesPageSize)));
std::shared_ptr<DerivedWalletAddress> 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<DerivedWalletAddress>(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<WalletAccount>(*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<DerivedWalletAddress> newEntry) {
m_derivedAddressIndex = index;
m_selectedDerivedAddress = newEntry;
emit selectedDerivedAddressChanged();
if(m_derivationPath != newEntry->data().path) {
m_derivationPath = newEntry->data().path;
emit derivationPathChanged();
}
}
std::tuple<DerivedWalletAddressPtr, int> 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};
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,74 @@
#include "Status/Wallet/WalletController.h"
#include "NewWalletAccountController.h"
#include <StatusGo/Wallet/WalletApi.h>
#include <StatusGo/Accounts/AccountsAPI.h>
#include <StatusGo/Accounts/Accounts.h>
#include <StatusGo/Accounts/accounts_types.h>
#include <StatusGo/Metadata/api_response.h>
#include <StatusGo/Utils.h>
#include <StatusGo/Types.h>
#include <Onboarding/Common/Constants.h>
#include <QQmlEngine>
#include <QJSEngine>
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<AccountsModel>(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<WalletAccountPtr> WalletController::getWalletAccounts(bool rootWalletAccountsOnly) const
{
auto all = GoAccounts::getAccounts();
std::vector<WalletAccountPtr> result;
for(auto account : all) {
if(!account.isChat && (!rootWalletAccountsOnly || account.isWallet))
result.push_back(std::make_shared<WalletAccount>(std::move(account)));
}
return result;
}
}

View File

@ -3,9 +3,12 @@
#include <StatusGo/Accounts/Accounts.h>
#include <Onboarding/Common/Constants.h>
#include <Onboarding/Accounts/AccountsService.h>
#include <Onboarding/OnboardingController.h>
#include <StatusGo/Utils.h>
#include <StatusGo/SignalsManager.h>
#include <IOTestHelpers.h>
#include <ScopedTestAccount.h>
@ -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());

View File

@ -6,7 +6,6 @@
#include <Onboarding/OnboardingController.h>
#include <IOTestHelpers.h>
#include <ScopedTestAccount.h>
#include <gtest/gtest.h>

View File

@ -3,6 +3,7 @@
#include <StatusGo/Metadata/api_response.h>
#include <Onboarding/Accounts/AccountsServiceInterface.h>
#include <Onboarding/Accounts/AccountsService.h>
#include <Onboarding/Common/Constants.h>
#include <Onboarding/OnboardingController.h>
@ -12,7 +13,9 @@
#include <gtest/gtest.h>
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<Onboarding::AccountsService>();
auto result = accountsService->init(testAccount.fusedTestFolder());
ASSERT_TRUE(result);
auto onboarding = std::make_shared<Onboarding::OnboardingController>(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