Skip to content

Commit

Permalink
Merge pull request #46 from lnbits/boltzatm
Browse files Browse the repository at this point in the history
Draft: atm receipt/claim page
  • Loading branch information
arcbtc authored Sep 24, 2024
2 parents f543b7f + 78cb5fa commit bcdafe4
Show file tree
Hide file tree
Showing 12 changed files with 1,619 additions and 562 deletions.
135 changes: 82 additions & 53 deletions crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import shortuuid
from fastapi import Request
from lnbits.db import Database
from lnbits.helpers import urlsafe_short_hash
from lnbits.helpers import update_query, urlsafe_short_hash
from lnurl import encode as lnurl_encode

from .models import CreateLnurldevice, Lnurldevice, LnurldevicePayment
Expand All @@ -19,24 +19,24 @@ async def create_lnurldevice(data: CreateLnurldevice, req: Request) -> Lnurldevi
lnurldevice_id = urlsafe_short_hash()
lnurldevice_key = urlsafe_short_hash()

if data.switches:
if isinstance(data.extra, list):
url = req.url_for("lnurldevice.lnurl_v2_params", device_id=lnurldevice_id)
for _switch in data.switches:
_switch.lnurl = lnurl_encode(
for _extra in data.extra:
_extra.lnurl = lnurl_encode(
str(url)
+ f"?pin={_switch.pin}"
+ f"&amount={_switch.amount}"
+ f"&duration={_switch.duration}"
+ f"&variable={_switch.variable}"
+ f"&comment={_switch.comment}"
+ f"?pin={_extra.pin}"
+ f"&amount={_extra.amount}"
+ f"&duration={_extra.duration}"
+ f"&variable={_extra.variable}"
+ f"&comment={_extra.comment}"
+ "&disabletime=0"
)

await db.execute(
"""
INSERT INTO lnurldevice.lnurldevice
(id, key, title, wallet, profit, currency, device, switches)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
(id, key, title, wallet, profit, currency, device, extra) VALUES
(?, ?, ?, ?, ?, ?, ?, ?)
""",
(
lnurldevice_id,
Expand All @@ -46,7 +46,11 @@ async def create_lnurldevice(data: CreateLnurldevice, req: Request) -> Lnurldevi
data.profit,
data.currency,
data.device,
json.dumps(data.switches, default=lambda x: x.dict()),
(
json.dumps(data.extra, default=lambda x: x.dict())
if data.extra != "boltz"
else data.extra
),
),
)

Expand All @@ -58,17 +62,16 @@ async def create_lnurldevice(data: CreateLnurldevice, req: Request) -> Lnurldevi
async def update_lnurldevice(
lnurldevice_id: str, data: CreateLnurldevice, req: Request
) -> Lnurldevice:

if data.switches:
if isinstance(data.extra, list):
url = req.url_for("lnurldevice.lnurl_v2_params", device_id=lnurldevice_id)
for _switch in data.switches:
_switch.lnurl = lnurl_encode(
for _extra in data.extra:
_extra.lnurl = lnurl_encode(
str(url)
+ f"?pin={_switch.pin}"
+ f"&amount={_switch.amount}"
+ f"&duration={_switch.duration}"
+ f"&variable={_switch.variable}"
+ f"&comment={_switch.comment}"
+ f"?pin={_extra.pin}"
+ f"&amount={_extra.amount}"
+ f"&duration={_extra.duration}"
+ f"&variable={_extra.variable}"
+ f"&comment={_extra.comment}"
)

await db.execute(
Expand All @@ -79,7 +82,7 @@ async def update_lnurldevice(
profit = ?,
currency = ?,
device = ?,
switches = ?
extra = ?
WHERE id = ?
""",
(
Expand All @@ -88,7 +91,11 @@ async def update_lnurldevice(
data.profit,
data.currency,
data.device,
json.dumps(data.switches, default=lambda x: x.dict()),
(
json.dumps(data.extra, default=lambda x: x.dict())
if data.extra != "boltz"
else data.extra
),
lnurldevice_id,
),
)
Expand All @@ -106,18 +113,17 @@ async def get_lnurldevice(lnurldevice_id: str, req: Request) -> Optional[Lnurlde

device = Lnurldevice(**row)

# this is needed for backwards compabtibility,
# before the LNURL were cached inside db
if device.switches:
# this is needed for backward compatibility, before the LNURL were cached inside db
if isinstance(device.extra, list):
url = req.url_for("lnurldevice.lnurl_v2_params", device_id=device.id)
for _switch in device.switches:
_switch.lnurl = lnurl_encode(
for _extra in device.extra:
_extra.lnurl = lnurl_encode(
str(url)
+ f"?pin={_switch.pin}"
+ f"&amount={_switch.amount}"
+ f"&duration={_switch.duration}"
+ f"&variable={_switch.variable}"
+ f"&comment={_switch.comment}"
+ f"?pin={_extra.pin}"
+ f"&amount={_extra.amount}"
+ f"&duration={_extra.duration}"
+ f"&variable={_extra.variable}"
+ f"&comment={_extra.comment}"
)

return device
Expand All @@ -139,16 +145,16 @@ async def get_lnurldevices(wallet_ids: List[str], req: Request) -> List[Lnurldev
devices = [Lnurldevice(**row) for row in rows]

for device in devices:
if device.switches:
if isinstance(device.extra, list):
url = req.url_for("lnurldevice.lnurl_v2_params", device_id=device.id)
for _switch in device.switches:
_switch.lnurl = lnurl_encode(
for _extra in device.extra:
_extra.lnurl = lnurl_encode(
str(url)
+ f"?pin={_switch.pin}"
+ f"&amount={_switch.amount}"
+ f"&duration={_switch.duration}"
+ f"&variable={_switch.variable}"
+ f"&comment={_switch.comment}"
+ f"?pin={_extra.pin}"
+ f"&amount={_extra.amount}"
+ f"&duration={_extra.duration}"
+ f"&variable={_extra.variable}"
+ f"&comment={_extra.comment}"
)

return devices
Expand All @@ -168,11 +174,6 @@ async def create_lnurldevicepayment(
sats: Optional[int] = 0,
) -> LnurldevicePayment:

# TODO: ben what is this for?
# if device.device == "atm":
# lnurldevicepayment_id = shortuuid.uuid(name=payload)
# else:

lnurldevicepayment_id = urlsafe_short_hash()
await db.execute(
"""
Expand All @@ -194,16 +195,13 @@ async def create_lnurldevicepayment(


async def update_lnurldevicepayment(
lnurldevicepayment_id: str, **kwargs
lnurldevicepayment: LnurldevicePayment,
) -> LnurldevicePayment:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
await db.execute(
f"UPDATE lnurldevice.lnurldevicepayment SET {q} WHERE id = ?",
(*kwargs.values(), lnurldevicepayment_id),
update_query("lnurldevice.lnurldevicepayment", lnurldevicepayment),
(*lnurldevicepayment.dict().values(), lnurldevicepayment.id),
)
dpayment = await get_lnurldevicepayment(lnurldevicepayment_id)
assert dpayment, "Couldnt retrieve update LnurldevicePayment"
return dpayment
return lnurldevicepayment


async def get_lnurldevicepayment(
Expand All @@ -216,6 +214,20 @@ async def get_lnurldevicepayment(
return LnurldevicePayment(**row) if row else None


async def get_lnurldevicepayments(
lnurldevice_ids: List[str],
) -> List[LnurldevicePayment]:
q = ",".join(["?"] * len(lnurldevice_ids))
rows = await db.fetchall(
f"""
SELECT * FROM lnurldevice.lnurldevicepayment WHERE deviceid IN ({q})
ORDER BY id
""",
(*lnurldevice_ids,),
)
return [LnurldevicePayment(**row) for row in rows]


async def get_lnurldevicepayment_by_p(
p: str,
) -> Optional[LnurldevicePayment]:
Expand All @@ -234,3 +246,20 @@ async def get_lnurlpayload(
(lnurldevicepayment_payload,),
)
return LnurldevicePayment(**row) if row else None


async def get_recent_lnurldevicepayment(payload: str) -> Optional[LnurldevicePayment]:
row = await db.fetchone(
"""
SELECT * FROM lnurldevice.lnurldevicepayment
WHERE payload = ? ORDER BY timestamp DESC LIMIT 1
""",
(payload,),
)
return LnurldevicePayment(**row) if row else None


async def delete_atm_payment_link(atm_id: str) -> None:
await db.execute(
"DELETE FROM lnurldevice.lnurldevicepayment WHERE id = ?", (atm_id,)
)
83 changes: 83 additions & 0 deletions helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import base64
import hmac
from io import BytesIO
from typing import Optional

from embit import compact
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis

from .crud import create_lnurldevicepayment, get_recent_lnurldevicepayment
from .models import Lnurldevice, LnurldevicePayment


async def register_atm_payment(
device: Lnurldevice, payload: str
) -> tuple[Optional[LnurldevicePayment], Optional[int]]:
"""
Register an ATM payment to avoid double pull.
"""
lnurldevicepayment = await get_recent_lnurldevicepayment(payload)
# If the payment is already registered and been paid, return None
if lnurldevicepayment and lnurldevicepayment.payload == lnurldevicepayment.payhash:
return None, None
# If the payment is already registered and not been paid, return lnurlpayment record
elif (
lnurldevicepayment and lnurldevicepayment.payload != lnurldevicepayment.payhash
):
return lnurldevicepayment, None

# else create a new lnurlpayment record
data = base64.urlsafe_b64decode(payload)
decrypted = xor_decrypt(device.key.encode(), data)
price_msat = (
await fiat_amount_as_satoshis(float(decrypted[1]) / 100, device.currency) * 1000
if device.currency != "sat"
else decrypted[1] * 1000
)
price_msat = int(price_msat * ((device.profit / 100) + 1))
lnurldevicepayment = await create_lnurldevicepayment(
deviceid=device.id,
payload=payload,
sats=int(price_msat / 1000),
pin=decrypted[0],
payhash="payment_hash",
)
return lnurldevicepayment, price_msat


def xor_decrypt(key, blob):
s = BytesIO(blob)
variant = s.read(1)[0]
if variant != 1:
raise RuntimeError("Not implemented")
# reading nonce
nonce_len = s.read(1)[0]
nonce = s.read(nonce_len)
if len(nonce) != nonce_len:
raise RuntimeError("Missing nonce bytes")
if nonce_len < 8:
raise RuntimeError("Nonce is too short")

# reading payload
payload_len = s.read(1)[0]
payload = s.read(payload_len)
if len(payload) > 32:
raise RuntimeError("Payload is too long for this encryption method")
if len(payload) != payload_len:
raise RuntimeError("Missing payload bytes")
hmacval = s.read()
expected = hmac.new(
key, b"Data:" + blob[: -len(hmacval)], digestmod="sha256"
).digest()
if len(hmacval) < 8:
raise RuntimeError("HMAC is too short")
if hmacval != expected[: len(hmacval)]:
raise RuntimeError("HMAC is invalid")
secret = hmac.new(key, b"Round secret:" + nonce, digestmod="sha256").digest()
payload = bytearray(payload)
for i in range(len(payload)):
payload[i] = payload[i] ^ secret[i]
s = BytesIO(payload)
pin = compact.read_from(s)
amount_in_cent = compact.read_from(s)
return str(pin), amount_in_cent
7 changes: 7 additions & 0 deletions migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,10 @@ async def m005_redux(db):

# drop old table columns
await db.execute(f"DROP TABLE {old_db}")


async def m006_redux(db):
# Rename switches so we can also use for atm
await db.execute(
"ALTER TABLE lnurldevice.lnurldevice RENAME COLUMN switches TO extra"
)
19 changes: 7 additions & 12 deletions models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import json
from sqlite3 import Row
from typing import List, Optional
from typing import List, Optional, Union

from lnurl.types import LnurlPayMetadata
from pydantic import BaseModel, Json


class LnurldeviceSwitch(BaseModel):
class LnurldeviceExtra(BaseModel):
amount: float = 0.0
duration: int = 0
pin: int = 0
Expand All @@ -21,7 +20,7 @@ class CreateLnurldevice(BaseModel):
currency: str
device: str
profit: float
switches: Optional[List[LnurldeviceSwitch]]
extra: Optional[Union[List[LnurldeviceExtra], str]]


class Lnurldevice(BaseModel):
Expand All @@ -32,13 +31,9 @@ class Lnurldevice(BaseModel):
profit: float
currency: str
device: str
switches: Optional[Json[List[LnurldeviceSwitch]]]
extra: Optional[Union[Json[List[LnurldeviceExtra]], str]]
timestamp: str

@classmethod
def from_row(cls, row: Row) -> "Lnurldevice":
return cls(**dict(row))

@property
def lnurlpay_metadata(self) -> LnurlPayMetadata:
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
Expand All @@ -53,6 +48,6 @@ class LnurldevicePayment(BaseModel):
sats: int
timestamp: str

@classmethod
def from_row(cls, row: Row) -> "LnurldevicePayment":
return cls(**dict(row))

class Lnurlencode(BaseModel):
url: str
Loading

0 comments on commit bcdafe4

Please sign in to comment.