Browse Source

add a QEAmount type for storing/passing BTC amounts in the widest sense

from a UI perspective. Stores sats, millisats (LN), whether MAX amount is
requested etc

some refactor QEInvoice type and Send page
patch-4
Sander van Grieken 3 years ago
parent
commit
5031391484
  1. 19
      electrum/gui/qml/components/ConfirmInvoiceDialog.qml
  2. 11
      electrum/gui/qml/components/Send.qml
  3. 5
      electrum/gui/qml/qeapp.py
  4. 6
      electrum/gui/qml/qebitcoin.py
  5. 8
      electrum/gui/qml/qeconfig.py
  6. 33
      electrum/gui/qml/qefx.py
  7. 80
      electrum/gui/qml/qeinvoice.py
  8. 8
      electrum/gui/qml/qeinvoicelistmodel.py
  9. 7
      electrum/gui/qml/qetransactionlistmodel.py
  10. 55
      electrum/gui/qml/qetypes.py

19
electrum/gui/qml/components/ConfirmInvoiceDialog.qml

@ -11,6 +11,7 @@ Dialog {
id: dialog
property Invoice invoice
property string invoice_key
width: parent.width
height: parent.height
@ -84,9 +85,20 @@ Dialog {
}
}
Label {
text: qsTr('Expiration')
visible: true
}
Label {
id: expiration
text: invoice.time + invoice.expiration
}
RowLayout {
Layout.columnSpan: 2
Layout.alignment: Qt.AlignHCenter
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
Layout.fillHeight: true
spacing: constants.paddingMedium
Button {
@ -115,4 +127,9 @@ Dialog {
}
Component.onCompleted: {
if (invoice_key != '') {
invoice.initFromKey(invoice_key)
}
}
}

11
electrum/gui/qml/components/Send.qml

@ -213,7 +213,12 @@ Pane {
model: DelegateModel {
id: delegateModel
model: Daemon.currentWallet.invoiceModel
delegate: InvoiceDelegate {}
delegate: InvoiceDelegate {
onClicked: {
var dialog = confirmInvoiceDialog.createObject(app, {'invoice' : invoice, 'invoice_key': model.key})
dialog.open()
}
}
}
remove: Transition {
@ -288,9 +293,7 @@ Pane {
// and maybe store invoice if expiry allows
}
}
onInvoiceTypeChanged: {
if (invoiceType == Invoice.Invalid)
return
onValidationSuccess: {
// address only -> fill form fields
// else -> show invoice confirmation dialog
if (invoiceType == Invoice.OnchainOnlyAddress)

5
electrum/gui/qml/qeapp.py

@ -5,7 +5,7 @@ import os
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QUrl, QLocale, qInstallMessageHandler, QTimer
from PyQt5.QtGui import QGuiApplication, QFontDatabase
from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine
from PyQt5.QtQml import qmlRegisterType, qmlRegisterUncreatableType, QQmlApplicationEngine
from electrum.logging import Logger, get_logger
from electrum import version
@ -20,6 +20,7 @@ from .qebitcoin import QEBitcoin
from .qefx import QEFX
from .qetxfinalizer import QETxFinalizer
from .qeinvoice import QEInvoice
from .qetypes import QEAmount
notification = None
@ -118,6 +119,8 @@ class ElectrumQmlApplication(QGuiApplication):
qmlRegisterType(QETxFinalizer, 'org.electrum', 1, 0, 'TxFinalizer')
qmlRegisterType(QEInvoice, 'org.electrum', 1, 0, 'Invoice')
qmlRegisterUncreatableType(QEAmount, 'org.electrum', 1, 0, 'Amount', 'Amount can only be used as property')
self.engine = QQmlApplicationEngine(parent=self)
self.engine.addImportPath('./qml')

6
electrum/gui/qml/qebitcoin.py

@ -10,6 +10,8 @@ from electrum.slip39 import decode_mnemonic, Slip39Error
from electrum import mnemonic
from electrum.util import parse_URI, create_bip21_uri, InvalidBitcoinURI
from .qetypes import QEAmount
class QEBitcoin(QObject):
def __init__(self, config, parent=None):
super().__init__(parent)
@ -121,11 +123,11 @@ class QEBitcoin(QObject):
except InvalidBitcoinURI as e:
return { 'error': str(e) }
@pyqtSlot(str, 'qint64', str, int, int, result=str)
@pyqtSlot(str, QEAmount, str, int, int, result=str)
def create_uri(self, address, satoshis, message, timestamp, expiry):
extra_params = {}
if expiry:
extra_params['time'] = str(timestamp)
extra_params['exp'] = str(expiry)
return create_bip21_uri(address, satoshis, message, extra_query_params=extra_params)
return create_bip21_uri(address, satoshis.satsInt, message, extra_query_params=extra_params)

8
electrum/gui/qml/qeconfig.py

@ -5,6 +5,8 @@ from decimal import Decimal
from electrum.logging import get_logger
from electrum.util import DECIMAL_POINT_DEFAULT
from .qetypes import QEAmount
class QEConfig(QObject):
def __init__(self, config, parent=None):
super().__init__(parent)
@ -70,7 +72,11 @@ class QEConfig(QObject):
@pyqtSlot('qint64', result=str)
@pyqtSlot('qint64', bool, result=str)
@pyqtSlot(QEAmount, result=str)
@pyqtSlot(QEAmount, bool, result=str)
def formatSats(self, satoshis, with_unit=False):
if isinstance(satoshis, QEAmount):
satoshis = satoshis.satsInt
if with_unit:
return self.config.format_amount_and_units(satoshis)
else:
@ -85,11 +91,11 @@ class QEConfig(QObject):
@pyqtSlot(str, result='qint64')
def unitsToSats(self, unitAmount):
# returns amt in satoshis
try:
x = Decimal(unitAmount)
except:
return 0
# scale it to max allowed precision, make it an int
max_prec_amount = int(pow(10, self.max_precision()) * x)
# if the max precision is simply what unit conversion allows, just return

33
electrum/gui/qml/qefx.py

@ -9,6 +9,8 @@ from electrum.simple_config import SimpleConfig
from electrum.util import register_callback
from electrum.bitcoin import COIN
from .qetypes import QEAmount
class QEFX(QObject):
def __init__(self, fxthread: FxThread, config: SimpleConfig, parent=None):
super().__init__(parent)
@ -88,25 +90,40 @@ class QEFX(QObject):
@pyqtSlot(str, result=str)
@pyqtSlot(str, bool, result=str)
@pyqtSlot(QEAmount, result=str)
@pyqtSlot(QEAmount, bool, result=str)
def fiatValue(self, satoshis, plain=True):
rate = self.fx.exchange_rate()
try:
sd = Decimal(satoshis)
if sd == 0:
if isinstance(satoshis, QEAmount):
satoshis = satoshis.satsInt
else:
try:
sd = Decimal(satoshis)
if sd == 0:
return ''
except:
return ''
except:
return ''
if plain:
return self.fx.ccy_amount_str(self.fx.fiat_value(satoshis, rate), False)
else:
return self.fx.value_str(satoshis, rate)
@pyqtSlot(str, str, result=str)
@pyqtSlot(str, str, bool, result=str)
@pyqtSlot(QEAmount, str, result=str)
@pyqtSlot(QEAmount, str, bool, result=str)
def fiatValueHistoric(self, satoshis, timestamp, plain=True):
try:
sd = Decimal(satoshis)
if sd == 0:
if isinstance(satoshis, QEAmount):
satoshis = satoshis.satsInt
else:
try:
sd = Decimal(satoshis)
if sd == 0:
return ''
except:
return ''
try:
td = Decimal(timestamp)
if td == 0:
return ''

80
electrum/gui/qml/qeinvoice.py

@ -12,6 +12,7 @@ from electrum.invoices import Invoice, OnchainInvoice, LNInvoice
from electrum.transaction import PartialTxOutput
from .qewallet import QEWallet
from .qetypes import QEAmount
class QEInvoice(QObject):
@ -30,28 +31,26 @@ class QEInvoice(QObject):
_invoiceType = Type.Invalid
_recipient = ''
_effectiveInvoice = None
_message = ''
_amount = 0
validationError = pyqtSignal([str,str], arguments=['code', 'message'])
validationWarning = pyqtSignal([str,str], arguments=['code', 'message'])
invoiceChanged = pyqtSignal()
invoiceSaved = pyqtSignal()
validationSuccess = pyqtSignal()
validationWarning = pyqtSignal([str,str], arguments=['code', 'message'])
validationError = pyqtSignal([str,str], arguments=['code', 'message'])
def __init__(self, config, parent=None):
super().__init__(parent)
self.config = config
self.clear()
invoiceTypeChanged = pyqtSignal()
@pyqtProperty(int, notify=invoiceTypeChanged)
@pyqtProperty(int, notify=invoiceChanged)
def invoiceType(self):
return self._invoiceType
# not a qt setter, don't let outside set state
def setInvoiceType(self, invoiceType: Type):
#if self._invoiceType != invoiceType:
self._invoiceType = invoiceType
self.invoiceTypeChanged.emit()
self._invoiceType = invoiceType
walletChanged = pyqtSignal()
@pyqtProperty(QEWallet, notify=walletChanged)
@ -77,16 +76,29 @@ class QEInvoice(QObject):
self.validateRecipient(recipient)
self.recipientChanged.emit()
messageChanged = pyqtSignal()
@pyqtProperty(str, notify=messageChanged)
@pyqtProperty(str, notify=invoiceChanged)
def message(self):
return self._message
return self._effectiveInvoice.message if self._effectiveInvoice else ''
amountChanged = pyqtSignal()
@pyqtProperty('quint64', notify=amountChanged)
@pyqtProperty(QEAmount, notify=invoiceChanged)
def amount(self):
# store ref to QEAmount on instance, otherwise we get destroyed when going out of scope
self._amount = QEAmount() #
if not self._effectiveInvoice:
return self._amount
sats = self._effectiveInvoice.get_amount_sat()
if not sats:
return self._amount
self._amount = QEAmount(amount_sat=sats)
return self._amount
@pyqtProperty('quint64', notify=invoiceChanged)
def expiration(self):
return self._effectiveInvoice.exp if self._effectiveInvoice else 0
@pyqtProperty('quint64', notify=invoiceChanged)
def time(self):
return self._effectiveInvoice.time if self._effectiveInvoice else 0
@pyqtSlot()
def clear(self):
@ -94,31 +106,38 @@ class QEInvoice(QObject):
self.invoiceSetsAmount = False
self.setInvoiceType(QEInvoice.Type.Invalid)
self._bip21 = None
self.invoiceChanged.emit()
# don't parse the recipient string, but init qeinvoice from an invoice key
# this should not emit validation signals
@pyqtSlot(str)
def initFromKey(self, key):
invoice = self._wallet.wallet.get_invoice(key)
self._logger.debug(repr(invoice))
if invoice:
self.set_effective_invoice(invoice)
def set_effective_invoice(self, invoice: Invoice):
self._effectiveInvoice = invoice
if invoice.is_lightning():
self.setInvoiceType(QEInvoice.Type.LightningInvoice)
else:
self.setInvoiceType(QEInvoice.Type.OnchainInvoice)
self.invoiceChanged.emit()
def setValidAddressOnly(self):
self._logger.debug('setValidAddressOnly')
self.setInvoiceType(QEInvoice.Type.OnchainOnlyAddress)
self._effectiveInvoice = None ###TODO
self.invoiceChanged.emit()
def setValidOnchainInvoice(self, invoice: OnchainInvoice):
self._logger.debug('setValidOnchainInvoice')
self.setInvoiceType(QEInvoice.Type.OnchainInvoice)
self._amount = invoice.get_amount_sat()
self.amountChanged.emit()
self._message = invoice.message
self.messageChanged.emit()
self._effectiveInvoice = invoice
self.set_effective_invoice(invoice)
def setValidLightningInvoice(self, invoice: LNInvoice):
self._logger.debug('setValidLightningInvoice')
self.setInvoiceType(QEInvoice.Type.LightningInvoice)
self._effectiveInvoice = invoice
self._amount = int(invoice.get_amount_sat()) # TODO: float/str msat precision
self.amountChanged.emit()
self._message = invoice.message
self.messageChanged.emit()
self.set_effective_invoice(invoice)
def create_onchain_invoice(self, outputs, message, payment_request, uri):
return self._wallet.wallet.create_invoice(
@ -150,6 +169,7 @@ class QEInvoice(QObject):
if ':' not in recipient:
# address only
self.setValidAddressOnly()
self.validationSuccess.emit()
return
else:
# fallback lightning invoice?
@ -194,13 +214,15 @@ class QEInvoice(QObject):
self._logger.debug(outputs)
message = self._bip21['message'] if 'message' in self._bip21 else ''
invoice = self.create_onchain_invoice(outputs, message, None, self._bip21)
self._logger.debug(invoice)
self._logger.debug(repr(invoice))
self.setValidOnchainInvoice(invoice)
self.validationSuccess.emit()
@pyqtSlot()
def save_invoice(self):
if not self._effectiveInvoice:
return
# TODO detect duplicate?
self._wallet.wallet.save_invoice(self._effectiveInvoice)
self.invoiceSaved.emit()

8
electrum/gui/qml/qeinvoicelistmodel.py

@ -7,6 +7,8 @@ from electrum.logging import get_logger
from electrum.util import Satoshis, format_time
from electrum.invoices import Invoice
from .qetypes import QEAmount
class QEAbstractInvoiceListModel(QAbstractListModel):
_logger = get_logger(__name__)
@ -35,6 +37,8 @@ class QEAbstractInvoiceListModel(QAbstractListModel):
return value
if isinstance(value, Satoshis):
return value.value
if isinstance(value, QEAmount):
return value
return str(value)
def clear(self):
@ -111,7 +115,7 @@ class QEInvoiceListModel(QEAbstractInvoiceListModel):
item = self.wallet.export_invoice(invoice)
item['type'] = invoice.type # 0=onchain, 2=LN
item['date'] = format_time(item['timestamp'])
item['amount'] = invoice.get_amount_sat()
item['amount'] = QEAmount(amount_sat=invoice.get_amount_sat())
if invoice.type == 0:
item['key'] = invoice.id
elif invoice.type == 2:
@ -136,7 +140,7 @@ class QERequestListModel(QEAbstractInvoiceListModel):
item['key'] = self.wallet.get_key_for_receive_request(req)
item['type'] = req.type # 0=onchain, 2=LN
item['date'] = format_time(item['timestamp'])
item['amount'] = req.get_amount_sat()
item['amount'] = QEAmount(amount_sat=req.get_amount_sat())
return item

7
electrum/gui/qml/qetransactionlistmodel.py

@ -6,6 +6,8 @@ from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex
from electrum.logging import get_logger
from electrum.util import Satoshis, TxMinedInfo
from .qetypes import QEAmount
class QETransactionListModel(QAbstractListModel):
def __init__(self, wallet, parent=None):
super().__init__(parent)
@ -36,6 +38,8 @@ class QETransactionListModel(QAbstractListModel):
return value
if isinstance(value, Satoshis):
return value.value
if isinstance(value, QEAmount):
return value
return str(value)
def clear(self):
@ -48,6 +52,9 @@ class QETransactionListModel(QAbstractListModel):
for output in item['outputs']:
output['value'] = output['value'].value
item['bc_value'] = QEAmount(amount_sat=item['bc_value'].value)
item['bc_balance'] = QEAmount(amount_sat=item['bc_balance'].value)
# newly arriving txs have no (block) timestamp
# TODO?
if not item['timestamp']:

55
electrum/gui/qml/qetypes.py

@ -0,0 +1,55 @@
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from electrum.logging import get_logger
from electrum.i18n import _
from electrum.util import profiler
# container for satoshi amounts that can be passed around more
# easily between python, QML-property and QML-javascript contexts
# QML 'int' is 32 bit signed, so overflows on satoshi amounts
# QML 'quint64' and 'qint64' can be used, but this breaks
# down when passing through property bindings
# should also capture millisats amounts and MAX/'!' indicators
# and (unformatted) string representations
class QEAmount(QObject):
_logger = get_logger(__name__)
def __init__(self, *, amount_sat: int = 0, amount_msat: int = 0, is_max: bool = False, parent=None):
super().__init__(parent)
self._amount_sat = amount_sat
self._amount_msat = amount_msat
self._is_max = is_max
valueChanged = pyqtSignal()
@pyqtProperty('qint64', notify=valueChanged)
def satsInt(self):
return self._amount_sat
@pyqtProperty('qint64', notify=valueChanged)
def msatsInt(self):
return self._amount_msat
@pyqtProperty(str, notify=valueChanged)
def satsStr(self):
return str(self._amount_sat)
@pyqtProperty(str, notify=valueChanged)
def msatsStr(self):
return str(self._amount_msat)
@pyqtProperty(bool, notify=valueChanged)
def isMax(self):
return self._is_max
def __eq__(self, other):
self._logger.debug('__eq__')
if isinstance(other, QEAmount):
return self._amount_sat == other._amount_sat and self._amount_msat == other._amount_msat and self._is_max == other._is_max
elif isinstance(other, int):
return self._amount_sat == other
elif isinstance(other, str):
return self.satsStr == other
return False
Loading…
Cancel
Save