This commit is contained in:
benarc 2021-10-13 17:08:48 +01:00
parent a914840351
commit 8db9459501
15 changed files with 2013 additions and 0 deletions

View File

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

View File

@ -0,0 +1,14 @@
from quart import Blueprint
from lnbits.db import Database
db = Database("ext_satsdice")
satsdice_ext: Blueprint = Blueprint(
"satsdice", __name__, static_folder="static", template_folder="templates"
)
from .views_api import * # noqa
from .views import * # noqa
from .lnurl import * # noqa

View File

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

View File

@ -0,0 +1,301 @@
from datetime import datetime
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import satsdiceWithdraw, HashCheck, satsdiceLink, satsdicePayment
##################SATSDICE PAY LINKS
async def create_satsdice_pay(
*,
wallet_id: str,
title: str,
base_url: str,
min_bet: str,
max_bet: str,
multiplier: int = 0,
chance: float = 0,
haircut: int = 0,
) -> 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,
wallet_id,
title,
base_url,
min_bet,
max_bet,
multiplier,
chance,
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]
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.from_row(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.from_row(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(
*, satsdice_pay: str, value: int, payment_hash: str
) -> satsdicePayment:
await db.execute(
"""
INSERT INTO satsdice.satsdice_payment (
payment_hash,
satsdice_pay,
value,
paid,
lost
)
VALUES (?, ?, ?, ?, ?)
""",
(
payment_hash,
satsdice_pay,
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(
*, payment_hash: str, satsdice_pay: str, value: int, used: int
) -> satsdiceWithdraw:
await db.execute(
"""
INSERT INTO satsdice.satsdice_withdraw (
id,
satsdice_pay,
value,
unique_hash,
k1,
open_time,
used
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
payment_hash,
satsdice_pay,
value,
urlsafe_short_hash(),
urlsafe_short_hash(),
int(datetime.now().timestamp()),
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,183 @@
import shortuuid # type: ignore
import hashlib
import math
from http import HTTPStatus
from datetime import datetime
from quart import jsonify, url_for, request
from lnbits.core.services import pay_invoice, create_invoice
from lnbits.utils.exchange_rates import get_fiat_rate_satoshis
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.route("/api/v1/lnurlp/<link_id>", methods=["GET"])
async def api_lnurlp_response(link_id):
link = await get_satsdice_pay(link_id)
if not link:
return (
jsonify({"status": "ERROR", "reason": "LNURL-payy not found."}),
HTTPStatus.OK,
)
resp = LnurlPayResponse(
callback=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 jsonify(params), HTTPStatus.OK
@satsdice_ext.route("/api/v1/lnurlp/cb/<link_id>", methods=["GET"])
async def api_lnurlp_callback(link_id):
link = await get_satsdice_pay(link_id)
if not link:
return (
jsonify({"status": "ERROR", "reason": "LNUeL-pay not found."}),
HTTPStatus.OK,
)
min, max = link.min_bet, link.max_bet
min = link.min_bet * 1000
max = link.max_bet * 1000
amount_received = int(request.args.get("amount") or 0)
if amount_received < min:
return (
jsonify(
LnurlErrorResponse(
reason=f"Amount {amount_received} is smaller than minimum {min}."
).dict()
),
HTTPStatus.OK,
)
elif amount_received > max:
return (
jsonify(
LnurlErrorResponse(
reason=f"Amount {amount_received} is greater than maximum {max}."
).dict()
),
HTTPStatus.OK,
)
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)
link = await create_satsdice_payment(
satsdice_pay=link.id, value=amount_received / 1000, payment_hash=payment_hash
)
if success_action:
resp = LnurlPayActionResponse(
pr=payment_request,
success_action=success_action,
routes=[],
)
else:
resp = LnurlPayActionResponse(
pr=payment_request,
routes=[],
)
return jsonify(resp.dict()), HTTPStatus.OK
##############LNURLW STUFF
@satsdice_ext.route("/api/v1/lnurlw/<unique_hash>", methods=["GET"])
async def api_lnurlw_response(unique_hash):
link = await get_satsdice_withdraw_by_hash(unique_hash)
if not link:
return (
jsonify({"status": "ERROR", "reason": "LNURL-satsdice not found."}),
HTTPStatus.OK,
)
if link.used:
return (
jsonify({"status": "ERROR", "reason": "satsdice is spent."}),
HTTPStatus.OK,
)
return jsonify(link.lnurl_response.dict()), HTTPStatus.OK
# CALLBACK
@satsdice_ext.route("/api/v1/lnurlw/cb/<unique_hash>", methods=["GET"])
async def api_lnurlw_callback(unique_hash):
link = await get_satsdice_withdraw_by_hash(unique_hash)
paylink = await get_satsdice_pay(link.satsdice_pay)
k1 = request.args.get("k1", type=str)
payment_request = request.args.get("pr", type=str)
now = int(datetime.now().timestamp())
if not link:
return (
jsonify({"status": "ERROR", "reason": "LNURL-satsdice not found."}),
HTTPStatus.OK,
)
if link.used:
return (
jsonify({"status": "ERROR", "reason": "satsdice is spent."}),
HTTPStatus.OK,
)
if link.k1 != k1:
return jsonify({"status": "ERROR", "reason": "Bad request."}), HTTPStatus.OK
if now < link.open_time:
return (
jsonify(
{"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}
),
HTTPStatus.OK,
)
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 jsonify({"status": "ERROR", "reason": str(e)})
except PermissionError:
await update_satsdice_withdraw(link.id, used=1)
return jsonify({"status": "ERROR", "reason": "satsdice link is empty."})
except Exception as e:
await update_satsdice_withdraw(link.id, used=1)
return jsonify({"status": "ERROR", "reason": str(e)})
return jsonify({"status": "OK"}), HTTPStatus.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,122 @@
import json
from quart import url_for
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
class satsdiceLink(NamedTuple):
id: int
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
@classmethod
def from_row(cls, row: Row) -> "satsdiceLink":
data = dict(row)
return cls(**data)
@property
def lnurl(self) -> Lnurl:
url = url_for("satsdice.api_lnurlp_response", link_id=self.id, _external=True)
return lnurl_encode(url)
@property
def lnurlpay_metadata(self) -> LnurlPayMetadata:
return LnurlPayMetadata(json.dumps([["text/plain", self.title]]))
def success_action(self, payment_hash: str) -> Optional[Dict]:
url = 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(NamedTuple):
payment_hash: str
satsdice_pay: str
value: int
paid: bool
lost: bool
@classmethod
def from_row(cls, row: Row) -> "satsdicePayment":
data = dict(row)
return cls(**data)
class satsdiceWithdraw(NamedTuple):
id: str
satsdice_pay: str
value: int
unique_hash: str
k1: str
open_time: int
used: int
@classmethod
def from_row(cls, row: Row) -> "satsdiceWithdraw":
data = dict(row)
return cls(**data)
@property
def is_spent(self) -> bool:
return self.used >= 1
@property
def lnurl(self) -> Lnurl:
url = url_for(
"satsdice.api_lnurlw_response",
unique_hash=self.unique_hash,
_external=True,
)
return lnurl_encode(url)
@property
def lnurl_response(self) -> LnurlWithdrawResponse:
url = 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(NamedTuple):
id: str
lnurl_id: str
@classmethod
def from_row(cls, row: Row) -> "Hash":
return cls(**dict(row))

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: {{
g.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: {{ g.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: {{
g.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: {{
g.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: {{ g.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: {{
g.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,526 @@
{% 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 "lnurlp/_api_docs.html" %}
<q-separator></q-separator>
{% include "lnurlp/_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 src="{{ url_for('static', filename='vendor/vue-qrcode@1.0.2/vue-qrcode.min.js') }}"></script>
<style></style>
<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 => {
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')
if (this.g.user.wallets.length) {
var getPayLinks = this.getPayLinks
getPayLinks()
this.checker = setInterval(() => {
getPayLinks()
}, 20000)
}
}
})
</script>
{% endblock %}

View File

@ -0,0 +1,128 @@
from quart import g, abort, render_template
from http import HTTPStatus
import pyqrcode
from io import BytesIO
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.core.crud import get_user, get_standalone_payment
from lnbits.core.services import check_invoice_status
import random
from . import satsdice_ext
from .crud import (
get_satsdice_pay,
update_satsdice_payment,
get_satsdice_payment,
create_satsdice_withdraw,
get_satsdice_withdraw,
)
@satsdice_ext.route("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
async def index():
return await render_template("satsdice/index.html", user=g.user)
@satsdice_ext.route("/<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 await render_template(
"satsdice/display.html",
chance=link.chance,
multiplier=link.multiplier,
lnurl=link.lnurl,
unique=True,
)
@satsdice_ext.route("/win/<link_id>/<payment_hash>")
async def displaywin(link_id, payment_hash):
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 await render_template(
"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 await render_template(
"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 await render_template(
"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 await render_template(
"satsdice/error.html", link=satsdicelink.id, paid=False, lost=True
)
withdrawLink = await create_satsdice_withdraw(
payment_hash=payment_hash,
satsdice_pay=satsdicelink.id,
value=paylink.value * satsdicelink.multiplier,
used=0,
)
return await render_template(
"satsdice/displaywin.html",
value=withdrawLink.value,
chance=satsdicelink.chance,
multiplier=satsdicelink.multiplier,
lnurl=withdrawLink.lnurl,
paid=False,
lost=False,
)
@satsdice_ext.route("/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,265 @@
from quart import g, jsonify, request
from http import HTTPStatus
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore
from lnbits.core.crud import get_user
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from . import satsdice_ext
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,
)
################LNURL pay
@satsdice_ext.route("/api/v1/links", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_links():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = (await get_user(g.wallet.user)).wallet_ids
try:
return (
jsonify(
[
{**link._asdict(), **{"lnurl": link.lnurl}}
for link in await get_satsdice_pays(wallet_ids)
]
),
HTTPStatus.OK,
)
except LnurlInvalidUrl:
return (
jsonify(
{
"message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor."
}
),
HTTPStatus.UPGRADE_REQUIRED,
)
@satsdice_ext.route("/api/v1/links/<link_id>", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_link_retrieve(link_id):
link = await get_satsdice_pay(link_id)
if not link:
return jsonify({"message": "Pay link does not exist."}), HTTPStatus.NOT_FOUND
if link.wallet != g.wallet.id:
return jsonify({"message": "Not your pay link."}), HTTPStatus.FORBIDDEN
return jsonify({**link._asdict(), **{"lnurl": link.lnurl}}), HTTPStatus.OK
@satsdice_ext.route("/api/v1/links", methods=["POST"])
@satsdice_ext.route("/api/v1/links/<link_id>", methods=["PUT"])
@api_check_wallet_key("invoice")
@api_validate_post_request(
schema={
"title": {"type": "string", "empty": False, "required": True},
"base_url": {"type": "string", "empty": False, "required": True},
"min_bet": {"type": "number", "required": True},
"max_bet": {"type": "number", "required": True},
"multiplier": {"type": "number", "required": True},
"chance": {"type": "float", "required": True},
"haircut": {"type": "number", "required": True},
}
)
async def api_link_create_or_update(link_id=None):
if g.data["min_bet"] > g.data["max_bet"]:
return jsonify({"message": "Min is greater than max."}), HTTPStatus.BAD_REQUEST
if link_id:
link = await get_satsdice_pay(link_id)
if not link:
return (
jsonify({"message": "Satsdice does not exist."}),
HTTPStatus.NOT_FOUND,
)
if link.wallet != g.wallet.id:
return (
jsonify({"message": "Come on, seriously, this isn't your satsdice!"}),
HTTPStatus.FORBIDDEN,
)
link = await update_satsdice_pay(link_id, **g.data)
else:
link = await create_satsdice_pay(wallet_id=g.wallet.id, **g.data)
return (
jsonify({**link._asdict(), **{"lnurl": link.lnurl}}),
HTTPStatus.OK if link_id else HTTPStatus.CREATED,
)
@satsdice_ext.route("/api/v1/links/<link_id>", methods=["DELETE"])
@api_check_wallet_key("invoice")
async def api_link_delete(link_id):
link = await get_satsdice_pay(link_id)
if not link:
return jsonify({"message": "Pay link does not exist."}), HTTPStatus.NOT_FOUND
if link.wallet != g.wallet.id:
return jsonify({"message": "Not your pay link."}), HTTPStatus.FORBIDDEN
await delete_satsdice_pay(link_id)
return "", HTTPStatus.NO_CONTENT
##########LNURL withdraw
@satsdice_ext.route("/api/v1/withdraws", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_withdraws():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = (await get_user(g.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:
return (
jsonify(
{
"message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor."
}
),
HTTPStatus.UPGRADE_REQUIRED,
)
@satsdice_ext.route("/api/v1/withdraws/<withdraw_id>", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_withdraw_retrieve(withdraw_id):
withdraw = await get_satsdice_withdraw(withdraw_id, 0)
if not withdraw:
return (
jsonify({"message": "satsdice withdraw does not exist."}),
HTTPStatus.NOT_FOUND,
)
if withdraw.wallet != g.wallet.id:
return (
jsonify({"message": "Not your satsdice withdraw."}),
HTTPStatus.FORBIDDEN,
)
return jsonify({**withdraw._asdict(), **{"lnurl": withdraw.lnurl}}), HTTPStatus.OK
@satsdice_ext.route("/api/v1/withdraws", methods=["POST"])
@satsdice_ext.route("/api/v1/withdraws/<withdraw_id>", methods=["PUT"])
@api_check_wallet_key("admin")
@api_validate_post_request(
schema={
"title": {"type": "string", "empty": False, "required": True},
"min_satsdiceable": {"type": "integer", "min": 1, "required": True},
"max_satsdiceable": {"type": "integer", "min": 1, "required": True},
"uses": {"type": "integer", "min": 1, "required": True},
"wait_time": {"type": "integer", "min": 1, "required": True},
"is_unique": {"type": "boolean", "required": True},
}
)
async def api_withdraw_create_or_update(withdraw_id=None):
if g.data["max_satsdiceable"] < g.data["min_satsdiceable"]:
return (
jsonify(
{
"message": "`max_satsdiceable` needs to be at least `min_satsdiceable`."
}
),
HTTPStatus.BAD_REQUEST,
)
usescsv = ""
for i in range(g.data["uses"]):
if g.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:
return (
jsonify({"message": "satsdice withdraw does not exist."}),
HTTPStatus.NOT_FOUND,
)
if withdraw.wallet != g.wallet.id:
return (
jsonify({"message": "Not your satsdice withdraw."}),
HTTPStatus.FORBIDDEN,
)
withdraw = await update_satsdice_withdraw(
withdraw_id, **g.data, usescsv=usescsv, used=0
)
else:
withdraw = await create_satsdice_withdraw(
wallet_id=g.wallet.id, **g.data, usescsv=usescsv
)
return (
jsonify({**withdraw._asdict(), **{"lnurl": withdraw.lnurl}}),
HTTPStatus.OK if withdraw_id else HTTPStatus.CREATED,
)
@satsdice_ext.route("/api/v1/withdraws/<withdraw_id>", methods=["DELETE"])
@api_check_wallet_key("admin")
async def api_withdraw_delete(withdraw_id):
withdraw = await get_satsdice_withdraw(withdraw_id)
if not withdraw:
return (
jsonify({"message": "satsdice withdraw does not exist."}),
HTTPStatus.NOT_FOUND,
)
if withdraw.wallet != g.wallet.id:
return (
jsonify({"message": "Not your satsdice withdraw."}),
HTTPStatus.FORBIDDEN,
)
await delete_satsdice_withdraw(withdraw_id)
return "", HTTPStatus.NO_CONTENT
@satsdice_ext.route("/api/v1/withdraws/<the_hash>/<lnurl_id>", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_withdraw_hash_retrieve(the_hash, lnurl_id):
hashCheck = await get_withdraw_hash_check(the_hash, lnurl_id)
return jsonify(hashCheck), HTTPStatus.OK