306cac192b
The LnAddr, lndecode and lnencode naming didn't imply that it is bolt 11 specific, making it confusing to work with, now that there are also bolt 12 "lnaddr". Renaming it to *bolt11* creates a clear separation to bolt 12 things and reduces mental load. This commit is pure renaming (using the PyCharm IDE refactor function), except for the removal of the `object` inheritance of LnAddr/BOLT11Addr, this is Python 2 legacy.
138 lines
5.0 KiB
Python
138 lines
5.0 KiB
Python
import urllib
|
|
import urllib.parse
|
|
import re
|
|
from decimal import Decimal
|
|
from typing import Optional
|
|
|
|
from . import bitcoin
|
|
from .util import format_satoshis_plain
|
|
from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
|
|
from .bolt11 import decode_bolt11_invoice, BOLT11DecodeException
|
|
|
|
# note: when checking against these, use .lower() to support case-insensitivity
|
|
BITCOIN_BIP21_URI_SCHEME = 'bitcoin'
|
|
LIGHTNING_URI_SCHEME = 'lightning'
|
|
|
|
# note: URI scheme handler registrations are duplicated all over the codebase:
|
|
# - for Android: contrib/android/bitcoin_intent.xml
|
|
# - for Linux Desktop: electrum.desktop
|
|
# - for Windows (setup.exe): contrib/build-wine/electrum.nsi
|
|
# - for macOS: contrib/osx/pyinstaller.spec
|
|
|
|
|
|
class InvalidBitcoinURI(Exception):
|
|
pass
|
|
|
|
|
|
def parse_bip21_URI(uri: str) -> dict:
|
|
"""Raises InvalidBitcoinURI on malformed URI."""
|
|
|
|
if not isinstance(uri, str):
|
|
raise InvalidBitcoinURI(f"expected string, not {repr(uri)}")
|
|
|
|
if ':' not in uri:
|
|
if not bitcoin.is_address(uri):
|
|
raise InvalidBitcoinURI("Not a bitcoin address")
|
|
return {'address': uri}
|
|
|
|
u = urllib.parse.urlparse(uri)
|
|
if u.scheme.lower() != BITCOIN_BIP21_URI_SCHEME:
|
|
raise InvalidBitcoinURI("Not a bitcoin URI")
|
|
address = u.path
|
|
|
|
# python for android fails to parse query
|
|
if address.find('?') > 0:
|
|
address, query = u.path.split('?')
|
|
pq = urllib.parse.parse_qs(query)
|
|
else:
|
|
pq = urllib.parse.parse_qs(u.query)
|
|
|
|
for k, v in pq.items():
|
|
if len(v) != 1:
|
|
raise InvalidBitcoinURI(f'Duplicate Key: {repr(k)}')
|
|
if k.startswith('req-'):
|
|
# we have no support for any req-* query parameters
|
|
raise InvalidBitcoinURI(f'Unsupported Key: {repr(k)}')
|
|
|
|
out = {k: v[0] for k, v in pq.items()}
|
|
if address:
|
|
if not bitcoin.is_address(address):
|
|
raise InvalidBitcoinURI(f"Invalid bitcoin address: {address}")
|
|
out['address'] = address
|
|
if 'amount' in out:
|
|
am = out['amount']
|
|
try:
|
|
m = re.match(r'([0-9.]+)X([0-9])', am)
|
|
if m:
|
|
k = int(m.group(2)) - 8
|
|
amount = Decimal(m.group(1)) * pow(Decimal(10), k)
|
|
else:
|
|
amount = Decimal(am) * COIN
|
|
if amount > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN or amount <= 0:
|
|
raise InvalidBitcoinURI(f"amount is out-of-bounds: {amount!r} BTC")
|
|
out['amount'] = int(amount)
|
|
except Exception as e:
|
|
raise InvalidBitcoinURI(f"failed to parse 'amount' field: {repr(e)}") from e
|
|
if 'message' in out:
|
|
out['message'] = out['message']
|
|
out['memo'] = out['message']
|
|
if 'time' in out:
|
|
try:
|
|
out['time'] = int(out['time'])
|
|
except Exception as e:
|
|
raise InvalidBitcoinURI(f"failed to parse 'time' field: {repr(e)}") from e
|
|
if 'exp' in out:
|
|
try:
|
|
out['exp'] = int(out['exp'])
|
|
except Exception as e:
|
|
raise InvalidBitcoinURI(f"failed to parse 'exp' field: {repr(e)}") from e
|
|
if 'sig' in out:
|
|
try:
|
|
out['sig'] = bitcoin.base_decode(out['sig'], base=58).hex()
|
|
except Exception as e:
|
|
raise InvalidBitcoinURI(f"failed to parse 'sig' field: {repr(e)}") from e
|
|
if 'lightning' in out:
|
|
try:
|
|
lnaddr = decode_bolt11_invoice(out['lightning'])
|
|
except BOLT11DecodeException as e:
|
|
raise InvalidBitcoinURI(f"Failed to decode 'lightning' field: {e!r}") from e
|
|
amount_sat = out.get('amount')
|
|
if amount_sat:
|
|
# allow small leeway due to msat precision
|
|
if lnaddr.get_amount_sat() is None or abs(amount_sat - int(lnaddr.get_amount_sat())) > 1:
|
|
raise InvalidBitcoinURI("Inconsistent lightning field in bip21: amount")
|
|
address = out.get('address')
|
|
ln_fallback_addr = lnaddr.get_fallback_address()
|
|
if address and ln_fallback_addr:
|
|
if ln_fallback_addr != address:
|
|
raise InvalidBitcoinURI("Inconsistent lightning field in bip21: address")
|
|
|
|
return out
|
|
|
|
|
|
def create_bip21_uri(addr, amount_sat: Optional[int], message: Optional[str],
|
|
*, extra_query_params: Optional[dict] = None) -> str:
|
|
if not bitcoin.is_address(addr):
|
|
return ""
|
|
if extra_query_params is None:
|
|
extra_query_params = {}
|
|
query = []
|
|
if amount_sat:
|
|
query.append('amount=%s' % format_satoshis_plain(amount_sat))
|
|
if message:
|
|
query.append('message=%s' % urllib.parse.quote(message))
|
|
for k, v in extra_query_params.items():
|
|
if not isinstance(k, str) or k != urllib.parse.quote(k):
|
|
raise Exception(f"illegal key for URI: {repr(k)}")
|
|
v = urllib.parse.quote(v)
|
|
query.append(f"{k}={v}")
|
|
p = urllib.parse.ParseResult(
|
|
scheme=BITCOIN_BIP21_URI_SCHEME,
|
|
netloc='',
|
|
path=addr,
|
|
params='',
|
|
query='&'.join(query),
|
|
fragment=''
|
|
)
|
|
return str(urllib.parse.urlunparse(p))
|