Skip to content

Commit 84cc1cb

Browse files
committed
Merge bitcoin-core/gui-qml#442: Introduce the Desktop Wallet Activity Page
565819d qml: Introduce the Desktop Wallet Activity Page (johnny9) Pull request description: This commit introduces the basic Activity Page and Activity details Page. The pages are backed by activitylistmodel which queries the wallet interface and converts the WalletTx objects into Transaction objects in the list that returns the properties for each send/receive to one of our wallet's addresses. ![Screenshot from 2025-03-10 11-27-20](https://github.com/user-attachments/assets/d2021b25-612d-464a-a49d-cce317a4a28b) ![Screenshot from 2025-03-10 11-27-29](https://github.com/user-attachments/assets/ca6f2533-67f2-45f0-81ca-b45152e56a80) ACKs for top commit: hebasto: ACK 565819d, I have skimmed through the code and it looks OK. Tree-SHA512: febbf0d38b26932d34d69490d6db117eb6e25e55cc412138c7f0d38476325a03fd734fe1f2abc99d8312fc476315c50d7cc623349b8ec95a54dd2a5e1d7ec7a3
2 parents c975b88 + 565819d commit 84cc1cb

21 files changed

+1046
-11
lines changed

qml/bitcoin.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616
#include <noui.h>
1717
#include <qml/appmode.h>
1818
#include <qml/bitcoinamount.h>
19+
#include <qml/clipboard.h>
1920
#ifdef __ANDROID__
2021
#include <qml/androidnotifier.h>
2122
#endif
2223
#include <qml/components/blockclockdial.h>
2324
#include <qml/controls/linegraph.h>
2425
#include <qml/guiconstants.h>
26+
#include <qml/models/activitylistmodel.h>
2527
#include <qml/models/chainmodel.h>
2628
#include <qml/models/networktraffictower.h>
2729
#include <qml/models/nodemodel.h>
@@ -326,12 +328,15 @@ int QmlGuiMain(int argc, char* argv[])
326328
engine.rootContext()->setContextProperty("needOnboarding", need_onboarding);
327329

328330
AppMode app_mode = SetupAppMode();
331+
Clipboard clipboard;
329332

330333
qmlRegisterSingletonInstance<AppMode>("org.bitcoincore.qt", 1, 0, "AppMode", &app_mode);
334+
qmlRegisterSingletonInstance<Clipboard>("org.bitcoincore.qt", 1, 0, "Clipboard", &clipboard);
331335
qmlRegisterType<BlockClockDial>("org.bitcoincore.qt", 1, 0, "BlockClockDial");
332336
qmlRegisterType<LineGraph>("org.bitcoincore.qt", 1, 0, "LineGraph");
333337
qmlRegisterUncreatableType<PeerDetailsModel>("org.bitcoincore.qt", 1, 0, "PeerDetailsModel", "");
334338
qmlRegisterType<BitcoinAmount>("org.bitcoincore.qt", 1, 0, "BitcoinAmount");
339+
qmlRegisterUncreatableType<Transaction>("org.bitcoincore.qt", 1, 0, "Transaction", "");
335340

