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:
parent
ffc053e0aa
commit
d5afd6beac
|
@ -32,6 +32,7 @@ add_subdirectory(libs/ApplicationCore)
|
||||||
add_subdirectory(libs/Assets)
|
add_subdirectory(libs/Assets)
|
||||||
add_subdirectory(libs/Helpers)
|
add_subdirectory(libs/Helpers)
|
||||||
add_subdirectory(libs/Onboarding)
|
add_subdirectory(libs/Onboarding)
|
||||||
|
add_subdirectory(libs/Wallet)
|
||||||
add_subdirectory(libs/StatusGoQt)
|
add_subdirectory(libs/StatusGoQt)
|
||||||
add_subdirectory(libs/StatusQ)
|
add_subdirectory(libs/StatusQ)
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,6 @@ qt6_add_qml_module(${PROJECT_NAME}
|
||||||
qml/Status/Application/MainView/MainView.qml
|
qml/Status/Application/MainView/MainView.qml
|
||||||
qml/Status/Application/MainView/StatusApplicationSections.qml
|
qml/Status/Application/MainView/StatusApplicationSections.qml
|
||||||
|
|
||||||
qml/Status/Application/MainView/StatusApplicationSections/Wallet/WalletNavBarSection.qml
|
|
||||||
|
|
||||||
qml/Status/Application/Settings/ApplicationSettings.qml
|
qml/Status/Application/Settings/ApplicationSettings.qml
|
||||||
|
|
||||||
qml/Status/Application/System/StatusTrayIcon.qml
|
qml/Status/Application/System/StatusTrayIcon.qml
|
||||||
|
@ -70,6 +68,7 @@ target_link_libraries(${PROJECT_NAME}
|
||||||
Status::ApplicationCore
|
Status::ApplicationCore
|
||||||
Status::Helpers
|
Status::Helpers
|
||||||
Status::Onboarding
|
Status::Onboarding
|
||||||
|
Status::Wallet
|
||||||
Status::Assets
|
Status::Assets
|
||||||
Status::StatusQ
|
Status::StatusQ
|
||||||
Status::StatusGoQt
|
Status::StatusGoQt
|
||||||
|
|
|
@ -7,7 +7,7 @@ import Status.Application
|
||||||
import Status.Containers
|
import Status.Containers
|
||||||
import Status.Controls
|
import Status.Controls
|
||||||
|
|
||||||
import Status.Application.Navigation
|
import Status.Controls.Navigation
|
||||||
|
|
||||||
/// Responsible for setup of user workflows after onboarding
|
/// Responsible for setup of user workflows after onboarding
|
||||||
Item {
|
Item {
|
||||||
|
@ -28,12 +28,12 @@ Item {
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
StatusNavigationBar {
|
NavigationBar {
|
||||||
id: navBar
|
id: navBar
|
||||||
|
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
||||||
sections: appSections.sectionsList
|
sections: appSections.sections
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
@ -46,17 +46,15 @@ Item {
|
||||||
visible: false // TODO: appController.bannerController.visible
|
visible: false // TODO: appController.bannerController.visible
|
||||||
}
|
}
|
||||||
Loader {
|
Loader {
|
||||||
id: mainLoader
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
sourceComponent: navBar.currentSection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusApplicationSections {
|
StatusApplicationSections {
|
||||||
id: appSections
|
id: appSections
|
||||||
// Chat ...
|
|
||||||
// Wallet ...
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,37 @@
|
||||||
import QtQml
|
import QtQml
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
|
||||||
|
import Status.Application.Navigation
|
||||||
import Status.Controls.Navigation
|
import Status.Controls.Navigation
|
||||||
|
import Status.Wallet
|
||||||
|
|
||||||
QtObject {
|
Item {
|
||||||
readonly property var sectionsList: [wallet, settings]
|
property var sections: [walletSection, settingsSection]
|
||||||
readonly property ApplicationSection wallet: ApplicationSection {
|
|
||||||
navButton: WalletButtonComponent
|
|
||||||
content: WalletContentComponent
|
|
||||||
|
|
||||||
component WalletButtonComponent: NavigationBarButton {
|
ButtonGroup {
|
||||||
}
|
id: oneSectionSelectedGroup
|
||||||
component WalletContentComponent: ApplicationContentView {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ...
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,8 +15,8 @@ qt6_add_qml_module(${PROJECT_NAME}
|
||||||
VERSION 1.0
|
VERSION 1.0
|
||||||
|
|
||||||
QML_FILES
|
QML_FILES
|
||||||
StatusNavigationBar.qml
|
NavigationBarButton.qml
|
||||||
StatusNavigationButton.qml
|
SimpleNavBarSection.qml
|
||||||
|
|
||||||
# Required to suppress "qmllint may not work" warning
|
# Required to suppress "qmllint may not work" warning
|
||||||
OUTPUT_DIRECTORY
|
OUTPUT_DIRECTORY
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import QtQml
|
|
||||||
|
|
||||||
import Status.Controls.Navigation
|
|
||||||
|
|
||||||
NavigationBarButton {
|
|
||||||
}
|
|
|
@ -29,8 +29,9 @@ Item {
|
||||||
id: onboardingViewComponent
|
id: onboardingViewComponent
|
||||||
|
|
||||||
OnboardingView {
|
OnboardingView {
|
||||||
onUserLoggedIn: {
|
onUserLoggedIn: function (statusAccount) {
|
||||||
splashScreenPopup.open()
|
splashScreenPopup.open()
|
||||||
|
//appController.statusAccount = statusAccount
|
||||||
contentLoader.sourceComponent = mainViewComponent
|
contentLoader.sourceComponent = mainViewComponent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,24 @@
|
||||||
#include "ApplicationController.h"
|
#include "ApplicationController.h"
|
||||||
|
|
||||||
|
namespace Status::Application {
|
||||||
|
|
||||||
ApplicationController::ApplicationController(QObject *parent)
|
ApplicationController::ApplicationController(QObject *parent)
|
||||||
: 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,11 @@
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QtQml/qqmlregistration.h>
|
#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
|
* @brief Responsible for providing general information and utility components
|
||||||
*/
|
*/
|
||||||
|
@ -11,9 +16,19 @@ class ApplicationController : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
|
Q_PROPERTY(QObject* statusAccount READ statusAccount WRITE setStatusAccount NOTIFY statusAccountChanged)
|
||||||
public:
|
public:
|
||||||
explicit ApplicationController(QObject *parent = nullptr);
|
explicit ApplicationController(QObject *parent = nullptr);
|
||||||
|
|
||||||
signals:
|
QObject *statusAccount() const;
|
||||||
|
void setStatusAccount(QObject *newStatusAccount);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void statusAccountChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QObject* m_statusAccount{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -72,5 +72,6 @@ target_sources(Helpers
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/logs.h
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/logs.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/logs.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/logs.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/NamedType.h
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/NamedType.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/QObjectVectorModel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/Singleton.h
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Helpers/Singleton.h
|
||||||
)
|
)
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -17,7 +17,8 @@ import Status.ApplicationCore
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
signal userLoggedIn()
|
/// \param statusAccount \c UserAccount
|
||||||
|
signal userLoggedIn(var statusAccount)
|
||||||
|
|
||||||
implicitWidth: 1232
|
implicitWidth: 1232
|
||||||
implicitHeight: 770
|
implicitHeight: 770
|
||||||
|
@ -47,7 +48,9 @@ Item {
|
||||||
initialItem: WelcomeView {
|
initialItem: WelcomeView {
|
||||||
onboardingController: onboardingModule.controller
|
onboardingController: onboardingModule.controller
|
||||||
onSetupNewAccount: stackView.push(setupNewProfileViewComponent)
|
onSetupNewAccount: stackView.push(setupNewProfileViewComponent)
|
||||||
onAccountLoggedIn: root.userLoggedIn()
|
onAccountLoggedIn: function (statusAccount) {
|
||||||
|
root.userLoggedIn(statusAccount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import Status.Onboarding
|
||||||
|
|
||||||
import Status.Containers
|
import Status.Containers
|
||||||
|
|
||||||
import "base"
|
import "base"
|
||||||
|
@ -12,7 +14,8 @@ OnboardingPageBase {
|
||||||
required property var onboardingController // OnboardingController
|
required property var onboardingController // OnboardingController
|
||||||
|
|
||||||
signal setupNewAccount()
|
signal setupNewAccount()
|
||||||
signal accountLoggedIn()
|
/// \param statusAccount \c UserAccount
|
||||||
|
signal accountLoggedIn(var statusAccount)
|
||||||
|
|
||||||
backAvailable: false
|
backAvailable: false
|
||||||
|
|
||||||
|
@ -51,10 +54,16 @@ OnboardingPageBase {
|
||||||
|
|
||||||
// TODO: remove dev helper
|
// TODO: remove dev helper
|
||||||
text: "1234567890"
|
text: "1234567890"
|
||||||
|
Timer {
|
||||||
|
interval: 100
|
||||||
|
running: loginButton.enabled && accountsComboBox.count
|
||||||
|
onTriggered: loginButton.clicked()
|
||||||
|
}
|
||||||
// END dev
|
// END dev
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
|
id: loginButton
|
||||||
text: qsTr("Login")
|
text: qsTr("Login")
|
||||||
enabled: passwordInput.text.length >= 10
|
enabled: passwordInput.text.length >= 10
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
@ -80,7 +89,7 @@ OnboardingPageBase {
|
||||||
target: onboardingController
|
target: onboardingController
|
||||||
|
|
||||||
function onAccountLoggedIn() {
|
function onAccountLoggedIn() {
|
||||||
root.accountLoggedIn()
|
root.accountLoggedIn(accountsComboBox.currentValue)
|
||||||
}
|
}
|
||||||
function onAccountLoginError(error) {
|
function onAccountLoginError(error) {
|
||||||
console.warn(`Error logging in "${error}"`)
|
console.warn(`Error logging in "${error}"`)
|
||||||
|
|
|
@ -81,8 +81,8 @@ const std::vector<GeneratedMultiAccount>& AccountsService::generatedAccounts() c
|
||||||
|
|
||||||
bool AccountsService::setupAccountAndLogin(const QString &accountId, const QString &password, const QString &displayName)
|
bool AccountsService::setupAccountAndLogin(const QString &accountId, const QString &password, const QString &displayName)
|
||||||
{
|
{
|
||||||
QString installationId(QUuid::createUuid().toString(QUuid::WithoutBraces));
|
const QString installationId(QUuid::createUuid().toString(QUuid::WithoutBraces));
|
||||||
QJsonObject accountData(getAccountDataForAccountId(accountId, displayName));
|
const QJsonObject accountData(getAccountDataForAccountId(accountId, displayName));
|
||||||
|
|
||||||
if(!setKeyStoreDir(accountData.value("key-uid").toString()))
|
if(!setKeyStoreDir(accountData.value("key-uid").toString()))
|
||||||
return false;
|
return false;
|
||||||
|
@ -123,7 +123,7 @@ bool AccountsService::isFirstTimeAccountLogin() const
|
||||||
bool AccountsService::setKeyStoreDir(const QString &key)
|
bool AccountsService::setKeyStoreDir(const QString &key)
|
||||||
{
|
{
|
||||||
m_keyStoreDir = m_statusgoDataDir / m_keyStoreDirName / key.toStdString();
|
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();
|
return !response.containsError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,12 +137,16 @@ QString AccountsService::login(MultiAccount account, const QString& password)
|
||||||
if(StatusGo::Accounts::openAccounts(m_statusgoDataDir.c_str()).containsError())
|
if(StatusGo::Accounts::openAccounts(m_statusgoDataDir.c_str()).containsError())
|
||||||
return QString("Failed to open accounts before logging in");
|
return QString("Failed to open accounts before logging in");
|
||||||
|
|
||||||
auto hashedPassword(Utils::hashPassword(password));
|
const auto hashedPassword(Utils::hashPassword(password));
|
||||||
|
|
||||||
QString thumbnailImage;
|
const QString installationId(QUuid::createUuid().toString(QUuid::WithoutBraces));
|
||||||
QString largeImage;
|
const QJsonObject nodeConfig(getDefaultNodeConfig(installationId));
|
||||||
auto response = StatusGo::Accounts::login(account.name, account.keyUid, hashedPassword,
|
|
||||||
thumbnailImage, largeImage);
|
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())
|
if(response.containsError())
|
||||||
{
|
{
|
||||||
qWarning() << response.error.message;
|
qWarning() << response.error.message;
|
||||||
|
@ -164,7 +168,7 @@ void AccountsService::clear()
|
||||||
|
|
||||||
QString AccountsService::generateAlias(const QString& publicKey)
|
QString AccountsService::generateAlias(const QString& publicKey)
|
||||||
{
|
{
|
||||||
auto response = StatusGo::Accounts::generateAlias(publicKey);
|
const auto response = StatusGo::Accounts::generateAlias(publicKey);
|
||||||
if(response.containsError())
|
if(response.containsError())
|
||||||
{
|
{
|
||||||
qWarning() << response.error.message;
|
qWarning() << response.error.message;
|
||||||
|
@ -182,7 +186,7 @@ void AccountsService::deleteMultiAccount(const MultiAccount &account)
|
||||||
DerivedAccounts AccountsService::storeDerivedAccounts(const QString& accountId, const StatusGo::HashedPassword& password,
|
DerivedAccounts AccountsService::storeDerivedAccounts(const QString& accountId, const StatusGo::HashedPassword& password,
|
||||||
const std::vector<Accounts::DerivationPath> &paths)
|
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())
|
if(response.containsError())
|
||||||
{
|
{
|
||||||
qWarning() << response.error.message;
|
qWarning() << response.error.message;
|
||||||
|
@ -193,7 +197,7 @@ DerivedAccounts AccountsService::storeDerivedAccounts(const QString& accountId,
|
||||||
|
|
||||||
StoredMultiAccount AccountsService::storeAccount(const QString& accountId, const StatusGo::HashedPassword& password)
|
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())
|
if(response.containsError())
|
||||||
{
|
{
|
||||||
qWarning() << response.error.message;
|
qWarning() << response.error.message;
|
||||||
|
@ -308,7 +312,7 @@ QJsonObject AccountsService::prepareAccountSettingsJsonObject(const GeneratedMul
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
auto templateDefaultNetworksJson = getDataFromFile(":/Status/StaticConfig/default-networks.json").value();
|
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);
|
QString defaultNetworksContent = templateDefaultNetworksJson.replace("%INFURA_KEY%", infuraKey);
|
||||||
QJsonArray defaultNetworksJson = QJsonDocument::fromJson(defaultNetworksContent.toUtf8()).array();
|
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)
|
QJsonArray getNodes(const QJsonObject& fleet, const QString& nodeType)
|
||||||
{
|
{
|
||||||
auto nodes = fleet[nodeType].toObject();
|
const auto nodes = fleet[nodeType].toObject();
|
||||||
QJsonArray result;
|
QJsonArray result;
|
||||||
for(auto it = nodes.begin(); it != nodes.end(); ++it)
|
for(auto it = nodes.begin(); it != nodes.end(); ++it)
|
||||||
result << *it;
|
result << *it;
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
#include <QtCore>
|
#include <QtCore>
|
||||||
#include <QStringLiteral>
|
#include <QStringLiteral>
|
||||||
|
|
||||||
namespace Accounts = Status::StatusGo::Accounts;
|
namespace GoAccounts = Status::StatusGo::Accounts;
|
||||||
|
|
||||||
namespace Status::Constants
|
namespace Status::Constants
|
||||||
{
|
{
|
||||||
|
@ -38,15 +38,15 @@ namespace General
|
||||||
|
|
||||||
inline const auto ZeroAddress = u"0x0000000000000000000000000000000000000000"_qs;
|
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
|
// 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
|
// 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
|
// 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};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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`
|
|
|
@ -9,7 +9,6 @@ UserAccount::UserAccount(std::unique_ptr<MultiAccount> data)
|
||||||
: QObject()
|
: QObject()
|
||||||
, m_data(std::move(data))
|
, m_data(std::move(data))
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString &UserAccount::name() const
|
const QString &UserAccount::name() const
|
||||||
|
|
|
@ -3,13 +3,10 @@
|
||||||
#include "UserAccount.h"
|
#include "UserAccount.h"
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
#include <QQmlEngine>
|
|
||||||
|
|
||||||
namespace Status::Onboarding {
|
namespace Status::Onboarding {
|
||||||
|
|
||||||
/*!
|
/// \todo Replace it with \c QObjectVectorModel
|
||||||
* \brief Available UserAccount elements
|
|
||||||
*/
|
|
||||||
class UserAccountsModel : public QAbstractListModel
|
class UserAccountsModel : public QAbstractListModel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
|
@ -22,7 +22,9 @@ namespace fs = std::filesystem;
|
||||||
|
|
||||||
namespace Status::Testing {
|
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_fusedTestFolder{std::make_unique<AutoCleanTempTestDir>(tempTestSubfolderName)}
|
||||||
, m_accountName(accountName)
|
, m_accountName(accountName)
|
||||||
, m_accountPassword(accountPassword)
|
, m_accountPassword(accountPassword)
|
||||||
|
@ -48,7 +50,7 @@ ScopedTestAccount::ScopedTestAccount(const std::string &tempTestSubfolderName, c
|
||||||
|
|
||||||
// Beware, smartpointer is a requirement
|
// Beware, smartpointer is a requirement
|
||||||
m_onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
|
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");
|
throw std::runtime_error("ScopedTestAccount - already have opened account");
|
||||||
|
|
||||||
int accountLoggedInCount = 0;
|
int accountLoggedInCount = 0;
|
||||||
|
@ -132,6 +134,11 @@ Accounts::ChatOrWalletAccount ScopedTestAccount::firstWalletAccount()
|
||||||
return *walletIt;
|
return *walletIt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Onboarding::MultiAccount &ScopedTestAccount::loggedInAccount() const
|
||||||
|
{
|
||||||
|
return m_onboarding->accountsService()->getLoggedInAccount();
|
||||||
|
}
|
||||||
|
|
||||||
Onboarding::OnboardingController *ScopedTestAccount::onboardingController() const
|
Onboarding::OnboardingController *ScopedTestAccount::onboardingController() const
|
||||||
{
|
{
|
||||||
return m_onboarding.get();
|
return m_onboarding.get();
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
#include <Wallet/WalletApi.h>
|
#include <Wallet/WalletApi.h>
|
||||||
|
|
||||||
|
#include <StatusGo/Utils.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
||||||
|
@ -11,10 +13,12 @@ class QCoreApplication;
|
||||||
|
|
||||||
namespace Status::Onboarding {
|
namespace Status::Onboarding {
|
||||||
class OnboardingController;
|
class OnboardingController;
|
||||||
|
class MultiAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Wallet = Status::StatusGo::Wallet;
|
namespace Wallet = Status::StatusGo::Wallet;
|
||||||
namespace Accounts = Status::StatusGo::Accounts;
|
namespace Accounts = Status::StatusGo::Accounts;
|
||||||
|
namespace GoUtils = Status::StatusGo::Utils;
|
||||||
|
|
||||||
namespace Status::Testing {
|
namespace Status::Testing {
|
||||||
|
|
||||||
|
@ -29,8 +33,7 @@ public:
|
||||||
*/
|
*/
|
||||||
explicit ScopedTestAccount(const std::string &tempTestSubfolderName,
|
explicit ScopedTestAccount(const std::string &tempTestSubfolderName,
|
||||||
const QString &accountName = defaultAccountName,
|
const QString &accountName = defaultAccountName,
|
||||||
const QString &accountPassword = defaultAccountPassword,
|
const QString &accountPassword = defaultAccountPassword);
|
||||||
bool ignorePreviousState = false /*workaround to status-go persisting state*/);
|
|
||||||
~ScopedTestAccount();
|
~ScopedTestAccount();
|
||||||
|
|
||||||
void processMessages(size_t millis, std::function<bool()> shouldWaitUntilTimeout);
|
void processMessages(size_t millis, std::function<bool()> shouldWaitUntilTimeout);
|
||||||
|
@ -38,8 +41,11 @@ public:
|
||||||
|
|
||||||
static Accounts::ChatOrWalletAccount firstChatAccount();
|
static Accounts::ChatOrWalletAccount firstChatAccount();
|
||||||
static Accounts::ChatOrWalletAccount firstWalletAccount();
|
static Accounts::ChatOrWalletAccount firstWalletAccount();
|
||||||
|
/// Root account
|
||||||
|
const Status::Onboarding::MultiAccount &loggedInAccount() const;
|
||||||
|
|
||||||
QString password() const { return m_accountPassword; };
|
QString password() const { return m_accountPassword; };
|
||||||
|
StatusGo::HashedPassword hashedPassword() const { return GoUtils::hashPassword(m_accountPassword); };
|
||||||
|
|
||||||
Status::Onboarding::OnboardingController* onboardingController() const;
|
Status::Onboarding::OnboardingController* onboardingController() const;
|
||||||
|
|
||||||
|
|
|
@ -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
|
# 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_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_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_BINARY_DIR}")
|
add_custom_target("Run_TestOnboardingQml" COMMAND ${CMAKE_CTEST_COMMAND} --test-dir "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||||
add_dependencies("Run_TestOnboardingQml" TestOnboardingQml)
|
add_dependencies("Run_TestOnboardingQml" TestOnboardingQml)
|
||||||
|
|
||||||
target_link_libraries(TestOnboardingQml PRIVATE
|
target_link_libraries(TestOnboardingQml PRIVATE
|
||||||
|
|
|
@ -120,8 +120,7 @@ TEST(OnboardingModule, TestLoginEndToEnd)
|
||||||
});
|
});
|
||||||
|
|
||||||
constexpr auto accountName = "TestLoginAccountName";
|
constexpr auto accountName = "TestLoginAccountName";
|
||||||
constexpr auto accountPassword = "1234567890";
|
ScopedTestAccount testAccount(test_info_->name(), accountName);
|
||||||
ScopedTestAccount testAccount(test_info_->name(), accountName, accountPassword, true);
|
|
||||||
testAccount.processMessages(1000, [createAndLogin]() {
|
testAccount.processMessages(1000, [createAndLogin]() {
|
||||||
return !createAndLogin;
|
return !createAndLogin;
|
||||||
});
|
});
|
||||||
|
@ -139,10 +138,9 @@ TEST(OnboardingModule, TestLoginEndToEnd)
|
||||||
|
|
||||||
auto onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
|
auto onboarding = std::make_shared<Onboarding::OnboardingController>(accountsService);
|
||||||
// We don't have a way yet to simulate status-go process exit
|
// 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();
|
auto accounts = accountsService->openAndListAccounts();
|
||||||
//ASSERT_EQ(accounts.size(), 1);
|
|
||||||
ASSERT_GT(accounts.size(), 0);
|
ASSERT_GT(accounts.size(), 0);
|
||||||
|
|
||||||
int accountLoggedInCount = 0;
|
int accountLoggedInCount = 0;
|
||||||
|
@ -150,13 +148,15 @@ TEST(OnboardingModule, TestLoginEndToEnd)
|
||||||
accountLoggedInCount++;
|
accountLoggedInCount++;
|
||||||
});
|
});
|
||||||
bool accountLoggedInError = false;
|
bool accountLoggedInError = false;
|
||||||
QObject::connect(onboarding.get(), &Onboarding::OnboardingController::accountLoginError, [&accountLoggedInError]() {
|
QObject::connect(onboarding.get(), &Onboarding::OnboardingController::accountLoginError,
|
||||||
accountLoggedInError = true;
|
[&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 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);
|
ASSERT_EQ(errorString.length(), 0);
|
||||||
|
|
||||||
testAccount.processMessages(1000, [accountLoggedInCount, accountLoggedInError]() {
|
testAccount.processMessages(1000, [accountLoggedInCount, accountLoggedInError]() {
|
||||||
|
@ -166,4 +166,42 @@ TEST(OnboardingModule, TestLoginEndToEnd)
|
||||||
ASSERT_EQ(accountLoggedInError, 0);
|
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
|
} // namespace
|
||||||
|
|
|
@ -42,6 +42,7 @@ void generateAccountWithDerivedPath(const HashedPassword &password, const QStrin
|
||||||
auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str());
|
auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str());
|
||||||
auto resultJson = json::parse(result);
|
auto resultJson = json::parse(result);
|
||||||
checkPrivateRpcCallResultAndReportError(resultJson);
|
checkPrivateRpcCallResultAndReportError(resultJson);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAccountWithMnemonicAndPath(const QString &mnemonic, const HashedPassword &password, const QString &name,
|
void addAccountWithMnemonicAndPath(const QString &mnemonic, const HashedPassword &password, const QString &name,
|
||||||
|
|
|
@ -20,8 +20,12 @@ namespace Status::StatusGo::Accounts
|
||||||
/// \throws \c CallPrivateRpcError
|
/// \throws \c CallPrivateRpcError
|
||||||
Accounts::ChatOrWalletAccounts getAccounts();
|
Accounts::ChatOrWalletAccounts getAccounts();
|
||||||
|
|
||||||
/// \brief Generate a new account
|
/// \brief Generate a new account for the specified derivation path
|
||||||
/// \note the underlying status-go api, SaveAccounts@accounts.go, returns `nil` for \c CallPrivateRpcResponse.result
|
///
|
||||||
|
/// \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
|
/// \see \c getAccounts
|
||||||
/// \throws \c CallPrivateRpcError
|
/// \throws \c CallPrivateRpcError
|
||||||
void generateAccountWithDerivedPath(const HashedPassword &password, const QString &name,
|
void generateAccountWithDerivedPath(const HashedPassword &password, const QString &name,
|
||||||
|
|
|
@ -19,19 +19,19 @@ namespace Status::StatusGo::Accounts {
|
||||||
/// \note equivalent of status-go's accounts.Account@multiaccounts/accounts/database.go
|
/// \note equivalent of status-go's accounts.Account@multiaccounts/accounts/database.go
|
||||||
struct ChatOrWalletAccount
|
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;
|
QString name;
|
||||||
|
EOAddress address;
|
||||||
|
bool isChat{false};
|
||||||
|
bool isWallet{false};
|
||||||
|
QColor color;
|
||||||
|
QString emoji;
|
||||||
|
std::optional<EOAddress> derivedFrom;
|
||||||
DerivationPath path;
|
DerivationPath path;
|
||||||
|
int clock{-1};
|
||||||
|
bool isHidden{false};
|
||||||
|
bool isRemoved{false};
|
||||||
QString publicKey;
|
QString publicKey;
|
||||||
bool isRemoved = false;
|
QString mixedcaseAddress;
|
||||||
bool isWallet = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
using ChatOrWalletAccounts = std::vector<ChatOrWalletAccount>;
|
using ChatOrWalletAccounts = std::vector<ChatOrWalletAccount>;
|
||||||
|
|
|
@ -23,7 +23,7 @@ namespace Status::StatusGo::Wallet {
|
||||||
*/
|
*/
|
||||||
struct DerivedAddress
|
struct DerivedAddress
|
||||||
{
|
{
|
||||||
Accounts::EOAddress address;
|
Accounts::EOAddress address;
|
||||||
Accounts::DerivationPath path;
|
Accounts::DerivationPath path;
|
||||||
bool hasActivity = false;
|
bool hasActivity = false;
|
||||||
bool alreadyCreated = false;
|
bool alreadyCreated = false;
|
||||||
|
|
|
@ -11,6 +11,7 @@ qt6_add_qml_module(${PROJECT_NAME}
|
||||||
VERSION 1.0
|
VERSION 1.0
|
||||||
|
|
||||||
QML_FILES
|
QML_FILES
|
||||||
|
|
||||||
StatusBanner.qml
|
StatusBanner.qml
|
||||||
|
|
||||||
# Required to suppress "qmllint may not work" warning
|
# Required to suppress "qmllint may not work" warning
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
||||||
/*!
|
/// Template for application section content
|
||||||
Template for application section content
|
|
||||||
*/
|
|
||||||
Item {
|
Item {
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import QtQml
|
import QtQml
|
||||||
|
|
||||||
/*!
|
/// An application section with button and content view
|
||||||
An application section with button and content view
|
|
||||||
*/
|
|
||||||
QtObject {
|
QtObject {
|
||||||
required property NavigationBarButtonComponent navButton
|
/// \c NavigationBarSection
|
||||||
required property ApplicationContentView content
|
required property Component navigationSection
|
||||||
|
|
||||||
component NavigationBarButtonComponent: NavigationBarButton {}
|
/// \c ApplicationContentView
|
||||||
component ApplicationContentViewComponent: ApplicationContentView {}
|
required property Component content
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,8 @@ qt6_add_qml_module(${PROJECT_NAME}
|
||||||
ApplicationState.qml
|
ApplicationState.qml
|
||||||
MacTrafficLights.qml
|
MacTrafficLights.qml
|
||||||
NavigationBar.qml
|
NavigationBar.qml
|
||||||
NavigationBarButton.qml
|
NavigationBarSection.qml
|
||||||
|
PanelAndContentBase.qml
|
||||||
|
|
||||||
# Required to suppress "qmllint may not work" warning
|
# Required to suppress "qmllint may not work" warning
|
||||||
OUTPUT_DIRECTORY
|
OUTPUT_DIRECTORY
|
||||||
|
|
|
@ -4,6 +4,7 @@ import QtQuick.Controls
|
||||||
import Status.Core.Theme
|
import Status.Core.Theme
|
||||||
import Status.Assets
|
import Status.Assets
|
||||||
|
|
||||||
|
/// MacOS window decoration for QML. To be used when the title-bar is hidden.
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,57 @@
|
||||||
|
import QtQml
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
|
||||||
/*!
|
import Status.Controls.Navigation
|
||||||
Template for side NavigationBar
|
|
||||||
|
|
||||||
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 {
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
implicitWidth: 78
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
import QtQuick
|
|
||||||
|
|
||||||
/*!
|
|
||||||
Template for a NavigationBar square button
|
|
||||||
*/
|
|
||||||
Item {
|
|
||||||
height: width
|
|
||||||
}
|
|
|
@ -1,7 +1,9 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
||||||
/*!
|
/// Template for a navigation bar section
|
||||||
Template for a Navigation Bar section
|
|
||||||
*/
|
|
||||||
Item {
|
Item {
|
||||||
|
readonly property int sideMargin: 18
|
||||||
|
property bool selected: false
|
||||||
|
|
||||||
|
implicitWidth: 78
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
ApplicationContentView {
|
||||||
|
readonly property int panelWidth: 304
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
cmake_minimum_required(VERSION 3.5)
|
cmake_minimum_required(VERSION 3.21)
|
||||||
|
|
||||||
project(TestStatusQ LANGUAGES CXX)
|
project(TestStatusQ LANGUAGES CXX)
|
||||||
|
|
||||||
|
@ -7,17 +7,9 @@ enable_testing(true)
|
||||||
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
|
set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true)
|
||||||
|
|
||||||
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml QuickTest REQUIRED)
|
find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Quick Qml QuickTest REQUIRED)
|
||||||
qt6_standard_project_setup()
|
|
||||||
|
|
||||||
qt6_add_qml_module(${PROJECT_NAME}
|
add_executable(TestStatusQ
|
||||||
URI Status.TestHelpers
|
"main.cpp"
|
||||||
VERSION 1.0
|
|
||||||
|
|
||||||
QML_FILES
|
|
||||||
|
|
||||||
# Required to suppress "qmllint may not work" warning
|
|
||||||
OUTPUT_DIRECTORY
|
|
||||||
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Status/TestHelpers
|
|
||||||
)
|
)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
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
|
# 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_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_test(NAME TestStatusQ WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} COMMAND ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/TestStatusQ -input "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||||
add_dependencies("Run_${PROJECT_NAME}" ${PROJECT_NAME})
|
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}
|
target_include_directories(${PROJECT_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
|
@ -37,7 +30,7 @@ target_include_directories(${PROJECT_NAME}
|
||||||
|
|
||||||
add_subdirectory(TestHelpers)
|
add_subdirectory(TestHelpers)
|
||||||
|
|
||||||
target_link_libraries(${PROJECT_NAME} PRIVATE
|
target_link_libraries(TestStatusQ PRIVATE
|
||||||
Qt6::QuickTest
|
Qt6::QuickTest
|
||||||
Qt6::Qml
|
Qt6::Qml
|
||||||
Qt6::Quick
|
Qt6::Quick
|
||||||
|
|
|
@ -1,26 +1,3 @@
|
||||||
#include <QtQuickTest/quicktest.h>
|
#include <QtQuickTest>
|
||||||
#include <QQmlEngine>
|
|
||||||
|
|
||||||
#include "TestHelpers/MonitorQtOutput.h"
|
QUICK_TEST_MAIN(TestOnboardingQml)
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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>;
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
|
@ -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>;
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
|
@ -0,0 +1,14 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/// WalletAccount
|
||||||
|
required property var asset
|
||||||
|
|
||||||
|
Label {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "$$$$$"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,9 +3,12 @@
|
||||||
#include <StatusGo/Accounts/Accounts.h>
|
#include <StatusGo/Accounts/Accounts.h>
|
||||||
|
|
||||||
#include <Onboarding/Common/Constants.h>
|
#include <Onboarding/Common/Constants.h>
|
||||||
|
#include <Onboarding/Accounts/AccountsService.h>
|
||||||
#include <Onboarding/OnboardingController.h>
|
#include <Onboarding/OnboardingController.h>
|
||||||
#include <StatusGo/Utils.h>
|
#include <StatusGo/Utils.h>
|
||||||
|
|
||||||
|
#include <StatusGo/SignalsManager.h>
|
||||||
|
|
||||||
#include <IOTestHelpers.h>
|
#include <IOTestHelpers.h>
|
||||||
#include <ScopedTestAccount.h>
|
#include <ScopedTestAccount.h>
|
||||||
|
|
||||||
|
@ -25,12 +28,10 @@ namespace Status::Testing {
|
||||||
TEST(AccountsAPI, TestGetAccounts)
|
TEST(AccountsAPI, TestGetAccounts)
|
||||||
{
|
{
|
||||||
constexpr auto testAccountName = "test_get_accounts_name";
|
constexpr auto testAccountName = "test_get_accounts_name";
|
||||||
constexpr auto testAccountPassword = "password*";
|
ScopedTestAccount testAccount(test_info_->name(), testAccountName);
|
||||||
ScopedTestAccount testAccount(test_info_->name(), testAccountName, testAccountPassword, true);
|
|
||||||
|
|
||||||
const auto accounts = Accounts::getAccounts();
|
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; });
|
const auto chatIt = std::find_if(accounts.begin(), accounts.end(), [](const auto& a) { return a.isChat; });
|
||||||
ASSERT_NE(chatIt, accounts.end());
|
ASSERT_NE(chatIt, accounts.end());
|
||||||
|
@ -47,47 +48,79 @@ TEST(AccountsAPI, TestGetAccounts)
|
||||||
ASSERT_TRUE(walletAccount.derivedFrom.has_value());
|
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 testRootAccountName = "test-generate_account_with_derived_path-name";
|
||||||
constexpr auto testAccountPassword = "password*";
|
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName);
|
||||||
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true);
|
|
||||||
|
|
||||||
auto password{Utils::hashPassword(testAccountPassword)};
|
const auto newTestWalletAccountNameBase = u"test_generated_new_wallet_account-name"_qs;
|
||||||
const auto newTestAccountName = u"test_generated_new_account-name"_qs;
|
|
||||||
const auto newTestAccountColor = QColor("fuchsia");
|
const auto newTestAccountColor = QColor("fuchsia");
|
||||||
const auto newTestAccountEmoji = u""_qs;
|
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();
|
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(),
|
const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(),
|
||||||
[newTestAccountName = std::as_const(newTestAccountName)](const auto& a) {
|
[&newTestWalletAccountName](const auto& a) {
|
||||||
return a.name == newTestAccountName;
|
return a.name == newTestWalletAccountName;
|
||||||
});
|
});
|
||||||
ASSERT_NE(newAccountIt, updatedAccounts.end());
|
ASSERT_EQ(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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(AccountsAPI, TestGenerateAccountWithDerivedPath_WrongPassword)
|
TEST(AccountsAPI, TestGenerateAccountWithDerivedPath_WrongPassword)
|
||||||
{
|
{
|
||||||
constexpr auto testRootAccountName = "test-generate_account_with_derived_path-name";
|
constexpr auto testRootAccountName = "test-generate_account_with_derived_path-name";
|
||||||
constexpr auto testAccountPassword = "password*";
|
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName);
|
||||||
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true);
|
|
||||||
|
|
||||||
const auto chatAccount = testAccount.firstChatAccount();
|
const auto chatAccount = testAccount.firstChatAccount();
|
||||||
try {
|
try {
|
||||||
|
@ -108,23 +141,21 @@ TEST(AccountsAPI, TestGenerateAccountWithDerivedPath_WrongPassword)
|
||||||
TEST(AccountsAPI, TestAddAccountWithMnemonicAndPath)
|
TEST(AccountsAPI, TestAddAccountWithMnemonicAndPath)
|
||||||
{
|
{
|
||||||
constexpr auto testRootAccountName = "test_root_account-name";
|
constexpr auto testRootAccountName = "test_root_account-name";
|
||||||
constexpr auto testAccountPassword = "password*";
|
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName);
|
||||||
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true);
|
|
||||||
|
|
||||||
auto password{Utils::hashPassword(testAccountPassword)};
|
|
||||||
const auto newTestAccountName = u"test_import_from_mnemonic-name"_qs;
|
const auto newTestAccountName = u"test_import_from_mnemonic-name"_qs;
|
||||||
const auto newTestAccountColor = QColor("fuchsia");
|
const auto newTestAccountColor = QColor("fuchsia");
|
||||||
const auto newTestAccountEmoji = u""_qs;
|
const auto newTestAccountEmoji = u""_qs;
|
||||||
const auto newTestAccountPath = Status::Constants::General::PathWalletRoot;
|
const auto newTestAccountPath = Status::Constants::General::PathWalletRoot;
|
||||||
|
|
||||||
Accounts::addAccountWithMnemonicAndPath("festival october control quarter husband dish throw couch depth stadium cigar whisper",
|
Accounts::addAccountWithMnemonicAndPath("festival october control quarter husband dish throw couch depth stadium cigar whisper",
|
||||||
password, newTestAccountName, newTestAccountColor, newTestAccountEmoji,
|
testAccount.hashedPassword(), newTestAccountName, newTestAccountColor, newTestAccountEmoji,
|
||||||
newTestAccountPath);
|
newTestAccountPath);
|
||||||
const auto updatedAccounts = Accounts::getAccounts();
|
const auto updatedAccounts = Accounts::getAccounts();
|
||||||
ASSERT_EQ(updatedAccounts.size(), 3);
|
ASSERT_EQ(updatedAccounts.size(), 3);
|
||||||
|
|
||||||
const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(),
|
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;
|
return a.name == newTestAccountName;
|
||||||
});
|
});
|
||||||
ASSERT_NE(newAccountIt, updatedAccounts.end());
|
ASSERT_NE(newAccountIt, updatedAccounts.end());
|
||||||
|
@ -144,10 +175,8 @@ TEST(AccountsAPI, TestAddAccountWithMnemonicAndPath)
|
||||||
TEST(AccountsAPI, TestAddAccountWithMnemonicAndPath_WrongMnemonicWorks)
|
TEST(AccountsAPI, TestAddAccountWithMnemonicAndPath_WrongMnemonicWorks)
|
||||||
{
|
{
|
||||||
constexpr auto testRootAccountName = "test_root_account-name";
|
constexpr auto testRootAccountName = "test_root_account-name";
|
||||||
constexpr auto testAccountPassword = "password*";
|
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName);
|
||||||
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true);
|
|
||||||
|
|
||||||
auto password{Utils::hashPassword(testAccountPassword)};
|
|
||||||
const auto newTestAccountName = u"test_import_from_wrong_mnemonic-name"_qs;
|
const auto newTestAccountName = u"test_import_from_wrong_mnemonic-name"_qs;
|
||||||
const auto newTestAccountColor = QColor("fuchsia");
|
const auto newTestAccountColor = QColor("fuchsia");
|
||||||
const auto newTestAccountEmoji = u""_qs;
|
const auto newTestAccountEmoji = u""_qs;
|
||||||
|
@ -155,14 +184,14 @@ TEST(AccountsAPI, TestAddAccountWithMnemonicAndPath_WrongMnemonicWorks)
|
||||||
|
|
||||||
// Added an inexistent word. The mnemonic is not checked.
|
// Added an inexistent word. The mnemonic is not checked.
|
||||||
Accounts::addAccountWithMnemonicAndPath("october control quarter husband dish throw couch depth stadium cigar waku",
|
Accounts::addAccountWithMnemonicAndPath("october control quarter husband dish throw couch depth stadium cigar waku",
|
||||||
password, newTestAccountName, newTestAccountColor, newTestAccountEmoji,
|
testAccount.hashedPassword(), newTestAccountName, newTestAccountColor, newTestAccountEmoji,
|
||||||
newTestAccountPath);
|
newTestAccountPath);
|
||||||
|
|
||||||
const auto updatedAccounts = Accounts::getAccounts();
|
const auto updatedAccounts = Accounts::getAccounts();
|
||||||
ASSERT_EQ(updatedAccounts.size(), 3);
|
ASSERT_EQ(updatedAccounts.size(), 3);
|
||||||
|
|
||||||
const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(),
|
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;
|
return a.name == newTestAccountName;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -182,8 +211,7 @@ TEST(AccountsAPI, TestAddAccountWithMnemonicAndPath_WrongMnemonicWorks)
|
||||||
TEST(AccountsAPI, TestAddAccountWatch)
|
TEST(AccountsAPI, TestAddAccountWatch)
|
||||||
{
|
{
|
||||||
constexpr auto testRootAccountName = "test_root_account-name";
|
constexpr auto testRootAccountName = "test_root_account-name";
|
||||||
constexpr auto testAccountPassword = "password*";
|
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName);
|
||||||
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true);
|
|
||||||
|
|
||||||
const auto newTestAccountName = u"test_watch_only-name"_qs;
|
const auto newTestAccountName = u"test_watch_only-name"_qs;
|
||||||
const auto newTestAccountColor = QColor("fuchsia");
|
const auto newTestAccountColor = QColor("fuchsia");
|
||||||
|
@ -194,7 +222,7 @@ TEST(AccountsAPI, TestAddAccountWatch)
|
||||||
ASSERT_EQ(updatedAccounts.size(), 3);
|
ASSERT_EQ(updatedAccounts.size(), 3);
|
||||||
|
|
||||||
const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(),
|
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;
|
return a.name == newTestAccountName;
|
||||||
});
|
});
|
||||||
ASSERT_NE(newAccountIt, updatedAccounts.end());
|
ASSERT_NE(newAccountIt, updatedAccounts.end());
|
||||||
|
@ -213,8 +241,7 @@ TEST(AccountsAPI, TestAddAccountWatch)
|
||||||
TEST(AccountsAPI, TestDeleteAccount)
|
TEST(AccountsAPI, TestDeleteAccount)
|
||||||
{
|
{
|
||||||
constexpr auto testRootAccountName = "test_root_account-name";
|
constexpr auto testRootAccountName = "test_root_account-name";
|
||||||
constexpr auto testAccountPassword = "password*";
|
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName);
|
||||||
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true);
|
|
||||||
|
|
||||||
const auto newTestAccountName = u"test_account_to_delete-name"_qs;
|
const auto newTestAccountName = u"test_account_to_delete-name"_qs;
|
||||||
const auto newTestAccountColor = QColor("fuchsia");
|
const auto newTestAccountColor = QColor("fuchsia");
|
||||||
|
@ -225,7 +252,7 @@ TEST(AccountsAPI, TestDeleteAccount)
|
||||||
ASSERT_EQ(updatedAccounts.size(), 3);
|
ASSERT_EQ(updatedAccounts.size(), 3);
|
||||||
|
|
||||||
const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(),
|
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;
|
return a.name == newTestAccountName;
|
||||||
});
|
});
|
||||||
ASSERT_NE(newAccountIt, updatedAccounts.end());
|
ASSERT_NE(newAccountIt, updatedAccounts.end());
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
#include <Onboarding/OnboardingController.h>
|
#include <Onboarding/OnboardingController.h>
|
||||||
|
|
||||||
#include <IOTestHelpers.h>
|
#include <IOTestHelpers.h>
|
||||||
#include <ScopedTestAccount.h>
|
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#include <StatusGo/Metadata/api_response.h>
|
#include <StatusGo/Metadata/api_response.h>
|
||||||
|
|
||||||
#include <Onboarding/Accounts/AccountsServiceInterface.h>
|
#include <Onboarding/Accounts/AccountsServiceInterface.h>
|
||||||
|
#include <Onboarding/Accounts/AccountsService.h>
|
||||||
#include <Onboarding/Common/Constants.h>
|
#include <Onboarding/Common/Constants.h>
|
||||||
#include <Onboarding/OnboardingController.h>
|
#include <Onboarding/OnboardingController.h>
|
||||||
|
|
||||||
|
@ -12,7 +13,9 @@
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
namespace Wallet = Status::StatusGo::Wallet;
|
namespace Wallet = Status::StatusGo::Wallet;
|
||||||
|
namespace Accounts = Status::StatusGo::Accounts;
|
||||||
namespace Utils = Status::StatusGo::Utils;
|
namespace Utils = Status::StatusGo::Utils;
|
||||||
|
namespace General = Status::Constants::General;
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
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
|
/// \todo after status-go API coverage all the integration tests should go away and only test the thin wrapper code
|
||||||
namespace Status::Testing {
|
namespace Status::Testing {
|
||||||
|
|
||||||
TEST(WalletApi, TestGetDerivedAddressesForPath)
|
TEST(WalletApi, TestGetDerivedAddressesForPath_FromRootAccount)
|
||||||
{
|
{
|
||||||
constexpr auto testRootAccountName = "test_root_account-name";
|
constexpr auto testRootAccountName = "test_root_account-name";
|
||||||
constexpr auto testAccountPassword = "password*";
|
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName);
|
||||||
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName, testAccountPassword, true);
|
|
||||||
|
|
||||||
const auto walletAccount = testAccount.firstWalletAccount();
|
const auto walletAccount = testAccount.firstWalletAccount();
|
||||||
const auto chatAccount = testAccount.firstChatAccount();
|
|
||||||
const auto rootAccount = testAccount.onboardingController()->accountsService()->getLoggedInAccount();
|
const auto rootAccount = testAccount.onboardingController()->accountsService()->getLoggedInAccount();
|
||||||
ASSERT_EQ(rootAccount.address, walletAccount.derivedFrom.value());
|
ASSERT_EQ(rootAccount.address, walletAccount.derivedFrom.value());
|
||||||
|
|
||||||
const auto password{Utils::hashPassword(testAccountPassword)};
|
const auto testPath = General::PathWalletRoot;
|
||||||
const auto testPath = Status::Constants::General::PathWalletRoot;
|
|
||||||
|
|
||||||
// chatAccount.address
|
const auto derivedAddresses = Wallet::getDerivedAddressesForPath(testAccount.hashedPassword(),
|
||||||
const auto chatDerivedAddresses = Wallet::getDerivedAddressesForPath(password, chatAccount.address, testPath, 3, 1);
|
walletAccount.derivedFrom.value(), testPath, 3, 1);
|
||||||
// Check that no change is done
|
// Check that accounts are generated in memory and none is saved
|
||||||
const auto updatedAccounts = Accounts::getAccounts();
|
const auto updatedAccounts = Accounts::getAccounts();
|
||||||
ASSERT_EQ(updatedAccounts.size(), 2);
|
ASSERT_EQ(updatedAccounts.size(), 2);
|
||||||
|
|
||||||
ASSERT_EQ(chatDerivedAddresses.size(), 3);
|
ASSERT_EQ(derivedAddresses.size(), 3);
|
||||||
// all alreadyCreated are false
|
auto defaultWalletAccountIt = std::find_if(derivedAddresses.begin(), derivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; });
|
||||||
ASSERT_TRUE(std::none_of(chatDerivedAddresses.begin(), chatDerivedAddresses.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
|
// 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
|
// 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);
|
TEST(Accounts, TestGetDerivedAddressesForPath_AfterLogin)
|
||||||
ASSERT_EQ(walletDerivedAddresses.size(), 2);
|
{
|
||||||
|
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
|
// 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);
|
TEST(WalletApi, TestGetDerivedAddressesForPath_FromWalletAccount_SecondLevel)
|
||||||
ASSERT_EQ(rootDerivedAddresses.size(), 4);
|
{
|
||||||
ASSERT_EQ(std::count_if(rootDerivedAddresses.begin(), rootDerivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; }), 1);
|
constexpr auto testRootAccountName = "test_root_account-name";
|
||||||
const auto &existingAddress = *std::find_if(rootDerivedAddresses.begin(), rootDerivedAddresses.end(), [](const auto& a) { return a.alreadyCreated; });
|
ScopedTestAccount testAccount(test_info_->name(), testRootAccountName);
|
||||||
ASSERT_EQ(existingAddress.address, walletAccount.address);
|
|
||||||
ASSERT_FALSE(existingAddress.hasActivity);
|
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
|
} // namespace
|
||||||
|
|
Loading…
Reference in New Issue