lnurl for kivy: I need this to buy beers
This commit is contained in:
@@ -18,7 +18,7 @@ from electrum.plugin import run_hook
|
|||||||
from electrum import util
|
from electrum import util
|
||||||
from electrum.util import (profiler, InvalidPassword, send_exception_to_crash_reporter,
|
from electrum.util import (profiler, InvalidPassword, send_exception_to_crash_reporter,
|
||||||
format_satoshis, format_satoshis_plain, format_fee_satoshis,
|
format_satoshis, format_satoshis_plain, format_fee_satoshis,
|
||||||
maybe_extract_lightning_payment_identifier, parse_max_spend)
|
maybe_extract_lightning_payment_identifier, parse_max_spend, is_uri)
|
||||||
from electrum.util import EventListener, event_listener
|
from electrum.util import EventListener, event_listener
|
||||||
from electrum.invoices import PR_PAID, PR_FAILED, Invoice
|
from electrum.invoices import PR_PAID, PR_FAILED, Invoice
|
||||||
from electrum import blockchain
|
from electrum import blockchain
|
||||||
@@ -235,10 +235,6 @@ class ElectrumWindow(App, Logger, EventListener):
|
|||||||
def set_URI(self, uri):
|
def set_URI(self, uri):
|
||||||
self.send_screen.set_URI(uri)
|
self.send_screen.set_URI(uri)
|
||||||
|
|
||||||
@switch_to_send_screen
|
|
||||||
def set_ln_invoice(self, invoice):
|
|
||||||
self.send_screen.set_ln_invoice(invoice)
|
|
||||||
|
|
||||||
def on_new_intent(self, intent):
|
def on_new_intent(self, intent):
|
||||||
data = str(intent.getDataString())
|
data = str(intent.getDataString())
|
||||||
scheme = str(intent.getScheme()).lower()
|
scheme = str(intent.getScheme()).lower()
|
||||||
@@ -482,19 +478,15 @@ class ElectrumWindow(App, Logger, EventListener):
|
|||||||
def on_qr(self, data: str):
|
def on_qr(self, data: str):
|
||||||
from electrum.bitcoin import is_address
|
from electrum.bitcoin import is_address
|
||||||
data = data.strip()
|
data = data.strip()
|
||||||
if is_address(data):
|
if is_address(data): # TODO does this actually work?
|
||||||
self.set_URI(data)
|
self.set_URI(data)
|
||||||
return
|
return
|
||||||
if data.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):
|
if is_uri(data) or maybe_extract_lightning_payment_identifier(data):
|
||||||
self.set_URI(data)
|
self.set_URI(data)
|
||||||
return
|
return
|
||||||
if data.lower().startswith('channel_backup:'):
|
if data.lower().startswith('channel_backup:'):
|
||||||
self.import_channel_backup(data)
|
self.import_channel_backup(data)
|
||||||
return
|
return
|
||||||
bolt11_invoice = maybe_extract_lightning_payment_identifier(data)
|
|
||||||
if bolt11_invoice is not None:
|
|
||||||
self.set_ln_invoice(bolt11_invoice)
|
|
||||||
return
|
|
||||||
# try to decode transaction
|
# try to decode transaction
|
||||||
from electrum.transaction import tx_from_any
|
from electrum.transaction import tx_from_any
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import asyncio
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import threading
|
import threading
|
||||||
from typing import TYPE_CHECKING, List, Optional, Dict, Any
|
from typing import TYPE_CHECKING, List, Optional, Dict, Any
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from kivy.app import App
|
from kivy.app import App
|
||||||
from kivy.clock import Clock
|
from kivy.clock import Clock
|
||||||
@@ -17,8 +18,9 @@ from electrum.invoices import (PR_DEFAULT_EXPIRATION_WHEN_CREATING,
|
|||||||
from electrum import bitcoin, constants
|
from electrum import bitcoin, constants
|
||||||
from electrum.transaction import tx_from_any, PartialTxOutput
|
from electrum.transaction import tx_from_any, PartialTxOutput
|
||||||
from electrum.util import (parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_lightning_payment_identifier,
|
from electrum.util import (parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_lightning_payment_identifier,
|
||||||
InvoiceError, format_time, parse_max_spend)
|
InvoiceError, format_time, parse_max_spend, BITCOIN_BIP21_URI_SCHEME)
|
||||||
from electrum.lnaddr import lndecode, LnInvoiceException
|
from electrum.lnaddr import lndecode, LnInvoiceException
|
||||||
|
from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl, lightning_address_to_url, LNURLError
|
||||||
from electrum.logging import Logger
|
from electrum.logging import Logger
|
||||||
|
|
||||||
from .dialogs.confirm_tx_dialog import ConfirmTxDialog
|
from .dialogs.confirm_tx_dialog import ConfirmTxDialog
|
||||||
@@ -167,17 +169,40 @@ class SendScreen(CScreen, Logger):
|
|||||||
CScreen.__init__(self, **kwargs)
|
CScreen.__init__(self, **kwargs)
|
||||||
Logger.__init__(self)
|
Logger.__init__(self)
|
||||||
self.is_max = False
|
self.is_max = False
|
||||||
|
# note: most the fields get declared in send.kv, this way they are kivy Properties
|
||||||
|
|
||||||
def set_URI(self, text: str):
|
def set_URI(self, text: str):
|
||||||
|
"""Takes
|
||||||
|
Lightning identifiers:
|
||||||
|
* lightning-URI (containing bolt11 or lnurl)
|
||||||
|
* bolt11 invoice
|
||||||
|
* lnurl
|
||||||
|
* lightning address
|
||||||
|
Bitcoin identifiers:
|
||||||
|
* bitcoin-URI
|
||||||
|
* bitcoin address TODO
|
||||||
|
and sets the sending screen.
|
||||||
|
|
||||||
|
TODO maybe rename method...
|
||||||
|
"""
|
||||||
if not self.app.wallet:
|
if not self.app.wallet:
|
||||||
return
|
return
|
||||||
# interpret as lighting URI
|
text = text.strip()
|
||||||
bolt11_invoice = maybe_extract_lightning_payment_identifier(text)
|
if not text:
|
||||||
if bolt11_invoice:
|
return
|
||||||
self.set_ln_invoice(bolt11_invoice)
|
invoice_or_lnurl = maybe_extract_lightning_payment_identifier(text)
|
||||||
# interpret as BIP21 URI
|
if lightning_address_to_url(text):
|
||||||
else:
|
self.set_lnurl6(text)
|
||||||
|
elif invoice_or_lnurl:
|
||||||
|
if invoice_or_lnurl.startswith('lnurl'):
|
||||||
|
self.set_lnurl6(invoice_or_lnurl)
|
||||||
|
else:
|
||||||
|
self.set_bolt11(invoice_or_lnurl)
|
||||||
|
elif text.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):
|
||||||
self.set_bip21(text)
|
self.set_bip21(text)
|
||||||
|
else:
|
||||||
|
self.app.show_error(f"Failed to parse text: {text[:10]}...")
|
||||||
|
return
|
||||||
|
|
||||||
def set_bip21(self, text: str):
|
def set_bip21(self, text: str):
|
||||||
try:
|
try:
|
||||||
@@ -194,7 +219,7 @@ class SendScreen(CScreen, Logger):
|
|||||||
self.payment_request = None
|
self.payment_request = None
|
||||||
self.is_lightning = False
|
self.is_lightning = False
|
||||||
|
|
||||||
def set_ln_invoice(self, invoice: str):
|
def set_bolt11(self, invoice: str):
|
||||||
try:
|
try:
|
||||||
invoice = str(invoice).lower()
|
invoice = str(invoice).lower()
|
||||||
lnaddr = lndecode(invoice)
|
lnaddr = lndecode(invoice)
|
||||||
@@ -207,6 +232,30 @@ class SendScreen(CScreen, Logger):
|
|||||||
self.payment_request = None
|
self.payment_request = None
|
||||||
self.is_lightning = True
|
self.is_lightning = True
|
||||||
|
|
||||||
|
def set_lnurl6(self, lnurl: str):
|
||||||
|
url = lightning_address_to_url(lnurl)
|
||||||
|
if not url:
|
||||||
|
url = decode_lnurl(lnurl)
|
||||||
|
domain = urlparse(url).netloc
|
||||||
|
lnurl_data = request_lnurl(url, self.app.network.send_http_on_proxy)
|
||||||
|
self.lnurl_callback_url = lnurl_data.get('callback')
|
||||||
|
self.lnurl_max_sendable_sat = int(lnurl_data.get('maxSendable')) // 1000
|
||||||
|
self.lnurl_min_sendable_sat = int(lnurl_data.get('minSendable')) // 1000
|
||||||
|
metadata = lnurl_data.get('metadata')
|
||||||
|
tag = lnurl_data.get('tag')
|
||||||
|
|
||||||
|
if tag == 'payRequest':
|
||||||
|
#self.payto_e.setFrozen(True)
|
||||||
|
for m in metadata:
|
||||||
|
if m[0] == 'text/plain':
|
||||||
|
self.is_lnurl = True
|
||||||
|
self.address = "invoice from lnurl"
|
||||||
|
self.message = f"lnurl: {domain}: {m[1]}"
|
||||||
|
self.amount = self.app.format_amount_and_units(self.lnurl_min_sendable_sat)
|
||||||
|
#self.save_button.setDisabled(True)
|
||||||
|
#self.send_button.setText('Get Invoice')
|
||||||
|
self.is_lightning = True
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
if self.app.wallet is None:
|
if self.app.wallet is None:
|
||||||
return
|
return
|
||||||
@@ -263,6 +312,10 @@ class SendScreen(CScreen, Logger):
|
|||||||
self.is_bip70 = False
|
self.is_bip70 = False
|
||||||
self.parsed_URI = None
|
self.parsed_URI = None
|
||||||
self.is_max = False
|
self.is_max = False
|
||||||
|
self.is_lnurl = False
|
||||||
|
self.lnurl_max_sendable_sat = None
|
||||||
|
self.lnurl_min_sendable_sat = None
|
||||||
|
self.lnurl_callback_url = None
|
||||||
|
|
||||||
def set_request(self, pr: 'PaymentRequest'):
|
def set_request(self, pr: 'PaymentRequest'):
|
||||||
self.address = pr.get_requestor()
|
self.address = pr.get_requestor()
|
||||||
@@ -287,11 +340,7 @@ class SendScreen(CScreen, Logger):
|
|||||||
self.app.tx_dialog(tx)
|
self.app.tx_dialog(tx)
|
||||||
return
|
return
|
||||||
# try to decode as URI/address
|
# try to decode as URI/address
|
||||||
bolt11_invoice = maybe_extract_lightning_payment_identifier(data)
|
self.set_URI(data)
|
||||||
if bolt11_invoice is not None:
|
|
||||||
self.set_ln_invoice(bolt11_invoice)
|
|
||||||
else:
|
|
||||||
self.set_URI(data)
|
|
||||||
|
|
||||||
def read_invoice(self):
|
def read_invoice(self):
|
||||||
address = str(self.address)
|
address = str(self.address)
|
||||||
@@ -341,6 +390,34 @@ class SendScreen(CScreen, Logger):
|
|||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def do_pay(self):
|
def do_pay(self):
|
||||||
|
if self.is_lnurl:
|
||||||
|
try:
|
||||||
|
amount = self.app.get_amount(self.amount)
|
||||||
|
except:
|
||||||
|
self.app.show_error(_('Invalid amount') + ':\n' + self.amount)
|
||||||
|
return
|
||||||
|
if not (self.lnurl_min_sendable_sat <= amount <= self.lnurl_max_sendable_sat):
|
||||||
|
self.app.show_error(f'Amount must be between {self.lnurl_min_sendable_sat} and {self.lnurl_max_sendable_sat} sat.')
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
invoice_data = callback_lnurl(
|
||||||
|
self.lnurl_callback_url,
|
||||||
|
params={'amount': amount * 1000},
|
||||||
|
request_over_proxy=self.app.network.send_http_on_proxy,
|
||||||
|
)
|
||||||
|
except LNURLError as e:
|
||||||
|
self.app.show_error(f"LNURL request encountered error: {e}")
|
||||||
|
self.do_clear()
|
||||||
|
return
|
||||||
|
invoice = invoice_data.get('pr')
|
||||||
|
self.set_bolt11(invoice)
|
||||||
|
#self.payto_e.setFrozen(True)
|
||||||
|
#self.amount_e.setDisabled(True)
|
||||||
|
#self.fiat_send_e.setDisabled(True)
|
||||||
|
#self.save_button.setEnabled(True)
|
||||||
|
#self.send_button.setText('Pay...')
|
||||||
|
self.is_lnurl = False
|
||||||
|
return
|
||||||
invoice = self.read_invoice()
|
invoice = self.read_invoice()
|
||||||
if not invoice:
|
if not invoice:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -69,6 +69,7 @@
|
|||||||
message: ''
|
message: ''
|
||||||
is_bip70: False
|
is_bip70: False
|
||||||
is_lightning: False
|
is_lightning: False
|
||||||
|
is_lnurl: False
|
||||||
is_locked: self.is_lightning or self.is_bip70
|
is_locked: self.is_lightning or self.is_bip70
|
||||||
BoxLayout
|
BoxLayout
|
||||||
padding: '12dp', '12dp', '12dp', '12dp'
|
padding: '12dp', '12dp', '12dp', '12dp'
|
||||||
@@ -109,7 +110,7 @@
|
|||||||
id: amount_e
|
id: amount_e
|
||||||
default_text: _('Amount')
|
default_text: _('Amount')
|
||||||
text: s.amount if s.amount else _('Amount')
|
text: s.amount if s.amount else _('Amount')
|
||||||
disabled: root.is_bip70 or (root.is_lightning and s.amount)
|
disabled: root.is_bip70 or (root.is_lightning and s.amount and not root.is_lnurl)
|
||||||
on_release: Clock.schedule_once(lambda dt: app.amount_dialog(s, not root.is_lightning))
|
on_release: Clock.schedule_once(lambda dt: app.amount_dialog(s, not root.is_lightning))
|
||||||
CardSeparator:
|
CardSeparator:
|
||||||
color: blue_bottom.foreground_color
|
color: blue_bottom.foreground_color
|
||||||
@@ -135,6 +136,7 @@
|
|||||||
size_hint: 0.5, 1
|
size_hint: 0.5, 1
|
||||||
on_release: s.do_save()
|
on_release: s.do_save()
|
||||||
icon: f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/save'
|
icon: f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/save'
|
||||||
|
disabled: root.is_lnurl
|
||||||
IconButton:
|
IconButton:
|
||||||
size_hint: 0.5, 1
|
size_hint: 0.5, 1
|
||||||
on_release: s.do_clear()
|
on_release: s.do_clear()
|
||||||
@@ -149,7 +151,7 @@
|
|||||||
size_hint: 1, 1
|
size_hint: 1, 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))
|
||||||
Button:
|
Button:
|
||||||
text: _('Pay')
|
text: _('Pay') if not root.is_lnurl else _('Get Invoice')
|
||||||
size_hint: 1, 1
|
size_hint: 1, 1
|
||||||
on_release: s.do_pay()
|
on_release: s.do_pay()
|
||||||
Widget:
|
Widget:
|
||||||
|
|||||||
Reference in New Issue
Block a user