Browse Source

bolt11.py now supports everything.

aiosqlite
fiatjaf 5 years ago
parent
commit
2cecaa229b
  1. 81
      lnbits/bolt11.py
  2. 26
      lnbits/extensions/withdraw/views_api.py
  3. 1
      requirements.txt

81
lnbits/bolt11.py

@ -2,14 +2,32 @@
import bitstring import bitstring
import re import re
import hashlib
from typing import List, NamedTuple
from bech32 import bech32_decode, CHARSET from bech32 import bech32_decode, CHARSET
from ecdsa import SECP256k1, VerifyingKey
from ecdsa.util import sigdecode_string
from binascii import unhexlify
class Route(NamedTuple):
pubkey: str
short_channel_id: str
base_fee_msat: int
ppm_fee: int
cltv: int
class Invoice(object): class Invoice(object):
def __init__(self): payment_hash: str = None
self.payment_hash: str = None amount_msat: int = 0
self.amount_msat: int = 0 description: str = None
self.description: str = None payee: str = None
date: int = None
expiry: int = 3600
secret: str = None
route_hints: List[Route] = []
min_final_cltv_expiry: int = 18
def decode(pr: str) -> Invoice: def decode(pr: str) -> Invoice:
@ -26,13 +44,20 @@ def decode(pr: str) -> Invoice:
data = u5_to_bitarray(data) data = u5_to_bitarray(data)
# Final signature 65 bytes, split it off. # final signature 65 bytes, split it off.
if len(data) < 65 * 8: if len(data) < 65 * 8:
raise ValueError("Too short to contain signature") raise ValueError("Too short to contain signature")
# extract the signature
signature = data[-65 * 8 :].tobytes()
# the tagged fields as a bitstream
data = bitstring.ConstBitStream(data[: -65 * 8]) data = bitstring.ConstBitStream(data[: -65 * 8])
# build the invoice object
invoice = Invoice() invoice = Invoice()
# decode the amount from the hrp
m = re.search("[^\d]+", hrp[2:]) m = re.search("[^\d]+", hrp[2:])
if m: if m:
amountstr = hrp[2 + m.end() :] amountstr = hrp[2 + m.end() :]
@ -40,11 +65,10 @@ def decode(pr: str) -> Invoice:
invoice.amount_msat = unshorten_amount(amountstr) invoice.amount_msat = unshorten_amount(amountstr)
# pull out date # pull out date
data.read(35).uint invoice.date = data.read(35).uint
while data.pos != data.len: while data.pos != data.len:
tag, tagdata, data = pull_tagged(data) tag, tagdata, data = pull_tagged(data)
data_length = len(tagdata) / 5 data_length = len(tagdata) / 5
if tag == "d": if tag == "d":
@ -53,6 +77,41 @@ def decode(pr: str) -> Invoice:
invoice.description = trim_to_bytes(tagdata).hex() invoice.description = trim_to_bytes(tagdata).hex()
elif tag == "p" and data_length == 52: elif tag == "p" and data_length == 52:
invoice.payment_hash = trim_to_bytes(tagdata).hex() invoice.payment_hash = trim_to_bytes(tagdata).hex()
elif tag == "x":
invoice.expiry = tagdata.uint
elif tag == "n":
invoice.payee = trim_to_bytes(tagdata).hex()
# this won't work in most cases, we must extract the payee
# from the signature
elif tag == "s":
invoice.secret = trim_to_bytes(tagdata).hex()
elif tag == "r":
s = bitstring.ConstBitStream(tagdata)
while s.pos + 264 + 64 + 32 + 32 + 16 < s.len:
route = Route(
pubkey=s.read(264).tobytes().hex(),
short_channel_id=readable_scid(s.read(64).intbe),
base_fee_msat=s.read(32).intbe,
ppm_fee=s.read(32).intbe,
cltv=s.read(16).intbe,
)
invoice.route_hints.append(route)
# BOLT #11:
# A reader MUST check that the `signature` is valid (see the `n` tagged
# field specified below).
# A reader MUST use the `n` field to validate the signature instead of
# performing signature recovery if a valid `n` field is provided.
message = bytearray([ord(c) for c in hrp]) + data.tobytes()
sig = signature[0:64]
if invoice.payee:
key = VerifyingKey.from_string(unhexlify(invoice.payee), curve=SECP256k1)
key.verify(sig, message, hashlib.sha256, sigdecode=sigdecode_string)
else:
keys = VerifyingKey.from_public_key_recovery(sig, message, SECP256k1, hashlib.sha256)
signaling_byte = signature[64]
key = keys[int(signaling_byte)]
invoice.payee = key.to_string("compressed").hex()
return invoice return invoice
@ -101,6 +160,14 @@ def trim_to_bytes(barr):
return b return b
def readable_scid(short_channel_id: int) -> str:
return "{blockheight}x{transactionindex}x{outputindex}".format(
blockheight=((short_channel_id >> 40) & 0xFFFFFF),
transactionindex=((short_channel_id >> 16) & 0xFFFFFF),
outputindex=(short_channel_id & 0xFFFF),
)
def u5_to_bitarray(arr): def u5_to_bitarray(arr):
ret = bitstring.BitArray() ret = bitstring.BitArray()
for a in arr: for a in arr:

