Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 20 additions & 101 deletions crud.py
Original file line number Diff line number Diff line change
@@ -1,67 +1,54 @@
from datetime import datetime

import shortuuid
from lnbits.db import Database
from lnbits.helpers import urlsafe_short_hash

from .models import CreateWithdrawData, HashCheck, PaginatedWithdraws, WithdrawLink
from .models import CreateWithdrawData, PaginatedWithdraws, WithdrawLink, WithdrawSecret

db = Database("ext_withdraw")


async def create_withdraw_link(
data: CreateWithdrawData, wallet_id: str
) -> WithdrawLink:
link_id = urlsafe_short_hash()[:22]
available_links = ",".join([str(i) for i in range(data.uses)])
withdraw_link = WithdrawLink(
id=link_id,
wallet=wallet_id,
unique_hash=urlsafe_short_hash(),
k1=urlsafe_short_hash(),
created_at=datetime.now(),
open_time=int(datetime.now().timestamp()) + data.wait_time,
title=data.title,
wallet=wallet_id,
min_withdrawable=data.min_withdrawable,
max_withdrawable=data.max_withdrawable,
uses=data.uses,
wait_time=data.wait_time,
is_unique=data.is_unique,
usescsv=available_links,
is_static=data.is_static,
is_public=data.is_public,
webhook_url=data.webhook_url,
webhook_headers=data.webhook_headers,
webhook_body=data.webhook_body,
custom_url=data.custom_url,
number=0,
)
secrets = []
for _ in range(data.uses):
secrets.append(
WithdrawSecret(
withdraw_id=withdraw_link.id,
amount=withdraw_link.max_withdrawable,
)
)
withdraw_link.secrets.total = data.uses
withdraw_link.secrets.items = secrets
await db.insert("withdraw.withdraw_link", withdraw_link)
return withdraw_link


