Browse Source

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
master
ThomasV 6 years ago
parent
commit
8010123c08
  1. 13
      electrum/commands.py
  2. 48
      electrum/gui/kivy/uix/screens.py
  3. 21
      electrum/gui/kivy/uix/ui_screens/receive.kv
  4. 26
      electrum/gui/qt/main_window.py
  5. 41
      electrum/gui/qt/request_list.py
  6. 8
      electrum/gui/qt/util.py
  7. 42
      electrum/lnworker.py
  8. 39
      electrum/util.py
  9. 23
      electrum/wallet.py

13
electrum/commands.py

@ -670,14 +670,9 @@ class Commands:
return decrypted.decode('utf-8') return decrypted.decode('utf-8')
def _format_request(self, out): def _format_request(self, out):
pr_str = { from .util import get_request_status
PR_UNKNOWN: 'Unknown',
PR_UNPAID: 'Pending',
PR_PAID: 'Paid',
PR_EXPIRED: 'Expired',
}
out['amount_BTC'] = format_satoshis(out.get('amount')) 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 return out
@command('w') @command('w')
@ -850,9 +845,9 @@ class Commands:
return await self.lnworker._pay(invoice, attempts=attempts) return await self.lnworker._pay(invoice, attempts=attempts)
@command('wn') @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 # 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)] invoice, direction, is_paid = self.lnworker.invoices[bh2u(payment_hash)]
return invoice return invoice

48
electrum/gui/kivy/uix/screens.py

@ -2,7 +2,6 @@ import asyncio
from weakref import ref from weakref import ref
from decimal import Decimal from decimal import Decimal
import re import re
import datetime
import threading import threading
import traceback, sys import traceback, sys
from enum import Enum, auto 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 import bitcoin, constants
from electrum.transaction import TxOutput, Transaction, tx_from_str 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 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.plugin import run_hook
from electrum.wallet import InternalAddressCorruption from electrum.wallet import InternalAddressCorruption
from electrum import simple_config from electrum import simple_config
@ -404,12 +403,14 @@ class SendScreen(CScreen):
class ReceiveScreen(CScreen): class ReceiveScreen(CScreen):
kvname = 'receive' kvname = 'receive'
cards = {}
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(ReceiveScreen, self).__init__(**kwargs) super(ReceiveScreen, self).__init__(**kwargs)
self.menu_actions = [(_('Show'), self.do_show), (_('Delete'), self.do_delete)] 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): def clear(self):
self.screen.address = '' self.screen.address = ''
@ -452,9 +453,8 @@ class ReceiveScreen(CScreen):
amount = self.screen.amount amount = self.screen.amount
amount = self.app.get_amount(amount) if amount else 0 amount = self.app.get_amount(amount) if amount else 0
message = self.screen.message message = self.screen.message
expiration = self.expiration
if lightning: 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()) request, direction, is_paid = self.app.wallet.lnworker.invoices.get(payment_hash.hex())
key = payment_hash.hex() key = payment_hash.hex()
else: else:
@ -463,40 +463,37 @@ class ReceiveScreen(CScreen):
self.app.show_info(_('No address available. Please remove some of your pending requests.')) self.app.show_info(_('No address available. Please remove some of your pending requests.'))
return return
self.screen.address = addr 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) self.app.wallet.add_payment_request(req, self.app.electrum_config)
key = addr key = addr
self.clear()
self.update() self.update()
self.app.show_request(lightning, key) self.app.show_request(lightning, key)
def get_card(self, req): def get_card(self, req):
is_lightning = req.get('lightning', False) is_lightning = req.get('lightning', False)
status = req['status']
#if status != PR_UNPAID:
# continue
if not is_lightning: if not is_lightning:
address = req['address'] address = req['address']
key = address key = address
else: else:
key = req['rhash'] key = req['rhash']
address = req['invoice'] address = req['invoice']
timestamp = req.get('time', 0)
amount = req.get('amount') amount = req.get('amount')
description = req.get('memo', '') description = req.get('memo', '')
ci = self.cards.get(key) ci = {}
if ci is None: ci['screen'] = self
ci = {} ci['address'] = address
ci['address'] = address ci['is_lightning'] = is_lightning
ci['is_lightning'] = is_lightning ci['key'] = key
ci['key'] = key
ci['screen'] = self
self.cards[key] = ci
ci['amount'] = self.app.format_amount_and_units(amount) if amount else '' ci['amount'] = self.app.format_amount_and_units(amount) if amount else ''
ci['memo'] = description ci['memo'] = description
ci['status'] = age(timestamp) ci['status'] = get_request_status(req)
ci['is_expired'] = req['status'] == PR_EXPIRED
return ci return ci
def update(self): def update(self):
if not self.loaded:
return
_list = self.app.wallet.get_sorted_requests(self.app.electrum_config) _list = self.app.wallet.get_sorted_requests(self.app.electrum_config)
requests_container = self.screen.ids.requests_container requests_container = self.screen.ids.requests_container
requests_container.data = [self.get_card(item) for item in _list if item.get('status') != PR_PAID] 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): def expiration_dialog(self, obj):
from .dialogs.choice_dialog import ChoiceDialog 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): def callback(c):
self.expiration = c self.app.electrum_config.set_key('request_expiry', c)
self.app.electrum_config.set_key('request_expiration', c) d = ChoiceDialog(_('Expiration date'), pr_expiration_values, self.expiry(), callback)
d = ChoiceDialog(_('Expiration date'), choices, self.expiration, callback)
d.open() d.open()
def do_delete(self, req): def do_delete(self, req):

