Skip to content

Commit d2edf67

Browse files
committed
Issue 17: add activity RBF controls and wallet bump/cancel wiring
1 parent aff31a4 commit d2edf67

File tree

9 files changed

+815
-10
lines changed

9 files changed

+815
-10
lines changed

qml/models/activitylistmodel.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
7575
return tx->status;
7676
case TypeRole:
7777
return tx->type;
78+
case TxidRole:
79+
return tx->txid;
7880
default:
7981
return QVariant();
8082
}
@@ -90,6 +92,7 @@ QHash<int, QByteArray> ActivityListModel::roleNames() const
9092
roles[LabelRole] = "label";
9193
roles[StatusRole] = "status";
9294
roles[TypeRole] = "type";
95+
roles[TxidRole] = "txid";
9396
return roles;
9497
}
9598

qml/models/activitylistmodel.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ class ActivityListModel : public QAbstractListModel
3232
DepthRole,
3333
LabelRole,
3434
StatusRole,
35-
TypeRole
35+
TypeRole,
36+
TxidRole
3637
};
3738

3839
int rowCount(const QModelIndex &parent = QModelIndex()) const override;

qml/models/transaction.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Transaction::Transaction(
3333
, status(Unconfirmed)
3434
, time(time)
3535
, type(type)
36+
, txid(QString::fromStdString(hash.ToString()))
3637
, involvesWatchAddress(false)
3738
{
3839
}
@@ -42,6 +43,7 @@ Transaction::Transaction(uint256 hash, qint64 time)
4243
, hash(hash)
4344
, time(time)
4445
, type(Type::Other)
46+
, txid(QString::fromStdString(hash.ToString()))
4547
, involvesWatchAddress(false)
4648
{
4749
}

qml/models/walletqmlmodel.cpp

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
#include <qml/models/paymentrequest.h>
1010
#include <qml/models/sendrecipient.h>
1111
#include <qml/models/sendrecipientslistmodel.h>
12+
#include <qml/models/transactionrbfadapter.h>
1213
#include <qml/models/walletqmlmodeltransaction.h>
1314

1415
#include <consensus/amount.h>
1516
#include <interfaces/wallet.h>
1617
#include <key_io.h>
1718
#include <addresstype.h>
1819
#include <outputtype.h>
20+
#include <policy/feerate.h>
1921
#include <qml/bitcoinunits.h>
2022
#include <support/allocators/secure.h>
2123
#include <wallet/coincontrol.h>
@@ -29,6 +31,82 @@
2931

3032
#include <limits>
3133

34+
namespace {
35+
class WalletTransactionRbfBackend final : public TransactionRbfBackend
36+
{
37+
public:
38+
explicit WalletTransactionRbfBackend(interfaces::Wallet& wallet)
39+
: m_wallet(wallet)
40+
{
41+
}
42+
43+
bool transactionCanBeBumped(const Txid& txid) const override
44+
{
45+
return m_wallet.transactionCanBeBumped(txid);
46+
}
47+
48+
bool createBumpTransaction(const Txid& txid,
49+
const unsigned int target_blocks,
50+
const std::optional<CAmount> custom_fee_sat_per_kvb,
51+
std::vector<bilingual_str>& errors,
52+
CAmount& old_fee,
53+
CAmount& new_fee,
54+
CMutableTransaction& mtx) override
55+
{
56+
wallet::CCoinControl coin_control;
57+
coin_control.m_confirm_target = target_blocks;
58+
if (custom_fee_sat_per_kvb.has_value()) {
59+
coin_control.m_feerate = CFeeRate(*custom_fee_sat_per_kvb);
60+
}
61+
62+
return m_wallet.createBumpTransaction(txid,
63+
coin_control,
64+
errors,
65+
old_fee,
66+
new_fee,
67+
mtx);
68+
}
69+
70+
bool signBumpTransaction(CMutableTransaction& mtx) override
71+
{
72+
return m_wallet.signBumpTransaction(mtx);
73+
}
74+
75+
bool commitBumpTransaction(const Txid& txid,
76+
CMutableTransaction&& mtx,
77+
std::vector<bilingual_str>& errors,
78+
Txid& bumped_txid) override
79+
{
80+
return m_wallet.commitBumpTransaction(txid,
81+
std::move(mtx),
82+
errors,
83+
bumped_txid);
84+
}
85+
86+
bool transactionCanBeAbandoned(const Txid& txid) const override
87+
{
88+
return m_wallet.transactionCanBeAbandoned(txid);
89+
}
90+
91+
bool abandonTransaction(const Txid& txid) override
92+
{
93+
return m_wallet.abandonTransaction(txid);
94+
}
95+
96+
private:
97+
interfaces::Wallet& m_wallet;
98+
};
99+
100+
QVariantMap BuildRbfResultMap(const TransactionRbfActionResult& result)
101+
{
102+
QVariantMap payload;
103+
payload[QStringLiteral("success")] = result.success;
104+
payload[QStringLiteral("message")] = result.message;
105+
payload[QStringLiteral("replacementTxid")] = result.replacement_txid;
106+
return payload;
107+
}
108+
} // namespace
109+
32110
WalletQmlModel::WalletQmlModel(std::unique_ptr<interfaces::Wallet> wallet, QObject *parent)
33111
: QObject(parent)
34112
{
@@ -537,6 +615,120 @@ void WalletQmlModel::setCustomFeeRateSatPerKvB(const qint64 custom_fee_rate_sat_
537615
Q_EMIT customFeeRateSatPerKvBChanged();
538616
}
539617

618+
bool WalletQmlModel::canBumpTransaction(const QString& txid_text) const
619+
{
620+
if (!m_wallet) {
621+
return false;
622+
}
623+
624+
WalletTransactionRbfBackend backend(*m_wallet);
625+
return TransactionRbfAdapter::CanBump(backend, txid_text);
626+
}
627+
628+
bool WalletQmlModel::canAbandonTransaction(const QString& txid_text) const
629+
{
630+
if (!m_wallet) {
631+
return false;
632+
}
633+
634+
WalletTransactionRbfBackend backend(*m_wallet);
635+
return TransactionRbfAdapter::CanAbandon(backend, txid_text);
636+
}
637+
638+
QString WalletQmlModel::bumpIneligibleReason() const
639+
{
640+
return TransactionRbfAdapter::BumpIneligibleReason();
641+
}
642+
643+
QString WalletQmlModel::abandonIneligibleReason() const
644+
{
645+
return TransactionRbfAdapter::AbandonIneligibleReason();
646+
}
647+
648+
QVariantMap WalletQmlModel::prepareBumpTransaction(const QString& txid_text,
649+
const unsigned int target_blocks,
650+
const bool custom_fee_enabled,
651+
const qint64 custom_fee_rate_sat_per_kvb)
652+
{
653+
QVariantMap payload;
654+
if (!m_wallet) {
655+
payload[QStringLiteral("success")] = false;
656+
payload[QStringLiteral("message")] = tr("No wallet is loaded.");
657+
return payload;
658+
}
659+
660+
WalletTransactionRbfBackend backend(*m_wallet);
661+
const TransactionRbfPreview preview = TransactionRbfAdapter::PrepareBump(backend,
662+
txid_text,
663+
target_blocks,
664+
custom_fee_enabled,
665+
custom_fee_rate_sat_per_kvb);
666+
667+
payload[QStringLiteral("success")] = preview.success;
668+
payload[QStringLiteral("message")] = preview.message;
669+
payload[QStringLiteral("oldFeeDisplay")] = QmlBitcoinUnits::formatWithSettingsUnit(preview.old_fee);
670+
payload[QStringLiteral("newFeeDisplay")] = QmlBitcoinUnits::formatWithSettingsUnit(preview.new_fee);
671+
672+
if (preview.success) {
673+
m_prepared_bump_transaction = preview.replacement;
674+
m_prepared_bump_txid = txid_text.trimmed();
675+
} else {
676+
clearPreparedBump();
677+
}
678+
679+
return payload;
680+
}
681+
682+
QVariantMap WalletQmlModel::commitPreparedBumpTransaction(const QString& txid_text)
683+
{
684+
if (!m_wallet) {
685+
QVariantMap payload;
686+
payload[QStringLiteral("success")] = false;
687+
payload[QStringLiteral("message")] = tr("No wallet is loaded.");
688+
return payload;
689+
}
690+
691+
if (!m_prepared_bump_transaction.has_value()) {
692+
QVariantMap payload;
693+
payload[QStringLiteral("success")] = false;
694+
payload[QStringLiteral("message")] = tr("Prepare a replacement transaction first.");
695+
return payload;
696+
}
697+
698+
if (!m_prepared_bump_txid.isEmpty() && m_prepared_bump_txid != txid_text.trimmed()) {
699+
QVariantMap payload;
700+
payload[QStringLiteral("success")] = false;
701+
payload[QStringLiteral("message")] = tr("Prepared replacement does not match the selected transaction.");
702+
return payload;
703+
}
704+
705+
WalletTransactionRbfBackend backend(*m_wallet);
706+
const TransactionRbfActionResult result = TransactionRbfAdapter::CommitBump(backend,
707+
txid_text,
708+
std::move(*m_prepared_bump_transaction));
709+
clearPreparedBump();
710+
return BuildRbfResultMap(result);
711+
}
712+
713+
QVariantMap WalletQmlModel::abandonTransactionWithResult(const QString& txid_text)
714+
{
715+
if (!m_wallet) {
716+
QVariantMap payload;
717+
payload[QStringLiteral("success")] = false;
718+
payload[QStringLiteral("message")] = tr("No wallet is loaded.");
719+
return payload;
720+
}
721+
722+
WalletTransactionRbfBackend backend(*m_wallet);
723+
return BuildRbfResultMap(TransactionRbfAdapter::Abandon(backend, txid_text));
724+
}
725+
726+
void WalletQmlModel::clearPreparedBump()
727+
{
728+
m_prepared_bump_transaction.reset();
729+
m_prepared_bump_txid.clear();
730+
}
731+
540732
void WalletQmlModel::setLastPrepareError(const QString& error_message)
541733
{
542734
if (m_last_prepare_error == error_message) {

qml/models/walletqmlmodel.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
#include <wallet/coincontrol.h>
2222

2323
#include <memory>
24+
#include <optional>
2425
#include <vector>
2526

2627
#include <QObject>
28+
#include <QVariantMap>
2729

2830
class WalletQmlModel : public QObject, public WalletBalanceProvider
2931
{
@@ -99,6 +101,17 @@ class WalletQmlModel : public QObject, public WalletBalanceProvider
99101
void setCustomFeeRateSatPerKvB(qint64 custom_fee_rate_sat_per_kvb);
100102
QString lastPrepareError() const { return m_last_prepare_error; }
101103

104+
Q_INVOKABLE bool canBumpTransaction(const QString& txid_text) const;
105+
Q_INVOKABLE bool canAbandonTransaction(const QString& txid_text) const;
106+
Q_INVOKABLE QString bumpIneligibleReason() const;
107+
Q_INVOKABLE QString abandonIneligibleReason() const;
108+
Q_INVOKABLE QVariantMap prepareBumpTransaction(const QString& txid_text,
109+
unsigned int target_blocks,
110+
bool custom_fee_enabled,
111+
qint64 custom_fee_rate_sat_per_kvb);
112+
Q_INVOKABLE QVariantMap commitPreparedBumpTransaction(const QString& txid_text);
113+
Q_INVOKABLE QVariantMap abandonTransactionWithResult(const QString& txid_text);
114+
102115
bool isWalletLoaded() const { return m_is_wallet_loaded; }
103116
void setWalletLoaded(bool loaded);
104117
interfaces::Wallet* walletInterface() const { return m_wallet.get(); }
@@ -117,6 +130,7 @@ class WalletQmlModel : public QObject, public WalletBalanceProvider
117130
private:
118131
unsigned int nextPaymentRequestId() const;
119132
void setLastPrepareError(const QString& error_message);
133+
void clearPreparedBump();
120134

121135
std::unique_ptr<interfaces::Wallet> m_wallet;
122136
ActivityListModel* m_activity_list_model{nullptr};
@@ -130,6 +144,8 @@ class WalletQmlModel : public QObject, public WalletBalanceProvider
130144
bool m_custom_fee_enabled{false};
131145
qint64 m_custom_fee_rate_sat_per_kvb{1000};
132146
QString m_last_prepare_error{};
147+
std::optional<CMutableTransaction> m_prepared_bump_transaction{};
148+
QString m_prepared_bump_txid{};
133149
bool m_is_wallet_loaded{false};
134150
std::unique_ptr<interfaces::Handler> m_handler_unload;
135151
std::unique_ptr<interfaces::Handler> m_handler_address_book_changed;

qml/pages/wallet/Activity.qml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ PageStack {
213213
required property string label;
214214
required property int status;
215215
required property int type;
216+
required property string txid;
216217

217218
HoverHandler {
218219
cursorShape: Qt.PointingHandCursor
@@ -317,6 +318,7 @@ PageStack {
317318
status: delegate.status
318319
address: delegate.address
319320
label: delegate.label
321+
txid: delegate.txid
320322
}
321323
}
322324
}

0 commit comments

Comments
 (0)