async def get_withdraw_link(link_id: str, num=0) -> WithdrawLink | None:
link = await db.fetchone(
async def get_withdraw_link(link_id: str) -> WithdrawLink | None:
return await db.fetchone(
"SELECT * FROM withdraw.withdraw_link WHERE id = :id",
{"id": link_id},
WithdrawLink,
)
if not link:
return None

link.number = num
return link


async def get_withdraw_link_by_hash(unique_hash: str, num=0) -> WithdrawLink | None:
link = await db.fetchone(
"SELECT * FROM withdraw.withdraw_link WHERE unique_hash = :hash",
{"hash": unique_hash},
async def get_withdraw_link_by_k1(k1: str) -> WithdrawLink | None:
return await db.fetchone(
"SELECT * FROM withdraw.withdraw_link WHERE secrets LIKE :k1",
{"k1": f"%{k1}%"},
WithdrawLink,
)
if not link:
return None

link.number = num
return link


async def get_withdraw_links(
Expand Down Expand Up @@ -96,22 +83,6 @@ async def get_withdraw_links(
return PaginatedWithdraws(data=links, total=int(result2.total))


async def remove_unique_withdraw_link(link: WithdrawLink, unique_hash: str) -> None:
unique_links = [
x.strip()
for x in link.usescsv.split(",")
if unique_hash != shortuuid.uuid(name=link.id + link.unique_hash + x.strip())
]
link.usescsv = ",".join(unique_links)
await update_withdraw_link(link)


async def increment_withdraw_link(link: WithdrawLink) -> None:
link.used = link.used + 1
link.open_time = int(datetime.now().timestamp()) + link.wait_time
await update_withdraw_link(link)


async def update_withdraw_link(link: WithdrawLink) -> WithdrawLink:
await db.update("withdraw.withdraw_link", link)
return link
Expand All @@ -121,55 +92,3 @@ async def delete_withdraw_link(link_id: str) -> None:
await db.execute(
"DELETE FROM withdraw.withdraw_link WHERE id = :id", {"id": link_id}
)


def chunks(lst, n):
for i in range(0, len(lst), n):
yield lst[i : i + n]


async def create_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:
await db.execute(
"""
INSERT INTO withdraw.hash_check (id, lnurl_id)
VALUES (:id, :lnurl_id)
""",
{"id": the_hash, "lnurl_id": lnurl_id},
)
hash_check = await get_hash_check(the_hash, lnurl_id)
return hash_check


async def get_hash_check(the_hash: str, lnurl_id: str) -> HashCheck:

hash_check = await db.fetchone(
"""
SELECT id as hash, lnurl_id as lnurl
FROM withdraw.hash_check WHERE id = :id
""",
{"id": the_hash},
HashCheck,
)
hash_check_lnurl = await db.fetchone(
"""
SELECT id as hash, lnurl_id as lnurl
FROM withdraw.hash_check WHERE lnurl_id = :id
""",
{"id": lnurl_id},
HashCheck,
)
if not hash_check_lnurl:
await create_hash_check(the_hash, lnurl_id)
return HashCheck(lnurl=True, hash=False)
else:
if not hash_check:
await create_hash_check(the_hash, lnurl_id)
return HashCheck(lnurl=True, hash=False)
else:
return HashCheck(lnurl=True, hash=True)


async def delete_hash_check(the_hash: str) -> None:
await db.execute(
"DELETE FROM withdraw.hash_check WHERE id = :hash", {"hash": the_hash}
)
28 changes: 0 additions & 28 deletions helpers.py

This file was deleted.

10 changes: 10 additions & 0 deletions migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,13 @@ async def m007_add_created_at_timestamp(db):
"ALTER TABLE withdraw.withdraw_link "
f"ADD COLUMN created_at TIMESTAMP DEFAULT {db.timestamp_column_default}"
)


async def m008_add_secrets_static_public(db):
await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN secrets TEXT;")
await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN is_static BOOLEAN;")
await db.execute("UPDATE withdraw.withdraw_link SET is_static = NOT is_unique;")
await db.execute(
"ALTER TABLE withdraw.withdraw_link ADD COLUMN is_public DEFAULT TRUE;"
)
# await db.execute("DROP TABLE withdraw.hash_check;")
113 changes: 81 additions & 32 deletions models.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,100 @@
from datetime import datetime
import json
from datetime import datetime, timezone

from fastapi import Query
from pydantic import BaseModel, Field
from lnbits.helpers import urlsafe_short_hash
from pydantic import BaseModel, Field, validator


class CreateWithdrawData(BaseModel):
title: str = Query(...)
min_withdrawable: int = Query(..., ge=1)
max_withdrawable: int = Query(..., ge=1)
uses: int = Query(..., ge=1)
uses: int = Query(..., ge=1, le=250)
wait_time: int = Query(..., ge=1)
is_unique: bool
webhook_url: str = Query(None)
webhook_headers: str = Query(None)
webhook_body: str = Query(None)
custom_url: str = Query(None)
is_static: bool = Query(True)
is_public: bool = Query(True)
webhook_url: str | None = Query(None)
webhook_headers: str | None = Query(None)
webhook_body: str | None = Query(None)
custom_url: str | None = Query(None)

@validator("max_withdrawable")
def check_max_withdrawable(cls, v: int, values) -> int:
if "min_withdrawable" in values and v < values["min_withdrawable"]:
raise ValueError("max_withdrawable must be at least min_withdrawable")
return v

class WithdrawLink(BaseModel):
id: str
wallet: str = Query(None)
title: str = Query(None)
min_withdrawable: int = Query(0)
max_withdrawable: int = Query(0)
uses: int = Query(0)
wait_time: int = Query(0)
is_unique: bool = Query(False)
unique_hash: str = Query(0)
k1: str = Query(None)
open_time: int = Query(0)
used: int = Query(0)
usescsv: str = Query(None)
number: int = Field(default=0, no_database=True)
webhook_url: str = Query(None)
webhook_headers: str = Query(None)
webhook_body: str = Query(None)
custom_url: str = Query(None)
created_at: datetime
@validator("webhook_body")
def check_webhook_body(cls, v):
if v:
try:
json.loads(v)
except Exception as exc:
raise ValueError("webhook_body must be valid JSON") from exc
return v

@validator("webhook_headers")
def check_headers_json(cls, v):
if v:
try:
json.loads(v)
except Exception as exc:
raise ValueError("webhook_headers must be valid JSON") from exc
return v


class WithdrawSecret(BaseModel):
k1: str = Field(default_factory=urlsafe_short_hash)
withdraw_id: str
amount: int
used: bool = False
used_at: int | None = None
payment_hash_melt: str | None = None


class WithdrawSecrets(BaseModel):
total: int = 0
used: int = 0
items: list[WithdrawSecret] = []

@property
def is_spent(self) -> bool:
return self.used >= self.uses
return self.used >= self.total

@property
def next_secret(self) -> WithdrawSecret | None:
return next((item for item in self.items if not item.used), None)

def get_secret(self, k1: str) -> WithdrawSecret | None:
return next((item for item in self.items if item.k1 == k1), None)

def use_secret(self, k1: str) -> WithdrawSecret | None:
for item in self.items:
if item.k1 == k1 and not item.used:
item.used = True
item.used_at = int(datetime.now().timestamp())
self.used += 1
return item
return None

class HashCheck(BaseModel):
hash: bool
lnurl: bool

class WithdrawLink(BaseModel):
id: str = Field(default_factory=lambda: urlsafe_short_hash()[:22])
wallet: str
title: str
min_withdrawable: int
max_withdrawable: int
wait_time: int
is_static: bool
is_public: bool
webhook_url: str | None = None
webhook_headers: str | None = None
webhook_body: str | None = None
custom_url: str | None = None
open_time: int = Field(default_factory=lambda: int(datetime.now().timestamp()))
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
secrets: WithdrawSecrets = WithdrawSecrets()


class PaginatedWithdraws(BaseModel):
Expand Down
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,7 @@ line-length = 88
# RUF - ruff
# B - bugbear
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"]
# UP007: pyupgrade: use X | Y instead of Optional. (python3.10)
# C901 `api_link_create_or_update` is too complex (15 > 10)
ignore = ["UP007", "C901"]
ignore = []

# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
Expand All @@ -68,6 +66,7 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.lint.pep8-naming]
classmethod-decorators = [
"root_validator",
"validator",
]

# Ignore unused imports in __init__.py files.
Expand Down
Loading