21
electrum/gui/kivy/uix/ui_screens/receive.kv

@ -13,29 +13,22 @@
valign: 'top' valign: 'top'
<RequestItem@CardItem> <RequestItem@CardItem>
is_expired: False
address: '' address: ''
memo: '' memo: ''
amount: '' amount: ''
status: '' 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: BoxLayout:
spacing: '8dp' spacing: '8dp'
height: '32dp' height: '32dp'
orientation: 'vertical' orientation: 'vertical'
Widget Widget
RequestLabel: RequestLabel:
text: root.address text: root.memo
shorten: True shorten: True
Widget Widget
RequestLabel: RequestLabel:
text: root.memo text: root.address
color: .699, .699, .699, 1 color: .699, .699, .699, 1
font_size: '13sp' font_size: '13sp'
shorten: True shorten: True
@ -54,7 +47,7 @@
text: root.status text: root.status
halign: 'right' halign: 'right'
font_size: '13sp' font_size: '13sp'
color: .699, .699, .699, 1 color: (1., .2, .2, 1) if root.is_expired else (.7, .7, .7, 1)
Widget Widget
<RequestRecycleView>: <RequestRecycleView>:
@ -75,7 +68,6 @@ ReceiveScreen:
message: '' message: ''
status: '' status: ''
is_lightning: False is_lightning: False
show_list: True
BoxLayout BoxLayout
padding: '12dp', '12dp', '12dp', '12dp' 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')) text: _('Lightning') if root.is_lightning else (s.address if s.address else _('Bitcoin Address'))
shorten: True shorten: True
on_release: root.is_lightning = not root.is_lightning on_release: root.is_lightning = not root.is_lightning
#on_release: Clock.schedule_once(lambda dt: app.addresses_dialog(s))
CardSeparator: CardSeparator:
opacity: message_selection.opacity opacity: message_selection.opacity
color: blue_bottom.foreground_color color: blue_bottom.foreground_color
@ -144,7 +135,7 @@ ReceiveScreen:
icon: 'atlas://electrum/gui/kivy/theming/light/list' icon: 'atlas://electrum/gui/kivy/theming/light/list'
size_hint: 0.5, None size_hint: 0.5, None
height: '48dp' height: '48dp'
on_release: root.show_list = not root.show_list on_release: Clock.schedule_once(lambda dt: app.addresses_dialog())
IconButton: IconButton:
icon: 'atlas://electrum/gui/kivy/theming/light/clock1' icon: 'atlas://electrum/gui/kivy/theming/light/clock1'
size_hint: 0.5, None size_hint: 0.5, None
@ -166,5 +157,3 @@ ReceiveScreen:
id: requests_container id: requests_container
scroll_type: ['bars', 'content'] scroll_type: ['bars', 'content']
bar_width: '25dp' bar_width: '25dp'
opacity: 1 if root.show_list else 0
disabled: not root.show_list

