Skip to content

Commit

Permalink
Merge pull request #9 from AngusP/various-improvements
Browse files Browse the repository at this point in the history
Move static (non-request-dependent) properties into PhoenixdLNURLSettings
  • Loading branch information
AngusP authored Apr 8, 2024
2 parents d9c7964 + 3ffc2fb commit 54aeaad
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 44 deletions.
43 changes: 8 additions & 35 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import hashlib
import json
import math
import sys
from contextlib import asynccontextmanager
from typing import Annotated

import aiohttp
import lnurl
import qrcode.image.svg
from fastapi import (
APIRouter,
FastAPI,
Expand All @@ -30,7 +26,6 @@
)
from lnurl.types import MilliSatoshi
from loguru import logger
from qrcode.main import QRCode
from starlette.exceptions import HTTPException as StarletteHTTPException

from .phoenixd_client import (
Expand All @@ -52,16 +47,6 @@
templates = Jinja2Templates(directory="app/templates")


def metadata_for_payrequest(settings: PhoenixdLNURLSettings) -> str:
return json.dumps(
[
["text/plain", f"Zap {settings.username} some sats"],
["text/identifier", settings.lnurl_address()],
# ["image/jpeg;base64", "TODO optional"],
]
)


@router.get(
path="/lnurl",
summary="Get a LUD-01 LNURL QR Code and LUD-16 identifier",
Expand All @@ -71,27 +56,18 @@ def metadata_for_payrequest(settings: PhoenixdLNURLSettings) -> str:
)
async def lnurl_get_lud01(request: Request) -> Response:
settings: PhoenixdLNURLSettings = request.app.state.settings
lnurl_address = settings.lnurl_address()
encoded = lnurl.encode(str(settings.base_url() / "lnurlp" / settings.username))
lnurl_qr = QRCode(
image_factory=qrcode.image.svg.SvgPathFillImage,
box_size=15,
)
lnurl_qr.add_data(encoded)
lnurl_qr.make(fit=True)
lnurl_qr_image = lnurl_qr.make_image()
return templates.TemplateResponse(
name="lnurl-splash.html",
context={
"request": request,
"username": settings.username,
"lnurl_address": lnurl_address,
"lnurl_address": settings.lnurl_address(),
"nostr_address": settings.user_nostr_address,
"profile_image_url": settings.user_profile_image_url,
"meta_description": settings.lnurl_hostname,
"meta_author": lnurl_address,
"encoded_lnurl": encoded,
"lnurl_qr": lnurl_qr_image.to_string(encoding="unicode"),
"meta_author": settings.lnurl_address(),
"encoded_lnurl": settings.lnurl_address_encoded(),
"lnurl_qr": settings.lnurl_qr(),
"smaller_heading": settings.is_long_username(),
},
)
Expand Down Expand Up @@ -135,7 +111,7 @@ async def lnurl_pay_request_lud06(
callback=str(settings.base_url() / f"lnurlp/{username}/callback"),
minSendable=settings.min_sats_receivable * 1000,
maxSendable=settings.max_sats_receivable * 1000,
metadata=metadata_for_payrequest(settings),
metadata=settings.metadata_for_payrequest(),
)
)

Expand Down Expand Up @@ -178,7 +154,7 @@ async def lnurl_pay_request_lud16(
callback=str(settings.base_url() / f"lnurlp/{username}/callback"),
minSendable=settings.min_sats_receivable * 1000,
maxSendable=settings.max_sats_receivable * 1000,
metadata=metadata_for_payrequest(settings),
metadata=settings.metadata_for_payrequest(),
)
)

Expand Down Expand Up @@ -252,14 +228,11 @@ async def lnurl_pay_request_callback_lud06(
).dict(),
)

