mirror of
https://github.com/logos-blockchain/logos-execution-zone-wallet-ui.git
synced 2026-08-08 11:03:11 +00:00
feat: improve private account listing
This commit is contained in:
parent
f726dada5f
commit
d31b5becd3
@ -31,6 +31,7 @@ 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();
|
||||
}
|
||||
@ -47,6 +48,7 @@ QHash<int, QByteArray> LEZWalletAccountModel::roleNames() const
|
||||
{ SectionKeyRole, "sectionKey" },
|
||||
{ KeysJsonRole, "keysJson" },
|
||||
{ IsFirstInGroupRole, "isFirstInGroup" },
|
||||
{ IsFirstPrivateRole, "isFirstPrivate" },
|
||||
{ IsInitializedRole, "isInitialized" }
|
||||
};
|
||||
}
|
||||
@ -100,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();
|
||||
|
||||
@ -18,7 +18,8 @@ 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.
|
||||
@ -42,6 +43,7 @@ public:
|
||||
SectionKeyRole,
|
||||
KeysJsonRole,
|
||||
IsFirstInGroupRole,
|
||||
IsFirstPrivateRole,
|
||||
IsInitializedRole
|
||||
};
|
||||
Q_ENUM(Role)
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QGuiApplication>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QSettings>
|
||||
@ -59,6 +60,13 @@ namespace {
|
||||
return p;
|
||||
}
|
||||
|
||||
// lez_core has no UI-facing concept of a statistics file; derive one deterministically
|
||||
// next to the storage file so onboarding doesn't need a third path picker.
|
||||
QString statisticsPathFor(const QString& localStoragePath) {
|
||||
const QFileInfo info(localStoragePath);
|
||||
return info.absolutePath() + QStringLiteral("/statistics.json");
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -166,7 +174,7 @@ void LEZWalletBackend::openIfPathsConfigured(int attempt)
|
||||
|
||||
qDebug() << "LEZWalletBackend: opening wallet with config" << configPath()
|
||||
<< "storage" << storagePath();
|
||||
int err = m_logos->lez_core.open(configPath(), storagePath());
|
||||
int err = m_logos->lez_core.open(configPath(), storagePath(), statisticsPathFor(storagePath()));
|
||||
if (err == WALLET_FFI_SUCCESS) {
|
||||
qDebug() << "LEZWalletBackend: wallet opened successfully";
|
||||
finishOpeningWallet();
|
||||
@ -501,7 +509,10 @@ void LEZWalletBackend::applySequencerAddrToConfig(const QString& configPath, con
|
||||
obj[QStringLiteral("seq_poll_max_retries")] = 10;
|
||||
obj[QStringLiteral("seq_block_poll_max_amount")] = 100;
|
||||
}
|
||||
obj[QStringLiteral("sequencer_addr")] = sequencerAddr;
|
||||
QJsonObject sequencerEntry;
|
||||
sequencerEntry[QStringLiteral("sequencer_addr")] = sequencerAddr;
|
||||
sequencerEntry[QStringLiteral("basic_auth")] = QJsonValue::Null;
|
||||
obj[QStringLiteral("sequencers")] = QJsonArray{ sequencerEntry };
|
||||
|
||||
QDir().mkpath(QFileInfo(configPath).absolutePath());
|
||||
if (file.open(QIODevice::WriteOnly | QIODevice::Truncate))
|
||||
@ -517,7 +528,7 @@ QString LEZWalletBackend::createNew(QString configPath, QString storagePath, QSt
|
||||
// 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.
|
||||
if (QFile::exists(localConfigPath) && QFile::exists(localStoragePath)) {
|
||||
int err = m_logos->lez_core.open(localConfigPath, localStoragePath);
|
||||
int err = m_logos->lez_core.open(localConfigPath, localStoragePath, statisticsPathFor(localStoragePath));
|
||||
if (err != WALLET_FFI_SUCCESS) {
|
||||
return QStringLiteral(
|
||||
"Could not load the wallet at the selected paths. Pick "
|
||||
@ -534,7 +545,7 @@ QString LEZWalletBackend::createNew(QString configPath, QString storagePath, QSt
|
||||
applySequencerAddrToConfig(localConfigPath, sequencerAddr);
|
||||
|
||||
const QString mnemonic = m_logos->lez_core.create_new(
|
||||
localConfigPath, localStoragePath, password);
|
||||
localConfigPath, localStoragePath, statisticsPathFor(localStoragePath), password);
|
||||
if (mnemonic.isEmpty())
|
||||
return QStringLiteral("Failed to create wallet. Check paths and try again.");
|
||||
|
||||
|
||||
@ -139,36 +139,124 @@ Rectangle {
|
||||
width: listView.width
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
// keysJson is only ever read inside the copy button's click handler below,
|
||||
// never bound to anything visible — so unlike accountId/isPublic/isFirstInGroup
|
||||
// (which are warmed up by their visible bindings), the Qt Remote Objects model
|
||||
// replica never requests this role from the source process until something
|
||||
// actually reads it. Without this binding, the first click reads a not-yet-
|
||||
// fetched (empty) value and the real data only arrives in time for the next
|
||||
// click. Referencing it here forces the role to be requested as soon as the
|
||||
// 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 ?? "")
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user