add lnurl-pay and lightning address support
* bundles all payment identifiers into handle_payment_identifier * adds lnurl decoding * adds lightning address decoding
This commit is contained in:
@@ -18,7 +18,7 @@ from electrum.plugin import run_hook
|
||||
from electrum import util
|
||||
from electrum.util import (profiler, InvalidPassword, send_exception_to_crash_reporter,
|
||||
format_satoshis, format_satoshis_plain, format_fee_satoshis,
|
||||
maybe_extract_bolt11_invoice, parse_max_spend)
|
||||
maybe_extract_lightning_payment_identifier, parse_max_spend)
|
||||
from electrum.util import EventListener, event_listener
|
||||
from electrum.invoices import PR_PAID, PR_FAILED, Invoice
|
||||
from electrum import blockchain
|
||||
@@ -491,7 +491,7 @@ class ElectrumWindow(App, Logger, EventListener):
|
||||
if data.lower().startswith('channel_backup:'):
|
||||
self.import_channel_backup(data)
|
||||
return
|
||||
bolt11_invoice = maybe_extract_bolt11_invoice(data)
|
||||
bolt11_invoice = maybe_extract_lightning_payment_identifier(data)
|
||||
if bolt11_invoice is not None:
|
||||
self.set_ln_invoice(bolt11_invoice)
|
||||
return
|
||||
|
||||
@@ -16,7 +16,7 @@ from electrum.invoices import (PR_DEFAULT_EXPIRATION_WHEN_CREATING,
|
||||
pr_expiration_values, Invoice)
|
||||
from electrum import bitcoin, constants
|
||||
from electrum.transaction import tx_from_any, PartialTxOutput
|
||||
from electrum.util import (parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_bolt11_invoice,
|
||||
from electrum.util import (parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_lightning_payment_identifier,
|
||||
InvoiceError, format_time, parse_max_spend)
|
||||
from electrum.lnaddr import lndecode, LnInvoiceException
|
||||
from electrum.logging import Logger
|
||||
@@ -172,7 +172,7 @@ class SendScreen(CScreen, Logger):
|
||||
if not self.app.wallet:
|
||||
return
|
||||
# interpret as lighting URI
|
||||
bolt11_invoice = maybe_extract_bolt11_invoice(text)
|
||||
bolt11_invoice = maybe_extract_lightning_payment_identifier(text)
|
||||
if bolt11_invoice:
|
||||
self.set_ln_invoice(bolt11_invoice)
|
||||
# interpret as BIP21 URI
|
||||
@@ -287,7 +287,7 @@ class SendScreen(CScreen, Logger):
|
||||
self.app.tx_dialog(tx)
|
||||
return
|
||||
# try to decode as URI/address
|
||||
bolt11_invoice = maybe_extract_bolt11_invoice(data)
|
||||
bolt11_invoice = maybe_extract_lightning_payment_identifier(data)
|
||||
if bolt11_invoice is not None:
|
||||
self.set_ln_invoice(bolt11_invoice)
|
||||
else:
|
||||
|
||||
@@ -87,7 +87,7 @@ class OpenFileEventFilter(QObject):
|
||||
def eventFilter(self, obj, event):
|
||||
if event.type() == QtCore.QEvent.FileOpen:
|
||||
if len(self.windows) >= 1:
|
||||
self.windows[0].pay_to_URI(event.url().toString())
|
||||
self.windows[0].handle_payment_identifier(event.url().toString())
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -383,7 +383,7 @@ class ElectrumGui(BaseElectrumGui, Logger):
|
||||
self.start_new_window(path, uri=None, force_wizard=True)
|
||||
return
|
||||
if uri:
|
||||
window.pay_to_URI(uri)
|
||||
window.handle_payment_identifier(uri)
|
||||
window.bring_to_top()
|
||||
window.setWindowState(window.windowState() & ~QtCore.Qt.WindowMinimized | QtCore.Qt.WindowActive)
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import queue
|
||||
import asyncio
|
||||
from typing import Optional, TYPE_CHECKING, Sequence, List, Union, Dict, Set
|
||||
import concurrent.futures
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from PyQt5.QtGui import QPixmap, QKeySequence, QIcon, QCursor, QFont
|
||||
from PyQt5.QtCore import Qt, QRect, QStringListModel, QSize, pyqtSignal, QPoint
|
||||
@@ -62,7 +63,7 @@ from electrum.util import (format_time,
|
||||
bh2u, bfh, InvalidPassword,
|
||||
UserFacingException,
|
||||
get_new_wallet_name, send_exception_to_crash_reporter,
|
||||
InvalidBitcoinURI, maybe_extract_bolt11_invoice, NotEnoughFunds,
|
||||
InvalidBitcoinURI, maybe_extract_lightning_payment_identifier, NotEnoughFunds,
|
||||
NoDynamicFeeEstimates,
|
||||
AddTransactionException, BITCOIN_BIP21_URI_SCHEME,
|
||||
InvoiceError, parse_max_spend)
|
||||
@@ -81,6 +82,7 @@ from electrum.simple_config import SimpleConfig
|
||||
from electrum.logging import Logger
|
||||
from electrum.lnutil import ln_dummy_address, extract_nodeid, ConnStringFormatError
|
||||
from electrum.lnaddr import lndecode, LnInvoiceException
|
||||
from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl, lightning_address_to_url, LNURLError
|
||||
|
||||
from .exception_window import Exception_Hook
|
||||
from .amountedit import AmountEdit, BTCAmountEdit, FreezableLineEdit, FeerateEdit, SizedFreezableLineEdit
|
||||
@@ -820,7 +822,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
d = self.network.get_donation_address()
|
||||
if d:
|
||||
host = self.network.get_parameters().server.host
|
||||
self.pay_to_URI('bitcoin:%s?message=donation for %s'%(d, host))
|
||||
self.handle_payment_identifier('bitcoin:%s?message=donation for %s' % (d, host))
|
||||
else:
|
||||
self.show_error(_('No donation address for this server'))
|
||||
|
||||
@@ -1573,8 +1575,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
grid.addWidget(self.max_button, 3, 3)
|
||||
|
||||
self.save_button = EnterButton(_("Save"), self.do_save_invoice)
|
||||
self.send_button = EnterButton(_("Pay") + "...", self.do_pay)
|
||||
self.send_button = EnterButton(_("Pay") + "...", self.do_pay_or_get_invoice)
|
||||
self.clear_button = EnterButton(_("Clear"), self.do_clear)
|
||||
self._is_lnurl = False
|
||||
|
||||
buttons = QHBoxLayout()
|
||||
buttons.addStretch(1)
|
||||
@@ -1909,7 +1912,31 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
self.invoice_list.update()
|
||||
self.pending_invoice = None
|
||||
|
||||
def do_pay(self):
|
||||
def do_pay_or_get_invoice(self):
|
||||
if self._is_lnurl:
|
||||
amount = self.amount_e.get_amount()
|
||||
if not (self.lnurl_min_sendable_sat <= amount <= self.lnurl_max_sendable_sat):
|
||||
self.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': self.amount_e.get_amount() * 1000},
|
||||
request_over_proxy=self.network.send_http_on_proxy,
|
||||
)
|
||||
except LNURLError as e:
|
||||
self.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
|
||||
self.pending_invoice = self.read_invoice()
|
||||
if not self.pending_invoice:
|
||||
return
|
||||
@@ -2221,7 +2248,31 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
else:
|
||||
self.payment_request_error_signal.emit()
|
||||
|
||||
def set_ln_invoice(self, invoice: str):
|
||||
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.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.payto_e.setTextNosignal(f"invoice from lnurl")
|
||||
self.message_e.setText(f"lnurl: {domain}: {m[1]}")
|
||||
self.amount_e.setAmount(self.lnurl_min_sendable_sat)
|
||||
self.save_button.setDisabled(True)
|
||||
self.send_button.setText('Get Invoice')
|
||||
self.set_onchain(False)
|
||||
|
||||
def set_bolt11(self, invoice: str):
|
||||
"""Parse ln invoice, and prepare the send tab for it."""
|
||||
try:
|
||||
lnaddr = lndecode(invoice)
|
||||
@@ -2237,9 +2288,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
else:
|
||||
description = ''
|
||||
self.payto_e.setFrozen(True)
|
||||
self.payto_e.setText(pubkey)
|
||||
self.payto_e.setTextNosignal(pubkey)
|
||||
self.payto_e.lightning_invoice = invoice
|
||||
self.message_e.setText(description)
|
||||
if not self.message_e.text():
|
||||
self.message_e.setText(description)
|
||||
if lnaddr.get_amount_sat() is not None:
|
||||
self.amount_e.setAmount(lnaddr.get_amount_sat())
|
||||
self.set_onchain(False)
|
||||
@@ -2267,7 +2319,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
message = out.get('message')
|
||||
lightning = out.get('lightning')
|
||||
if lightning:
|
||||
self.set_ln_invoice(lightning)
|
||||
self.handle_payment_identifier(lightning)
|
||||
return
|
||||
# use label as description (not BIP21 compliant)
|
||||
if label and not message:
|
||||
@@ -2279,20 +2331,41 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
if amount:
|
||||
self.amount_e.setAmount(amount)
|
||||
|
||||
def pay_to_URI(self, text: str):
|
||||
def handle_payment_identifier(self, text: str):
|
||||
"""Takes
|
||||
Lightning identifiers:
|
||||
* lightning-URI (containing bolt11 or lnurl)
|
||||
* bolt11 invoice
|
||||
* lnurl
|
||||
* lightning address
|
||||
Bitcoin identifiers:
|
||||
* bitcoin-URI
|
||||
and sets the sending screen.
|
||||
"""
|
||||
text = text.strip()
|
||||
if not text:
|
||||
return
|
||||
# first interpret as lightning invoice
|
||||
bolt11_invoice = maybe_extract_bolt11_invoice(text)
|
||||
if bolt11_invoice:
|
||||
self.set_ln_invoice(bolt11_invoice)
|
||||
else:
|
||||
invoice_or_lnurl = maybe_extract_lightning_payment_identifier(text)
|
||||
if lightning_address_to_url(text):
|
||||
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(util.BITCOIN_BIP21_URI_SCHEME + ':'):
|
||||
self.set_bip21(text)
|
||||
else:
|
||||
raise ValueError("Could not handle payment identifier.")
|
||||
# update fiat amount
|
||||
self.amount_e.textEdited.emit("")
|
||||
self.show_send_tab()
|
||||
|
||||
def do_clear(self):
|
||||
self.lnurl_max_sendable_sat = None
|
||||
self.lnurl_min_sendable_sat = None
|
||||
self.lnurl_callback_url = None
|
||||
self._is_lnurl = False
|
||||
self.max_button.setChecked(False)
|
||||
self.payment_request = None
|
||||
self.payto_URI = None
|
||||
@@ -2301,6 +2374,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
for e in [self.payto_e, self.message_e, self.amount_e]:
|
||||
e.setText('')
|
||||
e.setFrozen(False)
|
||||
for e in [self.send_button, self.save_button, self.payto_e, self.amount_e, self.fiat_send_e]:
|
||||
e.setEnabled(True)
|
||||
self.update_status()
|
||||
run_hook('do_clear', self)
|
||||
|
||||
@@ -3120,7 +3195,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
return
|
||||
# if the user scanned a bitcoin URI
|
||||
if data.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):
|
||||
self.pay_to_URI(data)
|
||||
self.handle_payment_identifier(data)
|
||||
return
|
||||
if data.lower().startswith('channel_backup:'):
|
||||
self.import_channel_backup(data)
|
||||
|
||||
@@ -29,13 +29,14 @@ from decimal import Decimal
|
||||
from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtGui import QFontMetrics, QFont
|
||||
from PyQt5.QtCore import QTimer
|
||||
|
||||
from electrum import bitcoin
|
||||
from electrum.util import bfh, maybe_extract_bolt11_invoice, BITCOIN_BIP21_URI_SCHEME, parse_max_spend
|
||||
from electrum.util import bfh, parse_max_spend
|
||||
from electrum.transaction import PartialTxOutput
|
||||
from electrum.bitcoin import opcodes, construct_script
|
||||
from electrum.logging import Logger
|
||||
from electrum.lnaddr import LnDecodeException
|
||||
from electrum.lnurl import LNURLError
|
||||
|
||||
from .qrtextedit import ScanQRTextEdit
|
||||
from .completion_text_edit import CompletionTextEdit
|
||||
@@ -84,7 +85,10 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
|
||||
self.heightMax = (self.fontSpacing * 10) + self.verticalMargins
|
||||
|
||||
self.c = None
|
||||
self.textChanged.connect(self.check_text)
|
||||
self.timer = QTimer()
|
||||
self.timer.setSingleShot(True)
|
||||
self.textChanged.connect(self.start_timer)
|
||||
self.timer.timeout.connect(self.check_text)
|
||||
self.outputs = [] # type: List[PartialTxOutput]
|
||||
self.errors = [] # type: List[PayToLineError]
|
||||
self.is_pr = False
|
||||
@@ -94,11 +98,23 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
|
||||
self.lightning_invoice = None
|
||||
self.previous_payto = ''
|
||||
|
||||
def start_timer(self):
|
||||
# we insert a timer between textChanged and check_text to not immediately
|
||||
# resolve lightning addresses, but rather to wait until the address is typed out fully
|
||||
delay_time_msec = 300 # about the average typing time in msec a person types a character
|
||||
self.logger.info("timer fires")
|
||||
self.timer.start(delay_time_msec)
|
||||
|
||||
def setFrozen(self, b):
|
||||
self.setReadOnly(b)
|
||||
self.setStyleSheet(frozen_style if b else normal_style)
|
||||
self.overlay_widget.setHidden(b)
|
||||
|
||||
def setTextNosignal(self, text: str):
|
||||
self.blockSignals(True)
|
||||
self.setText(text)
|
||||
self.blockSignals(False)
|
||||
|
||||
def setGreen(self):
|
||||
self.setStyleSheet(util.ColorScheme.GREEN.as_stylesheet(True))
|
||||
|
||||
@@ -170,14 +186,13 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
|
||||
|
||||
if len(lines) == 1:
|
||||
data = lines[0]
|
||||
# try bip21 URI
|
||||
if data.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):
|
||||
self.win.pay_to_URI(data)
|
||||
return
|
||||
# try LN invoice
|
||||
bolt11_invoice = maybe_extract_bolt11_invoice(data)
|
||||
if bolt11_invoice is not None:
|
||||
self.win.set_ln_invoice(bolt11_invoice)
|
||||
try:
|
||||
self.win.handle_payment_identifier(data)
|
||||
except LNURLError as e:
|
||||
self.show_error(e)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
return
|
||||
# try "address, amount" on-chain format
|
||||
try:
|
||||
|
||||
75
electrum/lnurl.py
Normal file
75
electrum/lnurl.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""Module for lnurl-related functionality."""
|
||||
# https://github.com/sipa/bech32/tree/master/ref/python
|
||||
# https://github.com/lnbits/lnurl
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Callable, Optional
|
||||
import re
|
||||
|
||||
import aiohttp.client_exceptions
|
||||
from aiohttp import ClientResponse
|
||||
|
||||
from electrum.segwit_addr import bech32_decode, Encoding, convertbits
|
||||
from electrum.lnaddr import LnDecodeException
|
||||
|
||||
|
||||
class LNURLError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def decode_lnurl(lnurl: str) -> str:
|
||||
"""Converts bech32 encoded lnurl to url."""
|
||||
decoded_bech32 = bech32_decode(
|
||||
lnurl, ignore_long_length=True
|
||||
)
|
||||
hrp = decoded_bech32.hrp
|
||||
data = decoded_bech32.data
|
||||
if decoded_bech32.encoding is None:
|
||||
raise LnDecodeException("Bad bech32 checksum")
|
||||
if decoded_bech32.encoding != Encoding.BECH32:
|
||||
raise LnDecodeException("Bad bech32 encoding: must be using vanilla BECH32")
|
||||
if not hrp.startswith("lnurl"):
|
||||
raise LnDecodeException("Does not start with lnurl")
|
||||
data = convertbits(data, 5, 8, False)
|
||||
url = bytes(data).decode("utf-8")
|
||||
return url
|
||||
|
||||
|
||||
def request_lnurl(url: str, request_over_proxy: Callable) -> dict:
|
||||
"""Requests payment data from a lnurl."""
|
||||
try:
|
||||
response = request_over_proxy("get", url, timeout=2)
|
||||
except asyncio.TimeoutError as e:
|
||||
raise LNURLError("Server did not reply in time.") from e
|
||||
except aiohttp.client_exceptions.ClientError as e:
|
||||
raise LNURLError(f"Client error: {e}") from e
|
||||
# TODO: handling of specific client errors
|
||||
response = json.loads(response)
|
||||
if "metadata" in response:
|
||||
response["metadata"] = json.loads(response["metadata"])
|
||||
status = response.get("status")
|
||||
if status and status == "ERROR":
|
||||
raise LNURLError(f"LNURL request encountered an error: {response['reason']}")
|
||||
return response
|
||||
|
||||
|
||||
def callback_lnurl(url: str, params: dict, request_over_proxy: Callable) -> dict:
|
||||
"""Requests an invoice from a lnurl supporting server."""
|
||||
try:
|
||||
response = request_over_proxy("get", url, params=params)
|
||||
except aiohttp.client_exceptions.ClientError as e:
|
||||
raise LNURLError(f"Client error: {e}") from e
|
||||
# TODO: handling of specific errors
|
||||
response = json.loads(response)
|
||||
status = response.get("status")
|
||||
if status and status == "ERROR":
|
||||
raise LNURLError(f"LNURL request encountered an error: {response['reason']}")
|
||||
return response
|
||||
|
||||
|
||||
def lightning_address_to_url(address: str) -> Optional[str]:
|
||||
"""Converts an email-type lightning address to a decoded lnurl."""
|
||||
if re.match(r"[^@]+@[^@]+\.[^@]+", address):
|
||||
username, domain = address.split("@")
|
||||
return f"https://{domain}/.well-known/lnurlp/{username}"
|
||||
12
electrum/tests/test_lnurl.py
Normal file
12
electrum/tests/test_lnurl.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from electrum import lnurl
|
||||
|
||||
|
||||
class TestLnurl(TestCase):
|
||||
def test_decode(self):
|
||||
LNURL = (
|
||||
"LNURL1DP68GURN8GHJ7UM9WFMXJCM99E5K7TELWY7NXENRXVMRGDTZXSENJCM98PJNWXQ96S9"
|
||||
)
|
||||
url = lnurl.decode_lnurl(LNURL)
|
||||
self.assertTrue("https://service.io/?q=3fc3645b439ce8e7", url)
|
||||
@@ -1065,7 +1065,7 @@ def create_bip21_uri(addr, amount_sat: Optional[int], message: Optional[str],
|
||||
return str(urllib.parse.urlunparse(p))
|
||||
|
||||
|
||||
def maybe_extract_bolt11_invoice(data: str) -> Optional[str]:
|
||||
def maybe_extract_lightning_payment_identifier(data: str) -> Optional[str]:
|
||||
data = data.strip() # whitespaces
|
||||
data = data.lower()
|
||||
if data.startswith(LIGHTNING_URI_SCHEME + ':ln'):
|
||||
@@ -1076,6 +1076,14 @@ def maybe_extract_bolt11_invoice(data: str) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def is_uri(data: str) -> bool:
|
||||
data = data.lower()
|
||||
if (data.startswith(LIGHTNING_URI_SCHEME + ":") or
|
||||
data.startswith(BITCOIN_BIP21_URI_SCHEME + ':')):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# Python bug (http://bugs.python.org/issue1927) causes raw_input
|
||||
# to be redirected improperly between stdin/stderr on Unix systems
|
||||
#TODO: py3
|
||||
|
||||
@@ -98,7 +98,7 @@ from electrum.wallet_db import WalletDB
|
||||
from electrum.wallet import Wallet
|
||||
from electrum.storage import WalletStorage
|
||||
from electrum.util import print_msg, print_stderr, json_encode, json_decode, UserCancelled
|
||||
from electrum.util import InvalidPassword, BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME
|
||||
from electrum.util import InvalidPassword
|
||||
from electrum.commands import get_parser, known_commands, Commands, config_variables
|
||||
from electrum import daemon
|
||||
from electrum import keystore
|
||||
@@ -362,10 +362,7 @@ def main():
|
||||
|
||||
# check uri
|
||||
uri = config_options.get('url')
|
||||
if uri and not (
|
||||
uri.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':') or
|
||||
uri.lower().startswith(LIGHTNING_URI_SCHEME + ':')
|
||||
):
|
||||
if uri and not util.is_uri(uri):
|
||||
print_stderr('unknown command:', uri)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user