Browse Source

GUI refactoring for Kivy and lightning.

This also touches Qt and wallet code.
dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
ThomasV 6 years ago
parent
commit
70cd29f9e1
  1. 4
      electrum/gui/kivy/Makefile
  2. 3
      electrum/gui/kivy/main.kv
  3. 26
      electrum/gui/kivy/main_window.py
  4. BIN
      electrum/gui/kivy/theming/light/copy.png
  5. 288
      electrum/gui/kivy/theming/light/lightning_switch.svg
  6. BIN
      electrum/gui/kivy/theming/light/list.png
  7. 7
      electrum/gui/kivy/uix/dialogs/addresses.py
  8. 12
      electrum/gui/kivy/uix/dialogs/qr_dialog.py
  9. 82
      electrum/gui/kivy/uix/dialogs/request_dialog.py
  10. 44
      electrum/gui/kivy/uix/dialogs/requests.py
  11. 158
      electrum/gui/kivy/uix/screens.py
  12. 93
      electrum/gui/kivy/uix/ui_screens/receive.kv
  13. 36
      electrum/gui/kivy/uix/ui_screens/send.kv
  14. 30
      electrum/gui/qt/main_window.py
  15. 58
      electrum/gui/qt/request_list.py
  16. 2
      electrum/lnpeer.py
  17. 25
      electrum/lnworker.py
  18. 59
      electrum/wallet.py

4
electrum/gui/kivy/Makefile