26
electrum/gui/qt/main_window.py

@ -73,6 +73,7 @@ from electrum.exchange_rate import FxThread
from electrum.simple_config import SimpleConfig from electrum.simple_config import SimpleConfig
from electrum.logging import Logger from electrum.logging import Logger
from electrum.paymentrequest import PR_PAID from electrum.paymentrequest import PR_PAID
from electrum.util import pr_expiration_values
from .exception_window import Exception_Hook from .exception_window import Exception_Hook
from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit 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, from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialog,
WindowModalDialog, ChoicesLayout, HelpLabel, FromList, Buttons, WindowModalDialog, ChoicesLayout, HelpLabel, FromList, Buttons,
OkButton, InfoButton, WWLabel, TaskThread, CancelButton, OkButton, InfoButton, WWLabel, TaskThread, CancelButton,
CloseButton, HelpButton, MessageBoxMixin, EnterButton, expiration_values, CloseButton, HelpButton, MessageBoxMixin, EnterButton,
ButtonsLineEdit, CopyCloseButton, import_meta_gui, export_meta_gui, ButtonsLineEdit, CopyCloseButton, import_meta_gui, export_meta_gui,
filename_field, address_field, char_width_in_lineedit, webopen) filename_field, address_field, char_width_in_lineedit, webopen)
from .util import ButtonsTextEdit from .util import ButtonsTextEdit
@ -753,6 +754,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
return fileName return fileName
def timer_actions(self): def timer_actions(self):
self.request_list.refresh_status()
# Note this runs in the GUI thread # Note this runs in the GUI thread
if self.need_update.is_set(): if self.need_update.is_set():
self.need_update.clear() 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.connect_fields(self, self.receive_amount_e, self.fiat_receive_e, None)
self.expires_combo = QComboBox() self.expires_combo = QComboBox()
self.expires_combo.addItems([i[0] for i in expiration_values]) evl = sorted(pr_expiration_values.items())
self.expires_combo.setCurrentIndex(3) 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()) 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([ msg = ' '.join([
_('Expiration date of your request.'), _('Expiration date of your request.'),
_('This information is seen by the recipient if you send them a signed payment 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): def create_invoice(self, is_lightning):
amount = self.receive_amount_e.get_amount() amount = self.receive_amount_e.get_amount()
message = self.receive_message_e.text() message = self.receive_message_e.text()
i = self.expires_combo.currentIndex() expiry = self.config.get('request_expiry', 3600)
expiration = list(map(lambda x: x[1], expiration_values))[i]
if is_lightning: 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) key = bh2u(payment_hash)
else: else:
key = self.create_bitcoin_request(amount, message, expiration) key = self.create_bitcoin_request(amount, message, expiry)
self.address_list.update() self.address_list.update()
self.request_list.update() self.request_list.update()
self.request_list.select_key(key) self.request_list.select_key(key)

41
electrum/gui/qt/request_list.py

@ -30,7 +30,7 @@ from PyQt5.QtWidgets import QMenu, QHeaderView
from PyQt5.QtCore import Qt, QItemSelectionModel from PyQt5.QtCore import Qt, QItemSelectionModel
from electrum.i18n import _ 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.util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT, pr_tooltips
from electrum.lnutil import SENT, RECEIVED from electrum.lnutil import SENT, RECEIVED
from electrum.plugin import run_hook from electrum.plugin import run_hook
@ -85,20 +85,28 @@ class RequestList(MyTreeView):
item = self.model().itemFromIndex(idx.sibling(idx.row(), self.Columns.DATE)) item = self.model().itemFromIndex(idx.sibling(idx.row(), self.Columns.DATE))
request_type = item.data(ROLE_REQUEST_TYPE) request_type = item.data(ROLE_REQUEST_TYPE)
key = item.data(ROLE_RHASH_OR_ADDR) key = item.data(ROLE_RHASH_OR_ADDR)
if request_type == REQUEST_TYPE_BITCOIN: is_lightning = request_type == REQUEST_TYPE_LN
req = self.wallet.receive_requests.get(key) req = self.wallet.get_request(key, is_lightning)
if req is None: if req is None:
self.update() self.update()
return return
req = self.wallet.get_request_URI(key) text = req.get('invoice') if is_lightning else req.get('URI')
elif request_type == REQUEST_TYPE_LN: self.parent.receive_address_e.setText(text)
req, direction, is_paid = self.wallet.lnworker.invoices.get(key) or (None, None, None)
if req is None: def refresh_status(self):
self.update() m = self.model()
return for r in range(m.rowCount()):
else: idx = m.index(r, self.Columns.STATUS)
raise Exception(f"unknown request type: {request_type}") date_idx = idx.sibling(idx.row(), self.Columns.DATE)
self.parent.receive_address_e.setText(req) 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): def update(self):
self.wallet = self.parent.wallet self.wallet = self.parent.wallet
@ -116,7 +124,8 @@ class RequestList(MyTreeView):
message = req['memo'] message = req['memo']
date = format_time(timestamp) date = format_time(timestamp)
amount_str = self.parent.format_amount(amount) if amount else "" 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] items = [QStandardItem(e) for e in labels]
self.set_editability(items) self.set_editability(items)
items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE) items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE)