336341
#ifdef ENABLE_WALLET
337342
qmlRegisterUncreatableType<WalletQmlModel>("org.bitcoincore.qt", 1, 0, "WalletQmlModel",

qml/bitcoin_qml.qrc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,15 @@
3232
<file>controls/InformationPage.qml</file>
3333
<file>controls/IPAddressValueInput.qml</file>
3434
<file>controls/KeyValueRow.qml</file>
35+
<file>controls/LabeledTextInput.qml</file>
3536
<file>controls/NavButton.qml</file>
36-
<file>controls/PageIndicator.qml</file>
3737
<file>controls/NavigationBar.qml</file>
3838
<file>controls/NavigationBar2.qml</file>
3939
<file>controls/NavigationTab.qml</file>
4040
<file>controls/OptionButton.qml</file>
4141
<file>controls/OptionSwitch.qml</file>
4242
<file>controls/OutlineButton.qml</file>
43+
<file>controls/PageIndicator.qml</file>
4344
<file>controls/PageStack.qml</file>
4445
<file>controls/ProgressIndicator.qml</file>
4546
<file>controls/qmldir</file>
@@ -72,6 +73,8 @@
7273
<file>pages/settings/SettingsProxy.qml</file>
7374
<file>pages/settings/SettingsStorage.qml</file>
7475
<file>pages/settings/SettingsTheme.qml</file>
76+
<file>pages/wallet/Activity.qml</file>
77+
<file>pages/wallet/ActivityDetails.qml</file>
7578
<file>pages/wallet/CreateBackup.qml</file>
7679
<file>pages/wallet/CreateConfirm.qml</file>
7780
<file>pages/wallet/CreateIntro.qml</file>
@@ -97,6 +100,7 @@
97100
<file alias="caret-right">res/icons/caret-right.png</file>
98101
<file alias="check">res/icons/check.png</file>
99102
<file alias="copy">res/icons/copy.png</file>
103+
<file alias="coinbase">res/icons/coinbase.png</file>
100104
<file alias="cross">res/icons/cross.png</file>
101105
<file alias="error">res/icons/error.png</file>
102106
<file alias="export">res/icons/export.png</file>
@@ -116,6 +120,8 @@
116120
<file alias="storage-light">res/icons/storage-light.png</file>
117121
<file alias="tooltip-arrow-dark">res/icons/tooltip-arrow-dark.png</file>
118122
<file alias="tooltip-arrow-light">res/icons/tooltip-arrow-light.png</file>
123+
<file alias="triangle-down">res/icons/triangle-down.png</file>
124+
<file alias="triangle-up">res/icons/triangle-up.png</file>
119125
<file alias="wallet">res/icons/wallet.png</file>
120126
<file alias="visible">res/icons/visible.png</file>
121127
</qresource>

qml/clipboard.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) 2024 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef BITCOIN_QML_CLIPBOARD_H
6+
#define BITCOIN_QML_CLIPBOARD_H
7+
8+
#include <QObject>
9+
#include <QClipboard>
10+
#include <QGuiApplication>
11+
12+
class Clipboard : public QObject
13+
{
14+
Q_OBJECT
15+
public:
16+
explicit Clipboard(QObject *parent = nullptr) : QObject(parent) {}
17+
18+
Q_INVOKABLE void setText(const QString &text) {
19+
QGuiApplication::clipboard()->setText(text);
20+
}
21+
22+
Q_INVOKABLE QString text() const {
23+
return QGuiApplication::clipboard()->text();
24+
}
25+
};
26+
27+
#endif // BITCOIN_QML_CLIPBOARD_H

