Files
pallectrum/electrum/gui/qml/components/WalletMainView.qml
Sander van Grieken 6c65161d27 qml: refactor qeinvoice.py
QEInvoice/QEInvoiceParser now properly split for mapping to Invoice type (QEInvoice)
and parsing/resolving of payment identifiers (QEInvoiceParser).
additionally, old, unused QEUserEnteredPayment was removed.

invoices are now never saved with user-entered amount if the original invoice
did not specify an amount (e.g. address-only, no-amount bip21 uri, or no-amount
lightning invoice). Furthermore, QEInvoice now adds an isSaved property so the
UI doesn't need to infer that from the existence of the invoice key.

Payments of lightning invoices are now triggered through QEInvoice.pay_lightning_invoice(),
using the internally kept Invoice instance. This replaces the old call path of
QEWallet.pay_lightning_invoice(invoice_key) which required the invoice to be saved
in the backend wallet before payment.

The LNURLpay flow arriving on InvoiceDialog implicitly triggered payment, this is
now indicated by InvoiceDialog.payImmediately property instead of inferrred from the
QEInvoiceParser isLnurlPay property.
2023-04-04 16:13:00 +02:00

486 lines
15 KiB
QML

import QtQuick 2.6
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.0
import QtQuick.Controls.Material 2.0
import QtQml 2.6
import org.electrum 1.0
import "controls"
Item {
id: mainView
property string title: Daemon.currentWallet ? Daemon.currentWallet.name : qsTr('no wallet loaded')
property var _sendDialog
property string _intentUri
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() {
_sendDialog = sendDialog.createObject(mainView, {invoiceParser: invoiceParser})
_sendDialog.open()
}
function closeSendDialog() {
if (_sendDialog) {
_sendDialog.close()
_sendDialog = null
}
}
function restartSendDialog() {
if (_sendDialog) {
_sendDialog.restart()
}
}
function showExportByTxid(txid, helptext) {
showExport(Daemon.currentWallet.getSerializedTx(txid), helptext)
}
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. This might cause issues if signing offline. As a workaround, try exporting the tx as file or text instead.')
})
dialog.open()
}
property QtObject menu: Menu {
parent: Overlay.overlay
dim: true
modal: true
Overlay.modal: Rectangle {
color: "#44000000"
}
id: menu
MenuItem {
icon.color: 'transparent'
action: Action {
text: qsTr('Wallet details')
enabled: Daemon.currentWallet
onTriggered: menu.openPage(Qt.resolvedUrl('WalletDetails.qml'))
icon.source: '../../icons/wallet.png'
}
}
MenuItem {
icon.color: 'transparent'
action: Action {
text: qsTr('Addresses');
onTriggered: menu.openPage(Qt.resolvedUrl('Addresses.qml'));
enabled: Daemon.currentWallet
icon.source: '../../icons/tab_addresses.png'
}
}
MenuItem {
icon.color: 'transparent'
action: Action {
text: qsTr('Channels');
enabled: Daemon.currentWallet && Daemon.currentWallet.isLightning
onTriggered: menu.openPage(Qt.resolvedUrl('Channels.qml'))
icon.source: '../../icons/lightning.png'
}
}
MenuSeparator { }
MenuItem {
icon.color: 'transparent'
action: Action {
text: qsTr('Other wallets');
onTriggered: menu.openPage(Qt.resolvedUrl('Wallets.qml'))
icon.source: '../../icons/file.png'
}
}
function openPage(url) {
stack.pushOnRoot(url)
currentIndex = -1
}
}
ColumnLayout {
anchors.fill: parent
spacing: 0
History {
id: history
visible: Daemon.currentWallet
Layout.fillWidth: true
Layout.fillHeight: true
}
ColumnLayout {
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true
spacing: 2*constants.paddingXLarge
visible: !Daemon.currentWallet
Item {
Layout.fillHeight: true
}
Label {
Layout.alignment: Qt.AlignHCenter
text: qsTr('No wallet loaded')
font.pixelSize: constants.fontSizeXXLarge
}
Pane {
Layout.alignment: Qt.AlignHCenter
padding: 0
background: Rectangle {
color: Material.dialogColor
}
FlatButton {
text: qsTr('Open/Create Wallet')
icon.source: '../../icons/wallet.png'
onClicked: {
if (Daemon.availableWallets.rowCount() > 0) {
stack.push(Qt.resolvedUrl('Wallets.qml'))
} else {
var newww = app.newWalletWizard.createObject(app)
newww.walletCreated.connect(function() {
Daemon.availableWallets.reload()
// and load the new wallet
Daemon.load_wallet(newww.path, newww.wizard_data['password'])
})
newww.open()
}
}
}
}
Item {
Layout.fillHeight: true
}
}
ButtonContainer {
id: buttonContainer
Layout.fillWidth: true
FlatButton {
id: receiveButton
visible: Daemon.currentWallet
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.delete_expired_requests()
app.stack.push(Qt.resolvedUrl('ReceiveRequests.qml'))
AppController.haptic()
}
}
FlatButton {
visible: Daemon.currentWallet
Layout.fillWidth: true
Layout.preferredWidth: 1
icon.source: '../../icons/tab_send.png'
text: qsTr('Send')
onClicked: openSendDialog()
onPressAndHold: {
Config.userKnowsPressAndHold = true
app.stack.push(Qt.resolvedUrl('Invoices.qml'))
AppController.haptic()
}
}
}
}
Invoice {
id: invoice
wallet: Daemon.currentWallet
}
InvoiceParser {
id: invoiceParser
wallet: Daemon.currentWallet
onValidationError: {
var dialog = app.messageDialog.createObject(app, { text: message })
dialog.closed.connect(function() {
restartSendDialog()
})
dialog.open()
}
onValidationWarning: {
if (code == 'no_channels') {
var dialog = app.messageDialog.createObject(app, { text: message })
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: console.log(code + ' ' + message)
onLnurlRetrieved: {
closeSendDialog()
var dialog = lnurlPayDialog.createObject(app, { invoiceParser: invoiceParser })
dialog.open()
}
onLnurlError: {
var dialog = app.messageDialog.createObject(app, { text: message })
dialog.open()
}
}
Connections {
target: AppController
function onUriReceived(uri) {
console.log('uri received: ' + uri)
if (!Daemon.currentWallet) {
console.log('No wallet open, deferring')
_intentUri = uri
return
}
invoiceParser.recipient = uri
}
}
Connections {
target: Daemon
function onWalletLoaded() {
if (_intentUri) {
invoiceParser.recipient = _intentUri
_intentUri = ''
}
}
}
Connections {
target: Daemon.currentWallet
function onRequestCreateSuccess(key) {
openRequest(key)
}
function onRequestCreateError(error) {
console.log(error)
var dialog = app.messageDialog.createObject(app, {text: error})
dialog.open()
}
function onOtpRequested() {
console.log('OTP requested')
var dialog = otpDialog.createObject(mainView)
dialog.accepted.connect(function() {
console.log('accepted ' + dialog.otpauth)
Daemon.currentWallet.finish_otp(dialog.otpauth)
})
dialog.open()
}
function onBroadcastFailed(txid, code, message) {
var dialog = app.messageDialog.createObject(app, {
text: message
})
dialog.open()
}
}
Component {
id: invoiceDialog
InvoiceDialog {
id: _invoiceDialog
width: parent.width
height: parent.height
onDoPay: {
if (invoice.invoiceType == Invoice.OnchainInvoice
|| (invoice.invoiceType == Invoice.LightningInvoice
&& invoice.amountOverride.isEmpty
? invoice.amount.satsInt > Daemon.currentWallet.lightningCanSend
: invoice.amountOverride.satsInt > Daemon.currentWallet.lightningCanSend
))
{
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.txaccepted.connect(function() {
if (!canComplete) {
if (Daemon.currentWallet.isWatchOnly) {
dialog.finalizer.save()
} else {
dialog.finalizer.signAndSave()
}
} else {
dialog.finalizer.signAndSend()
}
})
dialog.open()
} else if (invoice.invoiceType == Invoice.LightningInvoice) {
console.log('About to pay lightning invoice')
invoice.pay_lightning_invoice()
}
}
onClosed: destroy()
Connections {
target: Daemon.currentWallet
function onSaveTxSuccess(txid) {
_invoiceDialog.close()
}
}
}
}
Component {
id: sendDialog
SendDialog {
width: parent.width
height: parent.height
onTxFound: {
app.stack.push(Qt.resolvedUrl('TxDetails.qml'), { rawtx: data })
close()
}
onChannelBackupFound: {
var dialog = app.messageDialog.createObject(app, {
text: qsTr('Import Channel backup?'),
yesno: true
})
dialog.yesClicked.connect(function() {
Daemon.currentWallet.importChannelBackup(data)
close()
})
dialog.rejected.connect(function() {
close()
})
dialog.open()
}
onClosed: destroy()
}
}
function createRequest(lightning_only, reuse_address) {
var qamt = Config.unitsToSats(_request_amount)
Daemon.currentWallet.createRequest(qamt, _request_description, _request_expiry, lightning_only, reuse_address)
}
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(false, false)
}
onRejected: {
console.log('rejected')
}
onClosed: destroy()
}
}
Component {
id: receiveDialog
ReceiveDialog {
width: parent.width
height: parent.height
onClosed: destroy()
}
}
Component {
id: confirmPaymentDialog
ConfirmTxDialog {
id: _confirmPaymentDialog
title: qsTr('Confirm Payment')
finalizer: TxFinalizer {
wallet: Daemon.currentWallet
canRbf: true
onFinishedSave: {
if (wallet.isWatchOnly) {
// tx was saved. Show QR for signer(s)
showExportByTxid(txid, qsTr('Transaction created. Present this QR code to the signing device'))
} else {
// tx was (partially) signed and saved. Show QR for co-signers or online wallet
showExportByTxid(txid, qsTr('Transaction created and partially signed by this wallet. Present this QR code to the next co-signer'))
}
_confirmPaymentDialog.destroy()
}
}
// 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: lightningPaymentProgressDialog
LightningPaymentProgressDialog {
onClosed: destroy()
}
}
Component {
id: lnurlPayDialog
LnurlPayRequestDialog {
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()
}
}
}