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