Merge 753a92fb810267eaeb7d79e8c84d8888f64f5d4f into da1b021db5315eccd1e39835268bbc08633adc12

This commit is contained in:
Sergio Chouhy 2026-07-25 05:18:25 +00:00 committed by GitHub
commit 8538c8eaa6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 16989 additions and 3716 deletions

19516
flake.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
nix-bundle-lgx.url = "github:logos-co/nix-bundle-lgx";
lez_core.url = "github:logos-blockchain/logos-execution-zone-module?rev=1d4db06ebcdd4879a4395efbe4993a15992a261b";
lez_core.url = "github:logos-blockchain/logos-execution-zone-module?rev=033f807a3b1690fb2f8e661cb8067646b8a98242";
};
outputs = inputs@{ logos-module-builder, ... }:

View File

@ -19,13 +19,27 @@ void LEZAccountFilterModel::setFilterByPublic(bool value)
emit countChanged();
}
void LEZAccountFilterModel::setOnlyInitialized(bool value)
{
if (m_onlyInitialized == value)
return;
m_onlyInitialized = value;
invalidateFilter();
emit onlyInitializedChanged();
emit countChanged();
}
bool LEZAccountFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
if (!sourceModel())
return false;
const QModelIndex idx = sourceModel()->index(sourceRow, 0, sourceParent);
const bool isPublic = sourceModel()->data(idx, LEZWalletAccountModel::IsPublicRole).toBool();
return isPublic == m_filterByPublic;
if (isPublic != m_filterByPublic)
return false;
if (m_onlyInitialized && !sourceModel()->data(idx, LEZWalletAccountModel::IsInitializedRole).toBool())
return false;
return true;
}
int LEZAccountFilterModel::rowForAddress(const QString& address) const

View File

@ -6,6 +6,7 @@
class LEZAccountFilterModel : public QSortFilterProxyModel {
Q_OBJECT
Q_PROPERTY(bool filterByPublic READ filterByPublic WRITE setFilterByPublic NOTIFY filterByPublicChanged)
Q_PROPERTY(bool onlyInitialized READ onlyInitialized WRITE setOnlyInitialized NOTIFY onlyInitializedChanged)
Q_PROPERTY(int count READ count NOTIFY countChanged)
public:
@ -14,6 +15,13 @@ public:
bool filterByPublic() const { return m_filterByPublic; }
void setFilterByPublic(bool value);
// When true, uninitialized accounts are excluded. Used for the account-picker
// combo boxes (transfer/withdraw "from"/"to" fields) where an uninitialized
// account can't yet be a valid sender or recipient — as opposed to AccountsPanel's
// unfiltered accountModel, which must keep showing them so the user can initialize.
bool onlyInitialized() const { return m_onlyInitialized; }
void setOnlyInitialized(bool value);
int count() const { return rowCount(); }
Q_INVOKABLE int rowForAddress(const QString& address) const;
@ -23,8 +31,10 @@ protected:
signals:
void filterByPublicChanged();
void onlyInitializedChanged();
void countChanged();
private:
bool m_filterByPublic = true;
bool m_onlyInitialized = false;
};

View File

@ -31,6 +31,8 @@ QVariant LEZWalletAccountModel::data(const QModelIndex& index, int role) const
case SectionKeyRole: return e.sectionKey;
case KeysJsonRole: return e.keysJson;
case IsFirstInGroupRole: return e.isFirstInGroup;
case IsFirstPrivateRole: return e.isFirstPrivate;
case IsInitializedRole: return e.isInitialized;
default: return QVariant();
}
}
@ -45,7 +47,9 @@ QHash<int, QByteArray> LEZWalletAccountModel::roleNames() const
{ IsPublicRole, "isPublic" },
{ SectionKeyRole, "sectionKey" },
{ KeysJsonRole, "keysJson" },
{ IsFirstInGroupRole, "isFirstInGroup" }
{ IsFirstInGroupRole, "isFirstInGroup" },
{ IsFirstPrivateRole, "isFirstPrivate" },
{ IsInitializedRole, "isInitialized" }
};
}
@ -71,6 +75,8 @@ void LEZWalletAccountModel::replaceFromVariantList(const QVariantList& list)
const QVariantMap map = v.toMap();
e.accountId = map.value(QStringLiteral("account_id")).toString();
e.isPublic = map.value(QStringLiteral("is_public"), true).toBool();
e.isInitialized = map.value(QStringLiteral("is_initialized"), false).toBool();
e.name = map.value(QStringLiteral("name")).toString();
if (e.isPublic) {
e.sectionKey = PublicSectionKey;
} else {
@ -96,8 +102,13 @@ void LEZWalletAccountModel::replaceFromVariantList(const QVariantList& list)
if (a.isPublic != b.isPublic) return a.isPublic;
return a.sectionKey < b.sectionKey;
});
for (int i = 0; i < m_entries.size(); ++i)
for (int i = 0; i < m_entries.size(); ++i) {
m_entries[i].isFirstInGroup = (i == 0) || (m_entries[i].sectionKey != m_entries[i - 1].sectionKey);
// All private key-groups sit under a single "Private" title (unlike the public
// section, they don't each get their own top-level header) — so only the very
// first private row across all groups needs to flag it.
m_entries[i].isFirstPrivate = !m_entries[i].isPublic && (i == 0 || m_entries[i - 1].isPublic);
}
endResetModel();
if (oldCount != m_entries.size())
emit countChanged();

View File

@ -18,7 +18,12 @@ struct LEZWalletAccountEntry {
bool isPublic = true;
QString sectionKey;
QString keysJson; // {nullifier_public_key, viewing_public_key} shared by the whole section; private only
bool isFirstInGroup = false; // QML renders the section header above rows where this is true
bool isFirstInGroup = false; // QML renders the per-key-set header (copy button) above rows where this is true
bool isFirstPrivate = false; // QML renders the single "Private" section title above rows where this is true
// Whether some program (in practice, the authenticated-transfer program) has claimed
// this account yet. Defaults to false (shown as needing init) so an account whose
// state we failed to enrich isn't silently mistaken for a usable one.
bool isInitialized = false;
};
// Note: this model is exposed to QML via Qt Remote Objects model replication (see
@ -37,7 +42,9 @@ public:
IsPublicRole,
SectionKeyRole,
KeysJsonRole,
IsFirstInGroupRole
IsFirstInGroupRole,
IsFirstPrivateRole,
IsInitializedRole
};
Q_ENUM(Role)

View File

