diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py index 233873ef..789eacb0 100644 --- a/lnbits/extensions/jukebox/views_api.py +++ b/lnbits/extensions/jukebox/views_api.py @@ -1,4 +1,5 @@ from fastapi import Request + from http import HTTPStatus from starlette.exceptions import HTTPException from starlette.responses import HTMLResponse, JSONResponse # type: ignore @@ -38,8 +39,8 @@ async def api_get_jukeboxs( ): wallet_user = wallet.wallet.user - jukeboxs = [jukebox.dict() for jukebox in await get_jukeboxs(wallet_user)] try: + jukeboxs = [jukebox.dict() for jukebox in await get_jukeboxs(wallet_user)] return jukeboxs except: diff --git a/lnbits/extensions/lnurlp/views_api.py b/lnbits/extensions/lnurlp/views_api.py index f895f05b..7558006f 100644 --- a/lnbits/extensions/lnurlp/views_api.py +++ b/lnbits/extensions/lnurlp/views_api.py @@ -23,6 +23,7 @@ from .crud import ( delete_pay_link, ) + @lnurlp_ext.get("/api/v1/currencies") async def api_list_currencies_available(): return list(currencies.keys()) @@ -30,14 +31,21 @@ async def api_list_currencies_available(): @lnurlp_ext.get("/api/v1/links", status_code=HTTPStatus.OK) # @api_check_wallet_key("invoice") -async def api_links(req: Request, wallet: WalletTypeInfo = Depends(get_key_type), all_wallets: bool = Query(False)): +async def api_links( + req: Request, + wallet: WalletTypeInfo = Depends(get_key_type), + all_wallets: bool = Query(False), +): wallet_ids = [wallet.wallet.id] if all_wallets: wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids try: - return [{**link.dict(), "lnurl": link.lnurl(req)} for link in await get_pay_links(wallet_ids)] + return [ + {**link.dict(), "lnurl": link.lnurl(req)} + for link in await get_pay_links(wallet_ids) + ] # return [ # {**link.dict(), "lnurl": link.lnurl} # for link in await get_pay_links(wallet_ids) @@ -58,20 +66,20 @@ async def api_links(req: Request, wallet: WalletTypeInfo = Depends(get_key_type) @lnurlp_ext.get("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) # @api_check_wallet_key("invoice") -async def api_link_retrieve(r: Request, link_id, wallet: WalletTypeInfo = Depends(get_key_type)): +async def api_link_retrieve( + r: Request, link_id, wallet: WalletTypeInfo = Depends(get_key_type) +): link = await get_pay_link(link_id) if not link: raise HTTPException( - detail="Pay link does not exist.", - status_code=HTTPStatus.NOT_FOUND + detail="Pay link does not exist.", status_code=HTTPStatus.NOT_FOUND ) # return {"message": "Pay link does not exist."}, HTTPStatus.NOT_FOUND if link.wallet != wallet.wallet.id: raise HTTPException( - detail="Not your pay link.", - status_code=HTTPStatus.FORBIDDEN + detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN ) # return {"message": "Not your pay link."}, HTTPStatus.FORBIDDEN @@ -81,11 +89,14 @@ async def api_link_retrieve(r: Request, link_id, wallet: WalletTypeInfo = Depend @lnurlp_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED) @lnurlp_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) # @api_check_wallet_key("invoice") -async def api_link_create_or_update(data: CreatePayLinkData, link_id=None, wallet: WalletTypeInfo = Depends(get_key_type)): +async def api_link_create_or_update( + data: CreatePayLinkData, + link_id=None, + wallet: WalletTypeInfo = Depends(get_key_type), +): if data.min > data.max: raise HTTPException( - detail="Min is greater than max.", - status_code=HTTPStatus.BAD_REQUEST + detail="Min is greater than max.", status_code=HTTPStatus.BAD_REQUEST ) # return {"message": "Min is greater than max."}, HTTPStatus.BAD_REQUEST @@ -93,15 +104,14 @@ async def api_link_create_or_update(data: CreatePayLinkData, link_id=None, walle round(data.min) != data.min or round(data.max) != data.max ): raise HTTPException( - detail="Must use full satoshis.", - status_code=HTTPStatus.BAD_REQUEST + detail="Must use full satoshis.", status_code=HTTPStatus.BAD_REQUEST ) # return {"message": "Must use full satoshis."}, HTTPStatus.BAD_REQUEST if "success_url" in data and data.success_url[:8] != "https://": raise HTTPException( detail="Success URL must be secure https://...", - status_code=HTTPStatus.BAD_REQUEST + status_code=HTTPStatus.BAD_REQUEST, ) # return ( # {"message": "Success URL must be secure https://..."}, @@ -113,8 +123,7 @@ async def api_link_create_or_update(data: CreatePayLinkData, link_id=None, walle if not link: raise HTTPException( - detail="Pay link does not exist.", - status_code=HTTPStatus.NOT_FOUND + detail="Pay link does not exist.", status_code=HTTPStatus.NOT_FOUND ) # return ( # {"message": "Pay link does not exist."}, @@ -123,12 +132,11 @@ async def api_link_create_or_update(data: CreatePayLinkData, link_id=None, walle if link.wallet != wallet.wallet.id: raise HTTPException( - detail="Not your pay link.", - status_code=HTTPStatus.FORBIDDEN + detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN ) # return {"message": "Not your pay link."}, HTTPStatus.FORBIDDEN - link = await update_pay_link(link_id, data) + link = await update_pay_link(data, link_id=link_id) else: link = await create_pay_link(data, wallet_id=wallet.wallet.id) print("LINK", link) @@ -142,15 +150,13 @@ async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(get_key_type if not link: raise HTTPException( - detail="Pay link does not exist.", - status_code=HTTPStatus.NOT_FOUND + detail="Pay link does not exist.", status_code=HTTPStatus.NOT_FOUND ) # return {"message": "Pay link does not exist."}, HTTPStatus.NOT_FOUND if link.wallet != wallet.wallet.id: raise HTTPException( - detail="Not your pay link.", - status_code=HTTPStatus.FORBIDDEN + detail="Not your pay link.", status_code=HTTPStatus.FORBIDDEN ) # return {"message": "Not your pay link."}, HTTPStatus.FORBIDDEN diff --git a/lnbits/extensions/satsdice/README.md b/lnbits/extensions/satsdice/README.md new file mode 100644 index 00000000..c2419930 --- /dev/null +++ b/lnbits/extensions/satsdice/README.md @@ -0,0 +1,5 @@ +# satsdice + +## Create staic LNURL powered satsdices + +Gambling is dangerous, flip responsibly diff --git a/lnbits/extensions/satsdice/__init__.py b/lnbits/extensions/satsdice/__init__.py new file mode 100644 index 00000000..2149760f --- /dev/null +++ b/lnbits/extensions/satsdice/__init__.py @@ -0,0 +1,29 @@ +import asyncio +from fastapi import APIRouter, FastAPI +from fastapi.staticfiles import StaticFiles +from starlette.routing import Mount +from lnbits.db import Database +from lnbits.helpers import template_renderer +from lnbits.tasks import catch_everything_and_restart + +db = Database("ext_satsdice") + +satsdice_ext: APIRouter = APIRouter(prefix="/satsdice", tags=["satsdice"]) + + +def satsdice_renderer(): + return template_renderer( + [ + "lnbits/extensions/satsdice/templates", + ] + ) + + +from .views_api import * # noqa +from .views import * # noqa +from .lnurl import * # noqa + + +# def satsdice_start(): +# loop = asyncio.get_event_loop() +# loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/satsdice/config.json b/lnbits/extensions/satsdice/config.json new file mode 100644 index 00000000..e4c2eddb --- /dev/null +++ b/lnbits/extensions/satsdice/config.json @@ -0,0 +1,6 @@ +{ + "name": "Sats Dice", + "short_description": "LNURL Satoshi dice", + "icon": "casino", + "contributors": ["arcbtc"] +} diff --git a/lnbits/extensions/satsdice/crud.py b/lnbits/extensions/satsdice/crud.py new file mode 100644 index 00000000..ddf54eb5 --- /dev/null +++ b/lnbits/extensions/satsdice/crud.py @@ -0,0 +1,298 @@ +from datetime import datetime +from typing import List, Optional, Union +from lnbits.helpers import urlsafe_short_hash +from typing import List, Optional +from . import db +from .models import ( + satsdiceWithdraw, + HashCheck, + satsdiceLink, + satsdicePayment, + CreateSatsDiceLink, + CreateSatsDicePayment, + CreateSatsDiceWithdraw, +) +from lnbits.helpers import urlsafe_short_hash + + +async def create_satsdice_pay( + data: CreateSatsDiceLink, +) -> satsdiceLink: + satsdice_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO satsdice.satsdice_pay ( + id, + wallet, + title, + base_url, + min_bet, + max_bet, + amount, + served_meta, + served_pr, + multiplier, + chance, + haircut, + open_time + ) + VALUES (?, ?, ?, ?, ?, ?, 0, 0, 0, ?, ?, ?, ?) + """, + ( + satsdice_id, + data.wallet_id, + data.title, + data.base_url, + data.min_bet, + data.max_bet, + data.multiplier, + data.chance, + data.haircut, + int(datetime.now().timestamp()), + ), + ) + link = await get_satsdice_pay(satsdice_id) + assert link, "Newly created link couldn't be retrieved" + return link + + +async def get_satsdice_pay(link_id: str) -> Optional[satsdiceLink]: + row = await db.fetchone( + "SELECT * FROM satsdice.satsdice_pay WHERE id = ?", (link_id,) + ) + return satsdiceLink.from_row(row) if row else None + + +async def get_satsdice_pays(wallet_ids: Union[str, List[str]]) -> List[satsdiceLink]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + print("wallet_ids") + print(wallet_ids) + print("wallet_ids") + + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall( + f""" + SELECT * FROM satsdice.satsdice_pay WHERE wallet IN ({q}) + ORDER BY id + """, + (*wallet_ids,), + ) + return [satsdiceLink(**row) for row in rows] + + +async def update_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLink]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE satsdice.satsdice_pay SET {q} WHERE id = ?", + (*kwargs.values(), link_id), + ) + row = await db.fetchone( + "SELECT * FROM satsdice.satsdice_pay WHERE id = ?", (link_id,) + ) + return satsdiceLink(**row) if row else None + + +async def increment_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLink]: + q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE satsdice.satsdice_pay SET {q} WHERE id = ?", + (*kwargs.values(), link_id), + ) + row = await db.fetchone( + "SELECT * FROM satsdice.satsdice_pay WHERE id = ?", (link_id,) + ) + return satsdiceLink.from_row(row) if row else None + + +async def delete_satsdice_pay(link_id: int) -> None: + await db.execute("DELETE FROM satsdice.satsdice_pay WHERE id = ?", (link_id,)) + + +##################SATSDICE PAYMENT LINKS + + +async def create_satsdice_payment(data: CreateSatsDicePayment) -> satsdicePayment: + await db.execute( + """ + INSERT INTO satsdice.satsdice_payment ( + payment_hash, + satsdice_pay, + value, + paid, + lost + ) + VALUES (?, ?, ?, ?, ?) + """, + ( + data.payment_hash, + data.satsdice_pay, + data.value, + False, + False, + ), + ) + payment = await get_satsdice_payment(payment_hash) + assert payment, "Newly created withdraw couldn't be retrieved" + return payment + + +async def get_satsdice_payment(payment_hash: str) -> Optional[satsdicePayment]: + row = await db.fetchone( + "SELECT * FROM satsdice.satsdice_payment WHERE payment_hash = ?", + (payment_hash,), + ) + return satsdicePayment.from_row(row) if row else None + + +async def update_satsdice_payment( + payment_hash: int, **kwargs +) -> Optional[satsdicePayment]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + + await db.execute( + f"UPDATE satsdice.satsdice_payment SET {q} WHERE payment_hash = ?", + (bool(*kwargs.values()), payment_hash), + ) + row = await db.fetchone( + "SELECT * FROM satsdice.satsdice_payment WHERE payment_hash = ?", + (payment_hash,), + ) + return satsdicePayment.from_row(row) if row else None + + +##################SATSDICE WITHDRAW LINKS + + +async def create_satsdice_withdraw(data: CreateSatsDiceWithdraw) -> satsdiceWithdraw: + await db.execute( + """ + INSERT INTO satsdice.satsdice_withdraw ( + id, + satsdice_pay, + value, + unique_hash, + k1, + open_time, + used + ) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + ( + data.payment_hash, + data.satsdice_pay, + data.value, + urlsafe_short_hash(), + urlsafe_short_hash(), + int(datetime.now().timestamp()), + data.used, + ), + ) + withdraw = await get_satsdice_withdraw(payment_hash, 0) + assert withdraw, "Newly created withdraw couldn't be retrieved" + return withdraw + + +async def get_satsdice_withdraw(withdraw_id: str, num=0) -> Optional[satsdiceWithdraw]: + row = await db.fetchone( + "SELECT * FROM satsdice.satsdice_withdraw WHERE id = ?", (withdraw_id,) + ) + if not row: + return None + + withdraw = [] + for item in row: + withdraw.append(item) + withdraw.append(num) + return satsdiceWithdraw.from_row(row) + + +async def get_satsdice_withdraw_by_hash( + unique_hash: str, num=0 +) -> Optional[satsdiceWithdraw]: + row = await db.fetchone( + "SELECT * FROM satsdice.satsdice_withdraw WHERE unique_hash = ?", + (unique_hash,), + ) + if not row: + return None + + withdraw = [] + for item in row: + withdraw.append(item) + withdraw.append(num) + return satsdiceWithdraw.from_row(row) + + +async def get_satsdice_withdraws( + wallet_ids: Union[str, List[str]] +) -> List[satsdiceWithdraw]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall( + f"SELECT * FROM satsdice.satsdice_withdraw WHERE wallet IN ({q})", + (*wallet_ids,), + ) + + return [satsdiceWithdraw.from_row(row) for row in rows] + + +async def update_satsdice_withdraw( + withdraw_id: str, **kwargs +) -> Optional[satsdiceWithdraw]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE satsdice.satsdice_withdraw SET {q} WHERE id = ?", + (*kwargs.values(), withdraw_id), + ) + row = await db.fetchone( + "SELECT * FROM satsdice.satsdice_withdraw WHERE id = ?", (withdraw_id,) + ) + return satsdiceWithdraw.from_row(row) if row else None + + +async def delete_satsdice_withdraw(withdraw_id: str) -> None: + await db.execute( + "DELETE FROM satsdice.satsdice_withdraw WHERE id = ?", (withdraw_id,) + ) + + +async def create_withdraw_hash_check( + the_hash: str, + lnurl_id: str, +) -> HashCheck: + await db.execute( + """ + INSERT INTO satsdice.hash_checkw ( + id, + lnurl_id + ) + VALUES (?, ?) + """, + ( + the_hash, + lnurl_id, + ), + ) + hashCheck = await get_withdraw_hash_checkw(the_hash, lnurl_id) + return hashCheck + + +async def get_withdraw_hash_checkw(the_hash: str, lnurl_id: str) -> Optional[HashCheck]: + rowid = await db.fetchone( + "SELECT * FROM satsdice.hash_checkw WHERE id = ?", (the_hash,) + ) + rowlnurl = await db.fetchone( + "SELECT * FROM satsdice.hash_checkw WHERE lnurl_id = ?", (lnurl_id,) + ) + if not rowlnurl: + await create_withdraw_hash_check(the_hash, lnurl_id) + return {"lnurl": True, "hash": False} + else: + if not rowid: + await create_withdraw_hash_check(the_hash, lnurl_id) + return {"lnurl": True, "hash": False} + else: + return {"lnurl": True, "hash": True} diff --git a/lnbits/extensions/satsdice/lnurl.py b/lnbits/extensions/satsdice/lnurl.py new file mode 100644 index 00000000..705eb700 --- /dev/null +++ b/lnbits/extensions/satsdice/lnurl.py @@ -0,0 +1,178 @@ +import shortuuid # type: ignore +import hashlib +import math +from http import HTTPStatus +from datetime import datetime +from lnbits.core.services import pay_invoice, create_invoice +from http import HTTPStatus +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse, JSONResponse # type: ignore +from lnbits.utils.exchange_rates import get_fiat_rate_satoshis +from fastapi import FastAPI, Request +from fastapi.params import Depends +from typing import Optional +from fastapi.param_functions import Query +from . import satsdice_ext +from .crud import ( + get_satsdice_withdraw_by_hash, + update_satsdice_withdraw, + get_satsdice_pay, + create_satsdice_payment, +) +from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore + + +##############LNURLP STUFF + + +@satsdice_ext.get("/api/v1/lnurlp/{link_id}", name="satsdice.lnurlp_response") +async def api_lnurlp_response(req: Request, link_id: str = Query(None)): + link = await get_satsdice_pay(link_id) + if not link: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="LNURL-pay not found.", + ) + resp = LnurlPayResponse( + callback=req.url_for( + "satsdice.api_lnurlp_callback", link_id=link.id, _external=True + ), + min_sendable=math.ceil(link.min_bet * 1) * 1000, + max_sendable=round(link.max_bet * 1) * 1000, + metadata=link.lnurlpay_metadata, + ) + params = resp.dict() + + return params + + +@satsdice_ext.get("/api/v1/lnurlp/cb/{link_id}") +async def api_lnurlp_callback(link_id: str = Query(None), amount: str = Query(None)): + link = await get_satsdice_pay(link_id) + if not link: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="LNURL-pay not found.", + ) + + min, max = link.min_bet, link.max_bet + min = link.min_bet * 1000 + max = link.max_bet * 1000 + + amount_received = int(amount or 0) + if amount_received < min: + return LnurlErrorResponse( + reason=f"Amount {amount_received} is smaller than minimum {min}." + ).dict() + elif amount_received > max: + return LnurlErrorResponse( + reason=f"Amount {amount_received} is greater than maximum {max}." + ).dict() + + payment_hash, payment_request = await create_invoice( + wallet_id=link.wallet, + amount=int(amount_received / 1000), + memo="Satsdice bet", + description_hash=hashlib.sha256( + link.lnurlpay_metadata.encode("utf-8") + ).digest(), + extra={"tag": "satsdice", "link": link.id, "comment": "comment"}, + ) + + success_action = link.success_action(payment_hash) + data = [] + data.satsdice_payy = link.id + data.value = amount_received / 1000 + data.payment_hash = payment_hash + link = await create_satsdice_payment(data) + if success_action: + resp = LnurlPayActionResponse( + pr=payment_request, + success_action=success_action, + routes=[], + ) + else: + resp = LnurlPayActionResponse( + pr=payment_request, + routes=[], + ) + + return resp.dict() + + +##############LNURLW STUFF + + +@satsdice_ext.get("/api/v1/lnurlw/{unique_hash}", name="satsdice.lnurlw_response") +async def api_lnurlw_response(unique_hash: str = Query(None)): + link = await get_satsdice_withdraw_by_hash(unique_hash) + + if not link: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="LNURL-satsdice not found.", + ) + + if link.used: + raise HTTPException( + status_code=HTTPStatus.OK, + detail="satsdice is spent.", + ) + + return link.lnurl_response.dict() + + +# CALLBACK + + +@satsdice_ext.get("/api/v1/lnurlw/cb/{unique_hash}") +async def api_lnurlw_callback( + unique_hash: str = Query(None), k1: str = Query(None), pr: str = Query(None) +): + link = await get_satsdice_withdraw_by_hash(unique_hash) + paylink = await get_satsdice_pay(link.satsdice_pay) + payment_request = pr + now = int(datetime.now().timestamp()) + + if not link: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="LNURL-satsdice not found.", + ) + + if link.used: + raise HTTPException( + status_code=HTTPStatus.OK, + detail="satsdice is spent.", + ) + + if link.k1 != k1: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail="Bad request..", + ) + + if now < link.open_time: + return {"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."} + + try: + await update_satsdice_withdraw(link.id, used=1) + + await pay_invoice( + wallet_id=paylink.wallet, + payment_request=payment_request, + max_sat=link.value, + extra={"tag": "withdraw"}, + ) + + except ValueError as e: + await update_satsdice_withdraw(link.id, used=1) + return {"status": "ERROR", "reason": str(e)} + except PermissionError: + await update_satsdice_withdraw(link.id, used=1) + return {"status": "ERROR", "reason": "satsdice link is empty."} + except Exception as e: + await update_satsdice_withdraw(link.id, used=1) + return {"status": "ERROR", "reason": str(e)} + + return {"status": "OK"} diff --git a/lnbits/extensions/satsdice/migrations.py b/lnbits/extensions/satsdice/migrations.py new file mode 100644 index 00000000..61298241 --- /dev/null +++ b/lnbits/extensions/satsdice/migrations.py @@ -0,0 +1,73 @@ +async def m001_initial(db): + """ + Creates an improved satsdice table and migrates the existing data. + """ + await db.execute( + """ + CREATE TABLE satsdice.satsdice_pay ( + id TEXT PRIMARY KEY, + wallet TEXT, + title TEXT, + min_bet INTEGER, + max_bet INTEGER, + amount INTEGER DEFAULT 0, + served_meta INTEGER NOT NULL, + served_pr INTEGER NOT NULL, + multiplier FLOAT, + haircut FLOAT, + chance FLOAT, + base_url TEXT, + open_time INTEGER + ); + """ + ) + + +async def m002_initial(db): + """ + Creates an improved satsdice table and migrates the existing data. + """ + await db.execute( + """ + CREATE TABLE satsdice.satsdice_withdraw ( + id TEXT PRIMARY KEY, + satsdice_pay TEXT, + value INTEGER DEFAULT 1, + unique_hash TEXT UNIQUE, + k1 TEXT, + open_time INTEGER, + used INTEGER DEFAULT 0 + ); + """ + ) + + +async def m003_initial(db): + """ + Creates an improved satsdice table and migrates the existing data. + """ + await db.execute( + """ + CREATE TABLE satsdice.satsdice_payment ( + payment_hash TEXT PRIMARY KEY, + satsdice_pay TEXT, + value INTEGER, + paid BOOL DEFAULT FALSE, + lost BOOL DEFAULT FALSE + ); + """ + ) + + +async def m004_make_hash_check(db): + """ + Creates a hash check table. + """ + await db.execute( + """ + CREATE TABLE satsdice.hash_checkw ( + id TEXT PRIMARY KEY, + lnurl_id TEXT + ); + """ + ) diff --git a/lnbits/extensions/satsdice/models.py b/lnbits/extensions/satsdice/models.py new file mode 100644 index 00000000..820a6ed1 --- /dev/null +++ b/lnbits/extensions/satsdice/models.py @@ -0,0 +1,146 @@ +import json +from lnurl import Lnurl, LnurlWithdrawResponse, encode as lnurl_encode # type: ignore +from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult +from lnurl.types import LnurlPayMetadata # type: ignore +from sqlite3 import Row +from typing import NamedTuple, Optional, Dict +import shortuuid # type: ignore +from fastapi.param_functions import Query +from pydantic.main import BaseModel +from pydantic import BaseModel +from typing import Optional +from fastapi import FastAPI, Request + + +class satsdiceLink(BaseModel): + id: str + wallet: str + title: str + min_bet: int + max_bet: int + amount: int + served_meta: int + served_pr: int + multiplier: float + haircut: float + chance: float + base_url: str + open_time: int + + def lnurl(self, req: Request) -> Lnurl: + return lnurl_encode(req.url_for("satsdice.lnurlp_response", item_id=self.id)) + + @classmethod + def from_row(cls, row: Row) -> "satsdiceLink": + data = dict(row) + return cls(**data) + + @property + def lnurlpay_metadata(self) -> LnurlPayMetadata: + return LnurlPayMetadata(json.dumps([["text/plain", self.title]])) + + def success_action(self, payment_hash: str, req: Request) -> Optional[Dict]: + url = req.url_for( + "satsdice.displaywin", + link_id=self.id, + payment_hash=payment_hash, + _external=True, + ) + # url: ParseResult = urlparse(url) + print(url) + # qs: Dict = parse_qs(url.query) + # qs["payment_hash"] = payment_hash + # url = url._replace(query=urlencode(qs, doseq=True)) + return { + "tag": "url", + "description": "Check the attached link", + "url": url, + } + + +class satsdicePayment(BaseModel): + payment_hash: str + satsdice_pay: str + value: int + paid: bool + lost: bool + + +class satsdiceWithdraw(BaseModel): + id: str + satsdice_pay: str + value: int + unique_hash: str + k1: str + open_time: int + used: int + + def lnurl(self, req: Request) -> Lnurl: + return lnurl_encode( + req.url_for( + "satsdice.lnurlw_response", + unique_hash=self.unique_hash, + _external=True, + ) + ) + + @property + def is_spent(self) -> bool: + return self.used >= 1 + + @property + def lnurl_response(self, req: Request) -> LnurlWithdrawResponse: + url = req.url_for( + "satsdice.api_lnurlw_callback", + unique_hash=self.unique_hash, + _external=True, + ) + return LnurlWithdrawResponse( + callback=url, + k1=self.k1, + minWithdrawable=self.value * 1000, + maxWithdrawable=self.value * 1000, + default_description="Satsdice winnings!", + ) + + +class HashCheck(BaseModel): + id: str + lnurl_id: str + + @classmethod + def from_row(cls, row: Row) -> "Hash": + return cls(**dict(row)) + + +class CreateSatsDiceLink(BaseModel): + wallet_id: str = Query(None) + title: str = Query(None) + base_url: str = Query(None) + min_bet: str = Query(None) + max_bet: str = Query(None) + multiplier: int = Query(0) + chance: float = Query(0) + haircut: int = Query(0) + + +class CreateSatsDicePayment(BaseModel): + satsdice_pay: str = Query(None) + value: int = Query(0) + payment_hash: str = Query(None) + + +class CreateSatsDiceWithdraw(BaseModel): + payment_hash: str = Query(None) + satsdice_pay: str = Query(None) + value: int = Query(0) + used: int = Query(0) + + +class CreateSatsDiceWithdraws(BaseModel): + title: str = Query(None) + min_satsdiceable: int = Query(0) + max_satsdiceable: int = Query(0) + uses: int = Query(0) + wait_time: str = Query(None) + is_unique: bool = Query(False) diff --git a/lnbits/extensions/satsdice/templates/satsdice/_api_docs.html b/lnbits/extensions/satsdice/templates/satsdice/_api_docs.html new file mode 100644 index 00000000..7d73ae7e --- /dev/null +++ b/lnbits/extensions/satsdice/templates/satsdice/_api_docs.html @@ -0,0 +1,194 @@ + + + + + GET /satsdice/api/v1/links +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+
+ Returns 200 OK (application/json) +
+ [<satsdice_link_object>, ...] +
Curl example
+ curl -X GET {{ request.url_root }}api/v1/links -H "X-Api-Key: {{ + user.wallets[0].inkey }}" + +
+
+
+ + + + GET + /satsdice/api/v1/links/<satsdice_id> +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ {"lnurl": <string>} +
Curl example
+ curl -X GET {{ request.url_root }}api/v1/links/<satsdice_id> -H + "X-Api-Key: {{ user.wallets[0].inkey }}" + +
+
+
+ + + + POST /satsdice/api/v1/links +
Headers
+ {"X-Api-Key": <admin_key>}
+
Body (application/json)
+ {"title": <string>, "min_satsdiceable": <integer>, + "max_satsdiceable": <integer>, "uses": <integer>, + "wait_time": <integer>, "is_unique": <boolean>} +
+ Returns 201 CREATED (application/json) +
+ {"lnurl": <string>} +
Curl example
+ curl -X POST {{ request.url_root }}api/v1/links -d '{"title": + <string>, "min_satsdiceable": <integer>, + "max_satsdiceable": <integer>, "uses": <integer>, + "wait_time": <integer>, "is_unique": <boolean>}' -H + "Content-type: application/json" -H "X-Api-Key: {{ + user.wallets[0].adminkey }}" + +
+
+
+ + + + PUT + /satsdice/api/v1/links/<satsdice_id> +
Headers
+ {"X-Api-Key": <admin_key>}
+
Body (application/json)
+ {"title": <string>, "min_satsdiceable": <integer>, + "max_satsdiceable": <integer>, "uses": <integer>, + "wait_time": <integer>, "is_unique": <boolean>} +
+ Returns 200 OK (application/json) +
+ {"lnurl": <string>} +
Curl example
+ curl -X PUT {{ request.url_root }}api/v1/links/<satsdice_id> -d + '{"title": <string>, "min_satsdiceable": <integer>, + "max_satsdiceable": <integer>, "uses": <integer>, + "wait_time": <integer>, "is_unique": <boolean>}' -H + "Content-type: application/json" -H "X-Api-Key: {{ + user.wallets[0].adminkey }}" + +
+
+
+ + + + DELETE + /satsdice/api/v1/links/<satsdice_id> +
Headers
+ {"X-Api-Key": <admin_key>}
+
Returns 204 NO CONTENT
+ +
Curl example
+ curl -X DELETE {{ request.url_root }}api/v1/links/<satsdice_id> + -H "X-Api-Key: {{ user.wallets[0].adminkey }}" + +
+
+
+ + + + GET + /satsdice/api/v1/links/<the_hash>/<lnurl_id> +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ {"status": <bool>} +
Curl example
+ curl -X GET {{ request.url_root + }}api/v1/links/<the_hash>/<lnurl_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" + +
+
+
+ + + + GET + /satsdice/img/<lnurl_id> +
Curl example
+ curl -X GET {{ request.url_root }}/satsdice/img/<lnurl_id>" + +
+
+
+
diff --git a/lnbits/extensions/satsdice/templates/satsdice/_lnurl.html b/lnbits/extensions/satsdice/templates/satsdice/_lnurl.html new file mode 100644 index 00000000..20b67cab --- /dev/null +++ b/lnbits/extensions/satsdice/templates/satsdice/_lnurl.html @@ -0,0 +1,29 @@ + + + +

