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