Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/gui-functional-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,4 @@ jobs:
python3 test/functional/qml_test_peers.py
python3 test/functional/qml_test_proxy.py
python3 test/functional/qml_test_debug_log.py
python3 test/functional/qml_test_watchonly_wallet.py
4 changes: 4 additions & 0 deletions qml/bitcoin_qml.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
<file>controls/ToggleButton.qml</file>
<file>controls/utils.js</file>
<file>controls/ValueInput.qml</file>
<file>controls/WalletTypeListItem.qml</file>
<file>pages/initerrormessage.qml</file>
<file>pages/main.qml</file>
<file>pages/node/BannedPeers.qml</file>
Expand Down Expand Up @@ -105,7 +106,10 @@
<file>pages/wallet/CreatePassword.qml</file>
<file>pages/wallet/ImportWalletOptions.qml</file>
<file>pages/wallet/ImportWalletSuccess.qml</file>
<file>pages/wallet/CreateTypeSelector.qml</file>
<file>pages/wallet/CreateWalletWizard.qml</file>
<file>pages/wallet/WatchOnlyIntro.qml</file>
<file>pages/wallet/WatchOnlyXpub.qml</file>
<file>pages/wallet/DesktopWallets.qml</file>
<file>pages/wallet/MultipleSendReview.qml</file>
<file>pages/wallet/RequestPayment.qml</file>
Expand Down
63 changes: 63 additions & 0 deletions qml/controls/WalletTypeListItem.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) 2025 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

