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 id: dialog
property Invoice invoice property Invoice invoice
property string invoice_key
width: parent.width width: parent.width
height: parent.height height: parent.height
@ -84,9 +85,20 @@ Dialog {
} }
} }
Label {
text: qsTr('Expiration')
visible: true
}
Label {
id: expiration
text: invoice.time + invoice.expiration
}
RowLayout { RowLayout {
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
Layout.fillHeight: true
spacing: constants.paddingMedium spacing: constants.paddingMedium
Button { 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 { model: DelegateModel {
id: delegateModel id: delegateModel
model: Daemon.currentWallet.invoiceModel model: Daemon.currentWallet.invoiceModel
delegate: InvoiceDelegate {} delegate: InvoiceDelegate {
onClicked: {
var dialog = confirmInvoiceDialog.createObject(app, {'invoice' : invoice, 'invoice_key': model.key})
dialog.open()
}
}
} }
remove: Transition { remove: Transition {
@ -288,9 +293,7 @@ Pane {
// and maybe store invoice if expiry allows // and maybe store invoice if expiry allows
} }
} }
onInvoiceTypeChanged: { onValidationSuccess: {
if (invoiceType == Invoice.Invalid)
return
// address only -> fill form fields // address only -> fill form fields
// else -> show invoice confirmation dialog // else -> show invoice confirmation dialog
if (invoiceType == Invoice.OnchainOnlyAddress) 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.QtCore import pyqtSlot, pyqtSignal, QObject, QUrl, QLocale, qInstallMessageHandler, QTimer
from PyQt5.QtGui import QGuiApplication, QFontDatabase 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.logging import Logger, get_logger
from electrum import version from electrum import version
@ -20,6 +20,7 @@ from .qebitcoin import QEBitcoin
from .qefx import QEFX from .qefx import QEFX
from .qetxfinalizer import QETxFinalizer from .qetxfinalizer import QETxFinalizer
from .qeinvoice import QEInvoice from .qeinvoice import QEInvoice
from .qetypes import QEAmount
notification = None notification = None
@ -118,6 +119,8 @@ class ElectrumQmlApplication(QGuiApplication):
qmlRegisterType(QETxFinalizer, 'org.electrum', 1, 0, 'TxFinalizer') qmlRegisterType(QETxFinalizer, 'org.electrum', 1, 0, 'TxFinalizer')
qmlRegisterType(QEInvoice, 'org.electrum', 1, 0, 'Invoice') 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 = QQmlApplicationEngine(parent=self)
self.engine.addImportPath('./qml') 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 import mnemonic
from electrum.util import parse_URI, create_bip21_uri, InvalidBitcoinURI from electrum.util import parse_URI, create_bip21_uri, InvalidBitcoinURI
from .qetypes import QEAmount
class QEBitcoin(QObject): class QEBitcoin(QObject):
def __init__(self, config, parent=None): def __init__(self, config, parent=None):
super().__init__(parent) super().__init__(parent)
@ -121,11 +123,11 @@ class QEBitcoin(QObject):
except InvalidBitcoinURI as e: except InvalidBitcoinURI as e:
return { 'error': str(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): def create_uri(self, address, satoshis, message, timestamp, expiry):
extra_params = {} extra_params = {}
if expiry: if expiry:
extra_params['time'] = str(timestamp) extra_params['time'] = str(timestamp)
extra_params['exp'] = str(expiry) 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.logging import get_logger
from electrum.util import DECIMAL_POINT_DEFAULT from electrum.util import DECIMAL_POINT_DEFAULT
from .qetypes import QEAmount
class QEConfig(QObject): class QEConfig(QObject):
def __init__(self, config, parent=None): def __init__(self, config, parent=None):
super().__init__(parent) super().__init__(parent)
@ -70,7 +72,11 @@ class QEConfig(QObject):
@pyqtSlot('qint64', result=str) @pyqtSlot('qint64', result=str)
@pyqtSlot('qint64', bool, result=str) @pyqtSlot('qint64', bool, result=str)
@pyqtSlot(QEAmount, result=str)
@pyqtSlot(QEAmount, bool, result=str)
def formatSats(self, satoshis, with_unit=False): def formatSats(self, satoshis, with_unit=False):
if isinstance(satoshis, QEAmount):
satoshis = satoshis.satsInt
if with_unit: if with_unit:
return self.config.format_amount_and_units(satoshis) return self.config.format_amount_and_units(satoshis)
else: else:
@ -85,11 +91,11 @@ class QEConfig(QObject):
@pyqtSlot(str, result='qint64') @pyqtSlot(str, result='qint64')
def unitsToSats(self, unitAmount): def unitsToSats(self, unitAmount):
# returns amt in satoshis
try: try:
x = Decimal(unitAmount) x = Decimal(unitAmount)
except: except:
return 0 return 0
# scale it to max allowed precision, make it an int # scale it to max allowed precision, make it an int
max_prec_amount = int(pow(10, self.max_precision()) * x) max_prec_amount = int(pow(10, self.max_precision()) * x)
# if the max precision is simply what unit conversion allows, just return # 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.util import register_callback
from electrum.bitcoin import COIN from electrum.bitcoin import COIN
from .qetypes import QEAmount
class QEFX(QObject): class QEFX(QObject):
def __init__(self, fxthread: FxThread, config: SimpleConfig, parent=None): def __init__(self, fxthread: FxThread, config: SimpleConfig, parent=None):
super().__init__(parent) super().__init__(parent)
@ -88,25 +90,40 @@ class QEFX(QObject):
@pyqtSlot(str, result=str) @pyqtSlot(str, result=str)
@pyqtSlot(str, bool, result=str) @pyqtSlot(str, bool, result=str)
@pyqtSlot(QEAmount, result=str)
@pyqtSlot(QEAmount, bool, result=str)
def fiatValue(self, satoshis, plain=True): def fiatValue(self, satoshis, plain=True):
rate = self.fx.exchange_rate() rate = self.fx.exchange_rate()
try: if isinstance(satoshis, QEAmount):
sd = Decimal(satoshis) satoshis = satoshis.satsInt
if sd == 0: else:
try:
sd = Decimal(satoshis)
if sd == 0:
return ''
except:
return '' return ''
except:
return ''
if plain: if plain:
return self.fx.ccy_amount_str(self.fx.fiat_value(satoshis, rate), False) return self.fx.ccy_amount_str(self.fx.fiat_value(satoshis, rate), False)
else: else:
return self.fx.value_str(satoshis, rate) return self.fx.value_str(satoshis, rate)
@pyqtSlot(str, str, result=str) @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): def fiatValueHistoric(self, satoshis, timestamp, plain=True):
try: if isinstance(satoshis, QEAmount):
sd = Decimal(satoshis) satoshis = satoshis.satsInt
if sd == 0: else:
try:
sd = Decimal(satoshis)
if sd == 0:
return ''
except:
return '' return ''
try:
td = Decimal(timestamp) td = Decimal(timestamp)
if td == 0: if td == 0:
return '' return ''

80
electrum/gui/qml/qeinvoice.py

@ -12,6 +12,7 @@ from electrum.invoices import Invoice, OnchainInvoice, LNInvoice
from electrum.transaction import PartialTxOutput from electrum.transaction import PartialTxOutput
from .qewallet import QEWallet from .qewallet import QEWallet
from .qetypes import QEAmount
class QEInvoice(QObject): class QEInvoice(QObject):
@ -30,28 +31,26 @@ class QEInvoice(QObject):
_invoiceType = Type.Invalid _invoiceType = Type.Invalid
_recipient = '' _recipient = ''
_effectiveInvoice = None _effectiveInvoice = None
_message = ''
_amount = 0
validationError = pyqtSignal([str,str], arguments=['code', 'message']) invoiceChanged = pyqtSignal()
validationWarning = pyqtSignal([str,str], arguments=['code', 'message'])
invoiceSaved = 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): def __init__(self, config, parent=None):
super().__init__(parent) super().__init__(parent)
self.config = config self.config = config
self.clear() self.clear()
invoiceTypeChanged = pyqtSignal() @pyqtProperty(int, notify=invoiceChanged)
@pyqtProperty(int, notify=invoiceTypeChanged)
def invoiceType(self): def invoiceType(self):
return self._invoiceType return self._invoiceType
# not a qt setter, don't let outside set state # not a qt setter, don't let outside set state
def setInvoiceType(self, invoiceType: Type): def setInvoiceType(self, invoiceType: Type):
#if self._invoiceType != invoiceType: self._invoiceType = invoiceType
self._invoiceType = invoiceType
self.invoiceTypeChanged.emit()
walletChanged = pyqtSignal() walletChanged = pyqtSignal()
@pyqtProperty(QEWallet, notify=walletChanged) @pyqtProperty(QEWallet, notify=walletChanged)
@ -77,16 +76,29 @@ class QEInvoice(QObject):
self.validateRecipient(recipient) self.validateRecipient(recipient)
self.recipientChanged.emit() self.recipientChanged.emit()
messageChanged = pyqtSignal() @pyqtProperty(str, notify=invoiceChanged)
@pyqtProperty(str, notify=messageChanged)
def message(self): def message(self):
return self._message return self._effectiveInvoice.message if self._effectiveInvoice else ''
amountChanged = pyqtSignal() @pyqtProperty(QEAmount, notify=invoiceChanged)
@pyqtProperty('quint64', notify=amountChanged)
def amount(self): 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 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() @pyqtSlot()
def clear(self): def clear(self):
@ -94,31 +106,38 @@ class QEInvoice(QObject):
self.invoiceSetsAmount = False self.invoiceSetsAmount = False
self.setInvoiceType(QEInvoice.Type.Invalid) self.setInvoiceType(QEInvoice.Type.Invalid)
self._bip21 = None 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): def setValidAddressOnly(self):
self._logger.debug('setValidAddressOnly') self._logger.debug('setValidAddressOnly')
self.setInvoiceType(QEInvoice.Type.OnchainOnlyAddress) self.setInvoiceType(QEInvoice.Type.OnchainOnlyAddress)
self._effectiveInvoice = None ###TODO self._effectiveInvoice = None ###TODO
self.invoiceChanged.emit()
def setValidOnchainInvoice(self, invoice: OnchainInvoice): def setValidOnchainInvoice(self, invoice: OnchainInvoice):
self._logger.debug('setValidOnchainInvoice') self._logger.debug('setValidOnchainInvoice')
self.setInvoiceType(QEInvoice.Type.OnchainInvoice) self.set_effective_invoice(invoice)
self._amount = invoice.get_amount_sat()
self.amountChanged.emit()
self._message = invoice.message
self.messageChanged.emit()
self._effectiveInvoice = invoice
def setValidLightningInvoice(self, invoice: LNInvoice): def setValidLightningInvoice(self, invoice: LNInvoice):
self._logger.debug('setValidLightningInvoice') self._logger.debug('setValidLightningInvoice')
self.setInvoiceType(QEInvoice.Type.LightningInvoice) self.set_effective_invoice(invoice)
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()
def create_onchain_invoice(self, outputs, message, payment_request, uri): def create_onchain_invoice(self, outputs, message, payment_request, uri):
return self._wallet.wallet.create_invoice( return self._wallet.wallet.create_invoice(
@ -150,6 +169,7 @@ class QEInvoice(QObject):
if ':' not in recipient: if ':' not in recipient:
# address only # address only
self.setValidAddressOnly() self.setValidAddressOnly()
self.validationSuccess.emit()
return return
else: else:
# fallback lightning invoice? # fallback lightning invoice?
@ -194,13 +214,15 @@ class QEInvoice(QObject):
self._logger.debug(outputs) self._logger.debug(outputs)
message = self._bip21['message'] if 'message' in self._bip21 else '' message = self._bip21['message'] if 'message' in self._bip21 else ''
invoice = self.create_onchain_invoice(outputs, message, None, self._bip21) invoice = self.create_onchain_invoice(outputs, message, None, self._bip21)
self._logger.debug(invoice) self._logger.debug(repr(invoice))
self.setValidOnchainInvoice(invoice) self.setValidOnchainInvoice(invoice)
self.validationSuccess.emit()
@pyqtSlot() @pyqtSlot()
def save_invoice(self): def save_invoice(self):
if not self._effectiveInvoice: if not self._effectiveInvoice:
return return
# TODO detect duplicate?
self._wallet.wallet.save_invoice(self._effectiveInvoice) self._wallet.wallet.save_invoice(self._effectiveInvoice)
self.invoiceSaved.emit() 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.util import Satoshis, format_time
from electrum.invoices import Invoice from electrum.invoices import Invoice
from .qetypes import QEAmount
class QEAbstractInvoiceListModel(QAbstractListModel): class QEAbstractInvoiceListModel(QAbstractListModel):
_logger = get_logger(__name__) _logger = get_logger(__name__)
@ -35,6 +37,8 @@ class QEAbstractInvoiceListModel(QAbstractListModel):
return value return value
if isinstance(value, Satoshis): if isinstance(value, Satoshis):
return value.value return value.value
if isinstance(value, QEAmount):
return value
return str(value) return str(value)
def clear(self): def clear(self):
@ -111,7 +115,7 @@ class QEInvoiceListModel(QEAbstractInvoiceListModel):
item = self.wallet.export_invoice(invoice) item = self.wallet.export_invoice(invoice)
item['type'] = invoice.type # 0=onchain, 2=LN item['type'] = invoice.type # 0=onchain, 2=LN
item['date'] = format_time(item['timestamp']) 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: if invoice.type == 0:
item['key'] = invoice.id item['key'] = invoice.id
elif invoice.type == 2: elif invoice.type == 2:
@ -136,7 +140,7 @@ class QERequestListModel(QEAbstractInvoiceListModel):
item['key'] = self.wallet.get_key_for_receive_request(req) item['key'] = self.wallet.get_key_for_receive_request(req)
item['type'] = req.type # 0=onchain, 2=LN item['type'] = req.type # 0=onchain, 2=LN
item['date'] = format_time(item['timestamp']) item['date'] = format_time(item['timestamp'])
item['amount'] = req.get_amount_sat() item['amount'] = QEAmount(amount_sat=req.get_amount_sat())
return item 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.logging import get_logger
from electrum.util import Satoshis, TxMinedInfo from electrum.util import Satoshis, TxMinedInfo
from .qetypes import QEAmount
class QETransactionListModel(QAbstractListModel): class QETransactionListModel(QAbstractListModel):
def __init__(self, wallet, parent=None): def __init__(self, wallet, parent=None):
super().__init__(parent) super().__init__(parent)
@ -36,6 +38,8 @@ class QETransactionListModel(QAbstractListModel):
return value return value
if isinstance(value, Satoshis): if isinstance(value, Satoshis):
return value.value return value.value
if isinstance(value, QEAmount):
return value
return str(value) return str(value)
def clear(self): def clear(self):
@ -48,6 +52,9 @@ class QETransactionListModel(QAbstractListModel):
for output in item['outputs']: for output in item['outputs']:
output['value'] = output['value'].value 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 # newly arriving txs have no (block) timestamp
# TODO? # TODO?
if not item['timestamp']: 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