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 @@
+
+ WARNING: LNURL must be used over https or TOR
+ 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.
+ 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>"
+
+
+ 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.
+
+ Use a LNURL compatible bitcoin wallet to play the satsdice. +
++ Use a LNURL compatible bitcoin wallet to play the satsdice. +
+
+ 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 }}
+