Files
purple-electrumwallet/electrum/gui/qml/components/WalletMainView.qml
T
f321x 70084750ef qml: show wallet list as root if no wallet is loaded
Shows Wallets.qml as root if no wallet is loaded and removes the logic
for no loaded wallet from the WalletMainView as WalletMainView won't be
shown anymore without a Daemon.currentWallet.
2025-12-18 17:54:28 +01:00

780 lines
26 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Material
import QtQml
import org.electrum 1.0
import "controls"
Item {
id: mainView
property string title: Daemon.currentWallet.name
property var _sendDialog
property string _request_amount
property string _request_description
property string _request_expiry
function openInvoice(key) {
invoice.key = key
var dialog = invoiceDialog.createObject(app, { invoice: invoice })
dialog.open()
return dialog
}
function openRequest(key) {
var dialog = receiveDialog.createObject(app, { key: key })
dialog.open()
return dialog
}
function openSendDialog() {
// Qt based send dialog if not on android
if (!AppController.isAndroid()) {
_sendDialog = qtSendDialog.createObject(mainView, {invoiceParser: invoiceParser, piResolver: piResolver})
_sendDialog.open()
return
}
// Android based send dialog if on android
var scanner = app.scanDialog.createObject(mainView, {
hint: Daemon.currentWallet.isLightning
? qsTr('Scan an Invoice, an Address, an LNURL, a PSBT or a Channel Backup')
: qsTr('Scan an Invoice, an Address, an LNURL or a PSBT')
})
scanner.onFoundText.connect(function(data) {
data = data.trim()
if (bitcoin.isRawTx(data)) {
app.stack.push(Qt.resolvedUrl('TxDetails.qml'), { rawtx: data })
} else if (Daemon.currentWallet.isValidChannelBackup(data)) {
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Import Channel Backup?'),
yesno: true
})
dialog.accepted.connect(function() {
Daemon.currentWallet.importChannelBackup(data)
})
dialog.open()
} else {
piResolver.recipient = data
}
//scanner.destroy() // TODO
})
scanner.open()
}
function closeSendDialog() {
if (!AppController.isAndroid()) {
if (_sendDialog) {
_sendDialog.doClose()
_sendDialog = null
}
}
}
function restartSendDialog() {
if (!AppController.isAndroid()) {
if (_sendDialog) {
_sendDialog.restart()
}
return
} else {
openSendDialog()
}
}
function showExport(data, helptext) {
var dialog = exportTxDialog.createObject(app, {
text: data[0],
text_qr: data[1],
text_help: helptext,
text_warn: data[2]
? ''
: [qsTr('Warning: Some data (prev txs / "full utxos") was left out of the QR code as it would not fit.'),
qsTr('This might cause issues if signing offline.'),
qsTr('As a workaround, copy to clipboard or use the Share option instead.')].join(' '),
tx_label: data[3]
})
dialog.open()
}
function payOnchain(invoicedialog, invoice) {
var dialog = confirmPaymentDialog.createObject(mainView, {
address: invoice.address,
satoshis: invoice.amountOverride.isEmpty
? invoice.amount
: invoice.amountOverride,
message: invoice.message
})
var canComplete = !Daemon.currentWallet.isWatchOnly && Daemon.currentWallet.canSignWithoutCosigner
dialog.accepted.connect(function() {
if (invoice.canSave)
if (!invoice.saveInvoice())
return
if (!canComplete) {
if (Daemon.currentWallet.isWatchOnly) {
dialog.finalizer.saveOrShow()
} else {
dialog.finalizer.sign()
}
} else {
// store txid in invoicedialog so the dialog can detect broadcast success
invoicedialog.broadcastTxid = dialog.finalizer.finalizedTxid
dialog.finalizer.signAndSend()
}
})
dialog.open()
}
function createRequest(lightning, reuse_address) {
var qamt = Config.unitsToSats(_request_amount)
Daemon.currentWallet.createRequest(qamt, _request_description, _request_expiry, lightning, reuse_address)
}
function startSweep() {
var dialog = sweepDialog.createObject(app)
dialog.accepted.connect(function() {
var finalizerDialog = confirmSweepDialog.createObject(mainView, {
privateKeys: dialog.privateKeys,
message: qsTr('Sweep transaction'),
showOptions: false,
amountLabelText: qsTr('Total sweep amount'),
sendButtonText: Daemon.currentWallet.isWatchOnly
? qsTr('Sweep...')
: qsTr('Sweep')
})
finalizerDialog.accepted.connect(function() {
if (Daemon.currentWallet.isWatchOnly) {
var confirmdialog = app.messageDialog.createObject(mainView, {
title: qsTr('Confirm Sweep'),
text: qsTr('Current wallet is watch-only. You might not be able to spend from these addresses.\n\nAre you sure?'),
yesno: true
})
confirmdialog.accepted.connect(function() {
finalizerDialog.finalizer.send()
close()
})
confirmdialog.open()
return
}
console.log("Sending sweep transaction")
finalizerDialog.finalizer.send()
})
finalizerDialog.open()
})
dialog.open()
}
property QtObject menu: Menu {
id: menu
parent: Overlay.overlay
dim: true
modal: true
Overlay.modal: Rectangle {
color: "#44000000"
}
property int implicitChildrenWidth: 64
width: implicitChildrenWidth + 60 + constants.paddingLarge
MenuItem {
icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor
icon.source: '../../icons/wallet.png'
action: Action {
text: qsTr('Wallet details')
enabled: app.stack.currentItem.objectName != 'WalletDetails'
onTriggered: menu.openPage(Qt.resolvedUrl('WalletDetails.qml'))
}
}
MenuItem {
icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor
icon.source: '../../icons/tab_addresses.png'
action: Action {
text: qsTr('Addresses/Coins');
onTriggered: menu.openPage(Qt.resolvedUrl('Addresses.qml'));
enabled: app.stack.currentItem.objectName != 'Addresses'
}
}
MenuItem {
icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor
icon.source: '../../icons/lightning.png'
action: Action {
text: qsTr('Channels');
enabled: Daemon.currentWallet.isLightning && app.stack.currentItem.objectName != 'Channels'
onTriggered: menu.openPage(Qt.resolvedUrl('Channels.qml'))
}
}
MenuItem {
icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor
icon.source: '../../icons/pen.png'
action: Action {
text: Daemon.currentWallet.canSignMessage
? qsTr('Sign/Verify Message')
: qsTr('Verify Message')
onTriggered: {
var dialog = app.signVerifyMessageDialog.createObject(app)
dialog.open()
menu.deselect()
}
}
}
MenuItem {
icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor
icon.source: '../../icons/sweep.png'
action: Action {
text: qsTr('Sweep key(s)')
onTriggered: {
startSweep()
menu.deselect()
}
}
}
MenuSeparator { }
MenuItem {
icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor
icon.source: '../../icons/file.png'
action: Action {
text: qsTr('Other wallets')
enabled: app.stack.currentItem.objectName != 'Wallets'
onTriggered: menu.openPage(Qt.resolvedUrl('Wallets.qml'))
}
}
function openPage(url) {
stack.pushOnRoot(url)
deselect()
}
function deselect() {
currentIndex = -1
}
// determine widest element and store in implicitChildrenWidth
function updateImplicitWidth() {
for (let i = 0; i < menu.count; i++) {
var item = menu.itemAt(i)
var txt = item.text
var txtwidth = fontMetrics.advanceWidth(txt)
if (txtwidth > menu.implicitChildrenWidth) {
menu.implicitChildrenWidth = txtwidth
}
}
}
FontMetrics {
id: fontMetrics
font: menu.font
}
Component.onCompleted: updateImplicitWidth()
}
ColumnLayout {
anchors.fill: parent
spacing: 0
History {
id: history
Layout.fillWidth: true
Layout.fillHeight: true
}
ButtonContainer {
id: buttonContainer
Layout.fillWidth: true
FlatButton {
id: receiveButton
Layout.fillWidth: true
Layout.preferredWidth: 1
icon.source: '../../icons/tab_receive.png'
text: qsTr('Receive')
onClicked: {
var dialog = receiveDetailsDialog.createObject(mainView)
dialog.open()
}
onPressAndHold: {
Config.userKnowsPressAndHold = true
Daemon.currentWallet.deleteExpiredRequests()
app.stack.push(Qt.resolvedUrl('ReceiveRequests.qml'))
AppController.haptic()
}
}
FlatButton {
Layout.fillWidth: true
Layout.preferredWidth: 1
icon.source: '../../icons/tab_send.png'
text: qsTr('Send')
enabled: !invoiceParser.busy && !piResolver.busy && !requestDetails.busy
onClicked: openSendDialog()
onPressAndHold: {
Config.userKnowsPressAndHold = true
app.stack.push(Qt.resolvedUrl('Invoices.qml'))
AppController.haptic()
}
}
}
}
PIResolver {
id: piResolver
wallet: Daemon.currentWallet
onResolveError: (code, message) => {
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Error'),
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
text: message
})
dialog.open()
}
onInvoiceResolved: (pi) => {
invoiceParser.fromResolvedPaymentIdentifier(pi)
}
onRequestResolved: (pi) => {
requestDetails.fromResolvedPaymentIdentifier(pi)
}
}
RequestDetails {
id: requestDetails
wallet: Daemon.currentWallet
onNeedsLNURLUserInput: {
closeSendDialog()
var dialog = lnurlWithdrawDialog.createObject(app, {
requestDetails: requestDetails
})
dialog.open()
}
onLnurlError: (code, message) => {
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Error'),
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
text: message
})
dialog.open()
}
}
Invoice {
id: invoice
wallet: Daemon.currentWallet
}
InvoiceParser {
id: invoiceParser
wallet: Daemon.currentWallet
onValidationError: (code, message) => {
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Error'),
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
text: message
})
dialog.closed.connect(function() {
restartSendDialog()
})
dialog.open()
}
onValidationWarning: (code, message) => {
if (code == 'no_channels') {
var dialog = app.messageDialog.createObject(app, {
text: message
})
dialog.closed.connect(function() {
restartSendDialog()
})
dialog.open()
// TODO: ask user to open a channel, if funds allow
// and maybe store invoice if expiry allows
}
}
onValidationSuccess: {
closeSendDialog()
var dialog = invoiceDialog.createObject(app, {
invoice: invoiceParser,
payImmediately: invoiceParser.isLnurlPay
})
dialog.open()
}
onInvoiceCreateError: (code, message) => {
var msg = qsTr('Cannot save invoice') + ': ' + message
var dialog = app.messageDialog.createObject(app, {
text: msg
})
dialog.open()
}
onLnurlRetrieved: {
closeSendDialog()
if (invoiceParser.invoiceType === Invoice.Type.LNURLPayRequest) {
var dialog = lnurlPayDialog.createObject(app, {
invoiceParser: invoiceParser
})
} else {
console.log("Unsupported LNURL type:", invoiceParser.invoiceType)
return
}
dialog.open()
}
onLnurlError: (code, message) => {
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Error'),
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
text: message
})
dialog.open()
}
}
Bitcoin {
id: bitcoin
}
Connections {
target: Daemon
function onWalletLoaded() {
if (!Daemon.currentWallet) { // wallet got deleted
app.stack.replaceRoot('Wallets.qml')
return
}
infobanner.hide() // start hidden when switching wallets
}
}
Connections {
target: app
function onPendingIntentChanged() {
if (app.pendingIntent) {
piResolver.recipient = app.pendingIntent
app.pendingIntent = ""
}
}
}
Connections {
target: Daemon.currentWallet
function onRequestCreateSuccess(key) {
openRequest(key)
}
function onRequestCreateError(error) {
console.log(error)
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Error'),
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
text: error
})
dialog.open()
}
function onOtpRequested() {
console.log('OTP requested')
var dialog = otpDialog.createObject(mainView)
dialog.open()
}
function onBroadcastFailed(txid, code, message) {
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Error'),
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
text: message
})
dialog.open()
}
function onPaymentFailed(invoice_id, message) {
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Error'),
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
text: message ? message : qsTr('Payment failed')
})
dialog.open()
}
function onImportChannelBackupFailed(message) {
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Error'),
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
text: message
})
dialog.open()
}
function onBalanceChanged() {
// ln low reserve warning
if (Daemon.currentWallet.isLowReserve) {
var message = [
qsTr('You do not have enough on-chain funds to protect your Lightning channels.'),
qsTr('You should have at least %1 on-chain in order to be able to sweep channel outputs.').arg(Config.formatSats(Config.lnUtxoReserve) + ' ' + Config.baseUnit)
].join(' ')
infobanner.show(message, function() {
var dialog = app.messageDialog.createObject(app, {
text: message + '\n\n' + qsTr('Do you want to perform a swap?'),
yesno: true
})
dialog.accepted.connect(function() {
app.startSwap()
})
dialog.open()
})
} else {
infobanner.hide()
}
}
}
Component {
id: invoiceDialog
InvoiceDialog {
id: _invoiceDialog
width: parent.width
height: parent.height
onDoPay: {
var lninvoiceButPayOnchain = false
if (invoice.invoiceType == Invoice.LightningInvoice && invoice.address) {
// ln invoice with fallback
var amountToSend = invoice.amountOverride.isEmpty
? invoice.amount.satsInt
: invoice.amountOverride.satsInt
if (amountToSend > Daemon.currentWallet.lightningCanSend.satsInt) {
lninvoiceButPayOnchain = true
}
}
if (invoice.invoiceType == Invoice.OnchainInvoice) {
payOnchain(_invoiceDialog, invoice)
} else if (invoice.invoiceType == Invoice.LightningInvoice) {
if (lninvoiceButPayOnchain) {
var dialog = app.messageDialog.createObject(mainView, {
title: qsTr('Insufficient balance to pay over Lightning. Pay on-chain instead?'),
yesno: true
})
dialog.accepted.connect(function() {
payOnchain(_invoiceDialog, invoice)
})
dialog.open()
} else {
console.log('About to pay lightning invoice')
invoice.payLightningInvoice()
}
}
}
onClosed: destroy()
Connections {
target: Daemon.currentWallet
function onSaveTxSuccess(txid) {
_invoiceDialog.close()
}
}
}
}
Component {
id: qtSendDialog
SendDialog {
width: parent.width
height: parent.height
onTxFound: (data) => {
app.stack.push(Qt.resolvedUrl('TxDetails.qml'), { rawtx: data })
close()
}
onChannelBackupFound: (data) => {
if (!Daemon.currentWallet.isLightning) {
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Cannot import Channel Backup, Lightning not enabled.')
})
dialog.open()
return
}
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Import Channel Backup?'),
yesno: true
})
dialog.accepted.connect(function() {
Daemon.currentWallet.importChannelBackup(data)
close()
})
dialog.rejected.connect(function() {
close()
})
dialog.open()
}
onClosed: destroy()
}
}
Component {
id: receiveDetailsDialog
ReceiveDetailsDialog {
id: _receiveDetailsDialog
width: parent.width * 0.9
anchors.centerIn: parent
onAccepted: {
console.log('accepted')
_request_amount = _receiveDetailsDialog.amount
_request_description = _receiveDetailsDialog.description
_request_expiry = _receiveDetailsDialog.expiry
createRequest(_receiveDetailsDialog.isLightning, false)
}
onRejected: {
console.log('rejected')
}
onClosed: destroy()
}
}
Component {
id: receiveDialog
ReceiveDialog {
width: parent.width
height: parent.height
onRequestPaid: {
close()
var capturedHistoryModel = Daemon.currentWallet.historyModel
if (isLightning) {
var page = app.stack.push(Qt.resolvedUrl('LightningPaymentDetails.qml'), {'key': key})
var capturedKey = key
page.detailsChanged.connect(function() {
capturedHistoryModel.updateTxLabel(capturedKey, page.label)
}
)
} else {
let paidTxid = getPaidTxid()
var page = app.stack.push(Qt.resolvedUrl('TxDetails.qml'), {'txid': paidTxid})
page.detailsChanged.connect(function() {
capturedHistoryModel.updateTxLabel(paidTxid, page.label)
}
)
}
}
onClosed: destroy()
}
}
Component {
id: confirmPaymentDialog
ConfirmTxDialog {
id: _confirmPaymentDialog
title: qsTr('Confirm Payment')
finalizer: TxFinalizer {
wallet: Daemon.currentWallet
canRbf: true
onFinished: (signed, saved, complete) => {
if (!complete) {
var msg
if (wallet.isWatchOnly) {
// tx created in watchonly wallet. Show QR for signer(s)
if (wallet.isMultisig) {
msg = qsTr('Transaction created. Present this QR code to one of the co-cigners or signing devices')
} else {
msg = qsTr('Transaction created. Present this QR code to the signing device')
}
} else {
if (signed) {
msg = qsTr('Transaction created and partially signed by this wallet. Present this QR code to the next co-signer')
} else {
msg = qsTr('Transaction created but not signed by this wallet yet. Sign the transaction and present this QR code to the next co-signer')
}
}
showExport(getSerializedTx(), msg)
}
_confirmPaymentDialog.destroy()
}
onSignError: (message) => {
var dialog = app.messageDialog.createObject(mainView, {
title: qsTr('Error'),
text: [qsTr('Could not sign tx'), message].join('\n\n'),
iconSource: '../../../icons/warning.png'
})
dialog.open()
}
}
// TODO: lingering confirmPaymentDialogs can raise exceptions in
// the child finalizer when currentWallet disappears, but we need
// it long enough for the finalizer to finish..
// onClosed: destroy()
}
}
Component {
id: confirmSweepDialog
ConfirmTxDialog {
id: _confirmSweepDialog
property string privateKeys
title: qsTr('Confirm Sweep')
satoshis: MAX
finalizer: SweepFinalizer {
wallet: Daemon.currentWallet
canRbf: true
privateKeys: _confirmSweepDialog.privateKeys
}
}
}
Component {
id: lnurlPayDialog
LnurlPayRequestDialog {
width: parent.width * 0.9
anchors.centerIn: parent
onClosed: destroy()
}
}
Component {
id: lnurlWithdrawDialog
LnurlWithdrawRequestDialog {
width: parent.width * 0.9
anchors.centerIn: parent
onClosed: destroy()
}
}
Component {
id: otpDialog
OtpDialog {
width: parent.width * 2/3
anchors.centerIn: parent
onClosed: destroy()
}
}
Component {
id: exportTxDialog
ExportTxDialog {
onClosed: destroy()
}
}
Component {
id: sweepDialog
SweepDialog {
onClosed: destroy()
}
}
Component.onCompleted: {
console.log("WalletMainView completed: ", Daemon.currentWallet.name)
if (app.pendingIntent) {
piResolver.recipient = app.pendingIntent
app.pendingIntent = ""
}
}
}