Merge pull request #9476 from accumulator/qml_calc_max_amount
qml: calculate max amount when max toggle is enabled
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user