metadata_hash = hashlib.sha256(
metadata_for_payrequest(settings).encode("UTF-8")
).hexdigest()
invoice: CreateInvoiceResponse = (
await request.app.state.phoenixd_client.createinvoice(
amount_sat=amount_sat,
description=metadata_hash,
external_id=metadata_hash,
description=settings.metadata_hash(),
external_id=settings.metadata_hash(),
)
)
return LnurlPayActionResponse.parse_obj(
Expand Down
10 changes: 6 additions & 4 deletions app/main_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ def test_lnurl_pay_request_lud06_happy():
assert LnurlPayResponse.parse_obj(response.json()) == LnurlPayResponse.parse_obj(
dict(
callback="https://127.0.0.1/lnurlp/satoshi/callback",
minSendable=1000,
maxSendable=500_000,
# NOTE these are millisat values
minSendable=1_000_000,
maxSendable=500_000_000,
metadata=json.dumps(
[
["text/plain", "Zap satoshi some sats"],
Expand Down Expand Up @@ -81,8 +82,9 @@ def test_lnurl_pay_request_lud16_happy():
assert LnurlPayResponse.parse_obj(response.json()) == LnurlPayResponse.parse_obj(
dict(
callback="https://127.0.0.1/lnurlp/satoshi/callback",
minSendable=1000,
maxSendable=500_000,
# NOTE these are millisat values
minSendable=1_000_000,
maxSendable=500_000_000,
metadata=json.dumps(
[
["text/plain", "Zap satoshi some sats"],
Expand Down
40 changes: 38 additions & 2 deletions app/settings.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import hashlib
import json

import lnurl
import qrcode.image.svg
from pydantic import (
BaseSettings,
Field,
HttpUrl,
SecretStr,
)
from qrcode.main import QRCode
from yarl import URL

MAX_CORN = 21_000_000 * 100_000_000
Expand All @@ -25,15 +31,45 @@ class PhoenixdLNURLSettings(BaseSettings):
# Set in test environments. Unsafe on prod.
is_test: bool = False

# TODO use @computed_field when upgrading to Pydantic 2.x
# https://docs.pydantic.dev/2.6/api/fields/#pydantic.fields.computed_field
# So we can also use @functools.cached_property

def base_url(self) -> URL:
# TODO support `http` for `.onion` only (per LNURL spec)
return URL(f"https://{self.lnurl_hostname}")

def is_long_username(self) -> bool:
return len(self.username) > 10

def lnurl_address(self) -> str:
return f"{self.username}@{self.lnurl_hostname}"

def is_long_username(self) -> bool:
return len(self.username) > 10
def lnurl_address_encoded(self) -> lnurl.Lnurl:
return lnurl.encode(str(self.base_url() / "lnurlp" / self.username))

def lnurl_qr(self) -> str:
lnurl_qr = QRCode(
image_factory=qrcode.image.svg.SvgPathFillImage,
box_size=15,
)
lnurl_qr.add_data(self.lnurl_address_encoded())
lnurl_qr.make(fit=True)
return lnurl_qr.make_image().to_string(encoding="unicode")

def metadata_for_payrequest(self) -> str:
return json.dumps(
[
["text/plain", f"Zap {self.username} some sats"],
["text/identifier", self.lnurl_address()],
# ["image/jpeg;base64", "TODO optional"],
]
)

def metadata_hash(self) -> str:
return hashlib.sha256(
self.metadata_for_payrequest().encode("UTF-8")
).hexdigest()

class Config:
env_file = "phoenixd-lnurl.env"
Expand Down
18 changes: 16 additions & 2 deletions app/settings_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ def test_default_settings_load():
assert settings.is_test is True
assert settings.debug is False
assert settings.username == "satoshi"
assert settings.lnurl_hostname == "bitcoincore.org"
assert settings.phoenixd_url == SecretStr("http://satoshi:hunter2@127.0.0.1:9740")
assert settings.lnurl_hostname == "example.com"
assert settings.phoenixd_url == SecretStr("http://_:hunter2@127.0.0.1:9740")
assert settings.user_nostr_address is None
assert settings.user_profile_image_url is None
assert settings.log_level == "INFO"
Expand All @@ -40,6 +40,20 @@ def test_testenv_settings_derived_properties():
settings = PhoenixdLNURLSettings(_env_file="test.env")
assert settings.base_url() == URL("https://127.0.0.1")
assert settings.lnurl_address() == "satoshi@127.0.0.1"
assert (
settings.lnurl_address_encoded()
== "LNURL1DP68GURN8GHJ7VFJXUHRQT3S9CCJ7MRWW4EXCUP0WDSHGMMNDP5S4SDZXR"
)
assert settings.lnurl_qr()[:20] == '<svg width="61.5mm" '
assert (
settings.metadata_for_payrequest()
== '[["text/plain", "Zap satoshi some sats"], ["text/identifier", "satoshi@127.0.0.1"]]'
)
assert (
settings.metadata_hash()
== "297f16bedbf6942cdc656e19feb46a577a43a87e1f858fd8511ac7144b84f0de"
)

assert settings.is_long_username() is False

settings.username = "marttimalmi"
Expand Down
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ select = [
# isort
"I",
]
ignore = ["E501"]
ignore = [
# line-too-long
# We're using `ruff` to format the code, so iff the formatter
# can't get the line length low enough we'll assume it reads better
# as a long line rather than broken up
"E501",
]
fixable = ["ALL"]

[tool.ruff.lint.flake8-quotes]
Expand Down

0 comments on commit 54aeaad

Please sign in to comment.