qml/controls/LabeledTextInput.qml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,23 @@ Item {
1818
signal textEdited
1919

2020
id: root
21-
implicitHeight: label.height + input.height
21+
implicitHeight: input.height
2222

2323
CoreText {
2424
id: label
2525
anchors.left: parent.left
26-
anchors.top: parent.top
27-
color: Theme.color.neutral7
28-
font.pixelSize: 15
26+
anchors.verticalCenter: parent.verticalCenter
27+
horizontalAlignment: Text.AlignLeft
28+
width: 110
29+
color: Theme.color.neutral9
30+
font.pixelSize: 18
2931
}
3032

3133
TextField {
3234
id: input
33-
anchors.left: parent.left
35+
anchors.left: label.right
3436
anchors.right: iconContainer.left
35-
anchors.bottom: parent.bottom
37+
anchors.verticalCenter: parent.verticalCenter
3638
leftPadding: 0
3739
font.family: "Inter"
3840
font.styleName: "Regular"

qml/imageprovider.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ QPixmap ImageProvider::requestPixmap(const QString& id, QSize* size, const QSize
7272
return QIcon(":/icons/caret-right").pixmap(requested_size);
7373
}
7474

75+
if (id == "coinbase") {
76+
*size = requested_size;
77+
return QIcon(":/icons/coinbase").pixmap(requested_size);
78+
}
79+
7580
if (id == "check") {
7681
*size = requested_size;
7782
return QIcon(":/icons/check").pixmap(requested_size);
@@ -167,6 +172,16 @@ QPixmap ImageProvider::requestPixmap(const QString& id, QSize* size, const QSize
167172
return QIcon(":/icons/tooltip-arrow-light").pixmap(requested_size);
168173
}
169174

175+
if (id == "triangle-up") {
176+
*size = requested_size;
177+
return QIcon(":/icons/triangle-up").pixmap(requested_size);
178+
}
179+
180+
if (id == "triangle-down") {
181+
*size = requested_size;
182+
return QIcon(":/icons/triangle-down").pixmap(requested_size);
183+
}
184+
170185
if (id == "add-wallet-dark") {
171186
*size = requested_size;
172187
return QIcon(":/icons/add-wallet-dark").pixmap(requested_size);

qml/models/activitylistmodel.cpp

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright (c) 2024-2025 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <qml/models/activitylistmodel.h>
6+
7+
#include <qml/models/walletqmlmodel.h>
8+
9+
ActivityListModel::ActivityListModel(WalletQmlModel *parent)
10+
: QAbstractListModel(parent)
11+
, m_wallet_model(parent)
12+
{
13+
if (m_wallet_model != nullptr) {
14+
refreshWallet();
15+
subsctribeToCoreSignals();
16+
}
17+
}
18+
19+
ActivityListModel::~ActivityListModel()
20+
{
21+
unsubscribeFromCoreSignals();
22+
}
23+
24+
int ActivityListModel::rowCount(const QModelIndex &parent) const
25+
{
26+
Q_UNUSED(parent)
27+
return m_transactions.size();
28+
}
29+
30+
void ActivityListModel::updateTransactionStatus(QSharedPointer<Transaction> tx) const
31+
{
32+
if (m_wallet_model == nullptr) {
33+
return;
34+
}
35+
interfaces::WalletTxStatus wtx;
36+
int num_blocks;
37+
int64_t block_time;
38+
if (m_wallet_model->tryGetTxStatus(tx->hash, wtx, num_blocks, block_time)) {
39+
tx->updateStatus(wtx, num_blocks, block_time);
40+
} else {
41+
tx->status = Transaction::Status::NotAccepted;
42+
}
43+
}
44+
45+
QVariant ActivityListModel::data(const QModelIndex &index, int role) const
46+
{
47+
if (!index.isValid() || index.row() < 0 || index.row() >= m_transactions.size())
48+
return QVariant();
49+
50+
QSharedPointer<Transaction> tx = m_transactions.at(index.row());
51+
updateTransactionStatus(tx);
52+
53+
switch (role) {
54+
case AddressRole:
55+
return tx->address;
56+
case AmountRole:
57+
return tx->prettyAmount();
58+
case DateTimeRole:
59+
return tx->dateTimeString();
60+
case DepthRole:
61+
return tx->depth;
62+
case LabelRole:
63+
return tx->label;
64+
case StatusRole:
65+
return tx->status;
66+
case TypeRole:
67+
return tx->type;
68+
default:
69+
return QVariant();
70+
}
71+
}
72+
73+
QHash<int, QByteArray> ActivityListModel::roleNames() const
74+
{
75+
QHash<int, QByteArray> roles;
76+
roles[AddressRole] = "address";
77+
roles[AmountRole] = "amount";
78+
roles[DateTimeRole] = "date";
79+
roles[DepthRole] = "depth";
80+
roles[LabelRole] = "label";
81+
roles[StatusRole] = "status";
82+
roles[TypeRole] = "type";
83+
return roles;
84+
}
85+
86+
void ActivityListModel::refreshWallet()
87+
{
88+
if (m_wallet_model == nullptr) {
89+
return;
90+
}
91+
for (const auto &tx : m_wallet_model->getWalletTxs()) {
92+
auto transactions = Transaction::fromWalletTx(tx);
93+
m_transactions.append(transactions);
94+
for (const auto &transaction : transactions) {
95+
updateTransactionStatus(transaction);
96+
}
97+
}
98+
std::sort(m_transactions.begin(), m_transactions.end(),
99+
[](const QSharedPointer<Transaction> &a, const QSharedPointer<Transaction> &b) {
100+
return a->depth < b->depth;
101+
});
102+
}
103+
104+
void ActivityListModel::updateTransaction(const uint256& hash, const interfaces::WalletTxStatus& tx_status, int num_blocks, int64_t block_time)
105+
{
106+
int index = findTransactionIndex(hash);
107+
108+
if (index != -1) {
109+
QSharedPointer<Transaction> tx = m_transactions.at(index);
110+
tx->updateStatus(tx_status, num_blocks, block_time);
111+
Q_EMIT dataChanged(this->index(index), this->index(index));
112+
} else {
113+
// new transaction
114+
interfaces::WalletTx wtx = m_wallet_model->getWalletTx(hash);
115+
auto transactions = Transaction::fromWalletTx(wtx);
116+
for (const auto& tx : transactions) {
117+
tx->updateStatus(tx_status, num_blocks, block_time);
118+
m_transactions.push_front(tx);
119+
}
120+
Q_EMIT dataChanged(this->index(0), this->index(m_transactions.size() - 1));
121+
}
122+
}
123+
124+
125+
int ActivityListModel::findTransactionIndex(const uint256& hash) const
126+
{
127+
auto it = std::find_if(m_transactions.begin(), m_transactions.end(),
128+
[&hash](const QSharedPointer<Transaction>& tx) {
129+
return tx->hash == hash;
130+
});
131+
if (it != m_transactions.end()) {
132+
return std::distance(m_transactions.begin(), it);
133+
}
134+
return -1;
135+
}
136+
137+
void ActivityListModel::subsctribeToCoreSignals()
138+
{
139+
// Connect signals to wallet
140+
m_handler_transaction_changed = m_wallet_model->handleTransactionChanged([this](const uint256& hash, ChangeType status) {
141+
interfaces::WalletTxStatus wtx;
142+
int num_blocks;
143+
int64_t block_time;
144+
if (m_wallet_model->tryGetTxStatus(hash, wtx, num_blocks, block_time)) {
145+
updateTransaction(hash, wtx, num_blocks, block_time);
146+
}
147+
});
148+
}
149+
150+
void ActivityListModel::unsubscribeFromCoreSignals()
151+
{
152+
// Disconnect signals from wallet
153+
if (m_handler_transaction_changed) {
154+
m_handler_transaction_changed->disconnect();
155+
}
156+
}

qml/models/activitylistmodel.h

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) 2024-2025 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef BITCOIN_QML_MODELS_ACTIVITYLISTMODEL_H
6+
#define BITCOIN_QML_MODELS_ACTIVITYLISTMODEL_H
7+
8+
#include <interfaces/handler.h>
9+
#include <interfaces/wallet.h>
10+
#include <qml/models/transaction.h>
11+
12+
#include <memory>
13+
#include <QAbstractListModel>
14+
#include <QList>
15+
#include <QSharedPointer>
16+
#include <QString>
17+
18+
class WalletQmlModel;
19+
20+
class ActivityListModel : public QAbstractListModel
21+
{
22+
Q_OBJECT
23+
24+
public:
25+
explicit ActivityListModel(WalletQmlModel * parent = nullptr);
26+
~ActivityListModel();
27+
28+
enum TransactionRoles {
29+
AddressRole = Qt::UserRole + 1,
30+
AmountRole,
31+
DateTimeRole,
32+
DepthRole,
33+
LabelRole,
34+
StatusRole,
35+
TypeRole
36+
};
37+
38+
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
39+
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
40+
QHash<int, QByteArray> roleNames() const override;
41+
42+
private:
43+
void refreshWallet();
44+
void updateTransactionStatus(QSharedPointer<Transaction> tx) const;
45+
void subsctribeToCoreSignals();
46+
void unsubscribeFromCoreSignals();
47+
void updateTransaction(const uint256& hash, const interfaces::WalletTxStatus& wtx,
48+
int num_blocks, int64_t block_time);
49+
int findTransactionIndex(const uint256& hash) const;
50+
51+
QList<QSharedPointer<Transaction>> m_transactions;
52+
WalletQmlModel* m_wallet_model;
53+
std::unique_ptr<interfaces::Handler> m_handler_transaction_changed;
54+
std::unique_ptr<interfaces::Handler> m_handler_show_progress;
55+
};
56+
57+
#endif // BITCOIN_QML_MODELS_ACTIVITYLISTMODEL_H

0 commit comments

Comments
 (0)