add QERequestListModel and hook up the gui in Receive tab
This commit is contained in:
@@ -9,24 +9,196 @@ Pane {
|
|||||||
id: rootItem
|
id: rootItem
|
||||||
visible: Daemon.currentWallet !== undefined
|
visible: Daemon.currentWallet !== undefined
|
||||||
|
|
||||||
ColumnLayout {
|
GridLayout {
|
||||||
|
id: form
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: 20
|
rowSpacing: 10
|
||||||
|
columnSpacing: 10
|
||||||
|
columns: 3
|
||||||
|
|
||||||
Image {
|
Label {
|
||||||
id: img
|
text: qsTr('Message')
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField {
|
TextField {
|
||||||
id: text
|
id: message
|
||||||
|
onTextChanged: img.source = 'image://qrgen/' + text
|
||||||
|
Layout.columnSpan: 2
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr('Requested Amount')
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.preferredWidth: 50 // trigger wordwrap
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: amount
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.rowSpan: 3
|
||||||
|
width: img.width
|
||||||
|
height: img.height
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: img
|
||||||
|
cache: false
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
left: parent.left
|
||||||
|
}
|
||||||
|
source: 'image://qrgen/test'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr('Expires after')
|
||||||
|
Layout.fillWidth: false
|
||||||
|
}
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
id: expires
|
||||||
|
textRole: 'text'
|
||||||
|
valueRole: 'value'
|
||||||
|
model: ListModel {
|
||||||
|
id: expiresmodel
|
||||||
|
Component.onCompleted: {
|
||||||
|
// we need to fill the model like this, as ListElement can't evaluate script
|
||||||
|
expiresmodel.append({'text': qsTr('Never'), 'value': 0})
|
||||||
|
expiresmodel.append({'text': qsTr('10 minutes'), 'value': 10*60})
|
||||||
|
expiresmodel.append({'text': qsTr('1 hour'), 'value': 60*60})
|
||||||
|
expiresmodel.append({'text': qsTr('1 day'), 'value': 24*60*60})
|
||||||
|
expiresmodel.append({'text': qsTr('1 week'), 'value': 7*24*60*60})
|
||||||
|
expires.currentIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
text: 'generate'
|
Layout.columnSpan: 2
|
||||||
|
text: qsTr('Create Request')
|
||||||
onClicked: {
|
onClicked: {
|
||||||
img.source = 'image://qrgen/' + text.text
|
var a = parseFloat(amount.text)
|
||||||
|
Daemon.currentWallet.create_invoice(a, message.text, expires.currentValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Frame {
|
||||||
|
clip: true
|
||||||
|
verticalPadding: 0
|
||||||
|
horizontalPadding: 0
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: form.bottom
|
||||||
|
topMargin: constants.paddingXLarge
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
bottom: parent.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Qt.darker(Material.background, 1.25)
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
model: Daemon.currentWallet.requestModel
|
||||||
|
headerPositioning: ListView.OverlayHeader
|
||||||
|
|
||||||
|
header: Item {
|
||||||
|
z: 1
|
||||||
|
height: hitem.height
|
||||||
|
width: ListView.view.width
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: hitem
|
||||||
|
color: Qt.lighter(Material.background, 1.25)
|
||||||
|
}
|
||||||
|
RowLayout {
|
||||||
|
id: hitem
|
||||||
|
width: parent.width
|
||||||
|
Label {
|
||||||
|
text: qsTr('Receive queue')
|
||||||
|
font.pixelSize: constants.fontSizeXLarge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
z: -1
|
||||||
|
height: item.height
|
||||||
|
width: ListView.view.width
|
||||||
|
GridLayout {
|
||||||
|
id: item
|
||||||
|
columns: 5
|
||||||
|
Image {
|
||||||
|
Layout.rowSpan: 2
|
||||||
|
Layout.preferredWidth: 32
|
||||||
|
Layout.preferredHeight: 32
|
||||||
|
source: model.type == 0 ? "../../icons/bitcoin.png" : "../../icons/lightning.png"
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.columnSpan: 2
|
||||||
|
text: model.message
|
||||||
|
font.pixelSize: constants.fontSizeLarge
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr('Amount: ')
|
||||||
|
font.pixelSize: constants.fontSizeSmall
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: model.amount
|
||||||
|
font.pixelSize: constants.fontSizeSmall
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr('Timestamp: ')
|
||||||
|
font.pixelSize: constants.fontSizeSmall
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: model.timestamp
|
||||||
|
font.pixelSize: constants.fontSizeSmall
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr('Status: ')
|
||||||
|
font.pixelSize: constants.fontSizeSmall
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: model.status
|
||||||
|
font.pixelSize: constants.fontSizeSmall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
add: Transition {
|
||||||
|
NumberAnimation { properties: 'y'; from: -50; duration: 300 }
|
||||||
|
NumberAnimation { properties: 'opacity'; from: 0; to: 1.0; duration: 700 }
|
||||||
|
}
|
||||||
|
addDisplaced: Transition {
|
||||||
|
NumberAnimation { properties: 'y'; duration: 100 }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Daemon.currentWallet
|
||||||
|
function onRequestCreateSuccess() {
|
||||||
|
message.text = ''
|
||||||
|
amount.text = ''
|
||||||
|
}
|
||||||
|
function onRequestCreateError(error) {
|
||||||
|
console.log(error)
|
||||||
|
var dialog = app.messageDialog.createObject(app, {'text': error})
|
||||||
|
dialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
76
electrum/gui/qml/qerequestlistmodel.py
Normal file
76
electrum/gui/qml/qerequestlistmodel.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
||||||
|
from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex
|
||||||
|
|
||||||
|
from electrum.logging import get_logger
|
||||||
|
from electrum.util import Satoshis, format_time
|
||||||
|
from electrum.invoices import Invoice
|
||||||
|
|
||||||
|
class QERequestListModel(QAbstractListModel):
|
||||||
|
def __init__(self, wallet, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.wallet = wallet
|
||||||
|
self.requests = []
|
||||||
|
|
||||||
|
_logger = get_logger(__name__)
|
||||||
|
|
||||||
|
# define listmodel rolemap
|
||||||
|
_ROLE_NAMES=('type','timestamp','message','amount','status')
|
||||||
|
_ROLE_KEYS = range(Qt.UserRole + 1, Qt.UserRole + 1 + len(_ROLE_NAMES))
|
||||||
|
_ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES]))
|
||||||
|
|
||||||
|
def rowCount(self, index):
|
||||||
|
return len(self.requests)
|
||||||
|
|
||||||
|
def roleNames(self):
|
||||||
|
return self._ROLE_MAP
|
||||||
|
|
||||||
|
def data(self, index, role):
|
||||||
|
request = self.requests[index.row()]
|
||||||
|
role_index = role - (Qt.UserRole + 1)
|
||||||
|
value = request[self._ROLE_NAMES[role_index]]
|
||||||
|
if isinstance(value, bool) or isinstance(value, list) or isinstance(value, int) or value is None:
|
||||||
|
return value
|
||||||
|
if isinstance(value, Satoshis):
|
||||||
|
return value.value
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.beginResetModel()
|
||||||
|
self.requests = []
|
||||||
|
self.endResetModel()
|
||||||
|
|
||||||
|
def request_to_model(self, req: Invoice):
|
||||||
|
item = {}
|
||||||
|
key = self.wallet.get_key_for_receive_request(req) # (verified) address for onchain, rhash for LN
|
||||||
|
status = self.wallet.get_request_status(key)
|
||||||
|
item['status'] = req.get_status_str(status)
|
||||||
|
item['type'] = req.type # 0=onchain, 2=LN
|
||||||
|
timestamp = req.time
|
||||||
|
item['timestamp'] = format_time(timestamp)
|
||||||
|
item['amount'] = req.get_amount_sat()
|
||||||
|
item['message'] = req.message
|
||||||
|
|
||||||
|
#amount_str = self.parent.format_amount(amount) if amount else ""
|
||||||
|
|
||||||
|
return item
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def init_model(self):
|
||||||
|
requests = []
|
||||||
|
for req in self.wallet.get_unpaid_requests():
|
||||||
|
item = self.request_to_model(req)
|
||||||
|
self._logger.debug(str(item))
|
||||||
|
requests.append(item)
|
||||||
|
|
||||||
|
self.clear()
|
||||||
|
self.beginInsertRows(QModelIndex(), 0, len(self.requests) - 1)
|
||||||
|
self.requests = requests
|
||||||
|
self.endInsertRows()
|
||||||
|
|
||||||
|
def add_request(self, request: Invoice):
|
||||||
|
item = self.request_to_model(request)
|
||||||
|
self._logger.debug(str(item))
|
||||||
|
|
||||||
|
self.beginInsertRows(QModelIndex(), 0, 0)
|
||||||
|
self.requests.insert(0, item)
|
||||||
|
self.endInsertRows()
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
|
||||||
from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex, QByteArray
|
from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex, QByteArray
|
||||||
|
|
||||||
from electrum.util import register_callback, Satoshis
|
from typing import Optional, TYPE_CHECKING, Sequence, List, Union
|
||||||
|
|
||||||
|
from electrum.i18n import _
|
||||||
|
from electrum.util import register_callback, Satoshis, format_time
|
||||||
from electrum.logging import get_logger
|
from electrum.logging import get_logger
|
||||||
from electrum.wallet import Wallet, Abstract_Wallet
|
from electrum.wallet import Wallet, Abstract_Wallet
|
||||||
from electrum import bitcoin
|
from electrum import bitcoin
|
||||||
from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput
|
from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput
|
||||||
from electrum.invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_UNCONFIRMED, PR_TYPE_ONCHAIN, PR_TYPE_LN
|
from electrum.invoices import (Invoice, InvoiceError, PR_TYPE_ONCHAIN, PR_TYPE_LN,
|
||||||
|
PR_DEFAULT_EXPIRATION_WHEN_CREATING, PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_UNCONFIRMED, PR_TYPE_ONCHAIN, PR_TYPE_LN)
|
||||||
|
|
||||||
|
from .qerequestlistmodel import QERequestListModel
|
||||||
|
|
||||||
class QETransactionListModel(QAbstractListModel):
|
class QETransactionListModel(QAbstractListModel):
|
||||||
def __init__(self, wallet, parent=None):
|
def __init__(self, wallet, parent=None):
|
||||||
@@ -130,7 +136,11 @@ class QEWallet(QObject):
|
|||||||
self.wallet = wallet
|
self.wallet = wallet
|
||||||
self._historyModel = QETransactionListModel(wallet)
|
self._historyModel = QETransactionListModel(wallet)
|
||||||
self._addressModel = QEAddressListModel(wallet)
|
self._addressModel = QEAddressListModel(wallet)
|
||||||
|
self._requestModel = QERequestListModel(wallet)
|
||||||
|
|
||||||
self._historyModel.init_model()
|
self._historyModel.init_model()
|
||||||
|
self._requestModel.init_model()
|
||||||
|
|
||||||
register_callback(self.on_request_status, ['request_status'])
|
register_callback(self.on_request_status, ['request_status'])
|
||||||
register_callback(self.on_status, ['status'])
|
register_callback(self.on_status, ['status'])
|
||||||
|
|
||||||
@@ -138,6 +148,9 @@ class QEWallet(QObject):
|
|||||||
|
|
||||||
dataChanged = pyqtSignal() # dummy to silence warnings
|
dataChanged = pyqtSignal() # dummy to silence warnings
|
||||||
|
|
||||||
|
requestCreateSuccess = pyqtSignal()
|
||||||
|
requestCreateError = pyqtSignal([str], arguments=['error'])
|
||||||
|
|
||||||
requestStatus = pyqtSignal()
|
requestStatus = pyqtSignal()
|
||||||
def on_request_status(self, event, *args):
|
def on_request_status(self, event, *args):
|
||||||
self._logger.debug(str(event))
|
self._logger.debug(str(event))
|
||||||
@@ -153,6 +166,11 @@ class QEWallet(QObject):
|
|||||||
def addressModel(self):
|
def addressModel(self):
|
||||||
return self._addressModel
|
return self._addressModel
|
||||||
|
|
||||||
|
requestModelChanged = pyqtSignal()
|
||||||
|
@pyqtProperty(QERequestListModel, notify=requestModelChanged)
|
||||||
|
def requestModel(self):
|
||||||
|
return self._requestModel
|
||||||
|
|
||||||
@pyqtProperty('QString', notify=dataChanged)
|
@pyqtProperty('QString', notify=dataChanged)
|
||||||
def txinType(self):
|
def txinType(self):
|
||||||
return self.wallet.get_txin_type(self.wallet.dummy_address())
|
return self.wallet.get_txin_type(self.wallet.dummy_address())
|
||||||
@@ -218,3 +236,65 @@ class QEWallet(QObject):
|
|||||||
outputs = [PartialTxOutput.from_address_and_value(address, amount)]
|
outputs = [PartialTxOutput.from_address_and_value(address, amount)]
|
||||||
tx = self.wallet.make_unsigned_transaction(coins=coins,outputs=outputs)
|
tx = self.wallet.make_unsigned_transaction(coins=coins,outputs=outputs)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def create_bitcoin_request(self, amount: int, message: str, expiration: int) -> Optional[str]:
|
||||||
|
addr = self.wallet.get_unused_address()
|
||||||
|
if addr is None:
|
||||||
|
# TODO implement
|
||||||
|
return
|
||||||
|
#if not self.wallet.is_deterministic(): # imported wallet
|
||||||
|
#msg = [
|
||||||
|
#_('No more addresses in your wallet.'), ' ',
|
||||||
|
#_('You are using a non-deterministic wallet, which cannot create new addresses.'), ' ',
|
||||||
|
#_('If you want to create new addresses, use a deterministic wallet instead.'), '\n\n',
|
||||||
|
#_('Creating a new payment request will reuse one of your addresses and overwrite an existing request. Continue anyway?'),
|
||||||
|
#]
|
||||||
|
#if not self.question(''.join(msg)):
|
||||||
|
#return
|
||||||
|
#addr = self.wallet.get_receiving_address()
|
||||||
|
#else: # deterministic wallet
|
||||||
|
#if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")):
|
||||||
|
#return
|
||||||
|
#addr = self.wallet.create_new_address(False)
|
||||||
|
req = self.wallet.make_payment_request(addr, amount, message, expiration)
|
||||||
|
try:
|
||||||
|
self.wallet.add_payment_request(req)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.exception('Error adding payment request')
|
||||||
|
self.requestCreateError.emit(_('Error adding payment request') + ':\n' + repr(e))
|
||||||
|
else:
|
||||||
|
# TODO: check this flow. Only if alias is defined in config. OpenAlias?
|
||||||
|
pass
|
||||||
|
#self.sign_payment_request(addr)
|
||||||
|
self._requestModel.add_request(req)
|
||||||
|
return addr
|
||||||
|
|
||||||
|
@pyqtSlot(int, 'QString', int)
|
||||||
|
def create_invoice(self, amount: int, message: str, expiration: int, is_lightning: bool = False):
|
||||||
|
expiry = expiration #TODO: self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING)
|
||||||
|
try:
|
||||||
|
if is_lightning:
|
||||||
|
if not self.wallet.lnworker.channels:
|
||||||
|
#self.show_error(_("You need to open a Lightning channel first."))
|
||||||
|
self.requestCreateError.emit(_("You need to open a Lightning channel first."))
|
||||||
|
return
|
||||||
|
# TODO maybe show a warning if amount exceeds lnworker.num_sats_can_receive (as in kivy)
|
||||||
|
key = self.wallet.lnworker.add_request(amount, message, expiry)
|
||||||
|
else:
|
||||||
|
key = self.create_bitcoin_request(amount, message, expiry)
|
||||||
|
if not key:
|
||||||
|
return
|
||||||
|
#self.address_list.update()
|
||||||
|
self._addressModel.init_model()
|
||||||
|
except InvoiceError as e:
|
||||||
|
self.requestCreateError.emit(_('Error creating payment request') + ':\n' + str(e))
|
||||||
|
return
|
||||||
|
|
||||||
|
assert key is not None
|
||||||
|
self.requestCreateSuccess.emit()
|
||||||
|
|
||||||
|
# TODO:copy to clipboard
|
||||||
|
#r = self.wallet.get_request(key)
|
||||||
|
#content = r.invoice if r.is_lightning() else r.get_address()
|
||||||
|
#title = _('Invoice') if is_lightning else _('Address')
|
||||||
|
#self.do_copy(content, title=title)
|
||||||
|
|||||||
Reference in New Issue
Block a user