From b65d2997f0292b8c9d7851d3eb128874faa60b63 Mon Sep 17 00:00:00 2001 From: johnny9 Date: Wed, 27 May 2026 09:36:10 -0400 Subject: [PATCH 01/14] build: bump Bitcoin Core submodule to v31 --- .github/workflows/artifacts.yml | 10 +++++++-- .github/workflows/ci.yml | 3 ++- .github/workflows/gui-functional-tests.yml | 3 ++- CMakeLists.txt | 24 ++++++++-------------- bitcoin | 2 +- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml index 8aac7034bd..9c5a449baa 100644 --- a/.github/workflows/artifacts.yml +++ b/.github/workflows/artifacts.yml @@ -30,10 +30,14 @@ jobs: - name: MacOS Install Deps if: contains(matrix.os, 'macos') run: | - brew install ccache boost pkgconf libevent qt@6 qrencode coreutils + brew install ccache boost pkgconf libevent qt@6 qrencode coreutils llvm if [ "${BUILD_APP_TESTS}" = "ON" ]; then brew install googletest fi + llvm_prefix="$(brew --prefix llvm)" + echo "CC=${llvm_prefix}/bin/clang" >> "$GITHUB_ENV" + echo "CXX=${llvm_prefix}/bin/clang++" >> "$GITHUB_ENV" + echo "SDKROOT=$(xcrun --sdk macosx --show-sdk-path)" >> "$GITHUB_ENV" echo "CCACHE_DIR=${{ runner.temp }}/ccache" >> "$GITHUB_ENV" - name: Ubuntu Install Deps @@ -60,7 +64,9 @@ jobs: - name: Build run: | git submodule update --init - cmake -B build -DBUILD_APP_TESTS=${{ env.BUILD_APP_TESTS }} + cmake -B build \ + -DBUILD_APP_TESTS=${{ env.BUILD_APP_TESTS }} \ + -DENABLE_IPC=OFF cmake --build build -j$(nproc) - name: Save Ccache cache diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d47da8eb7f..cae7cc99c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,8 @@ jobs: run: | cmake -S . -B build -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_APP_TESTS=${{ env.BUILD_APP_TESTS }} + -DBUILD_APP_TESTS=${{ env.BUILD_APP_TESTS }} \ + -DENABLE_IPC=OFF - name: Build run: cmake --build build --parallel diff --git a/.github/workflows/gui-functional-tests.yml b/.github/workflows/gui-functional-tests.yml index 6f8f6f38f4..250a368e00 100644 --- a/.github/workflows/gui-functional-tests.yml +++ b/.github/workflows/gui-functional-tests.yml @@ -128,7 +128,8 @@ jobs: cmake -B build \ -DBUILD_APP_TESTS=OFF \ -DENABLE_TEST_AUTOMATION=ON \ - -DBUILD_DAEMON=ON + -DBUILD_DAEMON=ON \ + -DENABLE_IPC=OFF - name: Build run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a8291af95..8661d8764d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,23 +129,14 @@ qt6_add_resources(bitcoinqml "qml_gui_translations" ) # Embed bitcoin-qt translations built by the bitcoin submodule. -# The submodule (BUILD_GUI=ON) compiles .ts → .qm files into -# ${CMAKE_BINARY_DIR}/bitcoin/src/qt/locale/ at build time. -# We mark each file as GENERATED so CMake does not require it to exist -# at configure time, and add a dependency on the bitcoin-qt target so -# the .qm files are built before the resource compiler runs. -set(BITCOIN_LOCALE_TAGS - am ar ast_ES ay az@latin az be bg bn br bs ca cmn cs cy da - de_AT de_CH de el en eo es_CL es_CO es_DO es es_SV es_VE et eu - fa fil fi fo fr_CM fr_LU fr ga_IE ga gd gl_ES gl gu hak ha he hi - hr hu id is it ja ka kk@latin kk kl km kn ko ku_IQ ku ky la lb - lt lv mg mi mk ml mn mr_IN mr ms mt my nb ne nl no or pam pa pl - ps pt_BR pt ro ru sa sc sd si sk sl sm sn so sq - sr@ijekavianlatin sr@latin sr sv sw szl ta te th tk tl tn tr ug uk ur - uz@Cyrl uz@Latn uz ve vi yi yo yue zh_CN zh-Hans zh-Hant zh_HK zh zh_TW zu -) +# The submodule (BUILD_GUI=ON) compiles .ts -> .qm files into +# ${CMAKE_BINARY_DIR}/bitcoin/src/qt/locale/ at build time. Derive the +# embedded list from the submodule's current source list so Core translation +# additions/removals do not leave stale resource inputs behind. +include("${CMAKE_CURRENT_SOURCE_DIR}/bitcoin/src/qt/locale/ts_files.cmake") set(BITCOIN_QT_QM_FILES) -foreach(tag IN LISTS BITCOIN_LOCALE_TAGS) +foreach(ts_file IN LISTS ts_files) + string(REGEX REPLACE "^bitcoin_(.+)\\.ts$" "\\1" tag "${ts_file}") set(qm_file "${CMAKE_BINARY_DIR}/bitcoin/src/qt/locale/bitcoin_${tag}.qm") set_source_files_properties(${qm_file} PROPERTIES GENERATED TRUE @@ -153,6 +144,7 @@ foreach(tag IN LISTS BITCOIN_LOCALE_TAGS) ) list(APPEND BITCOIN_QT_QM_FILES ${qm_file}) endforeach() +unset(ts_files) qt6_add_resources(bitcoinqml "bitcoin_qt_translations" PREFIX "/translations" FILES ${BITCOIN_QT_QM_FILES} diff --git a/bitcoin b/bitcoin index 8ffbd7b778..6574cb4086 160000 --- a/bitcoin +++ b/bitcoin @@ -1 +1 @@ -Subproject commit 8ffbd7b778600aa1e824027f1e675929a4240856 +Subproject commit 6574cb40869b96b9ffc79c19dc8f4e467d60f321 From 36c63267051f96cc7511e0d9488593824b3390ac Mon Sep 17 00:00:00 2001 From: johnny9 Date: Wed, 27 May 2026 09:36:21 -0400 Subject: [PATCH 02/14] qml: adapt UI callbacks to Core v31 --- qml/bitcoin.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qml/bitcoin.cpp b/qml/bitcoin.cpp index 812d20c960..e0b796e23c 100644 --- a/qml/bitcoin.cpp +++ b/qml/bitcoin.cpp @@ -130,7 +130,6 @@ AppMode SetupAppMode() bool InitErrorMessageBox( const bilingual_str& message, - [[maybe_unused]] const std::string& caption, [[maybe_unused]] unsigned int style) { QQmlApplicationEngine engine; @@ -164,7 +163,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()); } } From 8200d44baed31f3b2f0a7b5125ffdc7c4bec9749 Mon Sep 17 00:00:00 2001 From: johnny9 Date: Wed, 27 May 2026 09:36:21 -0400 Subject: [PATCH 03/14] qml: keep Core headers out of chain model moc --- qml/models/chainmodel.cpp | 8 ++++++-- qml/models/chainmodel.h | 7 ++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/qml/models/chainmodel.cpp b/qml/models/chainmodel.cpp index ce3a6b12ae..d2344d467b 100644 --- a/qml/models/chainmodel.cpp +++ b/qml/models/chainmodel.cpp @@ -4,16 +4,20 @@ #include +#include +#include + #include #include #include #include -#include using interfaces::FoundBlock; ChainModel::ChainModel(interfaces::Chain& chain) - : m_chain{chain} + : m_assumed_blockchain_size{Params().AssumedBlockchainSize()}, + m_assumed_chainstate_size{Params().AssumedChainStateSize()}, + m_chain{chain} { QTimer* timer = new QTimer(); connect(timer, &QTimer::timeout, this, &ChainModel::setCurrentTimeRatio); diff --git a/qml/models/chainmodel.h b/qml/models/chainmodel.h index 9318510eda..b0456bb03b 100644 --- a/qml/models/chainmodel.h +++ b/qml/models/chainmodel.h @@ -5,9 +5,6 @@ #ifndef BITCOIN_QML_MODELS_CHAINMODEL_H #define BITCOIN_QML_MODELS_CHAINMODEL_H -#include -#include - #include #include #include @@ -51,8 +48,8 @@ public Q_SLOTS: private: QString m_current_network_name; - quint64 m_assumed_blockchain_size{ Params().AssumedBlockchainSize() }; - quint64 m_assumed_chainstate_size{ Params().AssumedChainStateSize() }; + quint64 m_assumed_blockchain_size; + quint64 m_assumed_chainstate_size; /* time_ratio: Ratio between the time at which an event * happened and 12 hours. So, for example, if a block is * found at 4 am or pm, the time_ratio would be 0.3. From c0abdd28a992a20201de2a6d1ef44bcf803d47a8 Mon Sep 17 00:00:00 2001 From: johnny9 Date: Wed, 27 May 2026 09:36:21 -0400 Subject: [PATCH 04/14] qml: adapt options model to Core v31 APIs --- qml/models/options_model.cpp | 8 ++++---- qml/models/options_model.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qml/models/options_model.cpp b/qml/models/options_model.cpp index ec99b0470d..eae0d885cc 100644 --- a/qml/models/options_model.cpp +++ b/qml/models/options_model.cpp @@ -93,19 +93,19 @@ OptionsQmlModel::OptionsQmlModel(interfaces::Node& node, bool is_onboarded) : m_node{node} , m_onboarded{is_onboarded} { - m_dbcache_size_mib = SettingToInt(m_node.getPersistentSetting("dbcache"), DEFAULT_DB_CACHE >> 20); + m_dbcache_size_mib = SettingTo(m_node.getPersistentSetting("dbcache"), DEFAULT_DB_CACHE >> 20); m_listen = SettingToBool(m_node.getPersistentSetting("listen"), DEFAULT_LISTEN); - m_max_mempool_size_mb = SettingToInt(m_node.getPersistentSetting("maxmempool"), DEFAULT_MAX_MEMPOOL_SIZE_MB); + m_max_mempool_size_mb = SettingTo(m_node.getPersistentSetting("maxmempool"), DEFAULT_MAX_MEMPOOL_SIZE_MB); m_natpmp = SettingToBool(m_node.getPersistentSetting("natpmp"), DEFAULT_NATPMP); - int64_t prune_value{SettingToInt(m_node.getPersistentSetting("prune"), 0)}; + int64_t prune_value{SettingTo(m_node.getPersistentSetting("prune"), 0)}; m_prune = (prune_value > 1); m_prune_size_gb = m_prune ? PruneMiBtoGB(prune_value) : DEFAULT_PRUNE_TARGET_GB; - m_script_threads = SettingToInt(m_node.getPersistentSetting("par"), DEFAULT_SCRIPTCHECK_THREADS); + m_script_threads = SettingTo(m_node.getPersistentSetting("par"), DEFAULT_SCRIPTCHECK_THREADS); m_server = SettingToBool(m_node.getPersistentSetting("server"), false); diff --git a/qml/models/options_model.h b/qml/models/options_model.h index 402c436593..c783c46d91 100644 --- a/qml/models/options_model.h +++ b/qml/models/options_model.h @@ -161,7 +161,7 @@ public Q_SLOTS: bool m_listen; int m_max_mempool_size_mb; const int m_min_max_mempool_size_mb{ - static_cast((DEFAULT_DESCENDANT_SIZE_LIMIT_KVB * 1000 * 40 + 999999) / 1000000) + static_cast((DEFAULT_CLUSTER_SIZE_LIMIT_KVB * 1000 * 40 + 999999) / 1000000) }; const int m_max_max_mempool_size_mb{ sizeof(void*) <= 4 ? 500 : 99999 From adecd10f0ee1ab2ef016e5e724af3927475e1bf8 Mon Sep 17 00:00:00 2001 From: johnny9 Date: Wed, 27 May 2026 09:36:21 -0400 Subject: [PATCH 05/14] qml: handle private broadcast peer type --- qml/peerstatsutil.cpp | 1 + 1 file changed, 1 insertion(+) 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); } From 836ffb876427e39a8bad04bcc2b1f643366244b2 Mon Sep 17 00:00:00 2001 From: johnny9 Date: Wed, 27 May 2026 09:38:06 -0400 Subject: [PATCH 06/14] qml: adapt wallet ownership and metadata APIs --- qml/models/addresslistmodel.cpp | 3 +-- qml/models/transaction.cpp | 16 ++++++---------- qml/models/walletqmlmodel.cpp | 10 ++++++---- test/mocks/mockwallet.h | 10 +++++----- test/test_addresslistmodel.cpp | 17 ++++++++--------- test/test_transaction.cpp | 21 +++++++++------------ test/test_walletqmlcontroller.cpp | 10 +++++----- test/test_walletqmlmodel.cpp | 12 ++++++------ 8 files changed, 46 insertions(+), 53 deletions(-) diff --git a/qml/models/addresslistmodel.cpp b/qml/models/addresslistmodel.cpp index 948f5214d1..b00a0ea02e 100644 --- a/qml/models/addresslistmodel.cpp +++ b/qml/models/addresslistmodel.cpp @@ -15,7 +15,6 @@ #include using wallet::AddressPurpose; -using wallet::ISMINE_NO; namespace { QString CategoryName(AddressListModel::Category category) @@ -212,7 +211,7 @@ std::vector AddressListModel::collectEntries() c } for (const interfaces::WalletAddress& wallet_address : m_wallet_model->getAddresses()) { - if (wallet_address.purpose != AddressPurpose::RECEIVE || wallet_address.is_mine == ISMINE_NO) { + if (wallet_address.purpose != AddressPurpose::RECEIVE || !wallet_address.is_mine) { continue; } diff --git a/qml/models/transaction.cpp b/qml/models/transaction.cpp index 62db750312..b930cc723e 100644 --- a/qml/models/transaction.cpp +++ b/qml/models/transaction.cpp @@ -11,10 +11,6 @@ #include -using wallet::ISMINE_SPENDABLE; -using wallet::ISMINE_NO; -using wallet::isminetype; - namespace { const int RecommendedNumConfirmations = 6; } @@ -137,7 +133,7 @@ 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(); QString txidStr = QString::fromStdString(hash.GetHex()); std::map mapValue = wtx.value_map; @@ -151,14 +147,14 @@ QList> Transaction::fromWalletTx(const interfaces::W } 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; } } @@ -215,7 +211,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 4d26d37b09..69870a8095 100644 --- a/qml/models/walletqmlmodel.cpp +++ b/qml/models/walletqmlmodel.cpp @@ -1146,7 +1146,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)) { if (!label.empty()) { return QString::fromStdString(label); } @@ -1173,7 +1173,7 @@ bool WalletQmlModel::setAddressLabel(const QString& address, const QString& labe } wallet::AddressPurpose purpose{wallet::AddressPurpose::RECEIVE}; - if (!m_wallet->getAddress(destination, nullptr, nullptr, &purpose)) { + if (!m_wallet->getAddress(destination, nullptr, &purpose)) { return false; } @@ -1221,7 +1221,7 @@ std::set WalletQmlModel::usedAddresses() const std::set receive_addresses; for (const interfaces::WalletAddress& wallet_address : getAddresses()) { - if (wallet_address.purpose != wallet::AddressPurpose::RECEIVE || wallet_address.is_mine == wallet::ISMINE_NO) { + if (wallet_address.purpose != wallet::AddressPurpose::RECEIVE || !wallet_address.is_mine) { continue; } @@ -1296,7 +1296,9 @@ std::unique_ptr WalletQmlModel::handleTransactionChanged(Tr if (!m_wallet) { return nullptr; } - return m_wallet->handleTransactionChanged(fn); + return m_wallet->handleTransactionChanged([fn = std::move(fn)](const Txid& txid, ChangeType status) { + fn(txid.ToUint256(), status); + }); } void WalletQmlModel::scheduleFeeEstimates() diff --git a/test/mocks/mockwallet.h b/test/mocks/mockwallet.h index dd9ccc4ff6..49c50ee6f0 100644 --- a/test/mocks/mockwallet.h +++ b/test/mocks/mockwallet.h @@ -44,7 +44,7 @@ class StubWallet : public interfaces::Wallet bool isSpendable(const CTxDestination&) override { return false; } bool setAddressBook(const CTxDestination&, const std::string&, const std::optional&) override { return false; } bool delAddressBook(const CTxDestination&) override { return false; } - bool getAddress(const CTxDestination&, std::string*, wallet::isminetype*, wallet::AddressPurpose*) override { return false; } + bool getAddress(const CTxDestination&, std::string*, wallet::AddressPurpose*) override { return false; } std::vector getAddresses() override { return {}; } std::vector getAddressReceiveRequests() override { return {}; } bool setAddressReceiveRequest(const CTxDestination&, const std::string&, const std::string&) override { return false; } @@ -71,10 +71,10 @@ class StubWallet : public interfaces::Wallet bool tryGetBalances(interfaces::WalletBalances&, uint256&) override { return false; } CAmount getBalance() override { return 0; } CAmount getAvailableBalance(const wallet::CCoinControl&) override { return 0; } - wallet::isminetype txinIsMine(const CTxIn&) override { return wallet::ISMINE_NO; } - wallet::isminetype txoutIsMine(const CTxOut&) override { return wallet::ISMINE_NO; } - CAmount getDebit(const CTxIn&, wallet::isminefilter) override { return 0; } - CAmount getCredit(const CTxOut&, wallet::isminefilter) override { return 0; } + bool txinIsMine(const CTxIn&) override { return false; } + bool txoutIsMine(const CTxOut&) override { return false; } + CAmount getDebit(const CTxIn&) override { return 0; } + CAmount getCredit(const CTxOut&) override { return 0; } CoinsList listCoins() override { return {}; } std::vector getCoins(const std::vector&) override { return {}; } CAmount getRequiredFee(unsigned int) override { return 0; } diff --git a/test/test_addresslistmodel.cpp b/test/test_addresslistmodel.cpp index 3fa4d33943..75ac272020 100644 --- a/test/test_addresslistmodel.cpp +++ b/test/test_addresslistmodel.cpp @@ -58,7 +58,7 @@ class TestAddressWallet final : public StubWallet util::Result getNewDestination(const OutputType, const std::string& label) override { m_labels[m_next_destination] = label; - m_addresses.emplace_back(m_next_destination, wallet::ISMINE_SPENDABLE, wallet::AddressPurpose::RECEIVE, label); + m_addresses.emplace_back(m_next_destination, true, wallet::AddressPurpose::RECEIVE, label); return m_next_destination; } bool isSpendable(const CTxDestination&) override { return true; } @@ -73,12 +73,11 @@ class TestAddressWallet final : public StubWallet return true; } bool delAddressBook(const CTxDestination&) override { return false; } - bool getAddress(const CTxDestination& dest, std::string* name, wallet::isminetype* is_mine, wallet::AddressPurpose* purpose) override + bool getAddress(const CTxDestination& dest, std::string* name, wallet::AddressPurpose* purpose) override { for (const auto& address : m_addresses) { if (address.dest != dest) continue; if (name) *name = address.name; - if (is_mine) *is_mine = address.is_mine; if (purpose) *purpose = address.purpose; return true; } @@ -98,7 +97,7 @@ interfaces::WalletTx WalletTxFor(const std::vector& destinations interfaces::WalletTx wallet_tx; wallet_tx.tx = TransactionWithOutputs(destinations, amounts); wallet_tx.txout_address = destinations; - wallet_tx.txout_address_is_mine.assign(destinations.size(), wallet::ISMINE_SPENDABLE); + wallet_tx.txout_address_is_mine.assign(destinations.size(), true); wallet_tx.txout_is_change = is_change; return wallet_tx; } @@ -126,9 +125,9 @@ void AddressListModelTests::receiveAddressesHideUsedUntilEnabled() const CTxDestination unused{TestDestination(1)}; const CTxDestination used{TestDestination(2)}; wallet->m_addresses = { - {unused, wallet::ISMINE_SPENDABLE, wallet::AddressPurpose::RECEIVE, "unused"}, - {used, wallet::ISMINE_SPENDABLE, wallet::AddressPurpose::RECEIVE, "used"}, - {TestDestination(3), wallet::ISMINE_SPENDABLE, wallet::AddressPurpose::SEND, "send"}, + {unused, true, wallet::AddressPurpose::RECEIVE, "unused"}, + {used, true, wallet::AddressPurpose::RECEIVE, "used"}, + {TestDestination(3), true, wallet::AddressPurpose::SEND, "send"}, }; TestAddressWallet* wallet_ptr{wallet.get()}; @@ -151,7 +150,7 @@ void AddressListModelTests::labelsCanBeEdited() auto wallet{std::make_unique()}; const CTxDestination dest{TestDestination(1)}; wallet->m_addresses = { - {dest, wallet::ISMINE_SPENDABLE, wallet::AddressPurpose::RECEIVE, "first label"}, + {dest, true, wallet::AddressPurpose::RECEIVE, "first label"}, }; WalletQmlModel wallet_model{std::move(wallet)}; AddressListModel* model{wallet_model.addressListModel()}; @@ -174,7 +173,7 @@ void AddressListModelTests::changeAddressesComeFromUnspentChangeOutputs() const interfaces::WalletTx change_tx{WalletTxFor({receive, change, spent_change}, {COIN, 2 * COIN, 3 * COIN}, {false, true, true})}; const Txid txid{change_tx.tx->GetHash()}; wallet->m_addresses = { - {receive, wallet::ISMINE_SPENDABLE, wallet::AddressPurpose::RECEIVE, "receive"}, + {receive, true, wallet::AddressPurpose::RECEIVE, "receive"}, }; interfaces::WalletTxOut change_out; change_out.txout = CTxOut{2 * COIN, GetScriptForDestination(change)}; diff --git a/test/test_transaction.cpp b/test/test_transaction.cpp index 6e993b4353..b7f78092b6 100644 --- a/test/test_transaction.cpp +++ b/test/test_transaction.cpp @@ -13,9 +13,6 @@ #include -using wallet::ISMINE_NO; -using wallet::ISMINE_SPENDABLE; - namespace { constexpr CAmount COIN_VALUE{100'000'000}; @@ -27,9 +24,9 @@ CTxDestination Destination(unsigned char value) } interfaces::WalletTx MakeWalletTx( - const std::vector& txin_is_mine, + const std::vector& txin_is_mine, const std::vector& output_values, - const std::vector& txout_is_mine, + const std::vector& txout_is_mine, const std::vector& txout_is_change, CAmount debit, CAmount credit = 0) @@ -38,7 +35,7 @@ interfaces::WalletTx MakeWalletTx( mtx.vin.emplace_back(COutPoint{Txid::FromUint256(uint256{1}), 0}); std::vector addresses; - std::vector address_is_mine; + std::vector address_is_mine; for (size_t i = 0; i < output_values.size(); ++i) { mtx.vout.emplace_back(output_values[i], CScript{}); addresses.push_back(Destination(static_cast(i + 1))); @@ -80,9 +77,9 @@ void TransactionTests::initTestCase() void TransactionTests::fromWalletTx_hidesSenderChangeOutput() { const interfaces::WalletTx wtx = MakeWalletTx( - /*txin_is_mine=*/{ISMINE_SPENDABLE}, + /*txin_is_mine=*/{true}, /*output_values=*/{70 * COIN_VALUE, 29 * COIN_VALUE}, - /*txout_is_mine=*/{ISMINE_NO, ISMINE_SPENDABLE}, + /*txout_is_mine=*/{false, true}, /*txout_is_change=*/{false, true}, /*debit=*/100 * COIN_VALUE); @@ -100,9 +97,9 @@ void TransactionTests::fromWalletTx_hidesSenderChangeOutput() void TransactionTests::fromWalletTx_mixedDebitKeepsNegativeNetAmount() { const interfaces::WalletTx wtx = MakeWalletTx( - /*txin_is_mine=*/{ISMINE_SPENDABLE, ISMINE_NO}, + /*txin_is_mine=*/{true, false}, /*output_values=*/{80 * COIN_VALUE}, - /*txout_is_mine=*/{ISMINE_NO}, + /*txout_is_mine=*/{false}, /*txout_is_change=*/{false}, /*debit=*/100 * COIN_VALUE); @@ -120,9 +117,9 @@ void TransactionTests::fromWalletTx_mixedDebitKeepsNegativeNetAmount() void TransactionTests::fromWalletTx_showsIncomingPaymentToChangeAddress() { const interfaces::WalletTx wtx = MakeWalletTx( - /*txin_is_mine=*/{ISMINE_NO}, + /*txin_is_mine=*/{false}, /*output_values=*/{5 * COIN_VALUE}, - /*txout_is_mine=*/{ISMINE_SPENDABLE}, + /*txout_is_mine=*/{true}, /*txout_is_change=*/{true}, /*debit=*/0, /*credit=*/5 * COIN_VALUE); diff --git a/test/test_walletqmlcontroller.cpp b/test/test_walletqmlcontroller.cpp index 4ae4597fc4..e29b13c49a 100644 --- a/test/test_walletqmlcontroller.cpp +++ b/test/test_walletqmlcontroller.cpp @@ -176,7 +176,7 @@ class FakeWallet : public interfaces::Wallet bool isSpendable(const CTxDestination&) override { return false; } bool setAddressBook(const CTxDestination&, const std::string&, const std::optional&) override { return true; } bool delAddressBook(const CTxDestination&) override { return true; } - bool getAddress(const CTxDestination&, std::string*, wallet::isminetype*, wallet::AddressPurpose*) override { return false; } + bool getAddress(const CTxDestination&, std::string*, wallet::AddressPurpose*) override { return false; } std::vector getAddresses() override { return {}; } std::vector getAddressReceiveRequests() override { return {}; } bool setAddressReceiveRequest(const CTxDestination&, const std::string&, const std::string&) override { return true; } @@ -216,10 +216,10 @@ class FakeWallet : public interfaces::Wallet bool tryGetBalances(interfaces::WalletBalances&, uint256&) override { return false; } CAmount getBalance() override { return 0; } CAmount getAvailableBalance(const wallet::CCoinControl&) override { return 0; } - wallet::isminetype txinIsMine(const CTxIn&) override { return {}; } - wallet::isminetype txoutIsMine(const CTxOut&) override { return {}; } - CAmount getDebit(const CTxIn&, wallet::isminefilter) override { return 0; } - CAmount getCredit(const CTxOut&, wallet::isminefilter) override { return 0; } + bool txinIsMine(const CTxIn&) override { return false; } + bool txoutIsMine(const CTxOut&) override { return false; } + CAmount getDebit(const CTxIn&) override { return 0; } + CAmount getCredit(const CTxOut&) override { return 0; } CoinsList listCoins() override { return {}; } std::vector getCoins(const std::vector&) override { return {}; } CAmount getRequiredFee(unsigned int) override { return 0; } diff --git a/test/test_walletqmlmodel.cpp b/test/test_walletqmlmodel.cpp index 877fb9275f..5196bb0b37 100644 --- a/test/test_walletqmlmodel.cpp +++ b/test/test_walletqmlmodel.cpp @@ -245,7 +245,7 @@ class FakePasswordWallet : public StubWallet bool isSpendable(const CTxDestination&) override { return false; } bool setAddressBook(const CTxDestination&, const std::string&, const std::optional&) override { return true; } bool delAddressBook(const CTxDestination&) override { return true; } - bool getAddress(const CTxDestination&, std::string* name, wallet::isminetype*, wallet::AddressPurpose*) override + bool getAddress(const CTxDestination&, std::string* name, wallet::AddressPurpose*) override { if (name) { *name = get_address_label; @@ -314,10 +314,10 @@ class FakePasswordWallet : public StubWallet } CAmount getBalance() override { return balance; } CAmount getAvailableBalance(const wallet::CCoinControl&) override { return balance; } - wallet::isminetype txinIsMine(const CTxIn&) override { return wallet::ISMINE_NO; } - wallet::isminetype txoutIsMine(const CTxOut&) override { return wallet::ISMINE_NO; } - CAmount getDebit(const CTxIn&, wallet::isminefilter) override { return 0; } - CAmount getCredit(const CTxOut&, wallet::isminefilter) override { return 0; } + bool txinIsMine(const CTxIn&) override { return false; } + bool txoutIsMine(const CTxOut&) override { return false; } + CAmount getDebit(const CTxIn&) override { return 0; } + CAmount getCredit(const CTxOut&) override { return 0; } CoinsList listCoins() override { return {}; } std::vector getCoins(const std::vector&) override { return {}; } CAmount getRequiredFee(unsigned int) override { return 0; } @@ -1355,7 +1355,7 @@ void WalletQmlModelTests::setCurrentPaymentRequestAddressUsesAddressListLabel() auto model = MakeWalletModel(wallet); wallet->wallet_addresses.emplace_back( DecodeDestination(VALID_MAINNET_ADDRESS.toStdString()), - wallet::ISMINE_SPENDABLE, + true, wallet::AddressPurpose::RECEIVE, "invoice 1024"); wallet->get_address_result = true; From 5c790b7595f4c3271cee9d7eae944c276ad32ac0 Mon Sep 17 00:00:00 2001 From: johnny9 Date: Wed, 27 May 2026 09:38:31 -0400 Subject: [PATCH 07/14] qml: adapt wallet transaction creation API --- qml/models/walletqmlmodel.cpp | 16 ++++++---------- test/mocks/mockwallet.h | 19 ++++++++++++++----- test/test_walletqmlcontroller.cpp | 6 +++--- test/test_walletqmlmodel.cpp | 17 +++++++++++++---- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/qml/models/walletqmlmodel.cpp b/qml/models/walletqmlmodel.cpp index 69870a8095..9a8b59e1eb 100644 --- a/qml/models/walletqmlmodel.cpp +++ b/qml/models/walletqmlmodel.cpp @@ -174,14 +174,12 @@ std::optional TryPreviewFee(interfaces::Wallet& wallet, const std::vector& recipients, const wallet::CCoinControl& coin_control) { - int change_position{-1}; - CAmount fee{0}; - const auto result = wallet.createTransaction(recipients, coin_control, /*sign=*/false, change_position, fee); + const auto result = wallet.createTransaction(recipients, coin_control, /*sign=*/false, /*change_pos=*/std::nullopt); if (!result) { return std::nullopt; } - return fee; + return result->fee; } std::optional> WithLargestRecipientPayingFee(const std::vector& recipients) @@ -1566,20 +1564,18 @@ bool WalletQmlModel::prepareTransactionInternal(std::optional pass return false; } - int nChangePosRet = -1; - CAmount nFeeRequired = 0; const bool sign = !m_wallet->privateKeysDisabled(); - const auto& result = m_wallet->createTransaction(*vec_send, coin_control, sign, nChangePosRet, nFeeRequired); + const auto& result = m_wallet->createTransaction(*vec_send, coin_control, sign, /*change_pos=*/std::nullopt); if (result) { if (m_current_transaction) { delete m_current_transaction; } - const CTransactionRef& newTx = *result; + const CTransactionRef& newTx = result->tx; m_current_transaction = new WalletQmlModelTransaction(m_send_recipients, this); m_current_transaction->setWtx(newTx); - m_current_transaction->setTransactionFee(nFeeRequired); + m_current_transaction->setTransactionFee(result->fee); if (subtract_fee_from_amount) { - m_current_transaction->reassignAmounts(nChangePosRet); + m_current_transaction->reassignAmounts(static_cast(result->change_pos.value_or(-1))); } m_current_transaction->setDisplayUnit(m_display_unit); relock_guard.relock(); diff --git a/test/mocks/mockwallet.h b/test/mocks/mockwallet.h index 49c50ee6f0..27bdc60270 100644 --- a/test/mocks/mockwallet.h +++ b/test/mocks/mockwallet.h @@ -53,7 +53,7 @@ class StubWallet : public interfaces::Wallet bool unlockCoin(const COutPoint&) override { return false; } bool isLockedCoin(const COutPoint&) override { return false; } void listLockedCoins(std::vector&) override {} - util::Result createTransaction(const std::vector&, const wallet::CCoinControl&, bool, int&, CAmount&) override { return util::Error{Untranslated("not implemented")}; } + util::Result createTransaction(const std::vector&, const wallet::CCoinControl&, bool, std::optional) override { return util::Error{Untranslated("not implemented")}; } void commitTransaction(CTransactionRef, interfaces::WalletValueMap, interfaces::WalletOrderForm) override {} bool transactionCanBeAbandoned(const Txid&) override { return false; } bool abandonTransaction(const Txid&) override { return false; } @@ -106,14 +106,23 @@ class MockWallet : public StubWallet return getNewDestinationValue(type, label); } - util::Result createTransaction(const std::vector& recipients, + util::Result createTransaction(const std::vector& recipients, const wallet::CCoinControl& coin_control, bool sign, - int& change_pos, - CAmount& fee) override + std::optional) override { if (createTransactionHandler) { - return createTransactionHandler(recipients, coin_control, sign, change_pos, fee); + int change_pos{-1}; + CAmount fee{0}; + auto result = createTransactionHandler(recipients, coin_control, sign, change_pos, fee); + if (!result) { + return util::Error{util::ErrorString(result)}; + } + return wallet::CreatedTransactionResult{ + *result, + fee, + change_pos >= 0 ? std::optional{static_cast(change_pos)} : std::nullopt, + FeeCalculation{}}; } return util::Error{Untranslated("no createTransactionHandler installed")}; } diff --git a/test/test_walletqmlcontroller.cpp b/test/test_walletqmlcontroller.cpp index e29b13c49a..226f21ce80 100644 --- a/test/test_walletqmlcontroller.cpp +++ b/test/test_walletqmlcontroller.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -185,11 +186,10 @@ class FakeWallet : public interfaces::Wallet bool unlockCoin(const COutPoint&) override { return true; } bool isLockedCoin(const COutPoint&) override { return false; } void listLockedCoins(std::vector& outputs) override { outputs.clear(); } - util::Result createTransaction(const std::vector&, + util::Result createTransaction(const std::vector&, const wallet::CCoinControl&, bool, - int&, - CAmount&) override + std::optional) override { return util::Error{Untranslated("Unexpected createTransaction call")}; } diff --git a/test/test_walletqmlmodel.cpp b/test/test_walletqmlmodel.cpp index 5196bb0b37..9285653620 100644 --- a/test/test_walletqmlmodel.cpp +++ b/test/test_walletqmlmodel.cpp @@ -265,14 +265,23 @@ class FakePasswordWallet : public StubWallet bool unlockCoin(const COutPoint&) override { return true; } bool isLockedCoin(const COutPoint&) override { return false; } void listLockedCoins(std::vector& outputs) override { outputs.clear(); } - util::Result createTransaction(const std::vector& recipients, + util::Result createTransaction(const std::vector& recipients, const wallet::CCoinControl& coin_control, bool sign, - int& change_pos, - CAmount& fee) override + std::optional) override { create_transaction_sign_args.push_back(sign); - return create_transaction_fn(recipients, coin_control, sign, change_pos, fee); + int change_pos{-1}; + CAmount fee{0}; + auto result = create_transaction_fn(recipients, coin_control, sign, change_pos, fee); + if (!result) { + return util::Error{util::ErrorString(result)}; + } + return wallet::CreatedTransactionResult{ + *result, + fee, + change_pos >= 0 ? std::optional{static_cast(change_pos)} : std::nullopt, + FeeCalculation{}}; } void commitTransaction(CTransactionRef, interfaces::WalletValueMap, interfaces::WalletOrderForm) override { From bd98c5a497b82b37ba271fa52fc67d5d5f7757c4 Mon Sep 17 00:00:00 2001 From: johnny9 Date: Wed, 27 May 2026 09:38:45 -0400 Subject: [PATCH 08/14] qml: pass load flag when restoring wallets --- qml/walletqmlcontroller.cpp | 3 ++- test/test_walletlistmodel.cpp | 3 ++- test/test_walletqmlcontroller.cpp | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/qml/walletqmlcontroller.cpp b/qml/walletqmlcontroller.cpp index ecb95f45e0..2ee8d87ccb 100644 --- a/qml/walletqmlcontroller.cpp +++ b/qml/walletqmlcontroller.cpp @@ -1034,7 +1034,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, + /*load_after_restore=*/true); const QString warnings = JoinWarnings(warning_messages); if (!wallet) { diff --git a/test/test_walletlistmodel.cpp b/test/test_walletlistmodel.cpp index 410d036480..f682cb9250 100644 --- a/test/test_walletlistmodel.cpp +++ b/test/test_walletlistmodel.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include @@ -37,7 +38,7 @@ class FakeWalletLoader : public interfaces::WalletLoader return util::Error{Untranslated("Unexpected loadWallet call")}; } std::string getWalletDir() override { return {}; } - util::Result> restoreWallet(const fs::path&, const std::string&, std::vector&) override + util::Result> restoreWallet(const fs::path&, const std::string&, std::vector&, bool) override { return util::Error{Untranslated("Unexpected restoreWallet call")}; } diff --git a/test/test_walletqmlcontroller.cpp b/test/test_walletqmlcontroller.cpp index 226f21ce80..a5d01a2331 100644 --- a/test/test_walletqmlcontroller.cpp +++ b/test/test_walletqmlcontroller.cpp @@ -110,7 +110,7 @@ class FakeWalletLoader : public interfaces::WalletLoader return load_wallet_fn(name, warnings); } std::string getWalletDir() override { return wallet_dir; } - util::Result> restoreWallet(const fs::path&, const std::string&, std::vector&) override + util::Result> restoreWallet(const fs::path&, const std::string&, std::vector&, bool) override { return util::Error{Untranslated("Unexpected restoreWallet call")}; } From 8a9dd027a0495d6f37e6a7cf3460c39762df4ef8 Mon Sep 17 00:00:00 2001 From: johnny9 Date: Wed, 27 May 2026 09:38:59 -0400 Subject: [PATCH 09/14] qml: interrupt node before GUI shutdown --- qml/models/nodemodel.cpp | 1 + test/functional/qml_wallet_test_lib.py | 15 ++++++++++---- test/test_nodemodel.cpp | 27 ++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/qml/models/nodemodel.cpp b/qml/models/nodemodel.cpp index 71309758cc..8182f74ec2 100644 --- a/qml/models/nodemodel.cpp +++ b/qml/models/nodemodel.cpp @@ -423,6 +423,7 @@ void NodeModel::requestShutdown() } m_shutdown_requested = true; stopShutdownPolling(); + m_node.startShutdown(); Q_EMIT requestedShutdown(); } diff --git a/test/functional/qml_wallet_test_lib.py b/test/functional/qml_wallet_test_lib.py index ac56881d94..373330b3d1 100644 --- a/test/functional/qml_wallet_test_lib.py +++ b/test/functional/qml_wallet_test_lib.py @@ -242,12 +242,19 @@ def start_gui(self, reset_gui_settings=False, extra_args=None, cwd=None): def stop_gui(self): if self.gui_process and self.gui_process.poll() is None: - self.gui_process.send_signal(signal.SIGTERM) try: - self.gui_process.wait(timeout=10) + rpc_call(self.gui_rpc_port, "stop") + except Exception: + pass + try: + self.gui_process.wait(timeout=20) except subprocess.TimeoutExpired: - self.gui_process.kill() - self.gui_process.wait() + self.gui_process.send_signal(signal.SIGTERM) + try: + self.gui_process.wait(timeout=10) + except subprocess.TimeoutExpired: + self.gui_process.kill() + self.gui_process.wait() self.gui_process = None if self.driver: self.driver.close() diff --git a/test/test_nodemodel.cpp b/test/test_nodemodel.cpp index 53ae5d8dfb..20b64ce8e8 100644 --- a/test/test_nodemodel.cpp +++ b/test/test_nodemodel.cpp @@ -166,6 +166,7 @@ private Q_SLOTS: void nodeInformationRowsExposeDiagnostics(); void initEmitsRequestedInitialize(); void initGuardBlocksSecondEmission(); + void shutdownPollingStartsShutdownBeforeEmittingSignal(); }; void NodeModelTests::refreshMempoolInfoUpdatesProperties() @@ -1384,6 +1385,32 @@ void NodeModelTests::initGuardBlocksSecondEmission() QCOMPARE(spy.count(), 1); } +void NodeModelTests::shutdownPollingStartsShutdownBeforeEmittingSignal() +{ + NiceMock node; + MempoolState mempool; + InstallDefaultHandlers(node); + InstallMempoolGetters(node, mempool); + ON_CALL(node, shutdownRequested()).WillByDefault(Return(true)); + + NodeModel model{node}; + WaitForInitialMempoolRefresh(mempool); + + QSignalSpy shutdown_spy{&model, &NodeModel::requestedShutdown}; + bool started_before_signal{false}; + EXPECT_CALL(node, startShutdown()).WillOnce(Invoke([&] { + started_before_signal = shutdown_spy.count() == 0; + })); + + model.startShutdownPolling(); + + QTRY_COMPARE_WITH_TIMEOUT(shutdown_spy.count(), 1, ASYNC_TIMEOUT_MS); + QVERIFY(started_before_signal); + + model.requestShutdown(); + QCOMPARE(shutdown_spy.count(), 1); +} + #ifdef BITCOINQML_NO_TEST_MAIN BITCOINQML_REGISTER_QT_TEST(NodeModelTests) #else From dacddb74fb9d59709ea859377563ba4f0a1f3ebf Mon Sep 17 00:00:00 2001 From: johnny9 Date: Thu, 11 Jun 2026 10:19:09 -0400 Subject: [PATCH 10/14] qml: adapt runtime dialog callbacks to Core v31 The Core v31 node interface no longer supplies a caption for message box or question callbacks, so derive runtime dialog titles from style and remove the stale caption plumbing from the test bridge. --- qml/bitcoin.cpp | 4 +-- qml/models/nodemodel.cpp | 35 ++++++++----------- qml/models/nodemodel.h | 7 ++-- qml/test/testbridge.cpp | 4 +-- qml/test/testbridge.h | 4 +-- test/functional/qml_driver.py | 3 +- .../qml_test_node_runtime_dialogs.py | 6 ---- test/test_nodemodel.cpp | 15 +------- 8 files changed, 24 insertions(+), 54 deletions(-) diff --git a/qml/bitcoin.cpp b/qml/bitcoin.cpp index e0b796e23c..74188cf80b 100644 --- a/qml/bitcoin.cpp +++ b/qml/bitcoin.cpp @@ -239,12 +239,12 @@ int QmlGuiMain(int argc, char* argv[]) std::unique_ptr init = interfaces::MakeGuiInit(argc, argv); QStringList startup_warnings; auto handler_message_box = ::uiInterface.ThreadSafeMessageBox_connect( - [&startup_warnings](const bilingual_str& message, const std::string& caption, unsigned int style) { + [&startup_warnings](const bilingual_str& message, unsigned int style) { if (style & CClientUIInterface::ICON_WARNING) { RecordStartupWarning(startup_warnings, message); return false; } - return InitErrorMessageBox(message, caption, style); + return InitErrorMessageBox(message, style); }); SetupEnvironment(); diff --git a/qml/models/nodemodel.cpp b/qml/models/nodemodel.cpp index 8182f74ec2..25204928f2 100644 --- a/qml/models/nodemodel.cpp +++ b/qml/models/nodemodel.cpp @@ -52,11 +52,8 @@ QStringList SplitWarnings(const QString& warnings) return result; } -QString RuntimeDialogTitle(const QString& caption, unsigned int style) +QString RuntimeDialogTitle(unsigned int style) { - if (!caption.isEmpty()) { - return caption; - } if (style & CClientUIInterface::ICON_ERROR) { return QObject::tr("Error"); } @@ -386,7 +383,7 @@ void NodeModel::showStartupWarnings() const QString warnings{m_startup_warning_messages.join(QStringLiteral("\n\n"))}; m_startup_warning_messages.clear(); // MSG_WARNING is modal; startup notices should be shown once without blocking initialization. - showRuntimeDialogOnGuiThread(warnings, QString{}, CClientUIInterface::ICON_WARNING, /*question=*/false); + showRuntimeDialogOnGuiThread(warnings, CClientUIInterface::ICON_WARNING, /*question=*/false); } void NodeModel::recordStartupErrorMessage(const QString& message) @@ -567,18 +564,16 @@ void NodeModel::ConnectToRuntimeDialogSignals() assert(!m_handler_question); m_handler_message_box = m_node.handleMessageBox( - [this](const bilingual_str& message, const std::string& caption, unsigned int style) { + [this](const bilingual_str& message, unsigned int style) { return showRuntimeDialog( QString::fromStdString(message.translated), - QString::fromStdString(caption), style, /*question=*/false); }); m_handler_question = m_node.handleQuestion( - [this](const bilingual_str& message, [[maybe_unused]] const std::string& non_interactive_message, const std::string& caption, unsigned int style) { + [this](const bilingual_str& message, [[maybe_unused]] const std::string& non_interactive_message, unsigned int style) { return showRuntimeDialog( QString::fromStdString(message.translated), - QString::fromStdString(caption), style, /*question=*/true); }); @@ -658,7 +653,7 @@ QVariantList NodeModel::nodeInformationRows() rows.push_back(InformationRow(tr("User agent"), QString::fromStdString(strSubVersion))); rows.push_back(InformationRow(tr("Datadir"), QString::fromStdString(fs::PathToString(gArgs.GetDataDirNet())))); rows.push_back(InformationRow(tr("Blocks dir"), QString::fromStdString(fs::PathToString(gArgs.GetBlocksDirPath())))); - rows.push_back(InformationRow(tr("Startup time"), QDateTime::fromSecsSinceEpoch(GetStartupTime()).toString())); + rows.push_back(InformationRow(tr("Startup time"), QDateTime::currentDateTime().addSecs(-TicksSeconds(GetUptime())).toString())); rows.push_back(InformationRow(tr("Network"), QString::fromStdString(Params().GetChainTypeString()))); rows.push_back(InformationRow(tr("Block height"), QString::number(block_height))); rows.push_back(InformationRow(tr("Header height"), QString::number(header_height))); @@ -672,27 +667,27 @@ QVariantList NodeModel::nodeInformationRows() return rows; } -bool NodeModel::showRuntimeDialog(const QString& message, const QString& caption, unsigned int style, bool question) +bool NodeModel::showRuntimeDialog(const QString& message, unsigned int style, bool question) { if (QThread::currentThread() == thread()) { - return showRuntimeDialogOnGuiThread(message, caption, style, question); + return showRuntimeDialogOnGuiThread(message, style, question); } if (!(style & CClientUIInterface::MODAL) && !question) { - QMetaObject::invokeMethod(this, [this, message, caption, style, question] { - showRuntimeDialogOnGuiThread(message, caption, style, question); + QMetaObject::invokeMethod(this, [this, message, style, question] { + showRuntimeDialogOnGuiThread(message, style, question); }, Qt::QueuedConnection); return false; } bool result{false}; - QMetaObject::invokeMethod(this, [this, &result, message, caption, style, question] { - result = showRuntimeDialogOnGuiThread(message, caption, style, question); + QMetaObject::invokeMethod(this, [this, &result, message, style, question] { + result = showRuntimeDialogOnGuiThread(message, style, question); }, Qt::BlockingQueuedConnection); return result; } -bool NodeModel::showRuntimeDialogOnGuiThread(const QString& message, const QString& caption, unsigned int style, bool question) +bool NodeModel::showRuntimeDialogOnGuiThread(const QString& message, unsigned int style, bool question) { if (!m_runtime_dialogs_enabled && !question) { if (style & CClientUIInterface::ICON_WARNING) { @@ -710,7 +705,6 @@ bool NodeModel::showRuntimeDialogOnGuiThread(const QString& message, const QStri const bool blocking{(style & CClientUIInterface::MODAL) || question}; auto request{std::make_shared()}; request->message = message; - request->caption = caption; request->style = style; request->question = question; if (!m_runtime_dialogs_enabled && (question || (style & CClientUIInterface::ICON_ERROR))) { @@ -743,7 +737,7 @@ bool NodeModel::showRuntimeDialogOnGuiThread(const QString& message, const QStri void NodeModel::showRuntimeDialogRequest(const std::shared_ptr& request) { m_runtime_dialog_active = request; - m_runtime_dialog_title = RuntimeDialogTitle(request->caption, request->style); + m_runtime_dialog_title = RuntimeDialogTitle(request->style); m_runtime_dialog_message = request->message; m_runtime_dialog_icon = RuntimeDialogIcon(request->style); m_runtime_dialog_buttons = RuntimeDialogButtons(request->style); @@ -778,11 +772,10 @@ void NodeModel::answerRuntimeDialog(unsigned int button) } #ifdef ENABLE_TEST_AUTOMATION -void NodeModel::showRuntimeDialogForTest(const QString& message, const QString& caption, unsigned int style, bool question) +void NodeModel::showRuntimeDialogForTest(const QString& message, unsigned int style, bool question) { auto request{std::make_shared()}; request->message = message; - request->caption = caption; request->style = style; request->question = question; diff --git a/qml/models/nodemodel.h b/qml/models/nodemodel.h index fc0c94b94b..07190ea476 100644 --- a/qml/models/nodemodel.h +++ b/qml/models/nodemodel.h @@ -127,7 +127,7 @@ class NodeModel : public QObject Q_INVOKABLE QVariantList nodeInformationRows(); Q_INVOKABLE void answerRuntimeDialog(unsigned int button); #ifdef ENABLE_TEST_AUTOMATION - Q_INVOKABLE void showRuntimeDialogForTest(const QString& message, const QString& caption, unsigned int style, bool question); + Q_INVOKABLE void showRuntimeDialogForTest(const QString& message, unsigned int style, bool question); #endif public Q_SLOTS: @@ -170,7 +170,6 @@ public Q_SLOTS: struct RuntimeDialogRequest { QString message; - QString caption; unsigned int style{0}; bool question{false}; bool answer{false}; @@ -253,8 +252,8 @@ public Q_SLOTS: void setWarnings(const QString& warnings); void setBlockSyncActive(bool active); void setHeaderSyncState(int height, int64_t block_time, bool presync); - bool showRuntimeDialog(const QString& message, const QString& caption, unsigned int style, bool question); - bool showRuntimeDialogOnGuiThread(const QString& message, const QString& caption, unsigned int style, bool question); + bool showRuntimeDialog(const QString& message, unsigned int style, bool question); + bool showRuntimeDialogOnGuiThread(const QString& message, unsigned int style, bool question); void showRuntimeDialogRequest(const std::shared_ptr& request); void requestMempoolInfoRefresh(); void fetchMempoolInfo(); diff --git a/qml/test/testbridge.cpp b/qml/test/testbridge.cpp index daafe61c66..636e674978 100644 --- a/qml/test/testbridge.cpp +++ b/qml/test/testbridge.cpp @@ -409,7 +409,6 @@ QByteArray TestBridge::processCommand(const QByteArray& json_cmd) } else if (cmd == QLatin1String("show_runtime_dialog")) { return cmdShowRuntimeDialog( obj.value(QStringLiteral("message")).toString(), - obj.value(QStringLiteral("caption")).toString(), static_cast(obj.value(QStringLiteral("style")).toDouble()), obj.value(QStringLiteral("question")).toBool(false)); } else if (cmd == QLatin1String("answer_runtime_dialog")) { @@ -883,7 +882,7 @@ QByteArray TestBridge::cmdSaveScreenshot(const QString& path) return QJsonDocument(resp).toJson(QJsonDocument::Compact); } -QByteArray TestBridge::cmdShowRuntimeDialog(const QString& message, const QString& caption, unsigned int style, bool question) +QByteArray TestBridge::cmdShowRuntimeDialog(const QString& message, unsigned int style, bool question) { QVariant node_model_value = m_engine->rootContext()->contextProperty(QStringLiteral("nodeModel")); QObject* node_model = node_model_value.value(); @@ -896,7 +895,6 @@ QByteArray TestBridge::cmdShowRuntimeDialog(const QString& message, const QStrin "showRuntimeDialogForTest", Qt::DirectConnection, Q_ARG(QString, message), - Q_ARG(QString, caption), Q_ARG(unsigned int, style), Q_ARG(bool, question)); if (!invoked) { diff --git a/qml/test/testbridge.h b/qml/test/testbridge.h index 8bf53e3254..0542be4adc 100644 --- a/qml/test/testbridge.h +++ b/qml/test/testbridge.h @@ -34,7 +34,7 @@ /// {"cmd": "click_list_item", "objectName": "", "index": , "childObjectName": ""} /// {"cmd": "get_list_item_property", "objectName": "", "index": , "prop": ""} /// {"cmd": "save_screenshot", "path": ""} -/// {"cmd": "show_runtime_dialog", "message": "", "caption": "", "style": <uint>, "question": <bool>} +/// {"cmd": "show_runtime_dialog", "message": "<text>", "style": <uint>, "question": <bool>} /// {"cmd": "answer_runtime_dialog", "button": <uint>} /// {"cmd": "list_objects"} /// {"cmd": "close_window"} @@ -86,7 +86,7 @@ private Q_SLOTS: QByteArray cmdClickListItem(const QString& view_object_name, int row_index, const QString& delegate_child_object_name); QByteArray cmdGetListItemProperty(const QString& view_object_name, int row_index, const QString& prop); QByteArray cmdSaveScreenshot(const QString& path); - QByteArray cmdShowRuntimeDialog(const QString& message, const QString& caption, unsigned int style, bool question); + QByteArray cmdShowRuntimeDialog(const QString& message, unsigned int style, bool question); QByteArray cmdAnswerRuntimeDialog(unsigned int button); QByteArray cmdListObjects(); QByteArray cmdCloseWindow(); diff --git a/test/functional/qml_driver.py b/test/functional/qml_driver.py index cce8c98e97..6e85cf3c71 100644 --- a/test/functional/qml_driver.py +++ b/test/functional/qml_driver.py @@ -227,13 +227,12 @@ def save_screenshot(self, path): ) return resp - def show_runtime_dialog(self, message, caption, style, question=False): + def show_runtime_dialog(self, message, style, question=False): """Open a NodeRuntimeDialog through the test automation bridge.""" resp = self._send( { "cmd": "show_runtime_dialog", "message": message, - "caption": caption, "style": style, "question": question, } diff --git a/test/functional/qml_test_node_runtime_dialogs.py b/test/functional/qml_test_node_runtime_dialogs.py index 93e275657c..f17fd62b32 100644 --- a/test/functional/qml_test_node_runtime_dialogs.py +++ b/test/functional/qml_test_node_runtime_dialogs.py @@ -37,7 +37,6 @@ "name": "database-read-error", "source": "bitcoin/src/init.cpp coins_error_cb", "message": "Error reading from database, shutting down.", - "caption": "", "style": MSG_ERROR, "question": False, "buttons": [BTN_OK], @@ -47,7 +46,6 @@ "name": "deprecated-checkpoints-warning", "source": "bitcoin/src/init.cpp -checkpoints warning", "message": "Option '-checkpoints' is set but checkpoints were removed. This option has no effect.", - "caption": "", "style": MSG_WARNING, "question": False, "buttons": [BTN_OK], @@ -57,7 +55,6 @@ "name": "reindex-question-ok-abort", "source": "bitcoin/src/init.cpp chainstate load failure retry question", "message": "Error opening block database.\n\nDo you want to rebuild the databases now?", - "caption": "", "style": MSG_ERROR | BTN_ABORT, "question": True, "buttons": [BTN_OK, BTN_ABORT], @@ -67,7 +64,6 @@ "name": "network-options-error", "source": "bitcoin/src/net.cpp outgoing connection option conflict", "message": "Cannot provide specific connections and have addrman find outgoing connections at the same time.", - "caption": "", "style": MSG_ERROR, "question": False, "buttons": [BTN_OK], @@ -77,7 +73,6 @@ "name": "abort-retry-ignore-button-mask", "source": "CClientUIInterface BTN_ABORT | BTN_RETRY | BTN_IGNORE contract sample using a net.cpp error message", "message": "Failed to listen on any port. Use -listen=0 if you want this.", - "caption": "", "style": ICON_ERROR | MODAL | BTN_ABORT | BTN_RETRY | BTN_IGNORE, "question": True, "buttons": [BTN_ABORT, BTN_RETRY, BTN_IGNORE], @@ -115,7 +110,6 @@ def screenshot_path(root, case_name): def open_case(gui, case): gui.show_runtime_dialog( message=case["message"], - caption=case["caption"], style=case["style"], question=case["question"], ) diff --git a/test/test_nodemodel.cpp b/test/test_nodemodel.cpp index 20b64ce8e8..6604663e3a 100644 --- a/test/test_nodemodel.cpp +++ b/test/test_nodemodel.cpp @@ -750,7 +750,6 @@ void NodeModelTests::startupWarningsAreShownOnceAndDoNotBecomeCurrentWarnings() QSignalSpy runtime_dialog_spy{&model, &NodeModel::runtimeDialogChanged}; QVERIFY(!message_box_fn( bilingual_str{"Startup warning", "Translated startup warning"}, - "", CClientUIInterface::MSG_WARNING)); QCOMPARE(runtime_dialog_spy.count(), 0); @@ -815,7 +814,6 @@ void NodeModelTests::runtimeMessageHandlerOpensAfterInitialization() std::thread worker([&] { result = message_box_fn( bilingual_str{"Runtime error", "Translated runtime error"}, - "", CClientUIInterface::MSG_ERROR); finished = true; }); @@ -852,7 +850,7 @@ void NodeModelTests::runtimeQuestionHandlerBlocksForAnswerAndReturnsResult() QObject::connect(&model, &NodeModel::runtimeDialogChanged, &model, [&] { if (!model.runtimeDialogVisible()) return; ++prompt_count; - QCOMPARE(model.runtimeDialogTitle(), QStringLiteral("Question caption")); + QCOMPARE(model.runtimeDialogTitle(), QStringLiteral("Error")); QCOMPARE(model.runtimeDialogMessage(), QStringLiteral("Translated rebuild?")); QCOMPARE(model.runtimeDialogButtons(), static_cast<unsigned int>(CClientUIInterface::BTN_OK | CClientUIInterface::BTN_ABORT)); QVERIFY(model.runtimeDialogQuestion()); @@ -865,7 +863,6 @@ void NodeModelTests::runtimeQuestionHandlerBlocksForAnswerAndReturnsResult() result = question_fn( bilingual_str{"Rebuild?", "Translated rebuild?"}, "Non interactive", - "Question caption", CClientUIInterface::MSG_ERROR | CClientUIInterface::BTN_ABORT); finished = true; }); @@ -918,7 +915,6 @@ void NodeModelTests::runtimeStartupQuestionFailureLetsInitializeResultRequestShu result = question_fn( bilingual_str{"Rebuild?", "Translated rebuild?"}, "Non interactive", - "", CClientUIInterface::MSG_ERROR | CClientUIInterface::BTN_ABORT); finished = true; }); @@ -980,7 +976,6 @@ void NodeModelTests::runtimeStartupErrorDialogLetsInitializeResultRequestShutdow std::thread worker([&] { result = message_box_fn( bilingual_str{"Failed to initialize", "Translated failed to initialize"}, - "", CClientUIInterface::MSG_ERROR); finished = true; }); @@ -1034,7 +1029,6 @@ void NodeModelTests::runtimeDialogDefaultsToOkWhenNoButtonsAreSpecified() std::thread worker([&] { result = message_box_fn( bilingual_str{"Information", "Translated information"}, - "", CClientUIInterface::ICON_INFORMATION | CClientUIInterface::MODAL); finished = true; }); @@ -1082,7 +1076,6 @@ void NodeModelTests::runtimeDialogExposesFullCoreButtonMask() std::thread worker([&] { result = message_box_fn( bilingual_str{"Full button mask", "Translated full button mask"}, - "", CClientUIInterface::ICON_WARNING | CClientUIInterface::MODAL | full_button_mask); finished = true; }); @@ -1128,7 +1121,6 @@ void NodeModelTests::runtimeBlockingDialogsAreQueued() second_result = question_fn( bilingual_str{"Second?", "Translated second?"}, "Non interactive", - "Second caption", CClientUIInterface::MSG_ERROR | CClientUIInterface::BTN_ABORT); } else if (model.runtimeDialogMessage() == QStringLiteral("Translated second?")) { QTimer::singleShot(0, &model, [&model] { @@ -1140,7 +1132,6 @@ void NodeModelTests::runtimeBlockingDialogsAreQueued() first_result = question_fn( bilingual_str{"First?", "Translated first?"}, "Non interactive", - "First caption", CClientUIInterface::MSG_ERROR | CClientUIInterface::BTN_ABORT); QCOMPARE(prompts, QStringList({QStringLiteral("Translated first?"), QStringLiteral("Translated second?")})); @@ -1171,7 +1162,6 @@ void NodeModelTests::runtimeNonBlockingDialogsAreQueued() QSignalSpy runtime_dialog_spy{&model, &NodeModel::runtimeDialogChanged}; QVERIFY(!message_box_fn( bilingual_str{"First", "Translated first"}, - "", CClientUIInterface::ICON_INFORMATION)); QCOMPARE(runtime_dialog_spy.count(), 1); QVERIFY(model.runtimeDialogVisible()); @@ -1179,7 +1169,6 @@ void NodeModelTests::runtimeNonBlockingDialogsAreQueued() QVERIFY(!message_box_fn( bilingual_str{"Second", "Translated second"}, - "", CClientUIInterface::ICON_WARNING)); QCOMPARE(runtime_dialog_spy.count(), 1); QVERIFY(model.runtimeDialogVisible()); @@ -1244,11 +1233,9 @@ void NodeModelTests::initializeFailureUsesNodeErrorMessages() std::thread worker([&] { message_box_fn( bilingual_str{"Unable to bind original", "Translated unable to bind"}, - "", CClientUIInterface::ICON_ERROR); message_box_fn( bilingual_str{"Failed to listen original", "Translated failed to listen"}, - "", CClientUIInterface::ICON_ERROR); finished = true; }); From 22ed52ffcf5cfbcd2c6af771c680137499e4d48f Mon Sep 17 00:00:00 2001 From: johnny9 <johnny9dev@pm.me> Date: Fri, 12 Jun 2026 11:46:00 -0400 Subject: [PATCH 11/14] qml: skip wallet setup after startup shutdown Avoid treating a late successful init result as node-ready once shutdown has already been requested. This keeps the wallet controller from rebuilding GUI wallet state while Core is shutting down, fixing qml_test_shutdown hangs during load_on_startup shutdown. --- qml/bitcoin.cpp | 14 ++++++++------ qml/models/nodemodel.cpp | 5 +++++ test/test_nodemodel.cpp | 22 ++++++++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/qml/bitcoin.cpp b/qml/bitcoin.cpp index 74188cf80b..a38514353d 100644 --- a/qml/bitcoin.cpp +++ b/qml/bitcoin.cpp @@ -340,19 +340,21 @@ int QmlGuiMain(int argc, char* argv[]) NodeModel node_model{*node}; node_model.addStartupWarnings(startup_warnings); QmlInitExecutor init_executor{*node}; + bool shutdown_requested{false}; #ifdef ENABLE_WALLET std::unique_ptr<WalletQmlController> wallet_controller; if (wallet_enabled) { wallet_controller = std::make_unique<WalletQmlController>(*node); - QObject::connect(&init_executor, &QmlInitExecutor::initializeResult, wallet_controller.get(), [wallet_controller = wallet_controller.get()](bool success) { - if (success) { - wallet_controller->initialize(); - } - }); + QObject::connect( + &init_executor, &QmlInitExecutor::initializeResult, wallet_controller.get(), + [wallet_controller = wallet_controller.get(), node = node.get(), &shutdown_requested](bool success) { + if (success && !shutdown_requested && !node->shutdownRequested()) { + wallet_controller->initialize(); + } + }); } #endif QObject::connect(&node_model, &NodeModel::requestedInitialize, &init_executor, &QmlInitExecutor::initialize); - bool shutdown_requested{false}; QObject::connect(&node_model, &NodeModel::requestedShutdown, [&] { if (shutdown_requested) { return; diff --git a/qml/models/nodemodel.cpp b/qml/models/nodemodel.cpp index 25204928f2..c88ebbe28f 100644 --- a/qml/models/nodemodel.cpp +++ b/qml/models/nodemodel.cpp @@ -426,6 +426,11 @@ void NodeModel::requestShutdown() void NodeModel::initializeResult(bool success, interfaces::BlockAndHeaderTipInfo tip_info) { + if (success && (m_shutdown_requested || m_node.shutdownRequested())) { + requestShutdown(); + return; + } + if (!success) { if (m_startup_failure_dialog_shown) { requestShutdown(); diff --git a/test/test_nodemodel.cpp b/test/test_nodemodel.cpp index 6604663e3a..466a174f41 100644 --- a/test/test_nodemodel.cpp +++ b/test/test_nodemodel.cpp @@ -144,6 +144,7 @@ private Q_SLOTS: void requestShutdownEmitsOnlyOnce(); void initializationFailureRequestsShutdownWhenCoreWasInterrupted(); void initializationFailureWithoutCoreInterruptOnlySetsErrorState(); + void initializationSuccessDuringCoreShutdownSkipsReadyState(); void destructorUnsubscribesCoreSignalsBeforeStoppingPolling(); void nodeNotificationHandlersUpdateModelThroughQueuedSignals(); void blockTipUpdatesQueuedAcrossThreadsRetainPayloadValues(); @@ -413,6 +414,27 @@ void NodeModelTests::initializationFailureWithoutCoreInterruptOnlySetsErrorState QCOMPARE(initialized_spy.count(), 1); } +void NodeModelTests::initializationSuccessDuringCoreShutdownSkipsReadyState() +{ + NiceMock<MockNode> node; + MempoolState mempool; + InstallDefaultHandlers(node); + InstallMempoolGetters(node, mempool); + ON_CALL(node, shutdownRequested()).WillByDefault(Return(true)); + + NodeModel model{node}; + WaitForInitialMempoolRefresh(mempool); + + QSignalSpy shutdown_spy{&model, &NodeModel::requestedShutdown}; + QSignalSpy initialized_spy{&model, &NodeModel::nodeInitialized}; + QSignalSpy ready_state_spy{&model, &NodeModel::setTimeRatioListInitial}; + model.initializeResult(true, {}); + + QCOMPARE(shutdown_spy.count(), 1); + QCOMPARE(initialized_spy.count(), 0); + QCOMPARE(ready_state_spy.count(), 0); +} + void NodeModelTests::destructorUnsubscribesCoreSignalsBeforeStoppingPolling() { NiceMock<MockNode> node; From ded8ef36054876fbe863158878ae3331f127ef14 Mon Sep 17 00:00:00 2001 From: johnny9 <johnny9dev@pm.me> Date: Thu, 11 Jun 2026 09:57:43 -0400 Subject: [PATCH 12/14] qml: avoid QtConcurrent in debug log model --- qml/bitcoin.cpp | 62 +++++++++--------- qml/models/debuglogmodel.cpp | 118 ++++++++++++++++++++++++++++++----- qml/models/debuglogmodel.h | 23 +++++-- 3 files changed, 154 insertions(+), 49 deletions(-) diff --git a/qml/bitcoin.cpp b/qml/bitcoin.cpp index a38514353d..58e54d07be 100644 --- a/qml/bitcoin.cpp +++ b/qml/bitcoin.cpp @@ -65,6 +65,7 @@ #include <tuple> #include <QDebug> +#include <QCoreApplication> #include <QFontDatabase> #include <QGuiApplication> #include <QQmlApplicationEngine> @@ -341,6 +342,7 @@ int QmlGuiMain(int argc, char* argv[]) node_model.addStartupWarnings(startup_warnings); QmlInitExecutor init_executor{*node}; bool shutdown_requested{false}; + DebugLogModel debug_log_model{gArgs.GetDataDirNet() / "debug.log"}; #ifdef ENABLE_WALLET std::unique_ptr<WalletQmlController> wallet_controller; if (wallet_enabled) { @@ -365,11 +367,12 @@ int QmlGuiMain(int argc, char* argv[]) wallet_controller->unloadWallets(); } #endif - node->startShutdown(); init_executor.shutdown(); }); QObject::connect(&init_executor, &QmlInitExecutor::initializeResult, &node_model, &NodeModel::initializeResult); - QObject::connect(&init_executor, &QmlInitExecutor::shutdownResult, qGuiApp, &QGuiApplication::quit, Qt::QueuedConnection); + QObject::connect(&init_executor, &QmlInitExecutor::shutdownResult, qGuiApp, [] { + QCoreApplication::exit(0); + }, Qt::QueuedConnection); QObject::connect(&init_executor, &QmlInitExecutor::runawayException, &node_model, &NodeModel::handleRunawayException); NetworkTrafficTower network_traffic_tower{node_model}; @@ -405,23 +408,21 @@ int QmlGuiMain(int argc, char* argv[]) LoadFontResource(":/fonts/bitcoincoresans/semibold"); LoadFontResource(":/fonts/robotomono/regular"); - QQmlApplicationEngine engine; + auto engine = std::make_unique<QQmlApplicationEngine>(); QScopedPointer<const NetworkStyle> network_style{NetworkStyle::instantiate(Params().GetChainType())}; assert(!network_style.isNull()); - engine.addImageProvider(QStringLiteral("images"), new ImageProvider{network_style.data()}); - engine.addImageProvider(QStringLiteral("qr"), new QRImageProvider); - - engine.rootContext()->setContextProperty("networkTrafficTower", &network_traffic_tower); - engine.rootContext()->setContextProperty("networkStatusModel", &network_status_model); - engine.rootContext()->setContextProperty("nodeModel", &node_model); - engine.rootContext()->setContextProperty("chainModel", &chain_model); - engine.rootContext()->setContextProperty("peerTableModel", &peer_model); - engine.rootContext()->setContextProperty("peerListModelProxy", &peer_model_sort_proxy); - engine.rootContext()->setContextProperty("banListModel", &ban_list_model); - - DebugLogModel debug_log_model{gArgs.GetDataDirNet() / "debug.log"}; - engine.rootContext()->setContextProperty("debugLogModel", &debug_log_model); + engine->addImageProvider(QStringLiteral("images"), new ImageProvider{network_style.data()}); + engine->addImageProvider(QStringLiteral("qr"), new QRImageProvider); + + engine->rootContext()->setContextProperty("networkTrafficTower", &network_traffic_tower); + engine->rootContext()->setContextProperty("networkStatusModel", &network_status_model); + engine->rootContext()->setContextProperty("nodeModel", &node_model); + engine->rootContext()->setContextProperty("chainModel", &chain_model); + engine->rootContext()->setContextProperty("peerTableModel", &peer_model); + engine->rootContext()->setContextProperty("peerListModelProxy", &peer_model_sort_proxy); + engine->rootContext()->setContextProperty("banListModel", &ban_list_model); + engine->rootContext()->setContextProperty("debugLogModel", &debug_log_model); #ifdef ENABLE_WALLET std::unique_ptr<WalletListModel> wallet_list_model; @@ -449,18 +450,18 @@ int QmlGuiMain(int argc, char* argv[]) list_model->listWalletDir(); } }); - engine.rootContext()->setContextProperty("walletController", wallet_controller.get()); - engine.rootContext()->setContextProperty("walletListModel", wallet_list_model.get()); + engine->rootContext()->setContextProperty("walletController", wallet_controller.get()); + engine->rootContext()->setContextProperty("walletListModel", wallet_list_model.get()); } #endif OptionsQmlModel options_model(*node, !need_onboarding.toBool()); - engine.rootContext()->setContextProperty("optionsModel", &options_model); - engine.rootContext()->setContextProperty("needOnboarding", need_onboarding); + engine->rootContext()->setContextProperty("optionsModel", &options_model); + engine->rootContext()->setContextProperty("needOnboarding", need_onboarding); #ifdef ENABLE_TEST_AUTOMATION - engine.rootContext()->setContextProperty("testAutomationEnabled", true); + engine->rootContext()->setContextProperty("testAutomationEnabled", true); #else - engine.rootContext()->setContextProperty("testAutomationEnabled", false); + engine->rootContext()->setContextProperty("testAutomationEnabled", false); #endif // -lang CLI flag overrides the persisted setting (bitcoin-qt compatibility). @@ -481,7 +482,7 @@ int QmlGuiMain(int argc, char* argv[]) // Retranslate the QML UI immediately when the user picks a new language. QObject::connect(&options_model, &OptionsQmlModel::languageChanged, [&]() { install_language(options_model.language()); - engine.retranslate(); + engine->retranslate(); }); BuildInfo build_info; @@ -515,12 +516,12 @@ int QmlGuiMain(int argc, char* argv[]) "WalletListModel cannot be instantiated from QML"); #endif - engine.load(QUrl(QStringLiteral("qrc:///qml/pages/main.qml"))); - if (engine.rootObjects().isEmpty()) { + engine->load(QUrl(QStringLiteral("qrc:///qml/pages/main.qml"))); + if (engine->rootObjects().isEmpty()) { return EXIT_FAILURE; } - auto window = qobject_cast<QQuickWindow*>(engine.rootObjects().first()); + auto window = qobject_cast<QQuickWindow*>(engine->rootObjects().first()); if (!window) { return EXIT_FAILURE; } @@ -534,7 +535,7 @@ int QmlGuiMain(int argc, char* argv[]) socket_path = QString::fromStdString( (gArgs.GetDataDirNet() / "test_bridge.sock").utf8string()); } - test_bridge = std::make_unique<TestBridge>(&engine, socket_path); + test_bridge = std::make_unique<TestBridge>(engine.get(), socket_path); } #endif @@ -544,5 +545,10 @@ int QmlGuiMain(int argc, char* argv[]) qInfo() << "Graphics API in use:" << QmlUtil::GraphicsApi(window); node_model.startShutdownPolling(); - return qGuiApp->exec(); + const int exit_code{qGuiApp->exec()}; +#ifdef ENABLE_TEST_AUTOMATION + test_bridge.reset(); +#endif + engine.reset(); + return exit_code; } diff --git a/qml/models/debuglogmodel.cpp b/qml/models/debuglogmodel.cpp index 44856167e6..6b8bd782c9 100644 --- a/qml/models/debuglogmodel.cpp +++ b/qml/models/debuglogmodel.cpp @@ -4,16 +4,21 @@ #include <qml/models/debuglogmodel.h> +#include <util/threadnames.h> + #include <algorithm> +#include <utility> #include <QDateTime> #include <QDesktopServices> #include <QFile> -#include <QFutureWatcher> +#include <QMetaObject> +#include <QObject> #include <QRegularExpression> #include <QTextStream> +#include <QThread> +#include <QTimer> #include <QUrl> -#include <QtConcurrent/QtConcurrentRun> static const QRegularExpression TIMESTAMP_RX( QStringLiteral(R"(^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)\s*(.*)$)")); @@ -22,6 +27,15 @@ DebugLogModel::DebugLogModel(const fs::path& log_path, QObject* parent) : QAbstractListModel(parent) , m_log_path(log_path) { + m_reader = new QObject; + m_reader_thread = new QThread(this); + m_reader->moveToThread(m_reader_thread); + connect(m_reader_thread, &QThread::finished, m_reader, &QObject::deleteLater); + m_reader_thread->start(); + QTimer::singleShot(0, m_reader, [] { + util::ThreadRename("qml-debuglog"); + }); + m_debounce.setSingleShot(true); m_debounce.setInterval(500); connect(&m_debounce, &QTimer::timeout, this, [this]() { refresh(); }); @@ -29,6 +43,14 @@ DebugLogModel::DebugLogModel(const fs::path& log_path, QObject* parent) connectFileWatcher(); } +DebugLogModel::~DebugLogModel() +{ + stop(); + if (m_reader_thread) { + m_reader_thread->wait(); + } +} + int DebugLogModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; @@ -75,9 +97,11 @@ void DebugLogModel::setFilter(const QString& filter) void DebugLogModel::refresh(bool full_load) { + if (m_stopping) return; + // Single-read-in-flight guard. If a read is already running, fold this // request into a trailing re-run rather than piling another job onto the - // thread pool. A burst of watcher events on a noisy node therefore + // worker thread. A burst of watcher events on a noisy node therefore // collapses to at most two reads: the one in flight, plus one trailer // that sees the final file state. if (m_read_in_flight) { @@ -96,14 +120,32 @@ void DebugLogModel::refresh(bool full_load) const fs::path path = m_log_path; const int load_limit = m_load_limit; - auto* watcher = new QFutureWatcher<ReadResult>(this); - connect(watcher, &QFutureWatcher<ReadResult>::finished, this, - [this, watcher, prev_top_content, full_load]() { - onReadCompleted(watcher->result(), prev_top_content, full_load); - watcher->deleteLater(); - }); - watcher->setFuture(QtConcurrent::run(&DebugLogModel::ReadAndFilter, - path, load_limit, full_load)); + if (!m_reader || !m_reader_thread || !m_reader_thread->isRunning()) { + m_read_in_flight = false; + return; + } + + const bool queued = QMetaObject::invokeMethod(m_reader, + [this, path, load_limit, full_load, prev_top_content]() mutable { + if (m_read_cancelled.load(std::memory_order_relaxed)) return; + + ReadResult result = ReadAndFilter(path, load_limit, full_load, m_read_cancelled); + if (m_read_cancelled.load(std::memory_order_relaxed)) return; + + QMetaObject::invokeMethod(this, + [this, + result = std::move(result), + prev_top_content, + full_load]() mutable { + if (m_stopping || m_read_cancelled.load(std::memory_order_relaxed)) return; + onReadCompleted(result, prev_top_content, full_load); + }, + Qt::QueuedConnection); + }, + Qt::QueuedConnection); + if (!queued) { + m_read_in_flight = false; + } } void DebugLogModel::loadMore() @@ -157,13 +199,41 @@ void DebugLogModel::updateRelativeTimes() buildDisplayLines(); } +void DebugLogModel::stop() +{ + if (m_stopping) return; + + m_stopping = true; + m_read_cancelled.store(true, std::memory_order_relaxed); + m_debounce.stop(); + + const auto watched_files = m_watcher.files(); + if (!watched_files.isEmpty()) { + m_watcher.removePaths(watched_files); + } + + m_refresh_pending = false; + m_pending_full_load = false; + m_read_in_flight = false; + + if (m_reader_thread) { + m_reader_thread->quit(); + if (QThread::currentThread() != m_reader_thread) { + m_reader_thread->wait(); + } + } +} + // ── Private ────────────────────────────────────────────────────────────────── DebugLogModel::ReadResult DebugLogModel::ReadAndFilter(const fs::path& log_path, int load_limit, - bool full_load) + bool full_load, + const std::atomic_bool& cancelled) { ReadResult result; + if (cancelled.load(std::memory_order_relaxed)) return result; + const QString path_str = QString::fromStdString(log_path.utf8string()); QFile probe(path_str); if (!probe.open(QIODevice::ReadOnly | QIODevice::Text)) { @@ -181,9 +251,14 @@ DebugLogModel::ReadResult DebugLogModel::ReadAndFilter(const fs::path& log_path, QList<LogLine> filtered; int fetch_size = load_limit; while (true) { - const QList<LogLine> raw = ReadRawLines(log_path, fetch_size); + if (cancelled.load(std::memory_order_relaxed)) return {}; + + const QList<LogLine> raw = ReadRawLines(log_path, fetch_size, cancelled); + if (cancelled.load(std::memory_order_relaxed)) return {}; + filtered.clear(); for (const LogLine& l : raw) { + if (cancelled.load(std::memory_order_relaxed)) return {}; if (!l.content.trimmed().isEmpty() || l.timestamp_ms >= 0) filtered.append(l); } @@ -196,8 +271,11 @@ DebugLogModel::ReadResult DebugLogModel::ReadAndFilter(const fs::path& log_path, } QList<DebugLogModel::LogLine> DebugLogModel::ReadRawLines(const fs::path& log_path, - int max_lines) + int max_lines, + const std::atomic_bool& cancelled) { + if (cancelled.load(std::memory_order_relaxed)) return {}; + const QString path_str = QString::fromStdString(log_path.utf8string()); QFile file(path_str); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return {}; @@ -213,7 +291,10 @@ QList<DebugLogModel::LogLine> DebugLogModel::ReadRawLines(const fs::path& log_pa QStringList raw; raw.reserve(max_lines); - while (!in.atEnd()) raw.append(in.readLine()); + while (!in.atEnd()) { + if (cancelled.load(std::memory_order_relaxed)) return {}; + raw.append(in.readLine()); + } if (raw.size() > max_lines) raw = raw.mid(raw.size() - max_lines); @@ -221,6 +302,8 @@ QList<DebugLogModel::LogLine> DebugLogModel::ReadRawLines(const fs::path& log_pa QList<LogLine> result; result.reserve(raw.size()); for (const QString& line : raw) { + if (cancelled.load(std::memory_order_relaxed)) return {}; + LogLine entry; const QRegularExpressionMatch m = TIMESTAMP_RX.match(line); if (m.hasMatch()) { @@ -243,6 +326,8 @@ void DebugLogModel::onReadCompleted(const ReadResult& result, const QString& prev_top_content, bool full_load) { + if (m_stopping) return; + // Propagate open-error state from the background read. if (!result.file_opened) { if (m_open_error != result.error_message) { @@ -337,7 +422,7 @@ void DebugLogModel::onReadCompleted(const ReadResult& result, m_read_in_flight = false; // If changes arrived while we were reading, run one trailing refresh. - if (m_refresh_pending) { + if (!m_stopping && m_refresh_pending) { m_refresh_pending = false; const bool do_full = m_pending_full_load; m_pending_full_load = false; @@ -352,6 +437,7 @@ void DebugLogModel::connectFileWatcher() m_watcher.addPath(path_str); connect(&m_watcher, &QFileSystemWatcher::fileChanged, this, [this](const QString& path) { + if (m_stopping) return; m_watcher.addPath(path); // re-add in case of log rotation m_debounce.start(); }); diff --git a/qml/models/debuglogmodel.h b/qml/models/debuglogmodel.h index 364ff9d0b1..f13fbb456e 100644 --- a/qml/models/debuglogmodel.h +++ b/qml/models/debuglogmodel.h @@ -13,6 +13,10 @@ #include <QString> #include <QTimer> +#include <atomic> + +class QThread; + //! List model for the in-app debug.log viewer. //! //! Exposes log lines as list items with three roles: @@ -27,7 +31,7 @@ //! //! File watching: the model connects a QFileSystemWatcher to the log file and //! coalesces rapid writes with a 500 ms debounce timer before calling refresh(). -//! Reads themselves run on the Qt thread pool so a busy, noisy node cannot +//! Reads themselves run on a dedicated worker thread so a busy, noisy node cannot //! stall the UI on every debug-log burst. class DebugLogModel : public QAbstractListModel { @@ -50,6 +54,7 @@ class DebugLogModel : public QAbstractListModel static constexpr int kMaxLoadLimit = 50'000; explicit DebugLogModel(const fs::path& log_path, QObject* parent = nullptr); + ~DebugLogModel() override; // QAbstractListModel interface int rowCount(const QModelIndex& parent = QModelIndex()) const override; @@ -70,6 +75,7 @@ class DebugLogModel : public QAbstractListModel Q_INVOKABLE void loadMore(); Q_INVOKABLE bool openLogFile(); Q_INVOKABLE void updateRelativeTimes(); + void stop(); Q_SIGNALS: void hasMoreLinesChanged(); @@ -97,13 +103,16 @@ class DebugLogModel : public QAbstractListModel }; //! File-reading worker. Pure function — no QObject / signal access — - //! so it can safely run on the thread pool. + //! so it can safely run on the dedicated worker thread. static ReadResult ReadAndFilter(const fs::path& log_path, int load_limit, - bool full_load); + bool full_load, + const std::atomic_bool& cancelled); //! Raw file read (one pass). Called from ReadAndFilter. - static QList<LogLine> ReadRawLines(const fs::path& log_path, int max_lines); + static QList<LogLine> ReadRawLines(const fs::path& log_path, + int max_lines, + const std::atomic_bool& cancelled); //! Completion handler invoked on the GUI thread after ReadAndFilter //! returns. Applies the result to m_all_lines and rebuilds the display. @@ -132,12 +141,16 @@ class DebugLogModel : public QAbstractListModel QFileSystemWatcher m_watcher; QTimer m_debounce; + QObject* m_reader{nullptr}; + QThread* m_reader_thread{nullptr}; //! Single-read-in-flight guard so concurrent refresh() calls fold into - //! one trailing read instead of piling up on the thread pool. + //! one trailing read instead of piling up on the worker thread. bool m_read_in_flight{false}; bool m_refresh_pending{false}; bool m_pending_full_load{false}; + bool m_stopping{false}; + std::atomic_bool m_read_cancelled{false}; }; #endif // BITCOIN_QML_MODELS_DEBUGLOGMODEL_H From 4469d932ad26a414ee65fe65fe98d4a7c8e81a0b Mon Sep 17 00:00:00 2001 From: johnny9 <johnny9dev@pm.me> Date: Thu, 11 Jun 2026 09:57:48 -0400 Subject: [PATCH 13/14] depends: add Qt Quick module patch --- CMakeLists.txt | 1 + ...ends-Add-Qt-Qml-and-Qt-Quick-modules.patch | 244 ++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 patches/depends-Add-Qt-Qml-and-Qt-Quick-modules.patch diff --git a/CMakeLists.txt b/CMakeLists.txt index 8661d8764d..4c2b7980ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,6 +172,7 @@ target_link_libraries(bitcoin-core-app bitcoinqml bitcoinqt ) +qt6_import_qml_plugins(bitcoin-core-app) option(BUILD_APP_TESTS "Build unit tests for the app" ON) if(BUILD_APP_TESTS) diff --git a/patches/depends-Add-Qt-Qml-and-Qt-Quick-modules.patch b/patches/depends-Add-Qt-Qml-and-Qt-Quick-modules.patch new file mode 100644 index 0000000000..73cf51cdd1 --- /dev/null +++ b/patches/depends-Add-Qt-Qml-and-Qt-Quick-modules.patch @@ -0,0 +1,244 @@ +From 09508f4d1c9f54f7c418ec5fd61974fd3b4176c8 Mon Sep 17 00:00:00 2001 +From: Hennadii Stepanov <32963518+hebasto@users.noreply.github.com> +Date: Mon, 8 Jun 2026 16:33:21 +0100 +Subject: [PATCH 2/3] depends: Add Qt Qml and Qt Quick modules + +--- + depends/packages/native_qt.mk | 22 +++++++++++++++--- + depends/packages/qt.mk | 27 +++++++++++++++++++++- + depends/packages/qt_details.mk | 6 +++++ + depends/patches/qt/qtbase_skip_tools.patch | 8 +++---- + depends/patches/qt/static_fixes.patch | 10 ++++++++ + 5 files changed, 64 insertions(+), 9 deletions(-) + +diff --git a/depends/packages/native_qt.mk b/depends/packages/native_qt.mk +index 2bf088c10a..6d2aec3734 100644 +--- a/depends/packages/native_qt.mk ++++ b/depends/packages/native_qt.mk +@@ -11,13 +11,21 @@ $(package)_patches += rcc_hardcode_timestamp.patch + $(package)_patches += qttools_skip_dependencies.patch + $(package)_patches += fix-macos26-qyield.patch + ++$(package)_qtshadertools_file_name=$(qt_details_qtshadertools_file_name) ++$(package)_qtshadertools_sha256_hash=$(qt_details_qtshadertools_sha256_hash) ++ ++$(package)_qtdeclarative_file_name=$(qt_details_qtdeclarative_file_name) ++$(package)_qtdeclarative_sha256_hash=$(qt_details_qtdeclarative_sha256_hash) ++ + $(package)_qttranslations_file_name=$(qt_details_qttranslations_file_name) + $(package)_qttranslations_sha256_hash=$(qt_details_qttranslations_sha256_hash) + + $(package)_qttools_file_name=$(qt_details_qttools_file_name) + $(package)_qttools_sha256_hash=$(qt_details_qttools_sha256_hash) + +-$(package)_extra_sources := $($(package)_qttranslations_file_name) ++$(package)_extra_sources := $($(package)_qtshadertools_file_name) ++$(package)_extra_sources += $($(package)_qtdeclarative_file_name) ++$(package)_extra_sources += $($(package)_qttranslations_file_name) + $(package)_extra_sources += $($(package)_qttools_file_name) + + $(package)_top_download_path=$(qt_details_top_download_path) +@@ -51,14 +59,13 @@ $(package)_config_opts += -no-feature-concurrent + $(package)_config_opts += -no-feature-network + $(package)_config_opts += -no-feature-printsupport + $(package)_config_opts += -no-feature-sql +-$(package)_config_opts += -no-feature-testlib + $(package)_config_opts += -no-feature-xml +-$(package)_config_opts += -no-gui + $(package)_config_opts += -no-widgets + + $(package)_config_opts += -no-glib + $(package)_config_opts += -no-icu + $(package)_config_opts += -no-libudev ++$(package)_config_opts += -no-opengl + $(package)_config_opts += -no-openssl + $(package)_config_opts += -no-zstd + $(package)_config_opts += -qt-pcre +@@ -67,6 +74,7 @@ $(package)_config_opts += -no-feature-backtrace + $(package)_config_opts += -no-feature-permissions + $(package)_config_opts += -no-feature-process + $(package)_config_opts += -no-feature-settings ++$(package)_config_opts += -no-feature-vulkan + + # Core tools. + $(package)_config_opts += -no-feature-androiddeployqt +@@ -106,6 +114,8 @@ endef + + define $(package)_fetch_cmds + $(call fetch_file,$(package),$($(package)_download_path),$($(package)_download_file),$($(package)_file_name),$($(package)_sha256_hash)) && \ ++$(call fetch_file,$(package),$($(package)_download_path),$($(package)_qtshadertools_file_name),$($(package)_qtshadertools_file_name),$($(package)_qtshadertools_sha256_hash)) && \ ++$(call fetch_file,$(package),$($(package)_download_path),$($(package)_qtdeclarative_file_name),$($(package)_qtdeclarative_file_name),$($(package)_qtdeclarative_sha256_hash)) && \ + $(call fetch_file,$(package),$($(package)_download_path),$($(package)_qttranslations_file_name),$($(package)_qttranslations_file_name),$($(package)_qttranslations_sha256_hash)) && \ + $(call fetch_file,$(package),$($(package)_download_path),$($(package)_qttools_file_name),$($(package)_qttools_file_name),$($(package)_qttools_sha256_hash)) && \ + $(call fetch_file,$(package),$($(package)_top_download_path),$($(package)_top_cmakelists_download_file),$($(package)_top_cmakelists_file_name)-$($(package)_version),$($(package)_top_cmakelists_sha256_hash)) && \ +@@ -116,6 +126,8 @@ endef + define $(package)_extract_cmds + mkdir -p $($(package)_extract_dir) && \ + echo "$($(package)_sha256_hash) $($(package)_source)" > $($(package)_extract_dir)/.$($(package)_file_name).hash && \ ++ echo "$($(package)_qtshadertools_sha256_hash) $($(package)_source_dir)/$($(package)_qtshadertools_file_name)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ ++ echo "$($(package)_qtdeclarative_sha256_hash) $($(package)_source_dir)/$($(package)_qtdeclarative_file_name)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ + echo "$($(package)_qttranslations_sha256_hash) $($(package)_source_dir)/$($(package)_qttranslations_file_name)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ + echo "$($(package)_qttools_sha256_hash) $($(package)_source_dir)/$($(package)_qttools_file_name)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ + echo "$($(package)_top_cmakelists_sha256_hash) $($(package)_source_dir)/$($(package)_top_cmakelists_file_name)-$($(package)_version)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ +@@ -124,6 +136,10 @@ define $(package)_extract_cmds + $(build_SHA256SUM) -c $($(package)_extract_dir)/.$($(package)_file_name).hash && \ + mkdir -p qtbase && \ + $(build_TAR) --no-same-owner --strip-components=1 -xf $($(package)_source) -C qtbase && \ ++ mkdir -p qtshadertools && \ ++ $(build_TAR) --no-same-owner --strip-components=1 -xf $($(package)_source_dir)/$($(package)_qtshadertools_file_name) -C qtshadertools && \ ++ mkdir -p qtdeclarative && \ ++ $(build_TAR) --no-same-owner --strip-components=1 -xf $($(package)_source_dir)/$($(package)_qtdeclarative_file_name) -C qtdeclarative && \ + mkdir -p qttranslations && \ + $(build_TAR) --no-same-owner --strip-components=1 -xf $($(package)_source_dir)/$($(package)_qttranslations_file_name) -C qttranslations && \ + mkdir -p qttools && \ +diff --git a/depends/packages/qt.mk b/depends/packages/qt.mk +index 9b7cec341d..b0f51fe437 100644 +--- a/depends/packages/qt.mk ++++ b/depends/packages/qt.mk +@@ -27,13 +27,21 @@ $(package)_patches += fix-gcc16-sfinae-qanystringview.patch + $(package)_patches += fix-macos26-qyield.patch + $(package)_patches += fix-qbytearray-include.patch + ++$(package)_qtshadertools_file_name=$(qt_details_qtshadertools_file_name) ++$(package)_qtshadertools_sha256_hash=$(qt_details_qtshadertools_sha256_hash) ++ ++$(package)_qtdeclarative_file_name=$(qt_details_qtdeclarative_file_name) ++$(package)_qtdeclarative_sha256_hash=$(qt_details_qtdeclarative_sha256_hash) ++ + $(package)_qttranslations_file_name=$(qt_details_qttranslations_file_name) + $(package)_qttranslations_sha256_hash=$(qt_details_qttranslations_sha256_hash) + + $(package)_qttools_file_name=$(qt_details_qttools_file_name) + $(package)_qttools_sha256_hash=$(qt_details_qttools_sha256_hash) + +-$(package)_extra_sources := $($(package)_qttranslations_file_name) ++$(package)_extra_sources := $($(package)_qtshadertools_file_name) ++$(package)_extra_sources += $($(package)_qtdeclarative_file_name) ++$(package)_extra_sources += $($(package)_qttranslations_file_name) + $(package)_extra_sources += $($(package)_qttools_file_name) + + $(package)_top_download_path=$(qt_details_top_download_path) +@@ -127,6 +135,9 @@ $(package)_config_opts += -no-feature-macdeployqt + $(package)_config_opts += -no-feature-qmake + $(package)_config_opts += -no-feature-windeployqt + ++# QML features. ++$(package)_config_opts += -no-feature-qml-profiler ++ + ifeq ($(host),$(build)) + # Qt Tools module. + $(package)_config_opts += -feature-linguist +@@ -224,6 +235,8 @@ endef + + define $(package)_fetch_cmds + $(call fetch_file,$(package),$($(package)_download_path),$($(package)_download_file),$($(package)_file_name),$($(package)_sha256_hash)) && \ ++$(call fetch_file,$(package),$($(package)_download_path),$($(package)_qtshadertools_file_name),$($(package)_qtshadertools_file_name),$($(package)_qtshadertools_sha256_hash)) && \ ++$(call fetch_file,$(package),$($(package)_download_path),$($(package)_qtdeclarative_file_name),$($(package)_qtdeclarative_file_name),$($(package)_qtdeclarative_sha256_hash)) && \ + $(call fetch_file,$(package),$($(package)_download_path),$($(package)_qttranslations_file_name),$($(package)_qttranslations_file_name),$($(package)_qttranslations_sha256_hash)) && \ + $(call fetch_file,$(package),$($(package)_download_path),$($(package)_qttools_file_name),$($(package)_qttools_file_name),$($(package)_qttools_sha256_hash)) && \ + $(call fetch_file,$(package),$($(package)_top_download_path),$($(package)_top_cmakelists_download_file),$($(package)_top_cmakelists_file_name)-$($(package)_version),$($(package)_top_cmakelists_sha256_hash)) && \ +@@ -235,6 +248,8 @@ ifeq ($(host),$(build)) + define $(package)_extract_cmds + mkdir -p $($(package)_extract_dir) && \ + echo "$($(package)_sha256_hash) $($(package)_source)" > $($(package)_extract_dir)/.$($(package)_file_name).hash && \ ++ echo "$($(package)_qtshadertools_sha256_hash) $($(package)_source_dir)/$($(package)_qtshadertools_file_name)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ ++ echo "$($(package)_qtdeclarative_sha256_hash) $($(package)_source_dir)/$($(package)_qtdeclarative_file_name)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ + echo "$($(package)_qttranslations_sha256_hash) $($(package)_source_dir)/$($(package)_qttranslations_file_name)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ + echo "$($(package)_qttools_sha256_hash) $($(package)_source_dir)/$($(package)_qttools_file_name)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ + echo "$($(package)_top_cmakelists_sha256_hash) $($(package)_source_dir)/$($(package)_top_cmakelists_file_name)-$($(package)_version)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ +@@ -243,6 +258,10 @@ define $(package)_extract_cmds + $(build_SHA256SUM) -c $($(package)_extract_dir)/.$($(package)_file_name).hash && \ + mkdir -p qtbase && \ + $(build_TAR) --no-same-owner --strip-components=1 -xf $($(package)_source) -C qtbase && \ ++ mkdir -p qtshadertools && \ ++ $(build_TAR) --no-same-owner --strip-components=1 -xf $($(package)_source_dir)/$($(package)_qtshadertools_file_name) -C qtshadertools && \ ++ mkdir -p qtdeclarative && \ ++ $(build_TAR) --no-same-owner --strip-components=1 -xf $($(package)_source_dir)/$($(package)_qtdeclarative_file_name) -C qtdeclarative && \ + mkdir -p qttranslations && \ + $(build_TAR) --no-same-owner --strip-components=1 -xf $($(package)_source_dir)/$($(package)_qttranslations_file_name) -C qttranslations && \ + mkdir -p qttools && \ +@@ -256,12 +275,18 @@ else + define $(package)_extract_cmds + mkdir -p $($(package)_extract_dir) && \ + echo "$($(package)_sha256_hash) $($(package)_source)" > $($(package)_extract_dir)/.$($(package)_file_name).hash && \ ++ echo "$($(package)_qtshadertools_sha256_hash) $($(package)_source_dir)/$($(package)_qtshadertools_file_name)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ ++ echo "$($(package)_qtdeclarative_sha256_hash) $($(package)_source_dir)/$($(package)_qtdeclarative_file_name)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ + echo "$($(package)_top_cmakelists_sha256_hash) $($(package)_source_dir)/$($(package)_top_cmakelists_file_name)-$($(package)_version)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ + echo "$($(package)_top_cmake_ecmoptionaladdsubdirectory_sha256_hash) $($(package)_source_dir)/$($(package)_top_cmake_ecmoptionaladdsubdirectory_file_name)-$($(package)_version)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ + echo "$($(package)_top_cmake_qttoplevelhelpers_sha256_hash) $($(package)_source_dir)/$($(package)_top_cmake_qttoplevelhelpers_file_name)-$($(package)_version)" >> $($(package)_extract_dir)/.$($(package)_file_name).hash && \ + $(build_SHA256SUM) -c $($(package)_extract_dir)/.$($(package)_file_name).hash && \ + mkdir -p qtbase && \ + $(build_TAR) --no-same-owner --strip-components=1 -xf $($(package)_source) -C qtbase && \ ++ mkdir -p qtshadertools && \ ++ $(build_TAR) --no-same-owner --strip-components=1 -xf $($(package)_source_dir)/$($(package)_qtshadertools_file_name) -C qtshadertools && \ ++ mkdir qtdeclarative && \ ++ $(build_TAR) --no-same-owner --strip-components=1 -xf $($(package)_source_dir)/$($(package)_qtdeclarative_file_name) -C qtdeclarative && \ + cp $($(package)_source_dir)/$($(package)_top_cmakelists_file_name)-$($(package)_version) ./$($(package)_top_cmakelists_file_name) && \ + mkdir -p cmake && \ + cp $($(package)_source_dir)/$($(package)_top_cmake_ecmoptionaladdsubdirectory_file_name)-$($(package)_version) cmake/$($(package)_top_cmake_ecmoptionaladdsubdirectory_file_name) && \ +diff --git a/depends/packages/qt_details.mk b/depends/packages/qt_details.mk +index e49ac6a287..1e7264ab1a 100644 +--- a/depends/packages/qt_details.mk ++++ b/depends/packages/qt_details.mk +@@ -5,6 +5,12 @@ qt_details_suffix := everywhere-src-$(qt_details_version).tar.xz + qt_details_qtbase_file_name := qtbase-$(qt_details_suffix) + qt_details_qtbase_sha256_hash := 56001b905601bb9023d399f3ba780d7fa940f3e4861e496a7c490331f49e0b80 + ++qt_details_qtshadertools_file_name = qtshadertools-$(qt_details_suffix) ++qt_details_qtshadertools_sha256_hash = f6ec88bf42deba84d8f6b5d0914636ceed4749ccb51d1945b2f79b322b7ecf47 ++ ++qt_details_qtdeclarative_file_name = qtdeclarative-$(qt_details_suffix) ++qt_details_qtdeclarative_sha256_hash = 1f03a2b8f5588b4face7da87926e9b2c1372b3a32157c52df07a75067a9db1af ++ + qt_details_qttranslations_file_name := qttranslations-$(qt_details_suffix) + qt_details_qttranslations_sha256_hash := c3c61d79c3d8fe316a20b3617c64673ce5b5519b2e45535f49bee313152fa531 + +diff --git a/depends/patches/qt/qtbase_skip_tools.patch b/depends/patches/qt/qtbase_skip_tools.patch +index eef65425d4..d5377c3f45 100644 +--- a/depends/patches/qt/qtbase_skip_tools.patch ++++ b/depends/patches/qt/qtbase_skip_tools.patch +@@ -3,8 +3,7 @@ Skip building/installing unneeded tools: + 1. Wrapper CMake scripts. + 2. CI support files. + 3. tracepointgen and tracegen tools. +-4. Qt Look Ahead LR Parser Generator (qlalr). +-5. Qt Vulkan Header Generator (qvkgen). ++4. Qt Vulkan Header Generator (qvkgen). + + + --- a/qtbase/cmake/QtBaseGlobalTargets.cmake +@@ -47,11 +46,10 @@ Skip building/installing unneeded tools: + + --- a/qtbase/src/tools/CMakeLists.txt + +++ b/qtbase/src/tools/CMakeLists.txt +-@@ -11,8 +11,6 @@ if (QT_FEATURE_dbus) +- add_subdirectory(qdbuscpp2xml) ++@@ -12,7 +12,6 @@ if (QT_FEATURE_dbus) + add_subdirectory(qdbusxml2cpp) + endif() +--add_subdirectory(qlalr) ++ add_subdirectory(qlalr) + -add_subdirectory(qvkgen) + if (QT_FEATURE_commandlineparser) + add_subdirectory(qtpaths) +diff --git a/depends/patches/qt/static_fixes.patch b/depends/patches/qt/static_fixes.patch +index 1ae3f5774a..317dd3608d 100644 +--- a/depends/patches/qt/static_fixes.patch ++++ b/depends/patches/qt/static_fixes.patch +@@ -78,3 +78,13 @@ index e8fb442dd43..e964138115c 100644 + CODE + "// xkb.h is using a variable called 'explicit', which is a reserved keyword in C++ + #define explicit dont_use_cxx_explicit ++--- a/qtbase/src/plugins/platforms/xcb/CMakeLists.txt +++++ b/qtbase/src/plugins/platforms/xcb/CMakeLists.txt ++@@ -65,6 +65,7 @@ qt_internal_add_module(XcbQpaPrivate ++ XCB::XFIXES ++ XCB::XKB ++ XKB::XKB +++ $<BUILD_INTERFACE:X11::Xau> ++ NO_UNITY_BUILD # X11 define clashes ++ ) ++ +-- +2.43.0 + From e5a893c991a3d7779b4d30c8765b76c623fa0b89 Mon Sep 17 00:00:00 2001 From: johnny9 <johnny9dev@pm.me> Date: Thu, 11 Jun 2026 09:57:51 -0400 Subject: [PATCH 14/14] ci: add depends build workflow --- .github/workflows/artifacts.yml | 2 +- .github/workflows/depends-build.yml | 168 ++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/depends-build.yml diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml index 9c5a449baa..c382d08a83 100644 --- a/.github/workflows/artifacts.yml +++ b/.github/workflows/artifacts.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-14, ubuntu-24.04] + os: [macos-15, ubuntu-24.04] steps: - name: Checkout diff --git a/.github/workflows/depends-build.yml b/.github/workflows/depends-build.yml new file mode 100644 index 0000000000..f726ba4ed1 --- /dev/null +++ b/.github/workflows/depends-build.yml @@ -0,0 +1,168 @@ +# Copyright (c) 2026 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +name: Depends Build + +on: + pull_request: + push: + branches: + - "**" + tags-ignore: + - "**" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + timeout-minutes: 360 + strategy: + fail-fast: false + matrix: + include: + - name: linux-x86_64 + os: ubuntu-24.04 + host: x86_64-pc-linux-gnu + make: make + cache-id: linux-x86_64 + - name: macos-native + os: macos-15 + host: "" + make: gmake + cache-id: macos-15-native + + env: + BUILD_DIR: build-depends + DEPENDS_PATCH: patches/depends-Add-Qt-Qml-and-Qt-Quick-modules.patch + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Linux build dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + bison \ + build-essential \ + cmake \ + curl \ + make \ + ninja-build \ + patch \ + pkgconf \ + python3 \ + xz-utils + echo "JOBS=$(nproc)" >> "$GITHUB_ENV" + + - name: Install macOS build dependencies + if: runner.os == 'macOS' + run: | + brew install bison cmake make ninja pkgconf python + echo "$(brew --prefix bison)/bin" >> "$GITHUB_PATH" + echo "JOBS=$(sysctl -n hw.logicalcpu)" >> "$GITHUB_ENV" + + - name: Select depends host + run: | + if [ -n "${{ matrix.host }}" ]; then + host="${{ matrix.host }}" + else + host="$(cd bitcoin/depends && ./config.guess)" + fi + host="$(cd bitcoin/depends && ./config.sub "$host")" + echo "HOST=${host}" >> "$GITHUB_ENV" + echo "Using depends host: ${host}" + + - name: Apply Bitcoin depends patch + run: | + if [ ! -f "${DEPENDS_PATCH}" ]; then + echo "::error::Missing ${DEPENDS_PATCH}" + exit 1 + fi + + patch="../${DEPENDS_PATCH}" + if git -C bitcoin apply --whitespace=nowarn --reverse --check "${patch}" >/dev/null 2>&1; then + echo "${DEPENDS_PATCH} is already applied" + else + git -C bitcoin apply --whitespace=nowarn --check "${patch}" + git -C bitcoin apply --whitespace=nowarn "${patch}" + fi + + - name: Restore depends sources cache + uses: actions/cache/restore@v4 + id: depends-sources-cache + with: + path: bitcoin/depends/sources + key: ${{ matrix.cache-id }}-depends-sources-${{ hashFiles('bitcoin/depends/packages/*.mk', 'bitcoin/depends/patches/**', 'patches/*.patch') }} + restore-keys: | + ${{ matrix.cache-id }}-depends-sources- + + - name: Restore depends package cache + uses: actions/cache/restore@v4 + id: depends-built-cache + with: + path: bitcoin/depends/built + key: ${{ matrix.cache-id }}-depends-built-${{ hashFiles('bitcoin/depends/Makefile', 'bitcoin/depends/funcs.mk', 'bitcoin/depends/builders/**', 'bitcoin/depends/hosts/**', 'bitcoin/depends/packages/*.mk', 'bitcoin/depends/patches/**', 'patches/*.patch') }} + restore-keys: | + ${{ matrix.cache-id }}-depends-built- + + - name: Build depends + run: | + cd bitcoin/depends + ${{ matrix.make }} -j"${JOBS}" HOST="${HOST}" DEBUG= LOG=1 + + - name: Save depends sources cache + uses: actions/cache/save@v4 + if: github.event_name != 'pull_request' && steps.depends-sources-cache.outputs.cache-hit != 'true' + with: + path: bitcoin/depends/sources + key: ${{ steps.depends-sources-cache.outputs.cache-primary-key }} + + - name: Save depends package cache + uses: actions/cache/save@v4 + if: github.event_name != 'pull_request' && steps.depends-built-cache.outputs.cache-hit != 'true' + with: + path: bitcoin/depends/built + key: ${{ steps.depends-built-cache.outputs.cache-primary-key }} + + - name: Configure + run: | + test -f "bitcoin/depends/${HOST}/toolchain.cmake" + cmake -S . -B "${BUILD_DIR}" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_TOOLCHAIN_FILE="${PWD}/bitcoin/depends/${HOST}/toolchain.cmake" \ + -DBUILD_APP_TESTS=OFF \ + -DBUILD_GUI=ON \ + -DENABLE_WALLET=ON \ + -DENABLE_IPC=OFF + + - name: Build bitcoin-core-app + run: | + cmake --build "${BUILD_DIR}" --target bitcoin-core-app --parallel "${JOBS}" + + - name: Upload depends logs + uses: actions/upload-artifact@v4 + if: failure() + with: + name: depends-logs-${{ matrix.cache-id }} + path: bitcoin/depends/*.log + if-no-files-found: ignore + + - name: Upload app artifact + uses: actions/upload-artifact@v4 + with: + name: bitcoin-core-app-${{ matrix.cache-id }} + path: ${{ env.BUILD_DIR }}/bin/bitcoin-core-app + if-no-files-found: error