AbstractButton {
id: root

property string title: ""
property string description: ""
property string iconSource: ""
property color iconColor: Theme.color.neutral9

padding: 15
implicitWidth: 450
opacity: enabled ? 1.0 : 0.4

background: Rectangle {
border.width: 1
border.color: root.hovered && root.enabled ? Theme.color.neutral9 : Theme.color.neutral5
radius: 10
color: "transparent"
FocusBorder {
visible: root.visualFocus
borderRadius: 14
}
}

contentItem: RowLayout {
spacing: 10
Icon {
source: root.iconSource
color: root.iconColor
size: 24
}
ColumnLayout {
spacing: 2
Layout.fillWidth: true
CoreText {
Layout.fillWidth: true
text: root.title
font.pixelSize: 18
bold: true
color: Theme.color.neutral9
horizontalAlignment: Text.AlignLeft
}
CoreText {
Layout.fillWidth: true
text: root.description
font.pixelSize: 15
color: Theme.color.neutral7
horizontalAlignment: Text.AlignLeft
wrapMode: Text.WordWrap
}
}
CaretRightIcon {
Layout.alignment: Qt.AlignVCenter
}
}
}
10 changes: 7 additions & 3 deletions qml/pages/wallet/CreateName.qml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Page {
property int walletType: CreateName.WalletType.SingleSig
property string initialWalletName: ""
readonly property bool externalSignerWallet: walletType === CreateName.WalletType.ExternalSigner
property bool loading: false
background: null

Component.onCompleted: {
Expand Down Expand Up @@ -86,10 +87,13 @@ Page {
Layout.leftMargin: 20
Layout.rightMargin: 20
Layout.alignment: Qt.AlignCenter
enabled: walletNameInput.text.length > 0
text: root.externalSignerWallet ? qsTr("Create wallet") : qsTr("Continue")
enabled: walletNameInput.text.length > 0 && !root.loading
text: {
if (root.loading) return qsTr("Initializing...")
if (root.externalSignerWallet) return qsTr("Create wallet")
return qsTr("Continue")
}
onClicked: {
console.log("Creating wallet with name: " + walletNameInput.text)
root.walletName = walletNameInput.text
root.next()
}
Expand Down
2 changes: 1 addition & 1 deletion qml/pages/wallet/CreatePassword.qml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Page {
signal next
background: null

required property string walletName;
required property string walletName

Component.onCompleted: walletController.clearWalletLoadStatus()

Expand Down
108 changes: 108 additions & 0 deletions qml/pages/wallet/CreateTypeSelector.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (c) 2025 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

import "../../controls"
import "../../components"

Page {
id: root
objectName: "createTypeSelector"
signal back
signal regularSelected
signal watchOnlySelected
signal importSelected
background: null

header: NavigationBar2 {
leftItem: NavButton {
objectName: "typeSelectorBackButton"
iconSource: "image://images/caret-left"
text: qsTr("Back")
onClicked: root.back()
}
centerItem: Item {
CoreText {
anchors.centerIn: parent
text: qsTr("Choose a wallet type")
font.pixelSize: 18
bold: true
color: Theme.color.neutral9
}
}
}

Flickable {
anchors.fill: parent
contentHeight: columnLayout.height
clip: true

ColumnLayout {
id: columnLayout
width: Math.min(parent.width, 450)
anchors.horizontalCenter: parent.horizontalCenter
spacing: 10

Item { Layout.preferredHeight: 10 }

WalletTypeListItem {
objectName: "walletTypeRegular"
Layout.fillWidth: true
Layout.leftMargin: 20
Layout.rightMargin: 20
title: qsTr("Regular")
description: qsTr("Fully managed in this application.")
iconSource: "image://images/singlesig-wallet"
onClicked: root.regularSelected()
}

WalletTypeListItem {
objectName: "walletTypeMultiKey"
Layout.fillWidth: true
Layout.leftMargin: 20
Layout.rightMargin: 20
title: qsTr("Multi-key")
description: qsTr("Requires 2 or more wallets or hardware devices to coordinate.")
iconSource: "image://images/singlesig-wallet"
enabled: false
}

WalletTypeListItem {
objectName: "walletTypeViewOnly"
Layout.fillWidth: true
Layout.leftMargin: 20
Layout.rightMargin: 20
title: qsTr("Watch-only")
description: qsTr("Keep an eye on another wallet you have.")
iconSource: "image://images/visible"
onClicked: root.watchOnlySelected()
}

WalletTypeListItem {
objectName: "walletTypeImport"
Layout.fillWidth: true
Layout.leftMargin: 20
Layout.rightMargin: 20
title: qsTr("Import wallet")
description: qsTr("Load an existing wallet.dat file.")
iconSource: "image://images/file"
onClicked: root.importSelected()
}

WalletTypeListItem {
objectName: "walletTypeCustom"
Layout.fillWidth: true
Layout.leftMargin: 20
Layout.rightMargin: 20
title: qsTr("Custom")
description: qsTr("For unique wallet configurations.")
iconSource: "image://images/gear-outline"
enabled: false
}
}
}
}
90 changes: 85 additions & 5 deletions qml/pages/wallet/CreateWalletWizard.qml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,31 @@ PageStack {

signal finished()
property string walletName: ""
property string walletType: ""
property string xpub: ""
property int launchContext: CreateWalletWizard.Context.Onboarding
property bool waitingForInit: false

Component.onCompleted: walletController.refreshExternalSignerStatus()
Component.onCompleted: {
if (!walletController.initialized) {
nodeModel.startNodeInitializionThread()
}
walletController.refreshExternalSignerStatus()
}

Connections {
target: walletController
enabled: root.waitingForInit
function onInitializedChanged() {
if (walletController.initialized) {
root.waitingForInit = false
walletController.createWatchOnlyWallet(root.walletName, root.xpub)
if (walletController.isWalletLoaded) {
root.push(watchOnlyConfirm)
}
}
}
}
onVisibleChanged: {
if (visible) {
walletController.refreshExternalSignerStatus()
Expand Down Expand Up @@ -76,8 +98,8 @@ PageStack {
header: qsTr("Add a wallet")
headerBold: true
description: walletController.canCreateExternalSignerWallet
? qsTr("Supported wallet types are external signer, view-only, single-key,\nand multi-key.")
: qsTr("Supported wallet types are view-only, single-key,\nand multi-key.")
? qsTr("Supported wallet types are external signer, watch-only, single-key,\nand multi-key.")
: qsTr("Supported wallet types are watch-only, single-key,\nand multi-key.")
}

ContinueButton {
Expand All @@ -90,7 +112,7 @@ PageStack {
Layout.alignment: Qt.AlignCenter
text: qsTr("Create wallet")
onClicked: {
root.push(intro)
root.push(typeSelector)
}
}

Expand Down Expand Up @@ -157,6 +179,51 @@ PageStack {
}
}
}
Component {
id: typeSelector
CreateTypeSelector {
onBack: root.pop()
onRegularSelected: {
root.walletType = "singlesig"
root.push(intro)
}
onWatchOnlySelected: {
root.walletType = "watchonly"
root.push(watchOnlyIntro)
}
onImportSelected: {
walletController.clearWalletLoadStatus()
root.push(import_options)
}
}
}
Component {
id: watchOnlyIntro
WatchOnlyIntro {
onBack: root.pop()
onNext: root.push(watchOnlyXpub)
}
}
Component {
id: watchOnlyXpub
WatchOnlyXpub {
id: xpubPage
onBack: root.pop()
onNext: {
root.xpub = xpubPage.xpub
root.push(name)
}
}
}
Component {
id: watchOnlyConfirm
CreateConfirm {
headerText: qsTr("Your watch-only wallet has been created")
descriptionText: qsTr("You can view transactions and balances. Spending requires an external signer. No backup is needed — your wallet can be recreated from the same extended public key.")
onBack: root.pop()
onNext: root.finished()
}
}
Component {
id: import_options
ImportWalletOptions {
Expand Down Expand Up @@ -185,9 +252,22 @@ PageStack {
id: name
CreateName {
id: createName
loading: root.waitingForInit
onBack: root.pop()
onNext: {
root.walletName = createName.walletName
root.push(password)
if (root.walletType === "watchonly") {
if (walletController.initialized) {
walletController.createWatchOnlyWallet(root.walletName, root.xpub)
if (walletController.isWalletLoaded) {
root.push(watchOnlyConfirm)
}
} else {
root.waitingForInit = true
}
} else {
root.push(password)
}
}
}
}
Expand Down
Loading
Loading