+ WARNING: LNURL must be used over https or TOR
+ LNURL is a range of lightning-network standards that allow us to use + lightning-network differently. An LNURL satsdice is the permission for + someone to pull a certain amount of funds from a lightning wallet. In + this extension time is also added - an amount can be satsdice over a + period of time. A typical use case for an LNURL satsdice is a faucet, + although it is a very powerful technology, with much further reaching + implications. For example, an LNURL satsdice could be minted to pay for + a subscription service. +

+

+ Exploring LNURL and finding use cases, is really helping inform + lightning protocol development, rather than the protocol dictating how + lightning-network should be engaged with. +

+ Check + Awesome LNURL + for further information. +
+
+
diff --git a/lnbits/extensions/satsdice/templates/satsdice/display.html b/lnbits/extensions/satsdice/templates/satsdice/display.html new file mode 100644 index 00000000..d4238e30 --- /dev/null +++ b/lnbits/extensions/satsdice/templates/satsdice/display.html @@ -0,0 +1,63 @@ +{% extends "public.html" %} {% block page %} +
+
+ + + +
+ Copy Satsdice LNURL +
+
+
+
+
+ + +
+ Chance of winning: {% raw %}{{ chance }}{% endraw %}, Amount + multiplier: {{ multiplier }} +
+

