1
0

Merge pull request #9476 from accumulator/qml_calc_max_amount

qml: calculate max amount when max toggle is enabled
This commit is contained in:
ghost43
2025-02-03 13:37:07 +00:00
committed by GitHub
7 changed files with 212 additions and 36 deletions

View File

@@ -142,6 +142,7 @@ ElDialog {
Layout.alignment: Qt.AlignHCenter
leftPadding: constants.paddingXLarge
rightPadding: constants.paddingXLarge
property bool editmode: false
@@ -216,10 +217,23 @@ ElDialog {
BtcField {
id: amountBtc
Layout.preferredWidth: amountFontMetrics.advanceWidth('0') * 14 + leftPadding + rightPadding
fiatfield: amountFiat
enabled: !amountMax.checked
readOnly: amountMax.checked
color: readOnly
? Material.accentColor
: Material.foreground
onTextAsSatsChanged: {
invoice.amountOverride = textAsSats
if (!amountMax.checked)
invoice.amountOverride.satsInt = textAsSats.satsInt
}
Connections {
target: invoice.amountOverride
function onSatsIntChanged() {
console.log('amountOverride satsIntChanged, sats=' + invoice.amountOverride.satsInt)
if (amountMax.checked) // amountOverride updated by max amount estimate
amountBtc.text = Config.formatSatsForEditing(invoice.amountOverride.satsInt)
}
}
}
@@ -239,24 +253,48 @@ ElDialog {
visible: _canMax
checked: false
onCheckedChanged: {
if (activeFocus)
if (activeFocus) {
invoice.amountOverride.isMax = checked
if (checked) {
maxAmountMessage.text = ''
invoice.updateMaxAmount()
}
}
}
}
FiatField {
id: amountFiat
Layout.preferredWidth: amountFontMetrics.advanceWidth('0') * 14 + leftPadding + rightPadding
btcfield: amountBtc
visible: Daemon.fx.enabled && !amountMax.checked
enabled: !amountMax.checked
visible: Daemon.fx.enabled
readOnly: amountMax.checked
color: readOnly
? Material.accentColor
: Material.foreground
}
Label {
Layout.columnSpan: 2
visible: Daemon.fx.enabled && !amountMax.checked
visible: Daemon.fx.enabled
text: Daemon.fx.fiatCurrency
color: Material.accentColor
}
InfoTextArea {
Layout.topMargin: constants.paddingMedium
Layout.fillWidth: true
Layout.columnSpan: 3
id: maxAmountMessage
visible: amountMax.checked && text
compact: true
Connections {
target: invoice
function onMaxAmountMessage(message) {
maxAmountMessage.text = message
}
}
}
}
}
@@ -425,7 +463,9 @@ ElDialog {
enabled: !invoice.isSaved && invoice.canSave
onClicked: {
if (invoice.amount.isEmpty) {
invoice.amountOverride = amountMax.checked ? MAX : Config.unitsToSats(amountBtc.text)
invoice.amountOverride = Config.unitsToSats(amountBtc.text)
if (amountMax.checked)
invoice.amountOverride.isMax = true
}
invoice.saveInvoice()
app.stack.push(Qt.resolvedUrl('Invoices.qml'))
@@ -440,7 +480,9 @@ ElDialog {
enabled: invoice.invoiceType != Invoice.Invalid && invoice.canPay
onClicked: {
if (invoice.amount.isEmpty) {
invoice.amountOverride = amountMax.checked ? MAX : Config.unitsToSats(amountBtc.text)
invoice.amountOverride = Config.unitsToSats(amountBtc.text)
if (amountMax.checked)
invoice.amountOverride.isMax = true
}
if (!invoice.isSaved) {
// save invoice if newly parsed
@@ -468,4 +510,9 @@ ElDialog {
}
}
}
FontMetrics {
id: amountFontMetrics
font: amountBtc.font
}
}

View File

@@ -167,11 +167,25 @@ ElDialog {
}
BtcField {
id: amount
id: amountBtc
fiatfield: amountFiat
Layout.preferredWidth: parent.width /3
onTextChanged: channelopener.amount = Config.unitsToSats(amount.text)
enabled: !is_max.checked
Layout.preferredWidth: amountFontMetrics.advanceWidth('0') * 14 + leftPadding + rightPadding
onTextAsSatsChanged: {
if (!is_max.checked)
channelopener.amount.satsInt = amountBtc.textAsSats.satsInt
}
readOnly: is_max.checked
color: readOnly
? Material.accentColor
: Material.foreground
Connections {
target: channelopener.amount
function onSatsIntChanged() {
if (is_max.checked) // amount updated by max amount estimate
amountBtc.text = Config.formatSatsForEditing(channelopener.amount.satsInt)
}
}
}
RowLayout {
@@ -184,7 +198,13 @@ ElDialog {
id: is_max
text: qsTr('Max')
onCheckedChanged: {
channelopener.amount = checked ? MAX : Config.unitsToSats(amount.text)
if (activeFocus) {
channelopener.amount.isMax = checked
if (checked) {
maxAmountMessage.text = ''
channelopener.updateMaxAmount()
}
}
}
}
}
@@ -193,10 +213,13 @@ ElDialog {
FiatField {
id: amountFiat
btcfield: amount
Layout.preferredWidth: amountFontMetrics.advanceWidth('0') * 14 + leftPadding + rightPadding
btcfield: amountBtc
visible: Daemon.fx.enabled
Layout.preferredWidth: parent.width /3
enabled: !is_max.checked
readOnly: is_max.checked
color: readOnly
? Material.accentColor
: Material.foreground
}
Label {
@@ -207,6 +230,16 @@ ElDialog {
}
Item { visible: Daemon.fx.enabled ; height: 1; width: 1 }
InfoTextArea {
Layout.topMargin: constants.paddingMedium
Layout.fillWidth: true
Layout.columnSpan: 3
id: maxAmountMessage
visible: is_max.checked && text
compact: true
}
}
}
@@ -288,6 +321,13 @@ ElDialog {
// TODO: handle incomplete TX
root.close()
}
onMaxAmountMessage: (message) => {
maxAmountMessage.text = message
}
}
FontMetrics {
id: amountFontMetrics
font: amountBtc.font
}
}

