2020
2121#include < key.h>
2222#include < key_io.h>
23+ #include < script/descriptor.h>
2324#include < wallet/wallet.h>
2425#include < wallet/test/util.h>
2526#include < walletinitinterface.h>
@@ -38,6 +39,7 @@ using wallet::CreateMockableWalletDatabase;
3839using wallet::RemoveWallet;
3940using wallet::WALLET_FLAG_DESCRIPTORS ;
4041using wallet::WalletContext;
42+ using wallet::WalletDescriptor;
4143
4244namespace
4345{
@@ -210,6 +212,86 @@ void TestAddAddressesToSendBook(interfaces::Node& node)
210212 QCOMPARE (table_view->model ()->rowCount (), 3 );
211213}
212214
215+ /* *
216+ * Test that CanSignMessageRole correctly filters the sign-message address picker:
217+ * - watch-only PKHash addresses (no private key) → not signable
218+ * - bech32 addresses (not PKHash) → not signable
219+ * - spendable PKHash addresses (private key imported) → signable
220+ * Also verifies AddressBookPage::AddressFilter::Signable shows only signable addresses.
221+ */
222+ void TestSignableAddressFilter (interfaces::Node& node)
223+ {
224+ TestChain100Setup test;
225+ auto wallet_loader = interfaces::MakeWalletLoader (*test.m_node .chain , *Assert (test.m_node .args ));
226+ test.m_node .wallet_loader = wallet_loader.get ();
227+ node.setContext (&test.m_node );
228+
229+ const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(
230+ node.context ()->chain .get (), " " , CreateMockableWalletDatabase ());
231+
232+ {
233+ LOCK (wallet->cs_wallet );
234+ wallet->SetWalletFlag (WALLET_FLAG_DESCRIPTORS );
235+ wallet->SetupDescriptorScriptPubKeyMans ();
236+
237+ // (a) Watch-only PKHash: address in book but no private key → not signable
238+ const PKHash watchonly_dest{GenerateRandomKey ().GetPubKey ()};
239+ wallet->SetAddressBook (watchonly_dest, " watchonly" , wallet::AddressPurpose::RECEIVE );
240+
241+ // (b) Bech32 (P2WPKH): not a PKHash destination → not signable
242+ const WitnessV0KeyHash bech32_dest{GenerateRandomKey ().GetPubKey ()};
243+ wallet->SetAddressBook (bech32_dest, " bech32" , wallet::AddressPurpose::RECEIVE );
244+
245+ // (c) Spendable PKHash: private key imported via pkh() descriptor → signable
246+ CKey legacy_key;
247+ legacy_key.MakeNewKey (true );
248+ FlatSigningProvider legacy_provider;
249+ std::string legacy_err;
250+ auto legacy_descs = Parse (" pkh(" + EncodeSecret (legacy_key) + " )" , legacy_provider, legacy_err, /* require_checksum=*/ false );
251+ assert (!legacy_descs.empty ());
252+ WalletDescriptor legacy_w_desc (std::move (legacy_descs[0 ]), /* creation_time=*/ 0 , /* range_start=*/ 0 , /* range_end=*/ 1 , /* next_index=*/ 1 );
253+ QVERIFY (wallet->AddWalletDescriptor (legacy_w_desc, legacy_provider, " " , /* internal=*/ false ));
254+ const PKHash legacy_dest{legacy_key.GetPubKey ()};
255+ wallet->SetAddressBook (legacy_dest, " legacy" , wallet::AddressPurpose::RECEIVE );
256+ }
257+
258+ std::unique_ptr<const PlatformStyle> platformStyle (PlatformStyle::instantiate (" other" ));
259+ OptionsModel optionsModel (node);
260+ bilingual_str error;
261+ QVERIFY (optionsModel.Init (error));
262+ ClientModel clientModel (node, &optionsModel);
263+ WalletContext& context = *node.walletLoader ().context ();
264+ AddWallet (context, wallet);
265+ WalletModel walletModel (interfaces::MakeWallet (context, wallet), clientModel, platformStyle.get ());
266+ RemoveWallet (context, wallet, /* load_on_start=*/ std::nullopt );
267+ AddressTableModel* addrModel = walletModel.getAddressTableModel ();
268+ QVERIFY (addrModel);
269+
270+ // Only the spendable PKHash should report CanSignMessageRole=true
271+ int signable_count = 0 ;
272+ for (int i = 0 ; i < addrModel->rowCount ({}); ++i) {
273+ if (addrModel->data (addrModel->index (i, AddressTableModel::Address, {}),
274+ AddressTableModel::CanSignMessageRole).toBool ()) {
275+ ++signable_count;
276+ }
277+ }
278+ QCOMPARE (signable_count, 1 );
279+
280+ // AddressFilter::Signable must show only the spendable PKHash; no filter shows all
281+ AddressBookPage page_all{platformStyle.get (), AddressBookPage::ForSelection, AddressBookPage::ReceivingTab};
282+ page_all.setModel (addrModel);
283+ AddressBookPage page_signable{platformStyle.get (), AddressBookPage::ForSelection, AddressBookPage::ReceivingTab,
284+ nullptr , AddressBookPage::AddressFilter::Signable};
285+ page_signable.setModel (addrModel);
286+
287+ auto * table_all = page_all.findChild <QTableView*>(" tableView" );
288+ auto * table_signable = page_signable.findChild <QTableView*>(" tableView" );
289+ QVERIFY (table_all != nullptr );
290+ QVERIFY (table_signable != nullptr );
291+ QCOMPARE (table_signable->model ()->rowCount (), 1 );
292+ QVERIFY (table_all->model ()->rowCount () >= 3 );
293+ }
294+
213295/* *
214296 * Regression test for the stale shared model bug: clicking the address book
215297 * button in SignVerifyMessageDialog previously called
@@ -288,4 +370,5 @@ void AddressBookTests::addressBookTests()
288370#endif
289371 TestAddAddressesToSendBook (m_node);
290372 TestAddressTableModelStability (m_node);
373+ TestSignableAddressFilter (m_node);
291374}
0 commit comments