commit 70cd29f9e1d442863492aef69faf191e6ba40a30
parent 1a23dcb8d5c394b74648ab6c4a8db47a9d258f37
Author: ThomasV <thomasv@electrum.org>
Date: Mon, 10 Jun 2019 14:05:02 +0200
GUI refactoring for Kivy and lightning.
This also touches Qt and wallet code.
Diffstat:
18 files changed, 329 insertions(+), 598 deletions(-)
diff --git a/electrum/gui/kivy/Makefile b/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
diff --git a/electrum/gui/kivy/main.kv b/electrum/gui/kivy/main.kv
@@ -450,6 +450,9 @@ BoxLayout:
name: 'network'
text: _('Network')
ActionOvrButton:
+ name: 'addresses_dialog'
+ text: _('Addresses')
+ ActionOvrButton:
name: 'lightning_channels_dialog'
text: _('Channels')
ActionOvrButton:
diff --git a/electrum/gui/kivy/main_window.py b/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()
diff --git a/electrum/gui/kivy/theming/light/copy.png b/electrum/gui/kivy/theming/light/copy.png
Binary files differ.
diff --git a/electrum/gui/kivy/theming/light/lightning_switch.svg b/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>
diff --git a/electrum/gui/kivy/theming/light/list.png b/electrum/gui/kivy/theming/light/list.png
Binary files differ.
diff --git a/electrum/gui/kivy/uix/dialogs/addresses.py b/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 }
diff --git a/electrum/gui/kivy/uix/dialogs/qr_dialog.py b/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'
diff --git a/electrum/gui/kivy/uix/dialogs/request_dialog.py b/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))
diff --git a/electrum/gui/kivy/uix/dialogs/requests.py b/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
diff --git a/electrum/gui/kivy/uix/screens.py b/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'
diff --git a/electrum/gui/kivy/uix/ui_screens/receive.kv b/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
diff --git a/electrum/gui/kivy/uix/ui_screens/send.kv b/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
diff --git a/electrum/gui/qt/main_window.py b/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
diff --git a/electrum/gui/qt/request_list.py b/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)
diff --git a/electrum/lnpeer.py b/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):
diff --git a/electrum/lnworker.py b/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()
diff --git a/electrum/wallet.py b/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()