View File

@@ -1,13 +1,14 @@
import threading
from concurrent.futures import CancelledError
from asyncio.exceptions import TimeoutError
from typing import TYPE_CHECKING, Optional
from typing import Optional
import electrum_ecc as ecc
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from electrum.i18n import _
from electrum.gui import messages
from electrum.util import bfh
from electrum.util import bfh, NotEnoughFunds, NoDynamicFeeEstimates
from electrum.lntransport import extract_nodeid, ConnStringFormatError
from electrum.bitcoin import DummyAddress
from electrum.lnworker import hardcoded_trampoline_nodes
@@ -29,6 +30,7 @@ class QEChannelOpener(QObject, AuthMixin):
channelOpenError = pyqtSignal([str], arguments=['message'])
channelOpenSuccess = pyqtSignal([str, bool, int, bool],
arguments=['cid', 'has_onchain_backup', 'min_depth', 'tx_complete'])
maxAmountMessage = pyqtSignal([str], arguments=['message'])
dataChanged = pyqtSignal() # generic notify signal
@@ -46,6 +48,8 @@ class QEChannelOpener(QObject, AuthMixin):
self._node_pubkey = None
self._connect_str_resolved = None
self._updating_max = False
walletChanged = pyqtSignal()
@pyqtProperty(QEWallet, notify=walletChanged)
def wallet(self):
@@ -228,3 +232,32 @@ class QEChannelOpener(QObject, AuthMixin):
@pyqtSlot(str, result=str)
def channelBackup(self, cid):
return self._wallet.wallet.lnworker.export_channel_backup(bfh(cid))
@pyqtSlot()
def updateMaxAmount(self):
if self._updating_max:
return
self._updating_max = True
def calc_max():
try:
coins = self._wallet.wallet.get_spendable_coins(None, nonlocal_only=True)
dummy_nodeid = ecc.GENERATOR.get_public_key_bytes(compressed=True)
make_tx = lambda amt: self._wallet.wallet.lnworker.mktx_for_open_channel(
coins=coins,
funding_sat='!',
node_id=dummy_nodeid,
fee_est=None)
amount, message = self._wallet.determine_max(mktx=make_tx)
if amount is None:
self._amount.isMax = False
else:
self._amount.satsInt = amount
if message:
self.maxAmountMessage.emit(message)
finally:
self._updating_max = False
threading.Thread(target=calc_max, daemon=True).start()

