Skip to content

Commit 4f32c7a

Browse files
committed
qml: handle receive address generation unlock
1 parent 408c9f8 commit 4f32c7a

10 files changed

Lines changed: 410 additions & 9 deletions

qml/components/ReceiveOptionsPopup.qml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ OptionPopup {
2020

2121
signal useAsTemplate()
2222
signal deleteFromHistory()
23+
signal viewAddressHistory()
2324

2425
implicitWidth: 300
2526
implicitHeight: columnLayout.implicitHeight + 20
@@ -66,9 +67,13 @@ OptionPopup {
6667
}
6768

6869
EllipsisMenuButtonItem {
70+
objectName: "receiveOptionsViewAddressHistoryButton"
6971
Layout.fillWidth: true
7072
text: qsTr("View address history")
71-
enabled: false
73+
onClicked: {
74+
root.close()
75+
root.viewAddressHistory()
76+
}
7277
}
7378

7479
EllipsisMenuButtonItem {

qml/models/walletqmlmodel.cpp

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -657,28 +657,72 @@ bool WalletQmlModel::setCurrentPaymentRequestAddress(QString address)
657657

658658
bool WalletQmlModel::commitPaymentRequest()
659659
{
660+
return commitPaymentRequestInternal(std::nullopt);
661+
}
662+
663+
bool WalletQmlModel::commitPaymentRequestWithPassphrase(const QString& passphrase)
664+
{
665+
return commitPaymentRequestInternal(std::optional<SecureString>{SecureStringFromQString(passphrase)});
666+
}
667+
668+
bool WalletQmlModel::commitPaymentRequestInternal(std::optional<SecureString> passphrase)
669+
{
670+
const auto clear_passphrase = [&]() {
671+
if (passphrase.has_value()) {
672+
ClearSecureString(*passphrase);
673+
passphrase.reset();
674+
}
675+
};
676+
677+
clearTransactionStatus();
660678
if (!m_wallet || !m_current_payment_request) {
679+
clear_passphrase();
680+
setTransactionStatus(tr("No payment request is available."));
661681
return false;
662682
}
663683

664684
if (m_current_payment_request->address().isEmpty()) {
685+
if (!m_wallet->canGetAddresses() && !m_wallet->privateKeysDisabled() && m_wallet->isCrypted() && m_wallet->isLocked() && !passphrase.has_value()) {
686+
refreshSecurityState();
687+
setTransactionStatus(tr("Enter your wallet password to generate a payment request."), true);
688+
return false;
689+
}
690+
691+
bool relock{false};
692+
if (!unlockForAction(passphrase, relock)) {
693+
return false;
694+
}
695+
WalletRelockGuard relock_guard{*m_wallet, [this] { refreshSecurityState(); }, relock};
696+
697+
if (!m_wallet->canGetAddresses()) {
698+
relock_guard.relock();
699+
setTransactionStatus(tr("This wallet cannot generate new addresses."));
700+
return false;
701+
}
702+
665703
const QString address_type_id = m_current_payment_request->addressType().isEmpty()
666704
? defaultReceiveAddressType()
667705
: m_current_payment_request->addressType();
668706
OutputType output_type = m_wallet->getDefaultAddressType();
669707
if (!address_type_id.isEmpty()) {
670708
const auto parsed_type{ParseOutputType(address_type_id.toStdString())};
671709
if (!parsed_type) {
710+
relock_guard.relock();
711+
setTransactionStatus(tr("Unknown address type."));
672712
return false;
673713
}
674714
output_type = *parsed_type;
675715
}
676716
const auto destination{m_wallet->getNewDestination(output_type, m_current_payment_request->label().toStdString())};
677717
if (!destination || !IsValidDestination(destination.value())) {
718+
relock_guard.relock();
719+
setTransactionStatus(destination ? tr("Generated address is invalid.") : LocalizedString(util::ErrorString(destination)));
678720
return false;
679721
}
680722
m_current_payment_request->setDestination(destination.value());
681723
m_current_payment_request->setAddressType(OutputTypeId(output_type));
724+
} else {
725+
clear_passphrase();
682726
}
683727

684728
const bool is_update{!m_current_payment_request->id().isEmpty()};
@@ -711,7 +755,10 @@ bool WalletQmlModel::commitPaymentRequest()
711755
request_id_text.toStdString(),
712756
ReceiveRequestHistoryModel::SerializeEntry(request_entry));
713757

714-
if (!persisted) return false;
758+
if (!persisted) {
759+
setTransactionStatus(tr("The payment request could not be saved."));
760+
return false;
761+
}
715762

716763
if (m_current_payment_request->id().isEmpty()) {
717764
m_current_payment_request->setId(static_cast<unsigned int>(request_id));
@@ -1190,6 +1237,14 @@ std::unique_ptr<interfaces::Handler> WalletQmlModel::handleUnload(UnloadFn fn)
11901237
return m_wallet->handleUnload(fn);
11911238
}
11921239

1240+
std::unique_ptr<interfaces::Handler> WalletQmlModel::handleCanGetAddressesChanged(CanGetAddressesChangedFn fn)
1241+
{
1242+
if (!m_wallet) {
1243+
return nullptr;
1244+
}
1245+
return m_wallet->handleCanGetAddressesChanged(fn);
1246+
}
1247+
11931248
bool WalletQmlModel::prepareTransaction()
11941249
{
11951250
return prepareTransactionInternal(std::nullopt);
@@ -1537,6 +1592,12 @@ void WalletQmlModel::subscribeToWalletSignals()
15371592
Q_EMIT walletUnloaded();
15381593
}, Qt::QueuedConnection);
15391594
});
1595+
m_handler_can_get_addresses_changed = handleCanGetAddressesChanged([this]() {
1596+
QMetaObject::invokeMethod(this, [this] {
1597+
Q_EMIT canGetAddressesChanged();
1598+
Q_EMIT addressListChanged();
1599+
}, Qt::QueuedConnection);
1600+
});
15401601
}
15411602

15421603
void WalletQmlModel::unsubscribeFromWalletSignals()
@@ -1553,6 +1614,9 @@ void WalletQmlModel::unsubscribeFromWalletSignals()
15531614
if (m_handler_unload) {
15541615
m_handler_unload->disconnect();
15551616
}
1617+
if (m_handler_can_get_addresses_changed) {
1618+
m_handler_can_get_addresses_changed->disconnect();
1619+
}
15561620
}
15571621

15581622
void WalletQmlModel::refreshSecurityState()

qml/models/walletqmlmodel.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class WalletQmlModel : public QObject
4343
Q_PROPERTY(QString balance READ balance NOTIFY balanceChanged)
4444
Q_PROPERTY(qint64 balanceSatoshi READ balanceSatoshi NOTIFY balanceChanged)
4545
Q_PROPERTY(bool hasExternalSigner READ hasExternalSigner CONSTANT)
46+
Q_PROPERTY(bool canGetAddresses READ canGetAddresses NOTIFY canGetAddressesChanged)
4647
Q_PROPERTY(ActivityListModel* activityListModel READ activityListModel CONSTANT)
4748
Q_PROPERTY(AddressListModel* addressListModel READ addressListModel CONSTANT)
4849
Q_PROPERTY(CoinsListModel* coinsListModel READ coinsListModel CONSTANT)
@@ -83,7 +84,9 @@ class WalletQmlModel : public QObject
8384
QString balance() const;
8485
qint64 balanceSatoshi() const;
8586
bool hasExternalSigner() const { return m_wallet && m_wallet->hasExternalSigner(); }
87+
bool canGetAddresses() const { return m_wallet && m_wallet->canGetAddresses(); }
8688
Q_INVOKABLE bool commitPaymentRequest();
89+
Q_INVOKABLE bool commitPaymentRequestWithPassphrase(const QString& passphrase);
8790
Q_INVOKABLE void reloadReceiveRequests();
8891
Q_INVOKABLE bool removeReceiveRequest(const QString& request_id);
8992
Q_INVOKABLE bool loadPaymentRequest(const QString& request_id);
@@ -143,6 +146,8 @@ class WalletQmlModel : public QObject
143146
virtual std::unique_ptr<interfaces::Handler> handleStatusChanged(StatusChangedFn fn);
144147
using UnloadFn = std::function<void()>;
145148
virtual std::unique_ptr<interfaces::Handler> handleUnload(UnloadFn fn);
149+
using CanGetAddressesChangedFn = std::function<void()>;
150+
virtual std::unique_ptr<interfaces::Handler> handleCanGetAddressesChanged(CanGetAddressesChangedFn fn);
146151