8
electrum/gui/qt/util.py

@ -45,16 +45,10 @@ pr_icons = {
PR_UNPAID:"unpaid.png", PR_UNPAID:"unpaid.png",
PR_PAID:"confirmed.png", PR_PAID:"confirmed.png",
PR_EXPIRED:"expired.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): class EnterButton(QPushButton):

42
electrum/lnworker.py

@ -868,8 +868,8 @@ class LNWallet(LNWorker):
raise PaymentFailure(_("No path found")) raise PaymentFailure(_("No path found"))
return route return route
def add_invoice(self, amount_sat, message): def add_invoice(self, amount_sat, message, expiry):
coro = self._add_invoice_coro(amount_sat, message) coro = self._add_invoice_coro(amount_sat, message, expiry)
fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
try: try:
return fut.result(timeout=5) return fut.result(timeout=5)
@ -877,7 +877,7 @@ class LNWallet(LNWorker):
raise Exception(_("add_invoice timed out")) raise Exception(_("add_invoice timed out"))
@log_exceptions @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_preimage = os.urandom(32)
payment_hash = sha256(payment_preimage) payment_hash = sha256(payment_preimage)
amount_btc = amount_sat/Decimal(COIN) if amount_sat else None 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.") "Other clients will likely not be able to send to us.")
invoice = lnencode(LnAddr(payment_hash, amount_btc, invoice = lnencode(LnAddr(payment_hash, amount_btc,
tags=[('d', message), tags=[('d', message),
('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE)] ('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE),
('x', expiry)]
+ routing_hints), + routing_hints),
self.node_keypair.privkey) self.node_keypair.privkey)
self.save_invoice(payment_hash, invoice, RECEIVED, is_paid=False) self.save_invoice(payment_hash, invoice, RECEIVED, is_paid=False)
@ -933,26 +934,31 @@ class LNWallet(LNWorker):
except KeyError as e: except KeyError as e:
raise UnknownPaymentHash(payment_hash) from 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): def get_invoices(self):
items = self.invoices.items() items = self.invoices.items()
out = [] out = []
for key, (invoice, direction, is_paid) in items: for key, (invoice, direction, is_paid) in items:
if direction == SENT: if direction == SENT:
continue continue
status = self.get_invoice_status(key) out.append(self.get_request(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
})
return out return out
async def _calc_routing_hints_for_invoice(self, amount_sat): async def _calc_routing_hints_for_invoice(self, amount_sat):

39
electrum/util.py

@ -78,16 +78,34 @@ PR_UNPAID = 0
PR_EXPIRED = 1 PR_EXPIRED = 1
PR_UNKNOWN = 2 # sent but not propagated PR_UNKNOWN = 2 # sent but not propagated
PR_PAID = 3 # send and propagated PR_PAID = 3 # send and propagated
PR_INFLIGHT = 4 # lightning PR_INFLIGHT = 4 # unconfirmed
pr_tooltips = { pr_tooltips = {
PR_UNPAID:_('Pending'), PR_UNPAID:_('Pending'),
PR_PAID:_('Paid'), PR_PAID:_('Paid'),
PR_UNKNOWN:_('Unknown'), PR_UNKNOWN:_('Unknown'),
PR_EXPIRED:_('Expired'), 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 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_seconds = int(round(abs(distance_in_time.days * 86400 + distance_in_time.seconds)))
distance_in_minutes = int(round(distance_in_seconds/60)) distance_in_minutes = int(round(distance_in_seconds/60))
if distance_in_minutes <= 1: if distance_in_minutes == 0:
if include_seconds: if include_seconds:
for remainder in [5, 10, 20]: return "%s seconds" % distance_in_seconds
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"
else: else:
if distance_in_minutes == 0: return "less than a minute"
return "less than a minute"
else:
return "1 minute"
elif distance_in_minutes < 45: elif distance_in_minutes < 45:
return "%s minutes" % distance_in_minutes return "%s minutes" % distance_in_minutes
elif distance_in_minutes < 90: elif distance_in_minutes < 90:

23
electrum/wallet.py

@ -46,6 +46,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler,
WalletFileException, BitcoinException, WalletFileException, BitcoinException,
InvalidPassword, format_time, timestamp_to_datetime, Satoshis, InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex) Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
from .util import age
from .simple_config import get_config from .simple_config import get_config
from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script, from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script,
is_minikey, relayfee, dust_threshold) is_minikey, relayfee, dust_threshold)
@ -59,7 +60,7 @@ from .transaction import Transaction, TxOutput, TxOutputHwInfo
from .plugin import run_hook from .plugin import run_hook
from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE) 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) InvoiceStore)
from .contacts import Contacts from .contacts import Contacts
from .interface import NetworkException from .interface import NetworkException
@ -1204,7 +1205,7 @@ class Abstract_Wallet(AddressSynchronizer):
txid, n = txo.split(':') txid, n = txo.split(':')
info = self.db.get_verified_tx(txid) info = self.db.get_verified_tx(txid)
if info: if info:
conf = local_height - info.height conf = local_height - info.height + 1
else: else:
conf = 0 conf = 0
l.append((conf, v)) l.append((conf, v))
@ -1282,13 +1283,23 @@ class Abstract_Wallet(AddressSynchronizer):
expiration = r.get('exp') expiration = r.get('exp')
if expiration and type(expiration) != int: if expiration and type(expiration) != int:
expiration = 0 expiration = 0
paid, conf = self.get_payment_status(address, amount) paid, conf = self.get_payment_status(address, amount)
status = PR_PAID if paid else PR_UNPAID if not paid:
if status == PR_UNPAID and expiration is not None and time.time() > timestamp + expiration: if expiration is not None and time.time() > timestamp + expiration:
status = PR_EXPIRED status = PR_EXPIRED
else:
status = PR_UNPAID
else:
status = PR_INFLIGHT if conf <= 0 else PR_PAID
return status, conf 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): def receive_tx_callback(self, tx_hash, tx, tx_height):
super().receive_tx_callback(tx_hash, tx, tx_height) super().receive_tx_callback(tx_hash, tx, tx_height)
for txo in tx.outputs(): for txo in tx.outputs():

Loading…
Cancel
Save