From 1e515b9239c66756210cd87d507e43d3975499f2 Mon Sep 17 00:00:00 2001 From: D33r-Gee Date: Wed, 2 Oct 2024 14:41:51 -0700 Subject: [PATCH 1/3] qml: Introduce UI Flow for Loading Snapshot - This introduce the UI flow to load a AssumeUTXO snapshot - It modifies the connection settings - Adds a SnapshotLoadSettings file, Icon, and modified progress bar. - Also it adds error page on snapshotloading failure --- bitcoin | 2 +- qml/bitcoin_qml.qrc | 6 + qml/components/ConnectionSettings.qml | 31 +++ qml/components/SnapshotLoadSettings.qml | 269 ++++++++++++++++++++ qml/controls/GreenCheckIcon.qml | 11 + qml/controls/Header.qml | 2 + qml/controls/ProgressIndicator.qml | 3 +- qml/controls/Theme.qml | 3 + qml/models/chainmodel.cpp | 42 +++ qml/models/chainmodel.h | 6 +- qml/models/nodemodel.cpp | 99 +++++++ qml/models/nodemodel.h | 43 +++- qml/pages/settings/SettingsConnection.qml | 12 + qml/pages/settings/SettingsSnapshotLoad.qml | 38 +++ 14 files changed, 562 insertions(+), 5 deletions(-) create mode 100644 qml/components/SnapshotLoadSettings.qml create mode 100644 qml/controls/GreenCheckIcon.qml create mode 100644 qml/pages/settings/SettingsSnapshotLoad.qml diff --git a/bitcoin b/bitcoin index 8ffbd7b778..34e536d425 160000 --- a/bitcoin +++ b/bitcoin @@ -1 +1 @@ -Subproject commit 8ffbd7b778600aa1e824027f1e675929a4240856 +Subproject commit 34e536d42529ac6f4b70b1802ce709282de2e28a diff --git a/qml/bitcoin_qml.qrc b/qml/bitcoin_qml.qrc index 2dd1f5f2f8..b93d99c524 100644 --- a/qml/bitcoin_qml.qrc +++ b/qml/bitcoin_qml.qrc @@ -18,6 +18,7 @@ components/ProxySettings.qml components/StorageLocations.qml components/Separator.qml + components/SnapshotLoadSettings.qml components/StorageOptions.qml components/StorageSettings.qml components/ThemeSettings.qml @@ -31,6 +32,7 @@ controls/CoreTextField.qml controls/ExternalLink.qml controls/FocusBorder.qml + controls/GreenCheckIcon.qml controls/Header.qml controls/Icon.qml controls/IconButton.qml @@ -82,6 +84,7 @@ pages/settings/SettingsDeveloper.qml pages/settings/SettingsDisplay.qml pages/settings/SettingsProxy.qml + pages/settings/SettingsSnapshotLoad.qml pages/settings/SettingsStorage.qml pages/settings/SettingsTheme.qml pages/wallet/Activity.qml @@ -125,6 +128,9 @@ res/icons/circle-green-check.png res/icons/circle-red-cross.png res/icons/coinbase.png + res/icons/circle-file.png + res/icons/circle-green-check.png + res/icons/circle-red-cross.png res/icons/cross.png res/icons/ellipsis.png res/icons/error.png diff --git a/qml/components/ConnectionSettings.qml b/qml/components/ConnectionSettings.qml index 4a39574819..718a751c12 100644 --- a/qml/components/ConnectionSettings.qml +++ b/qml/components/ConnectionSettings.qml @@ -10,7 +10,38 @@ import "../controls" ColumnLayout { id: root signal next + signal gotoSnapshot + property bool onboarding: false + property bool snapshotImportCompleted: onboarding ? false : chainModel.isSnapshotActive + property bool isIBDCompleted: nodeModel.isIBDCompleted spacing: 4 + Setting { + id: gotoSnapshot + visible: !snapshotImportCompleted && !root.isIBDCompleted + Layout.fillWidth: true + header: qsTr("Load snapshot") + description: qsTr("Instant use with background sync") + actionItem: Item { + width: 26 + height: 26 + CaretRightIcon { + anchors.centerIn: parent + visible: !snapshotImportCompleted + color: gotoSnapshot.stateColor + } + GreenCheckIcon { + anchors.centerIn: parent + visible: snapshotImportCompleted + color: Theme.color.transparent + size: 30 + } + } + onClicked: root.gotoSnapshot() + } + Separator { + visible: !snapshotImportCompleted && !root.isIBDCompleted + Layout.fillWidth: true + } Setting { Layout.fillWidth: true header: qsTr("Enable listening") diff --git a/qml/components/SnapshotLoadSettings.qml b/qml/components/SnapshotLoadSettings.qml new file mode 100644 index 0000000000..cfcf3a97a9 --- /dev/null +++ b/qml/components/SnapshotLoadSettings.qml @@ -0,0 +1,269 @@ +// Copyright (c) 2023-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Dialogs + +import "../controls" + +ColumnLayout { + id: columnLayout + signal back + property bool snapshotLoading: nodeModel.snapshotLoading + property bool snapshotLoaded: nodeModel.isSnapshotLoaded + property bool snapshotImportCompleted: onboarding ? false : chainModel.isSnapshotActive + property bool onboarding: false + property bool snapshotVerified: onboarding ? false : chainModel.isSnapshotActive + property string snapshotFileName: "" + property var snapshotInfo: (snapshotVerified || snapshotLoaded) ? chainModel.getSnapshotInfo() : ({}) + property string selectedFile: "" + property bool headersSynced: nodeModel.headersSynced + property bool snapshotError: nodeModel.snapshotError + + width: Math.min(parent.width, 450) + anchors.horizontalCenter: parent.horizontalCenter + + StackLayout { + id: settingsStack + currentIndex: onboarding ? 0 : snapshotLoaded ? 2 : snapshotVerified ? 2 : snapshotLoading ? 1 : snapshotError ? 3 : 0 + + ColumnLayout { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(parent.width, 450) + + Image { + Layout.alignment: Qt.AlignCenter + source: "image://images/circle-file" + + sourceSize.width: 200 + sourceSize.height: 200 + } + + Header { + Layout.fillWidth: true + Layout.topMargin: 20 + headerBold: true + header: qsTr("Load snapshot") + descriptionBold: false + descriptionColor: Theme.color.neutral6 + descriptionSize: 17 + descriptionLineHeight: 1.1 + description: qsTr("You can start using the application more quickly by loading a recent transaction snapshot." + + " It will be automatically verified in the background.") + } + + CoreText { + Layout.fillWidth: true + Layout.topMargin: 20 + color: Theme.color.neutral6 + font.pixelSize: 17 + visible: !headersSynced && !onboarding + text: !headersSynced + ? qsTr("Please wait for headers to sync before loading a snapshot.") + : qsTr("") + wrap: true + wrapMode: Text.WordWrap + } + + ContinueButton { + Layout.preferredWidth: Math.min(300, columnLayout.width - 2 * Layout.leftMargin) + Layout.topMargin: 40 + Layout.leftMargin: 20 + Layout.rightMargin: Layout.leftMargin + Layout.bottomMargin: 20 + Layout.alignment: Qt.AlignCenter + text: qsTr("Choose snapshot file") + enabled: headersSynced || onboarding + onClicked: fileDialog.open() + } + + FileDialog { + id: fileDialog + currentFolder: optionsModel.getDefaultDataDirectory + nameFilters: ["Snapshot files (*.dat)", "All files (*)"] + onAccepted: { + snapshotFileName = selectedFile + console.log("snapshotFileName", snapshotFileName) + if (!onboarding) { + nodeModel.snapshotLoadThread(snapshotFileName) + } else { + nodeModel.setSnapshotFilePath(snapshotFileName) + back() + } + } + } + } + + ColumnLayout { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(parent.width, 450) + + Image { + Layout.alignment: Qt.AlignCenter + source: "image://images/circle-file" + + sourceSize.width: 200 + sourceSize.height: 200 + } + + Header { + Layout.fillWidth: true + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + header: qsTr("Loading Snapshot") + description: qsTr("This might take a while...") + } + + ProgressIndicator { + id: progressIndicator + Layout.topMargin: 20 + width: 200 + height: 20 + progress: nodeModel.snapshotProgress + Layout.alignment: Qt.AlignCenter + progressColor: Theme.color.blue + } + } + + ColumnLayout { + id: loadedSnapshotColumn + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(parent.width, 450) + + Image { + Layout.alignment: Qt.AlignCenter + source: "image://images/circle-green-check" + + sourceSize.width: 60 + sourceSize.height: 60 + } + + Header { + Layout.fillWidth: true + Layout.topMargin: 20 + headerBold: true + header: qsTr("Snapshot Loaded") + descriptionBold: false + descriptionColor: Theme.color.neutral6 + descriptionSize: 17 + descriptionLineHeight: 1.1 + description: snapshotInfo && snapshotInfo["date"] ? + qsTr("It contains unspent transactions up to %1. Next, transactions will be verified and newer transactions downloaded.").arg(snapshotInfo["date"]) : + qsTr("It contains transactions up to DEBUG. Newer transactions still need to be downloaded." + + " The data will be verified in the background.") + } + + ContinueButton { + Layout.preferredWidth: Math.min(300, columnLayout.width - 2 * Layout.leftMargin) + Layout.topMargin: 40 + Layout.alignment: Qt.AlignCenter + text: qsTr("Done") + onClicked: { + chainModel.isSnapshotActiveChanged() + back() + } + } + + Setting { + id: viewDetails + Layout.alignment: Qt.AlignCenter + header: qsTr("View details") + actionItem: CaretRightIcon { + id: caretIcon + color: viewDetails.stateColor + rotation: viewDetails.expanded ? 90 : 0 + Behavior on rotation { NumberAnimation { duration: 200 } } + } + + property bool expanded: false + + onClicked: { + expanded = !expanded + } + } + + ColumnLayout { + id: detailsContent + visible: viewDetails.expanded + Layout.preferredWidth: Math.min(300, parent.width - 2 * Layout.leftMargin) + Layout.alignment: Qt.AlignCenter + Layout.leftMargin: 80 + Layout.rightMargin: 80 + Layout.topMargin: 10 + spacing: 10 + // TODO: make sure the block height number aligns right + RowLayout { + CoreText { + text: qsTr("Block Height:") + Layout.alignment: Qt.AlignLeft + font.pixelSize: 14 + } + CoreText { + text: snapshotInfo && snapshotInfo["height"] ? + snapshotInfo["height"] : qsTr("DEBUG") + Layout.alignment: Qt.AlignRight + font.pixelSize: 14 + } + } + Separator { Layout.fillWidth: true } + ColumnLayout { + Layout.fillWidth: true + spacing: 5 + CoreText { + text: qsTr("Hash:") + font.pixelSize: 14 + } + CoreText { + text: snapshotInfo && snapshotInfo["hashSerializedFirstHalf"] ? + snapshotInfo["hashSerializedFirstHalf"] + "\n" + snapshotInfo["hashSerializedSecondHalf"] : + qsTr("DEBUG") + Layout.fillWidth: true + font.pixelSize: 14 + textFormat: Text.PlainText + } + } + } + } + + ColumnLayout { + id: snapshotErrorColumn + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(parent.width, 450) + + Image { + Layout.alignment: Qt.AlignCenter + source: "image://images/circle-red-cross" + + sourceSize.width: 60 + sourceSize.height: 60 + } + + Header { + Layout.fillWidth: true + Layout.topMargin: 20 + headerBold: true + header: qsTr("Snapshot Could Not Be Verified") + descriptionBold: false + descriptionColor: Theme.color.neutral6 + descriptionSize: 17 + descriptionLineHeight: 1.1 + description: qsTr("There was a problem with the transactions in the snapshot. Please try a different file.") + } + + ContinueButton { + Layout.preferredWidth: Math.min(300, columnLayout.width - 2 * Layout.leftMargin) + Layout.topMargin: 40 + Layout.alignment: Qt.AlignCenter + text: qsTr("OK") + onClicked: { + nodeModel.setSnapshotError(false) + back() + } + } + } + } +} diff --git a/qml/controls/GreenCheckIcon.qml b/qml/controls/GreenCheckIcon.qml new file mode 100644 index 0000000000..02977857b2 --- /dev/null +++ b/qml/controls/GreenCheckIcon.qml @@ -0,0 +1,11 @@ +// Copyright (c) 2023 - present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Icon { + source: "image://images/green-check" + size: 30 +} diff --git a/qml/controls/Header.qml b/qml/controls/Header.qml index f3c4c0c3e3..ece49234d2 100644 --- a/qml/controls/Header.qml +++ b/qml/controls/Header.qml @@ -25,6 +25,7 @@ ColumnLayout { property int subtextSize: 15 property color subtextColor: Theme.color.neutral9 property bool wrap: true + property real descriptionLineHeight: 1 spacing: 0 Loader { @@ -60,6 +61,7 @@ ColumnLayout { text: root.description horizontalAlignment: root.center ? Text.AlignHCenter : Text.AlignLeft wrapMode: wrap ? Text.WordWrap : Text.NoWrap + lineHeight: root.descriptionLineHeight Behavior on color { ColorAnimation { duration: 150 } diff --git a/qml/controls/ProgressIndicator.qml b/qml/controls/ProgressIndicator.qml index 117a4baebb..9d6d62d329 100644 --- a/qml/controls/ProgressIndicator.qml +++ b/qml/controls/ProgressIndicator.qml @@ -7,6 +7,7 @@ import QtQuick.Controls 2.15 Control { property real progress: 0 + property color progressColor: Theme.color.orange Behavior on progress { NumberAnimation { easing.type: Easing.Bezier @@ -26,7 +27,7 @@ Control { width: contentItem.width height: contentItem.height radius: contentItem.radius - color: Theme.color.orange + color: progressColor } } } diff --git a/qml/controls/Theme.qml b/qml/controls/Theme.qml index f57e152cbd..3c7621c2b5 100644 --- a/qml/controls/Theme.qml +++ b/qml/controls/Theme.qml @@ -27,6 +27,7 @@ Control { required property color blue required property color amber required property color purple + required property color transparent required property color neutral0 required property color neutral1 required property color neutral2 @@ -59,6 +60,7 @@ Control { blue: "#3CA3DE" amber: "#C9B500" purple: "#C075DC" + transparent: "#00000000" neutral0: "#000000" neutral1: "#1A1A1A" neutral2: "#2D2D2D" @@ -91,6 +93,7 @@ Control { blue: "#2D9CDB" amber: "#C9B500" purple: "#BB6BD9" + transparent: "#00000000" neutral0: "#FFFFFF" neutral1: "#F8F8F8" neutral2: "#F4F4F4" diff --git a/qml/models/chainmodel.cpp b/qml/models/chainmodel.cpp index ce3a6b12ae..c79cfc170c 100644 --- a/qml/models/chainmodel.cpp +++ b/qml/models/chainmodel.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include using interfaces::FoundBlock; @@ -106,3 +109,42 @@ void ChainModel::setCurrentTimeRatio() Q_EMIT timeRatioListChanged(); } + +// TODO: Change this once a better solution has been found. +// Using hardcoded snapshot info to display in SnapshotSettings.qml +QVariantMap ChainModel::getSnapshotInfo() { + QVariantMap snapshot_info; + + std::vector available_heights = Params().GetAvailableSnapshotHeights(); + if (!available_heights.empty()) { + // Get the highest height snapshot (last element in the vector) + const int height = available_heights.back(); + std::optional maybe_snapshot = Params().AssumeutxoForHeight(height); + + if (maybe_snapshot.has_value()) { + const auto& latest_snapshot = maybe_snapshot.value(); + const auto& hash_serialized = latest_snapshot.hash_serialized; + + // Get block time using the interfaces pattern if we already know the header + int64_t block_time = 0; + bool found_block = m_chain.findBlock(latest_snapshot.blockhash, FoundBlock().time(block_time)); + + QString fullHash = QString::fromStdString(hash_serialized.ToString()); + + int midPoint = fullHash.length() / 2; + QString firstHalf = fullHash.left(midPoint); + QString secondHalf = fullHash.mid(midPoint); + + snapshot_info["height"] = height; + snapshot_info["hashSerializedFirstHalf"] = firstHalf; + snapshot_info["hashSerializedSecondHalf"] = secondHalf; + if (found_block && block_time > 0) { + snapshot_info["date"] = QDateTime::fromSecsSinceEpoch(block_time).toString("MMMM d yyyy"); + } else { + snapshot_info["date"] = "Unknown"; + } + } + } + + return snapshot_info; +} diff --git a/qml/models/chainmodel.h b/qml/models/chainmodel.h index 9318510eda..6a5124be7f 100644 --- a/qml/models/chainmodel.h +++ b/qml/models/chainmodel.h @@ -27,6 +27,7 @@ class ChainModel : public QObject Q_PROPERTY(quint64 assumedBlockchainSize READ assumedBlockchainSize CONSTANT) Q_PROPERTY(quint64 assumedChainstateSize READ assumedChainstateSize CONSTANT) Q_PROPERTY(QVariantList timeRatioList READ timeRatioList NOTIFY timeRatioListChanged) + Q_PROPERTY(bool isSnapshotActive READ isSnapshotActive NOTIFY isSnapshotActiveChanged) public: explicit ChainModel(interfaces::Chain& chain); @@ -36,11 +37,13 @@ class ChainModel : public QObject quint64 assumedBlockchainSize() const { return m_assumed_blockchain_size; }; quint64 assumedChainstateSize() const { return m_assumed_chainstate_size; }; QVariantList timeRatioList() const { return m_time_ratio_list; }; - + bool isSnapshotActive() const { return m_chain.hasAssumedValidChain(); }; int timestampAtMeridian(); void setCurrentTimeRatio(); + Q_INVOKABLE QVariantMap getSnapshotInfo(); + public Q_SLOTS: void setTimeRatioList(int new_time); void setTimeRatioListInitial(); @@ -48,6 +51,7 @@ public Q_SLOTS: Q_SIGNALS: void timeRatioListChanged(); void currentNetworkNameChanged(); + void isSnapshotActiveChanged(); private: QString m_current_network_name; diff --git a/qml/models/nodemodel.cpp b/qml/models/nodemodel.cpp index a3abd02188..a413b926fd 100644 --- a/qml/models/nodemodel.cpp +++ b/qml/models/nodemodel.cpp @@ -16,8 +16,12 @@ #include #include +#include #include #include +#include +#include +#include NodeModel::NodeModel(interfaces::Node& node) : m_node{node} @@ -25,6 +29,7 @@ NodeModel::NodeModel(interfaces::Node& node) ConnectToBlockTipSignal(); ConnectToNumConnectionsChangedSignal(); ConnectToBannedListChangedSignal(); + ConnectToSnapshotLoadProgressSignal(); } void NodeModel::setBlockTipHeight(int new_height) @@ -83,6 +88,14 @@ void NodeModel::setVerificationProgress(double new_progress) if (new_progress != m_verification_progress) { setRemainingSyncTime(new_progress); + if (new_progress >= 0.00014) { + setHeadersSynced(true); + } + + if (new_progress >= 0.999) { + setIsIBDCompleted(true); + } + m_verification_progress = new_progress; Q_EMIT verificationProgressChanged(); } @@ -220,3 +233,89 @@ void NodeModel::ConnectToBannedListChangedSignal() }); }); } + +void NodeModel::ConnectToSnapshotLoadProgressSignal() +{ + assert(!m_handler_snapshot_load_progress); + + m_handler_snapshot_load_progress = m_node.handleSnapshotLoadProgress( + [this](double progress) { + setSnapshotProgress(progress); + }); +} + +void NodeModel::snapshotLoadThread(QString path_file) { + m_snapshot_loading = true; + Q_EMIT snapshotLoadingChanged(); + + path_file = QUrl(path_file).toLocalFile(); + + QThread* snapshot_thread = QThread::create([this, path_file]() { + const fs::path path_file_fs = fs::u8path(path_file.toStdString()); + auto snapshot = m_node.snapshot(path_file_fs); + + bool result = snapshot ? snapshot->activate() : false; + + if (!result) { + QMetaObject::invokeMethod(this, [this]() { + m_snapshot_loading = false; + Q_EMIT snapshotLoadingChanged(); + m_snapshot_error = true; + Q_EMIT snapshotErrorChanged(); + }, Qt::QueuedConnection); + } else { + QMetaObject::invokeMethod(this, [this, result]() { + m_snapshot_loaded = true; + Q_EMIT snapshotLoaded(result); + Q_EMIT snapshotLoadingChanged(); + }, Qt::QueuedConnection); + } + }); + + connect(snapshot_thread, &QThread::finished, snapshot_thread, &QThread::deleteLater); + + snapshot_thread->start(); +} + +void NodeModel::setSnapshotProgress(double new_progress) { + if (new_progress != m_snapshot_progress) { + m_snapshot_progress = new_progress; + Q_EMIT snapshotProgressChanged(); + } +} + +void NodeModel::setHeadersSynced(bool new_synced) { + if (new_synced != m_headers_synced) { + m_headers_synced = new_synced; + Q_EMIT headersSyncedChanged(); + checkAndLoadSnapshot(); + } +} + +void NodeModel::setIsIBDCompleted(bool new_completed) { + if (new_completed != m_is_ibd_completed) { + m_is_ibd_completed = new_completed; + Q_EMIT isIBDCompletedChanged(); + } +} + +void NodeModel::setSnapshotFilePath(const QString& new_path) { + if (new_path != m_snapshot_file_path) { + m_snapshot_file_path = new_path; + Q_EMIT snapshotFilePathChanged(); + } +} + +void NodeModel::checkAndLoadSnapshot() +{ + if (m_headers_synced && !m_snapshot_file_path.isEmpty()) { + snapshotLoadThread(m_snapshot_file_path); + } +} + +void NodeModel::setSnapshotError(bool new_error) { + if (new_error != m_snapshot_error) { + m_snapshot_error = new_error; + Q_EMIT snapshotErrorChanged(); + } +} diff --git a/qml/models/nodemodel.h b/qml/models/nodemodel.h index ea4a7ade8f..740a9884c5 100644 --- a/qml/models/nodemodel.h +++ b/qml/models/nodemodel.h @@ -37,6 +37,13 @@ class NodeModel : public QObject Q_PROPERTY(double verificationProgress READ verificationProgress NOTIFY verificationProgressChanged) Q_PROPERTY(bool pause READ pause WRITE setPause NOTIFY pauseChanged) Q_PROPERTY(bool faulted READ errorState WRITE setErrorState NOTIFY errorStateChanged) + Q_PROPERTY(double snapshotProgress READ snapshotProgress WRITE setSnapshotProgress NOTIFY snapshotProgressChanged) + Q_PROPERTY(bool snapshotLoading READ snapshotLoading NOTIFY snapshotLoadingChanged) + Q_PROPERTY(bool isSnapshotLoaded READ isSnapshotLoaded NOTIFY snapshotLoaded) + Q_PROPERTY(bool headersSynced READ headersSynced WRITE setHeadersSynced NOTIFY headersSyncedChanged) + Q_PROPERTY(bool isIBDCompleted READ isIBDCompleted WRITE setIsIBDCompleted NOTIFY isIBDCompletedChanged) + Q_PROPERTY(QString snapshotFilePath READ snapshotFilePath WRITE setSnapshotFilePath NOTIFY snapshotFilePathChanged) + Q_PROPERTY(bool snapshotError READ snapshotError WRITE setSnapshotError NOTIFY snapshotErrorChanged) public: explicit NodeModel(interfaces::Node& node); @@ -55,6 +62,18 @@ class NodeModel : public QObject void setPause(bool new_pause); bool errorState() const { return m_faulted; } void setErrorState(bool new_error); + bool isSnapshotLoaded() const { return m_snapshot_loaded; } + double snapshotProgress() const { return m_snapshot_progress; } + void setSnapshotProgress(double new_progress); + bool snapshotLoading() const { return m_snapshot_loading; } + bool headersSynced() const { return m_headers_synced; } + void setHeadersSynced(bool new_synced); + bool isIBDCompleted() const { return m_is_ibd_completed; } + void setIsIBDCompleted(bool new_completed); + QString snapshotFilePath() const { return m_snapshot_file_path; } + Q_INVOKABLE void setSnapshotFilePath(const QString& new_path); + bool snapshotError() const { return m_snapshot_error; } + Q_INVOKABLE void setSnapshotError(bool new_error); Q_INVOKABLE float getTotalBytesReceived() const { return (float)m_node.getTotalBytesRecv(); } Q_INVOKABLE float getTotalBytesSent() const { return (float)m_node.getTotalBytesSent(); } @@ -62,6 +81,9 @@ class NodeModel : public QObject Q_INVOKABLE void startNodeInitializionThread(); Q_INVOKABLE void requestShutdown(); + Q_INVOKABLE void snapshotLoadThread(QString path_file); + Q_INVOKABLE void checkAndLoadSnapshot(); + void startShutdownPolling(); void stopShutdownPolling(); @@ -87,6 +109,15 @@ public Q_SLOTS: void setTimeRatioListInitial(); void nodeInitialized(); void bannedListChanged(); + void initializationFinished(); + void snapshotLoaded(bool result); + void snapshotProgressChanged(); + void snapshotLoadingChanged(); + void showProgress(const QString& title, int progress); + void headersSyncedChanged(); + void isIBDCompletedChanged(); + void snapshotFilePathChanged(); + void snapshotErrorChanged(); protected: void timerEvent(QTimerEvent* event) override; @@ -100,8 +131,15 @@ public Q_SLOTS: double m_verification_progress{0.0}; bool m_pause{false}; bool m_faulted{false}; - + double m_snapshot_progress{0.0}; int m_shutdown_polling_timer_id{0}; + int m_snapshot_timer_id{0}; + bool m_snapshot_loading{false}; + bool m_snapshot_loaded{false}; + bool m_headers_synced{false}; + bool m_is_ibd_completed{false}; + QString m_snapshot_file_path; + bool m_snapshot_error{false}; QVector> m_block_process_time; @@ -109,10 +147,11 @@ public Q_SLOTS: std::unique_ptr m_handler_notify_block_tip; std::unique_ptr m_handler_notify_num_peers_changed; std::unique_ptr m_handler_notify_banned_list_changed; - + std::unique_ptr m_handler_snapshot_load_progress; void ConnectToBlockTipSignal(); void ConnectToNumConnectionsChangedSignal(); void ConnectToBannedListChangedSignal(); + void ConnectToSnapshotLoadProgressSignal(); }; #endif // BITCOIN_QML_MODELS_NODEMODEL_H diff --git a/qml/pages/settings/SettingsConnection.qml b/qml/pages/settings/SettingsConnection.qml index ae60004055..297bed2098 100644 --- a/qml/pages/settings/SettingsConnection.qml +++ b/qml/pages/settings/SettingsConnection.qml @@ -12,6 +12,7 @@ Page { id: root signal back property bool onboarding: false + property bool snapshotImportCompleted: onboarding ? false : chainModel.isSnapshotActive background: null PageStack { id: stack @@ -31,6 +32,9 @@ Page { detailActive: true detailItem: ConnectionSettings { onNext: stack.push(proxySettings) + onGotoSnapshot: stack.push(loadSnapshotSettings) + snapshotImportCompleted: root.snapshotImportCompleted + onboarding: root.onboarding } states: [ @@ -88,5 +92,13 @@ Page { onBack: stack.pop() } } + Component { + id: loadSnapshotSettings + SettingsSnapshotLoad { + onboarding: root.onboarding + snapshotImportCompleted: root.snapshotImportCompleted + onBack: stack.pop() + } + } } } diff --git a/qml/pages/settings/SettingsSnapshotLoad.qml b/qml/pages/settings/SettingsSnapshotLoad.qml new file mode 100644 index 0000000000..cd308ede94 --- /dev/null +++ b/qml/pages/settings/SettingsSnapshotLoad.qml @@ -0,0 +1,38 @@ +// Copyright (c) 2024-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import "../../controls" +import "../../components" + +Page { + signal back + property bool onboarding: false + property bool snapshotImportCompleted: onboarding ? false : chainModel.isSnapshotActive + + id: root + + background: null + implicitWidth: 450 + leftPadding: 20 + rightPadding: 20 + topPadding: 30 + + header: NavigationBar2 { + leftItem: NavButton { + iconSource: "image://images/caret-left" + text: qsTr("Back") + onClicked: root.back() + } + } + SnapshotLoadSettings { + width: Math.min(parent.width, 450) + anchors.horizontalCenter: parent.horizontalCenter + onboarding: root.onboarding + snapshotImportCompleted: root.snapshotImportCompleted + onBack: root.back() + } +} From 42830d8c3f49ce8c1097199dd417ac2bcd1374b7 Mon Sep 17 00:00:00 2001 From: D33r-Gee Date: Wed, 6 Aug 2025 11:18:37 -0700 Subject: [PATCH 2/3] submodule: Update submodule to PR #33117 from bitcoin/bitcoin -Also pointing url to D33r-Gee /bitcoin repository --- .gitmodules | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index fabdfe5061..54ec96622c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "bitcoin"] path = bitcoin - url = https://github.com/bitcoin/bitcoin + url = https://github.com/D33r-Gee/bitcoin + branch = interface-load-snapshot From a90455815bd0e7d6348aa4432132a3320b424b8d Mon Sep 17 00:00:00 2001 From: D33r-Gee Date: Thu, 26 Feb 2026 10:01:23 -0800 Subject: [PATCH 3/3] fix: QML interface compatibility with modern upstream Bitcoin Core Addresses multiple compilation and runtime issues introduced by incoming interface updates: - qml/bitcoin.cpp: Update ThreadSafeMessageBox UI callback signature and switch LogPrintf to LogInfo. - qml/models/chainmodel.cpp: Avoid calling getBlockHash on a hardcoded snapshot height that is not yet locally fetched; use AssumeutxoData.blockhash natively to prevent out-of-bound crashes. - qml/models/chainmodel.h: Guard out chain headers with Q_MOC_RUN to avoid standard-concept parser errors inside QT automoc. - qml/models/options_model.cpp: Add inline SettingToInt/SettingToBool helpers to operate safely over common::SettingsValue. - qml/models/peerdetailsmodel.h: Fallback to the new presync_height field dynamically inside CNodeStateStats as m_starting_height has been deprecated. - qml/models/walletqmlmodel.cpp: Supply std::nullopt to correctly satisfy the modernized interfaces::Wallet::createTransaction 4-argument parameters interface. - qml/models/transaction.cpp & walletqmlmodel.cpp: Drop obsolete uint256::FromUint256 wrappers in favor of executing the respective Txid.ToUint256() functionality. - qml/peerstatsutil.cpp: Add missing PRIVATE_BROADCAST enum handling inside ConnectionTypeToQString mapping. - test/mocks/mocknode.h: Update getProxy signature and insert missing snapshot & handleSnapshotLoadProgress mock definitions. - qml/walletqmlcontroller.cpp: Provide true for the trailing `load_after_restore` argument on interfaces::WalletLoader::restoreWallet. - qml/models/walletqmlmodel.cpp: Drop trailing nullptr (purpose) as the updated interfaces::Wallet::getAddress now strictly asks for 3 parameters. --- CMakeLists.txt | 1 + qml/bitcoin.cpp | 3 +-- qml/models/chainmodel.h | 2 ++ qml/models/options_model.cpp | 26 ++++++++++++++++++++++++++ qml/models/peerdetailsmodel.h | 2 +- qml/models/transaction.cpp | 16 ++++++---------- qml/models/walletqmlmodel.cpp | 16 +++++++++------- qml/peerstatsutil.cpp | 1 + qml/walletqmlcontroller.cpp | 3 ++- test/mocks/mocknode.h | 4 +++- 10 files changed, 52 insertions(+), 22 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b788e0dad..c028f491ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ set(USE_QRCODE TRUE) # We need this libraries, can ignore the executable bitcoin-qt set(BUILD_GUI ON) set(ENABLE_WALLET ON) +set(ENABLE_IPC OFF) # Bitcoin Core codebase # Builds libraries: univalue, core_interface, bitcoin_node, bitcoin_wallet diff --git a/qml/bitcoin.cpp b/qml/bitcoin.cpp index 4a1eb88dc9..632050dd5a 100644 --- a/qml/bitcoin.cpp +++ b/qml/bitcoin.cpp @@ -122,7 +122,6 @@ AppMode SetupAppMode() bool InitErrorMessageBox( const bilingual_str& message, - [[maybe_unused]] const std::string& caption, [[maybe_unused]] unsigned int style) { QQmlApplicationEngine engine; @@ -146,7 +145,7 @@ void DebugMessageHandler(QtMsgType type, const QMessageLogContext& context, cons if (type == QtDebugMsg) { LogDebug(BCLog::QT, "GUI: %s\n", msg.toStdString()); } else { - LogPrintf("GUI: %s\n", msg.toStdString()); + LogInfo("GUI: %s\n", msg.toStdString()); } } diff --git a/qml/models/chainmodel.h b/qml/models/chainmodel.h index 6a5124be7f..824a86bc28 100644 --- a/qml/models/chainmodel.h +++ b/qml/models/chainmodel.h @@ -5,8 +5,10 @@ #ifndef BITCOIN_QML_MODELS_CHAINMODEL_H #define BITCOIN_QML_MODELS_CHAINMODEL_H +#ifndef Q_MOC_RUN #include #include +#endif #include #include diff --git a/qml/models/options_model.cpp b/qml/models/options_model.cpp index fd531ab8c1..1b3e05cb79 100644 --- a/qml/models/options_model.cpp +++ b/qml/models/options_model.cpp @@ -40,6 +40,32 @@ OptionsQmlModel::OptionsQmlModel(interfaces::Node& node, bool is_onboarded) : m_node{node} , m_onboarded{is_onboarded} { + auto SettingToInt = [](const common::SettingsValue& val, int64_t def) -> int64_t { + if (val.isNull()) return def; + const common::SettingsValue* v = &val; + if (v->isArray() && !v->empty()) { + v = &v->getValues()[0]; + } + if (v->isNum()) return v->getInt(); + if (v->isStr()) { + try { return std::stoll(v->get_str()); } catch (...) { return def; } + } + return def; + }; + auto SettingToBool = [](const common::SettingsValue& val, bool def) -> bool { + if (val.isNull()) return def; + const common::SettingsValue* v = &val; + if (v->isArray() && !v->empty()) { + v = &v->getValues()[0]; + } + if (v->isBool()) return v->get_bool(); + if (v->isNum()) return v->getInt() != 0; + if (v->isStr()) { + std::string s = v->get_str(); + return s == "1" || s == "true" || s == "yes"; + } + return def; + }; m_dbcache_size_mib = SettingToInt(m_node.getPersistentSetting("dbcache"), DEFAULT_DB_CACHE >> 20); m_listen = SettingToBool(m_node.getPersistentSetting("listen"), DEFAULT_LISTEN); diff --git a/qml/models/peerdetailsmodel.h b/qml/models/peerdetailsmodel.h index 42a66db420..88a39350fb 100644 --- a/qml/models/peerdetailsmodel.h +++ b/qml/models/peerdetailsmodel.h @@ -54,7 +54,7 @@ class PeerDetailsModel : public QObject QString services() const { return PeerStatsUtil::FormatServicesStr(m_combinedStats->nodeStateStats.their_services); } bool transactionRelay() const { return m_combinedStats->nodeStateStats.m_relay_txs; } bool addressRelay() const { return m_combinedStats->nodeStateStats.m_addr_relay_enabled; } - QString startingHeight() const { return QString::number(m_combinedStats->nodeStateStats.m_starting_height); } + QString startingHeight() const { return QString::number(m_combinedStats->nodeStateStats.presync_height); } QString syncedHeaders() const { return QString::number(m_combinedStats->nodeStateStats.nSyncHeight); } QString syncedBlocks() const { return QString::number(m_combinedStats->nodeStateStats.nCommonHeight); } QString direction() const { return QString::fromStdString(m_combinedStats->nodeStats.fInbound ? "Inbound" : "Outbound"); } diff --git a/qml/models/transaction.cpp b/qml/models/transaction.cpp index f9a6de3523..dc1cbd7545 100644 --- a/qml/models/transaction.cpp +++ b/qml/models/transaction.cpp @@ -10,10 +10,6 @@ #include -using wallet::ISMINE_SPENDABLE; -using wallet::ISMINE_NO; -using wallet::isminetype; - namespace { const int RecommendedNumConfirmations = 6; } @@ -138,18 +134,18 @@ QList> Transaction::fromWalletTx(const interfaces::W CAmount nCredit = wtx.credit; CAmount nDebit = wtx.debit; CAmount nNet = nCredit - nDebit; - uint256 hash = wtx.tx->GetHash(); + uint256 hash = wtx.tx->GetHash().ToUint256(); std::map mapValue = wtx.value_map; bool involvesWatchAddress = false; - isminetype fAllFromMe = ISMINE_SPENDABLE; + bool fAllFromMe = true; bool any_from_me = false; if (wtx.is_coinbase) { - fAllFromMe = ISMINE_NO; + fAllFromMe = false; } else { - for (const isminetype mine : wtx.txin_is_mine) + for (const bool mine : wtx.txin_is_mine) { - if(fAllFromMe > mine) fAllFromMe = mine; + if (!mine) fAllFromMe = false; if (mine) any_from_me = true; } } @@ -202,7 +198,7 @@ QList> Transaction::fromWalletTx(const interfaces::W parts.append(sub); } - isminetype mine = wtx.txout_is_mine[i]; + bool mine = wtx.txout_is_mine[i]; if(mine) { // diff --git a/qml/models/walletqmlmodel.cpp b/qml/models/walletqmlmodel.cpp index 62f8f89b36..1db9e9ca04 100644 --- a/qml/models/walletqmlmodel.cpp +++ b/qml/models/walletqmlmodel.cpp @@ -233,7 +233,7 @@ QString WalletQmlModel::getAddressLabel(const QString& address) const } std::string label; - if (!m_wallet->getAddress(destination, &label, nullptr, nullptr)) { + if (!m_wallet->getAddress(destination, &label, nullptr)) { return {}; } @@ -245,7 +245,11 @@ std::unique_ptr WalletQmlModel::handleTransactionChanged(Tr if (!m_wallet) { return nullptr; } - return m_wallet->handleTransactionChanged(fn); + // Convert the function to match the expected signature + auto converted_fn = [fn](const Txid& txid, ChangeType change_type) { + fn(txid.ToUint256(), change_type); + }; + return m_wallet->handleTransactionChanged(converted_fn); } bool WalletQmlModel::prepareTransaction() @@ -269,17 +273,15 @@ bool WalletQmlModel::prepareTransaction() return false; } - int nChangePosRet = -1; - CAmount nFeeRequired = 0; - const auto& res = m_wallet->createTransaction(vecSend, m_coin_control, true, nChangePosRet, nFeeRequired); + const auto& res = m_wallet->createTransaction(vecSend, m_coin_control, true, std::nullopt); if (res) { if (m_current_transaction) { delete m_current_transaction; } - CTransactionRef newTx = *res; + CTransactionRef newTx = res->tx; m_current_transaction = new WalletQmlModelTransaction(m_send_recipients, this); m_current_transaction->setWtx(newTx); - m_current_transaction->setTransactionFee(nFeeRequired); + m_current_transaction->setTransactionFee(res->fee); Q_EMIT currentTransactionChanged(); return true; } else { diff --git a/qml/peerstatsutil.cpp b/qml/peerstatsutil.cpp index 67b14ec7a8..3a39c8f225 100644 --- a/qml/peerstatsutil.cpp +++ b/qml/peerstatsutil.cpp @@ -32,6 +32,7 @@ QString ConnectionTypeToQString(ConnectionType conn_type, bool prepend_direction case ConnectionType::MANUAL: return prefix + QObject::tr("Manual"); case ConnectionType::FEELER: return prefix + QObject::tr("Feeler"); case ConnectionType::ADDR_FETCH: return prefix + QObject::tr("Address Fetch"); + case ConnectionType::PRIVATE_BROADCAST: return prefix + QObject::tr("Private Broadcast"); } assert(false); } diff --git a/qml/walletqmlcontroller.cpp b/qml/walletqmlcontroller.cpp index 6c9e4a641a..d77b0542d9 100644 --- a/qml/walletqmlcontroller.cpp +++ b/qml/walletqmlcontroller.cpp @@ -396,7 +396,8 @@ void WalletQmlController::startWalletImport(const QString& path) auto wallet = m_node.walletLoader().restoreWallet( fs::PathFromString(normalized_path.toStdString()), restore_wallet_name.toStdString(), - warning_messages); + warning_messages, + true); const QString warnings = JoinWarnings(warning_messages); if (!wallet) { diff --git a/test/mocks/mocknode.h b/test/mocks/mocknode.h index 52ad9a7c50..d5eddff874 100644 --- a/test/mocks/mocknode.h +++ b/test/mocks/mocknode.h @@ -50,7 +50,9 @@ class MockNode : public interfaces::Node MOCK_METHOD(void, forceSetting, (const std::string&, const common::SettingsValue&), (override)); MOCK_METHOD(void, resetSettings, (), (override)); MOCK_METHOD(void, mapPort, (bool), (override)); - MOCK_METHOD(bool, getProxy, (Network, Proxy&), (override)); + MOCK_METHOD(std::optional, getProxy, (Network), (override)); + MOCK_METHOD((std::unique_ptr), snapshot, (const fs::path&), (override)); + MOCK_METHOD((std::unique_ptr), handleSnapshotLoadProgress, (SnapshotLoadProgressFn), (override)); MOCK_METHOD(size_t, getNodeCount, (ConnectionDirection), (override)); MOCK_METHOD(bool, getNodesStats, (NodesStats&), (override)); MOCK_METHOD(bool, getBanned, (banmap_t&), (override));