@ -1,5 +1,6 @@
#include "LEZWalletBackend.h"
#include <algorithm>
#include <QAbstractItemModel>
#include <QClipboard>
#include <QCoreApplication>
@ -57,6 +58,21 @@ namespace {
return QUrl::fromUserInput(p).toLocalFile();
return p;
}
// An account is uninitialized until some program claims it (program_owner goes
// from all-zero to that program's ID) — see DEFAULT_PROGRAM_ID in the execution
// zone's state machine. Accounts this wallet creates are only ever claimed by the
// authenticated-transfer program (via an explicit init or as a side effect of
// receiving a transfer to a still-unclaimed account), so "non-zero owner" is
// enough to show as initialized without needing that program's ID here.
bool accountJsonIsInitialized(const QString& accountJson) {
const QJsonDocument doc = QJsonDocument::fromJson(accountJson.toUtf8());
if (!doc.isObject())
return false;
const QString programOwner = doc.object().value(QStringLiteral("program_owner")).toString();
return std::any_of(programOwner.cbegin(), programOwner.cend(),
[](QChar c) { return c != QLatin1Char('0'); });
}
}
LEZWalletBackend::LEZWalletBackend(LogosAPI* logosAPI, QObject* parent)
@ -68,8 +84,13 @@ LEZWalletBackend::LEZWalletBackend(LogosAPI* logosAPI, QObject* parent)
m_logosAPI(logosAPI ? logosAPI : new LogosAPI("lez_wallet_ui", this)),
m_logos(new LogosModules(m_logosAPI))
{
// Both feed the transfer/withdraw "from"/"to" account-picker combo boxes, where an
// uninitialized account isn't a usable sender or recipient — unlike m_accountModel
// (unfiltered), which AccountsPanel needs to keep showing them on for initialization.
m_filteredAccountModel->setOnlyInitialized(true);
m_filteredAccountModel->setSourceModel(m_accountModel);
m_privateAccountModel->setFilterByPublic(false);
m_privateAccountModel->setOnlyInitialized(true);
m_privateAccountModel->setSourceModel(m_accountModel);
m_claimableAccountModel->setSourceModel(m_accountModel);
@ -165,8 +186,9 @@ QVariantList LEZWalletBackend::buildEnrichedAccountList()
enriched.reserve(raw.size());
for (const QVariant& v : raw) {
QVariantMap map = v.toMap();
if (!map.value(QStringLiteral("is_public"), true).toBool()) {
const QString accountId = map.value(QStringLiteral("account_id")).toString();
const QString accountId = map.value(QStringLiteral("account_id")).toString();
const bool isPublic = map.value(QStringLiteral("is_public"), true).toBool();
if (!isPublic) {
const QString keysJson = getPrivateAccountKeys(accountId);
const QJsonDocument doc = QJsonDocument::fromJson(keysJson.toUtf8());
if (doc.isObject()) {
@ -174,6 +196,13 @@ QVariantList LEZWalletBackend::buildEnrichedAccountList()
map[QStringLiteral("keys_json")] = keysJson;
}
}
const QString accountJson = isPublic
? m_logos->lez_core.get_account_public(accountId)
: m_logos->lez_core.get_account_private(accountId);
map[QStringLiteral("is_initialized")] = accountJsonIsInitialized(accountJson);
const QStringList labels = m_logos->lez_core.get_all_labels_for_account(accountId, !isPublic);
if (!labels.isEmpty())
map[QStringLiteral("name")] = labels.join(QStringLiteral(", "));
enriched.append(map);
}
return enriched;
@ -305,6 +334,25 @@ QString LEZWalletBackend::getPrivateAccountKeys(QString accountIdHex)
return m_logos->lez_core.get_private_account_keys(accountIdHex);
}
QString LEZWalletBackend::initializeAccount(QString accountIdHex, bool isPublic)
{
// Public registration is a plain public tx (like transferPublic, no proof needed),
// so the generated accessor's default timeout is fine. Private registration
// generates a proof, like transferPrivate/vaultClaim above, so it goes through
// invokeRemoteMethod with NO_TIMEOUT instead.
const QString result = isPublic
? m_logos->lez_core.register_public_account(accountIdHex)
: m_logosAPI->getClient(LEZ_MODULE)->invokeRemoteMethod(
LEZ_MODULE, "register_private_account",
QVariantList{accountIdHex.trimmed()},
NO_TIMEOUT).toString();
const QJsonDocument doc = QJsonDocument::fromJson(result.toUtf8());
if (doc.isObject() && doc.object().value(QStringLiteral("success")).toBool())
refreshAccounts();
return result;
}
bool LEZWalletBackend::syncToBlock(quint64 blockId)
{
int err = m_logos->lez_core.sync_to_block(blockId);
@ -456,6 +504,10 @@ QString LEZWalletBackend::createNew(QString configPath, QString storagePath, QSt
const QString localConfigPath = toLocalPath(configPath);
const QString localStoragePath = toLocalPath(storagePath);
// Clear any mnemonic left over from a previous attempt in this session —
// only a freshly generated one (below) should ever be shown to the user.
setLastCreatedMnemonic(QString());
// Both files already on disk: this is most likely an existing wallet the
// user pointed us at (e.g. from the setup screen), not a request to
// overwrite it. Try to load it instead of blindly creating a new one.
@ -476,11 +528,73 @@ QString LEZWalletBackend::createNew(QString configPath, QString storagePath, QSt
if (!sequencerAddr.isEmpty())
applySequencerAddrToConfig(localConfigPath, sequencerAddr);
// create_new() writes storage.json directly to this path — unlike the
// config path (handled inside applySequencerAddrToConfig above), nothing
// else ensures its parent directory exists, so wallet_ffi_create_new()
// silently fails whenever storagePath points into a not-yet-created folder.
QDir().mkpath(QFileInfo(localStoragePath).absolutePath());
const QString mnemonic = m_logos->lez_core.create_new(
localConfigPath, localStoragePath, password);
if (mnemonic.isEmpty())
return QStringLiteral("Failed to create wallet. Check paths and try again.");
// Surfaced to the QML layer via the lastCreatedMnemonic PROP so the setup
// screen can show a one-time "save your recovery phrase" prompt; cleared
// once the user acknowledges it (see clearLastCreatedMnemonic()).
setLastCreatedMnemonic(mnemonic);
persistConfigPath(localConfigPath);
persistStoragePath(localStoragePath);
finishOpeningWallet();
return QString();
}
QString LEZWalletBackend::restoreFromMnemonic(QString configPath, QString storagePath, QString mnemonic, QString password, QString sequencerAddr)
{
// Depth of the key tree to reconstruct per keychain (public/private) when
// re-deriving from the mnemonic. NOT a linear BIP44-style gap limit — the
// wallet FFI builds an actual tree of 2^depth candidate keys at this depth
// and docs explicitly warn it "induces exponential growth in execution
// time". Keep this small; anything much bigger can take a very long time
// (or effectively hang) before accounts are found and the wallet opens.
constexpr int RESTORE_SCAN_DEPTH = 5;
const QString localConfigPath = toLocalPath(configPath);
const QString localStoragePath = toLocalPath(storagePath);
// The user already has this mnemonic (they just typed it in), so there's
// nothing new to show — and any leftover from a prior createNew() in this
// session shouldn't linger either.
setLastCreatedMnemonic(QString());
if (QFile::exists(localConfigPath) || QFile::exists(localStoragePath)) {
return QStringLiteral(
"A wallet already exists at the selected paths. Choose paths "
"that don't exist yet to restore a wallet there.");
}
if (!sequencerAddr.isEmpty())
applySequencerAddrToConfig(localConfigPath, sequencerAddr);
// Same not-yet-created-folder gap as createNew() above — make sure the
// storage path's parent directory exists before create_new() tries to
// write storage.json there.
QDir().mkpath(QFileInfo(localStoragePath).absolutePath());
// restore_storage() needs an already-open wallet handle to restore into,
// so first create fresh (empty) storage at the chosen paths, then
// overwrite its key material by re-deriving from the given mnemonic.
const QString created = m_logos->lez_core.create_new(
localConfigPath, localStoragePath, password);
if (created.isEmpty())
return QStringLiteral("Failed to initialize wallet storage. Check paths and try again.");
const int err = m_logos->lez_core.restore_storage(
mnemonic.trimmed(), password, QVariant(RESTORE_SCAN_DEPTH));
if (err != WALLET_FFI_SUCCESS)
return QStringLiteral("Failed to restore wallet. Check the recovery phrase and try again.");
persistConfigPath(localConfigPath);
persistStoragePath(localStoragePath);
finishOpeningWallet();
@ -492,3 +606,27 @@ void LEZWalletBackend::copyToClipboard(QString text)
if (QGuiApplication::clipboard())
QGuiApplication::clipboard()->setText(text);
}
void LEZWalletBackend::clearLastCreatedMnemonic()
{
setLastCreatedMnemonic(QString());
}
bool LEZWalletBackend::checkLabelAvailable(QString label)
{
return m_logos->lez_core.check_label_available(label.trimmed());
}
QString LEZWalletBackend::addLabel(QString label, QString accountIdHex, bool isPublic)
{
const QString trimmedLabel = label.trimmed();
if (trimmedLabel.isEmpty())
return QStringLiteral("Error: Label cannot be empty.");
const int err = m_logos->lez_core.add_label(trimmedLabel, accountIdHex.trimmed(), !isPublic);
if (err != WALLET_FFI_SUCCESS)
return QStringLiteral("Error: wallet FFI error %1").arg(err);
refreshAccounts();
return QString();
}

View File

@ -41,6 +41,7 @@ public slots:
void refreshBalances() override;
QString getPublicAccountKey(QString accountIdHex) override;
QString getPrivateAccountKeys(QString accountIdHex) override;
QString initializeAccount(QString accountIdHex, bool isPublic) override;
bool syncToBlock(quint64 blockId) override;
QString transferPublic(QString fromHex, QString toHex, QString amountStr) override;
QString transferPrivate(QString fromHex, QString toHex, QString amountStr) override;
@ -52,7 +53,11 @@ public slots:
void refreshVaultBalances() override;
QString vaultClaim(QString fromHex, bool isPublic, QString amountStr) override;
QString createNew(QString configPath, QString storagePath, QString password, QString sequencerAddr) override;
QString restoreFromMnemonic(QString configPath, QString storagePath, QString mnemonic, QString password, QString sequencerAddr) override;
void copyToClipboard(QString text) override;
void clearLastCreatedMnemonic() override;
bool checkLabelAvailable(QString label) override;
QString addLabel(QString label, QString accountIdHex, bool isPublic) override;
private slots:
void syncNextChunk();

View File

@ -6,6 +6,7 @@ class LEZWalletBackend
PROP(int lastSyncedBlock READONLY)
PROP(int currentBlockHeight READONLY)
PROP(QString sequencerAddr READONLY)
PROP(QString lastCreatedMnemonic READONLY)
SLOT(QString createAccountPublic())
SLOT(QString createAccountPrivate())
@ -14,6 +15,7 @@ class LEZWalletBackend
SLOT(void refreshBalances())
SLOT(QString getPublicAccountKey(QString accountIdHex))
SLOT(QString getPrivateAccountKeys(QString accountIdHex))
SLOT(QString initializeAccount(QString accountIdHex, bool isPublic))
SLOT(bool syncToBlock(quint64 blockId))
SLOT(QString transferPublic(QString fromHex, QString toHex, QString amountStr))
@ -29,6 +31,11 @@ class LEZWalletBackend
SLOT(QString vaultClaim(QString fromHex, bool isPublic, QString amountStr))
SLOT(QString createNew(QString configPath, QString storagePath, QString password, QString sequencerAddr))
SLOT(QString restoreFromMnemonic(QString configPath, QString storagePath, QString mnemonic, QString password, QString sequencerAddr))
SLOT(void copyToClipboard(QString text))
SLOT(void clearLastCreatedMnemonic())
SLOT(bool checkLabelAvailable(QString label))
SLOT(QString addLabel(QString label, QString accountIdHex, bool isPublic))
}

View File

@ -5,6 +5,7 @@ import QtQuick.Layouts
import Logos.Theme
import Logos.Controls
import "views"
import "popups"
Rectangle {
id: root
@ -15,6 +16,9 @@ Rectangle {
readonly property var privateAccountModel: logos.model("lez_wallet_ui", "privateAccountModel")
readonly property var claimableAccountModel: logos.model("lez_wallet_ui", "claimableAccountModel")
property bool ready: false
// Maps accountId -> true while that account's initializeAccount() call is in flight,
// so AccountDelegate can show the click was registered instead of appearing inert.
property var pendingInitializations: ({})
Connections {
target: logos
@ -108,6 +112,77 @@ Rectangle {
visible: false
}
MnemonicRevealDialog {
id: mnemonicDialog
onCopyRequested: (text) => {
clipHelper.text = text
clipHelper.selectAll()
clipHelper.copy()
}
onAcknowledged: if (backend) backend.clearLastCreatedMnemonic()
}
Connections {
target: backend
function onLastCreatedMnemonicChanged() {
if (backend.lastCreatedMnemonic.length > 0) {
mnemonicDialog.mnemonic = backend.lastCreatedMnemonic
mnemonicDialog.open()
}
}
}
SetLabelDialog {
id: setLabelDialog
onCheckAvailabilityRequested: (label) => {
if (!backend) return
logos.watch(backend.checkLabelAvailable(label),
function(available) {
// The user may have kept typing while this round-trip was in
// flight only apply the result if it still matches the
// current text, otherwise it's stale.
if (label === setLabelDialog.trimmedText) {
setLabelDialog.checkingAvailability = false
setLabelDialog.labelAvailable = available
}
},
function(error) {
console.warn("checkLabelAvailable failed:", error)
if (label === setLabelDialog.trimmedText) {
setLabelDialog.checkingAvailability = false
// Fail open addLabel() itself still validates on submit.
setLabelDialog.labelAvailable = true
}
})
}
onSaveRequested: (accountId, isPublic, label) => {
if (!backend) {
setLabelDialog.reportSaveError(qsTr("Wallet backend unavailable."))
return
}
logos.watch(backend.addLabel(label, accountId, isPublic),
function(errorMessage) {
// The dialog may have been reopened for a different account while
// this round-trip was in flight only apply the result if it's
// still about the same account, otherwise it's stale.
if (setLabelDialog.accountId !== accountId || setLabelDialog.isPublic !== isPublic)
return
if (errorMessage)
setLabelDialog.reportSaveError(ffiErrors.format(errorMessage))
else
setLabelDialog.closeOnSaveSuccess()
},
function(error) {
if (setLabelDialog.accountId !== accountId || setLabelDialog.isPublic !== isPublic)
return
setLabelDialog.reportSaveError(error)
})
}
}
StackView {
id: stackView
anchors.fill: parent
@ -132,6 +207,20 @@ Rectangle {
}
)
}
onRecoverWallet: function(configPath, storagePath, mnemonic, password, sequencerUrl) {
if (!backend) return
// restoreFromMnemonic() returns an empty string on success, or a
// human-readable error message otherwise.
logos.watch(backend.restoreFromMnemonic(configPath, storagePath, mnemonic, password, sequencerUrl),
function(errorMessage) {
if (errorMessage)
createError = errorMessage
},
function(error) {
createError = qsTr("Error restoring wallet: %1").arg(error)
}
)
}
}
}
@ -145,13 +234,18 @@ Rectangle {
claimableAccountModel: root.claimableAccountModel
lastSyncedBlock: backend ? backend.lastSyncedBlock : 0
currentBlockHeight: backend ? backend.currentBlockHeight : 0
pendingInitializations: root.pendingInitializations
onCreatePublicAccountRequested: {
onCreatePublicAccountRequested: (initializeOnCreate) => {
if (!backend) { console.warn("backend is null"); return }
// Result not consumed here accountModel updates via NOTIFY when
// the backend's refreshAccounts() runs after creation.
// accountModel updates via NOTIFY when the backend's refreshAccounts()
// runs after creation; the id is only needed to chase it with an
// initializeAccount() call when the user asked for that.
logos.watch(backend.createAccountPublic(),
function(_id) { /* ignored */ },
function(id) {
if (initializeOnCreate && id)
dashboardView.initializeAccount(id, true)
},
function(error) { console.warn("createAccountPublic failed:", error) })
}
onCreatePrivateAccountRequested: {
@ -277,6 +371,43 @@ Rectangle {
clipHelper.selectAll()
clipHelper.copy()
}
onInitializeAccountRequested: (accountId, isPublic) => dashboardView.initializeAccount(accountId, isPublic)
onLabelRequested: (accountId, isPublic) => {
setLabelDialog.accountId = accountId
setLabelDialog.isPublic = isPublic
setLabelDialog.open()
}
// Shared by the manual Initialize button (onInitializeAccountRequested)
// and initialize-on-create (onCreatePublicAccountRequested above).
function initializeAccount(accountId, isPublic) {
if (!backend) return
// Reassign (not mutate) so the pendingInitializations binding
// propagated down to each AccountDelegate re-evaluates.
var pending = Object.assign({}, root.pendingInitializations)
pending[accountId] = true
root.pendingInitializations = pending
function clearPending() {
var updated = Object.assign({}, root.pendingInitializations)
delete updated[accountId]
root.pendingInitializations = updated
}
// Same {success, tx_hash, error} shape as the transfer/vaultClaim
// slots below, so it gets the same result-panel feedback. The
// accountModel's tag updates via NOTIFY once the backend
// refreshes accounts after a successful initialization.
logos.watch(backend.initializeAccount(accountId, isPublic),
function(raw) {
clearPending()
ffiErrors.applyTransferResult(dashboardView, raw)
},
function(error) {
clearPending()
dashboardView.transferResult = qsTr("Error: %1").arg(error)
dashboardView.transferResultIsError = true
dashboardView.transferTxHash = ""
})
}
}
}
}

View File

@ -15,6 +15,20 @@ ItemDelegate {
// logos.module() bridge in the parent view.
signal copyRequested(string text)
// Emitted when the user clicks "Initialize" on an uninitialized account. The
// parent wires this to backend.initializeAccount(...) for the same reason as
// copyRequested above.
signal initializeRequested(string accountId, bool isPublic)
// Emitted when the user clicks "Add label", so the parent can open
// SetLabelDialog for this account. Only reachable for unlabeled accounts
// the wallet core has no way to rename or remove a label once added.
signal labelRequested(string accountId, bool isPublic)
// Set by the parent while this account's initializeAccount() call is in flight,
// so the button can show it took the click instead of appearing to do nothing.
property bool initializing: false
leftPadding: Theme.spacing.medium
rightPadding: Theme.spacing.medium
topPadding: Theme.spacing.medium
@ -39,6 +53,38 @@ ItemDelegate {
font.bold: true
}
LogosText {
// The wallet core only supports adding a label, never renaming or
// removing one so once an account has one, there's nothing left
// to offer here.
text: qsTr("Add label")
visible: !model.name
font.pixelSize: Theme.typography.secondaryText
font.underline: labelLinkArea.containsMouse
color: Theme.palette.textMuted
MouseArea {
id: labelLinkArea
anchors.fill: parent
anchors.margins: -Theme.spacing.small
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.labelRequested(model.accountId ?? "", model.isPublic ?? true)
}
}
Item { Layout.fillWidth: true }
LogosText {
text: model.balance && model.balance.length > 0 ? model.balance : "—"
font.bold: true
}
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
Rectangle {
Layout.preferredWidth: tagLabel.implicitWidth + Theme.spacing.small * 2
Layout.preferredHeight: tagLabel.implicitHeight + 4
@ -54,12 +100,23 @@ ItemDelegate {
}
}
Item { Layout.fillWidth: true }
Rectangle {
Layout.preferredWidth: initLabel.implicitWidth + Theme.spacing.small * 2
Layout.preferredHeight: initLabel.implicitHeight + 4
radius: 4
color: Theme.colors.getColor(
model.isInitialized ? Theme.palette.success : Theme.palette.warning, 0.18)
LogosText {
text: model.balance && model.balance.length > 0 ? model.balance : "—"
font.bold: true
LogosText {
id: initLabel
anchors.centerIn: parent
text: model.isInitialized ? qsTr("Initialized") : qsTr("Uninitialized")
font.pixelSize: Theme.typography.secondaryText
color: model.isInitialized ? Theme.palette.success : Theme.palette.warning
}
}
Item { Layout.fillWidth: true }
}
RowLayout {
@ -82,5 +139,14 @@ ItemDelegate {
icon.color: Theme.palette.textMuted
}
}
FeedbackButton {
Layout.fillWidth: true
Layout.preferredHeight: 32
visible: (model.isPublic ?? true) && !model.isInitialized
enabled: !root.initializing
text: root.initializing ? qsTr("Initializing…") : qsTr("Initialize")
onClicked: root.initializeRequested(model.accountId ?? "", model.isPublic ?? true)
}
}
}

View File

@ -0,0 +1,18 @@
import QtQuick
import Logos.Theme
import Logos.Controls
// Drop-in replacement for LogosButton that darkens while pressed. LogosButton's
// own background only reacts to isActive (hover OR press, same color for both),
// so a click on an already-hovered button showed no visible change at all.
LogosButton {
id: root
Rectangle {
anchors.fill: parent
radius: root.radius
color: Theme.palette.text
opacity: root.mouseAreaItem.pressed ? 0.15 : 0
}
}

View File

@ -5,10 +5,12 @@ import QtQuick.Layouts
import Logos.Theme
import Logos.Controls
import "../controls"
Popup {
id: root
signal createPublicRequested()
signal createPublicRequested(bool initializeOnCreate)
signal createPrivateRequested()
modal: true
@ -72,20 +74,27 @@ Popup {
Layout.fillWidth: true
}
LogosCheckbox {
id: initializeOnCreateCheck
checked: true
text: qsTr("Initialize on creation")
visible: tabBar.currentIndex === 0
}
RowLayout {
Layout.topMargin: Theme.spacing.medium
spacing: Theme.spacing.medium
Layout.fillWidth: true
Item { Layout.fillWidth: true }
LogosButton {
FeedbackButton {
text: qsTr("Cancel")
onClicked: root.close()
}
LogosButton {
FeedbackButton {
text: qsTr("Create")
onClicked: {
if (tabBar.currentIndex === 0)
root.createPublicRequested()
root.createPublicRequested(initializeOnCreateCheck.checked)
else
root.createPrivateRequested()
root.close()

View File

@ -0,0 +1,94 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Logos.Theme
import Logos.Controls
import "../controls"
// Shown right after a new wallet is created, so the user can reveal, copy,
// and write down their recovery phrase before it's cleared from memory.
// Hidden by default the caller drives `mnemonic` and listens for
// `acknowledged()` to clear its backend-side copy once the user is done.
Popup {
id: root
property string mnemonic: ""
property bool revealed: false
signal copyRequested(string text)
signal acknowledged()
modal: true
dim: true
padding: Theme.spacing.large
// No dismissal via Escape/outside click the user must explicitly
// confirm they've saved the phrase via the Continue button below.
closePolicy: Popup.NoAutoClose
anchors.centerIn: parent
onOpened: root.revealed = false
background: Rectangle {
color: Theme.palette.backgroundSecondary
radius: Theme.spacing.radiusXlarge
border.color: Theme.palette.backgroundElevated
}
contentItem: ColumnLayout {
width: 360
spacing: Theme.spacing.large
LogosText {
text: qsTr("Save your recovery phrase")
font.pixelSize: Theme.typography.titleText
font.weight: Theme.typography.weightBold
color: Theme.palette.text
}
LogosText {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: qsTr("Write these words down in order and store them somewhere safe. Anyone with this phrase can access your funds, and it cannot be recovered if lost.")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosTextArea {
id: mnemonicText
Layout.fillWidth: true
Layout.preferredHeight: 90
readOnly: true
text: root.revealed ? root.mnemonic : "•••• ".repeat(6)
}
LogosCopyButton {
Layout.alignment: Qt.AlignTop
onCopyText: root.copyRequested(root.mnemonic)
}
}
FeedbackButton {
text: root.revealed ? qsTr("Hide phrase") : qsTr("Show phrase")
onClicked: root.revealed = !root.revealed
}
RowLayout {
Layout.topMargin: Theme.spacing.medium
Layout.fillWidth: true
Item { Layout.fillWidth: true }
FeedbackButton {
text: qsTr("Continue")
onClicked: {
root.acknowledged()
root.close()
}
}
}
}
}

View File

@ -0,0 +1,150 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Logos.Theme
import Logos.Controls
import "../controls"
Popup {
id: root
// Context set by the parent right before open(). Only ever opened for an
// unlabeled account the wallet core has no way to rename or remove a label
// once added, so there's no "current label" to prefill or compare against.
property string accountId: ""
property bool isPublic: true
// Debounced availability-check result. The parent owns the backend connection,
// so it drives these two properties in response to checkAvailabilityRequested.
property bool checkingAvailability: false
property bool labelAvailable: true
// Save round-trip state, likewise driven by the parent: saving stays true and the
// popup stays open until the parent reports success (closeOnSaveSuccess) or failure
// (reportSaveError) so a failed save isn't silently swallowed by an immediate close.
property bool saving: false
property string saveError: ""
signal checkAvailabilityRequested(string label)
signal saveRequested(string accountId, bool isPublic, string label)
function closeOnSaveSuccess() {
root.saving = false
root.close()
}
function reportSaveError(message) {
root.saving = false
root.saveError = message
}
readonly property string trimmedText: labelField.text.trim()
readonly property bool saveEnabled: trimmedText.length > 0
&& root.labelAvailable && !root.checkingAvailability
modal: true
dim: true
padding: Theme.spacing.large
// No dismissal while a save is in flight mirrors the disabled Cancel/Save
// buttons below, so a stale response can't land on a dialog the user thinks
// they already closed.
closePolicy: root.saving ? Popup.NoAutoClose : (Popup.CloseOnEscape | Popup.CloseOnPressOutside)
anchors.centerIn: parent
background: Rectangle {
color: Theme.palette.backgroundSecondary
radius: Theme.spacing.radiusXlarge
border.color: Theme.palette.backgroundElevated
}
onOpened: {
labelField.text = ""
root.labelAvailable = true
root.checkingAvailability = false
root.saving = false
root.saveError = ""
labelField.forceActiveFocus()
labelField.selectAll()
}
Timer {
id: debounce
interval: 400
onTriggered: {
if (root.trimmedText.length > 0) {
root.checkingAvailability = true
root.checkAvailabilityRequested(root.trimmedText)
} else {
root.checkingAvailability = false
}
}
}
contentItem: ColumnLayout {
width: 320
spacing: Theme.spacing.large
LogosText {
text: qsTr("Add label")
font.pixelSize: Theme.typography.titleText
font.weight: Theme.typography.weightBold
color: Theme.palette.text
}
LogosTextField {
id: labelField
Layout.fillWidth: true
enabled: !root.saving
onTextChanged: {
root.saveError = ""
// Mark as checking immediately so saveEnabled can't go true on stale
// (or no) availability data during the debounce window the Timer
// below only confirms/updates the result once typing settles.
root.checkingAvailability = root.trimmedText.length > 0
debounce.restart()
}
}
LogosText {
Layout.fillWidth: true
wrapMode: Text.WordWrap
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.error
visible: root.trimmedText.length > 0 && !root.checkingAvailability && !root.labelAvailable
text: qsTr("That label is already in use.")
}
LogosText {
Layout.fillWidth: true
wrapMode: Text.WordWrap
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.error
visible: root.saveError.length > 0
text: root.saveError
}
RowLayout {
Layout.topMargin: Theme.spacing.medium
spacing: Theme.spacing.medium
Layout.fillWidth: true
Item { Layout.fillWidth: true }
FeedbackButton {
text: qsTr("Cancel")
enabled: !root.saving
onClicked: root.close()
}
FeedbackButton {
text: root.saving ? qsTr("Saving…") : qsTr("Save")
enabled: root.saveEnabled && !root.saving
onClicked: {
root.saving = true
root.saveError = ""
root.saveRequested(root.accountId, root.isPublic, root.trimmedText)
}
}
}
}
}

View File

@ -15,19 +15,23 @@ Rectangle {
property var accountModel: null
property int lastSyncedBlock: 0
property int currentBlockHeight: 0
// Maps accountId -> true while that account's initializeAccount() call is in flight.
property var pendingInitializations: ({})
// --- Public API: signals out ---
signal createPublicAccountRequested()
signal createPublicAccountRequested(bool initializeOnCreate)
signal createPrivateAccountRequested()
signal fetchBalancesRequested()
signal copyRequested(string text)
signal initializeAccountRequested(string accountId, bool isPublic)
signal labelRequested(string accountId, bool isPublic)
radius: Theme.spacing.radiusXlarge
color: Theme.palette.backgroundSecondary
CreateAccountDialog {
id: createAccountDialog
onCreatePublicRequested: root.createPublicAccountRequested()
onCreatePublicRequested: (initializeOnCreate) => root.createPublicAccountRequested(initializeOnCreate)
onCreatePrivateRequested: root.createPrivateAccountRequested()
}
@ -51,7 +55,7 @@ Rectangle {
Item { Layout.fillWidth: true }
LogosButton {
FeedbackButton {
Layout.preferredHeight: 40
Layout.preferredWidth: 80
text: qsTr("+ Create")
@ -145,26 +149,124 @@ Rectangle {
// row is created.
property string keysJsonWarm: model.keysJson ?? ""
// "Public Accounts" title: the public section is a single group, so this
// is equivalent to showing it once above the first public row.
RowLayout {
Layout.fillWidth: true
visible: model.isFirstInGroup ?? false
visible: model.isPublic && (model.isFirstInGroup ?? false)
spacing: Theme.spacing.small
LogosText {
text: model.isPublic
? qsTr("Public Accounts")
: qsTr("Private")
font.pixelSize: Theme.typography.secondaryText
text: qsTr("Public Accounts")
font.pixelSize: Theme.typography.primaryText
font.bold: true
color: Theme.palette.textSecondary
color: Theme.palette.text
}
}
// "Private Accounts" title: shown once above the whole private section,
// unlike the per-key-set row below which repeats for every private key
// group. Wrapped the same way as the "Public Accounts" title above so
// both line up identically.
RowLayout {
Layout.fillWidth: true
visible: model.isFirstPrivate ?? false
spacing: Theme.spacing.small
LogosText {
text: qsTr("Private Accounts")
font.pixelSize: Theme.typography.primaryText
font.bold: true
color: Theme.palette.text
}
}
// Per-key-set row: separates each private key group within the Private
// section, naming the group by its Npk/Vpk pair, and holds the copy
// button for that pair.
RowLayout {
id: keyGroupHeader
Layout.fillWidth: true
visible: !model.isPublic && (model.isFirstInGroup ?? false)
spacing: Theme.spacing.small
property var groupKeys: {
try { return JSON.parse(model.keysJson ?? "{}") } catch (e) { return {} }
}
Item { Layout.fillWidth: true }
function shortKey(key) {
return key && key.length > 12 ? key.slice(0, 6) + "…" + key.slice(-4) : (key || "")
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
LogosText {
Layout.fillWidth: true
text: qsTr("Accounts under keys")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
}
// Each of Npk/Vpk gets its own bullet, aligned with "Private
// Accounts"/"Accounts under keys" above. Labels share a fixed
// width (the wider of the two) so the value column still lines
// up between the two rows.
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosText {
text: "•"
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
}
LogosText {
id: npkLabel
Layout.preferredWidth: Math.max(npkLabel.implicitWidth, vpkLabel.implicitWidth)
text: qsTr("Npk:")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
}
LogosText {
Layout.fillWidth: true
text: keyGroupHeader.shortKey(keyGroupHeader.groupKeys.nullifier_public_key)
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
elide: Text.ElideRight
}
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosText {
text: "•"
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
}
LogosText {
id: vpkLabel
Layout.preferredWidth: Math.max(npkLabel.implicitWidth, vpkLabel.implicitWidth)
text: qsTr("Vpk:")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
}
LogosText {
Layout.fillWidth: true
text: keyGroupHeader.shortKey(keyGroupHeader.groupKeys.viewing_public_key)
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
elide: Text.ElideRight
}
}
}
LogosCopyButton {
Layout.preferredHeight: 32
Layout.preferredWidth: 32
visible: !model.isPublic
icon.color: Theme.palette.textMuted
onCopyText: root.copyRequested(model.keysJson ?? "")
}
@ -172,13 +274,16 @@ Rectangle {
AccountDelegate {
Layout.fillWidth: true
initializing: root.pendingInitializations[model.accountId] === true
onCopyRequested: (text) => root.copyRequested(text)
onInitializeRequested: (accountId, isPublic) => root.initializeAccountRequested(accountId, isPublic)
onLabelRequested: (accountId, isPublic) => root.labelRequested(accountId, isPublic)
}
}
}
// Footer: Fetch / Refresh Balances
LogosButton {
FeedbackButton {
Layout.fillWidth: true
text: qsTr("Refresh Balances")
onClicked: root.fetchBalancesRequested()

View File

@ -5,6 +5,7 @@ import QtQuick.Layouts
import Logos.Theme
import Logos.Controls
import "../controls"
import "../Base58.js" as Base58
Item {
@ -79,7 +80,7 @@ Item {
}
}
LogosButton {
FeedbackButton {
text: qsTr("Claim")
enabled: !root.claimPending
onClicked: root.vaultClaimRequested(model.accountId, model.isPublic, model.vaultBalance)
@ -88,7 +89,7 @@ Item {
}
}
LogosButton {
FeedbackButton {
Layout.fillWidth: true
text: qsTr("Refresh")
enabled: !root.claimPending

View File

@ -19,9 +19,10 @@ Rectangle {
property bool transferPending: false
property int lastSyncedBlock: 0
property int currentBlockHeight: 0
property var pendingInitializations: ({})
// --- Public API: output signals (parent connects and calls backend) ---
signal createPublicAccountRequested()
signal createPublicAccountRequested(bool initializeOnCreate)
signal createPrivateAccountRequested()
signal fetchBalancesRequested()
signal transferPublicRequested(string fromAccountId, string toAddress, string amount)
@ -34,6 +35,8 @@ Rectangle {
signal vaultClaimRequested(string fromAccountId, bool isPublic, string amount)
signal refreshClaimableDepositsRequested()
signal copyRequested(string copyText)
signal initializeAccountRequested(string accountId, bool isPublic)
signal labelRequested(string accountId, bool isPublic)
color: Theme.palette.background
@ -50,11 +53,14 @@ Rectangle {
accountModel: root.accountModel
lastSyncedBlock: root.lastSyncedBlock
currentBlockHeight: root.currentBlockHeight
pendingInitializations: root.pendingInitializations
onCreatePublicAccountRequested: root.createPublicAccountRequested()
onCreatePublicAccountRequested: (initializeOnCreate) => root.createPublicAccountRequested(initializeOnCreate)
onCreatePrivateAccountRequested: root.createPrivateAccountRequested()
onFetchBalancesRequested: root.fetchBalancesRequested()
onCopyRequested: (text) => root.copyRequested(text)
onInitializeAccountRequested: (accountId, isPublic) => root.initializeAccountRequested(accountId, isPublic)
onLabelRequested: (accountId, isPublic) => root.labelRequested(accountId, isPublic)
}
TransferPanel {

View File

@ -6,18 +6,28 @@ import QtQuick.Dialogs
import Logos.Theme
import Logos.Controls
import "../controls"
Control {
id: root
property string configPath: ""
property string storePath: ""
property string createError: ""
property bool recoverMode: false
// Create/restore is a blocking round-trip to the wallet backend (it can take
// a while, especially restoring), which freezes the UI thread for its
// duration this at least shows a message right before that happens
// instead of the form just going inert with no explanation.
property bool busy: false
signal createWallet(string configPath, string storagePath, string password, string sequencerUrl)
signal recoverWallet(string configPath, string storagePath, string mnemonic, string password, string sequencerUrl)
readonly property string testnetUrl: "https://testnet.lez.logos.co"
readonly property string localhostUrl: "http://127.0.0.1:3040"
onCreateErrorChanged: if (createError.length > 0) root.busy = false
QtObject {
id: d
@ -31,142 +41,217 @@ Control {
}
}
ColumnLayout {
id: cardColumn
// Fires the actual (blocking) request one tick after the button click, so
// the "Restoring"/"Creating" busy state above has a chance to actually
// render before the UI thread freezes for the call.
Timer {
id: submitTimer
interval: 50
onTriggered: {
if (root.recoverMode) {
root.recoverWallet(configPathField.text, storagePathField.text, mnemonicField.text.trim(), passwordField.text, sequencerUrlField.text)
} else {
root.createWallet(configPathField.text, storagePathField.text, passwordField.text, sequencerUrlField.text)
}
}
}
// Scrollable so the form stays reachable (including the submit button)
// when its content is taller than the window, e.g. with the recovery
// phrase field shown rather than silently clipping/overflowing.
LogosScrollView {
id: scrollView
anchors.fill: parent
anchors.margins: Theme.spacing.xlarge
spacing: Theme.spacing.large
LogosText {
text: qsTr("Set up LEZ Wallet")
font.pixelSize: Theme.typography.titleText
font.weight: Theme.typography.weightBold
color: Theme.palette.text
}
LogosText {
text: qsTr("Configure storage and secure with a password.")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
Layout.topMargin: -Theme.spacing.small
}
Item {
width: scrollView.availableWidth
implicitHeight: cardColumn.implicitHeight + 2 * Theme.spacing.xlarge
LogosText {
text: qsTr("Storage")
font.pixelSize: Theme.typography.secondaryText
font.weight: Theme.typography.weightMedium
color: Theme.palette.text
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosTextField {
id: storagePathField
Layout.fillWidth: true
placeholderText: qsTr("Add store path")
text: root.storePath
}
LogosButton {
text: qsTr("Browse")
onClicked: storageFolderDialog.open()
}
}
LogosText {
text: qsTr("Config file")
font.pixelSize: Theme.typography.secondaryText
font.weight: Theme.typography.weightMedium
color: Theme.palette.text
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosTextField {
id: configPathField
Layout.fillWidth: true
placeholderText: qsTr("Add path to config")
text: root.configPath
}
LogosButton {
Layout.preferredHeight: configPathField.height
text: qsTr("Browse")
onClicked: configFileDialog.open()
}
}
ColumnLayout {
id: cardColumn
RowLayout {
Layout.fillWidth: true
Layout.topMargin: Theme.spacing.medium
spacing: Theme.spacing.small
LogosText {
text: qsTr("Network")
font.pixelSize: Theme.typography.secondaryText
font.weight: Theme.typography.weightMedium
color: Theme.palette.text
}
LogosTextField {
id: sequencerUrlField
Layout.fillWidth: true
placeholderText: qsTr("Sequencer URL")
text: root.testnetUrl
}
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosButton {
text: qsTr("Testnet")
opacity: sequencerUrlField.text === root.testnetUrl ? 1.0 : 0.4
onClicked: sequencerUrlField.text = root.testnetUrl
}
LogosButton {
text: qsTr("Localhost")
opacity: sequencerUrlField.text === root.localhostUrl ? 1.0 : 0.4
onClicked: sequencerUrlField.text = root.localhostUrl
}
}
x: Theme.spacing.xlarge
y: Theme.spacing.xlarge
width: parent.width - 2 * Theme.spacing.xlarge
spacing: Theme.spacing.large
LogosText {
text: qsTr("Security")
font.pixelSize: Theme.typography.secondaryText
font.weight: Theme.typography.weightMedium
color: Theme.palette.text
Layout.topMargin: Theme.spacing.medium
}
LogosTextField {
id: passwordField
Layout.fillWidth: true
placeholderText: qsTr("Password")
echoMode: TextInput.Password
}
LogosTextField {
id: confirmField
Layout.fillWidth: true
placeholderText: qsTr("Confirm")
echoMode: TextInput.Password
}
LogosText {
text: qsTr("Set up LEZ Wallet")
font.pixelSize: Theme.typography.titleText
font.weight: Theme.typography.weightBold
color: Theme.palette.text
}
LogosText {
text: qsTr("Configure storage and secure with a password.")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
Layout.topMargin: -Theme.spacing.small
}
LogosText {
id: errorLabel
Layout.fillWidth: true
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.error
wrapMode: Text.WordWrap
visible: text.length > 0
text: root.createError
}
LogosTabBar {
id: modeTabBar
Layout.fillWidth: true
currentIndex: root.recoverMode ? 1 : 0
onCurrentIndexChanged: {
root.recoverMode = currentIndex === 1
root.createError = ""
}
LogosButton {
Layout.alignment: Qt.AlignRight
text: qsTr("Create Wallet")
font.pixelSize: Theme.typography.secondaryText
onClicked: {
if (passwordField.text.length === 0) {
root.createError = qsTr("Password cannot be empty.")
} else if (passwordField.text !== confirmField.text) {
root.createError = qsTr("Passwords do not match.")
} else {
root.createError = ""
root.createWallet(configPathField.text, storagePathField.text, passwordField.text, sequencerUrlField.text)
LogosTabButton { text: qsTr("Create New") }
LogosTabButton { text: qsTr("Recover from Mnemonic") }
}
LogosText {
text: qsTr("Storage")
font.pixelSize: Theme.typography.secondaryText
font.weight: Theme.typography.weightMedium
color: Theme.palette.text
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosTextField {
id: storagePathField
Layout.fillWidth: true
placeholderText: qsTr("Add store path")
text: root.storePath
}
FeedbackButton {
text: qsTr("Browse")
onClicked: storageFolderDialog.open()
}
}
LogosText {
text: qsTr("Config file")
font.pixelSize: Theme.typography.secondaryText
font.weight: Theme.typography.weightMedium
color: Theme.palette.text
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosTextField {
id: configPathField
Layout.fillWidth: true
placeholderText: qsTr("Add path to config")
text: root.configPath
}
FeedbackButton {
Layout.preferredHeight: configPathField.height
text: qsTr("Browse")
onClicked: configFileDialog.open()
}
}
RowLayout {
Layout.fillWidth: true
Layout.topMargin: Theme.spacing.medium
spacing: Theme.spacing.small
LogosText {
text: qsTr("Network")
font.pixelSize: Theme.typography.secondaryText
font.weight: Theme.typography.weightMedium
color: Theme.palette.text
}
LogosTextField {
id: sequencerUrlField
Layout.fillWidth: true
placeholderText: qsTr("Sequencer URL")
text: root.testnetUrl
}
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
FeedbackButton {
text: qsTr("Testnet")
opacity: sequencerUrlField.text === root.testnetUrl ? 1.0 : 0.4
onClicked: sequencerUrlField.text = root.testnetUrl
}
FeedbackButton {
text: qsTr("Localhost")
opacity: sequencerUrlField.text === root.localhostUrl ? 1.0 : 0.4
onClicked: sequencerUrlField.text = root.localhostUrl
}
}
LogosText {
text: qsTr("Recovery phrase")
visible: root.recoverMode
font.pixelSize: Theme.typography.secondaryText
font.weight: Theme.typography.weightMedium
color: Theme.palette.text
Layout.topMargin: Theme.spacing.medium
}
LogosTextArea {
id: mnemonicField
visible: root.recoverMode
Layout.fillWidth: true
Layout.preferredHeight: 90
placeholderText: qsTr("Enter your 12 or 24-word recovery phrase")
}
LogosText {
text: qsTr("Security")
font.pixelSize: Theme.typography.secondaryText
font.weight: Theme.typography.weightMedium
color: Theme.palette.text
Layout.topMargin: Theme.spacing.medium
}
LogosTextField {
id: passwordField
Layout.fillWidth: true
placeholderText: qsTr("Password")
echoMode: TextInput.Password
}
LogosTextField {
id: confirmField
Layout.fillWidth: true
placeholderText: qsTr("Confirm")
echoMode: TextInput.Password
}
LogosText {
Layout.fillWidth: true
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
wrapMode: Text.WordWrap
visible: root.busy
text: root.recoverMode
? qsTr("Restoring your wallet from the recovery phrase… this can take a little while. Please don't close the app.")
: qsTr("Creating your wallet…")
}
LogosText {
id: errorLabel
Layout.fillWidth: true
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.error
wrapMode: Text.WordWrap
visible: !root.busy && text.length > 0
text: root.createError
}
FeedbackButton {
Layout.alignment: Qt.AlignRight
enabled: !root.busy
text: root.busy
? (root.recoverMode ? qsTr("Restoring…") : qsTr("Creating…"))
: (root.recoverMode ? qsTr("Restore Wallet") : qsTr("Create Wallet"))
font.pixelSize: Theme.typography.secondaryText
onClicked: {
if (passwordField.text.length === 0) {
root.createError = qsTr("Password cannot be empty.")
} else if (passwordField.text !== confirmField.text) {
root.createError = qsTr("Passwords do not match.")
} else if (root.recoverMode && mnemonicField.text.trim().length === 0) {
root.createError = qsTr("Recovery phrase cannot be empty.")
} else {
root.createError = ""
root.busy = true
submitTimer.start()
}
}
}
}
}

View File

@ -188,7 +188,7 @@ Item {
}
// Send button
LogosButton {
FeedbackButton {
Layout.fillWidth: true
text: qsTr("Send")
font.pixelSize: Theme.typography.secondaryText

View File

@ -99,7 +99,7 @@ Item {
}
// Withdraw button
LogosButton {
FeedbackButton {
Layout.fillWidth: true
text: qsTr("Withdraw")
font.pixelSize: Theme.typography.secondaryText