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
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/bitcoin b/bitcoin
index 8ffbd7b778..34e536d425 160000
--- a/bitcoin
+++ b/bitcoin
@@ -1 +1 @@
-Subproject commit 8ffbd7b778600aa1e824027f1e675929a4240856
+Subproject commit 34e536d42529ac6f4b70b1802ce709282de2e28a
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/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..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
@@ -27,6 +29,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 +39,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 +53,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/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/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()
+ }
+}
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));