remove satspay (#1520)
This commit is contained in:
parent
5a8db02c60
commit
6dade2edf3
15 changed files with 0 additions and 2291 deletions
|
|
@ -1,27 +0,0 @@
|
||||||
# SatsPay Server
|
|
||||||
|
|
||||||
## Create onchain and LN charges. Includes webhooks!
|
|
||||||
|
|
||||||
Easilly create invoices that support Lightning Network and on-chain BTC payment.
|
|
||||||
|
|
||||||
1. Create a "NEW CHARGE"\
|
|
||||||

|
|
||||||
2. Fill out the invoice fields
|
|
||||||
- set a descprition for the payment
|
|
||||||
- the amount in sats
|
|
||||||
- the time, in minutes, the invoice is valid for, after this period the invoice can't be payed
|
|
||||||
- set a webhook that will get the transaction details after a successful payment
|
|
||||||
- set to where the user should redirect after payment
|
|
||||||
- set the text for the button that will show after payment (not setting this, will display "NONE" in the button)
|
|
||||||
- select if you want onchain payment, LN payment or both
|
|
||||||
- depending on what you select you'll have to choose the respective wallets where to receive your payment\
|
|
||||||

|
|
||||||
3. The charge will appear on the _Charges_ section\
|
|
||||||

|
|
||||||
4. Your customer/payee will get the payment page
|
|
||||||
- they can choose to pay on LN\
|
|
||||||

|
|
||||||
- or pay on chain\
|
|
||||||

|
|
||||||
5. You can check the state of your charges in LNbits\
|
|
||||||

|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from fastapi import APIRouter
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
|
|
||||||
from lnbits.db import Database
|
|
||||||
from lnbits.helpers import template_renderer
|
|
||||||
from lnbits.tasks import catch_everything_and_restart
|
|
||||||
|
|
||||||
db = Database("ext_satspay")
|
|
||||||
|
|
||||||
|
|
||||||
satspay_ext: APIRouter = APIRouter(prefix="/satspay", tags=["satspay"])
|
|
||||||
|
|
||||||
satspay_static_files = [
|
|
||||||
{
|
|
||||||
"path": "/satspay/static",
|
|
||||||
"app": StaticFiles(directory="lnbits/extensions/satspay/static"),
|
|
||||||
"name": "satspay_static",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def satspay_renderer():
|
|
||||||
return template_renderer(["lnbits/extensions/satspay/templates"])
|
|
||||||
|
|
||||||
|
|
||||||
from .tasks import wait_for_paid_invoices
|
|
||||||
from .views import * # noqa: F401,F403
|
|
||||||
from .views_api import * # noqa: F401,F403
|
|
||||||
|
|
||||||
|
|
||||||
def satspay_start():
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"name": "SatsPay Server",
|
|
||||||
"short_description": "Create onchain and LN charges",
|
|
||||||
"tile": "/satspay/static/image/satspay.png",
|
|
||||||
"contributors": ["arcbtc"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
import json
|
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.core.services import create_invoice
|
|
||||||
from lnbits.core.views.api import api_payment
|
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
|
||||||
|
|
||||||
from ..watchonly.crud import get_config, get_fresh_address # type: ignore
|
|
||||||
from . import db
|
|
||||||
from .helpers import fetch_onchain_balance
|
|
||||||
from .models import Charges, CreateCharge, SatsPayThemes
|
|
||||||
|
|
||||||
|
|
||||||
async def create_charge(user: str, data: CreateCharge) -> Charges:
|
|
||||||
data = CreateCharge(**data.dict())
|
|
||||||
charge_id = urlsafe_short_hash()
|
|
||||||
if data.onchainwallet:
|
|
||||||
config = await get_config(user)
|
|
||||||
assert config
|
|
||||||
data.extra = json.dumps(
|
|
||||||
{"mempool_endpoint": config.mempool_endpoint, "network": config.network}
|
|
||||||
)
|
|
||||||
onchain = await get_fresh_address(data.onchainwallet)
|
|
||||||
if not onchain:
|
|
||||||
raise Exception(f"Wallet '{data.onchainwallet}' can no longer be accessed.")
|
|
||||||
onchainaddress = onchain.address
|
|
||||||
else:
|
|
||||||
onchainaddress = None
|
|
||||||
if data.lnbitswallet:
|
|
||||||
payment_hash, payment_request = await create_invoice(
|
|
||||||
wallet_id=data.lnbitswallet,
|
|
||||||
amount=data.amount,
|
|
||||||
memo=charge_id,
|
|
||||||
extra={"tag": "charge"},
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
payment_hash = None
|
|
||||||
payment_request = None
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO satspay.charges (
|
|
||||||
id,
|
|
||||||
"user",
|
|
||||||
description,
|
|
||||||
onchainwallet,
|
|
||||||
onchainaddress,
|
|
||||||
lnbitswallet,
|
|
||||||
payment_request,
|
|
||||||
payment_hash,
|
|
||||||
webhook,
|
|
||||||
completelink,
|
|
||||||
completelinktext,
|
|
||||||
time,
|
|
||||||
amount,
|
|
||||||
balance,
|
|
||||||
extra,
|
|
||||||
custom_css
|
|
||||||
)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
charge_id,
|
|
||||||
user,
|
|
||||||
data.description,
|
|
||||||
data.onchainwallet,
|
|
||||||
onchainaddress,
|
|
||||||
data.lnbitswallet,
|
|
||||||
payment_request,
|
|
||||||
payment_hash,
|
|
||||||
data.webhook,
|
|
||||||
data.completelink,
|
|
||||||
data.completelinktext,
|
|
||||||
data.time,
|
|
||||||
data.amount,
|
|
||||||
0,
|
|
||||||
data.extra,
|
|
||||||
data.custom_css,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
charge = await get_charge(charge_id)
|
|
||||||
assert charge, "Newly created charge does not exist"
|
|
||||||
return charge
|
|
||||||
|
|
||||||
|
|
||||||
async def update_charge(charge_id: str, **kwargs) -> Optional[Charges]:
|
|
||||||
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
|
|
||||||
await db.execute(
|
|
||||||
f"UPDATE satspay.charges SET {q} WHERE id = ?", (*kwargs.values(), charge_id)
|
|
||||||
)
|
|
||||||
row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
|
|
||||||
return Charges.from_row(row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_charge(charge_id: str) -> Optional[Charges]:
|
|
||||||
row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
|
|
||||||
return Charges.from_row(row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_charges(user: str) -> List[Charges]:
|
|
||||||
rows = await db.fetchall(
|
|
||||||
"""SELECT * FROM satspay.charges WHERE "user" = ? ORDER BY "timestamp" DESC """,
|
|
||||||
(user,),
|
|
||||||
)
|
|
||||||
return [Charges.from_row(row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_charge(charge_id: str) -> None:
|
|
||||||
await db.execute("DELETE FROM satspay.charges WHERE id = ?", (charge_id,))
|
|
||||||
|
|
||||||
|
|
||||||
async def check_address_balance(charge_id: str) -> Optional[Charges]:
|
|
||||||
charge = await get_charge(charge_id)
|
|
||||||
assert charge
|
|
||||||
|
|
||||||
if not charge.paid:
|
|
||||||
if charge.onchainaddress:
|
|
||||||
try:
|
|
||||||
respAmount = await fetch_onchain_balance(charge)
|
|
||||||
if respAmount > charge.balance:
|
|
||||||
await update_charge(charge_id=charge_id, balance=respAmount)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(e)
|
|
||||||
if charge.lnbitswallet:
|
|
||||||
invoice_status = await api_payment(charge.payment_hash)
|
|
||||||
|
|
||||||
if invoice_status["paid"]:
|
|
||||||
return await update_charge(charge_id=charge_id, balance=charge.amount)
|
|
||||||
return await get_charge(charge_id)
|
|
||||||
|
|
||||||
|
|
||||||
################## SETTINGS ###################
|
|
||||||
|
|
||||||
|
|
||||||
async def save_theme(data: SatsPayThemes, css_id: Optional[str]):
|
|
||||||
# insert or update
|
|
||||||
if css_id:
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
UPDATE satspay.themes SET custom_css = ?, title = ? WHERE css_id = ?
|
|
||||||
""",
|
|
||||||
(data.custom_css, data.title, css_id),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
css_id = urlsafe_short_hash()
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO satspay.themes (
|
|
||||||
css_id,
|
|
||||||
title,
|
|
||||||
"user",
|
|
||||||
custom_css
|
|
||||||
)
|
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
css_id,
|
|
||||||
data.title,
|
|
||||||
data.user,
|
|
||||||
data.custom_css,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return await get_theme(css_id)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_theme(css_id: str) -> Optional[SatsPayThemes]:
|
|
||||||
row = await db.fetchone("SELECT * FROM satspay.themes WHERE css_id = ?", (css_id,))
|
|
||||||
return SatsPayThemes.from_row(row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_themes(user_id: str) -> List[SatsPayThemes]:
|
|
||||||
rows = await db.fetchall(
|
|
||||||
"""SELECT * FROM satspay.themes WHERE "user" = ? ORDER BY "title" DESC """,
|
|
||||||
(user_id,),
|
|
||||||
)
|
|
||||||
return [SatsPayThemes.from_row(row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_theme(theme_id: str) -> None:
|
|
||||||
await db.execute("DELETE FROM satspay.themes WHERE css_id = ?", (theme_id,))
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
import httpx
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from .models import Charges
|
|
||||||
|
|
||||||
|
|
||||||
def public_charge(charge: Charges):
|
|
||||||
c = {
|
|
||||||
"id": charge.id,
|
|
||||||
"description": charge.description,
|
|
||||||
"onchainaddress": charge.onchainaddress,
|
|
||||||
"payment_request": charge.payment_request,
|
|
||||||
"payment_hash": charge.payment_hash,
|
|
||||||
"time": charge.time,
|
|
||||||
"amount": charge.amount,
|
|
||||||
"balance": charge.balance,
|
|
||||||
"paid": charge.paid,
|
|
||||||
"timestamp": charge.timestamp,
|
|
||||||
"time_elapsed": charge.time_elapsed,
|
|
||||||
"time_left": charge.time_left,
|
|
||||||
"custom_css": charge.custom_css,
|
|
||||||
}
|
|
||||||
|
|
||||||
if charge.paid:
|
|
||||||
c["completelink"] = charge.completelink
|
|
||||||
c["completelinktext"] = charge.completelinktext
|
|
||||||
|
|
||||||
return c
|
|
||||||
|
|
||||||
|
|
||||||
async def call_webhook(charge: Charges):
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
try:
|
|
||||||
assert charge.webhook
|
|
||||||
r = await client.post(
|
|
||||||
charge.webhook,
|
|
||||||
json=public_charge(charge),
|
|
||||||
timeout=40,
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"webhook_success": r.is_success,
|
|
||||||
"webhook_message": r.reason_phrase,
|
|
||||||
"webhook_response": r.text,
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Failed to call webhook for charge {charge.id}")
|
|
||||||
logger.warning(e)
|
|
||||||
return {"webhook_success": False, "webhook_message": str(e)}
|
|
||||||
|
|
||||||
|
|
||||||
async def fetch_onchain_balance(charge: Charges):
|
|
||||||
endpoint = (
|
|
||||||
f"{charge.config.mempool_endpoint}/testnet"
|
|
||||||
if charge.config.network == "Testnet"
|
|
||||||
else charge.config.mempool_endpoint
|
|
||||||
)
|
|
||||||
assert endpoint
|
|
||||||
assert charge.onchainaddress
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
r = await client.get(endpoint + "/api/address/" + charge.onchainaddress)
|
|
||||||
return r.json()["chain_stats"]["funded_txo_sum"]
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
async def m001_initial(db):
|
|
||||||
"""
|
|
||||||
Initial wallet table.
|
|
||||||
"""
|
|
||||||
|
|
||||||
await db.execute(
|
|
||||||
f"""
|
|
||||||
CREATE TABLE satspay.charges (
|
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
|
||||||
"user" TEXT,
|
|
||||||
description TEXT,
|
|
||||||
onchainwallet TEXT,
|
|
||||||
onchainaddress TEXT,
|
|
||||||
lnbitswallet TEXT,
|
|
||||||
payment_request TEXT,
|
|
||||||
payment_hash TEXT,
|
|
||||||
webhook TEXT,
|
|
||||||
completelink TEXT,
|
|
||||||
completelinktext TEXT,
|
|
||||||
time INTEGER,
|
|
||||||
amount {db.big_int},
|
|
||||||
balance {db.big_int} DEFAULT 0,
|
|
||||||
timestamp TIMESTAMP NOT NULL DEFAULT """
|
|
||||||
+ db.timestamp_now
|
|
||||||
+ """
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m002_add_charge_extra_data(db):
|
|
||||||
"""
|
|
||||||
Add 'extra' column for storing various config about the charge (JSON format)
|
|
||||||
"""
|
|
||||||
await db.execute(
|
|
||||||
"""ALTER TABLE satspay.charges
|
|
||||||
ADD COLUMN extra TEXT DEFAULT '{"mempool_endpoint": "https://mempool.space", "network": "Mainnet"}';
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m003_add_themes_table(db):
|
|
||||||
"""
|
|
||||||
Themes table
|
|
||||||
"""
|
|
||||||
|
|
||||||
await db.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE satspay.themes (
|
|
||||||
css_id TEXT NOT NULL PRIMARY KEY,
|
|
||||||
"user" TEXT,
|
|
||||||
title TEXT,
|
|
||||||
custom_css TEXT
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def m004_add_custom_css_to_charges(db):
|
|
||||||
"""
|
|
||||||
Add custom css option column to the 'charges' table
|
|
||||||
"""
|
|
||||||
|
|
||||||
await db.execute("ALTER TABLE satspay.charges ADD COLUMN custom_css TEXT;")
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
import json
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from sqlite3 import Row
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from fastapi.param_functions import Query
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
DEFAULT_MEMPOOL_CONFIG = (
|
|
||||||
'{"mempool_endpoint": "https://mempool.space", "network": "Mainnet"}'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateCharge(BaseModel):
|
|
||||||
onchainwallet: str = Query(None)
|
|
||||||
lnbitswallet: str = Query(None)
|
|
||||||
description: str = Query(...)
|
|
||||||
webhook: str = Query(None)
|
|
||||||
completelink: str = Query(None)
|
|
||||||
completelinktext: str = Query(None)
|
|
||||||
custom_css: Optional[str]
|
|
||||||
time: int = Query(..., ge=1)
|
|
||||||
amount: int = Query(..., ge=1)
|
|
||||||
extra: str = DEFAULT_MEMPOOL_CONFIG
|
|
||||||
|
|
||||||
|
|
||||||
class ChargeConfig(BaseModel):
|
|
||||||
mempool_endpoint: Optional[str]
|
|
||||||
network: Optional[str]
|
|
||||||
webhook_success: Optional[bool] = False
|
|
||||||
webhook_message: Optional[str]
|
|
||||||
|
|
||||||
|
|
||||||
class Charges(BaseModel):
|
|
||||||
id: str
|
|
||||||
description: Optional[str]
|
|
||||||
onchainwallet: Optional[str]
|
|
||||||
onchainaddress: Optional[str]
|
|
||||||
lnbitswallet: Optional[str]
|
|
||||||
payment_request: Optional[str]
|
|
||||||
payment_hash: Optional[str]
|
|
||||||
webhook: Optional[str]
|
|
||||||
completelink: Optional[str]
|
|
||||||
completelinktext: Optional[str] = "Back to Merchant"
|
|
||||||
custom_css: Optional[str]
|
|
||||||
extra: str = DEFAULT_MEMPOOL_CONFIG
|
|
||||||
time: int
|
|
||||||
amount: int
|
|
||||||
balance: int
|
|
||||||
timestamp: int
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_row(cls, row: Row) -> "Charges":
|
|
||||||
return cls(**dict(row))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def time_left(self):
|
|
||||||
now = datetime.utcnow().timestamp()
|
|
||||||
start = datetime.fromtimestamp(self.timestamp)
|
|
||||||
expiration = (start + timedelta(minutes=self.time)).timestamp()
|
|
||||||
return (expiration - now) / 60
|
|
||||||
|
|
||||||
@property
|
|
||||||
def time_elapsed(self):
|
|
||||||
return self.time_left < 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def paid(self):
|
|
||||||
if self.balance >= self.amount:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def config(self) -> ChargeConfig:
|
|
||||||
charge_config = json.loads(self.extra)
|
|
||||||
return ChargeConfig(**charge_config)
|
|
||||||
|
|
||||||
def must_call_webhook(self):
|
|
||||||
return self.webhook and self.paid and self.config.webhook_success is False
|
|
||||||
|
|
||||||
|
|
||||||
class SatsPayThemes(BaseModel):
|
|
||||||
css_id: str = Query(None)
|
|
||||||
title: str = Query(None)
|
|
||||||
custom_css: str = Query(None)
|
|
||||||
user: Optional[str]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_row(cls, row: Row) -> "SatsPayThemes":
|
|
||||||
return cls(**dict(row))
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB |
|
|
@ -1,36 +0,0 @@
|
||||||
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
||||||
const retryWithDelay = async function (fn, retryCount = 0) {
|
|
||||||
try {
|
|
||||||
await sleep(25)
|
|
||||||
// Do not return the call directly, use result.
|
|
||||||
// Otherwise the error will not be cought in this try-catch block.
|
|
||||||
const result = await fn()
|
|
||||||
return result
|
|
||||||
} catch (err) {
|
|
||||||
if (retryCount > 100) throw err
|
|
||||||
await sleep((retryCount + 1) * 1000)
|
|
||||||
return retryWithDelay(fn, retryCount + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapCharge = (obj, oldObj = {}) => {
|
|
||||||
const charge = {...oldObj, ...obj}
|
|
||||||
|
|
||||||
charge.progress = obj.time_left < 0 ? 1 : 1 - obj.time_left / obj.time
|
|
||||||
charge.time = minutesToTime(obj.time)
|
|
||||||
charge.timeLeft = minutesToTime(obj.time_left)
|
|
||||||
|
|
||||||
charge.displayUrl = ['/satspay/', obj.id].join('')
|
|
||||||
charge.expanded = oldObj.expanded || false
|
|
||||||
charge.pendingBalance = oldObj.pendingBalance || 0
|
|
||||||
charge.extra = charge.extra ? JSON.parse(charge.extra) : charge.extra
|
|
||||||
return charge
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapCSS = (obj, oldObj = {}) => {
|
|
||||||
const theme = _.clone(obj)
|
|
||||||
return theme
|
|
||||||
}
|
|
||||||
|
|
||||||
const minutesToTime = min =>
|
|
||||||
min > 0 ? new Date(min * 1000).toISOString().substring(14, 19) : ''
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.core.models import Payment
|
|
||||||
from lnbits.helpers import get_current_extension_name
|
|
||||||
from lnbits.tasks import register_invoice_listener
|
|
||||||
|
|
||||||
from .crud import check_address_balance, get_charge, update_charge
|
|
||||||
from .helpers import call_webhook
|
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_paid_invoices():
|
|
||||||
invoice_queue = asyncio.Queue()
|
|
||||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
|
||||||
|
|
||||||
while True:
|
|
||||||
payment = await invoice_queue.get()
|
|
||||||
await on_invoice_paid(payment)
|
|
||||||
|
|
||||||
|
|
||||||
async def on_invoice_paid(payment: Payment) -> None:
|
|
||||||
|
|
||||||
if payment.extra.get("tag") != "charge":
|
|
||||||
# not a charge invoice
|
|
||||||
return
|
|
||||||
|
|
||||||
assert payment.memo
|
|
||||||
charge = await get_charge(payment.memo)
|
|
||||||
if not charge:
|
|
||||||
logger.error("this should never happen", payment)
|
|
||||||
return
|
|
||||||
|
|
||||||
await payment.set_pending(False)
|
|
||||||
charge = await check_address_balance(charge_id=charge.id)
|
|
||||||
assert charge
|
|
||||||
|
|
||||||
if charge.must_call_webhook():
|
|
||||||
resp = await call_webhook(charge)
|
|
||||||
extra = {**charge.config.dict(), **resp}
|
|
||||||
await update_charge(charge_id=charge.id, extra=json.dumps(extra))
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
<q-card>
|
|
||||||
<q-card-section>
|
|
||||||
<p>
|
|
||||||
SatsPayServer, create Onchain/LN charges.<br />WARNING: If using with the
|
|
||||||
WatchOnly extension, we highly reccomend using a fresh extended public Key
|
|
||||||
specifically for SatsPayServer!<br />
|
|
||||||
<small>
|
|
||||||
Created by,
|
|
||||||
<a class="text-secondary" href="https://github.com/benarc">Ben Arc</a>,
|
|
||||||
<a
|
|
||||||
class="text-secondary"
|
|
||||||
target="_blank"
|
|
||||||
style="color: unset"
|
|
||||||
href="https://github.com/motorina0"
|
|
||||||
>motorina0</a
|
|
||||||
></small
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<a
|
|
||||||
class="text-secondary"
|
|
||||||
target="_blank"
|
|
||||||
href="/docs#/satspay"
|
|
||||||
class="text-white"
|
|
||||||
>Swagger REST API Documentation</a
|
|
||||||
>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
|
|
@ -1,479 +0,0 @@
|
||||||
{% extends "public.html" %} {% block page %}
|
|
||||||
<div class="row items-center q-mt-md">
|
|
||||||
<div class="col-lg-4 col-md-3 col-sm-1"></div>
|
|
||||||
<div class="col-lg-4 col-md-6 col-sm-10">
|
|
||||||
<q-card>
|
|
||||||
<div class="row q-mb-md">
|
|
||||||
<div class="col text-center q-mt-md">
|
|
||||||
<span class="text-h4" v-text="charge.description"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col text-center">
|
|
||||||
<div
|
|
||||||
color="white"
|
|
||||||
style="background-color: grey; height: 30px; padding: 5px"
|
|
||||||
v-if="!charge.timeLeft"
|
|
||||||
>
|
|
||||||
Time elapsed
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
color="white"
|
|
||||||
style="background-color: grey; height: 30px; padding: 5px"
|
|
||||||
v-else-if="charge.paid"
|
|
||||||
>
|
|
||||||
Charge paid
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<q-linear-progress
|
|
||||||
size="30px"
|
|
||||||
:value="charge.progress"
|
|
||||||
color="secondary"
|
|
||||||
>
|
|
||||||
<q-item-section>
|
|
||||||
<q-item style="padding: 3px">
|
|
||||||
<q-spinner color="white" size="0.8em"></q-spinner
|
|
||||||
><span style="font-size: 15px; color: white"
|
|
||||||
><span class="q-pr-xl q-pl-md"> Awaiting payment...</span>
|
|
||||||
<span class="q-pl-xl" style="color: white">
|
|
||||||
{% raw %} {{ charge.timeLeft }} {% endraw %}</span
|
|
||||||
></span
|
|
||||||
>
|
|
||||||
</q-item>
|
|
||||||
</q-item-section>
|
|
||||||
</q-linear-progress>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row q-ml-md q-mt-md q-mb-lg">
|
|
||||||
<div class="col">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-4 q-pr-lg">Charge Id:</div>
|
|
||||||
<div class="col-8 q-pr-lg">
|
|
||||||
<q-btn flat dense outline @click="copyText(charge.id)"
|
|
||||||
><span v-text="charge.id"></span
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center">
|
|
||||||
<div class="col-4 q-pr-lg">Total to pay:</div>
|
|
||||||
<div class="col-8 q-pr-lg">
|
|
||||||
<q-badge color="blue">
|
|
||||||
<span v-text="charge.amount" class="text-subtitle2"></span> sat
|
|
||||||
</q-badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center q-mt-sm">
|
|
||||||
<div class="col-4 q-pr-lg">Amount paid:</div>
|
|
||||||
<div class="col-8 q-pr-lg">
|
|
||||||
<q-badge color="orange">
|
|
||||||
<span v-text="charge.balance" class="text-subtitle2"></span>
|
|
||||||
sat</q-badge
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="pendingFunds" class="row items-center q-mt-sm">
|
|
||||||
<div class="col-4 q-pr-lg">Amount pending:</div>
|
|
||||||
<div class="col-8 q-pr-lg">
|
|
||||||
<q-badge color="gray">
|
|
||||||
<span v-text="pendingFunds" class="text-subtitle2"></span> sat
|
|
||||||
</q-badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center q-mt-sm">
|
|
||||||
<div class="col-4 q-pr-lg">Amount due:</div>
|
|
||||||
<div class="col-8 q-pr-lg">
|
|
||||||
<q-badge v-if="charge.amount - charge.balance > 0" color="green">
|
|
||||||
<span
|
|
||||||
v-text="charge.amount - charge.balance"
|
|
||||||
class="text-subtitle2"
|
|
||||||
></span>
|
|
||||||
sat
|
|
||||||
</q-badge>
|
|
||||||
<q-badge
|
|
||||||
v-else="charge.amount - charge.balance <= 0"
|
|
||||||
color="green"
|
|
||||||
class="text-subtitle2"
|
|
||||||
>
|
|
||||||
none</q-badge
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
disable
|
|
||||||
v-if="!charge.payment_request || charge.time_elapsed"
|
|
||||||
style="color: primary; width: 100%"
|
|
||||||
label="lightning⚡"
|
|
||||||
>
|
|
||||||
<q-tooltip>
|
|
||||||
bitcoin lightning payment method not available
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
v-else
|
|
||||||
@click="payInvoice"
|
|
||||||
style="color: primary; width: 100%"
|
|
||||||
label="lightning⚡"
|
|
||||||
>
|
|
||||||
<q-tooltip> pay with lightning </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
disable
|
|
||||||
v-if="!charge.onchainaddress || charge.time_elapsed"
|
|
||||||
style="color: primary; width: 100%"
|
|
||||||
label="onchain⛓️"
|
|
||||||
>
|
|
||||||
<q-tooltip>
|
|
||||||
bitcoin onchain payment method not available
|
|
||||||
</q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
v-else
|
|
||||||
@click="payOnchain"
|
|
||||||
style="color: primary; width: 100%"
|
|
||||||
label="onchain⛓️"
|
|
||||||
>
|
|
||||||
<q-tooltip> pay onchain </q-tooltip>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<q-separator></q-separator>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
<q-card class="q-pa-lg" v-if="lnbtc">
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<div class="row items-center q-mt-sm">
|
|
||||||
<div class="col-md-2 col-sm-0"></div>
|
|
||||||
<div class="col-md-8 col-sm-12">
|
|
||||||
<div v-if="!charge.timeLeft && !charge.paid">
|
|
||||||
<q-icon
|
|
||||||
name="block"
|
|
||||||
style="color: #ccc; font-size: 21.4em"
|
|
||||||
></q-icon>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="charge.paid">
|
|
||||||
<q-icon
|
|
||||||
name="check"
|
|
||||||
style="color: green; font-size: 21.4em"
|
|
||||||
></q-icon>
|
|
||||||
<div class="row text-center q-mt-lg">
|
|
||||||
<div class="col text-center">
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
v-if="charge.completelink"
|
|
||||||
type="a"
|
|
||||||
:href="charge.completelink"
|
|
||||||
:label="charge.completelinktext"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<div class="row text-center q-mb-sm">
|
|
||||||
<div class="col text-center">
|
|
||||||
<span class="text-subtitle2"
|
|
||||||
>Pay this lightning-network invoice:</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a
|
|
||||||
class="text-secondary"
|
|
||||||
:href="'lightning:'+charge.payment_request"
|
|
||||||
>
|
|
||||||
<q-responsive :ratio="1" class="q-mx-md">
|
|
||||||
<qrcode
|
|
||||||
:value="'lightning:' + charge.payment_request.toUpperCase()"
|
|
||||||
:options="{width: 800}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode>
|
|
||||||
</q-responsive>
|
|
||||||
</a>
|
|
||||||
<div class="row text-center q-mt-lg">
|
|
||||||
<div class="col text-center">
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
@click="copyText(charge.payment_request)"
|
|
||||||
>Copy invoice</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2 col-sm-0"></div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
<q-card class="q-pa-lg" v-if="onbtc">
|
|
||||||
<q-card-section class="q-pa-none">
|
|
||||||
<div v-if="charge.timeLeft && !charge.paid" class="row items-center">
|
|
||||||
<div class="col text-center">
|
|
||||||
<a
|
|
||||||
class="text-secondary"
|
|
||||||
style="color: unset"
|
|
||||||
:href="'https://' + mempoolHostname + '/address/' + charge.onchainaddress"
|
|
||||||
target="_blank"
|
|
||||||
><span
|
|
||||||
class="text-subtitle1"
|
|
||||||
v-text="charge.onchainaddress"
|
|
||||||
></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center q-mt-md">
|
|
||||||
<div class="col-md-2 col-sm-0"></div>
|
|
||||||
<div class="col-md-8 col-sm-12 text-center">
|
|
||||||
<div v-if="!charge.timeLeft && !charge.paid">
|
|
||||||
<q-icon
|
|
||||||
name="block"
|
|
||||||
style="color: #ccc; font-size: 21.4em"
|
|
||||||
></q-icon>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="charge.paid">
|
|
||||||
<q-icon
|
|
||||||
name="check"
|
|
||||||
style="color: green; font-size: 21.4em"
|
|
||||||
></q-icon>
|
|
||||||
<div class="row text-center q-mt-lg">
|
|
||||||
<div class="col text-center">
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
v-if="charge.webhook"
|
|
||||||
type="a"
|
|
||||||
:href="charge.completelink"
|
|
||||||
:label="charge.completelinktext"
|
|
||||||
></q-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<div class="row items-center q-mb-sm">
|
|
||||||
<div class="col text-center">
|
|
||||||
<span class="text-subtitle2"
|
|
||||||
>Send
|
|
||||||
|
|
||||||
<span v-text="charge.amount"></span>
|
|
||||||
sats to this onchain address</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a
|
|
||||||
class="text-secondary"
|
|
||||||
:href="'bitcoin:'+charge.onchainaddress"
|
|
||||||
>
|
|
||||||
<q-responsive :ratio="1" class="q-mx-md">
|
|
||||||
<qrcode
|
|
||||||
:value="charge.onchainaddress"
|
|
||||||
:options="{width: 800}"
|
|
||||||
class="rounded-borders"
|
|
||||||
></qrcode>
|
|
||||||
</q-responsive>
|
|
||||||
</a>
|
|
||||||
<div class="row items-center q-mt-lg">
|
|
||||||
<div class="col text-center">
|
|
||||||
<q-btn
|
|
||||||
outline
|
|
||||||
color="grey"
|
|
||||||
@click="copyText(charge.onchainaddress)"
|
|
||||||
>Copy address</q-btn
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2 col-sm-0"></div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg- 4 col-md-3 col-sm-1"></div>
|
|
||||||
</div>
|
|
||||||
{% endblock %} {% block styles %}
|
|
||||||
<link
|
|
||||||
href="/satspay/css/{{ charge_data.custom_css }}"
|
|
||||||
rel="stylesheet"
|
|
||||||
type="text/css"
|
|
||||||
/>
|
|
||||||
<style>
|
|
||||||
header button.q-btn-dropdown {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %} {% block scripts %}
|
|
||||||
|
|
||||||
<script src="https://mempool.space/mempool.js"></script>
|
|
||||||
<script src="{{ url_for('satspay_static', path='js/utils.js') }}"></script>
|
|
||||||
<script>
|
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
el: '#vue',
|
|
||||||
mixins: [windowMixin],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
charge: JSON.parse('{{charge_data | tojson}}'),
|
|
||||||
mempoolEndpoint: '{{mempool_endpoint}}',
|
|
||||||
network: '{{network}}',
|
|
||||||
pendingFunds: 0,
|
|
||||||
ws: null,
|
|
||||||
newProgress: 0.4,
|
|
||||||
counter: 1,
|
|
||||||
lnbtc: true,
|
|
||||||
onbtc: false,
|
|
||||||
wallet: {
|
|
||||||
inkey: ''
|
|
||||||
},
|
|
||||||
cancelListener: () => {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
mempoolHostname: function () {
|
|
||||||
let hostname = new URL(this.mempoolEndpoint).hostname
|
|
||||||
if (this.network === 'Testnet') {
|
|
||||||
hostname += '/testnet'
|
|
||||||
}
|
|
||||||
return hostname
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
checkBalances: async function () {
|
|
||||||
if (!this.charge.payment_request && this.charge.hasOnchainStaleBalance)
|
|
||||||
return
|
|
||||||
try {
|
|
||||||
const {data} = await LNbits.api.request(
|
|
||||||
'GET',
|
|
||||||
`/satspay/api/v1/charge/balance/${this.charge.id}`
|
|
||||||
)
|
|
||||||
this.charge = mapCharge(data, this.charge)
|
|
||||||
} catch (error) {
|
|
||||||
LNbits.utils.notifyApiError(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
checkPendingOnchain: async function () {
|
|
||||||
if (!this.charge.onchainaddress) return
|
|
||||||
|
|
||||||
const {
|
|
||||||
bitcoin: {addresses: addressesAPI}
|
|
||||||
} = mempoolJS({
|
|
||||||
hostname: new URL(this.mempoolEndpoint).hostname
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
const utxos = await addressesAPI.getAddressTxsUtxo({
|
|
||||||
address: this.charge.onchainaddress
|
|
||||||
})
|
|
||||||
const newBalance = utxos.reduce((t, u) => t + u.value, 0)
|
|
||||||
this.charge.hasOnchainStaleBalance =
|
|
||||||
this.charge.balance === newBalance
|
|
||||||
|
|
||||||
this.pendingFunds = utxos
|
|
||||||
.filter(u => !u.status.confirmed)
|
|
||||||
.reduce((t, u) => t + u.value, 0)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('cannot check pending funds')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
payInvoice: function () {
|
|
||||||
this.lnbtc = true
|
|
||||||
this.onbtc = false
|
|
||||||
},
|
|
||||||
payOnchain: function () {
|
|
||||||
this.lnbtc = false
|
|
||||||
this.onbtc = true
|
|
||||||
},
|
|
||||||
|
|
||||||
loopRefresh: function () {
|
|
||||||
// invoice only
|
|
||||||
const refreshIntervalId = setInterval(async () => {
|
|
||||||
if (this.charge.paid || !this.charge.timeLeft) {
|
|
||||||
clearInterval(refreshIntervalId)
|
|
||||||
}
|
|
||||||
if (this.counter % 10 === 0) {
|
|
||||||
await this.checkBalances()
|
|
||||||
await this.checkPendingOnchain()
|
|
||||||
}
|
|
||||||
this.counter++
|
|
||||||
}, 1000)
|
|
||||||
},
|
|
||||||
initWs: async function () {
|
|
||||||
const {
|
|
||||||
bitcoin: {websocket}
|
|
||||||
} = mempoolJS({
|
|
||||||
hostname: new URL(this.mempoolEndpoint).hostname
|
|
||||||
})
|
|
||||||
|
|
||||||
this.ws = new WebSocket(`wss://${this.mempoolHostname}/api/v1/ws`)
|
|
||||||
this.ws.addEventListener('open', x => {
|
|
||||||
if (this.charge.onchainaddress) {
|
|
||||||
this.trackAddress(this.charge.onchainaddress)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.ws.addEventListener('message', async ({data}) => {
|
|
||||||
const res = JSON.parse(data.toString())
|
|
||||||
if (res['address-transactions']) {
|
|
||||||
await this.checkBalances()
|
|
||||||
this.$q.notify({
|
|
||||||
type: 'positive',
|
|
||||||
message: 'New payment received!',
|
|
||||||
timeout: 10000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
loopPingWs: function () {
|
|
||||||
setInterval(() => {
|
|
||||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) this.initWs()
|
|
||||||
this.ws.send(JSON.stringify({action: 'ping'}))
|
|
||||||
}, 30 * 1000)
|
|
||||||
},
|
|
||||||
trackAddress: async function (address, retry = 0) {
|
|
||||||
try {
|
|
||||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) this.initWs()
|
|
||||||
this.ws.send(JSON.stringify({'track-address': address}))
|
|
||||||
} catch (error) {
|
|
||||||
await sleep(1000)
|
|
||||||
if (retry > 10) throw error
|
|
||||||
this.trackAddress(address, retry + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created: async function () {
|
|
||||||
// Remove a user defined theme
|
|
||||||
if (this.charge.custom_css) {
|
|
||||||
document.body.setAttribute('data-theme', '')
|
|
||||||
}
|
|
||||||
if (this.charge.payment_request) this.payInvoice()
|
|
||||||
else this.payOnchain()
|
|
||||||
|
|
||||||
await this.checkBalances()
|
|
||||||
|
|
||||||
if (!this.charge.paid) {
|
|
||||||
this.loopRefresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.charge.onchainaddress) {
|
|
||||||
this.loopPingWs()
|
|
||||||
this.checkPendingOnchain()
|
|
||||||
this.trackAddress(this.charge.onchainaddress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,49 +0,0 @@
|
||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException, Request, Response
|
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
from starlette.responses import HTMLResponse
|
|
||||||
|
|
||||||
from lnbits.core.models import User
|
|
||||||
from lnbits.decorators import check_user_exists
|
|
||||||
|
|
||||||
from . import satspay_ext, satspay_renderer
|
|
||||||
from .crud import get_charge, get_theme
|
|
||||||
from .helpers import public_charge
|
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.get("/", response_class=HTMLResponse)
|
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
|
||||||
return satspay_renderer().TemplateResponse(
|
|
||||||
"satspay/index.html",
|
|
||||||
{"request": request, "user": user.dict(), "admin": user.admin},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.get("/{charge_id}", response_class=HTMLResponse)
|
|
||||||
async def display_charge(request: Request, charge_id: str):
|
|
||||||
charge = await get_charge(charge_id)
|
|
||||||
if not charge:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
|
|
||||||
)
|
|
||||||
|
|
||||||
return satspay_renderer().TemplateResponse(
|
|
||||||
"satspay/display.html",
|
|
||||||
{
|
|
||||||
"request": request,
|
|
||||||
"charge_data": public_charge(charge),
|
|
||||||
"mempool_endpoint": charge.config.mempool_endpoint,
|
|
||||||
"network": charge.config.network,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.get("/css/{css_id}")
|
|
||||||
async def display_css(css_id: str):
|
|
||||||
theme = await get_theme(css_id)
|
|
||||||
if theme:
|
|
||||||
return Response(content=theme.custom_css, media_type="text/css")
|
|
||||||
return None
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
import json
|
|
||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException, Query
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
from lnbits.decorators import (
|
|
||||||
WalletTypeInfo,
|
|
||||||
check_admin,
|
|
||||||
get_key_type,
|
|
||||||
require_admin_key,
|
|
||||||
require_invoice_key,
|
|
||||||
)
|
|
||||||
|
|
||||||
from . import satspay_ext
|
|
||||||
from .crud import (
|
|
||||||
check_address_balance,
|
|
||||||
create_charge,
|
|
||||||
delete_charge,
|
|
||||||
delete_theme,
|
|
||||||
get_charge,
|
|
||||||
get_charges,
|
|
||||||
get_theme,
|
|
||||||
get_themes,
|
|
||||||
save_theme,
|
|
||||||
update_charge,
|
|
||||||
)
|
|
||||||
from .helpers import call_webhook, public_charge
|
|
||||||
from .models import CreateCharge, SatsPayThemes
|
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.post("/api/v1/charge")
|
|
||||||
async def api_charge_create(
|
|
||||||
data: CreateCharge, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
charge = await create_charge(user=wallet.wallet.user, data=data)
|
|
||||||
assert charge
|
|
||||||
return {
|
|
||||||
**charge.dict(),
|
|
||||||
**{"time_elapsed": charge.time_elapsed},
|
|
||||||
**{"time_left": charge.time_left},
|
|
||||||
**{"paid": charge.paid},
|
|
||||||
}
|
|
||||||
except Exception as ex:
|
|
||||||
logger.debug(f"Satspay error: {str}")
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.put(
|
|
||||||
"/api/v1/charge/{charge_id}", dependencies=[Depends(require_admin_key)]
|
|
||||||
)
|
|
||||||
async def api_charge_update(
|
|
||||||
data: CreateCharge,
|
|
||||||
charge_id: str,
|
|
||||||
):
|
|
||||||
charge = await update_charge(charge_id=charge_id, data=data)
|
|
||||||
assert charge
|
|
||||||
return charge.dict()
|
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.get("/api/v1/charges")
|
|
||||||
async def api_charges_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
|
|
||||||
try:
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
**charge.dict(),
|
|
||||||
**{"time_elapsed": charge.time_elapsed},
|
|
||||||
**{"time_left": charge.time_left},
|
|
||||||
**{"paid": charge.paid},
|
|
||||||
**{"webhook_message": charge.config.webhook_message},
|
|
||||||
}
|
|
||||||
for charge in await get_charges(wallet.wallet.user)
|
|
||||||
]
|
|
||||||
except:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.get("/api/v1/charge/{charge_id}", dependencies=[Depends(get_key_type)])
|
|
||||||
async def api_charge_retrieve(charge_id: str):
|
|
||||||
charge = await get_charge(charge_id)
|
|
||||||
|
|
||||||
if not charge:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Charge does not exist."
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
**charge.dict(),
|
|
||||||
**{"time_elapsed": charge.time_elapsed},
|
|
||||||
**{"time_left": charge.time_left},
|
|
||||||
**{"paid": charge.paid},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.delete("/api/v1/charge/{charge_id}", dependencies=[Depends(get_key_type)])
|
|
||||||
async def api_charge_delete(charge_id: str):
|
|
||||||
charge = await get_charge(charge_id)
|
|
||||||
|
|
||||||
if not charge:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Charge does not exist."
|
|
||||||
)
|
|
||||||
|
|
||||||
await delete_charge(charge_id)
|
|
||||||
return "", HTTPStatus.NO_CONTENT
|
|
||||||
|
|
||||||
|
|
||||||
#############################BALANCE##########################
|
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.get("/api/v1/charges/balance/{charge_ids}")
|
|
||||||
async def api_charges_balance(charge_ids):
|
|
||||||
charge_id_list = charge_ids.split(",")
|
|
||||||
charges = []
|
|
||||||
for charge_id in charge_id_list:
|
|
||||||
charge = await api_charge_balance(charge_id)
|
|
||||||
charges.append(charge)
|
|
||||||
return charges
|
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.get("/api/v1/charge/balance/{charge_id}")
|
|
||||||
async def api_charge_balance(charge_id):
|
|
||||||
charge = await check_address_balance(charge_id)
|
|
||||||
|
|
||||||
if not charge:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Charge does not exist."
|
|
||||||
)
|
|
||||||
|
|
||||||
if charge.must_call_webhook():
|
|
||||||
resp = await call_webhook(charge)
|
|
||||||
extra = {**charge.config.dict(), **resp}
|
|
||||||
await update_charge(charge_id=charge.id, extra=json.dumps(extra))
|
|
||||||
|
|
||||||
return {**public_charge(charge)}
|
|
||||||
|
|
||||||
|
|
||||||
#############################THEMES##########################
|
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.post("/api/v1/themes", dependencies=[Depends(check_admin)])
|
|
||||||
@satspay_ext.post("/api/v1/themes/{css_id}", dependencies=[Depends(check_admin)])
|
|
||||||
async def api_themes_save(
|
|
||||||
data: SatsPayThemes,
|
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
||||||
css_id: str = Query(...),
|
|
||||||
):
|
|
||||||
|
|
||||||
if css_id:
|
|
||||||
theme = await save_theme(css_id=css_id, data=data)
|
|
||||||
else:
|
|
||||||
data.user = wallet.wallet.user
|
|
||||||
theme = await save_theme(data=data, css_id="no_id")
|
|
||||||
return theme
|
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.get("/api/v1/themes")
|
|
||||||
async def api_themes_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
|
|
||||||
try:
|
|
||||||
return await get_themes(wallet.wallet.user)
|
|
||||||
except HTTPException:
|
|
||||||
logger.error("Error loading satspay themes")
|
|
||||||
logger.error(HTTPException)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
@satspay_ext.delete("/api/v1/themes/{theme_id}", dependencies=[Depends(get_key_type)])
|
|
||||||
async def api_theme_delete(theme_id):
|
|
||||||
theme = await get_theme(theme_id)
|
|
||||||
|
|
||||||
if not theme:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Theme does not exist."
|
|
||||||
)
|
|
||||||
|
|
||||||
await delete_theme(theme_id)
|
|
||||||
return "", HTTPStatus.NO_CONTENT
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue