Skip to content

Commit 4b9128e

Browse files
committed
Issue 18: add PSBT operations page with import/sign/finalize/broadcast
1 parent b8bd645 commit 4b9128e

14 files changed

Lines changed: 1036 additions & 2 deletions

doc/test-automation-selectors.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ It supports the parity backlog Definition of Done (`DefinitionOfDone.md`) requir
2222
## Current Examples
2323

2424
- Receive flow: `receiveAmountInput`, `receiveLabelInput`, `receiveContactSelectButton`, `receiveContactsPopup`, `receiveContactsSearchInput`, `receiveContactRow`, `receiveContactSelectFirstActionButton`, `receiveContactUseButton`, `receiveCreateAddressButton`, `receiveCopySelectedUriButton`, `receiveQrImage`
25-
- Send flow: `sendAddressInput`, `sendOpenContactsButton`, `sendContactsPopup`, `sendContactsSearchInput`, `sendContactLabelInput`, `sendContactAddressInput`, `sendContactSaveButton`, `sendContactDeleteButton`, `sendContactUseButton`, `sendContinueButton`, `sendReviewCopyPsbtButton`, `sendReviewSavePsbtButton`, `sendReviewSavePsbtFileDialog`, `sendReviewPsbtStatusText`, `multipleSendReviewCopyPsbtButton`, `multipleSendReviewSavePsbtButton`, `multipleSendReviewSavePsbtFileDialog`, `multipleSendReviewPsbtStatusText`, `sendResultPopup`
25+
- Send flow: `sendAddressInput`, `sendOpenContactsButton`, `sendContactsPopup`, `sendContactsSearchInput`, `sendContactLabelInput`, `sendContactAddressInput`, `sendContactSaveButton`, `sendContactDeleteButton`, `sendContactUseButton`, `sendContinueButton`, `sendOptionsPsbtOperationsButton`, `sendReviewCopyPsbtButton`, `sendReviewSavePsbtButton`, `sendReviewSavePsbtFileDialog`, `sendReviewPsbtStatusText`, `multipleSendReviewCopyPsbtButton`, `multipleSendReviewSavePsbtButton`, `multipleSendReviewSavePsbtFileDialog`, `multipleSendReviewPsbtStatusText`, `sendResultPopup`
26+
- PSBT operations flow: `psbtOperationsPage`, `psbtOperationsBackButton`, `psbtImportClipboardButton`, `psbtImportFileButton`, `psbtImportFileDialog`, `psbtWorkflowStatusText`, `psbtInputSummaryText`, `psbtSignButton`, `psbtFinalizeButton`, `psbtBroadcastButton`, `psbtCopyButton`, `psbtSaveButton`, `psbtSaveFileDialog`, `psbtStatusText`, `psbtBroadcastTxidText`
2627
- Wallet tabs/pages: `walletSendPage`, `walletRequestPaymentPage`, `activityListView`, `activityOpenFirstRowActionButton`, `activityOpenRowAddressInput`, `activityOpenRowByAddressActionButton`, `activityDetailsPage`, `activityDetailsBackButton`
2728
- Activity RBF actions: `activityDetailsBumpButton`, `activityDetailsBumpPopup`, `activityDetailsBumpPreviewButton`, `activityDetailsBumpConfirmPopup`, `activityDetailsBumpConfirmButton`, `activityDetailsBumpDisabledReasonText`, `activityDetailsCancelButton`, `activityDetailsCancelPopup`, `activityDetailsCancelConfirmButton`, `activityDetailsRbfStatusText`, `activityDetailsReplacementTxidText`
2829
- Onboarding storage flow: `onboardingStorageAmountDetailedSettingsButton`, `storageSettingsPruneTargetInput`, `storageLocationDefaultOption`

qml/bitcoin_qml.qrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<file>controls/LabeledTextInput.qml</file>
4141
<file>controls/LabeledCoinControlButton.qml</file>
4242
<file>controls/EllipsisMenuToggleItem.qml</file>
43+
<file>controls/EllipsisMenuActionItem.qml</file>
4344
<file>controls/NavButton.qml</file>
4445
<file>controls/NavigationBar.qml</file>
4546
<file>controls/NavigationBar2.qml</file>
@@ -101,6 +102,7 @@
101102
<file>pages/wallet/CreateWalletWizard.qml</file>
102103
<file>pages/wallet/DesktopWallets.qml</file>
103104
<file>pages/wallet/MultipleSendReview.qml</file>
105+
<file>pages/wallet/PsbtOperations.qml</file>
104106
<file>pages/wallet/RequestPayment.qml</file>
105107
<file>pages/wallet/Send.qml</file>
106108
<file>pages/wallet/SendResult.qml</file>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) 2026 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+
import QtQuick 2.15
6+
import QtQuick.Controls 2.15
7+
import QtQuick.Layouts 1.15
8+
import org.bitcoincore.qt 1.0
9+
10+
Button {
11+
property int bgRadius: 5
12+
property color bgDefaultColor: "transparent"
13+
property color bgHoverColor: Theme.color.neutral2
14+
property color textColor: Theme.color.neutral7
15+
property color textHoverColor: Theme.color.neutral9
16+
17+
id: root
18+
hoverEnabled: AppMode.isDesktop
19+
20+
implicitWidth: 280
21+
22+
MouseArea {
23+
anchors.fill: parent
24+
enabled: false
25+
hoverEnabled: true
26+
cursorShape: Qt.PointingHandCursor
27+
}
28+
29+
contentItem: RowLayout {
30+
spacing: 7
31+
anchors.fill: parent
32+
anchors.centerIn: parent
33+
anchors.margins: 10
34+
35+
CoreText {
36+
id: buttonText
37+
Layout.fillWidth: true
38+
Layout.alignment: Qt.AlignVCenter
39+
horizontalAlignment: Text.AlignLeft
40+
font.pixelSize: 15
41+
text: root.text
42+
color: root.textColor
43+
}
44+
}
45+
46+
background: Rectangle {
47+
id: bg
48+
color: root.bgDefaultColor
49+
radius: root.bgRadius
50+
51+
Behavior on color {
52+
ColorAnimation { duration: 150 }
53+
}
54+
}
55+
56+
states: [
57+
State {
58+
name: "HOVER"; when: root.hovered
59+
PropertyChanges { target: bg; color: root.bgHoverColor }
60+
PropertyChanges { target: buttonText; color: root.textHoverColor }
61+
}
62+
]
63+
}