147152
bool canBumpTransaction(const uint256& txid) const;
148153

@@ -196,6 +201,7 @@ class WalletQmlModel : public QObject
196201
void walletUnloaded();
197202
void settingsErrorChanged();
198203
void addressListChanged();
204+
void canGetAddressesChanged();
199205

200206
private:
201207
void initializeFeeEstimator();
@@ -209,6 +215,7 @@ class WalletQmlModel : public QObject
209215
void subscribeToWalletSignals();
210216
void unsubscribeFromWalletSignals();
211217
void refreshSecurityState();
218+
bool commitPaymentRequestInternal(std::optional<SecureString> passphrase);
212219
bool prepareTransactionInternal(std::optional<SecureString> passphrase);
213220
bool sendTransactionInternal();
214221
bool unlockForAction(std::optional<SecureString>& passphrase, bool& relock);
@@ -251,6 +258,7 @@ class WalletQmlModel : public QObject
251258
std::unique_ptr<interfaces::Handler> m_handler_address_list_changed;
252259
std::unique_ptr<interfaces::Handler> m_handler_transaction_changed;
253260
std::unique_ptr<interfaces::Handler> m_handler_unload;
261+
std::unique_ptr<interfaces::Handler> m_handler_can_get_addresses_changed;
254262
int m_display_unit{0};
255263
};
256264

qml/pages/node/NodeSettings.qml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ PageStack {
5050
}
5151
}
5252

