feat: update some flows and fix UI issues + polish

This commit is contained in:
Khushboo Mehta 2026-02-23 18:09:06 +01:00
parent c24cca889f
commit 0a82f61a93
11 changed files with 250 additions and 110 deletions

33
flake.lock generated
View File

@ -528,11 +528,11 @@
]
},
"locked": {
"lastModified": 1771838299,
"narHash": "sha256-Uf45wbh2q5ewoiw4u04YImc2Gij3OXIfbB5NYpUm5dw=",
"lastModified": 1771862755,
"narHash": "sha256-uRQztNMMLhHnQw0P2ZisB1LZ1V7pNqkZRI3pe0wUIts=",
"owner": "logos-co",
"repo": "logos-design-system",
"rev": "fc6f52d85a008aa1bb513f6b42648df4bcf0713d",
"rev": "4d55203607759c9ef042be4505bbc6efffbbd444",
"type": "github"
},
"original": {
@ -548,11 +548,11 @@
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1771420202,
"narHash": "sha256-r3lO12wvYPm6Xe7YFGK40iYe+1OID5YlGlup6RGgvs8=",
"lastModified": 1771604479,
"narHash": "sha256-DxgL4uT8+F0ALuEpENkbJ0OcyNhdFgX8Mr8tezMnZ5E=",
"owner": "logos-blockchain",
"repo": "lssa",
"rev": "89ce9f322a1fc4214ec818e094e6efc97be02d9c",
"rev": "bc84d5cf31b3f6967060c35a5b98618d99780ade",
"type": "github"
},
"original": {
@ -574,14 +574,17 @@
]
},
"locked": {
"lastModified": 1771839856,
"narHash": "sha256-MtgK/B6mx9xyojbRc6RY8vHuMkMrS4XMV7BKcO6zK/I=",
"path": "/Users/khushboomehta/Documents/logos/logos-execution-zone-module",
"type": "path"
"lastModified": 1771922567,
"narHash": "sha256-ObbXljdS/hR0PCgBosZgzwMHWRxvSGWmaxgFEy3OvOE=",
"owner": "logos-blockchain",
"repo": "logos-execution-zone-module",
"rev": "8474602359b69f56cfad232b052b52ae2701f74c",
"type": "github"
},
"original": {
"path": "/Users/khushboomehta/Documents/logos/logos-execution-zone-module",
"type": "path"
"owner": "logos-blockchain",
"repo": "logos-execution-zone-module",
"type": "github"
}
},
"logos-liblogos": {
@ -778,11 +781,11 @@
]
},
"locked": {
"lastModified": 1770837874,
"narHash": "sha256-wr75lv1q4U1FS5+l/6ypwzJFJe06l2RyUvx1npoRS88=",
"lastModified": 1771871578,
"narHash": "sha256-6Mu3cmdhd8e7i+n8OWcaIBye+i12gwlwt1fhd9QCbCI=",
"owner": "logos-co",
"repo": "logos-liblogos",
"rev": "e3741c01fd3abf6b7bd9ff2fa8edf89c41fc0cea",
"rev": "19d29d4ef99292d9285b3a561cb7ea8029be3b74",
"type": "github"
},
"original": {

View File

@ -1,6 +1,8 @@
#include "LEZWalletBackend.h"
#include <QAbstractItemModel>
#include <QClipboard>
#include <QDebug>
#include <QGuiApplication>
#include <QJsonArray>
#include <QSettings>
#include <QUrl>
@ -12,6 +14,19 @@ namespace {
const char STORAGE_PATH_KEY[] = "storagePath";
const QString WALLET_MODULE_NAME = QStringLiteral("liblogos_execution_zone_wallet_module");
const int WALLET_FFI_SUCCESS = 0;
// Convert decimal amount string to 32-char hex (16 bytes little-endian) for transfer_public/transfer_private.
QString amountToLe16Hex(const QString& amountStr) {
const QString trimmed = amountStr.trimmed();
if (trimmed.isEmpty()) return QString();
bool parseOk = false;
const quint64 value = trimmed.toULongLong(&parseOk);
if (!parseOk) return QString();
uint8_t bytes[16] = {0};
for (int i = 0; i < 8; ++i)
bytes[i] = static_cast<uint8_t>((value >> (i * 8)) & 0xff);
return QByteArray(reinterpret_cast<const char*>(bytes), 16).toHex();
}
}
LEZWalletBackend::LEZWalletBackend(LogosAPI* logosAPI, QObject* parent)
@ -42,16 +57,21 @@ LEZWalletBackend::LEZWalletBackend(LogosAPI* logosAPI, QObject* parent)
}
if (!m_configPath.isEmpty() && !m_storagePath.isEmpty()) {
qDebug() << "LEZWalletBackend: opening wallet with config path" << m_configPath << "and storage path" << m_storagePath;
QVariant result = m_walletClient->invokeRemoteMethod(
WALLET_MODULE_NAME, "open", m_configPath, m_storagePath);
int err = result.isValid() ? result.toInt() : -1;
if (err == WALLET_FFI_SUCCESS) {
qWarning() << "LEZWalletBackend: wallet opened successfully";
setWalletOpen(true);
refreshAccounts();
refreshBlockHeights();
refreshSequencerAddr();
}
}
// Save wallet when app quits; host may not call destroyWidget() so destructor might not run.
connect(qApp, &QCoreApplication::aboutToQuit, this, [this]() { saveWallet(); }, Qt::DirectConnection);
}
LEZWalletBackend::~LEZWalletBackend()
@ -112,6 +132,7 @@ void LEZWalletBackend::refreshAccounts()
}
m_accountModel->replaceFromJsonArray(arr);
emit accountModelChanged();
refreshBalances();
}
void LEZWalletBackend::refreshBalances()
@ -125,13 +146,11 @@ void LEZWalletBackend::refreshBalances()
}
}
void LEZWalletBackend::refreshBlockHeights()
void LEZWalletBackend::fetchAndUpdateBlockHeights()
{
if (!m_walletClient) return;
QVariant last = m_walletClient->invokeRemoteMethod(WALLET_MODULE_NAME, "get_last_synced_block");
QVariant current = m_walletClient->invokeRemoteMethod(WALLET_MODULE_NAME, "get_current_block_height");
quint64 lastVal = last.isValid() ? last.toULongLong() : 0;
quint64 currentVal = current.isValid() ? current.toULongLong() : 0;
const quint64 lastVal = m_walletClient->invokeRemoteMethod(WALLET_MODULE_NAME, "get_last_synced_block").toULongLong();
const quint64 currentVal = m_walletClient->invokeRemoteMethod(WALLET_MODULE_NAME, "get_current_block_height").toULongLong();
if (m_lastSyncedBlock != lastVal) {
m_lastSyncedBlock = lastVal;
emit lastSyncedBlockChanged();
@ -142,6 +161,13 @@ void LEZWalletBackend::refreshBlockHeights()
}
}
void LEZWalletBackend::refreshBlockHeights()
{
fetchAndUpdateBlockHeights();
if (m_currentBlockHeight > 0 && m_lastSyncedBlock < m_currentBlockHeight && syncToBlock(m_currentBlockHeight))
fetchAndUpdateBlockHeights();
}
void LEZWalletBackend::refreshSequencerAddr()
{
if (!m_walletClient) return;
@ -205,11 +231,7 @@ bool LEZWalletBackend::syncToBlock(quint64 blockId)
QVariant result = m_walletClient->invokeRemoteMethod(
WALLET_MODULE_NAME, "sync_to_block", blockId);
int err = result.isValid() ? result.toInt() : -1;
if (err == WALLET_FFI_SUCCESS) {
refreshBlockHeights();
return true;
}
return false;
return err == WALLET_FFI_SUCCESS;
}
QString LEZWalletBackend::transferPublic(
@ -218,19 +240,33 @@ QString LEZWalletBackend::transferPublic(
const QString& amountLe16Hex)
{
if (!m_walletClient) return QStringLiteral("Error: Module not initialized.");
const QString amountHex = amountToLe16Hex(amountLe16Hex);
if (amountHex.isEmpty()) return QStringLiteral("Error: Invalid amount.");
QVariant result = m_walletClient->invokeRemoteMethod(
WALLET_MODULE_NAME, "transfer_public", fromHex, toHex, amountLe16Hex);
WALLET_MODULE_NAME, "transfer_public", fromHex, toHex, amountHex);
return result.isValid() ? result.toString() : QStringLiteral("Error: Call failed.");
}
QString LEZWalletBackend::transferPrivate(
const QString& fromHex,
const QString& toKeysJson,
const QString& toHex,
const QString& amountLe16Hex)
{
if (!m_walletClient) return QStringLiteral("Error: Module not initialized.");
const QString amountHex = amountToLe16Hex(amountLe16Hex);
if (amountHex.isEmpty()) return QStringLiteral("Error: Invalid amount.");
QString keysPayload = toHex.trimmed();
// If "To" is not JSON (e.g. user pasted account id hex), resolve to keys via get_private_account_keys.
if (!keysPayload.startsWith(QLatin1Char('{'))) {
qDebug() << "LEZWalletBackend::transferPrivate: keysPayload is not JSON, resolving to keys via get_private_account_keys";
const QString resolved = getPrivateAccountKeys(keysPayload);
if (!resolved.isEmpty())
keysPayload = resolved;
}
QVariant result = m_walletClient->invokeRemoteMethod(
WALLET_MODULE_NAME, "transfer_private", fromHex, toKeysJson, amountLe16Hex);
WALLET_MODULE_NAME, "transfer_private", fromHex, keysPayload, amountHex);
return result.isValid() ? result.toString() : QStringLiteral("Error: Call failed.");
}
@ -269,3 +305,9 @@ int LEZWalletBackend::indexOfAddressInModel(QObject* model, const QString& addre
}
return -1;
}
void LEZWalletBackend::copyToClipboard(const QString& text)
{
if (QGuiApplication::clipboard())
QGuiApplication::clipboard()->setText(text);
}

