remove satspay (#1520)

This commit is contained in:
dni ⚡ 2023-02-20 11:23:43 +01:00 committed by GitHub
parent 5a8db02c60
commit 6dade2edf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 0 additions and 2291 deletions

View File

@ -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"\
![new charge](https://i.imgur.com/fUl6p74.png)
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\
![charge form](https://i.imgur.com/F10yRiW.png)
3. The charge will appear on the _Charges_ section\
![charges](https://i.imgur.com/zqHpVxc.png)
4. Your customer/payee will get the payment page
- they can choose to pay on LN\
![offchain payment](https://i.imgur.com/4191SMV.png)
- or pay on chain\
![onchain payment](https://i.imgur.com/wzLRR5N.png)
5. You can check the state of your charges in LNbits\
![invoice state](https://i.imgur.com/JnBd22p.png)

View File

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

View File

@ -1,6 +0,0 @@
{
"name": "SatsPay Server",
"short_description": "Create onchain and LN charges",
"tile": "/satspay/static/image/satspay.png",
"contributors": ["arcbtc"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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