Merge branch 'FastAPI' of https://github.com/arcbtc/lnbits into FastAPI

This commit is contained in:
Tiago vasconcelos 2021-10-15 17:06:04 +01:00
commit 6dea1f7271
17 changed files with 2092 additions and 23 deletions

View File

@ -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:

View File

@ -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

View File

@ -0,0 +1,5 @@
# satsdice
## Create staic LNURL powered satsdices
Gambling is dangerous, flip responsibly

View 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))

View File

@ -0,0 +1,6 @@
{
"name": "Sats Dice",
"short_description": "LNURL Satoshi dice",
"icon": "casino",
"contributors": ["arcbtc"]
}

View 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}

View 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"}

View 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
);
"""
)

View 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)

View 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": &lt;invoice_key&gt;}</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>[&lt;satsdice_link_object&gt;, ...]</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/&lt;satsdice_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</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": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}api/v1/links/&lt;satsdice_id&gt; -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": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"title": &lt;string&gt;, "min_satsdiceable": &lt;integer&gt;,
"max_satsdiceable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>{"lnurl": &lt;string&gt;}</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":
&lt;string&gt;, "min_satsdiceable": &lt;integer&gt;,
"max_satsdiceable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -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/&lt;satsdice_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"title": &lt;string&gt;, "min_satsdiceable": &lt;integer&gt;,
"max_satsdiceable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X PUT {{ request.url_root }}api/v1/links/&lt;satsdice_id&gt; -d
'{"title": &lt;string&gt;, "min_satsdiceable": &lt;integer&gt;,
"max_satsdiceable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -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/&lt;satsdice_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</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/&lt;satsdice_id&gt;
-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/&lt;the_hash&gt;/&lt;lnurl_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</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": &lt;bool&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root
}}api/v1/links/&lt;the_hash&gt;/&lt;lnurl_id&gt; -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/&lt;lnurl_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}/satsdice/img/&lt;lnurl_id&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>

View 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>

View 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 %}

View File

@ -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 %}

View 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 %}

View 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 %}

View 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",
},
)

View 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