+ Use a LNURL compatible bitcoin wallet to play the satsdice. +

+
+ + + {% include "satsdice/_lnurl.html" %} + +
+
+
+{% endblock %} {% block scripts %} + +{% endblock %} diff --git a/lnbits/extensions/satsdice/templates/satsdice/displaywin.html b/lnbits/extensions/satsdice/templates/satsdice/displaywin.html new file mode 100644 index 00000000..aa4f1375 --- /dev/null +++ b/lnbits/extensions/satsdice/templates/satsdice/displaywin.html @@ -0,0 +1,56 @@ +{% extends "public.html" %} {% block page %} +
+
+ + + +
+ Copy winnings LNURL +
+
+
+
+
+ + +
+ Congrats! You have won {{ value }}sats (you must claim the sats now) +
+

+ Use a LNURL compatible bitcoin wallet to play the satsdice. +

+
+ + + {% include "satsdice/_lnurl.html" %} + +
+
+
+{% endblock %} {% block scripts %} + +{% endblock %} diff --git a/lnbits/extensions/satsdice/templates/satsdice/error.html b/lnbits/extensions/satsdice/templates/satsdice/error.html new file mode 100644 index 00000000..1c8fc618 --- /dev/null +++ b/lnbits/extensions/satsdice/templates/satsdice/error.html @@ -0,0 +1,48 @@ +{% extends "public.html" %} {% from "macros.jinja" import window_vars with +context %}{% block page %} +
+
+ + +
+ {% if lost %} +
+ You lost. Play again? +
+ {% endif %} {% if paid %} +
+ Winnings spent. Play again? +
+ {% endif %} +
+ +
+
+
+
+
+
+{% endblock %} {% block scripts %}{{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/extensions/satsdice/templates/satsdice/index.html b/lnbits/extensions/satsdice/templates/satsdice/index.html new file mode 100644 index 00000000..b9c1fae9 --- /dev/null +++ b/lnbits/extensions/satsdice/templates/satsdice/index.html @@ -0,0 +1,527 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + New satsdice + + + + + +
+
+
satsdices
+
+
+ + {% raw %} + + + {% endraw %} + +
+
+
+ +
+ + +
+ {{SITE_TITLE}} Sats Dice extension +
+
+ + + + {% include "satsdice/_api_docs.html" %} + + {% include "satsdice/_lnurl.html" %} + + +
+
+ + + + + + + {% raw %} + + +
+
+ +
+
+ +
+
+ + +
+ + Multipler: x{{ multiValue }}, Chance of winning: {{ chanceValueCalc + | percent }} + + + +
+ +
+ Update flip link + Create satsdice + Cancel +
+
+
+
+ + + + + + +

+ ID: {{ qrCodeDialog.data.id }}
+ Amount: {{ qrCodeDialog.data.amount }}
+ {{ qrCodeDialog.data.currency }} price: {{ + fiatRates[qrCodeDialog.data.currency] ? + fiatRates[qrCodeDialog.data.currency] + ' sat' : 'Loading...' }}
+ Accepts comments: {{ qrCodeDialog.data.comments }}
+ Dispatches webhook to: {{ qrCodeDialog.data.webhook + }}
+ On success: {{ qrCodeDialog.data.success }}
+

+ {% endraw %} +
+ Copy Satsdice LNURL + Copy shareable link + + Launch shareable link + Print Satsdice + Close +
+
+
+
+ +{% endblock %} {% block scripts %} {{ window_vars(user) }} + + +{% endblock %} diff --git a/lnbits/extensions/satsdice/views.py b/lnbits/extensions/satsdice/views.py new file mode 100644 index 00000000..ceebf822 --- /dev/null +++ b/lnbits/extensions/satsdice/views.py @@ -0,0 +1,140 @@ +from datetime import datetime +from http import HTTPStatus +from lnbits.decorators import check_user_exists, WalletTypeInfo, get_key_type +from . import satsdice_ext, satsdice_renderer +from .crud import ( + get_satsdice_pay, + update_satsdice_payment, + get_satsdice_payment, + create_satsdice_withdraw, + get_satsdice_withdraw, +) +from lnbits.core.crud import ( + get_payments, + get_standalone_payment, + delete_expired_invoices, + get_balance_checks, +) +from lnbits.core.services import ( + check_invoice_status, +) +from fastapi import FastAPI, Request +from fastapi.params import Depends +from fastapi.templating import Jinja2Templates +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse +from lnbits.core.models import User, Payment + +from fastapi.params import Depends +from fastapi.param_functions import Query + +templates = Jinja2Templates(directory="templates") + + +@satsdice_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return satsdice_renderer().TemplateResponse( + "satsdice/index.html", {"request": request, "user": user.dict()} + ) + + +@satsdice_ext.get("/{link_id}") +async def display(link_id): + link = await get_satsdice_pay(link_id) or abort( + HTTPStatus.NOT_FOUND, "satsdice link does not exist." + ) + return satsdice_renderer().TemplateResponse( + "satsdice/display.html", + chance=link.chance, + multiplier=link.multiplier, + lnurl=link.lnurl, + unique=True, + ) + + +@satsdice_ext.get("/win/{link_id}/{payment_hash}") +async def displaywin(link_id: str = Query(None), payment_hash: str = Query(None)): + satsdicelink = await get_satsdice_pay(link_id) or abort( + HTTPStatus.NOT_FOUND, "satsdice link does not exist." + ) + withdrawLink = await get_satsdice_withdraw(payment_hash) + + if withdrawLink: + return satsdice_renderer().TemplateResponse( + "satsdice/displaywin.html", + value=withdrawLink.value, + chance=satsdicelink.chance, + multiplier=satsdicelink.multiplier, + lnurl=withdrawLink.lnurl, + paid=False, + lost=False, + ) + + payment = await get_standalone_payment(payment_hash) or abort( + HTTPStatus.NOT_FOUND, "satsdice link does not exist." + ) + + if payment.pending == 1: + await check_invoice_status(payment.wallet_id, payment_hash) + payment = await get_standalone_payment(payment_hash) or abort( + HTTPStatus.NOT_FOUND, "satsdice link does not exist." + ) + if payment.pending == 1: + print("pending") + return satsdice_renderer().TemplateResponse( + "satsdice/error.html", link=satsdicelink.id, paid=False, lost=False + ) + + await update_satsdice_payment(payment_hash, paid=1) + + paylink = await get_satsdice_payment(payment_hash) or abort( + HTTPStatus.NOT_FOUND, "satsdice link does not exist." + ) + + if paylink.lost == 1: + print("lost") + return satsdice_renderer().TemplateResponse( + "satsdice/error.html", link=satsdicelink.id, paid=False, lost=True + ) + rand = random.randint(0, 100) + chance = satsdicelink.chance + if rand > chance: + await update_satsdice_payment(payment_hash, lost=1) + return satsdice_renderer().TemplateResponse( + "satsdice/error.html", link=satsdicelink.id, paid=False, lost=True + ) + data = [] + data.payment_hash = payment_hash + data.satsdice_pay = (satsdicelink.id,) + data.value = (paylink.value * satsdicelink.multiplier,) + data.used = 0 + withdrawLink = await create_satsdice_withdraw(data) + return satsdice_renderer().TemplateResponse( + "satsdice/displaywin.html", + value=withdrawLink.value, + chance=satsdicelink.chance, + multiplier=satsdicelink.multiplier, + lnurl=withdrawLink.lnurl, + paid=False, + lost=False, + ) + + +@satsdice_ext.get("/img/{link_id}") +async def img(link_id): + link = await get_satsdice_pay(link_id) or abort( + HTTPStatus.NOT_FOUND, "satsdice link does not exist." + ) + qr = pyqrcode.create(link.lnurl) + stream = BytesIO() + qr.svg(stream, scale=3) + return ( + stream.getvalue(), + 200, + { + "Content-Type": "image/svg+xml", + "Cache-Control": "no-cache, no-store, must-revalidate", + "Pragma": "no-cache", + "Expires": "0", + }, + ) diff --git a/lnbits/extensions/satsdice/views_api.py b/lnbits/extensions/satsdice/views_api.py new file mode 100644 index 00000000..466f6483 --- /dev/null +++ b/lnbits/extensions/satsdice/views_api.py @@ -0,0 +1,270 @@ +from http import HTTPStatus +from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore +from http import HTTPStatus +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse, JSONResponse # type: ignore +from lnbits.core.crud import get_user +from lnbits.decorators import api_validate_post_request +from .models import CreateSatsDiceLink, CreateSatsDiceWithdraws, CreateSatsDicePayment +from . import satsdice_ext +from fastapi import FastAPI, Request +from fastapi.params import Depends +from typing import Optional +from fastapi.param_functions import Query +from .crud import ( + create_satsdice_pay, + get_satsdice_pay, + get_satsdice_pays, + update_satsdice_pay, + delete_satsdice_pay, + create_satsdice_withdraw, + get_satsdice_withdraw, + get_satsdice_withdraws, + update_satsdice_withdraw, + delete_satsdice_withdraw, + create_withdraw_hash_check, +) +from lnbits.decorators import ( + check_user_exists, + WalletTypeInfo, + get_key_type, + api_validate_post_request, +) + +################LNURL pay + + +@satsdice_ext.get("/api/v1/links") +async def api_links( + request: Request, + wallet: WalletTypeInfo = Depends(get_key_type), + all_wallets: str = Query(None), +): + wallet_ids = [wallet.wallet.id] + + if all_wallets: + wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids + + try: + links = await get_satsdice_pays(wallet_ids) + print(links[0]) + + return [{link.dict(), {"lnurl": link.lnurl(request)}} for link in links] + except LnurlInvalidUrl: + raise HTTPException( + status_code=HTTPStatus.UPGRADE_REQUIRED, + detail="LNURLs need to be delivered over a publically accessible `https` domain or Tor.", + ) + + +@satsdice_ext.get("/api/v1/links/{link_id}") +async def api_link_retrieve( + data: CreateSatsDiceLink, + link_id: str = Query(None), + wallet: WalletTypeInfo = Depends(get_key_type), +): + link = await get_satsdice_pay(link_id) + + if not link: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Pay link does not exist.", + ) + + if link.wallet != wallet.wallet.id: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Not your pay link.", + ) + + return {**link._asdict(), **{"lnurl": link.lnurl}} + + +@satsdice_ext.post("/api/v1/links", status_code=HTTPStatus.CREATED) +@satsdice_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) +async def api_link_create_or_update( + data: CreateSatsDiceLink, + wallet: WalletTypeInfo = Depends(get_key_type), + link_id: str = Query(None), +): + if data.min_bet > data.max_bet: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail="Bad request", + ) + if link_id: + link = await get_satsdice_pay(link_id) + + if not link: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Satsdice does not exist", + ) + + if link.wallet != wallet.wallet.id: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Come on, seriously, this isn't your satsdice!", + ) + data.link_id = link_id + link = await update_satsdice_pay(data) + else: + data.wallet_id = wallet.wallet.id + link = await create_satsdice_pay(data) + + return {link.dict(), {"lnurl": link.lnurl}} + + +@satsdice_ext.delete("/api/v1/links/{link_id}") +async def api_link_delete( + wallet: WalletTypeInfo = Depends(get_key_type), link_id: str = Query(None) +): + link = await get_satsdice_pay(link_id) + + if not link: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Pay link does not exist.", + ) + + if link.wallet != g.wallet.id: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Not your pay link.", + ) + + await delete_satsdice_pay(link_id) + + return "", HTTPStatus.NO_CONTENT + + +##########LNURL withdraw + + +@satsdice_ext.get("/api/v1/withdraws") +async def api_withdraws( + wallet: WalletTypeInfo = Depends(get_key_type), all_wallets: str = Query(None) +): + wallet_ids = [wallet.wallet.id] + + if all_wallets: + wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids + try: + return ( + jsonify( + [ + { + **withdraw._asdict(), + **{"lnurl": withdraw.lnurl}, + } + for withdraw in await get_satsdice_withdraws(wallet_ids) + ] + ), + HTTPStatus.OK, + ) + except LnurlInvalidUrl: + raise HTTPException( + status_code=HTTPStatus.UPGRADE_REQUIRED, + detail="LNURLs need to be delivered over a publically accessible `https` domain or Tor.", + ) + + +@satsdice_ext.get("/api/v1/withdraws/{withdraw_id}") +async def api_withdraw_retrieve( + wallet: WalletTypeInfo = Depends(get_key_type), withdraw_id: str = Query(None) +): + withdraw = await get_satsdice_withdraw(withdraw_id, 0) + + if not withdraw: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="satsdice withdraw does not exist.", + ) + + if withdraw.wallet != wallet.wallet.id: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Not your satsdice withdraw.", + ) + + return {**withdraw._asdict(), **{"lnurl": withdraw.lnurl}}, HTTPStatus.OK + + +@satsdice_ext.post("/api/v1/withdraws", status_code=HTTPStatus.CREATED) +@satsdice_ext.put("/api/v1/withdraws/{withdraw_id}", status_code=HTTPStatus.OK) +async def api_withdraw_create_or_update( + data: CreateSatsDiceWithdraws, + wallet: WalletTypeInfo = Depends(get_key_type), + withdraw_id: str = Query(None), +): + if data.max_satsdiceable < data.min_satsdiceable: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail="`max_satsdiceable` needs to be at least `min_satsdiceable`.", + ) + + usescsv = "" + for i in range(data.uses): + if data.is_unique: + usescsv += "," + str(i + 1) + else: + usescsv += "," + str(1) + usescsv = usescsv[1:] + + if withdraw_id: + withdraw = await get_satsdice_withdraw(withdraw_id, 0) + if not withdraw: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="satsdice withdraw does not exist.", + ) + if withdraw.wallet != wallet.wallet.id: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Not your satsdice withdraw.", + ) + + withdraw = await update_satsdice_withdraw( + withdraw_id, **data, usescsv=usescsv, used=0 + ) + else: + withdraw = await create_satsdice_withdraw( + wallet_id=wallet.wallet.id, **data, usescsv=usescsv + ) + + return {**withdraw._asdict(), **{"lnurl": withdraw.lnurl}} + + +@satsdice_ext.delete("/api/v1/withdraws/{withdraw_id}") +async def api_withdraw_delete( + data: CreateSatsDiceWithdraws, + wallet: WalletTypeInfo = Depends(get_key_type), + withdraw_id: str = Query(None), +): + withdraw = await get_satsdice_withdraw(withdraw_id) + + if not withdraw: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="satsdice withdraw does not exist.", + ) + + if withdraw.wallet != wallet.wallet.id: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="Not your satsdice withdraw.", + ) + + await delete_satsdice_withdraw(withdraw_id) + + return "", HTTPStatus.NO_CONTENT + + +@satsdice_ext.get("/api/v1/withdraws/{the_hash}/{lnurl_id}") +async def api_withdraw_hash_retrieve( + wallet: WalletTypeInfo = Depends(get_key_type), + lnurl_id: str = Query(None), + the_hash: str = Query(None), +): + hashCheck = await get_withdraw_hash_check(the_hash, lnurl_id) + return hashCheck