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
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user