qml: SweepDialog
This commit is contained in:
@@ -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
Normal file
159
electrum/gui/qml/components/SweepDialog.qml
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'))
|
||||
|
||||
Reference in New Issue
Block a user