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:
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}"
|
||||
Reference in New Issue
Block a user