53+
function openAddressList() {
54+
if (!walletController.selectedWallet) {
55+
return
56+
}
57+
root.openWalletSettings()
58+
walletController.selectedWallet.addressListModel.refresh()
59+
root.push(addresses_page)
60+
}
61+
5362
Connections {
5463
target: typeof walletController !== "undefined" ? walletController : null
5564
function onOpenWalletSettingsRequested() {
@@ -323,8 +332,7 @@ PageStack {
323332
onPasswordRequested: root.push(wallet_password_page, { "updating": walletController.selectedWallet.isEncrypted })
324333
onSignVerifyMessageRequested: root.push(sign_verify_message_page)
325334
onAddressesRequested: {
326-
walletController.selectedWallet.addressListModel.refresh()
327-
root.push(addresses_page)
335+
root.openAddressList()
328336
}
329337
}
330338
}

qml/pages/wallet/DesktopWallets.qml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,10 @@ Page {
220220
}
221221
}
222222
RequestPayment {
223+
onAddressHistoryRequested: {
224+
settingsTabButton.checked = true
225+
nodeSettings.openAddressList()
226+
}
223227
}
224228
Item {
225229
id: blockClockTab

qml/pages/wallet/RequestPayment.qml

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@ Page {
2222
property bool hasAddress: root.request !== null && root.request.address !== ""
2323
property bool hasAddressType: receiveOptionsPopup.showAddressType && hasAddress && root.request !== null && root.request.addressType !== ""
2424
property bool showAddressTypeSelector: receiveOptionsPopup.showAddressType && root.request !== null && root.request.isEditing && !hasAddress
25-
property var availableAddressTypes: wallet ? wallet.availableReceiveAddressTypes() : []
25+
property bool canGetAddresses: wallet ? wallet.canGetAddresses : false
26+
property bool canUnlockForAddress: wallet ? wallet.canManagePassphrase && wallet.isEncrypted && wallet.isLocked : false
27+
property var availableAddressTypes: canGetAddresses && wallet ? wallet.availableReceiveAddressTypes() : []
2628
property bool hasSavedRequest: root.request !== null && root.request.id !== ""
29+
property string requestErrorText: ""
30+
31+
signal addressHistoryRequested()
2732

2833
Binding {
2934
target: root.request ? root.request.amount : null
@@ -67,6 +72,30 @@ Page {
6772
root.request.addressType = root.selectedReceiveAddressType()
6873
}
6974

75+
function paymentRequestFallbackError() {
76+
return root.wallet && root.wallet.transactionError.length > 0
77+
? root.wallet.transactionError
78+
: qsTr("The payment request could not be generated.")
79+
}
80+
81+
function handleCommitPaymentRequestFailure() {
82+
if (root.wallet && root.wallet.transactionNeedsUnlock) {
83+
receivePassphrasePopup.errorText = ""
84+
receivePassphrasePopup.open()
85+
return
86+
}
87+
root.requestErrorText = root.paymentRequestFallbackError()
88+
}
89+
90+
function commitPaymentRequest() {
91+
if (!root.wallet || !root.request) return
92+
root.requestErrorText = ""
93+
root.ensureAddressTypeSelected()
94+
if (!root.wallet.commitPaymentRequest()) {
95+
root.handleCommitPaymentRequestFailure()
96+
}
97+
}
98+
7099
function formatAddressRichText(addr) {
71100
if (!addr) return ""
72101
var c1 = Theme.color.neutral9
@@ -164,6 +193,7 @@ Page {
164193
showRequestActions: root.hasSavedRequest
165194
onUseAsTemplate: root.useCurrentRequestAsTemplate()
166195
onDeleteFromHistory: root.deleteCurrentRequest()
196+
onViewAddressHistory: root.addressHistoryRequested()
167197
}
168198
}
169199

@@ -635,11 +665,24 @@ Page {
635665
Layout.topMargin: 4
636666
}
637667

668+
CoreText {
669+
objectName: "requestPaymentErrorText"
670+
Layout.fillWidth: true
671+
Layout.topMargin: 12
672+
visible: root.requestErrorText.length > 0
673+
text: root.requestErrorText
674+
color: Theme.color.red
675+
font.pixelSize: 15
676+
horizontalAlignment: Text.AlignLeft
677+
wrapMode: Text.WordWrap
678+
}
679+
638680
ContinueButton {
639681
id: generateButton
640682
objectName: "requestPaymentGenerateButton"
641683
Layout.fillWidth: true
642684
Layout.topMargin: 30
685+
enabled: root.request !== null && (!root.request.isEditing || root.hasAddress || root.canGetAddresses || root.canUnlockForAddress)
643686
text: {
644687
if (!root.request || root.request.isEditing) {
645688
return root.request && root.request.id !== ""
@@ -651,9 +694,9 @@ Page {
651694
onClicked: {
652695
if (!root.request) return
653696
if (root.request.isEditing) {
654-
root.ensureAddressTypeSelected()
655-
root.wallet.commitPaymentRequest()
697+
root.commitPaymentRequest()
656698
} else {
699+
root.requestErrorText = ""
657700
root.request.clear()
658701
root.ensureAddressTypeSelected()
659702
}
@@ -832,6 +875,32 @@ Page {
832875
}
833876
}
834877

878+
Components.WalletPassphrasePopup {
879+
id: receivePassphrasePopup
880+
parent: Overlay.overlay
881+
width: Math.min(420, root.width - 40)
882+
popupObjectName: "receivePassphrasePopup"
883+
passphraseFieldObjectName: "receivePassphraseField"
884+
errorTextObjectName: "receivePassphraseErrorText"
885+
cancelButtonObjectName: "receivePassphraseCancelButton"
886+
confirmButtonObjectName: "receivePassphraseConfirmButton"
887+
titleText: qsTr("Enter wallet password")
888+
descriptionText: qsTr("Enter your wallet password to generate this payment request.")
889+
confirmText: qsTr("Unlock and generate")
890+
busyConfirmText: qsTr("Unlocking...")
891+
onSubmitted: (passphrase) => {
892+
receivePassphrasePopup.busy = true
893+
if (root.wallet && root.wallet.commitPaymentRequestWithPassphrase(passphrase)) {
894+
receivePassphrasePopup.busy = false
895+
receivePassphrasePopup.close()
896+
root.requestErrorText = ""
897+
return
898+
}
899+
receivePassphrasePopup.busy = false
900+
receivePassphrasePopup.errorText = root.paymentRequestFallbackError()
901+
}
902+
}
903+
835904
Components.QRCodePopup {
836905
id: qrPopup
837906
objectName: "requestPaymentQRPopup"

0 commit comments

Comments
 (0)