Browse Source

qml: SweepDialog

master
Sander van Grieken 1 year ago
parent
commit
44c0e583d6
No known key found for this signature in database
GPG Key ID: 9BCF8209EA402EBA
  1. 4
      electrum/gui/qml/components/ConfirmTxDialog.qml
  2. 159
      electrum/gui/qml/components/SweepDialog.qml
  3. 55
      electrum/gui/qml/components/WalletMainView.qml
  4. 3
      electrum/gui/qml/qeapp.py
  5. 105
      electrum/gui/qml/qetxfinalizer.py

4
electrum/gui/qml/components/ConfirmTxDialog.qml

@ -14,6 +14,7 @@ ElDialog {
required property Amount satoshis
property string address
property string message
property bool showOptions: true
property alias amountLabelText: amountLabel.text
property alias sendButtonText: sendButton.text
@ -142,12 +143,13 @@ ElDialog {
Layout.columnSpan: 2
labelText: qsTr('Options')
color: Material.accentColor
visible: showOptions
}
TextHighlightPane {
Layout.columnSpan: 2
Layout.fillWidth: true
visible: !optionstoggle.collapsed
visible: optionstoggle.visible && !optionstoggle.collapsed
height: optionslayout.height
GridLayout {

159
electrum/gui/qml/components/SweepDialog.qml

@ -0,0 +1,159 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import org.electrum
import "controls"
ElDialog {
id: root
title: qsTr('Sweep private keys')
iconSource: Qt.resolvedUrl('../../icons/add.png')
property bool valid: false
property string privateKeys
width: parent.width
height: parent.height
padding: 0
function verifyPrivateKey(key) {
valid = false
validationtext.text = ''
key = key.trim()
if (!key) {
return false
}
if (!bitcoin.isPrivateKeyList(key)) {
validationtext.text = qsTr('Error: invalid private key(s)')
return false
}
return valid = true
}
function addPrivateKey(key) {
if (sweepkeys.text.includes(key))
return
if (sweepkeys.text && !sweepkeys.text.endsWith('\n'))
sweepkeys.text = sweepkeys.text + '\n'
sweepkeys.text = sweepkeys.text + key + '\n'
}
ColumnLayout {
anchors.fill: parent
spacing: 0
ColumnLayout {
Layout.leftMargin: constants.paddingLarge
Layout.rightMargin: constants.paddingLarge
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
RowLayout {
Layout.fillWidth: true
TextHighlightPane {
Layout.fillWidth: true
Label {
text: qsTr('Enter the list of private keys to sweep into this wallet')
wrapMode: Text.Wrap
}
}
HelpButton {
heading: qsTr('Sweep private keys')
helptext: qsTr('This will create a transaction sending all funds associated with the private keys to the current wallet') +
'<br/><br/>' + qsTr('WIF keys are typed in Electrum, based on script type.') + '<br/><br/>' +
qsTr('A few examples') + ':<br/>' +
'<tt><b>p2pkh</b>:KxZcY47uGp9a... \t-> 1DckmggQM...<br/>' +
'<b>p2wpkh-p2sh</b>:KxZcY47uGp9a... \t-> 3NhNeZQXF...<br/>' +
'<b>p2wpkh</b>:KxZcY47uGp9a... \t-> bc1q3fjfk...</tt>'
}
}
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
ElTextArea {
id: sweepkeys
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 160
font.family: FixedFont
wrapMode: TextEdit.WrapAnywhere
onTextChanged: {
if (anyActiveFocus) {
verifyPrivateKey(text)
}
}
inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
background: PaneInsetBackground {
baseColor: constants.darkerDialogBackground
}
}
ColumnLayout {
Layout.alignment: Qt.AlignTop
ToolButton {
icon.source: '../../icons/paste.png'
icon.height: constants.iconSizeMedium
icon.width: constants.iconSizeMedium
onClicked: {
if (verifyPrivateKey(AppController.clipboardToText()))
addPrivateKey(AppController.clipboardToText())
}
}
ToolButton {
icon.source: '../../icons/qrcode.png'
icon.height: constants.iconSizeMedium
icon.width: constants.iconSizeMedium
scale: 1.2
onClicked: {
var dialog = app.scanDialog.createObject(app, {
hint: qsTr('Scan a private key')
})
dialog.onFound.connect(function() {
if (verifyPrivateKey(dialog.scanData))
addPrivateKey(dialog.scanData)
dialog.close()
})
dialog.open()
}
}
}
}
InfoTextArea {
id: validationtext
iconStyle: InfoTextArea.IconStyle.Warn
Layout.fillWidth: true
Layout.margins: constants.paddingMedium
visible: text
}
}
}
FlatButton {
Layout.fillWidth: true
Layout.preferredWidth: 1
enabled: valid
icon.source: '../../icons/tab_send.png'
text: qsTr('Sweep')
onClicked: {
console.log('sweeping')
root.privateKeys = sweepkeys.text
console.log(root.privateKeys)
root.accept()
}
}
}
Bitcoin {
id: bitcoin
}
}

55
electrum/gui/qml/components/WalletMainView.qml

@ -131,6 +131,25 @@ Item {
Daemon.currentWallet.createRequest(qamt, _request_description, _request_expiry, lightning_only, 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: qsTr('Sweep')
})
finalizerDialog.accepted.connect(function() {
console.log("Sending sweep transaction")
finalizerDialog.finalizer.send()
})
finalizerDialog.open()
})
dialog.open()
}
property QtObject menu: Menu {
id: menu
@ -187,6 +206,19 @@ Item {
}
}
MenuItem {
icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor
icon.source: '../../icons/add.png'
action: Action {
text: qsTr('Sweep key')
enabled: !Daemon.currentWallet.isWatchOnly // watchonly might be acceptable
onTriggered: {
startSweep()
menu.deselect()
}
}
}
MenuSeparator { }
MenuItem {
@ -608,6 +640,22 @@ Item {
}
}
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 {
@ -635,5 +683,12 @@ Item {
}
}
Component {
id: sweepDialog
SweepDialog {
onClosed: destroy()
}
}
}

