commit 8010123c0858bf9735cdfc6637899799ea6891f2
parent 336cf81a6def88ae803c11926c93b56de2bed1b8
Author: ThomasV <thomasv@electrum.org>
Date: Wed, 21 Aug 2019 18:25:36 +0200
Display and refresh the status of incoming payment requests:
- All requests have an expiration date
- Paid requests are automatically removed from the list
- Unpaid, unconfirmed and expired requests are displayed
- Fix a bug in get_payment_status, conf was off by one
Diffstat:
9 files changed, 137 insertions(+), 124 deletions(-)
diff --git a/electrum/commands.py b/electrum/commands.py
@@ -670,14 +670,9 @@ class Commands:
return decrypted.decode('utf-8')
def _format_request(self, out):
- pr_str = {
- PR_UNKNOWN: 'Unknown',
- PR_UNPAID: 'Pending',
- PR_PAID: 'Paid',
- PR_EXPIRED: 'Expired',
- }
+ from .util import get_request_status
out['amount_BTC'] = format_satoshis(out.get('amount'))
- out['status'] = pr_str[out.get('status', PR_UNKNOWN)]
+ out['status'] = get_request_status(out)
return out
@command('w')
@@ -850,9 +845,9 @@ class Commands:
return await self.lnworker._pay(invoice, attempts=attempts)
@command('wn')
- async def addinvoice(self, requested_amount, message):
+ async def addinvoice(self, requested_amount, message, expiration=3600):
# using requested_amount because it is documented in param_descriptions
- payment_hash = await self.lnworker._add_invoice_coro(satoshis(requested_amount), message)
+ payment_hash = await self.lnworker._add_invoice_coro(satoshis(requested_amount), message, expiration)
invoice, direction, is_paid = self.lnworker.invoices[bh2u(payment_hash)]
return invoice
diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
@@ -2,7 +2,6 @@ import asyncio
from weakref import ref
from decimal import Decimal
import re
-import datetime
import threading
import traceback, sys
from enum import Enum, auto
@@ -27,7 +26,7 @@ from electrum.util import profiler, parse_URI, format_time, InvalidPassword, Not
from electrum import bitcoin, constants
from electrum.transaction import TxOutput, Transaction, tx_from_str
from electrum.util import send_exception_to_crash_reporter, parse_URI, InvalidBitcoinURI
-from electrum.util import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED, TxMinedInfo, age
+from electrum.util import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED, TxMinedInfo, get_request_status, pr_expiration_values
from electrum.plugin import run_hook
from electrum.wallet import InternalAddressCorruption
from electrum import simple_config
@@ -404,12 +403,14 @@ class SendScreen(CScreen):
class ReceiveScreen(CScreen):
kvname = 'receive'
- cards = {}
def __init__(self, **kwargs):
super(ReceiveScreen, self).__init__(**kwargs)
self.menu_actions = [(_('Show'), self.do_show), (_('Delete'), self.do_delete)]
- self.expiration = self.app.electrum_config.get('request_expiration', 3600) # 1 hour
+ Clock.schedule_interval(lambda dt: self.update(), 5)
+
+ def expiry(self):
+ return self.app.electrum_config.get('request_expiry', 3600) # 1 hour
def clear(self):
self.screen.address = ''
@@ -452,9 +453,8 @@ class ReceiveScreen(CScreen):
amount = self.screen.amount
amount = self.app.get_amount(amount) if amount else 0
message = self.screen.message
- expiration = self.expiration
if lightning:
- payment_hash = self.app.wallet.lnworker.add_invoice(amount, message)
+ payment_hash = self.app.wallet.lnworker.add_invoice(amount, message, self.expiry())
request, direction, is_paid = self.app.wallet.lnworker.invoices.get(payment_hash.hex())
key = payment_hash.hex()
else:
@@ -463,40 +463,37 @@ class ReceiveScreen(CScreen):
self.app.show_info(_('No address available. Please remove some of your pending requests.'))
return
self.screen.address = addr
- req = self.app.wallet.make_payment_request(addr, amount, message, expiration)
+ req = self.app.wallet.make_payment_request(addr, amount, message, self.expiry())
self.app.wallet.add_payment_request(req, self.app.electrum_config)
key = addr
+ self.clear()
self.update()
self.app.show_request(lightning, key)
def get_card(self, req):
is_lightning = req.get('lightning', False)
- status = req['status']
- #if status != PR_UNPAID:
- # continue
if not is_lightning:
address = req['address']
key = address
else:
key = req['rhash']
address = req['invoice']
- timestamp = req.get('time', 0)
amount = req.get('amount')
description = req.get('memo', '')
- ci = self.cards.get(key)
- if ci is None:
- ci = {}
- ci['address'] = address
- ci['is_lightning'] = is_lightning
- ci['key'] = key
- ci['screen'] = self
- self.cards[key] = ci
+ ci = {}
+ ci['screen'] = self
+ ci['address'] = address
+ ci['is_lightning'] = is_lightning
+ ci['key'] = key
ci['amount'] = self.app.format_amount_and_units(amount) if amount else ''
ci['memo'] = description
- ci['status'] = age(timestamp)
+ ci['status'] = get_request_status(req)
+ ci['is_expired'] = req['status'] == PR_EXPIRED
return ci
def update(self):
+ if not self.loaded:
+ return
_list = self.app.wallet.get_sorted_requests(self.app.electrum_config)
requests_container = self.screen.ids.requests_container
requests_container.data = [self.get_card(item) for item in _list if item.get('status') != PR_PAID]
@@ -507,16 +504,9 @@ class ReceiveScreen(CScreen):
def expiration_dialog(self, obj):
from .dialogs.choice_dialog import ChoiceDialog
- choices = {
- 10*60: _('10 minutes'),
- 60*60: _('1 hour'),
- 24*60*60: _('1 day'),
- 7*24*60*60: _('1 week')
- }
def callback(c):
- self.expiration = c
- self.app.electrum_config.set_key('request_expiration', c)
- d = ChoiceDialog(_('Expiration date'), choices, self.expiration, callback)
+ self.app.electrum_config.set_key('request_expiry', c)
+ d = ChoiceDialog(_('Expiration date'), pr_expiration_values, self.expiry(), callback)
d.open()
def do_delete(self, req):
diff --git a/electrum/gui/kivy/uix/ui_screens/receive.kv b/electrum/gui/kivy/uix/ui_screens/receive.kv
@@ -13,29 +13,22 @@
valign: 'top'
<RequestItem@CardItem>
+ is_expired: False
address: ''
memo: ''
amount: ''
status: ''
- date: ''
- icon: 'atlas://electrum/gui/kivy/theming/light/important'
- Image:
- id: icon
- source: root.icon
- size_hint: None, 1
- width: self.height *.54
- mipmap: True
BoxLayout:
spacing: '8dp'
height: '32dp'
orientation: 'vertical'
Widget
RequestLabel:
- text: root.address
+ text: root.memo
shorten: True
Widget
RequestLabel:
- text: root.memo
+ text: root.address
color: .699, .699, .699, 1
font_size: '13sp'
shorten: True
@@ -54,7 +47,7 @@
text: root.status
halign: 'right'
font_size: '13sp'
- color: .699, .699, .699, 1
+ color: (1., .2, .2, 1) if root.is_expired else (.7, .7, .7, 1)
Widget
<RequestRecycleView>:
@@ -75,7 +68,6 @@ ReceiveScreen:
message: ''
status: ''
is_lightning: False
- show_list: True
BoxLayout
padding: '12dp', '12dp', '12dp', '12dp'
@@ -100,7 +92,6 @@ ReceiveScreen:
text: _('Lightning') if root.is_lightning else (s.address if s.address else _('Bitcoin Address'))
shorten: True
on_release: root.is_lightning = not root.is_lightning
- #on_release: Clock.schedule_once(lambda dt: app.addresses_dialog(s))
CardSeparator:
opacity: message_selection.opacity
color: blue_bottom.foreground_color
@@ -144,7 +135,7 @@ ReceiveScreen:
icon: 'atlas://electrum/gui/kivy/theming/light/list'
size_hint: 0.5, None
height: '48dp'
- on_release: root.show_list = not root.show_list
+ on_release: Clock.schedule_once(lambda dt: app.addresses_dialog())
IconButton:
icon: 'atlas://electrum/gui/kivy/theming/light/clock1'
size_hint: 0.5, None
@@ -166,5 +157,3 @@ ReceiveScreen:
id: requests_container
scroll_type: ['bars', 'content']
bar_width: '25dp'
- opacity: 1 if root.show_list else 0
- disabled: not root.show_list
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
@@ -73,6 +73,7 @@ from electrum.exchange_rate import FxThread
from electrum.simple_config import SimpleConfig
from electrum.logging import Logger
from electrum.paymentrequest import PR_PAID
+from electrum.util import pr_expiration_values
from .exception_window import Exception_Hook
from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit
@@ -83,7 +84,7 @@ from .fee_slider import FeeSlider
from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialog,
WindowModalDialog, ChoicesLayout, HelpLabel, FromList, Buttons,
OkButton, InfoButton, WWLabel, TaskThread, CancelButton,
- CloseButton, HelpButton, MessageBoxMixin, EnterButton, expiration_values,
+ CloseButton, HelpButton, MessageBoxMixin, EnterButton,
ButtonsLineEdit, CopyCloseButton, import_meta_gui, export_meta_gui,
filename_field, address_field, char_width_in_lineedit, webopen)
from .util import ButtonsTextEdit
@@ -753,6 +754,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
return fileName
def timer_actions(self):
+ self.request_list.refresh_status()
# Note this runs in the GUI thread
if self.need_update.is_set():
self.need_update.clear()
@@ -945,9 +947,20 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.connect_fields(self, self.receive_amount_e, self.fiat_receive_e, None)
self.expires_combo = QComboBox()
- self.expires_combo.addItems([i[0] for i in expiration_values])
- self.expires_combo.setCurrentIndex(3)
+ evl = sorted(pr_expiration_values.items())
+ evl_keys = [i[0] for i in evl]
+ evl_values = [i[1] for i in evl]
+ default_expiry = self.config.get('request_expiry', 3600)
+ try:
+ i = evl_keys.index(default_expiry)
+ except ValueError:
+ i = 0
+ self.expires_combo.addItems(evl_values)
+ self.expires_combo.setCurrentIndex(i)
self.expires_combo.setFixedWidth(self.receive_amount_e.width())
+ def on_expiry(i):
+ self.config.set_key('request_expiry', evl_keys[i])
+ self.expires_combo.currentIndexChanged.connect(on_expiry)
msg = ' '.join([
_('Expiration date of your request.'),
_('This information is seen by the recipient if you send them a signed payment request.'),
@@ -1057,13 +1070,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def create_invoice(self, is_lightning):
amount = self.receive_amount_e.get_amount()
message = self.receive_message_e.text()
- i = self.expires_combo.currentIndex()
- expiration = list(map(lambda x: x[1], expiration_values))[i]
+ expiry = self.config.get('request_expiry', 3600)
if is_lightning:
- payment_hash = self.wallet.lnworker.add_invoice(amount, message)
+ payment_hash = self.wallet.lnworker.add_invoice(amount, message, expiry)
key = bh2u(payment_hash)
else:
- key = self.create_bitcoin_request(amount, message, expiration)
+ key = self.create_bitcoin_request(amount, message, expiry)
self.address_list.update()
self.request_list.update()
self.request_list.select_key(key)
diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py
@@ -30,7 +30,7 @@ from PyQt5.QtWidgets import QMenu, QHeaderView
from PyQt5.QtCore import Qt, QItemSelectionModel
from electrum.i18n import _
-from electrum.util import format_time, age
+from electrum.util import format_time, age, get_request_status
from electrum.util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT, pr_tooltips
from electrum.lnutil import SENT, RECEIVED
from electrum.plugin import run_hook
@@ -85,20 +85,28 @@ class RequestList(MyTreeView):
item = self.model().itemFromIndex(idx.sibling(idx.row(), self.Columns.DATE))
request_type = item.data(ROLE_REQUEST_TYPE)
key = item.data(ROLE_RHASH_OR_ADDR)
- if request_type == REQUEST_TYPE_BITCOIN:
- req = self.wallet.receive_requests.get(key)
- if req is None:
- self.update()
- return
- req = self.wallet.get_request_URI(key)
- elif request_type == REQUEST_TYPE_LN:
- req, direction, is_paid = self.wallet.lnworker.invoices.get(key) or (None, None, None)
- if req is None:
- self.update()
- return
- else:
- raise Exception(f"unknown request type: {request_type}")
- self.parent.receive_address_e.setText(req)
+ is_lightning = request_type == REQUEST_TYPE_LN
+ req = self.wallet.get_request(key, is_lightning)
+ if req is None:
+ self.update()
+ return
+ text = req.get('invoice') if is_lightning else req.get('URI')
+ self.parent.receive_address_e.setText(text)
+
+ def refresh_status(self):
+ m = self.model()
+ for r in range(m.rowCount()):
+ idx = m.index(r, self.Columns.STATUS)
+ date_idx = idx.sibling(idx.row(), self.Columns.DATE)
+ date_item = m.itemFromIndex(date_idx)
+ status_item = m.itemFromIndex(idx)
+ key = date_item.data(ROLE_RHASH_OR_ADDR)
+ is_lightning = date_item.data(ROLE_REQUEST_TYPE) == REQUEST_TYPE_LN
+ req = self.wallet.get_request(key, is_lightning)
+ if req:
+ status_str = get_request_status(req)
+ status_item.setText(status_str)
+ status_item.setIcon(read_QIcon(pr_icons.get(req['status'])))
def update(self):
self.wallet = self.parent.wallet
@@ -116,7 +124,8 @@ class RequestList(MyTreeView):
message = req['memo']
date = format_time(timestamp)
amount_str = self.parent.format_amount(amount) if amount else ""
- labels = [date, message, amount_str, pr_tooltips.get(status,'')]
+ status_str = get_request_status(req)
+ labels = [date, message, amount_str, status_str]
items = [QStandardItem(e) for e in labels]
self.set_editability(items)
items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE)
diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py
@@ -45,16 +45,10 @@ pr_icons = {
PR_UNPAID:"unpaid.png",
PR_PAID:"confirmed.png",
PR_EXPIRED:"expired.png",
- PR_INFLIGHT:"lightning.png",
+ PR_INFLIGHT:"unconfirmed.png",
}
-expiration_values = [
- (_('1 hour'), 60*60),
- (_('1 day'), 24*60*60),
- (_('1 week'), 7*24*60*60),
- (_('Never'), None)
-]
class EnterButton(QPushButton):
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
@@ -868,8 +868,8 @@ class LNWallet(LNWorker):
raise PaymentFailure(_("No path found"))
return route
- def add_invoice(self, amount_sat, message):
- coro = self._add_invoice_coro(amount_sat, message)
+ def add_invoice(self, amount_sat, message, expiry):
+ coro = self._add_invoice_coro(amount_sat, message, expiry)
fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
try:
return fut.result(timeout=5)
@@ -877,7 +877,7 @@ class LNWallet(LNWorker):
raise Exception(_("add_invoice timed out"))
@log_exceptions
- async def _add_invoice_coro(self, amount_sat, message):
+ async def _add_invoice_coro(self, amount_sat, message, expiry):
payment_preimage = os.urandom(32)
payment_hash = sha256(payment_preimage)
amount_btc = amount_sat/Decimal(COIN) if amount_sat else None
@@ -887,7 +887,8 @@ class LNWallet(LNWorker):
"Other clients will likely not be able to send to us.")
invoice = lnencode(LnAddr(payment_hash, amount_btc,
tags=[('d', message),
- ('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE)]
+ ('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE),
+ ('x', expiry)]
+ routing_hints),
self.node_keypair.privkey)
self.save_invoice(payment_hash, invoice, RECEIVED, is_paid=False)
@@ -933,26 +934,31 @@ class LNWallet(LNWorker):
except KeyError as e:
raise UnknownPaymentHash(payment_hash) from e
+ def get_request(self, key):
+ invoice, direction, is_paid = self.invoices[key]
+ status = self.get_invoice_status(key)
+ lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
+ amount_sat = lnaddr.amount*COIN if lnaddr.amount else None
+ description = lnaddr.get_description()
+ timestamp = lnaddr.date
+ return {
+ 'lightning':True,
+ 'status':status,
+ 'amount':amount_sat,
+ 'time':timestamp,
+ 'exp':lnaddr.get_expiry(),
+ 'memo':description,
+ 'rhash':key,
+ 'invoice': invoice
+ }
+
def get_invoices(self):
items = self.invoices.items()
out = []
for key, (invoice, direction, is_paid) in items:
if direction == SENT:
continue
- status = self.get_invoice_status(key)
- lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
- amount_sat = lnaddr.amount*COIN if lnaddr.amount else None
- description = lnaddr.get_description()
- timestamp = lnaddr.date
- out.append({
- 'lightning':True,
- 'status':status,
- 'amount':amount_sat,
- 'time':timestamp,
- 'memo':description,
- 'rhash':key,
- 'invoice': invoice
- })
+ out.append(self.get_request(key))
return out
async def _calc_routing_hints_for_invoice(self, amount_sat):
diff --git a/electrum/util.py b/electrum/util.py
@@ -78,16 +78,34 @@ PR_UNPAID = 0
PR_EXPIRED = 1
PR_UNKNOWN = 2 # sent but not propagated
PR_PAID = 3 # send and propagated
-PR_INFLIGHT = 4 # lightning
+PR_INFLIGHT = 4 # unconfirmed
pr_tooltips = {
PR_UNPAID:_('Pending'),
PR_PAID:_('Paid'),
PR_UNKNOWN:_('Unknown'),
PR_EXPIRED:_('Expired'),
- PR_INFLIGHT:_('Inflight')
+ PR_INFLIGHT:_('Paid (unconfirmed)')
}
+pr_expiration_values = {
+ 10*60: _('10 minutes'),
+ 60*60: _('1 hour'),
+ 24*60*60: _('1 day'),
+ 7*24*60*60: _('1 week')
+}
+
+def get_request_status(req):
+ status = req['status']
+ status_str = pr_tooltips[status]
+ if status == PR_UNPAID:
+ if req.get('exp'):
+ expiration = req['exp'] + req['time']
+ status_str = _('Expires') + ' ' + age(expiration, include_seconds=True)
+ else:
+ status_str = _('Pending')
+ return status_str
+
class UnknownBaseUnit(Exception): pass
@@ -638,22 +656,11 @@ def time_difference(distance_in_time, include_seconds):
distance_in_seconds = int(round(abs(distance_in_time.days * 86400 + distance_in_time.seconds)))
distance_in_minutes = int(round(distance_in_seconds/60))
- if distance_in_minutes <= 1:
+ if distance_in_minutes == 0:
if include_seconds:
- for remainder in [5, 10, 20]:
- if distance_in_seconds < remainder:
- return "less than %s seconds" % remainder
- if distance_in_seconds < 40:
- return "half a minute"
- elif distance_in_seconds < 60:
- return "less than a minute"
- else:
- return "1 minute"
+ return "%s seconds" % distance_in_seconds
else:
- if distance_in_minutes == 0:
- return "less than a minute"
- else:
- return "1 minute"
+ return "less than a minute"
elif distance_in_minutes < 45:
return "%s minutes" % distance_in_minutes
elif distance_in_minutes < 90:
diff --git a/electrum/wallet.py b/electrum/wallet.py
@@ -46,6 +46,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler,
WalletFileException, BitcoinException,
InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
+from .util import age
from .simple_config import get_config
from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script,
is_minikey, relayfee, dust_threshold)
@@ -59,7 +60,7 @@ from .transaction import Transaction, TxOutput, TxOutputHwInfo
from .plugin import run_hook
from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE)
-from .paymentrequest import (PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED,
+from .paymentrequest import (PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT,
InvoiceStore)
from .contacts import Contacts
from .interface import NetworkException
@@ -1204,7 +1205,7 @@ class Abstract_Wallet(AddressSynchronizer):
txid, n = txo.split(':')
info = self.db.get_verified_tx(txid)
if info:
- conf = local_height - info.height
+ conf = local_height - info.height + 1
else:
conf = 0
l.append((conf, v))
@@ -1282,13 +1283,23 @@ class Abstract_Wallet(AddressSynchronizer):
expiration = r.get('exp')
if expiration and type(expiration) != int:
expiration = 0
-
paid, conf = self.get_payment_status(address, amount)
- status = PR_PAID if paid else PR_UNPAID
- if status == PR_UNPAID and expiration is not None and time.time() > timestamp + expiration:
- status = PR_EXPIRED
+ if not paid:
+ if expiration is not None and time.time() > timestamp + expiration:
+ status = PR_EXPIRED
+ else:
+ status = PR_UNPAID
+ else:
+ status = PR_INFLIGHT if conf <= 0 else PR_PAID
return status, conf
+ def get_request(self, key, is_lightning):
+ if not is_lightning:
+ req = self.get_payment_request(key, {})
+ else:
+ req = self.lnworker.get_request(key)
+ return req
+
def receive_tx_callback(self, tx_hash, tx, tx_height):
super().receive_tx_callback(tx_hash, tx, tx_height)
for txo in tx.outputs():