View File

@@ -1,3 +1,4 @@
import threading
from enum import IntEnum
from typing import Optional, Dict, Any
from urllib.parse import urlparse
@@ -9,10 +10,12 @@ from electrum.logging import get_logger
from electrum.invoices import (Invoice, PR_UNPAID, PR_EXPIRED, PR_UNKNOWN, PR_PAID, PR_INFLIGHT,
PR_FAILED, PR_ROUTING, PR_UNCONFIRMED, PR_BROADCASTING, PR_BROADCAST, LN_EXPIRY_NEVER)
from electrum.transaction import PartialTxOutput, TxOutput
from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates
from electrum.lnutil import format_short_channel_id
from electrum.bitcoin import COIN
from electrum.bitcoin import COIN, address_to_script
from electrum.paymentrequest import PaymentRequest
from electrum.payment_identifier import (PaymentIdentifier, PaymentIdentifierState, PaymentIdentifierType)
from .qetypes import QEAmount
from .qewallet import QEWallet
from .util import status_update_timer_interval, QtEventListener, event_listener
@@ -42,6 +45,7 @@ class QEInvoice(QObject, QtEventListener):
invoiceChanged = pyqtSignal()
invoiceSaved = pyqtSignal([str], arguments=['key'])
amountOverrideChanged = pyqtSignal()
maxAmountMessage = pyqtSignal([str], arguments=['message'])
def __init__(self, parent=None):
super().__init__(parent)
@@ -64,6 +68,8 @@ class QEInvoice(QObject, QtEventListener):
self._amountOverride.valueChanged.connect(self._on_amountoverride_value_changed)
self._updating_max = False
self.register_callbacks()
self.destroyed.connect(lambda: self.on_destroy())
@@ -392,6 +398,38 @@ class QEInvoice(QObject, QtEventListener):
def get_max_spendable_lightning(self):
return self._wallet.wallet.lnworker.num_sats_can_send() if self._wallet.wallet.lnworker else 0
@pyqtSlot()
def updateMaxAmount(self):
if self._updating_max:
return
assert self.invoiceType == QEInvoice.Type.OnchainInvoice
# only single address invoice supported
invoice_address = self._effectiveInvoice.get_address()
self._updating_max = True
def calc_max(address):
try:
outputs = [PartialTxOutput(scriptpubkey=address_to_script(address), value='!')]
make_tx = lambda fee_est, *, confirmed_only=False: self._wallet.wallet.make_unsigned_transaction(
coins=self._wallet.wallet.get_spendable_coins(None),
outputs=outputs,
fee=fee_est,
is_sweep=False)
amount, message = self._wallet.determine_max(mktx=make_tx)
if amount is None:
self._amountOverride.isMax = False
else:
self._amountOverride.satsInt = amount
if message:
self.maxAmountMessage.emit(message)
finally:
self._updating_max = False
threading.Thread(target=calc_max, args=(invoice_address,), daemon=True).start()
class QEInvoiceParser(QEInvoice):
_logger = get_logger(__name__)

View File