3
electrum/gui/qml/qeapp.py

@ -29,7 +29,7 @@ from .qeqr import QEQRParser, QEQRImageProvider, QEQRImageProviderHelper
from .qeqrscanner import QEQRScanner
from .qebitcoin import QEBitcoin
from .qefx import QEFX
from .qetxfinalizer import QETxFinalizer, QETxRbfFeeBumper, QETxCpfpFeeBumper, QETxCanceller
from .qetxfinalizer import QETxFinalizer, QETxRbfFeeBumper, QETxCpfpFeeBumper, QETxCanceller, QETxSweepFinalizer
from .qeinvoice import QEInvoice, QEInvoiceParser
from .qerequestdetails import QERequestDetails
from .qetypes import QEAmount
@ -396,6 +396,7 @@ class ElectrumQmlApplication(QGuiApplication):
qmlRegisterType(QETxRbfFeeBumper, 'org.electrum', 1, 0, 'TxRbfFeeBumper')
qmlRegisterType(QETxCpfpFeeBumper, 'org.electrum', 1, 0, 'TxCpfpFeeBumper')
qmlRegisterType(QETxCanceller, 'org.electrum', 1, 0, 'TxCanceller')
qmlRegisterType(QETxSweepFinalizer, 'org.electrum', 1, 0, 'SweepFinalizer')
qmlRegisterType(QEBip39RecoveryListModel, 'org.electrum', 1, 0, 'Bip39RecoveryListModel')
# TODO QT6: these were declared as uncreatable, but that doesn't seem to work for pyqt6

105
electrum/gui/qml/qetxfinalizer.py

@ -1,3 +1,5 @@
import copy
import threading
from decimal import Decimal
from typing import Optional, TYPE_CHECKING
from functools import partial
@ -7,8 +9,9 @@ from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from electrum.logging import get_logger
from electrum.i18n import _
from electrum.transaction import PartialTxOutput, PartialTransaction, Transaction, TxOutpoint
from electrum.util import NotEnoughFunds, profiler, quantize_feerate
from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx, CannotCPFP, BumpFeeStrategy
from electrum.util import NotEnoughFunds, profiler, quantize_feerate, UserFacingException
from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx, CannotCPFP, BumpFeeStrategy, sweep_preparations
from electrum import keystore
from electrum.plugin import run_hook
from .qewallet import QEWallet
@ -868,3 +871,101 @@ class QETxCpfpFeeBumper(TxFeeSlider, TxMonMixin):
@pyqtSlot(result=str)
def getNewTx(self):
return str(self._new_tx)
class QETxSweepFinalizer(QETxFinalizer):
_logger = get_logger(__name__)
txinsRetrieved = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self._private_keys = ''
self._txins = None
self._amount = QEAmount(is_max=True)
self.txinsRetrieved.connect(self.update)
privateKeysChanged = pyqtSignal()
@pyqtProperty(str, notify=privateKeysChanged)
def privateKeys(self):
return self._private_keys
@privateKeys.setter
def privateKeys(self, private_keys):
if self._private_keys != private_keys:
self._private_keys = private_keys
self.update_privkeys()
self.privateKeysChanged.emit()
def make_sweep_tx(self):
address = self._wallet.wallet.get_unused_address() # TODO: dont fail
coins, keypairs = copy.deepcopy(self._txins)
outputs = [PartialTxOutput.from_address_and_value(address, value='!')]
tx = self._wallet.wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=None, rbf=self._rbf, is_sweep=True)
self._logger.debug('fee: %d, inputs: %d, outputs: %d' % (tx.get_fee(), len(tx.inputs()), len(tx.outputs())))
tx.sign(keypairs)
return tx
def update_privkeys(self):
privkeys = keystore.get_private_keys(self._private_keys)
def fetch_privkeys_info():
try:
self._txins = self._wallet.wallet.network.run_from_another_thread(sweep_preparations(privkeys, self._wallet.wallet.network))
self._logger.info(f'txins {self._txins!r}')
except UserFacingException as e:
self.warning = str(e)
return
self.txinsRetrieved.emit()
threading.Thread(target=fetch_privkeys_info, daemon=True).start()
def update(self):
if not self._wallet:
self._logger.debug('wallet not set, ignoring update()')
return
if not self._private_keys:
self._logger.debug('private keys not set, ignoring update()')
return
try:
# make unsigned transaction
tx = self.make_sweep_tx()
except Exception as e:
self._logger.error(str(e))
self.warning = repr(e)
self._valid = False
self.validChanged.emit()
return
self._tx = tx
amount = tx.output_value()
self._effectiveAmount.satsInt = amount
self.effectiveAmountChanged.emit()
self.update_from_tx(tx)
fee_warning_tuple = self._wallet.wallet.get_tx_fee_warning(
invoice_amt=amount, tx_size=tx.estimated_size(), fee=tx.get_fee())
if fee_warning_tuple:
allow_send, long_warning, short_warning = fee_warning_tuple
self.warning = _('Warning') + ': ' + long_warning
else:
self.warning = ''
self._valid = True
self.validChanged.emit()
self.on_signed_tx(False, tx)
@pyqtSlot()
def send(self):
self._wallet.broadcast(self._tx)
self._wallet.wallet.set_label(self._tx.txid(), _('Sweep transaction'))

Loading…
Cancel
Save