Browse Source

Merge pull request #9203 from accumulator/qml_sweep_privkeys

qml: Sweep from privkeys
master
accumulator 1 year ago committed by GitHub
parent
commit
117c7b2c2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. BIN
      electrum/gui/icons/sweep.png
  2. 4
      electrum/gui/qml/components/ConfirmTxDialog.qml
  3. 158
      electrum/gui/qml/components/SweepDialog.qml
  4. 55
      electrum/gui/qml/components/WalletMainView.qml
  5. 3
      electrum/gui/qml/qeapp.py
  6. 114
      electrum/gui/qml/qetxfinalizer.py

BIN
electrum/gui/icons/sweep.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

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 {

158
electrum/gui/qml/components/SweepDialog.qml

@ -0,0 +1,158 @@
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/sweep.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
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/sweep.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
@ -399,6 +399,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

114
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,110 @@ 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):
addresses = self._wallet.wallet.get_unused_addresses()
if not addresses:
try:
addresses = self._wallet.wallet.get_receiving_addresses()
except AttributeError:
addresses = self._wallet.wallet.get_addresses()
assert len(addresses) > 0, 'no address in wallet to send to'
address = addresses[0]
assert self._wallet.wallet.adb.is_mine(address)
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.debug(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