@@ -3,7 +3,7 @@ import base64
import queue
import threading
import time
from typing import TYPE_CHECKING, Callable, Optional, Any
from typing import TYPE_CHECKING, Callable, Optional, Any, Tuple
from functools import partial
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer
@@ -13,7 +13,8 @@ from electrum.invoices import InvoiceError, PR_PAID, PR_BROADCASTING, PR_BROADCA
from electrum.logging import get_logger
from electrum.network import TxBroadcastError, BestEffortRequestFailed
from electrum.transaction import PartialTransaction, Transaction
from electrum.util import InvalidPassword, event_listener, AddTransactionException, get_asyncio_loop
from electrum.util import InvalidPassword, event_listener, AddTransactionException, get_asyncio_loop, NotEnoughFunds, \
NoDynamicFeeEstimates
from electrum.plugin import run_hook
from electrum.wallet import Multisig_Wallet
from electrum.crypto import pw_decode_with_version_and_mac
@@ -817,3 +818,20 @@ class QEWallet(AuthMixin, QObject, QtEventListener):
def signMessage(self, address, message):
sig = self.wallet.sign_message(address, message, self.password)
return base64.b64encode(sig).decode('ascii')
def determine_max(self, *, mktx: Callable[[Optional[int]], PartialTransaction]) -> Tuple[Optional[int], Optional[str]]:
# TODO: merge with SendTab.spend_max() and move to backend wallet
amount = message = None
try:
try:
tx = mktx(None)
except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
# Check if we had enough funds excluding fees,
# if so, still provide opportunity to set lower fees.
tx = mktx(0)
amount = tx.output_value()
except NotEnoughFunds as e:
self._logger.debug(str(e))
message = self.wallet.get_text_not_enough_funds_mentioning_frozen()
return amount, message

View File

@@ -267,7 +267,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
tx = make_tx(0)
except NotEnoughFunds as e:
self.max_button.setChecked(False)
text = self.get_text_not_enough_funds_mentioning_frozen()
text = self.wallet.get_text_not_enough_funds_mentioning_frozen()
self.show_error(text)
return
@@ -283,7 +283,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
if x_fee_amount:
twofactor_fee_str = self.format_amount_and_units(x_fee_amount)
msg += "\n" + _("2fa fee: {} (for the next batch of transactions)").format(twofactor_fee_str)
frozen_bal = self.get_frozen_balance_str()
frozen_bal = self.wallet.get_frozen_balance_str()
if frozen_bal:
msg += "\n" + _("Some coins are frozen: {} (can be unfrozen in the Addresses or in the Coins tab)").format(frozen_bal)
QToolTip.showText(self.max_button.mapToGlobal(QPoint(0, 0)), msg)
@@ -353,19 +353,6 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
callback=sign_done,
external_keypairs=external_keypairs)
def get_text_not_enough_funds_mentioning_frozen(self) -> str:
text = _("Not enough funds")
frozen_str = self.get_frozen_balance_str()
if frozen_str:
text += " ({} {})".format(frozen_str, _("are frozen"))
return text
def get_frozen_balance_str(self) -> Optional[str]:
frozen_bal = sum(self.wallet.get_frozen_balance())
if not frozen_bal:
return None
return self.format_amount_and_units(frozen_bal)
def do_clear(self):
self.logger.debug('do_clear')
self.lock_fields(lock_recipient=False, lock_amount=False, lock_max=True, lock_description=False)

View File

@@ -3356,6 +3356,19 @@ class Abstract_Wallet(ABC, Logger, EventListener):
def get_unlocked_password(self):
return self._password_in_memory
def get_text_not_enough_funds_mentioning_frozen(self) -> str:
text = _('Not enough funds')
frozen_str = self.get_frozen_balance_str()
if frozen_str:
text += ' ' + _('({} are frozen)').format(frozen_str)
return text
def get_frozen_balance_str(self) -> Optional[str]:
frozen_bal = sum(self.get_frozen_balance())
if not frozen_bal:
return None
return self.config.format_amount_and_units(frozen_bal)
class Simple_Wallet(Abstract_Wallet):
# wallet with a single keystore