View File

@ -51,13 +51,14 @@ public:
const QString& amountLe16Hex);
Q_INVOKABLE QString transferPrivate(
const QString& fromHex,
const QString& toKeysJson,
const QString& toHex,
const QString& amountLe16Hex);
Q_INVOKABLE bool createNew(
const QString& configPath,
const QString& storagePath,
const QString& password);
Q_INVOKABLE int indexOfAddressInModel(QObject* model, const QString& address) const;
Q_INVOKABLE void copyToClipboard(const QString& text);
signals:
void isWalletOpenChanged();
@ -74,6 +75,7 @@ private:
void refreshBlockHeights();
void refreshSequencerAddr();
void saveWallet();
void fetchAndUpdateBlockHeights();
bool m_isWalletOpen;
QString m_configPath;

View File

@ -22,8 +22,8 @@ QWidget* LEZWalletPlugin::createWidget(LogosAPI* logosAPI) {
LEZWalletBackend* backend = new LEZWalletBackend(logosAPI, quickWidget);
quickWidget->rootContext()->setContextProperty("backend", backend);
QString qmlSource = "qrc:/qml/ExecutionZoneWalletView.qml";
QString importPath = "qrc:/qml";
QString qmlSource = "qrc:/lezwallet/qml/ExecutionZoneWalletView.qml";
QString importPath = "qrc:/lezwallet/qml";
QString envPath = QString::fromUtf8(qgetenv("DEV_QML_PATH")).trimmed();
if (!envPath.isEmpty()) {

View File

@ -10,15 +10,67 @@ import "views"
Rectangle {
id: root
// Map wallet FFI error codes to user-facing strings. Matches lssa/wallet-ffi WalletFfiError enum.
QtObject {
id: ffiErrors
readonly property var codeToMessage: ({
0: qsTr("Success"),
1: qsTr("Invalid argument (null pointer)"),
2: qsTr("Invalid UTF-8 string"),
3: qsTr("Wallet not initialized"),
4: qsTr("Configuration error"),
5: qsTr("Storage or persistence error"),
6: qsTr("Network or RPC error"),
7: qsTr("Account not found"),
8: qsTr("Key not found for account"),
9: qsTr("Insufficient funds"),
10: qsTr("Invalid account ID format"),
11: qsTr("Runtime error"),
12: qsTr("Password required but not provided"),
13: qsTr("Block synchronization error"),
14: qsTr("Serialization error"),
15: qsTr("Invalid type conversion"),
16: qsTr("Invalid key value"),
99: qsTr("Internal error")
})
function format(errorMessage) {
if (!errorMessage || typeof errorMessage !== "string")
return errorMessage || ""
var match = errorMessage.match(/wallet FFI error (\d+)/)
if (match) {
var code = match[1]
var msg = codeToMessage[code]
if (msg)
return msg
return qsTr("Wallet error (code %1)").arg(code)
}
return errorMessage
}
}
QtObject {
id: d
readonly property bool isWalletOpen: backend && backend.isWalletOpen
onIsWalletOpenChanged: {
if(isWalletOpen) {
stackView.push(mainView)
} else {
stackView.push(onboardingView)
}
}
}
color: Theme.palette.background
StackView {
id: stackView
anchors.fill: parent
initialItem: backend && backend.isWalletOpen ? mainView: onboardingView
Component {
id: onboardingView
OnboardingView {
storePath: backend.storagePath
configPath: backend.configPath
onCreateWallet: function(configPath, storagePath, password) {
if (!backend || !backend.createNew(configPath, storagePath, password))
createError = qsTr("Failed to create wallet. Check paths and try again.")
@ -31,7 +83,6 @@ Rectangle {
DashboardView {
id: dashboardView
accountModel: backend ? backend.accountModel : null
filteredAccountModel: backend ? backend.filteredAccountModel : null
onCreatePublicAccountRequested: {
if (!backend) {
@ -59,9 +110,25 @@ Rectangle {
console.warning("backend is null")
return
}
dashboardView.transferResult = isPublic
var raw = isPublic
? backend.transferPublic(fromId, toAddress, amount)
: backend.transferPrivate(fromId, toAddress, amount)
var msg = raw || ""
var isError = false
try {
var obj = JSON.parse(raw)
if (obj.success) {
msg = obj.tx_hash ? qsTr("Success. Tx: %1").arg(obj.tx_hash) : qsTr("Success.")
} else if (obj.error) {
msg = ffiErrors.format(obj.error)
isError = true
}
} catch (e) {
if (msg.length > 0)
isError = true
}
dashboardView.transferResult = msg
dashboardView.transferResultIsError = isError
}
}
}

View File

@ -8,47 +8,69 @@ import Logos.Controls
ItemDelegate {
id: root
implicitHeight: 80
leftPadding: Theme.spacing.medium
rightPadding: Theme.spacing.medium
topPadding: Theme.spacing.medium
bottomPadding: Theme.spacing.medium
background: Rectangle {
color: root.highlighted ? Theme.palette.backgroundMuted : "transparent"
radius: Theme.spacing.radiusSmall
color: root.highlighted || root.hovered ?
Theme.palette.backgroundMuted :
Theme.palette.backgroundTertiary
radius: Theme.spacing.radiusLarge
}
contentItem: RowLayout {
contentItem: ColumnLayout {
spacing: Theme.spacing.small
LogosText {
text: model.name
font.pixelSize: Theme.typography.secondaryText
font.bold: true
}
Rectangle {
Layout.preferredWidth: tagLabel.implicitWidth + Theme.spacing.small * 2
Layout.preferredHeight: tagLabel.implicitHeight + 4
radius: 2
color: model.isPublic ? Theme.palette.backgroundElevated : Theme.palette.backgroundSecondary
RowLayout {
spacing: Theme.spacing.small
LogosText {
id: tagLabel
anchors.centerIn: parent
text: model.isPublic ? qsTr("Public") : qsTr("Private")
font.pixelSize: Theme.typography.captionText
color: Theme.palette.textSecondary
text: model.name
font.pixelSize: Theme.typography.secondaryText
font.bold: true
}
Rectangle {
Layout.preferredWidth: tagLabel.implicitWidth + Theme.spacing.small * 2
Layout.preferredHeight: tagLabel.implicitHeight + 4
radius: 4
color: Theme.palette.backgroundSecondary
LogosText {
id: tagLabel
anchors.centerIn: parent
text: model.isPublic ? qsTr("Public") : qsTr("Private")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
}
}
Item { Layout.fillWidth: true }
LogosText {
text: model.balance && model.balance.length > 0 ? model.balance : "—"
font.bold: true
}
}
Item { Layout.fillWidth: true }
LogosText {
text: model.balance && model.balance.length > 0 ? model.balance : "—"
id: addressLabel
verticalAlignment: Text.AlignVCenter
text: model.address && model.address.length > 9
? model.address.slice(0, 4) + "…" + model.address.slice(-5)
: (model.address || "")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
color: Theme.palette.textMuted
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onDoubleClicked: {
if (model.address && typeof backend !== "undefined")
backend.copyToClipboard(model.address)
}
}
}
}
}

View File

@ -16,11 +16,7 @@ Popup {
padding: Theme.spacing.large
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
// Center in overlay (main window when modal)
parent: Overlay.overlay
anchors.centerIn: parent
// width: contentWrapper.width + leftPadding + rightPadding
// height: contentWrapper.height + topPadding + bottomPadding
background: Rectangle {
color: Theme.palette.backgroundSecondary

View File

@ -8,12 +8,10 @@ import Logos.Controls
Rectangle {
id: root
color: Theme.palette.background
// --- Public API: input properties (set by parent / MainView) ---
property var accountModel: null
property var filteredAccountModel: null
property string transferResult: ""
property bool transferResultIsError: false
// --- Public API: output signals (parent connects and calls backend) ---
signal createPublicAccountRequested()
@ -21,6 +19,8 @@ Rectangle {
signal fetchBalancesRequested()
signal transferRequested(bool isPublic, string fromAccountId, string toAddress, string amount)
color: Theme.palette.background
RowLayout {
anchors.fill: parent
anchors.margins: Theme.spacing.xlarge
@ -42,9 +42,9 @@ Rectangle {
id: transferPanel
Layout.fillWidth: true
Layout.fillHeight: true
fromAccountModel: root.filteredAccountModel
fromAccountModel: root.accountModel
transferResult: root.transferResult
transferResultIsError: root.transferResultIsError
onTransferRequested: function(isPublic, fromId, toAddress, amount) {
root.transferRequested(isPublic, fromId, toAddress, amount)

View File

@ -9,11 +9,25 @@ import Logos.Controls
Control {
id: root
property string configPath: ""
property string storePath: ""
property string createError: ""
signal createWallet(string configPath, string storagePath, string password)
QtObject {
id: d
function configParentFolderUrl(path) {
if (!path || path.length === 0) return ""
var p = path
var i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"))
if (i <= 0) return ""
var dir = p.substring(0, i)
return dir.indexOf("file://") === 0 ? dir : "file://" + dir
}
}
ColumnLayout {
id: cardColumn
@ -46,7 +60,8 @@ Control {
LogosTextField {
id: storagePathField
Layout.fillWidth: true
placeholderText: qsTr("/Users/you/.lez-wallet/")
placeholderText: qsTr("Add store path")
text: root.storePath
}
LogosButton {
text: qsTr("Browse")
@ -66,6 +81,7 @@ Control {
id: configPathField
Layout.fillWidth: true
placeholderText: qsTr("Add path to config")
text: root.configPath
}
LogosButton {
Layout.preferredHeight: configPathField.height
@ -121,16 +137,19 @@ Control {
}
}
FolderDialog {
FileDialog {
id: storageFolderDialog
modality: Qt.NonModal
onAccepted: storagePathField.text = selectedFolder.toString().replace(/^file:\/\//, "")
nameFilters: ["JSON files (*.json)"]
currentFolder: root.storePath ? d.configParentFolderUrl(root.storePath) : ""
onAccepted: storagePathField.text = selectedFile.toString().replace(/^file:\/\//, "")
}
FileDialog {
id: configFileDialog
modality: Qt.NonModal
nameFilters: ["YAML files (*.yaml)"]
nameFilters: ["JSON files (*.json)"]
currentFolder: root.configPath ? d.configParentFolderUrl(oot.configPath) : ""
onAccepted: {
if (selectedFile) configPathField.text = selectedFile.toString().replace(/^file:\/\//, "")
}

View File

@ -4,13 +4,15 @@ import QtQuick.Layouts
import Logos.Theme
import Logos.Controls
import "../controls"
Rectangle {
id: root
// --- Public API: data in ---
property var fromAccountModel: null // LEZAccountFilterModel from backend (filtered by public/private)
property var fromAccountModel: null
property string transferResult: ""
property bool transferResultIsError: false
// --- Public API: signals out ---
signal transferRequested(bool isPublic, string fromAccountId, string toAddress, string amount)
@ -25,13 +27,6 @@ Rectangle {
|| (fromFilterCount === 0 && manualFromField.text.trim().length > 0))
}
Binding {
target: fromAccountModel
property: "filterByPublic"
value: transferTypeBar.currentIndex === 0
when: fromAccountModel != null
}
radius: Theme.spacing.radiusXlarge
color: Theme.palette.backgroundSecondary
@ -113,44 +108,36 @@ Rectangle {
visible: fromCombo.count > 0
}
contentItem: TextInput {
readOnly: true
selectByMouse: true
width: fromCombo.width - indicatorText.width - 12
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.text
text: fromCombo.currentValue ?? ""
verticalAlignment: Text.AlignVCenter
clip: true
}
delegate: ItemDelegate {
id: delegate
width: fromCombo.width
leftPadding: 12
rightPadding: 12
contentItem: LogosText {
width: parent.width - parent.leftPadding - parent.rightPadding
contentItem: Item {
implicitWidth: fromCombo.width - indicatorText.width - 12
TextInput {
id: fromComboContentInput
anchors.fill: parent
readOnly: true
selectByMouse: true
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.text
text: model.name
elide: Text.ElideRight
horizontalAlignment: Text.AlignLeft
text: fromCombo.displayText
verticalAlignment: Text.AlignVCenter
clip: true
}
background: Rectangle {
color: delegate.highlighted
? Theme.palette.backgroundElevated
: Theme.palette.backgroundSecondary
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: fromCombo.popup.visible ? fromCombo.popup.close() : fromCombo.popup.open()
}
}
delegate: AccountDelegate {
width: fromCombo.popup.width - fromCombo.popup.leftPadding - fromCombo.popup.rightPadding
highlighted: fromCombo.highlightedIndex === index
}
popup: Popup {
y: fromCombo.height - 1
width: fromCombo.width
width: 400
height: Math.min(contentItem.implicitHeight + 8, 300)
padding: 0
padding: Theme.spacing.small
contentItem: ListView {
clip: true
@ -161,7 +148,7 @@ Rectangle {
}
background: Rectangle {
color: Theme.palette.backgroundSecondary
color: Theme.palette.backgroundTertiary
border.width: 1
border.color: Theme.palette.backgroundElevated
radius: Theme.spacing.radiusSmall
@ -226,7 +213,9 @@ Rectangle {
Layout.fillWidth: true
text: root.transferResult
font.pixelSize: Theme.typography.secondaryText
color: root.transferResult.length > 0 ? Theme.palette.textSecondary : "transparent"
color: root.transferResult.length > 0
? (root.transferResultIsError ? Theme.palette.error : Theme.palette.textSecondary)
: "transparent"
wrapMode: Text.WordWrap
}

View File

@ -1,5 +1,5 @@
<RCC>
<qresource prefix="/">
<qresource prefix="/lezwallet">
<file>qml/ExecutionZoneWalletView.qml</file>
<file>qml/controls/AccountDelegate.qml</file>
<file>qml/popups/CreateAccountDialog.qml</file>