26
lnbits/extensions/withdraw/views_api.py

@ -2,12 +2,11 @@ from datetime import datetime
from flask import g, jsonify, request from flask import g, jsonify, request
from http import HTTPStatus from http import HTTPStatus
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
import shortuuid # type: ignore import shortuuid # type: ignore
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.core.services import pay_invoice from lnbits.core.services import pay_invoice
from lnbits.decorators import api_check_wallet_key, api_validate_post_request from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.helpers import urlsafe_short_hash
from lnbits.extensions.withdraw import withdraw_ext from lnbits.extensions.withdraw import withdraw_ext
from .crud import ( from .crud import (
@ -49,7 +48,7 @@ def api_link_retrieve(link_id):
if link.wallet != g.wallet.id: if link.wallet != g.wallet.id:
return jsonify({"message": "Not your withdraw link."}), HTTPStatus.FORBIDDEN return jsonify({"message": "Not your withdraw link."}), HTTPStatus.FORBIDDEN
return jsonify({**link._asdict(), **{"lnurl": link.lnurl}}), HTTPStatus.OK return jsonify({**link._asdict(), **{"lnurl": link.lnurl}}), HTTPStatus.OK
@ -80,7 +79,7 @@ def api_link_create_or_update(link_id=None):
usescsv += "," + str(i + 1) usescsv += "," + str(i + 1)
else: else:
usescsv += "," + str(1) usescsv += "," + str(1)
usescsv = usescsv[1:] usescsv = usescsv[1:]
if link_id: if link_id:
link = get_withdraw_link(link_id, 0) link = get_withdraw_link(link_id, 0)
@ -109,7 +108,9 @@ def api_link_delete(link_id):
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT
#FOR LNURLs WHICH ARE NOT UNIQUE
# FOR LNURLs WHICH ARE NOT UNIQUE
@withdraw_ext.route("/api/v1/lnurl/<unique_hash>", methods=["GET"]) @withdraw_ext.route("/api/v1/lnurl/<unique_hash>", methods=["GET"])
def api_lnurl_response(unique_hash): def api_lnurl_response(unique_hash):
@ -123,13 +124,14 @@ def api_lnurl_response(unique_hash):
usescsv = "" usescsv = ""
for x in range(1, link.uses - link.used): for x in range(1, link.uses - link.used):
usescsv += "," + str(1) usescsv += "," + str(1)
usescsv = usescsv[1:] usescsv = usescsv[1:]
link = update_withdraw_link(link.id, used=link.used + 1, usescsv=usescsv) link = update_withdraw_link(link.id, used=link.used + 1, usescsv=usescsv)
return jsonify(link.lnurl_response.dict()), HTTPStatus.OK return jsonify(link.lnurl_response.dict()), HTTPStatus.OK
#FOR LNURLs WHICH ARE UNIQUE
# FOR LNURLs WHICH ARE UNIQUE
@withdraw_ext.route("/api/v1/lnurl/<unique_hash>/<id_unique_hash>", methods=["GET"]) @withdraw_ext.route("/api/v1/lnurl/<unique_hash>/<id_unique_hash>", methods=["GET"])
def api_lnurl_multi_response(unique_hash, id_unique_hash): def api_lnurl_multi_response(unique_hash, id_unique_hash):
@ -139,11 +141,7 @@ def api_lnurl_multi_response(unique_hash, id_unique_hash):
return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK
useslist = link.usescsv.split(",") useslist = link.usescsv.split(",")
usescsv = "" usescsv = ""
hashed = []
found = False found = False
print(link.uses - link.used)
print("link.uses - link.used")
print("svfsfv")
if link.is_unique == 0: if link.is_unique == 0:
for x in range(link.uses - link.used): for x in range(link.uses - link.used):
usescsv += "," + str(1) usescsv += "," + str(1)
@ -159,10 +157,10 @@ def api_lnurl_multi_response(unique_hash, id_unique_hash):
if not found: if not found:
return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK return jsonify({"status": "ERROR", "reason": "LNURL-withdraw not found."}), HTTPStatus.OK
usescsv = usescsv[1:] usescsv = usescsv[1:]
link = update_withdraw_link(link.id, used=link.used + 1, usescsv=usescsv) link = update_withdraw_link(link.id, used=link.used + 1, usescsv=usescsv)
return jsonify(link.lnurl_response.dict()), HTTPStatus.OK return jsonify(link.lnurl_response.dict()), HTTPStatus.OK
@withdraw_ext.route("/api/v1/lnurl/cb/<unique_hash>", methods=["GET"]) @withdraw_ext.route("/api/v1/lnurl/cb/<unique_hash>", methods=["GET"])
def api_lnurl_callback(unique_hash): def api_lnurl_callback(unique_hash):

1
requirements.txt

@ -5,6 +5,7 @@ cerberus==1.3.2
certifi==2020.6.20 certifi==2020.6.20
chardet==3.0.4 chardet==3.0.4
click==7.1.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' click==7.1.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
ecdsa==0.16.0
flask-assets==2.0 flask-assets==2.0
flask-compress==1.5.0 flask-compress==1.5.0
flask-cors==3.0.8 flask-cors==3.0.8

Loading…
Cancel
Save