qml/controls/SendOptionsPopup.qml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ OptionPopup {
1313
id: root
1414
objectName: "sendOptionsPopup"
1515

16+
signal psbtOperationsRequested()
17+
1618
property alias coinControlEnabled: coinControlToggle.checked
1719
property alias multipleRecipientsEnabled: multipleRecipientsToggle.checked
1820
property alias customFeeEnabled: customFeeToggle.checked
1921

2022
implicitWidth: 300
21-
implicitHeight: 145
23+
implicitHeight: 190
2224

2325
clip: true
2426
modal: true
@@ -59,5 +61,19 @@ OptionPopup {
5961
Layout.fillWidth: true
6062
text: qsTr("Enable custom fee")
6163
}
64+
65+
Separator {
66+
Layout.fillWidth: true
67+
}
68+
69+
EllipsisMenuActionItem {
70+
objectName: "sendOptionsPsbtOperationsButton"
71+
Layout.fillWidth: true
72+
text: qsTr("PSBT operations")
73+
onClicked: {
74+
root.close()
75+
root.psbtOperationsRequested()
76+
}
77+
}
6278
}
6379
}

qml/models/nodemodel.cpp

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
#include <interfaces/node.h>
88
#include <net.h>
99
#include <node/interface_ui.h>
10+
#include <node/types.h>
11+
#include <qml/models/psbtoperationsadapter.h>
12+
#include <psbt.h>
1013
#include <validation.h>
14+
#include <common/messages.h>
1115

1216
#include <cassert>
1317
#include <chrono>
@@ -190,3 +194,46 @@ QString NodeModel::defaultProxyAddress()
190194
{
191195
return QString::fromStdString(std::string(DEFAULT_PROXY_HOST) + ":" + util::ToString(DEFAULT_PROXY_PORT));
192196
}
197+
198+
QVariantMap NodeModel::broadcastSignedPsbt(const QString& psbt_base64)
199+
{
200+
QVariantMap payload;
201+
202+
const PsbtDecodeResult decoded = PsbtOperationsAdapter::DecodeClipboardBase64(psbt_base64);
203+
if (!decoded.success || !decoded.psbt.has_value()) {
204+
payload[QStringLiteral("success")] = false;
205+
payload[QStringLiteral("message")] = decoded.message;
206+
return payload;
207+
}
208+
209+
PartiallySignedTransaction psbt = *decoded.psbt;
210+
CMutableTransaction mtx;
211+
if (!FinalizeAndExtractPSBT(psbt, mtx)) {
212+
payload[QStringLiteral("success")] = false;
213+
payload[QStringLiteral("message")] = tr("Unknown error processing transaction.");
214+
return payload;
215+
}
216+
217+
const CTransactionRef tx = MakeTransactionRef(mtx);
218+
std::string err_string;
219+
const node::TransactionError error = m_node.broadcastTransaction(
220+
tx,
221+
node::DEFAULT_MAX_RAW_TX_FEE_RATE.GetFeePerK(),
222+
err_string);
223+
224+
if (error == node::TransactionError::OK) {
225+
payload[QStringLiteral("success")] = true;
226+
payload[QStringLiteral("message")] = tr("Transaction broadcast successfully! Transaction ID: %1")
227+
.arg(QString::fromStdString(tx->GetHash().GetHex()));
228+
payload[QStringLiteral("txid")] = QString::fromStdString(tx->GetHash().GetHex());
229+
return payload;
230+
}
231+
232+
payload[QStringLiteral("success")] = false;
233+
payload[QStringLiteral("message")] = tr("Transaction broadcast failed: %1")
234+
.arg(QString::fromStdString(common::TransactionErrorString(error).translated));
235+
if (!err_string.empty()) {
236+
payload[QStringLiteral("details")] = QString::fromStdString(err_string);
237+
}
238+
return payload;
239+
}

qml/models/nodemodel.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
#include <QObject>
1515
#include <QString>
16+
#include <QVariantMap>
1617

1718
const char DEFAULT_PROXY_HOST[] = "127.0.0.1";
1819
constexpr uint16_t DEFAULT_PROXY_PORT = 9050;
@@ -67,6 +68,7 @@ class NodeModel : public QObject
6768

6869
Q_INVOKABLE bool validateProxyAddress(QString addr_port);
6970
Q_INVOKABLE QString defaultProxyAddress();
71+
Q_INVOKABLE QVariantMap broadcastSignedPsbt(const QString& psbt_base64);
7072

7173
public Q_SLOTS:
7274
void initializeResult(bool success, interfaces::BlockAndHeaderTipInfo tip_info);

0 commit comments

Comments
 (0)