electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

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:
Melectrum/gui/kivy/Makefile | 4+---
Melectrum/gui/kivy/main.kv | 3+++
Melectrum/gui/kivy/main_window.py | 26+++++++++++++++++++++-----
Aelectrum/gui/kivy/theming/light/copy.png | 0
Delectrum/gui/kivy/theming/light/lightning_switch.svg | 288-------------------------------------------------------------------------------
Aelectrum/gui/kivy/theming/light/list.png | 0
Melectrum/gui/kivy/uix/dialogs/addresses.py | 7+++----
Melectrum/gui/kivy/uix/dialogs/qr_dialog.py | 12+++++++++++-
Aelectrum/gui/kivy/uix/dialogs/request_dialog.py | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Melectrum/gui/kivy/uix/dialogs/requests.py | 44+++++++++++++++++++++++++++++++-------------
Melectrum/gui/kivy/uix/screens.py | 158+++++++++++++++++++------------------------------------------------------------
Melectrum/gui/kivy/uix/ui_screens/receive.kv | 93+++++++++++++++++++++++--------------------------------------------------------
Melectrum/gui/kivy/uix/ui_screens/send.kv | 36++++++++++++++++++------------------
Melectrum/gui/qt/main_window.py | 30++++++------------------------
Melectrum/gui/qt/request_list.py | 58++++++++++++++++++++++------------------------------------
Melectrum/lnpeer.py | 2+-
Melectrum/lnworker.py | 25++++++++++++++++++++++++-
Melectrum/wallet.py | 59+++++++++++++++++++++++++++++++++++++++++------------------
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()