mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-03-04 09:58:33 +01:00
Wallets/unhashed_description (#870)
* new argument: unhashed_description * accept in api * set unhashed_description for memo case * bolt11.py: dont be like CLN, accept the hash * send hash to lnd in b64 * fix cln * skip descr_hash for cln * skip * format
This commit is contained in:
parent
3457ff101e
commit
e5d8c500d2
16 changed files with 101 additions and 21 deletions
|
@ -216,7 +216,7 @@ def lnencode(addr, privkey):
|
||||||
expirybits = expirybits[5:]
|
expirybits = expirybits[5:]
|
||||||
data += tagged("x", expirybits)
|
data += tagged("x", expirybits)
|
||||||
elif k == "h":
|
elif k == "h":
|
||||||
data += tagged_bytes("h", hashlib.sha256(v.encode("utf-8")).digest())
|
data += tagged_bytes("h", v)
|
||||||
elif k == "n":
|
elif k == "n":
|
||||||
data += tagged_bytes("n", v)
|
data += tagged_bytes("n", v)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -54,6 +54,7 @@ async def create_invoice(
|
||||||
amount: int, # in satoshis
|
amount: int, # in satoshis
|
||||||
memo: str,
|
memo: str,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
|
unhashed_description: Optional[bytes] = None,
|
||||||
extra: Optional[Dict] = None,
|
extra: Optional[Dict] = None,
|
||||||
webhook: Optional[str] = None,
|
webhook: Optional[str] = None,
|
||||||
internal: Optional[bool] = False,
|
internal: Optional[bool] = False,
|
||||||
|
@ -65,7 +66,10 @@ async def create_invoice(
|
||||||
wallet = FAKE_WALLET if internal else WALLET
|
wallet = FAKE_WALLET if internal else WALLET
|
||||||
|
|
||||||
ok, checking_id, payment_request, error_message = await wallet.create_invoice(
|
ok, checking_id, payment_request, error_message = await wallet.create_invoice(
|
||||||
amount=amount, memo=invoice_memo, description_hash=description_hash
|
amount=amount,
|
||||||
|
memo=invoice_memo,
|
||||||
|
description_hash=description_hash,
|
||||||
|
unhashed_description=unhashed_description,
|
||||||
)
|
)
|
||||||
if not ok:
|
if not ok:
|
||||||
raise InvoiceFailure(error_message or "unexpected backend error.")
|
raise InvoiceFailure(error_message or "unexpected backend error.")
|
||||||
|
|
|
@ -141,6 +141,7 @@ class CreateInvoiceData(BaseModel):
|
||||||
memo: Optional[str] = None
|
memo: Optional[str] = None
|
||||||
unit: Optional[str] = "sat"
|
unit: Optional[str] = "sat"
|
||||||
description_hash: Optional[str] = None
|
description_hash: Optional[str] = None
|
||||||
|
unhashed_description: Optional[str] = None
|
||||||
lnurl_callback: Optional[str] = None
|
lnurl_callback: Optional[str] = None
|
||||||
lnurl_balance_check: Optional[str] = None
|
lnurl_balance_check: Optional[str] = None
|
||||||
extra: Optional[dict] = None
|
extra: Optional[dict] = None
|
||||||
|
@ -152,9 +153,15 @@ class CreateInvoiceData(BaseModel):
|
||||||
async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||||
if data.description_hash:
|
if data.description_hash:
|
||||||
description_hash = unhexlify(data.description_hash)
|
description_hash = unhexlify(data.description_hash)
|
||||||
|
unhashed_description = b""
|
||||||
|
memo = ""
|
||||||
|
elif data.unhashed_description:
|
||||||
|
unhashed_description = unhexlify(data.unhashed_description)
|
||||||
|
description_hash = b""
|
||||||
memo = ""
|
memo = ""
|
||||||
else:
|
else:
|
||||||
description_hash = b""
|
description_hash = b""
|
||||||
|
unhashed_description = b""
|
||||||
memo = data.memo or LNBITS_SITE_TITLE
|
memo = data.memo or LNBITS_SITE_TITLE
|
||||||
if data.unit == "sat":
|
if data.unit == "sat":
|
||||||
amount = int(data.amount)
|
amount = int(data.amount)
|
||||||
|
@ -170,6 +177,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||||
amount=amount,
|
amount=amount,
|
||||||
memo=memo,
|
memo=memo,
|
||||||
description_hash=description_hash,
|
description_hash=description_hash,
|
||||||
|
unhashed_description=unhashed_description,
|
||||||
extra=data.extra,
|
extra=data.extra,
|
||||||
webhook=data.webhook,
|
webhook=data.webhook,
|
||||||
internal=data.internal,
|
internal=data.internal,
|
||||||
|
|
|
@ -46,12 +46,19 @@ class ClicheWallet(Wallet):
|
||||||
amount: int,
|
amount: int,
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
|
unhashed_description: Optional[bytes] = None,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
if description_hash:
|
if unhashed_description or description_hash:
|
||||||
description_hash_hashed = hashlib.sha256(description_hash).hexdigest()
|
description_hash_str = (
|
||||||
|
description_hash.hex()
|
||||||
|
if description_hash
|
||||||
|
else hashlib.sha256(unhashed_description).hexdigest()
|
||||||
|
if unhashed_description
|
||||||
|
else None
|
||||||
|
)
|
||||||
ws = create_connection(self.endpoint)
|
ws = create_connection(self.endpoint)
|
||||||
ws.send(
|
ws.send(
|
||||||
f"create-invoice --msatoshi {amount*1000} --description_hash {description_hash_hashed}"
|
f"create-invoice --msatoshi {amount*1000} --description_hash {description_hash_str}"
|
||||||
)
|
)
|
||||||
r = ws.recv()
|
r = ws.recv()
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -82,21 +82,24 @@ class CoreLightningWallet(Wallet):
|
||||||
amount: int,
|
amount: int,
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
|
unhashed_description: Optional[bytes] = None,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
label = "lbl{}".format(random.random())
|
label = "lbl{}".format(random.random())
|
||||||
msat: int = int(amount * 1000)
|
msat: int = int(amount * 1000)
|
||||||
try:
|
try:
|
||||||
if description_hash and not self.supports_description_hash:
|
if description_hash:
|
||||||
raise Unsupported("description_hash")
|
raise Unsupported("description_hash")
|
||||||
|
if unhashed_description and not self.supports_description_hash:
|
||||||
|
raise Unsupported("unhashed_description")
|
||||||
r = self.ln.invoice(
|
r = self.ln.invoice(
|
||||||
msatoshi=msat,
|
msatoshi=msat,
|
||||||
label=label,
|
label=label,
|
||||||
description=description_hash.decode("utf-8")
|
description=unhashed_description.decode("utf-8")
|
||||||
if description_hash
|
if unhashed_description
|
||||||
else memo,
|
else memo,
|
||||||
exposeprivatechannels=True,
|
exposeprivatechannels=True,
|
||||||
deschashonly=True
|
deschashonly=True
|
||||||
if description_hash
|
if unhashed_description
|
||||||
else False, # we can't pass None here
|
else False, # we can't pass None here
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -69,11 +69,14 @@ class EclairWallet(Wallet):
|
||||||
amount: int,
|
amount: int,
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
|
unhashed_description: Optional[bytes] = None,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
|
|
||||||
data: Dict = {"amountMsat": amount * 1000}
|
data: Dict = {"amountMsat": amount * 1000}
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = hashlib.sha256(description_hash).hexdigest()
|
data["description_hash"] = description_hash.hex()
|
||||||
|
elif unhashed_description:
|
||||||
|
data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest()
|
||||||
else:
|
else:
|
||||||
data["description"] = memo or ""
|
data["description"] = memo or ""
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ class FakeWallet(Wallet):
|
||||||
amount: int,
|
amount: int,
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
|
unhashed_description: Optional[bytes] = None,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
# we set a default secret since FakeWallet is used for internal=True invoices
|
# we set a default secret since FakeWallet is used for internal=True invoices
|
||||||
# and the user might not have configured a secret yet
|
# and the user might not have configured a secret yet
|
||||||
|
@ -61,7 +62,10 @@ class FakeWallet(Wallet):
|
||||||
data["timestamp"] = datetime.now().timestamp()
|
data["timestamp"] = datetime.now().timestamp()
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["tags_set"] = ["h"]
|
data["tags_set"] = ["h"]
|
||||||
data["description_hash"] = description_hash.decode("utf-8")
|
data["description_hash"] = description_hash
|
||||||
|
elif unhashed_description:
|
||||||
|
data["tags_set"] = ["d"]
|
||||||
|
data["description_hash"] = hashlib.sha256(unhashed_description).digest()
|
||||||
else:
|
else:
|
||||||
data["tags_set"] = ["d"]
|
data["tags_set"] = ["d"]
|
||||||
data["memo"] = memo
|
data["memo"] = memo
|
||||||
|
|
|
@ -57,10 +57,13 @@ class LNbitsWallet(Wallet):
|
||||||
amount: int,
|
amount: int,
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
|
unhashed_description: Optional[bytes] = None,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict = {"out": False, "amount": amount}
|
data: Dict = {"out": False, "amount": amount}
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = hashlib.sha256(description_hash).hexdigest()
|
data["description_hash"] = description_hash.hex()
|
||||||
|
elif unhashed_description:
|
||||||
|
data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest()
|
||||||
else:
|
else:
|
||||||
data["memo"] = memo or ""
|
data["memo"] = memo or ""
|
||||||
|
|
||||||
|
|
|
@ -134,14 +134,15 @@ class LndWallet(Wallet):
|
||||||
amount: int,
|
amount: int,
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
|
unhashed_description: Optional[bytes] = None,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
params: Dict = {"value": amount, "expiry": 600, "private": True}
|
params: Dict = {"value": amount, "expiry": 600, "private": True}
|
||||||
|
|
||||||
if description_hash:
|
if description_hash:
|
||||||
|
params["description_hash"] = description_hash
|
||||||
|
elif unhashed_description:
|
||||||
params["description_hash"] = hashlib.sha256(
|
params["description_hash"] = hashlib.sha256(
|
||||||
description_hash
|
unhashed_description
|
||||||
).digest() # as bytes directly
|
).digest() # as bytes directly
|
||||||
|
|
||||||
else:
|
else:
|
||||||
params["memo"] = memo or ""
|
params["memo"] = memo or ""
|
||||||
|
|
||||||
|
|
|
@ -73,11 +73,17 @@ class LndRestWallet(Wallet):
|
||||||
amount: int,
|
amount: int,
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
|
unhashed_description: Optional[bytes] = None,
|
||||||
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict = {"value": amount, "private": True}
|
data: Dict = {"value": amount, "private": True}
|
||||||
if description_hash:
|
if description_hash:
|
||||||
|
data["description_hash"] = base64.b64encode(description_hash).decode(
|
||||||
|
"ascii"
|
||||||
|
)
|
||||||
|
elif unhashed_description:
|
||||||
data["description_hash"] = base64.b64encode(
|
data["description_hash"] = base64.b64encode(
|
||||||
hashlib.sha256(description_hash).digest()
|
hashlib.sha256(unhashed_description).digest()
|
||||||
).decode("ascii")
|
).decode("ascii")
|
||||||
else:
|
else:
|
||||||
data["memo"] = memo or ""
|
data["memo"] = memo or ""
|
||||||
|
|
|
@ -52,10 +52,14 @@ class LNPayWallet(Wallet):
|
||||||
amount: int,
|
amount: int,
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
|
unhashed_description: Optional[bytes] = None,
|
||||||
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict = {"num_satoshis": f"{amount}"}
|
data: Dict = {"num_satoshis": f"{amount}"}
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = hashlib.sha256(description_hash).hexdigest()
|
data["description_hash"] = description_hash.hex()
|
||||||
|
elif unhashed_description:
|
||||||
|
data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest()
|
||||||
else:
|
else:
|
||||||
data["memo"] = memo or ""
|
data["memo"] = memo or ""
|
||||||
|
|
||||||
|
|
|
@ -52,10 +52,14 @@ class LntxbotWallet(Wallet):
|
||||||
amount: int,
|
amount: int,
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
|
unhashed_description: Optional[bytes] = None,
|
||||||
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
data: Dict = {"amt": str(amount)}
|
data: Dict = {"amt": str(amount)}
|
||||||
if description_hash:
|
if description_hash:
|
||||||
data["description_hash"] = hashlib.sha256(description_hash).hexdigest()
|
data["description_hash"] = description_hash.hex()
|
||||||
|
elif unhashed_description:
|
||||||
|
data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest()
|
||||||
else:
|
else:
|
||||||
data["memo"] = memo or ""
|
data["memo"] = memo or ""
|
||||||
|
|
||||||
|
|
|
@ -54,8 +54,10 @@ class OpenNodeWallet(Wallet):
|
||||||
amount: int,
|
amount: int,
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
|
unhashed_description: Optional[bytes] = None,
|
||||||
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
if description_hash:
|
if description_hash or unhashed_description:
|
||||||
raise Unsupported("description_hash")
|
raise Unsupported("description_hash")
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
|
|
|
@ -93,6 +93,8 @@ class SparkWallet(Wallet):
|
||||||
amount: int,
|
amount: int,
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
|
unhashed_description: Optional[bytes] = None,
|
||||||
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
label = "lbs{}".format(random.random())
|
label = "lbs{}".format(random.random())
|
||||||
checking_id = label
|
checking_id = label
|
||||||
|
@ -102,7 +104,13 @@ class SparkWallet(Wallet):
|
||||||
r = await self.invoicewithdescriptionhash(
|
r = await self.invoicewithdescriptionhash(
|
||||||
msatoshi=amount * 1000,
|
msatoshi=amount * 1000,
|
||||||
label=label,
|
label=label,
|
||||||
description_hash=hashlib.sha256(description_hash).hexdigest(),
|
description_hash=description_hash.hex(),
|
||||||
|
)
|
||||||
|
elif unhashed_description:
|
||||||
|
r = await self.invoicewithdescriptionhash(
|
||||||
|
msatoshi=amount * 1000,
|
||||||
|
label=label,
|
||||||
|
description_hash=hashlib.sha256(unhashed_description).hexdigest(),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
r = await self.invoice(
|
r = await self.invoice(
|
||||||
|
|
|
@ -18,6 +18,7 @@ class VoidWallet(Wallet):
|
||||||
amount: int,
|
amount: int,
|
||||||
memo: Optional[str] = None,
|
memo: Optional[str] = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: Optional[bytes] = None,
|
||||||
|
**kwargs,
|
||||||
) -> InvoiceResponse:
|
) -> InvoiceResponse:
|
||||||
raise Unsupported("")
|
raise Unsupported("")
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ from lnbits.core.views.api import (
|
||||||
api_payment,
|
api_payment,
|
||||||
api_payments_create_invoice,
|
api_payments_create_invoice,
|
||||||
)
|
)
|
||||||
|
from lnbits.settings import wallet_class
|
||||||
|
|
||||||
from ...helpers import get_random_invoice_data
|
from ...helpers import get_random_invoice_data
|
||||||
|
|
||||||
|
@ -192,11 +193,32 @@ async def test_api_payment_with_key(invoice, inkey_headers_from):
|
||||||
|
|
||||||
|
|
||||||
# check POST /api/v1/payments: invoice creation with a description hash
|
# check POST /api/v1/payments: invoice creation with a description hash
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
wallet_class.__name__ in ["CoreLightningWallet"],
|
||||||
|
reason="wallet does not support description_hash",
|
||||||
|
)
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_invoice_with_description_hash(client, inkey_headers_to):
|
async def test_create_invoice_with_description_hash(client, inkey_headers_to):
|
||||||
data = await get_random_invoice_data()
|
data = await get_random_invoice_data()
|
||||||
descr_hash = hashlib.sha256("asdasdasd".encode("utf-8")).hexdigest()
|
descr_hash = hashlib.sha256("asdasdasd".encode("utf-8")).hexdigest()
|
||||||
data["description_hash"] = "asdasdasd".encode("utf-8").hex()
|
data["description_hash"] = descr_hash
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
"/api/v1/payments", json=data, headers=inkey_headers_to
|
||||||
|
)
|
||||||
|
invoice = response.json()
|
||||||
|
|
||||||
|
invoice_bolt11 = bolt11.decode(invoice["payment_request"])
|
||||||
|
assert invoice_bolt11.description_hash == descr_hash
|
||||||
|
assert invoice_bolt11.description is None
|
||||||
|
return invoice
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_invoice_with_unhashed_description(client, inkey_headers_to):
|
||||||
|
data = await get_random_invoice_data()
|
||||||
|
descr_hash = hashlib.sha256("asdasdasd".encode("utf-8")).hexdigest()
|
||||||
|
data["unhashed_description"] = "asdasdasd".encode("utf-8").hex()
|
||||||
|
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
"/api/v1/payments", json=data, headers=inkey_headers_to
|
"/api/v1/payments", json=data, headers=inkey_headers_to
|
||||||
|
|
Loading…
Add table
Reference in a new issue