From 1a4d596dbfc7b8d006715ec4bcf31fed4a655037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Mon, 7 Aug 2023 22:23:35 +0200 Subject: [PATCH 1/5] [REFACTOR] use bolt11 library refactored to use https://pypi.org/project/bolt11/ --- .flake8 | 8 ++++++ cashu/lightning/fake.py | 61 ++++++++++++++++++++--------------------- cashu/mint/ledger.py | 14 ++++++---- cashu/wallet/wallet.py | 7 +++-- poetry.lock | 18 ++++++++++++ pyproject.toml | 1 + 6 files changed, 69 insertions(+), 40 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..5b4a8d71 --- /dev/null +++ b/.flake8 @@ -0,0 +1,8 @@ +[flake8] +max-line-length = 150 +exclude = cashu/nostr +ignore = + # E203 whitespace before ':' black does not like it + E203, + # W503: line break before binary operator + W503, diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index dfeed100..08a93289 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -2,9 +2,12 @@ import hashlib import random from datetime import datetime -from typing import AsyncGenerator, Dict, Optional, Set +from typing import AsyncGenerator, Optional, Set, Union + +from bolt11.decode import decode +from bolt11.encode import encode +from bolt11.types import Bolt11, MilliSatoshi -from ..core.bolt11 import Invoice, decode, encode from .base import ( InvoiceResponse, PaymentResponse, @@ -39,45 +42,40 @@ async def create_invoice( memo: Optional[str] = None, description_hash: Optional[bytes] = None, unhashed_description: Optional[bytes] = None, - **kwargs, + expiry: Optional[int] = None, + **_, ) -> InvoiceResponse: - data: Dict = { - "out": False, - "amount": amount * 1000, - "currency": "bc", - "privkey": self.privkey, - "memo": memo, - "description_hash": b"", - "description": "", - "fallback": None, - "expires": kwargs.get("expiry"), - "timestamp": datetime.now().timestamp(), - "route": None, - "tags_set": [], - } + tags: dict[str, Union[str, int]] = {} if description_hash: - data["tags_set"] = ["h"] - data["description_hash"] = description_hash + tags["h"] = bytes.hex(description_hash) elif unhashed_description: - data["tags_set"] = ["d"] - data["description_hash"] = hashlib.sha256(unhashed_description).digest() + tags["h"] = hashlib.sha256(unhashed_description).hexdigest() else: - data["tags_set"] = ["d"] - data["memo"] = memo - data["description"] = memo - randomHash = ( + tags["d"] = memo or "" + + if expiry: + tags["x"] = expiry + # random hash + checking_id = ( self.privkey[:6] + hashlib.sha256(str(random.getrandbits(256)).encode()).hexdigest()[6:] ) - data["paymenthash"] = randomHash - payment_request = encode(data) - checking_id = randomHash - + tags["p"] = checking_id + bolt11 = Bolt11( + currency="bc", + amount_msat=MilliSatoshi(amount * 1000), + date=int(datetime.now().timestamp()), + tags=tags, + ) + payment_request = encode(bolt11, self.privkey) return InvoiceResponse(True, checking_id, payment_request) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: invoice = decode(bolt11) - + if not invoice.payment_hash: + return PaymentResponse( + ok=False, error_message="Missing payment_hash in invoice!" + ) if invoice.payment_hash[:6] == self.privkey[:6] or BRR: await self.queue.put(invoice) self.paid_invoices.add(invoice.payment_hash) @@ -96,5 +94,6 @@ async def get_payment_status(self, _: str) -> PaymentStatus: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: while True: - value: Invoice = await self.queue.get() + value: Bolt11 = await self.queue.get() + assert value.payment_hash, "Missing payment_hash in paid_invoices_stream" yield value.payment_hash diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 8f972d58..add0c8f3 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -3,9 +3,9 @@ import time from typing import Dict, List, Literal, Optional, Set, Tuple, Union +from bolt11.decode import decode from loguru import logger -from ..core import bolt11 from ..core.base import ( BlindedMessage, BlindedSignature, @@ -927,7 +927,8 @@ async def melt( logger.trace("verified proofs") total_provided = sum_proofs(proofs) - invoice_obj = bolt11.decode(invoice) + invoice_obj = decode(invoice) + assert invoice_obj.amount_msat, "invoice has no amount" invoice_amount = math.ceil(invoice_obj.amount_msat / 1000) if settings.mint_max_peg_out and invoice_amount > settings.mint_max_peg_out: raise NotAllowedError( @@ -983,8 +984,8 @@ async def melt( async def check_proof_state( self, proofs: List[Proof] ) -> Tuple[List[bool], List[bool]]: - """Checks if provided proofs are spend or are pending. - Used by wallets to check if their proofs have been redeemed by a receiver or they are still in-flight in a transaction. + """Checks if provided proofs are spend or are pending. Used by wallets to check + if their proofs have been redeemed by a receiver or they are still in-flight in a transaction. Returns two lists that are in the same order as the provided proofs. Wallet must match the list to the proofs they have provided in order to figure out which proof is spendable or pending @@ -1014,7 +1015,9 @@ async def check_fees(self, pr: str): # hack: check if it's internal, if it exists, it will return paid = False, # if id does not exist (not internal), it returns paid = None if settings.lightning: - decoded_invoice = bolt11.decode(pr) + decoded_invoice = decode(pr) + assert decoded_invoice.amount_msat, "invoice has no amount" + assert decoded_invoice.payment_hash, "invoice has no payment hash" amount = math.ceil(decoded_invoice.amount_msat / 1000) logger.trace( "check_fees: checking lightning invoice:" @@ -1108,7 +1111,6 @@ async def split( logger.trace("split successful") return promises - return prom_fst, prom_snd async def restore( self, outputs: List[BlindedMessage] diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 8926ffa7..2cec5e9b 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -11,11 +11,12 @@ import requests from bip32 import BIP32 +from bolt11.decode import decode +from bolt11.types import Bolt11 from loguru import logger from mnemonic import Mnemonic from requests import Response -from ..core import bolt11 as bolt11 from ..core.base import ( BlindedMessage, BlindedSignature, @@ -44,7 +45,6 @@ TokenV3Token, WalletKeyset, ) -from ..core.bolt11 import Invoice as InvoiceBolt11 from ..core.crypto import b_dhke from ..core.crypto.secp import PrivateKey, PublicKey from ..core.db import Database @@ -1447,7 +1447,8 @@ async def get_pay_amount_with_fees(self, invoice: str): Decodes the amount from a Lightning invoice and returns the total amount (amount+fees) to be paid. """ - decoded_invoice: InvoiceBolt11 = bolt11.decode(invoice) + decoded_invoice: Bolt11 = decode(invoice) + assert decoded_invoice.amount_msat, "Invoice has no amount." # check if it's an internal payment fees = int((await self.check_fees(invoice))["fee"]) logger.debug(f"Mint wants {fees} sat as fee reserve.") diff --git a/poetry.lock b/poetry.lock index 33e4df0c..0aa4d99d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -148,6 +148,24 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "bolt11" +version = "1.0.4" +description = "A library for encoding and decoding BOLT11 payment requests." +optional = false +python-versions = ">=3.8.1,<4.0.0" +files = [ + {file = "bolt11-1.0.4-py3-none-any.whl", hash = "sha256:9da33b9a59cdf0a665c93df03a891232f4a6e2773b55edbb54075f965b7bd95b"}, + {file = "bolt11-1.0.4.tar.gz", hash = "sha256:79cdf900a27037337138afa4334f44207f487e6934d66f094b1ed10418a9e9fe"}, +] + +[package.dependencies] +base58 = ">=2.1.1,<3.0.0" +bech32 = ">=1.2.0,<2.0.0" +bitstring = ">=3,<4" +ecdsa = ">=0.18.0,<0.19.0" +secp256k1 = ">=0.14.0,<0.15.0" + [[package]] name = "certifi" version = "2023.7.22" diff --git a/pyproject.toml b/pyproject.toml index f5e34b2c..afff1d4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ psycopg2-binary = { version = "^2.9.7", optional = true } httpx = "^0.24.1" bip32 = "^3.4" mnemonic = "^0.20" +bolt11 = "^1.0.4" [tool.poetry.extras] pgsql = ["psycopg2-binary"] From 3ea409706f624855e7c36baf051ce830dce35f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Sun, 13 Aug 2023 12:26:00 +0200 Subject: [PATCH 2/5] update to bolt11 2.0.0 --- cashu/lightning/fake.py | 41 ++++++++++++++++++++++++++++------------- cashu/mint/ledger.py | 1 - pyproject.toml | 2 +- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/cashu/lightning/fake.py b/cashu/lightning/fake.py index 08a93289..5ef181f6 100644 --- a/cashu/lightning/fake.py +++ b/cashu/lightning/fake.py @@ -2,10 +2,12 @@ import hashlib import random from datetime import datetime -from typing import AsyncGenerator, Optional, Set, Union +from os import urandom +from typing import AsyncGenerator, Optional, Set from bolt11.decode import decode from bolt11.encode import encode +from bolt11.models.tags import TagChar, Tags from bolt11.types import Bolt11, MilliSatoshi from .base import ( @@ -43,39 +45,53 @@ async def create_invoice( description_hash: Optional[bytes] = None, unhashed_description: Optional[bytes] = None, expiry: Optional[int] = None, + payment_secret: Optional[bytes] = None, **_, ) -> InvoiceResponse: - tags: dict[str, Union[str, int]] = {} + tags = Tags() if description_hash: - tags["h"] = bytes.hex(description_hash) + tags.add(TagChar.description_hash, description_hash.hex()) elif unhashed_description: - tags["h"] = hashlib.sha256(unhashed_description).hexdigest() + tags.add( + TagChar.description_hash, + hashlib.sha256(unhashed_description).hexdigest(), + ) else: - tags["d"] = memo or "" + tags.add(TagChar.description, memo or "") if expiry: - tags["x"] = expiry + tags.add(TagChar.expire_time, expiry) + # random hash checking_id = ( self.privkey[:6] + hashlib.sha256(str(random.getrandbits(256)).encode()).hexdigest()[6:] ) - tags["p"] = checking_id + + tags.add(TagChar.payment_hash, checking_id) + + if payment_secret: + secret = payment_secret.hex() + else: + secret = urandom(32).hex() + tags.add(TagChar.payment_secret, secret) + bolt11 = Bolt11( currency="bc", amount_msat=MilliSatoshi(amount * 1000), date=int(datetime.now().timestamp()), tags=tags, ) + payment_request = encode(bolt11, self.privkey) return InvoiceResponse(True, checking_id, payment_request) async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: - invoice = decode(bolt11) - if not invoice.payment_hash: - return PaymentResponse( - ok=False, error_message="Missing payment_hash in invoice!" - ) + try: + invoice = decode(bolt11) + except Exception as exc: + return PaymentResponse(ok=False, error_message=str(exc)) + if invoice.payment_hash[:6] == self.privkey[:6] or BRR: await self.queue.put(invoice) self.paid_invoices.add(invoice.payment_hash) @@ -95,5 +111,4 @@ async def get_payment_status(self, _: str) -> PaymentStatus: async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: while True: value: Bolt11 = await self.queue.get() - assert value.payment_hash, "Missing payment_hash in paid_invoices_stream" yield value.payment_hash diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index add0c8f3..27d7ee9c 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -1017,7 +1017,6 @@ async def check_fees(self, pr: str): if settings.lightning: decoded_invoice = decode(pr) assert decoded_invoice.amount_msat, "invoice has no amount" - assert decoded_invoice.payment_hash, "invoice has no payment hash" amount = math.ceil(decoded_invoice.amount_msat / 1000) logger.trace( "check_fees: checking lightning invoice:" diff --git a/pyproject.toml b/pyproject.toml index afff1d4b..facb5e6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ psycopg2-binary = { version = "^2.9.7", optional = true } httpx = "^0.24.1" bip32 = "^3.4" mnemonic = "^0.20" -bolt11 = "^1.0.4" +bolt11 = "^2.0.0" [tool.poetry.extras] pgsql = ["psycopg2-binary"] From c94d44a2f433871b5983269b744a6cd7e4c62ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Thu, 24 Aug 2023 09:50:26 +0200 Subject: [PATCH 3/5] update to 2.0.4 --- poetry.lock | 21 +++++++++++---------- pyproject.toml | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0aa4d99d..8dec1682 100644 --- a/poetry.lock +++ b/poetry.lock @@ -150,21 +150,22 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "bolt11" -version = "1.0.4" +version = "2.0.4" description = "A library for encoding and decoding BOLT11 payment requests." optional = false -python-versions = ">=3.8.1,<4.0.0" +python-versions = ">=3.8.1" files = [ - {file = "bolt11-1.0.4-py3-none-any.whl", hash = "sha256:9da33b9a59cdf0a665c93df03a891232f4a6e2773b55edbb54075f965b7bd95b"}, - {file = "bolt11-1.0.4.tar.gz", hash = "sha256:79cdf900a27037337138afa4334f44207f487e6934d66f094b1ed10418a9e9fe"}, + {file = "bolt11-2.0.4-py3-none-any.whl", hash = "sha256:a94ab866e15e81d1119009c72ffa21911473913cc4035abb35e16aa552fee84d"}, + {file = "bolt11-2.0.4.tar.gz", hash = "sha256:965c7281bc74a2ef8ea7ba99d4bda1cca8a7a391b8a35c67cfd3ae80ccaaadd5"}, ] [package.dependencies] -base58 = ">=2.1.1,<3.0.0" -bech32 = ">=1.2.0,<2.0.0" -bitstring = ">=3,<4" -ecdsa = ">=0.18.0,<0.19.0" -secp256k1 = ">=0.14.0,<0.15.0" +base58 = "*" +bech32 = "*" +bitstring = "*" +click = "*" +ecdsa = "*" +secp256k1 = "*" [[package]] name = "certifi" @@ -1667,4 +1668,4 @@ pgsql = ["psycopg2-binary"] [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "b801dad77ac4c6b9aca8f25c4fe4d8bab9cb998a2e12d91d9bbf69cb66bb3085" +content-hash = "f6e1904bda936a3da449e8a9ddaea900a2fdbc3f16021fe58ae690e163ecc24a" diff --git a/pyproject.toml b/pyproject.toml index facb5e6f..afb0e3c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ psycopg2-binary = { version = "^2.9.7", optional = true } httpx = "^0.24.1" bip32 = "^3.4" mnemonic = "^0.20" -bolt11 = "^2.0.0" +bolt11 = "^2.0.4" [tool.poetry.extras] pgsql = ["psycopg2-binary"] From e451dbc6a42f1ed65ddc24ceda039b3f869274a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Thu, 24 Aug 2023 09:51:00 +0200 Subject: [PATCH 4/5] remove --- .flake8 | 8 - cashu/core/bolt11.py | 369 ------------------------------------------- 2 files changed, 377 deletions(-) delete mode 100644 .flake8 delete mode 100644 cashu/core/bolt11.py diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 5b4a8d71..00000000 --- a/.flake8 +++ /dev/null @@ -1,8 +0,0 @@ -[flake8] -max-line-length = 150 -exclude = cashu/nostr -ignore = - # E203 whitespace before ':' black does not like it - E203, - # W503: line break before binary operator - W503, diff --git a/cashu/core/bolt11.py b/cashu/core/bolt11.py deleted file mode 100644 index 3c59fbb5..00000000 --- a/cashu/core/bolt11.py +++ /dev/null @@ -1,369 +0,0 @@ -import hashlib -import re -import time -from binascii import unhexlify -from decimal import Decimal -from typing import List, NamedTuple, Optional - -import bitstring # type: ignore -import secp256k1 -from bech32 import CHARSET, bech32_decode, bech32_encode -from ecdsa import SECP256k1, VerifyingKey # type: ignore -from ecdsa.util import sigdecode_string # type: ignore - - -class Route(NamedTuple): - pubkey: str - short_channel_id: str - base_fee_msat: int - ppm_fee: int - cltv: int - - -class Invoice(object): - payment_hash: str - amount_msat: int = 0 - description: Optional[str] = None - description_hash: Optional[str] = None - payee: Optional[str] = None - date: int - expiry: int = 3600 - secret: Optional[str] = None - route_hints: List[Route] = [] - min_final_cltv_expiry: int = 18 - - -def decode(pr: str) -> Invoice: - """bolt11 decoder, - based on https://github.com/rustyrussell/lightning-payencode/blob/master/lnaddr.py - """ - - hrp, decoded_data = bech32_decode(pr) - if hrp is None or decoded_data is None: - raise ValueError("Bad bech32 checksum") - if not hrp.startswith("ln"): - raise ValueError("Does not start with ln") - - bitarray = _u5_to_bitarray(decoded_data) - - # final signature 65 bytes, split it off. - if len(bitarray) < 65 * 8: - raise ValueError("Too short to contain signature") - - # extract the signature - signature = bitarray[-65 * 8 :].tobytes() - - # the tagged fields as a bitstream - data = bitstring.ConstBitStream(bitarray[: -65 * 8]) - - # build the invoice object - invoice = Invoice() - - # decode the amount from the hrp - m = re.search(r"[^\d]+", hrp[2:]) - if m: - amountstr = hrp[2 + m.end() :] - if amountstr != "": - invoice.amount_msat = _unshorten_amount(amountstr) - - # pull out date - invoice.date = data.read(35).uint - - while data.pos != data.len: - tag, tagdata, data = _pull_tagged(data) - data_length = len(tagdata) / 5 - - if tag == "d": - invoice.description = _trim_to_bytes(tagdata).decode("utf-8") - elif tag == "h" and data_length == 52: - invoice.description_hash = _trim_to_bytes(tagdata).hex() - elif tag == "p" and data_length == 52: - 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 - - -def encode(options): - """Convert options into LnAddr and pass it to the encoder""" - addr = LnAddr() - addr.currency = options["currency"] - addr.fallback = options["fallback"] if options["fallback"] else None - if options["amount"]: - addr.amount = options["amount"] - if options["timestamp"]: - addr.date = int(options["timestamp"]) - - addr.paymenthash = unhexlify(options["paymenthash"]) - - if options["description"]: - addr.tags.append(("d", options["description"])) - if options["description_hash"]: - addr.tags.append(("h", options["description_hash"])) - if options["expires"]: - addr.tags.append(("x", options["expires"])) - - if options["fallback"]: - addr.tags.append(("f", options["fallback"])) - if options["route"]: - for r in options["route"]: - splits = r.split("/") - route = [] - while len(splits) >= 5: - route.append( - ( - unhexlify(splits[0]), - unhexlify(splits[1]), - int(splits[2]), - int(splits[3]), - int(splits[4]), - ) - ) - splits = splits[5:] - assert len(splits) == 0 - addr.tags.append(("r", route)) - return lnencode(addr, options["privkey"]) - - -def lnencode(addr, privkey): - if addr.amount: - amount = Decimal(str(addr.amount)) - # We can only send down to millisatoshi. - if amount * 10**12 % 10: - raise ValueError( - "Cannot encode {}: too many decimal places".format(addr.amount) - ) - - amount = addr.currency + shorten_amount(amount) - else: - amount = addr.currency if addr.currency else "" - - hrp = "ln" + amount + "0n" - - # Start with the timestamp - data = bitstring.pack("uint:35", addr.date) - - # Payment hash - data += tagged_bytes("p", addr.paymenthash) - tags_set = set() - - for k, v in addr.tags: - # BOLT #11: - # - # A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields, - if k in ("d", "h", "n", "x"): - if k in tags_set: - raise ValueError("Duplicate '{}' tag".format(k)) - - if k == "r": - route = bitstring.BitArray() - for step in v: - pubkey, channel, feebase, feerate, cltv = step - route.append( - bitstring.BitArray(pubkey) - + bitstring.BitArray(channel) - + bitstring.pack("intbe:32", feebase) - + bitstring.pack("intbe:32", feerate) - + bitstring.pack("intbe:16", cltv) - ) - data += tagged("r", route) - elif k == "f": - data += encode_fallback(v, addr.currency) - elif k == "d": - data += tagged_bytes("d", v.encode()) - elif k == "x": - # Get minimal length by trimming leading 5 bits at a time. - expirybits = bitstring.pack("intbe:64", v)[4:64] - while expirybits.startswith("0b00000"): - expirybits = expirybits[5:] - data += tagged("x", expirybits) - elif k == "h": - data += tagged_bytes("h", v) - elif k == "n": - data += tagged_bytes("n", v) - else: - # FIXME: Support unknown tags? - raise ValueError("Unknown tag {}".format(k)) - - tags_set.add(k) - - # BOLT #11: - # - # A writer MUST include either a `d` or `h` field, and MUST NOT include - # both. - if "d" in tags_set and "h" in tags_set: - raise ValueError("Cannot include both 'd' and 'h'") - if not "d" in tags_set and not "h" in tags_set: - raise ValueError("Must include either 'd' or 'h'") - - # We actually sign the hrp, then data (padded to 8 bits with zeroes). - privkey = secp256k1.PrivateKey(bytes(unhexlify(privkey))) - sig = privkey.ecdsa_sign_recoverable( - bytearray([ord(c) for c in hrp]) + data.tobytes() - ) - # This doesn't actually serialize, but returns a pair of values :( - sig, recid = privkey.ecdsa_recoverable_serialize(sig) - data += bytes(sig) + bytes([recid]) - - return bech32_encode(hrp, bitarray_to_u5(data)) - - -class LnAddr(object): - def __init__( - self, paymenthash=None, amount=None, currency="bc", tags=None, date=None - ): - self.date = int(time.time()) if not date else int(date) - self.tags = [] if not tags else tags - self.unknown_tags = [] - self.paymenthash = paymenthash - self.signature = None - self.pubkey = None - self.currency = currency - self.amount = amount - - def __str__(self): - return "LnAddr[{}, amount={}{} tags=[{}]]".format( - hexlify(self.pubkey.serialize()).decode("utf-8"), - self.amount, - self.currency, - ", ".join([k + "=" + str(v) for k, v in self.tags]), - ) - - -def shorten_amount(amount): - """Given an amount in bitcoin, shorten it""" - # Convert to pico initially - amount = int(amount * 10**12) - units = ["p", "n", "u", "m", ""] - for unit in units: - if amount % 1000 == 0: - amount //= 1000 - else: - break - return str(amount) + unit - - -def _unshorten_amount(amount: str) -> int: - """Given a shortened amount, return millisatoshis""" - # BOLT #11: - # The following `multiplier` letters are defined: - # - # * `m` (milli): multiply by 0.001 - # * `u` (micro): multiply by 0.000001 - # * `n` (nano): multiply by 0.000000001 - # * `p` (pico): multiply by 0.000000000001 - units = {"p": 10**12, "n": 10**9, "u": 10**6, "m": 10**3} - unit = str(amount)[-1] - - # BOLT #11: - # A reader SHOULD fail if `amount` contains a non-digit, or is followed by - # anything except a `multiplier` in the table above. - if not re.fullmatch(r"\d+[pnum]?", str(amount)): - raise ValueError("Invalid amount '{}'".format(amount)) - - if unit in units: - return int(int(amount[:-1]) * 100_000_000_000 / units[unit]) - else: - return int(amount) * 100_000_000_000 - - -def _pull_tagged(stream): - tag = stream.read(5).uint - length = stream.read(5).uint * 32 + stream.read(5).uint - return (CHARSET[tag], stream.read(length * 5), stream) - - -def is_p2pkh(currency, prefix): - return prefix == base58_prefix_map[currency][0] - - -def is_p2sh(currency, prefix): - return prefix == base58_prefix_map[currency][1] - - -# Tagged field containing BitArray -def tagged(char, l): - # Tagged fields need to be zero-padded to 5 bits. - while l.len % 5 != 0: - l.append("0b0") - return ( - bitstring.pack( - "uint:5, uint:5, uint:5", - CHARSET.find(char), - (l.len / 5) / 32, - (l.len / 5) % 32, - ) - + l - ) - - -def tagged_bytes(char, l): - return tagged(char, bitstring.BitArray(l)) - - -def _trim_to_bytes(barr): - # Adds a byte if necessary. - b = barr.tobytes() - if barr.len % 8 != 0: - return b[:-1] - 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: List[int]) -> bitstring.BitArray: - ret = bitstring.BitArray() - for a in arr: - ret += bitstring.pack("uint:5", a) - return ret - - -def bitarray_to_u5(barr): - assert barr.len % 5 == 0 - ret = [] - s = bitstring.ConstBitStream(barr) - while s.pos != s.len: - ret.append(s.read(5).uint) - return ret From 2988c43c76d8641ad4d60f67027e20a72437f33d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Thu, 24 Aug 2023 23:41:26 +0200 Subject: [PATCH 5/5] more obvious bolt11_encode --- cashu/mint/ledger.py | 6 +++--- cashu/wallet/wallet.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 27d7ee9c..b31fcf7a 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -3,7 +3,7 @@ import time from typing import Dict, List, Literal, Optional, Set, Tuple, Union -from bolt11.decode import decode +from bolt11.decode import decode as bolt11_decode from loguru import logger from ..core.base import ( @@ -927,7 +927,7 @@ async def melt( logger.trace("verified proofs") total_provided = sum_proofs(proofs) - invoice_obj = decode(invoice) + invoice_obj = bolt11_decode(invoice) assert invoice_obj.amount_msat, "invoice has no amount" invoice_amount = math.ceil(invoice_obj.amount_msat / 1000) if settings.mint_max_peg_out and invoice_amount > settings.mint_max_peg_out: @@ -1015,7 +1015,7 @@ async def check_fees(self, pr: str): # hack: check if it's internal, if it exists, it will return paid = False, # if id does not exist (not internal), it returns paid = None if settings.lightning: - decoded_invoice = decode(pr) + decoded_invoice = bolt11_decode(pr) assert decoded_invoice.amount_msat, "invoice has no amount" amount = math.ceil(decoded_invoice.amount_msat / 1000) logger.trace( diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 2cec5e9b..a58abcda 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -11,7 +11,7 @@ import requests from bip32 import BIP32 -from bolt11.decode import decode +from bolt11.decode import decode as bolt11_decode from bolt11.types import Bolt11 from loguru import logger from mnemonic import Mnemonic @@ -1447,7 +1447,7 @@ async def get_pay_amount_with_fees(self, invoice: str): Decodes the amount from a Lightning invoice and returns the total amount (amount+fees) to be paid. """ - decoded_invoice: Bolt11 = decode(invoice) + decoded_invoice: Bolt11 = bolt11_decode(invoice) assert decoded_invoice.amount_msat, "Invoice has no amount." # check if it's an internal payment fees = int((await self.check_fees(invoice))["fee"])