Merge branch 'FastAPI' of https://github.com/arcbtc/lnbits into FastAPI
This commit is contained in:
commit
6dea1f7271
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
5
lnbits/extensions/satsdice/README.md
Normal file
5
lnbits/extensions/satsdice/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# satsdice
|
||||
|
||||
## Create staic LNURL powered satsdices
|
||||
|
||||
Gambling is dangerous, flip responsibly
|
29
lnbits/extensions/satsdice/__init__.py
Normal file
29
lnbits/extensions/satsdice/__init__.py
Normal file
|
@ -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))
|
6
lnbits/extensions/satsdice/config.json
Normal file
6
lnbits/extensions/satsdice/config.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "Sats Dice",
|
||||
"short_description": "LNURL Satoshi dice",
|
||||
"icon": "casino",
|
||||
"contributors": ["arcbtc"]
|
||||
}
|
298
lnbits/extensions/satsdice/crud.py
Normal file
298
lnbits/extensions/satsdice/crud.py
Normal file
|
@ -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}
|
178
lnbits/extensions/satsdice/lnurl.py
Normal file
178
lnbits/extensions/satsdice/lnurl.py
Normal file
|
@ -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"}
|
73
lnbits/extensions/satsdice/migrations.py
Normal file
73
lnbits/extensions/satsdice/migrations.py
Normal file
|
@ -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
|
||||
);
|
||||
"""
|
||||
)
|
146
lnbits/extensions/satsdice/models.py
Normal file
146
lnbits/extensions/satsdice/models.py
Normal file
|
@ -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)
|
194
lnbits/extensions/satsdice/templates/satsdice/_api_docs.html
Normal file
194
lnbits/extensions/satsdice/templates/satsdice/_api_docs.html
Normal file
|
@ -0,0 +1,194 @@
|
|||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-expansion-item group="api" dense expand-separator label="List satsdices">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-blue">GET</span> /satsdice/api/v1/links</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<satsdice_link_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/links -H "X-Api-Key: {{
|
||||
user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Get a satsdice link"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span>
|
||||
/satsdice/api/v1/links/<satsdice_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code>{"lnurl": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/links/<satsdice_id> -H
|
||||
"X-Api-Key: {{ user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Create a satsdice link"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-green">POST</span> /satsdice/api/v1/links</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code
|
||||
>{"title": <string>, "min_satsdiceable": <integer>,
|
||||
"max_satsdiceable": <integer>, "uses": <integer>,
|
||||
"wait_time": <integer>, "is_unique": <boolean>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code>{"lnurl": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>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 }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Update a satsdice link"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-green">PUT</span>
|
||||
/satsdice/api/v1/links/<satsdice_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code
|
||||
>{"title": <string>, "min_satsdiceable": <integer>,
|
||||
"max_satsdiceable": <integer>, "uses": <integer>,
|
||||
"wait_time": <integer>, "is_unique": <boolean>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>{"lnurl": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>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 }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Delete a satsdice link"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-pink">DELETE</span>
|
||||
/satsdice/api/v1/links/<satsdice_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
|
||||
<code></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.url_root }}api/v1/links/<satsdice_id>
|
||||
-H "X-Api-Key: {{ user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Get hash check (for captchas to prevent milking)"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span>
|
||||
/satsdice/api/v1/links/<the_hash>/<lnurl_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code>{"status": <bool>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root
|
||||
}}api/v1/links/<the_hash>/<lnurl_id> -H "X-Api-Key: {{
|
||||
user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Get image to embed"
|
||||
class="q-pb-md"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span>
|
||||
/satsdice/img/<lnurl_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}/satsdice/img/<lnurl_id>"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
</q-expansion-item>
|
29
lnbits/extensions/satsdice/templates/satsdice/_lnurl.html
Normal file
29
lnbits/extensions/satsdice/templates/satsdice/_lnurl.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
<q-expansion-item group="extras" icon="info" label="Powered by LNURL">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<p>
|
||||
<b>WARNING: LNURL must be used over https or TOR</b><br />
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<small
|
||||
>Check
|
||||
<a href="https://github.com/fiatjaf/awesome-lnurl" target="_blank"
|
||||
>Awesome LNURL</a
|
||||
>
|
||||
for further information.</small
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
63
lnbits/extensions/satsdice/templates/satsdice/display.html
Normal file
63
lnbits/extensions/satsdice/templates/satsdice/display.html
Normal file
|
@ -0,0 +1,63 @@
|
|||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-sm-6 col-md-5 col-lg-4">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<div class="text-center">
|
||||
<a href="lightning:{{ lnurl }}">
|
||||
<q-responsive :ratio="1" class="q-mx-md">
|
||||
<qrcode
|
||||
:value="'{{ lnurl }}'"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" @click="copyText('{{ lnurl }}')"
|
||||
>Copy Satsdice LNURL</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-mb-sm q-mt-none">
|
||||
Chance of winning: {% raw %}{{ chance }}{% endraw %}, Amount
|
||||
multiplier: {{ multiplier }}
|
||||
</h6>
|
||||
<p class="q-my-none">
|
||||
Use a LNURL compatible bitcoin wallet to play the satsdice.
|
||||
</p>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list> {% include "satsdice/_lnurl.html" %} </q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %}
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
here: location.protocol + '//' + location.host,
|
||||
chance: parseFloat('{{chance}}') + '%'
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
percent(val) {
|
||||
return (chance / 100) * 100 + '%'
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,56 @@
|
|||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-sm-6 col-md-5 col-lg-4">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<div class="text-center">
|
||||
<a href="lightning:{{ lnurl }}">
|
||||
<q-responsive :ratio="1" class="q-mx-md">
|
||||
<qrcode
|
||||
:value="'{{ lnurl }}'"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn outline color="grey" @click="copyText('{{ lnurl }}')"
|
||||
>Copy winnings LNURL</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-mb-sm q-mt-none">
|
||||
Congrats! You have won {{ value }}sats (you must claim the sats now)
|
||||
</h6>
|
||||
<p class="q-my-none">
|
||||
Use a LNURL compatible bitcoin wallet to play the satsdice.
|
||||
</p>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list> {% include "satsdice/_lnurl.html" %} </q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %}
|
||||
<script>
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
here: location.protocol + '//' + location.host
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
48
lnbits/extensions/satsdice/templates/satsdice/error.html
Normal file
48
lnbits/extensions/satsdice/templates/satsdice/error.html
Normal file
|
@ -0,0 +1,48 @@
|
|||
{% extends "public.html" %} {% from "macros.jinja" import window_vars with
|
||||
context %}{% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-md-7 col-lg-6 q-gutter-y-md">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<center>
|
||||
{% if lost %}
|
||||
<h5 class="q-my-none">
|
||||
You lost. <a href="/satsdice/{{ link }}">Play again?</a>
|
||||
</h5>
|
||||
{% endif %} {% if paid %}
|
||||
<h5 class="q-my-none">
|
||||
Winnings spent. <a href="/satsdice/{{ link }}">Play again?</a>
|
||||
</h5>
|
||||
{% endif %}
|
||||
<br />
|
||||
<q-icon
|
||||
name="sentiment_dissatisfied"
|
||||
class="text-grey"
|
||||
style="font-size: 20rem"
|
||||
></q-icon>
|
||||
<br />
|
||||
</center>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %}{{ window_vars(user) }}
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
satsdice_lost: '{{lost}}',
|
||||
satsdice_paid: '{{paid}}'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getUrl() {}
|
||||
},
|
||||
created() {
|
||||
console.log('play_location')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
527
lnbits/extensions/satsdice/templates/satsdice/index.html
Normal file
527
lnbits/extensions/satsdice/templates/satsdice/index.html
Normal file
|
@ -0,0 +1,527 @@
|
|||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-btn unelevated color="primary" @click="formDialog.show = true"
|
||||
>New satsdice</q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">satsdices</h5>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="payLinks"
|
||||
row-key="id"
|
||||
:pagination.sync="payLinksTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th style="width: 10%"></q-th>
|
||||
<q-th auto-width style="text-align: left">Title</q-th>
|
||||
<q-th auto-width style="text-align: left">Min bet</q-th>
|
||||
<q-th auto-width style="text-align: left">Max bet</q-th>
|
||||
<q-th auto-width style="text-align: left">Multiplier</q-th>
|
||||
<q-th auto-width style="text-align: left">Haircut</q-th>
|
||||
<q-th auto-width style="text-align: left">Chance</q-th>
|
||||
<q-th auto-width style="text-align: left"></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td style="width: 10%">
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="visibility"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="openQrCodeDialog(props.row.id)"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
|
||||
<q-td auto-width>{{ props.row.title }}</q-td>
|
||||
<q-td auto-width>{{ props.row.min_bet }}</q-td>
|
||||
<q-td auto-width>{{ props.row.max_bet }}</q-td>
|
||||
<q-td auto-width>*{{ props.row.multiplier }}</q-td>
|
||||
<q-td auto-width>{{ props.row.haircut }}</q-td>
|
||||
<q-td auto-width>{{ props.row.chance }}%</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="openUpdateDialog(props.row.id)"
|
||||
icon="edit"
|
||||
color="light-blue"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deletePayLink(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">
|
||||
{{SITE_TITLE}} Sats Dice extension
|
||||
</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "satsdice/_api_docs.html" %}
|
||||
<q-separator></q-separator>
|
||||
{% include "satsdice/_lnurl.html" %}
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendFormData" class="q-gutter-md">
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
{% raw %}
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.title"
|
||||
type="text"
|
||||
label="Title *"
|
||||
></q-input>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-input
|
||||
class="q-pr-xs"
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.min_bet"
|
||||
type="number"
|
||||
label="Min bet size (sats)"
|
||||
></q-input>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-input
|
||||
class="q-pl-xs"
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.max_bet"
|
||||
type="number"
|
||||
label="Max bet size (sats)"
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.haircut"
|
||||
type="number"
|
||||
label="Haircut (chance of winning % to remove)"
|
||||
></q-input>
|
||||
<center>
|
||||
<q-badge color="secondary" class="q-mb-lg">
|
||||
Multipler: x{{ multiValue }}, Chance of winning: {{ chanceValueCalc
|
||||
| percent }}
|
||||
</q-badge>
|
||||
|
||||
<q-slider
|
||||
style="width: 95%"
|
||||
class="q-pt-lg"
|
||||
v-model="multiValue"
|
||||
:min="1.5"
|
||||
:max="20"
|
||||
:step="2"
|
||||
label
|
||||
label-always
|
||||
color="primary"
|
||||
markers
|
||||
snap
|
||||
></q-slider>
|
||||
</center>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="formDialog.data.id"
|
||||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
>Update flip link</q-btn
|
||||
>
|
||||
<q-btn
|
||||
v-else
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="
|
||||
formDialog.data.wallet == null ||
|
||||
formDialog.data.title == null || formDialog.data.min < 1 || formDialog.data.max < formDialog.data.min"
|
||||
type="submit"
|
||||
>Create satsdice</q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="qrCodeDialog.show" position="top">
|
||||
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<qrcode
|
||||
:value="qrCodeDialog.data.lnurl"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></qrcode>
|
||||
</q-responsive>
|
||||
<p style="word-break: break-all">
|
||||
<strong>ID:</strong> {{ qrCodeDialog.data.id }}<br />
|
||||
<strong>Amount:</strong> {{ qrCodeDialog.data.amount }}<br />
|
||||
<span v-if="qrCodeDialog.data.currency"
|
||||
><strong>{{ qrCodeDialog.data.currency }} price:</strong> {{
|
||||
fiatRates[qrCodeDialog.data.currency] ?
|
||||
fiatRates[qrCodeDialog.data.currency] + ' sat' : 'Loading...' }}<br
|
||||
/></span>
|
||||
<strong>Accepts comments:</strong> {{ qrCodeDialog.data.comments }}<br />
|
||||
<strong>Dispatches webhook to:</strong> {{ qrCodeDialog.data.webhook
|
||||
}}<br />
|
||||
<strong>On success:</strong> {{ qrCodeDialog.data.success }}<br />
|
||||
</p>
|
||||
{% endraw %}
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
@click="copyText(qrCodeDialog.data.lnurl, 'Satsdice copied to clipboard!')"
|
||||
class="q-ml-sm"
|
||||
>Copy Satsdice LNURL</q-btn
|
||||
>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
icon="share"
|
||||
@click="copyText(qrCodeDialog.data.pay_url, 'Link copied to clipboard!')"
|
||||
><q-tooltip>Copy shareable link</q-tooltip></q-btn
|
||||
>
|
||||
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
icon="launch"
|
||||
type="a"
|
||||
:href="qrCodeDialog.data.pay_url"
|
||||
target="_blank"
|
||||
><q-tooltip>Launch shareable link</q-tooltip></q-btn
|
||||
>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
icon="print"
|
||||
type="a"
|
||||
:href="qrCodeDialog.data.print_url"
|
||||
target="_blank"
|
||||
><q-tooltip>Print Satsdice</q-tooltip></q-btn
|
||||
>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
|
||||
<script>
|
||||
/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
var locationPath = [
|
||||
window.location.protocol,
|
||||
'//',
|
||||
window.location.host,
|
||||
window.location.pathname
|
||||
].join('')
|
||||
|
||||
var mapPayLink = obj => {
|
||||
obj._data = _.clone(obj)
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
obj.amount = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||
obj.print_url = [locationPath, 'print/', obj.id].join('')
|
||||
obj.pay_url = [locationPath, obj.id].join('')
|
||||
console.log(obj)
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data() {
|
||||
return {
|
||||
chanceValue: 0,
|
||||
multiValue: 1.5,
|
||||
currencies: [],
|
||||
fiatRates: {},
|
||||
checker: null,
|
||||
payLinks: [],
|
||||
payLinksTable: {
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
formDialog: {
|
||||
show: false,
|
||||
fixedAmount: true,
|
||||
data: {
|
||||
haircut: 0,
|
||||
min_bet: 1,
|
||||
max_bet: 1000,
|
||||
currency: 'satoshis',
|
||||
comment_chars: 0
|
||||
}
|
||||
},
|
||||
qrCodeDialog: {
|
||||
show: false,
|
||||
data: null
|
||||
}
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
percent(val) {
|
||||
return val + '%'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
chanceValueCalc() {
|
||||
this.chanceValue = (
|
||||
(1 / this.multiValue) * 100 -
|
||||
this.formDialog.data.haircut
|
||||
).toFixed(2)
|
||||
return this.chanceValue
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
chanceValueTableCalc(multiplier, haircut) {
|
||||
return ((1 / multiplier) * 100 - haircut).toFixed(2)
|
||||
},
|
||||
getPayLinks() {
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/satsdice/api/v1/links?all_wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(response => {
|
||||
console.log(response.data)
|
||||
this.payLinks = response.data.map(mapPayLink)
|
||||
})
|
||||
.catch(err => {
|
||||
clearInterval(this.checker)
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
closeFormDialog() {
|
||||
this.resetFormData()
|
||||
},
|
||||
openQrCodeDialog(linkId) {
|
||||
var link = _.findWhere(this.payLinks, {id: linkId})
|
||||
if (link.currency) this.updateFiatRate(link.currency)
|
||||
|
||||
this.qrCodeDialog.data = {
|
||||
id: link.id,
|
||||
amount:
|
||||
(link.min === link.max ? link.min : `${link.min} - ${link.max}`) +
|
||||
' ' +
|
||||
(link.currency || 'sat'),
|
||||
currency: link.currency,
|
||||
comments: link.comment_chars
|
||||
? `${link.comment_chars} characters`
|
||||
: 'no',
|
||||
webhook: link.webhook_url || 'nowhere',
|
||||
success:
|
||||
link.success_text || link.success_url
|
||||
? 'Display message "' +
|
||||
link.success_text +
|
||||
'"' +
|
||||
(link.success_url ? ' and URL "' + link.success_url + '"' : '')
|
||||
: 'do nothing',
|
||||
lnurl: link.lnurl,
|
||||
pay_url: link.pay_url,
|
||||
print_url: link.print_url
|
||||
}
|
||||
this.qrCodeDialog.show = true
|
||||
},
|
||||
openUpdateDialog(linkId) {
|
||||
const link = _.findWhere(this.payLinks, {id: linkId})
|
||||
if (link.currency) this.updateFiatRate(link.currency)
|
||||
|
||||
this.formDialog.data = _.clone(link._data)
|
||||
this.formDialog.show = true
|
||||
this.formDialog.fixedAmount =
|
||||
this.formDialog.data.min === this.formDialog.data.max
|
||||
},
|
||||
sendFormData() {
|
||||
const wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: this.formDialog.data.wallet
|
||||
})
|
||||
var data = _.omit(this.formDialog.data, 'wallet')
|
||||
data.min_bet = parseInt(data.min_bet)
|
||||
data.max_bet = parseInt(data.max_bet)
|
||||
data.multiplier = parseFloat(this.multiValue)
|
||||
data.haircut = parseFloat(data.haircut)
|
||||
data.chance = parseFloat(this.chanceValue)
|
||||
data.base_url = window.location.origin
|
||||
|
||||
if (data.currency === 'satoshis') data.currency = null
|
||||
if (isNaN(parseInt(data.comment_chars))) data.comment_chars = 0
|
||||
|
||||
if (data.id) {
|
||||
this.updatePayLink(wallet, data)
|
||||
} else {
|
||||
this.createPayLink(wallet, data)
|
||||
}
|
||||
},
|
||||
resetFormData() {
|
||||
this.formDialog = {
|
||||
show: false,
|
||||
fixedAmount: true,
|
||||
data: {}
|
||||
}
|
||||
},
|
||||
updatePayLink(wallet, data) {
|
||||
let values = _.omit(
|
||||
_.pick(
|
||||
data,
|
||||
'chance',
|
||||
'base_url',
|
||||
'multiplier',
|
||||
'haircut',
|
||||
'title',
|
||||
'min_bet',
|
||||
'max_bet',
|
||||
'webhook_url',
|
||||
'success_text',
|
||||
'success_url',
|
||||
'comment_chars',
|
||||
'currency'
|
||||
),
|
||||
(value, key) =>
|
||||
(key === 'webhook_url' ||
|
||||
key === 'success_text' ||
|
||||
key === 'success_url') &&
|
||||
(value === null || value === '')
|
||||
)
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/satsdice/api/v1/links/' + data.id,
|
||||
wallet.adminkey,
|
||||
values
|
||||
)
|
||||
.then(response => {
|
||||
this.payLinks = _.reject(this.payLinks, obj => obj.id === data.id)
|
||||
this.payLinks.push(mapPayLink(response.data))
|
||||
this.formDialog.show = false
|
||||
this.resetFormData()
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
createPayLink(wallet, data) {
|
||||
LNbits.api
|
||||
.request('POST', '/satsdice/api/v1/links', wallet.adminkey, data)
|
||||
.then(response => {
|
||||
this.payLinks.push(mapPayLink(response.data))
|
||||
this.formDialog.show = false
|
||||
this.resetFormData()
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
deletePayLink(linkId) {
|
||||
var link = _.findWhere(this.payLinks, {id: linkId})
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this pay link?')
|
||||
.onOk(() => {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/satsdice/api/v1/links/' + linkId,
|
||||
_.findWhere(this.g.user.wallets, {id: link.wallet}).adminkey
|
||||
)
|
||||
.then(response => {
|
||||
this.payLinks = _.reject(
|
||||
this.payLinks,
|
||||
obj => obj.id === linkId
|
||||
)
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
updateFiatRate(currency) {
|
||||
LNbits.api
|
||||
.request('GET', '/satsdice/api/v1/rate/' + currency, null)
|
||||
.then(response => {
|
||||
let rates = _.clone(this.fiatRates)
|
||||
rates[currency] = response.data.rate
|
||||
this.fiatRates = rates
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
console.log('this.multiValue')
|
||||
console.log(this.g.user)
|
||||
if (this.g.user.wallets.length) {
|
||||
var getPayLinks = this.getPayLinks
|
||||
getPayLinks()
|
||||
this.checker = setInterval(() => {
|
||||
getPayLinks()
|
||||
}, 20000)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
140
lnbits/extensions/satsdice/views.py
Normal file
140
lnbits/extensions/satsdice/views.py
Normal file
|
@ -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",
|
||||
},
|
||||
)
|
270
lnbits/extensions/satsdice/views_api.py
Normal file
270
lnbits/extensions/satsdice/views_api.py
Normal file
|
@ -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
|
Loading…
Reference in New Issue
Block a user