From e6b5d5affdc762d89676f35bcca383cec5edbc48 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 7 Jul 2026 12:00:14 +0200 Subject: [PATCH] Add explorer tab and make fields copyable --- src/BlockModel.cpp | 36 +++ src/BlockModel.h | 11 + src/BlockchainBackend.cpp | 37 +++ src/BlockchainBackend.h | 3 + src/BlockchainBackend.rep | 3 + src/qml/BlockchainView.qml | 60 +++++ src/qml/controls/JsonBlock.qml | 16 +- src/qml/views/ExplorerView.qml | 446 +++++++++++++++++++++++++++++++++ src/qml/views/qmldir | 1 + 9 files changed, 610 insertions(+), 3 deletions(-) create mode 100644 src/qml/views/ExplorerView.qml diff --git a/src/BlockModel.cpp b/src/BlockModel.cpp index 2f7719b..dc32333 100644 --- a/src/BlockModel.cpp +++ b/src/BlockModel.cpp @@ -112,6 +112,7 @@ void BlockModel::appendRaw(const QString& timestamp, const QString& rawJson) const QJsonObject header = block.value(QStringLiteral("header")).toObject(); e.version = header.value(QStringLiteral("version")).toString(); + e.blockId = header.value(QStringLiteral("id")).toString(); e.parentBlock = header.value(QStringLiteral("parent_block")).toString(); const QJsonValue slotV = header.value(QStringLiteral("slot")); @@ -156,6 +157,41 @@ void BlockModel::appendRaw(const QString& timestamp, const QString& rawJson) emit countChanged(); } +QVariantMap BlockModel::findTransaction(const QString& txId) const +{ + // Normalise for comparison: trim, drop an optional 0x prefix, lowercase. + auto normalize = [](const QString& s) { + QString t = s.trimmed(); + if (t.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)) + t = t.mid(2); + return t.toLower(); + }; + + const QString needle = normalize(txId); + if (needle.isEmpty()) + return QVariantMap{{"found", false}}; + + for (const Entry& e : m_entries) { + for (const QString& txJson : e.transactions) { + QJsonParseError err{}; + const QJsonDocument doc = QJsonDocument::fromJson(txJson.toUtf8(), &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) + continue; + const QString id = doc.object().value(QStringLiteral("id")).toString(); + if (!id.isEmpty() && normalize(id) == needle) { + return QVariantMap{ + {"found", true}, + {"value", txJson}, + {"blockId", e.blockId}, + {"slot", e.slot}, + {"timestamp", e.timestamp}, + }; + } + } + } + return QVariantMap{{"found", false}}; +} + void BlockModel::clear() { if (m_entries.isEmpty()) diff --git a/src/BlockModel.h b/src/BlockModel.h index c955f81..647fed3 100644 --- a/src/BlockModel.h +++ b/src/BlockModel.h @@ -3,6 +3,7 @@ #include #include #include +#include #include // Structured model of recent blockchain blocks, fed by the backend's `newBlock` @@ -43,6 +44,15 @@ public: Q_INVOKABLE void appendRaw(const QString& timestamp, const QString& rawJson); Q_INVOKABLE void clear(); + // Search the retained blocks for a transaction whose `id` matches `txId` + // (case-insensitive, `0x`-tolerant). The blocks fed by the node's newBlock + // subscription carry a per-transaction `id`; the node cannot look a mined + // transaction up by hash (its tx-by-hash store is mempool-only and pruned + // shortly after inclusion), so this in-memory scan is how a tx copied from + // the blocks view is resolved. Returns { found, value, blockId, slot, + // timestamp }; `value` is the prettified transaction JSON when found. + QVariantMap findTransaction(const QString& txId) const; + signals: void countChanged(); @@ -58,6 +68,7 @@ private: QString proof; QString voucherCm; QString signature; + QString blockId; int txCount = 0; QStringList transactions; QString rawJson; diff --git a/src/BlockchainBackend.cpp b/src/BlockchainBackend.cpp index f540484..dbd4d43 100644 --- a/src/BlockchainBackend.cpp +++ b/src/BlockchainBackend.cpp @@ -219,6 +219,43 @@ QVariantMap BlockchainBackend::getCryptarchiaInfo() BLOCKCHAIN_MODULE_NAME, QStringLiteral("get_cryptarchia_info")))); } +QVariantMap BlockchainBackend::getBlock(QString headerIdHex) +{ + if (!m_blockchainClient) + return result::toVariantMap(result::err(QStringLiteral("Module not initialized."))); + + return result::toVariantMap(result::toLogosResult(m_blockchainClient->invokeRemoteMethod( + BLOCKCHAIN_MODULE_NAME, QStringLiteral("get_block"), headerIdHex.trimmed()))); +} + +QVariantMap BlockchainBackend::getTransaction(QString txHashHex) +{ + if (!m_blockchainClient) + return result::toVariantMap(result::err(QStringLiteral("Module not initialized."))); + + return result::toVariantMap(result::toLogosResult(m_blockchainClient->invokeRemoteMethod( + BLOCKCHAIN_MODULE_NAME, QStringLiteral("get_transaction"), txHashHex.trimmed()))); +} + +QVariantMap BlockchainBackend::findTransactionInBlocks(QString txHashHex) +{ + // Local, in-memory resolution against the blocks currently held by the + // model. The node's get_transaction only serves mempool (pending / very + // recently mined) transactions, so a tx copied from the blocks view — which + // is already mined — is looked up here instead. Returns the same shape as + // the remote calls: { success, value, ... } with block context on success. + const QVariantMap hit = m_blockModel->findTransaction(txHashHex); + QVariantMap out; + out.insert("success", hit.value("found").toBool()); + out.insert("value", hit.value("value")); + out.insert("blockId", hit.value("blockId")); + out.insert("slot", hit.value("slot")); + out.insert("timestamp", hit.value("timestamp")); + if (!out.value("success").toBool()) + out.insert("error", QStringLiteral("Not in loaded blocks.")); + return out; +} + QVariantMap BlockchainBackend::getPeerId() { if (!m_blockchainClient) diff --git a/src/BlockchainBackend.h b/src/BlockchainBackend.h index bf3ccc0..11c28f1 100644 --- a/src/BlockchainBackend.h +++ b/src/BlockchainBackend.h @@ -46,6 +46,9 @@ public slots: QVariantMap transferFunds(QString fromKeyHex, QString toKeyHex, QString amountStr) override; QVariantMap claimLeaderRewards() override; QVariantMap getCryptarchiaInfo() override; + QVariantMap getBlock(QString headerIdHex) override; + QVariantMap getTransaction(QString txHashHex) override; + QVariantMap findTransactionInBlocks(QString txHashHex) override; QVariantMap getPeerId() override; QVariantMap getClaimableVouchers() override; QVariantMap generateConfig(QString outputPath, QStringList initialPeers, int netPort, diff --git a/src/BlockchainBackend.rep b/src/BlockchainBackend.rep index 10190dd..cea0932 100644 --- a/src/BlockchainBackend.rep +++ b/src/BlockchainBackend.rep @@ -16,6 +16,9 @@ class BlockchainBackend SLOT(QVariantMap transferFunds(QString fromKeyHex, QString toKeyHex, QString amountStr)) SLOT(QVariantMap claimLeaderRewards()) SLOT(QVariantMap getCryptarchiaInfo()) + SLOT(QVariantMap getBlock(QString headerIdHex)) + SLOT(QVariantMap getTransaction(QString txHashHex)) + SLOT(QVariantMap findTransactionInBlocks(QString txHashHex)) SLOT(QVariantMap getPeerId()) SLOT(QVariantMap getClaimableVouchers()) SLOT(QVariantMap generateConfig(QString outputPath, QStringList initialPeers, int netPort, int blendPort, QString httpAddr, QString externalAddress, bool noPublicIpCheck, int deploymentMode, QString deploymentConfigPath, QString statePath)) diff --git a/src/qml/BlockchainView.qml b/src/qml/BlockchainView.qml index fdd8543..99b7d6a 100644 --- a/src/qml/BlockchainView.qml +++ b/src/qml/BlockchainView.qml @@ -333,6 +333,10 @@ Rectangle { text: qsTr("Operations") enabled: opPage.nodeRunning } + LogosTabButton { + text: qsTr("Explorer") + enabled: opPage.nodeRunning + } } StackLayout { @@ -581,6 +585,62 @@ Rectangle { } } } + + // ---- Tab 2: Explorer (block / transaction lookup) ---- + ExplorerView { + id: explorerView + nodeRunning: opPage.nodeRunning + + // Auto-detect the id kind. The node can't fetch a mined + // transaction by hash (its tx store is mempool-only, pruned + // ~10 min after inclusion), so resolve a tx in this order: + // 1. loaded blocks — the blocks view already holds each + // tx and its id, so a copied tx id resolves locally; + // 2. get_block — the id is a block header id; + // 3. get_transaction — a still-pending mempool tx. + onSearchRequested: function(id) { + if (!root.backend) return + + // Every backend call is remoted through QtRO, so each + // must be resolved via logos.watch (even the local scan, + // whose search runs synchronously on the source side). + + // Step 1: scan the loaded blocks for the tx by its id. + logos.watch( + root.backend.findTransactionInBlocks(id), + function(local) { + if (local.success) { + explorerView.setTransactionResult(id, local.value, local.slot, local.blockId) + return + } + // Step 2: block by header id. + logos.watch( + root.backend.getBlock(id), + function(blockResult) { + if (blockResult.success) { + explorerView.setBlockResult(id, blockResult.value) + return + } + // Step 3: pending transaction via the node. + logos.watch( + root.backend.getTransaction(id), + function(txResult) { + if (txResult.success) + explorerView.setTransactionResult(id, txResult.value) + else + explorerView.setNotFound(id) + }, + function(error) { explorerView.setError(id, _d.errorText(error)) } + ) + }, + function(error) { explorerView.setError(id, _d.errorText(error)) } + ) + }, + function(error) { explorerView.setError(id, _d.errorText(error)) } + ) + } + onCopyToClipboard: (text) => root.copyText(text) + } } // Sidebar nav entry used by the Operations tab. `pinnable` adds a diff --git a/src/qml/controls/JsonBlock.qml b/src/qml/controls/JsonBlock.qml index 4d67eb4..f7c427d 100644 --- a/src/qml/controls/JsonBlock.qml +++ b/src/qml/controls/JsonBlock.qml @@ -5,7 +5,9 @@ import Logos.Theme import Logos.Controls // A boxed, monospace, wrapping JSON/text block. Used for prettified payloads -// and raw fallbacks. +// and raw fallbacks. Rendered with a read-only TextEdit so the content can be +// selected with the mouse and copied (Cmd/Ctrl+C), in addition to the dedicated +// copy buttons elsewhere. Rectangle { id: root @@ -18,15 +20,23 @@ Rectangle { border.color: Theme.palette.border border.width: 1 - LogosText { + TextEdit { id: jsonText anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: Theme.spacing.small text: root.json + readOnly: true + selectByMouse: true + persistentSelection: true + // Treat the payload as literal text; never interpret it as rich text. + textFormat: TextEdit.PlainText + color: Theme.palette.text + selectionColor: Theme.palette.overlayOrange + selectedTextColor: Theme.palette.text font.pixelSize: Theme.typography.secondaryText font.family: "monospace" - wrapMode: Text.WrapAnywhere + wrapMode: TextEdit.WrapAnywhere } } diff --git a/src/qml/views/ExplorerView.qml b/src/qml/views/ExplorerView.qml new file mode 100644 index 0000000..f198a9b --- /dev/null +++ b/src/qml/views/ExplorerView.qml @@ -0,0 +1,446 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Logos.Theme +import Logos.Controls + +import "../controls" + +// Block/transaction explorer. The user pastes a block header id or a +// transaction hash; the lookup is auto-detected (block first, then tx — +// both are hex hashes and can't be told apart by shape) and the matching +// entity is rendered with every field copyable. +// +// Orchestration lives in BlockchainView: `searchRequested` triggers a +// get_block; on a miss it falls back to get_transaction. Results flow back +// through setBlockResult / setTransactionResult / setNotFound / setError. +ColumnLayout { + id: root + + signal searchRequested(string id) + signal copyToClipboard(string text) + + property bool nodeRunning: false + + // ---- Result state (driven by BlockchainView) ---- + // kind: "" (none) | "block" | "transaction" | "notfound" | "error" + property string kind: "" + property string queriedId: "" + property string rawJson: "" + property string errorText: "" + property bool busy: false + + // Block context for a transaction resolved from a loaded block (empty when + // the tx came from the node's mempool lookup). + property string txSlot: "" + property string txBlockId: "" + + // Parsed block fields (populated when kind === "block"). + property var block: null + + function _reset() { + root.kind = "" + root.rawJson = "" + root.errorText = "" + root.block = null + root.txSlot = "" + root.txBlockId = "" + } + + // Called by BlockchainView when a get_block succeeds. `value` is the JSON + // string returned by the module (tolerant of a few nesting shapes, mirroring + // BlockModel::appendRaw). + function setBlockResult(id, value) { + root.busy = false + root._reset() + root.queriedId = id + root.kind = "block" + root.rawJson = value + root.block = parseBlock(value) + } + + // `slot` / `blockId` are optional — supplied when the tx was resolved from + // a loaded block, omitted for a pending mempool tx. + function setTransactionResult(id, value, slot, blockId) { + root.busy = false + root._reset() + root.queriedId = id + root.kind = "transaction" + root.rawJson = value + root.txSlot = slot !== undefined && slot !== null ? String(slot) : "" + root.txBlockId = blockId !== undefined && blockId !== null ? String(blockId) : "" + } + + function setNotFound(id) { + root.busy = false + root._reset() + root.queriedId = id + root.kind = "notfound" + } + + function setError(id, message) { + root.busy = false + root._reset() + root.queriedId = id + root.kind = "error" + root.errorText = message + } + + // Tolerant block parser mirroring BlockModel::appendRaw: + // { "block": "" } | { "block": {...} } | { "header": ... } + // Returns a normalised object { header, slot, version, parentBlock, blockRoot, + // signature, proof, entropy, leaderKey, voucherCm, transactions:[jsonStr] } or null. + function parseBlock(s) { + var obj = null + try { obj = JSON.parse(s) } catch (e) { return null } + if (!obj || typeof obj !== "object") return null + + var b = null + if (obj.block !== undefined) { + if (typeof obj.block === "string") { + try { b = JSON.parse(obj.block) } catch (e) { b = null } + } else if (typeof obj.block === "object") { + b = obj.block + } + } else if (obj.header !== undefined) { + b = obj + } + if (!b || typeof b !== "object") return null + + var header = b.header || {} + var pol = header.proof_of_leadership || {} + var txs = [] + var arr = b.transactions || [] + for (var i = 0; i < arr.length; ++i) { + try { txs.push(JSON.stringify(arr[i])) } + catch (e) { txs.push(String(arr[i])) } + } + return { + slot: header.slot !== undefined ? String(header.slot) : "", + version: header.version !== undefined ? String(header.version) : "", + parentBlock: header.parent_block || "", + blockRoot: header.block_root || "", + signature: b.signature || "", + proof: pol.proof || "", + entropy: pol.entropy_contribution || "", + leaderKey: pol.leader_key || "", + voucherCm: pol.voucher_cm || "", + transactions: txs + } + } + + function pretty(s) { + try { return JSON.stringify(JSON.parse(s), null, 2) } catch (e) { return s } + } + + function doSearch() { + var id = idField.text.trim() + if (id.length === 0) return + root.busy = true + root.searchRequested(id) + } + + spacing: Theme.spacing.large + + // ---- Search bar ---- + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: searchCol.implicitHeight + 2 * Theme.spacing.large + color: Theme.palette.backgroundTertiary + radius: Theme.spacing.radiusLarge + border.color: Theme.palette.border + border.width: 1 + + ColumnLayout { + id: searchCol + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacing.large + spacing: Theme.spacing.small + + RowLayout { + Layout.fillWidth: true + LogosText { + text: qsTr("Explorer") + font.pixelSize: Theme.typography.secondaryText + font.bold: true + } + Item { Layout.fillWidth: true } + InfoButton { + Layout.alignment: Qt.AlignVCenter + text: qsTr("Paste a block header id or a transaction hash, then press Search. The lookup is auto-detected: it tries a block first, then a transaction.") + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Theme.spacing.small + + LogosTextField { + id: idField + Layout.fillWidth: true + Layout.preferredHeight: 30 + placeholderText: qsTr("Block id or transaction hash (hex)") + enabled: root.nodeRunning && !root.busy + + // LogosTextField wraps a TextInput (no `accepted` signal of + // its own); connect to the inner input to search on Enter. + Connections { + target: idField.textInput + function onAccepted() { root.doSearch() } + } + } + + LogosButton { + Layout.preferredWidth: 90 + Layout.preferredHeight: 30 + text: root.busy ? qsTr("…") : qsTr("Search") + enabled: root.nodeRunning && !root.busy && idField.text.trim().length > 0 + onClicked: root.doSearch() + } + } + + LogosText { + Layout.fillWidth: true + visible: !root.nodeRunning + text: qsTr("Start the node to look up blocks and transactions.") + color: Theme.palette.textSecondary + font.pixelSize: Theme.typography.secondaryText + wrapMode: Text.WordWrap + } + } + } + + // ---- Status line (not found / error) ---- + LogosText { + Layout.fillWidth: true + visible: root.kind === "notfound" || root.kind === "error" + text: root.kind === "notfound" + ? qsTr("Nothing found for “%1”.\nBlocks are looked up by header id. Transactions resolve from the blocks currently loaded above, or from the node's mempool while still pending — mined transactions can't be fetched by hash, so open their block instead.").arg(root.queriedId) + : root.errorText + color: root.kind === "notfound" ? Theme.palette.textSecondary : Theme.palette.error + font.pixelSize: Theme.typography.secondaryText + wrapMode: Text.WordWrap + } + + // ---- Result area ---- + ScrollView { + id: resultScroll + Layout.fillWidth: true + Layout.fillHeight: true + visible: root.kind === "block" || root.kind === "transaction" + clip: true + + ColumnLayout { + width: resultScroll.availableWidth + spacing: Theme.spacing.large + + // ---- Block result ---- + Rectangle { + Layout.fillWidth: true + visible: root.kind === "block" + Layout.preferredHeight: blockCol.implicitHeight + 2 * Theme.spacing.large + color: Theme.palette.backgroundTertiary + radius: Theme.spacing.radiusLarge + border.color: Theme.palette.border + border.width: 1 + + ColumnLayout { + id: blockCol + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacing.large + spacing: Theme.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Theme.spacing.small + LogosText { + text: qsTr("Block") + font.pixelSize: Theme.typography.secondaryText + font.bold: true + } + // slot / version badges + LogosText { + visible: root.block && root.block.slot.length > 0 + text: qsTr("slot %1").arg(root.block ? root.block.slot : "") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + Rectangle { + visible: root.block && root.block.version.length > 0 + radius: Theme.spacing.radiusSmall + color: Theme.palette.backgroundSecondary + border.color: Theme.palette.border + border.width: 1 + implicitWidth: verText.implicitWidth + 2 * Theme.spacing.small + implicitHeight: verText.implicitHeight + Theme.spacing.tiny + LogosText { + id: verText + anchors.centerIn: parent + text: root.block ? root.block.version : "" + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + } + Item { Layout.fillWidth: true } + LogosCopyButton { + ToolTip.visible: hovered + ToolTip.text: qsTr("Copy raw block JSON") + onCopyText: root.copyToClipboard(root.pretty(root.rawJson)) + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Theme.palette.borderSecondary + } + + HashRow { + label: qsTr("Block id"); value: root.queriedId + onCopyRequested: (t) => root.copyToClipboard(t) + } + HashRow { + label: qsTr("Parent block"); value: root.block ? root.block.parentBlock : "" + onCopyRequested: (t) => root.copyToClipboard(t) + } + HashRow { + label: qsTr("Block root"); value: root.block ? root.block.blockRoot : "" + onCopyRequested: (t) => root.copyToClipboard(t) + } + HashRow { + label: qsTr("Signature"); value: root.block ? root.block.signature : "" + onCopyRequested: (t) => root.copyToClipboard(t) + } + + LogosText { + text: qsTr("Proof of leadership") + font.pixelSize: Theme.typography.secondaryText + font.bold: true + Layout.topMargin: Theme.spacing.small + } + HashRow { + label: qsTr("Leader key"); value: root.block ? root.block.leaderKey : "" + onCopyRequested: (t) => root.copyToClipboard(t) + } + HashRow { + label: qsTr("Entropy"); value: root.block ? root.block.entropy : "" + onCopyRequested: (t) => root.copyToClipboard(t) + } + HashRow { + label: qsTr("Proof"); value: root.block ? root.block.proof : "" + onCopyRequested: (t) => root.copyToClipboard(t) + } + HashRow { + label: qsTr("Voucher cm"); value: root.block ? root.block.voucherCm : "" + onCopyRequested: (t) => root.copyToClipboard(t) + } + + LogosText { + text: qsTr("Transactions (%1)").arg(root.block ? root.block.transactions.length : 0) + font.pixelSize: Theme.typography.secondaryText + font.bold: true + Layout.topMargin: Theme.spacing.small + } + ColumnLayout { + Layout.fillWidth: true + spacing: Theme.spacing.small + + Repeater { + model: root.block ? root.block.transactions : [] + delegate: TransactionDelegate { + required property int index + required property string modelData + Layout.fillWidth: true + idx: index + json: modelData + onCopyToClipboard: (t) => root.copyToClipboard(t) + } + } + LogosText { + visible: root.block && root.block.transactions.length === 0 + text: qsTr("No transactions in this block.") + color: Theme.palette.textSecondary + font.pixelSize: Theme.typography.secondaryText + } + } + } + } + + // ---- Transaction result ---- + Rectangle { + Layout.fillWidth: true + visible: root.kind === "transaction" + Layout.preferredHeight: txCol.implicitHeight + 2 * Theme.spacing.large + color: Theme.palette.backgroundTertiary + radius: Theme.spacing.radiusLarge + border.color: Theme.palette.border + border.width: 1 + + ColumnLayout { + id: txCol + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: Theme.spacing.large + spacing: Theme.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Theme.spacing.small + LogosText { + text: qsTr("Transaction") + font.pixelSize: Theme.typography.secondaryText + font.bold: true + } + LogosText { + visible: root.txSlot.length > 0 + text: qsTr("in block · slot %1").arg(root.txSlot) + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + Item { Layout.fillWidth: true } + LogosCopyButton { + ToolTip.visible: hovered + ToolTip.text: qsTr("Copy raw transaction JSON") + onCopyText: root.copyToClipboard(root.pretty(root.rawJson)) + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Theme.palette.borderSecondary + } + + HashRow { + label: qsTr("Transaction id"); value: root.queriedId + onCopyRequested: (t) => root.copyToClipboard(t) + } + HashRow { + visible: root.txBlockId.length > 0 + label: qsTr("Block id"); value: root.txBlockId + onCopyRequested: (t) => root.copyToClipboard(t) + } + + TransactionDelegate { + Layout.fillWidth: true + idx: 0 + json: root.rawJson + open: true + onCopyToClipboard: (t) => root.copyToClipboard(t) + } + } + } + } + } + + Item { + Layout.fillHeight: true + visible: root.kind === "" || root.kind === "notfound" || root.kind === "error" + } +} diff --git a/src/qml/views/qmldir b/src/qml/views/qmldir index 3696ca1..80d61f7 100644 --- a/src/qml/views/qmldir +++ b/src/qml/views/qmldir @@ -6,6 +6,7 @@ NodeInfoView 1.0 NodeInfoView.qml AccountsView 1.0 AccountsView.qml TransferView 1.0 TransferView.qml LeaderRewardsView 1.0 LeaderRewardsView.qml +ExplorerView 1.0 ExplorerView.qml GenerateConfigView 1.0 GenerateConfigView.qml ConfigChoiceView 1.0 ConfigChoiceView.qml SetConfigPathView 1.0 SetConfigPathView.qml