@ -5,9 +5,7 @@ PYTHON = python3
.PHONY: theming apk clean
theming:
bash -c 'for i in network lightning; do convert -background none theming/light/$$i.{svg,png}; done'
convert -background none -crop +0+390 theming/light/lightning_switch.svg theming/light/lightning_switch_off.png
convert -background none -crop 840x390+0+0 theming/light/lightning_switch.svg theming/light/lightning_switch_on.png
#bash -c 'for i in network lightning; do convert -background none theming/light/$$i.{svg,png}; done'
$(PYTHON) -m kivy.atlas theming/light 1024 theming/light/*.png
prepare:
# running pre build setup

3
electrum/gui/kivy/main.kv

@ -449,6 +449,9 @@ BoxLayout:
ActionOvrButton:
name: 'network'
text: _('Network')
ActionOvrButton:
name: 'addresses_dialog'
text: _('Addresses')
ActionOvrButton:
name: 'lightning_channels_dialog'
text: _('Channels')

26
electrum/gui/kivy/main_window.py

@ -195,6 +195,12 @@ class ElectrumWindow(App):
def on_fee_histogram(self, *args):
self._trigger_update_history()
def on_payment_received(self, event, wallet, key, status):
if self.request_popup and self.request_popup.key == key:
self.request_popup.set_status(status)
if status == PR_PAID:
self.show_info(_('Payment Received') + '\n' + key)
def _get_bu(self):
decimal_point = self.electrum_config.get('decimal_point', DECIMAL_POINT_DEFAULT)
try:
@ -328,6 +334,7 @@ class ElectrumWindow(App):
self._settings_dialog = None
self._password_dialog = None
self.fee_status = self.electrum_config.get_fee_status()
self.request_popup = None
def on_pr(self, pr):
if not self.wallet:
@ -397,9 +404,17 @@ class ElectrumWindow(App):
tab = self.tabs.ids[name + '_tab']
panel.switch_to(tab)
def show_request(self, addr):
self.switch_to('receive')
self.receive_screen.screen.address = addr
def show_request(self, is_lightning, key):
from .uix.dialogs.request_dialog import RequestDialog
if is_lightning:
request, direction, is_paid = self.wallet.lnworker.invoices.get(key) or (None, None, None)
status = self.wallet.lnworker.get_invoice_status(key)
else:
request = self.wallet.get_request_URI(key)
status, conf = self.wallet.get_request_status(key)
self.request_popup = RequestDialog('Request', request, key)
self.request_popup.set_status(status)
self.request_popup.open()
def show_pr_details(self, req, status, is_invoice):
from electrum.util import format_time
@ -534,6 +549,7 @@ class ElectrumWindow(App):
self.network.register_callback(self.on_fee_histogram, ['fee_histogram'])
self.network.register_callback(self.on_quotes, ['on_quotes'])
self.network.register_callback(self.on_history, ['on_history'])
self.network.register_callback(self.on_payment_received, ['payment_received'])
# load wallet
self.load_wallet_by_name(self.electrum_config.get_wallet_path())
# URI passed in config
@ -1047,9 +1063,9 @@ class ElectrumWindow(App):
popup.update()
popup.open()
def addresses_dialog(self, screen):
def addresses_dialog(self):
from .uix.dialogs.addresses import AddressesDialog
popup = AddressesDialog(self, screen, None)
popup = AddressesDialog(self)
popup.update()
popup.open()

BIN
electrum/gui/kivy/theming/light/copy.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 B

288
electrum/gui/kivy/theming/light/lightning_switch.svg

@ -1,288 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="840"
height="820.04102"
viewBox="0 0 222.25 216.96919"
version="1.1"
id="svg8"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="lightning_switch.svg">
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient1019">
<stop
style="stop-color:#6464f6;stop-opacity:1;"
offset="0"
id="stop1015" />
<stop
style="stop-color:#76acff;stop-opacity:1"
offset="1"
id="stop1017" />
</linearGradient>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter2183"
x="-0.023532996"
width="1.047066"
y="-0.030062485"
height="1.060125">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.92777831"
id="feGaussianBlur2185" />
</filter>
<linearGradient
id="linearGradient980"
x1="94.415001"
x2="166.42999"
y1="48.271999"
y2="-6.3376999"
gradientTransform="matrix(0.90487595,0,0,0.90487595,-32.116675,75.52401)"
gradientUnits="userSpaceOnUse">
<stop
id="stop973"
stop-color="#fff"
offset="0" />
<stop
id="stop975"
stop-color="#fff"
stop-opacity="0"
offset="1" />
</linearGradient>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter3047"
x="-0.055550463"
width="1.1111009"
y="-0.068128757"
height="1.1362575">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="2.1025669"
id="feGaussianBlur3049" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter3047-6"
x="-0.055550463"
width="1.1111009"
y="-0.068128757"
height="1.1362575">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="2.1025669"
id="feGaussianBlur3049-1" />
</filter>
<filter
style="color-interpolation-filters:sRGB"
inkscape:label="Color Shift"
id="filter3759">
<feColorMatrix
type="hueRotate"
values="330"
result="color1"
id="feColorMatrix3755" />
<feColorMatrix
type="saturate"
values="0"
result="color2"
id="feColorMatrix3757" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter7464"
x="-0.085763194"
width="1.1715264"
y="-0.19973423"
height="1.3994684">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="6.6951018"
id="feGaussianBlur7466" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter7532"
x="-0.042373311"
width="1.0847466"
y="-0.098647438"
height="1.197295">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="3.3066669"
id="feGaussianBlur7534" />
</filter>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1019"
id="linearGradient861"
gradientUnits="userSpaceOnUse"
x1="68.955536"
y1="108.44135"
x2="68.688263"
y2="66.212761" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient980"
id="linearGradient863"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.90487595,0,0,0.90487595,-32.116675,75.52401)"
x1="94.415001"
y1="48.271999"
x2="166.42999"
y2="-6.3376999" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#000000"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="256.4408"
inkscape:cy="683.69642"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:lockguides="true"
inkscape:window-width="3066"
inkscape:window-height="1689"
inkscape:window-x="134"
inkscape:window-y="55"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
units="px"
inkscape:pagecheckerboard="false" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-5.9067634,-55.147908)"
style="display:inline">
<use
x="0"
y="0"
xlink:href="#g6758"
id="use6798"
transform="translate(0,108.47917)"
width="100%"
height="100%" />
<g
id="g6758"
transform="matrix(1.0279896,0,0,1,-0.39555549,0)">
<rect
y="68.48455"
x="19.243406"
height="80.448128"
width="187.35594"
id="rect815"
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#646464;fill-opacity:1;stroke:#555555;stroke-width:5;stroke-linecap:square;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker:none;enable-background:accumulate" />
<path
inkscape:connector-curvature="0"
id="path817"
d="M 19.243406,68.484551 H 206.59935 V 148.93268 H 19.243406 Z"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter7464)" />
<path
transform="matrix(1.0553762,0,0,1.123304,-2.8259824,-10.045808)"
inkscape:connector-curvature="0"
id="path817-5"
d="M 16.068404,65.838715 H 203.35612 V 146.28683 H 16.068404 Z"
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:11.48041821;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.19002374;filter:url(#filter7532)"
sodipodi:nodetypes="ccccc" />
</g>
<g
id="g3255">
<rect
y="71.281387"
x="21.910538"
height="74.067993"
width="90.839211"
id="rect1013"
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:url(#linearGradient861);fill-opacity:1;stroke:none;stroke-width:5;stroke-linecap:square;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker:none;filter:url(#filter2183);enable-background:accumulate" />
<path
d="M 38.12696,110.58365 78.846174,76.421035 c 1.883303,-1.403703 4.668394,-4.204849 2.34658,0.828194 l -13.527082,25.467191 23.120205,0.34508 c 1.057575,0.11762 2.815437,-0.14879 1.173278,1.44929 L 51.377359,139.985 c -2.604817,2.07419 -6.255505,5.67223 -2.69162,-1.2423 l 13.251022,-25.39781 -22.913402,-0.55213 c -2.156371,0.0996 -2.643184,-0.5521 -0.897201,-2.20849 z"
id="path817-3"
style="fill:url(#linearGradient863);fill-rule:evenodd;stroke-width:0.13605724"
inkscape:connector-curvature="0" />
<g
transform="rotate(180,67.330143,108.31538)"
id="g3148">
<path
style="fill:none;fill-rule:evenodd;stroke:#000976;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter3047)"
d="M 112.74975,71.281387 V 145.34938 H 21.910537"
id="path2289"
inkscape:connector-curvature="0"
transform="rotate(-180,67.330143,108.31538)" />
<path
style="fill:none;fill-rule:evenodd;stroke:#91c5ff;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.50831353;filter:url(#filter3047-6)"
d="M 112.74975,71.281389 V 145.34938 H 21.910538"
id="path2289-8"
inkscape:connector-curvature="0" />
</g>
</g>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.21456909px;line-height:100%;font-family:FreeSans;-inkscape-font-specification:'Sans Bold';text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.48097134px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="163.34431"
y="121.71754"
id="text3053"><tspan
sodipodi:role="line"
id="tspan3051"
x="163.34431"
y="121.71754"
style="fill:#ffffff;fill-opacity:1;stroke-width:2.48097134px">ON</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.21456909px;line-height:100%;font-family:FreeSans;-inkscape-font-specification:'Sans Bold';text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.48097134px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="70.730072"
y="229.30695"
id="text3053-9"><tspan
sodipodi:role="line"
id="tspan3051-3"
x="70.730072"
y="229.30695"
style="fill:#ffffff;fill-opacity:1;stroke-width:2.48097134px">OFF</tspan></text>
<use
x="0"
y="0"
xlink:href="#g3255"
id="use3263"
transform="translate(96.165992,108.52486)"
width="100%"
height="100%"
style="filter:url(#filter3759)" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

BIN
electrum/gui/kivy/theming/light/list.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

7
electrum/gui/kivy/uix/dialogs/addresses.py

@ -104,11 +104,9 @@ from electrum.gui.kivy.uix.context_menu import ContextMenu
class AddressesDialog(Factory.Popup):
def __init__(self, app, screen, callback):
def __init__(self, app):
Factory.Popup.__init__(self)
self.app = app
self.screen = screen
self.callback = callback
self.context_menu = None
def get_card(self, addr, balance, is_used, label):
@ -155,7 +153,8 @@ class AddressesDialog(Factory.Popup):
def do_use(self, obj):
self.hide_menu()
self.dismiss()
self.app.show_request(obj.address)
self.app.switch_to('receive')
self.app.receive_screen.set_address(obj.address)
def do_view(self, obj):
req = { 'address': obj.address, 'status' : obj.status }

12
electrum/gui/kivy/uix/dialogs/qr_dialog.py

@ -23,6 +23,11 @@ Builder.load_string('''
spacing: '10dp'
QRCodeWidget:
id: qr
shaded: False
foreground_color: (0, 0, 0, 0.5) if self.shaded else (0, 0, 0, 0)
on_touch_down:
touch = args[1]
if self.collide_point(*touch.pos): self.shaded = not self.shaded
TopLabel:
text: root.data if root.show_text else ''
Widget:
@ -33,9 +38,14 @@ Builder.load_string('''
Button:
size_hint: 1, None
height: '48dp'
text: _('Copy to clipboard')
text: _('Copy')
on_release:
root.copy_to_clipboard()
IconButton:
icon: 'atlas://electrum/gui/kivy/theming/light/share'
size_hint: 0.6, None
height: '48dp'
on_release: s.parent.do_share()
Button:
size_hint: 1, None
height: '48dp'

82
electrum/gui/kivy/uix/dialogs/request_dialog.py

@ -0,0 +1,82 @@
from kivy.factory import Factory
from kivy.lang import Builder
from kivy.core.clipboard import Clipboard
from kivy.app import App
from kivy.clock import Clock
from electrum.gui.kivy.i18n import _
from electrum.util import pr_tooltips
Builder.load_string('''
<RequestDialog@Popup>
id: popup
title: ''
data: ''
status: 'unknown'
shaded: False
show_text: False
AnchorLayout:
anchor_x: 'center'
BoxLayout:
orientation: 'vertical'
size_hint: 1, 1
padding: '10dp'
spacing: '10dp'
QRCodeWidget:
id: qr
shaded: False
foreground_color: (0, 0, 0, 0.5) if self.shaded else (0, 0, 0, 0)
on_touch_down:
touch = args[1]
if self.collide_point(*touch.pos): self.shaded = not self.shaded
TopLabel:
text: root.data
TopLabel:
text: _('Status') + ': ' + root.status
Widget:
size_hint: 1, 0.2
BoxLayout:
size_hint: 1, None
height: '48dp'
Button:
size_hint: 1, None
height: '48dp'
text: _('Copy')
on_release:
root.copy_to_clipboard()
IconButton:
icon: 'atlas://electrum/gui/kivy/theming/light/share'
size_hint: 0.6, None
height: '48dp'
on_release: s.parent.do_share()
Button:
size_hint: 1, None
height: '48dp'
text: _('Close')
on_release:
popup.dismiss()
''')
class RequestDialog(Factory.Popup):
def __init__(self, title, data, key):
Factory.Popup.__init__(self)
self.app = App.get_running_app()
self.title = title
self.data = data
self.key = key
#self.text_for_clipboard = text_for_clipboard if text_for_clipboard else data
def on_open(self):
self.ids.qr.set_data(self.data)
def set_status(self, status):
self.status = pr_tooltips[status]
def on_dismiss(self):
self.app.request_popup = None
def copy_to_clipboard(self):
Clipboard.copy(self.data)
msg = _('Text copied to clipboard.')
Clock.schedule_once(lambda dt: self.app.show_info(msg))

44
electrum/gui/kivy/uix/dialogs/requests.py

@ -4,6 +4,12 @@ from kivy.properties import ObjectProperty
from kivy.lang import Builder
from decimal import Decimal
from electrum.util import age, PR_UNPAID
from electrum.lnutil import SENT, RECEIVED
from electrum.lnaddr import lndecode
import electrum.constants as constants
from electrum.bitcoin import COIN
Builder.load_string('''
<RequestLabel@Label>
#color: .305, .309, .309, 1
@ -58,7 +64,7 @@ Builder.load_string('''
<RequestsDialog@Popup>
id: popup
title: _('Requests')
title: _('Pending requests')
BoxLayout:
id:box
orientation: 'vertical'
@ -103,21 +109,20 @@ class RequestsDialog(Factory.Popup):
self.cards = {}
self.context_menu = None
def get_card(self, req):
address = req['address']
ci = self.cards.get(address)
def get_card(self, is_lightning, key, address, amount, memo, timestamp):
ci = self.cards.get(key)
if ci is None:
ci = Factory.RequestItem()
ci.address = address
ci.screen = self
self.cards[address] = ci
ci.is_lightning = is_lightning
ci.key = key
self.cards[key] = ci
amount = req.get('amount')
ci.amount = self.app.format_amount_and_units(amount) if amount else ''
ci.memo = req.get('memo', '')
status, conf = self.app.wallet.get_request_status(address)
ci.status = request_text[status]
ci.icon = pr_icon[status]
ci.memo = memo
ci.status = age(timestamp)
#ci.icon = pr_icon[status]
#exp = pr.get_expiration_date()
#ci.date = format_time(exp) if exp else _('Never')
return ci
@ -127,14 +132,27 @@ class RequestsDialog(Factory.Popup):
requests_list = self.ids.requests_container
requests_list.clear_widgets()
_list = self.app.wallet.get_sorted_requests(self.app.electrum_config)
for pr in _list:
ci = self.get_card(pr)
for req in _list[::-1]:
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.get_card(is_lightning, key, address, amount, description, timestamp)
requests_list.add_widget(ci)
def do_show(self, obj):
self.hide_menu()
self.dismiss()
self.app.show_request(obj.address)
self.app.show_request(obj.is_lightning, obj.key)
def do_delete(self, req):
from .question import Question

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

@ -31,7 +31,7 @@ from electrum.plugin import run_hook
from electrum.wallet import InternalAddressCorruption
from electrum import simple_config
from electrum.lnaddr import lndecode
from electrum.lnutil import RECEIVED, SENT
from electrum.lnutil import RECEIVED, SENT, PaymentFailure
from .context_menu import ContextMenu
from .dialogs.lightning_open_channel import LightningOpenChannelDialog
@ -233,7 +233,7 @@ class SendScreen(CScreen):
self.screen.destinationtype = Destination.Address
self.payment_request = None
def do_save(self):
def save_invoice(self):
if not self.screen.address:
return
if self.screen.destinationtype == Destination.PR:
@ -247,7 +247,7 @@ class SendScreen(CScreen):
pr = make_unsigned_request(req).SerializeToString()
pr = PaymentRequest(pr)
self.app.wallet.invoices.add(pr)
self.app.show_info(_("Invoice saved"))
#self.app.show_info(_("Invoice saved"))
if pr.is_pr():
self.screen.destinationtype = Destination.PR
self.payment_request = pr
@ -275,6 +275,8 @@ class SendScreen(CScreen):
self.set_ln_invoice(data.rstrip())
else:
self.set_URI(data)
# save automatically
self.save_invoice()
def _do_send_lightning(self):
if not self.screen.amount:
@ -282,27 +284,15 @@ class SendScreen(CScreen):
return
invoice = self.screen.address
amount_sat = self.app.get_amount(self.screen.amount)
addr = self.app.wallet.lnworker._check_invoice(invoice, amount_sat)
try:
route = self.app.wallet.lnworker._create_route_from_invoice(decoded_invoice=addr)
except Exception as e:
dia = LightningOpenChannelDialog(self.app, addr, str(e) + _(':\nYou can open a channel.'))
dia.open()
success = self.app.wallet.lnworker.pay(invoice, attempts=10, amount_sat=amount_sat, timeout=60)
except PaymentFailure as e:
self.app.show_error(_('Payment failure') + '\n' + str(e))
return
self.app.network.register_callback(self.payment_completed_async_thread, ['ln_payment_completed'])
_addr, _peer, coro = self.app.wallet.lnworker._pay(invoice, amount_sat)
fut = asyncio.run_coroutine_threadsafe(coro, self.app.network.asyncio_loop)
fut.add_done_callback(self.ln_payment_result)
def payment_completed_async_thread(self, event, date, direction, htlc, preimage, chan_id):
Clock.schedule_once(lambda dt: self.payment_completed(direction, htlc, preimage))
def payment_completed(self, direction, htlc, preimage):
self.app.show_info(_('Payment received') if direction == RECEIVED else _('Payment sent'))
def ln_payment_result(self, fut):
if fut.exception():
self.app.show_error(_('Lightning payment failed:') + '\n' + repr(fut.exception()))
if success:
self.app.show_info(_('Payment was sent'))
else:
self.app.show_error(_('Payment failed'))
def do_send(self):
if self.screen.destinationtype == Destination.LN:
@ -389,37 +379,14 @@ class ReceiveScreen(CScreen):
kvname = 'receive'
def update(self):
if not self.screen.address:
self.get_new_address()
else:
status = self.app.wallet.get_request_status(self.screen.address)
self.screen.status = _('Payment received') if status == PR_PAID else ''
def clear(self):
self.screen.address = ''
self.screen.amount = ''
self.screen.message = ''
self.screen.lnaddr = ''
def get_new_address(self) -> bool:
"""Sets the address field, and returns whether the set address
is unused."""
if not self.app.wallet:
return False
self.clear()
unused = True
try:
addr = self.app.wallet.get_unused_address()
if addr is None:
addr = self.app.wallet.get_receiving_address() or ''
unused = False
except InternalAddressCorruption as e:
addr = ''
self.app.show_error(str(e))
send_exception_to_crash_reporter(e)
def set_address(self, addr):
self.screen.address = addr
return unused
def on_address(self, addr):
req = self.app.wallet.get_payment_request(addr, self.app.electrum_config)
@ -430,7 +397,6 @@ class ReceiveScreen(CScreen):
self.screen.amount = self.app.format_amount_and_units(amount) if amount else ''
status = req.get('status', PR_UNKNOWN)
self.screen.status = _('Payment received') if status == PR_PAID else ''
Clock.schedule_once(lambda dt: self.update_qr())
def get_URI(self):
from electrum.util import create_bip21_uri
@ -441,73 +407,37 @@ class ReceiveScreen(CScreen):
amount = Decimal(a) * pow(10, self.app.decimal_point())
return create_bip21_uri(self.screen.address, amount, self.screen.message)
@profiler
def update_qr(self):
qr = self.screen.ids.qr
if self.screen.ids.lnbutton.state == 'down':
qr.set_data(self.screen.lnaddr)
else:
uri = self.get_URI()
qr.set_data(uri)
def do_share(self):
if self.screen.ids.lnbutton.state == 'down':
if self.screen.lnaddr:
self.app.do_share('lightning://' + self.lnaddr, _('Share Lightning invoice'))
else:
uri = self.get_URI()
self.app.do_share(uri, _("Share Bitcoin Request"))
uri = self.get_URI()
self.app.do_share(uri, _("Share Bitcoin Request"))
def do_copy(self):
if self.screen.ids.lnbutton.state == 'down':
if self.screen.lnaddr:
self.app._clipboard.copy(self.screen.lnaddr)
self.app.show_info(_('Invoice copied to clipboard'))
else:
uri = self.get_URI()
self.app._clipboard.copy(uri)
self.app.show_info(_('Request copied to clipboard'))
def save_request(self):
addr = self.screen.address
if not addr:
return False
uri = self.get_URI()
self.app._clipboard.copy(uri)
self.app.show_info(_('Request copied to clipboard'))
def new_request(self, lightning):
amount = self.screen.amount
message = self.screen.message
amount = self.app.get_amount(amount) if amount else 0
req = self.app.wallet.make_payment_request(addr, amount, message, None)
try:
message = self.screen.message
expiration = 3600 # 1 hour
if lightning:
payment_hash = self.app.wallet.lnworker.add_invoice(amount, message)
request, direction, is_paid = self.app.wallet.lnworker.invoices.get(payment_hash.hex())
key = payment_hash.hex()
else:
addr = self.screen.address or self.app.wallet.get_unused_address()
if not addr:
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)
self.app.wallet.add_payment_request(req, self.app.electrum_config)
added_request = True
except Exception as e:
self.app.show_error(_('Error adding payment request') + ':\n' + repr(e))
added_request = False
finally:
self.app.update_tab('requests')
return added_request
def on_amount_or_message(self):
if self.screen.ids.lnbutton.state == 'down':
if self.screen.amount:
self.screen.lnaddr = self.app.wallet.lnworker.add_invoice(self.app.get_amount(self.screen.amount), self.screen.message)
Clock.schedule_once(lambda dt: self.update_qr())
def do_new(self):
is_unused = self.get_new_address()
if not is_unused:
self.app.show_info(_('Please use the existing requests first.'))
def do_save(self):
if self.save_request():
self.app.show_info(_('Request was saved.'))
def do_open_lnaddr(self, lnaddr):
self.clear()
self.screen.lnaddr = lnaddr
obj = lndecode(lnaddr, expected_hrp=constants.net.SEGWIT_HRP)
self.screen.message = dict(obj.tags).get('d', '')
self.screen.amount = self.app.format_amount_and_units(int(obj.amount * bitcoin.COIN))
self.on_amount_or_message()
#request = self.get_URI()
key = addr
self.app.show_request(lightning, key)
class TabbedCarousel(Factory.TabbedPanel):
'''Custom TabbedPanel using a carousel used in the Main Screen
@ -581,15 +511,3 @@ class TabbedCarousel(Factory.TabbedPanel):
self.carousel.add_widget(widget)
return
super(TabbedCarousel, self).add_widget(widget, index=index)
class LightningButton(ToggleButtonBehavior, Image):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.source = 'atlas://electrum/gui/kivy/theming/light/lightning_switch_off'
def on_state(self, widget, value):
self.state = value
if value == 'down':
self.source = 'atlas://electrum/gui/kivy/theming/light/lightning_switch_on'
else:
self.source = 'atlas://electrum/gui/kivy/theming/light/lightning_switch_off'

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

@ -9,51 +9,16 @@
ReceiveScreen:
id: s
name: 'receive'
address: ''
amount: ''
message: ''
status: ''
lnaddr: ''
on_address:
self.parent.on_address(self.address)
on_amount:
self.parent.on_amount_or_message()
on_message:
self.parent.on_amount_or_message()
is_lightning: False
BoxLayout
padding: '12dp', '12dp', '12dp', '12dp'
spacing: '12dp'
orientation: 'vertical'
size_hint: 1, 1
FloatLayout:
id: bl
QRCodeWidget:
opacity: 0 if lnbutton.state == 'down' and not s.lnaddr else 1
id: qr
size_hint: None, 1
width: min(self.height, bl.width)
pos_hint: {'center': (.5, .5)}
shaded: False
foreground_color: (0, 0, 0, 0.5) if self.shaded else (0, 0, 0, 0)
on_touch_down:
touch = args[1]
if self.collide_point(*touch.pos): self.shaded = not self.shaded
Label:
text: root.status
opacity: 1 if root.status else 0
pos_hint: {'center': (.5, .5)}
size_hint: None, 1
width: min(self.height, bl.width)
bcolor: 0.3, 0.3, 0.3, 0.9
canvas.before:
Color:
rgba: self.bcolor
Rectangle:
pos: self.pos
size: self.size
SendReceiveBlueBottom:
id: blue_bottom
@ -64,15 +29,17 @@ ReceiveScreen:
height: blue_bottom.item_height
spacing: '5dp'
Image:
source: 'atlas://electrum/gui/kivy/theming/light/lightning' if lnbutton.state == 'down' else 'atlas://electrum/gui/kivy/theming/light/globe'
source: 'atlas://electrum/gui/kivy/theming/light/globe'
size_hint: None, None
size: '22dp', '22dp'
pos_hint: {'center_y': .5}
BlueButton:
id: address_label
text: (s.address if s.address else _('Bitcoin Address')) if lnbutton.state != 'down' else (s.lnaddr if s.lnaddr else _('Please enter amount'))
text: _('Lightning') if root.is_lightning else (s.address if s.address else _('Bitcoin Address'))
shorten: True
on_release: Clock.schedule_once(lambda dt: app.addresses_dialog(s) if lnbutton.state != 'down' else s.parent.do_copy())
#on_release: Clock.schedule_once(lambda dt: app.addresses_dialog(s))
on_release:
root.is_lightning = not root.is_lightning
CardSeparator:
opacity: message_selection.opacity
color: blue_bottom.foreground_color
@ -113,37 +80,31 @@ ReceiveScreen:
size_hint: 1, None
height: '48dp'
IconButton:
opacity: 1 if lnbutton.state != 'down' else 0
icon: 'atlas://electrum/gui/kivy/theming/light/save' if lnbutton.state != 'down' else ''
size_hint: (0 if lnbutton.state == 'down' else 0.6), None
height: '48dp'
on_release: s.parent.do_save() if lnbutton.state != 'down' else None
width: (0 if lnbutton.state == 'down' else 100)
Button:
text: _('Requests') if lnbutton.state != 'down' else _('Lightning Invoices')
size_hint: 1 + (.6 if lnbutton.state == 'down' else 0), None
icon: 'atlas://electrum/gui/kivy/theming/light/list'
size_hint: 1, None
height: '48dp'
on_release: Clock.schedule_once(lambda dt: app.requests_dialog(s) if lnbutton.state != 'down' else app.lightning_invoices_dialog(s.parent.do_open_lnaddr))
on_release: Clock.schedule_once(lambda dt: app.requests_dialog(s))
#Widget:
# size_hint: 0.5, 1
Button:
text: _('Copy')
text: _('Clear')
size_hint: 1, None
height: '48dp'
on_release: s.parent.do_copy()
IconButton:
icon: 'atlas://electrum/gui/kivy/theming/light/share'
size_hint: 0.6, None
height: '48dp'
on_release: s.parent.do_share()
BoxLayout:
size_hint: 1, None
height: '48dp'
LightningButton
id: lnbutton
on_state: s.parent.on_amount_or_message()
Widget
size_hint: 1, 1
on_release: Clock.schedule_once(lambda dt: s.parent.clear())
Button:
text: _('New')
text: _('Request')
size_hint: 1, None
height: '48dp'
on_release: Clock.schedule_once(lambda dt: s.parent.do_new())
on_release: Clock.schedule_once(lambda dt: s.parent.new_request(root.is_lightning))
Widget:
size_hint: 1, 1
#BoxLayout:
# size_hint: 1, None
# height: '48dp'
# IconButton:
# icon: 'atlas://electrum/gui/kivy/theming/light/list'
# size_hint: 0.5, None
# height: '48dp'
# on_release: Clock.schedule_once(lambda dt: app.requests_dialog(s))
# Widget:
# size_hint: 2.5, 1

36
electrum/gui/kivy/uix/ui_screens/send.kv

@ -70,7 +70,7 @@ SendScreen:
pos_hint: {'center_y': .5}
BlueButton:
id: description
text: s.message if s.message else ({Destination.LN: _('Lightning invoice contains no description'), Destination.Address: _('Description'), Destination.PR: _('No Description')}[root.destinationtype])
text: s.message if s.message else ({Destination.LN: _('No description'), Destination.Address: _('Description'), Destination.PR: _('No Description')}[root.destinationtype])
disabled: root.destinationtype != Destination.Address
on_release: Clock.schedule_once(lambda dt: app.description_dialog(s))
CardSeparator:
@ -95,34 +95,34 @@ SendScreen:
size_hint: 1, None
height: '48dp'
IconButton:
size_hint: 0.6, 1
on_release: s.parent.do_save()
icon: 'atlas://electrum/gui/kivy/theming/light/save'
Button:
text: _('Invoices')
size_hint: 1, 1
on_release: Clock.schedule_once(lambda dt: app.invoices_dialog(s))
Button:
text: _('Paste')
size_hint: 0.5, 1
icon: 'atlas://electrum/gui/kivy/theming/light/copy'
on_release: s.parent.do_paste()
IconButton:
id: qr
size_hint: 0.6, 1
size_hint: 0.5, 1
on_release: Clock.schedule_once(lambda dt: app.scan_qr(on_complete=app.on_qr))
icon: 'atlas://electrum/gui/kivy/theming/light/camera'
BoxLayout:
size_hint: 1, None
height: '48dp'
Button:
text: _('Clear')
on_release: s.parent.do_clear()
Widget:
size_hint: 1, 1
on_release: s.parent.do_clear()
Button:
text: _('Pay')
size_hint: 1, 1
on_release: s.parent.do_send()
Widget:
size_hint: 1, 1
#BoxLayout:
# size_hint: 1, None
# height: '48dp'
#IconButton:
# size_hint: 0.5, 1
# on_release: s.parent.do_save()
# icon: 'atlas://electrum/gui/kivy/theming/light/save'
#IconButton:
# size_hint: 0.5, 1
# icon: 'atlas://electrum/gui/kivy/theming/light/list'
# on_release: Clock.schedule_once(lambda dt: app.invoices_dialog(s))
#Widget:
# size_hint: 2.5, 1

30
electrum/gui/qt/main_window.py

@ -224,7 +224,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
interests = ['wallet_updated', 'network_updated', 'blockchain_updated',
'new_transaction', 'status',
'banner', 'verified', 'fee', 'fee_histogram', 'on_quotes',
'on_history', 'channel', 'channels', 'ln_message',
'on_history', 'channel', 'channels', 'payment_received',
'ln_payment_completed', 'ln_payment_attempt']
# To avoid leaking references to "self" that prevent the
# window from being GC-ed when closed, callbacks should be
@ -362,7 +362,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
wallet, tx = args
if wallet == self.wallet:
self.tx_notification_queue.put(tx)
elif event in ['status', 'banner', 'verified', 'fee', 'fee_histogram', 'ln_message']:
elif event in ['status', 'banner', 'verified', 'fee', 'fee_histogram', 'payment_received']:
# Handle in GUI thread
self.network_signal.emit(event, args)
elif event == 'on_quotes':
@ -404,10 +404,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.fee_slider.update()
self.require_fee_update = True
self.history_model.on_fee_histogram()
elif event == 'ln_message':
lnworker, message, htlc_id = args
if lnworker == self.wallet.lnworker:
self.notify(message)
elif event == 'payment_received':
wallet, key, status = args
if wallet == self.wallet:
self.notify(_('Payment received') + '\n' + key)
else:
self.logger.info(f"unexpected network_qt signal: {event} {args}")
@ -1039,24 +1039,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.invoice_list.update()
self.clear_receive_tab()
def get_request_URI(self, addr):
req = self.wallet.receive_requests[addr]
message = self.wallet.labels.get(addr, '')
amount = req['amount']
extra_query_params = {}
if req.get('time'):
extra_query_params['time'] = str(int(req.get('time')))
if req.get('exp'):
extra_query_params['exp'] = str(int(req.get('exp')))
if req.get('name') and req.get('sig'):
sig = bfh(req.get('sig'))
sig = bitcoin.base_encode(sig, base=58)
extra_query_params['name'] = req['name']
extra_query_params['sig'] = sig
uri = util.create_bip21_uri(addr, amount, message, extra_query_params=extra_query_params)
return str(uri)
def sign_payment_request(self, addr):
alias = self.config.get('alias')
alias_privkey = None

58
electrum/gui/qt/request_list.py

@ -90,9 +90,9 @@ class RequestList(MyTreeView):
if req is None:
self.update()
return
req = self.parent.get_request_URI(key)
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)
req, direction, is_paid = self.wallet.lnworker.invoices.get(key) or (None, None, None)
if req is None:
self.update()
return
@ -107,51 +107,37 @@ class RequestList(MyTreeView):
self.model().clear()
self.update_headers(self.__class__.headers)
for req in self.wallet.get_sorted_requests(self.config):
address = req['address']
if address not in domain:
continue
request_type = REQUEST_TYPE_LN if req.get('lightning', False) else REQUEST_TYPE_BITCOIN
timestamp = req.get('time', 0)
amount = req.get('amount')
expiration = req.get('exp', None)
message = req['memo']
date = format_time(timestamp)
status = req.get('status')
signature = req.get('sig')
requestor = req.get('name', '')
amount_str = self.parent.format_amount(amount) if amount else ""
labels = [date, message, amount_str, pr_tooltips.get(status,'')]
items = [QStandardItem(e) for e in labels]
self.set_editability(items)
if signature is not None:
items[self.Columns.DATE].setIcon(read_QIcon("seal.png"))
items[self.Columns.DATE].setToolTip(f'signed by {requestor}')
else:
items[self.Columns.DATE].setIcon(read_QIcon("bitcoin.png"))
items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE)
items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status)))
if request_type == REQUEST_TYPE_LN:
items[self.Columns.DATE].setData(req['rhash'], ROLE_RHASH_OR_ADDR)
items[self.Columns.DATE].setIcon(read_QIcon("lightning.png"))
items[self.Columns.DATE].setData(REQUEST_TYPE_LN, ROLE_REQUEST_TYPE)
else:
address = req['address']
if address not in domain:
continue
expiration = req.get('exp', None)
signature = req.get('sig')
requestor = req.get('name', '')
items[self.Columns.DATE].setData(address, ROLE_RHASH_OR_ADDR)
if signature is not None:
items[self.Columns.DATE].setIcon(read_QIcon("seal.png"))
items[self.Columns.DATE].setToolTip(f'signed by {requestor}')
else:
items[self.Columns.DATE].setIcon(read_QIcon("bitcoin.png"))
self.model().insertRow(self.model().rowCount(), items)
items[self.Columns.DATE].setData(REQUEST_TYPE_BITCOIN, ROLE_REQUEST_TYPE)
items[self.Columns.DATE].setData(address, ROLE_RHASH_OR_ADDR)
self.filter()
# lightning
lnworker = self.wallet.lnworker
items = lnworker.invoices.items() if lnworker else []
for key, (invoice, direction, is_paid) in items:
if direction == SENT:
continue
status = lnworker.get_invoice_status(key)
lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
amount_sat = lnaddr.amount*COIN if lnaddr.amount else None
amount_str = self.parent.format_amount(amount_sat) if amount_sat else ''
description = lnaddr.get_description()
date = format_time(lnaddr.date)
labels = [date, description, amount_str, pr_tooltips.get(status,'')]
items = [QStandardItem(e) for e in labels]
self.set_editability(items)
items[self.Columns.DATE].setIcon(read_QIcon("lightning.png"))
items[self.Columns.DATE].setData(REQUEST_TYPE_LN, ROLE_REQUEST_TYPE)
items[self.Columns.DATE].setData(key, ROLE_RHASH_OR_ADDR)
items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status)))
self.model().insertRow(self.model().rowCount(), items)
# sort requests by date
self.model().sort(self.Columns.DATE)
# hide list if empty
@ -192,7 +178,7 @@ class RequestList(MyTreeView):
def create_menu_bitcoin_payreq(self, menu, addr):
menu.addAction(_("Copy Address"), lambda: self.parent.do_copy('Address', addr))
menu.addAction(_("Copy URI"), lambda: self.parent.do_copy('URI', self.parent.get_request_URI(addr)))
menu.addAction(_("Copy URI"), lambda: self.parent.do_copy('URI', self.wallet.get_request_URI(addr)))
menu.addAction(_("Save as BIP70 file"), lambda: self.parent.export_payment_request(addr))
menu.addAction(_("Delete"), lambda: self.parent.delete_payment_request(addr))
run_hook('receive_list_menu', menu, addr)

2
electrum/lnpeer.py

@ -1249,7 +1249,7 @@ class Peer(Logger):
id=htlc_id,
payment_preimage=preimage)
await self.await_remote(chan, remote_ctn)
self.network.trigger_callback('ln_message', self.lnworker, 'Payment received', htlc_id)
#self.lnworker.payment_received(htlc_id)
async def fail_htlc(self, chan: Channel, htlc_id: int, onion_packet: OnionPacket,
reason: OnionRoutingFailureMessage):

25
electrum/lnworker.py

@ -668,7 +668,6 @@ class LNWallet(LNWorker):
except concurrent.futures.TimeoutError:
raise PaymentFailure(_("Payment timed out"))
def get_channel_by_short_id(self, short_channel_id):
with self.lock:
for chan in self.channels.values():
@ -812,6 +811,8 @@ class LNWallet(LNWorker):
return
invoice, direction, _ = self.invoices[key]
self.save_invoice(payment_hash, invoice, direction, is_paid=True)
if direction == RECEIVED:
self.network.trigger_callback('payment_received', self.wallet, key, PR_PAID)
def get_invoice(self, payment_hash: bytes) -> LnAddr:
try:
@ -820,6 +821,28 @@ class LNWallet(LNWorker):
except KeyError as e:
raise UnknownPaymentHash(payment_hash) from e
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
})
return out
def _calc_routing_hints_for_invoice(self, amount_sat):
"""calculate routing hints (BOLT-11 'r' field)"""
self.channel_db.load_data()

59
electrum/wallet.py

@ -34,6 +34,7 @@ import json
import copy
import errno
import traceback
import operator
from functools import partial
from numbers import Number
from decimal import Decimal
@ -44,7 +45,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler,
format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates,
WalletFileException, BitcoinException,
InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate)
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri)
from .simple_config import get_config
from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script,
is_minikey, relayfee, dust_threshold)
@ -1134,10 +1135,10 @@ class Abstract_Wallet(AddressSynchronizer):
return wrapper
def get_unused_addresses(self):
# fixme: use slots from expired requests
domain = self.get_receiving_addresses()
in_use = [k for k in self.receive_requests.keys() if self.get_request_status(k)[0] != PR_EXPIRED]
return [addr for addr in domain if not self.db.get_addr_history(addr)
and addr not in self.receive_requests.keys()]
and addr not in in_use]
@check_returned_address
def get_unused_address(self):
@ -1218,31 +1219,50 @@ class Abstract_Wallet(AddressSynchronizer):
out['websocket_port'] = config.get('websocket_port', 9999)
return out
def get_request_URI(self, addr):
req = self.receive_requests[addr]
message = self.labels.get(addr, '')
amount = req['amount']
extra_query_params = {}
if req.get('time'):
extra_query_params['time'] = str(int(req.get('time')))
if req.get('exp'):
extra_query_params['exp'] = str(int(req.get('exp')))
if req.get('name') and req.get('sig'):
sig = bfh(req.get('sig'))
sig = bitcoin.base_encode(sig, base=58)
extra_query_params['name'] = req['name']
extra_query_params['sig'] = sig
uri = create_bip21_uri(addr, amount, message, extra_query_params=extra_query_params)
return str(uri)
def get_request_status(self, key):
r = self.receive_requests.get(key)
if r is None:
return PR_UNKNOWN
address = r['address']
amount = r.get('amount')
amount = r.get('amount', 0)
timestamp = r.get('time', 0)
if timestamp and type(timestamp) != int:
timestamp = 0
expiration = r.get('exp')
if expiration and type(expiration) != int:
expiration = 0
conf = None
if amount:
if self.is_up_to_date():
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
else:
status = PR_UNKNOWN
else:
status = PR_UNKNOWN
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
return status, conf
def receive_tx_callback(self, tx_hash, tx, tx_height):
super().receive_tx_callback(tx_hash, tx, tx_height)
for txo in tx.outputs():
addr = self.get_txout_address(txo)
if addr in self.receive_requests:
status, conf = self.get_request_status(addr)
self.network.trigger_callback('payment_received', self, addr, status)
def make_payment_request(self, addr, amount, message, expiration):
timestamp = int(time.time())
_id = bh2u(sha256d(addr + "%d"%timestamp))[0:10]
@ -1306,9 +1326,12 @@ class Abstract_Wallet(AddressSynchronizer):
return True
def get_sorted_requests(self, config):
keys = map(lambda x: (self.get_address_index(x), x), self.receive_requests.keys())
sorted_keys = sorted(filter(lambda x: x[0] is not None, keys))
return [self.get_payment_request(x[1], config) for x in sorted_keys]
""" sorted by timestamp """
out = [self.get_payment_request(x, config) for x in self.receive_requests.keys()]
if self.lnworker:
out += self.lnworker.get_invoices()
out.sort(key=operator.itemgetter('time'))
return out
def get_fingerprint(self):
raise NotImplementedError()

Loading…
Cancel
Save