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 .PHONY: theming apk clean
theming: theming:
bash -c 'for i in network lightning; do convert -background none theming/light/$$i.{svg,png}; done' #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
$(PYTHON) -m kivy.atlas theming/light 1024 theming/light/*.png $(PYTHON) -m kivy.atlas theming/light 1024 theming/light/*.png
prepare: prepare:
# running pre build setup # running pre build setup

3
electrum/gui/kivy/main.kv

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

26
electrum/gui/kivy/main_window.py

@ -195,6 +195,12 @@ class ElectrumWindow(App):
def on_fee_histogram(self, *args): def on_fee_histogram(self, *args):
self._trigger_update_history() 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): def _get_bu(self):
decimal_point = self.electrum_config.get('decimal_point', DECIMAL_POINT_DEFAULT) decimal_point = self.electrum_config.get('decimal_point', DECIMAL_POINT_DEFAULT)
try: try:
@ -328,6 +334,7 @@ class ElectrumWindow(App):
self._settings_dialog = None self._settings_dialog = None
self._password_dialog = None self._password_dialog = None
self.fee_status = self.electrum_config.get_fee_status() self.fee_status = self.electrum_config.get_fee_status()
self.request_popup = None
def on_pr(self, pr): def on_pr(self, pr):
if not self.wallet: if not self.wallet:
@ -397,9 +404,17 @@ class ElectrumWindow(App):
tab = self.tabs.ids[name + '_tab'] tab = self.tabs.ids[name + '_tab']
panel.switch_to(tab) panel.switch_to(tab)
def show_request(self, addr): def show_request(self, is_lightning, key):
self.switch_to('receive') from .uix.dialogs.request_dialog import RequestDialog
self.receive_screen.screen.address = addr 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): def show_pr_details(self, req, status, is_invoice):
from electrum.util import format_time 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_fee_histogram, ['fee_histogram'])
self.network.register_callback(self.on_quotes, ['on_quotes']) self.network.register_callback(self.on_quotes, ['on_quotes'])
self.network.register_callback(self.on_history, ['on_history']) self.network.register_callback(self.on_history, ['on_history'])
self.network.register_callback(self.on_payment_received, ['payment_received'])
# load wallet # load wallet
self.load_wallet_by_name(self.electrum_config.get_wallet_path()) self.load_wallet_by_name(self.electrum_config.get_wallet_path())
# URI passed in config # URI passed in config
@ -1047,9 +1063,9 @@ class ElectrumWindow(App):
popup.update() popup.update()
popup.open() popup.open()
def addresses_dialog(self, screen): def addresses_dialog(self):
from .uix.dialogs.addresses import AddressesDialog from .uix.dialogs.addresses import AddressesDialog
popup = AddressesDialog(self, screen, None) popup = AddressesDialog(self)
popup.update() popup.update()
popup.open() 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): class AddressesDialog(Factory.Popup):
def __init__(self, app, screen, callback): def __init__(self, app):
Factory.Popup.__init__(self) Factory.Popup.__init__(self)
self.app = app self.app = app
self.screen = screen
self.callback = callback
self.context_menu = None self.context_menu = None
def get_card(self, addr, balance, is_used, label): def get_card(self, addr, balance, is_used, label):
@ -155,7 +153,8 @@ class AddressesDialog(Factory.Popup):
def do_use(self, obj): def do_use(self, obj):
self.hide_menu() self.hide_menu()
self.dismiss() 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): def do_view(self, obj):
req = { 'address': obj.address, 'status' : obj.status } 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' spacing: '10dp'
QRCodeWidget: QRCodeWidget:
id: qr 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: TopLabel:
text: root.data if root.show_text else '' text: root.data if root.show_text else ''
Widget: Widget:
@ -33,9 +38,14 @@ Builder.load_string('''
Button: Button:
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
text: _('Copy to clipboard') text: _('Copy')
on_release: on_release:
root.copy_to_clipboard() 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: Button:
size_hint: 1, None size_hint: 1, None
height: '48dp' 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 kivy.lang import Builder
from decimal import Decimal 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(''' Builder.load_string('''
<RequestLabel@Label> <RequestLabel@Label>
#color: .305, .309, .309, 1 #color: .305, .309, .309, 1
@ -58,7 +64,7 @@ Builder.load_string('''
<RequestsDialog@Popup> <RequestsDialog@Popup>
id: popup id: popup
title: _('Requests') title: _('Pending requests')
BoxLayout: BoxLayout:
id:box id:box
orientation: 'vertical' orientation: 'vertical'
@ -103,21 +109,20 @@ class RequestsDialog(Factory.Popup):
self.cards = {} self.cards = {}
self.context_menu = None self.context_menu = None
def get_card(self, req): def get_card(self, is_lightning, key, address, amount, memo, timestamp):
address = req['address'] ci = self.cards.get(key)
ci = self.cards.get(address)
if ci is None: if ci is None:
ci = Factory.RequestItem() ci = Factory.RequestItem()
ci.address = address ci.address = address
ci.screen = self 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.amount = self.app.format_amount_and_units(amount) if amount else ''
ci.memo = req.get('memo', '') ci.memo = memo
status, conf = self.app.wallet.get_request_status(address) ci.status = age(timestamp)
ci.status = request_text[status] #ci.icon = pr_icon[status]
ci.icon = pr_icon[status]
#exp = pr.get_expiration_date() #exp = pr.get_expiration_date()
#ci.date = format_time(exp) if exp else _('Never') #ci.date = format_time(exp) if exp else _('Never')
return ci return ci
@ -127,14 +132,27 @@ class RequestsDialog(Factory.Popup):
requests_list = self.ids.requests_container requests_list = self.ids.requests_container
requests_list.clear_widgets() requests_list.clear_widgets()
_list = self.app.wallet.get_sorted_requests(self.app.electrum_config) _list = self.app.wallet.get_sorted_requests(self.app.electrum_config)
for pr in _list: for req in _list[::-1]:
ci = self.get_card(pr) 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) requests_list.add_widget(ci)
def do_show(self, obj): def do_show(self, obj):
self.hide_menu() self.hide_menu()
self.dismiss() self.dismiss()
self.app.show_request(obj.address) self.app.show_request(obj.is_lightning, obj.key)
def do_delete(self, req): def do_delete(self, req):
from .question import Question 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.wallet import InternalAddressCorruption
from electrum import simple_config from electrum import simple_config
from electrum.lnaddr import lndecode from electrum.lnaddr import lndecode
from electrum.lnutil import RECEIVED, SENT from electrum.lnutil import RECEIVED, SENT, PaymentFailure
from .context_menu import ContextMenu from .context_menu import ContextMenu
from .dialogs.lightning_open_channel import LightningOpenChannelDialog from .dialogs.lightning_open_channel import LightningOpenChannelDialog
@ -233,7 +233,7 @@ class SendScreen(CScreen):
self.screen.destinationtype = Destination.Address self.screen.destinationtype = Destination.Address
self.payment_request = None self.payment_request = None
def do_save(self): def save_invoice(self):
if not self.screen.address: if not self.screen.address:
return return
if self.screen.destinationtype == Destination.PR: if self.screen.destinationtype == Destination.PR:
@ -247,7 +247,7 @@ class SendScreen(CScreen):
pr = make_unsigned_request(req).SerializeToString() pr = make_unsigned_request(req).SerializeToString()
pr = PaymentRequest(pr) pr = PaymentRequest(pr)
self.app.wallet.invoices.add(pr) self.app.wallet.invoices.add(pr)
self.app.show_info(_("Invoice saved")) #self.app.show_info(_("Invoice saved"))
if pr.is_pr(): if pr.is_pr():
self.screen.destinationtype = Destination.PR self.screen.destinationtype = Destination.PR
self.payment_request = pr self.payment_request = pr
@ -275,6 +275,8 @@ class SendScreen(CScreen):
self.set_ln_invoice(data.rstrip()) self.set_ln_invoice(data.rstrip())
else: else:
self.set_URI(data) self.set_URI(data)
# save automatically
self.save_invoice()
def _do_send_lightning(self): def _do_send_lightning(self):
if not self.screen.amount: if not self.screen.amount:
@ -282,27 +284,15 @@ class SendScreen(CScreen):
return return
invoice = self.screen.address invoice = self.screen.address
amount_sat = self.app.get_amount(self.screen.amount) amount_sat = self.app.get_amount(self.screen.amount)
addr = self.app.wallet.lnworker._check_invoice(invoice, amount_sat)
try: try:
route = self.app.wallet.lnworker._create_route_from_invoice(decoded_invoice=addr) success = self.app.wallet.lnworker.pay(invoice, attempts=10, amount_sat=amount_sat, timeout=60)
except Exception as e: except PaymentFailure as e:
dia = LightningOpenChannelDialog(self.app, addr, str(e) + _(':\nYou can open a channel.')) self.app.show_error(_('Payment failure') + '\n' + str(e))
dia.open()
return return
self.app.network.register_callback(self.payment_completed_async_thread, ['ln_payment_completed']) if success:
_addr, _peer, coro = self.app.wallet.lnworker._pay(invoice, amount_sat) self.app.show_info(_('Payment was sent'))
fut = asyncio.run_coroutine_threadsafe(coro, self.app.network.asyncio_loop) else:
fut.add_done_callback(self.ln_payment_result) self.app.show_error(_('Payment failed'))
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()))
def do_send(self): def do_send(self):
if self.screen.destinationtype == Destination.LN: if self.screen.destinationtype == Destination.LN:
@ -389,37 +379,14 @@ class ReceiveScreen(CScreen):
kvname = 'receive' 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): def clear(self):
self.screen.address = '' self.screen.address = ''
self.screen.amount = '' self.screen.amount = ''
self.screen.message = '' self.screen.message = ''
self.screen.lnaddr = '' self.screen.lnaddr = ''
def get_new_address(self) -> bool: def set_address(self, addr):
"""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)
self.screen.address = addr self.screen.address = addr
return unused
def on_address(self, addr): def on_address(self, addr):
req = self.app.wallet.get_payment_request(addr, self.app.electrum_config) 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 '' self.screen.amount = self.app.format_amount_and_units(amount) if amount else ''
status = req.get('status', PR_UNKNOWN) status = req.get('status', PR_UNKNOWN)
self.screen.status = _('Payment received') if status == PR_PAID else '' self.screen.status = _('Payment received') if status == PR_PAID else ''
Clock.schedule_once(lambda dt: self.update_qr())
def get_URI(self): def get_URI(self):
from electrum.util import create_bip21_uri from electrum.util import create_bip21_uri
@ -441,73 +407,37 @@ class ReceiveScreen(CScreen):
amount = Decimal(a) * pow(10, self.app.decimal_point()) amount = Decimal(a) * pow(10, self.app.decimal_point())
return create_bip21_uri(self.screen.address, amount, self.screen.message) 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): def do_share(self):
if self.screen.ids.lnbutton.state == 'down': uri = self.get_URI()
if self.screen.lnaddr: self.app.do_share(uri, _("Share Bitcoin Request"))
self.app.do_share('lightning://' + self.lnaddr, _('Share Lightning invoice'))
else:
uri = self.get_URI()
self.app.do_share(uri, _("Share Bitcoin Request"))
def do_copy(self): def do_copy(self):
if self.screen.ids.lnbutton.state == 'down': uri = self.get_URI()
if self.screen.lnaddr: self.app._clipboard.copy(uri)
self.app._clipboard.copy(self.screen.lnaddr) self.app.show_info(_('Request copied to clipboard'))
self.app.show_info(_('Invoice copied to clipboard'))
else: def new_request(self, lightning):
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
amount = self.screen.amount amount = self.screen.amount
message = self.screen.message
amount = self.app.get_amount(amount) if amount else 0 amount = self.app.get_amount(amount) if amount else 0
req = self.app.wallet.make_payment_request(addr, amount, message, None) message = self.screen.message
try: 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) self.app.wallet.add_payment_request(req, self.app.electrum_config)
added_request = True #request = self.get_URI()
except Exception as e: key = addr
self.app.show_error(_('Error adding payment request') + ':\n' + repr(e)) self.app.show_request(lightning, key)
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()
class TabbedCarousel(Factory.TabbedPanel): class TabbedCarousel(Factory.TabbedPanel):
'''Custom TabbedPanel using a carousel used in the Main Screen '''Custom TabbedPanel using a carousel used in the Main Screen
@ -581,15 +511,3 @@ class TabbedCarousel(Factory.TabbedPanel):
self.carousel.add_widget(widget) self.carousel.add_widget(widget)
return return
super(TabbedCarousel, self).add_widget(widget, index=index) 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: ReceiveScreen:
id: s id: s
name: 'receive' name: 'receive'
address: '' address: ''
amount: '' amount: ''
message: '' message: ''
status: '' status: ''
lnaddr: '' is_lightning: False
on_address:
self.parent.on_address(self.address)
on_amount:
self.parent.on_amount_or_message()
on_message:
self.parent.on_amount_or_message()
BoxLayout BoxLayout
padding: '12dp', '12dp', '12dp', '12dp' padding: '12dp', '12dp', '12dp', '12dp'
spacing: '12dp' spacing: '12dp'
orientation: 'vertical' 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: SendReceiveBlueBottom:
id: blue_bottom id: blue_bottom
@ -64,15 +29,17 @@ ReceiveScreen:
height: blue_bottom.item_height height: blue_bottom.item_height
spacing: '5dp' spacing: '5dp'
Image: 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_hint: None, None
size: '22dp', '22dp' size: '22dp', '22dp'
pos_hint: {'center_y': .5} pos_hint: {'center_y': .5}
BlueButton: BlueButton:
id: address_label 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 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: CardSeparator:
opacity: message_selection.opacity opacity: message_selection.opacity
color: blue_bottom.foreground_color color: blue_bottom.foreground_color
@ -113,37 +80,31 @@ ReceiveScreen:
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
IconButton: IconButton:
opacity: 1 if lnbutton.state != 'down' else 0 icon: 'atlas://electrum/gui/kivy/theming/light/list'
icon: 'atlas://electrum/gui/kivy/theming/light/save' if lnbutton.state != 'down' else '' size_hint: 1, None
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
height: '48dp' 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: Button:
text: _('Copy') text: _('Clear')
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
on_release: s.parent.do_copy() on_release: Clock.schedule_once(lambda dt: s.parent.clear())
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
Button: Button:
text: _('New') text: _('Request')
size_hint: 1, None size_hint: 1, None
height: '48dp' 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} pos_hint: {'center_y': .5}
BlueButton: BlueButton:
id: description 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 disabled: root.destinationtype != Destination.Address
on_release: Clock.schedule_once(lambda dt: app.description_dialog(s)) on_release: Clock.schedule_once(lambda dt: app.description_dialog(s))
CardSeparator: CardSeparator:
@ -95,34 +95,34 @@ SendScreen:
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'
IconButton: IconButton:
size_hint: 0.6, 1 size_hint: 0.5, 1
on_release: s.parent.do_save() icon: 'atlas://electrum/gui/kivy/theming/light/copy'
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')
on_release: s.parent.do_paste() on_release: s.parent.do_paste()
IconButton: IconButton:
id: qr 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)) on_release: Clock.schedule_once(lambda dt: app.scan_qr(on_complete=app.on_qr))
icon: 'atlas://electrum/gui/kivy/theming/light/camera' icon: 'atlas://electrum/gui/kivy/theming/light/camera'
BoxLayout:
size_hint: 1, None
height: '48dp'
Button: Button:
text: _('Clear') text: _('Clear')
on_release: s.parent.do_clear()
Widget:
size_hint: 1, 1 size_hint: 1, 1
on_release: s.parent.do_clear()
Button: Button:
text: _('Pay') text: _('Pay')
size_hint: 1, 1 size_hint: 1, 1
on_release: s.parent.do_send() on_release: s.parent.do_send()
Widget: Widget:
size_hint: 1, 1 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', interests = ['wallet_updated', 'network_updated', 'blockchain_updated',
'new_transaction', 'status', 'new_transaction', 'status',
'banner', 'verified', 'fee', 'fee_histogram', 'on_quotes', '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'] 'ln_payment_completed', 'ln_payment_attempt']
# To avoid leaking references to "self" that prevent the # To avoid leaking references to "self" that prevent the
# window from being GC-ed when closed, callbacks should be # window from being GC-ed when closed, callbacks should be
@ -362,7 +362,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
wallet, tx = args wallet, tx = args
if wallet == self.wallet: if wallet == self.wallet:
self.tx_notification_queue.put(tx) 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 # Handle in GUI thread
self.network_signal.emit(event, args) self.network_signal.emit(event, args)
elif event == 'on_quotes': elif event == 'on_quotes':
@ -404,10 +404,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.fee_slider.update() self.fee_slider.update()
self.require_fee_update = True self.require_fee_update = True
self.history_model.on_fee_histogram() self.history_model.on_fee_histogram()
elif event == 'ln_message': elif event == 'payment_received':
lnworker, message, htlc_id = args wallet, key, status = args
if lnworker == self.wallet.lnworker: if wallet == self.wallet:
self.notify(message) self.notify(_('Payment received') + '\n' + key)
else: else:
self.logger.info(f"unexpected network_qt signal: {event} {args}") self.logger.info(f"unexpected network_qt signal: {event} {args}")
@ -1039,24 +1039,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.invoice_list.update() self.invoice_list.update()
self.clear_receive_tab() 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): def sign_payment_request(self, addr):
alias = self.config.get('alias') alias = self.config.get('alias')
alias_privkey = None alias_privkey = None

58
electrum/gui/qt/request_list.py

@ -90,9 +90,9 @@ class RequestList(MyTreeView):
if req is None: if req is None:
self.update() self.update()
return return
req = self.parent.get_request_URI(key) req = self.wallet.get_request_URI(key)
elif request_type == REQUEST_TYPE_LN: 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: if req is None:
self.update() self.update()
return return
@ -107,51 +107,37 @@ class RequestList(MyTreeView):
self.model().clear() self.model().clear()
self.update_headers(self.__class__.headers) self.update_headers(self.__class__.headers)
for req in self.wallet.get_sorted_requests(self.config): for req in self.wallet.get_sorted_requests(self.config):
address = req['address'] request_type = REQUEST_TYPE_LN if req.get('lightning', False) else REQUEST_TYPE_BITCOIN
if address not in domain:
continue
timestamp = req.get('time', 0) timestamp = req.get('time', 0)
amount = req.get('amount') amount = req.get('amount')
expiration = req.get('exp', None)
message = req['memo'] message = req['memo']
date = format_time(timestamp) date = format_time(timestamp)
status = req.get('status') status = req.get('status')
signature = req.get('sig')
requestor = req.get('name', '')
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,'')] labels = [date, message, amount_str, pr_tooltips.get(status,'')]
items = [QStandardItem(e) for e in labels] items = [QStandardItem(e) for e in labels]
self.set_editability(items) self.set_editability(items)
if signature is not None: items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE)
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.STATUS].setIcon(read_QIcon(pr_icons.get(status))) 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) 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() 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 # sort requests by date
self.model().sort(self.Columns.DATE) self.model().sort(self.Columns.DATE)
# hide list if empty # hide list if empty
@ -192,7 +178,7 @@ class RequestList(MyTreeView):
def create_menu_bitcoin_payreq(self, menu, addr): def create_menu_bitcoin_payreq(self, menu, addr):
menu.addAction(_("Copy Address"), lambda: self.parent.do_copy('Address', 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(_("Save as BIP70 file"), lambda: self.parent.export_payment_request(addr))
menu.addAction(_("Delete"), lambda: self.parent.delete_payment_request(addr)) menu.addAction(_("Delete"), lambda: self.parent.delete_payment_request(addr))
run_hook('receive_list_menu', menu, addr) run_hook('receive_list_menu', menu, addr)

2
electrum/lnpeer.py

@ -1249,7 +1249,7 @@ class Peer(Logger):
id=htlc_id, id=htlc_id,
payment_preimage=preimage) payment_preimage=preimage)
await self.await_remote(chan, remote_ctn) 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, async def fail_htlc(self, chan: Channel, htlc_id: int, onion_packet: OnionPacket,
reason: OnionRoutingFailureMessage): reason: OnionRoutingFailureMessage):

25
electrum/lnworker.py

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

59
electrum/wallet.py

@ -34,6 +34,7 @@ import json
import copy import copy
import errno import errno
import traceback import traceback
import operator
from functools import partial from functools import partial
from numbers import Number from numbers import Number
from decimal import Decimal from decimal import Decimal
@ -44,7 +45,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler,
format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates, format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates,
WalletFileException, BitcoinException, WalletFileException, BitcoinException,
InvalidPassword, format_time, timestamp_to_datetime, Satoshis, 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 .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)
@ -1134,10 +1135,10 @@ class Abstract_Wallet(AddressSynchronizer):
return wrapper return wrapper
def get_unused_addresses(self): def get_unused_addresses(self):
# fixme: use slots from expired requests
domain = self.get_receiving_addresses() 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) 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 @check_returned_address
def get_unused_address(self): def get_unused_address(self):
@ -1218,31 +1219,50 @@ class Abstract_Wallet(AddressSynchronizer):
out['websocket_port'] = config.get('websocket_port', 9999) out['websocket_port'] = config.get('websocket_port', 9999)
return out 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): def get_request_status(self, key):
r = self.receive_requests.get(key) r = self.receive_requests.get(key)
if r is None: if r is None:
return PR_UNKNOWN return PR_UNKNOWN
address = r['address'] address = r['address']
amount = r.get('amount') amount = r.get('amount', 0)
timestamp = r.get('time', 0) timestamp = r.get('time', 0)
if timestamp and type(timestamp) != int: if timestamp and type(timestamp) != int:
timestamp = 0 timestamp = 0
expiration = r.get('exp') expiration = r.get('exp')
if expiration and type(expiration) != int: if expiration and type(expiration) != int:
expiration = 0 expiration = 0
conf = None
if amount: paid, conf = self.get_payment_status(address, amount)
if self.is_up_to_date(): status = PR_PAID if paid else PR_UNPAID
paid, conf = self.get_payment_status(address, amount) if status == PR_UNPAID and expiration is not None and time.time() > timestamp + expiration:
status = PR_PAID if paid else PR_UNPAID status = PR_EXPIRED
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
return status, conf 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): def make_payment_request(self, addr, amount, message, expiration):
timestamp = int(time.time()) timestamp = int(time.time())
_id = bh2u(sha256d(addr + "%d"%timestamp))[0:10] _id = bh2u(sha256d(addr + "%d"%timestamp))[0:10]
@ -1306,9 +1326,12 @@ class Abstract_Wallet(AddressSynchronizer):
return True return True
def get_sorted_requests(self, config): def get_sorted_requests(self, config):
keys = map(lambda x: (self.get_address_index(x), x), self.receive_requests.keys()) """ sorted by timestamp """
sorted_keys = sorted(filter(lambda x: x[0] is not None, keys)) out = [self.get_payment_request(x, config) for x in self.receive_requests.keys()]
return [self.get_payment_request(x[1], config) for x in sorted_keys] if self.lnworker:
out += self.lnworker.get_invoices()
out.sort(key=operator.itemgetter('time'))
return out
def get_fingerprint(self): def get_fingerprint(self):
raise NotImplementedError() raise NotImplementedError()

Loading…
Cancel
Save