mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-03-13 11:35:56 +01:00
Merge remote-tracking branch 'origin/FinalAdminUI' into FinalAdminUI
This commit is contained in:
commit
59deca7b6f
49 changed files with 6472 additions and 542 deletions
14
.env.example
14
.env.example
|
@ -6,15 +6,16 @@ PORT=5000
|
|||
|
||||
DEBUG=false
|
||||
|
||||
# User IDs seperated by comma
|
||||
# Allow users and admins by user IDs (comma separated list)
|
||||
LNBITS_ALLOWED_USERS=""
|
||||
LNBITS_ADMIN_USERS=""
|
||||
# Extensions only admin can access
|
||||
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
|
||||
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
|
||||
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available
|
||||
LNBITS_ADMIN_UI=false
|
||||
LNBITS_ADMIN_UI=false
|
||||
|
||||
# Restricts access, User IDs seperated by comma
|
||||
LNBITS_ALLOWED_USERS=""
|
||||
LNBITS_ALLOWED_USERS=""
|
||||
|
||||
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
||||
|
||||
|
@ -108,3 +109,8 @@ ECLAIR_PASS=eclairpw
|
|||
# Enter /api in LightningTipBot to get your key
|
||||
LNTIPS_API_KEY=LNTIPS_ADMIN_KEY
|
||||
LNTIPS_API_ENDPOINT=https://ln.tips
|
||||
|
||||
# Cashu Mint
|
||||
# Use a long-enough random (!) private key.
|
||||
# Once set, you cannot change this key as for now.
|
||||
CASHU_PRIVATE_KEY="SuperSecretPrivateKey"
|
||||
|
|
|
@ -69,8 +69,7 @@ async def migrate_databases():
|
|||
(db_name, version, version),
|
||||
)
|
||||
|
||||
async def run_migration(db, migrations_module):
|
||||
db_name = migrations_module.__name__.split(".")[-2]
|
||||
async def run_migration(db, migrations_module, db_name):
|
||||
for key, migrate in migrations_module.__dict__.items():
|
||||
match = match = matcher.match(key)
|
||||
if match:
|
||||
|
@ -101,20 +100,24 @@ async def migrate_databases():
|
|||
rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall()
|
||||
current_versions = {row["db"]: row["version"] for row in rows}
|
||||
matcher = re.compile(r"^m(\d\d\d)_")
|
||||
await run_migration(conn, core_migrations)
|
||||
db_name = core_migrations.__name__.split(".")[-2]
|
||||
await run_migration(conn, core_migrations, db_name)
|
||||
|
||||
for ext in get_valid_extensions():
|
||||
try:
|
||||
ext_migrations = importlib.import_module(
|
||||
f"lnbits.extensions.{ext.code}.migrations"
|
||||
|
||||
module_str = (
|
||||
ext.migration_module or f"lnbits.extensions.{ext.code}.migrations"
|
||||
)
|
||||
ext_migrations = importlib.import_module(module_str)
|
||||
ext_db = importlib.import_module(f"lnbits.extensions.{ext.code}").db
|
||||
db_name = ext.db_name or module_str.split(".")[-2]
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
f"Please make sure that the extension `{ext.code}` has a migrations file."
|
||||
)
|
||||
|
||||
async with ext_db.connect() as ext_conn:
|
||||
await run_migration(ext_conn, ext_migrations)
|
||||
await run_migration(ext_conn, ext_migrations, db_name)
|
||||
|
||||
logger.info("✔️ All migrations done.")
|
||||
|
|
|
@ -12,7 +12,7 @@ from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
|||
import async_timeout
|
||||
import httpx
|
||||
import pyqrcode
|
||||
from fastapi import Depends, Header, Query, Request
|
||||
from fastapi import Depends, Header, Query, Request, Response
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.params import Body
|
||||
from loguru import logger
|
||||
|
@ -584,8 +584,8 @@ class DecodePayment(BaseModel):
|
|||
data: str
|
||||
|
||||
|
||||
@core_app.post("/api/v1/payments/decode")
|
||||
async def api_payments_decode(data: DecodePayment):
|
||||
@core_app.post("/api/v1/payments/decode", status_code=HTTPStatus.OK)
|
||||
async def api_payments_decode(data: DecodePayment, response: Response):
|
||||
payment_str = data.data
|
||||
try:
|
||||
if payment_str[:5] == "LNURL":
|
||||
|
@ -606,6 +606,7 @@ async def api_payments_decode(data: DecodePayment):
|
|||
"min_final_cltv_expiry": invoice.min_final_cltv_expiry,
|
||||
}
|
||||
except:
|
||||
response.status_code = HTTPStatus.BAD_REQUEST
|
||||
return {"message": "Failed to decode"}
|
||||
|
||||
|
||||
|
|
29
lnbits/db.py
29
lnbits/db.py
|
@ -1,6 +1,7 @@
|
|||
import asyncio
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
|
@ -73,18 +74,40 @@ class Connection(Compat):
|
|||
query = query.replace("?", "%s")
|
||||
return query
|
||||
|
||||
def rewrite_values(self, values):
|
||||
# strip html
|
||||
CLEANR = re.compile("<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});")
|
||||
|
||||
def cleanhtml(raw_html):
|
||||
if isinstance(raw_html, str):
|
||||
cleantext = re.sub(CLEANR, "", raw_html)
|
||||
return cleantext
|
||||
else:
|
||||
return raw_html
|
||||
|
||||
# tuple to list and back to tuple
|
||||
value_list = [values] if isinstance(values, str) else list(values)
|
||||
values = tuple([cleanhtml(l) for l in value_list])
|
||||
return values
|
||||
|
||||
async def fetchall(self, query: str, values: tuple = ()) -> list:
|
||||
result = await self.conn.execute(self.rewrite_query(query), values)
|
||||
result = await self.conn.execute(
|
||||
self.rewrite_query(query), self.rewrite_values(values)
|
||||
)
|
||||
return await result.fetchall()
|
||||
|
||||
async def fetchone(self, query: str, values: tuple = ()):
|
||||
result = await self.conn.execute(self.rewrite_query(query), values)
|
||||
result = await self.conn.execute(
|
||||
self.rewrite_query(query), self.rewrite_values(values)
|
||||
)
|
||||
row = await result.fetchone()
|
||||
await result.close()
|
||||
return row
|
||||
|
||||
async def execute(self, query: str, values: tuple = ()):
|
||||
return await self.conn.execute(self.rewrite_query(query), values)
|
||||
return await self.conn.execute(
|
||||
self.rewrite_query(query), self.rewrite_values(values)
|
||||
)
|
||||
|
||||
|
||||
class Database(Compat):
|
||||
|
|
11
lnbits/extensions/cashu/README.md
Normal file
11
lnbits/extensions/cashu/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Cashu
|
||||
|
||||
## Create ecash mint for pegging in/out of ecash
|
||||
|
||||
|
||||
|
||||
### Usage
|
||||
|
||||
1. Enable extension
|
||||
2. Create a Mint
|
||||
3. Share wallet
|
48
lnbits/extensions/cashu/__init__.py
Normal file
48
lnbits/extensions/cashu/__init__.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
import asyncio
|
||||
|
||||
from environs import Env # type: ignore
|
||||
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_cashu")
|
||||
|
||||
import sys
|
||||
|
||||
cashu_static_files = [
|
||||
{
|
||||
"path": "/cashu/static",
|
||||
"app": StaticFiles(directory="lnbits/extensions/cashu/static"),
|
||||
"name": "cashu_static",
|
||||
}
|
||||
]
|
||||
from cashu.mint.ledger import Ledger
|
||||
|
||||
env = Env()
|
||||
env.read_env()
|
||||
|
||||
ledger = Ledger(
|
||||
db=db,
|
||||
seed=env.str("CASHU_PRIVATE_KEY", default="SuperSecretPrivateKey"),
|
||||
derivation_path="0/0/0/1",
|
||||
)
|
||||
|
||||
cashu_ext: APIRouter = APIRouter(prefix="/cashu", tags=["cashu"])
|
||||
|
||||
|
||||
def cashu_renderer():
|
||||
return template_renderer(["lnbits/extensions/cashu/templates"])
|
||||
|
||||
|
||||
from .tasks import startup_cashu_mint, wait_for_paid_invoices
|
||||
from .views import * # noqa
|
||||
from .views_api import * # noqa
|
||||
|
||||
|
||||
def cashu_start():
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(catch_everything_and_restart(startup_cashu_mint))
|
||||
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
7
lnbits/extensions/cashu/config.json
Normal file
7
lnbits/extensions/cashu/config.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "Cashu",
|
||||
"short_description": "Ecash mint and wallet",
|
||||
"icon": "account_balance",
|
||||
"contributors": ["calle", "vlad", "arcbtc"],
|
||||
"hidden": false
|
||||
}
|
63
lnbits/extensions/cashu/crud.py
Normal file
63
lnbits/extensions/cashu/crud.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
import os
|
||||
import random
|
||||
import time
|
||||
from binascii import hexlify, unhexlify
|
||||
from typing import Any, List, Optional, Union
|
||||
|
||||
from cashu.core.base import MintKeyset
|
||||
from embit import bip32, bip39, ec, script
|
||||
from embit.networks import NETWORKS
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.db import Connection, Database
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from . import db
|
||||
from .models import Cashu, Pegs, Promises, Proof
|
||||
|
||||
|
||||
async def create_cashu(
|
||||
cashu_id: str, keyset_id: str, wallet_id: str, data: Cashu
|
||||
) -> Cashu:
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO cashu.cashu (id, wallet, name, tickershort, fraction, maxsats, coins, keyset_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
cashu_id,
|
||||
wallet_id,
|
||||
data.name,
|
||||
data.tickershort,
|
||||
data.fraction,
|
||||
data.maxsats,
|
||||
data.coins,
|
||||
keyset_id,
|
||||
),
|
||||
)
|
||||
|
||||
cashu = await get_cashu(cashu_id)
|
||||
assert cashu, "Newly created cashu couldn't be retrieved"
|
||||
return cashu
|
||||
|
||||
|
||||
async def get_cashu(cashu_id) -> Optional[Cashu]:
|
||||
row = await db.fetchone("SELECT * FROM cashu.cashu WHERE id = ?", (cashu_id,))
|
||||
return Cashu(**row) if row else None
|
||||
|
||||
|
||||
async def get_cashus(wallet_ids: Union[str, List[str]]) -> List[Cashu]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
q = ",".join(["?"] * len(wallet_ids))
|
||||
rows = await db.fetchall(
|
||||
f"SELECT * FROM cashu.cashu WHERE wallet IN ({q})", (*wallet_ids,)
|
||||
)
|
||||
|
||||
return [Cashu(**row) for row in rows]
|
||||
|
||||
|
||||
async def delete_cashu(cashu_id) -> None:
|
||||
await db.execute("DELETE FROM cashu.cashu WHERE id = ?", (cashu_id,))
|
33
lnbits/extensions/cashu/migrations.py
Normal file
33
lnbits/extensions/cashu/migrations.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
async def m001_initial(db):
|
||||
"""
|
||||
Initial cashu table.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE cashu.cashu (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
tickershort TEXT DEFAULT 'sats',
|
||||
fraction BOOL,
|
||||
maxsats INT,
|
||||
coins INT,
|
||||
keyset_id TEXT NOT NULL,
|
||||
issued_sat INT
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
"""
|
||||
Initial cashus table.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE cashu.pegs (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
inout BOOL NOT NULL,
|
||||
amount INT
|
||||
);
|
||||
"""
|
||||
)
|
147
lnbits/extensions/cashu/models.py
Normal file
147
lnbits/extensions/cashu/models.py
Normal file
|
@ -0,0 +1,147 @@
|
|||
from sqlite3 import Row
|
||||
from typing import List, Union
|
||||
|
||||
from fastapi import Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Cashu(BaseModel):
|
||||
id: str = Query(None)
|
||||
name: str = Query(None)
|
||||
wallet: str = Query(None)
|
||||
tickershort: str = Query(None)
|
||||
fraction: bool = Query(None)
|
||||
maxsats: int = Query(0)
|
||||
coins: int = Query(0)
|
||||
keyset_id: str = Query(None)
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
return cls(**dict(row))
|
||||
|
||||
|
||||
class Pegs(BaseModel):
|
||||
id: str
|
||||
wallet: str
|
||||
inout: str
|
||||
amount: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
return cls(**dict(row))
|
||||
|
||||
|
||||
class PayLnurlWData(BaseModel):
|
||||
lnurl: str
|
||||
|
||||
|
||||
class Promises(BaseModel):
|
||||
id: str
|
||||
amount: int
|
||||
B_b: str
|
||||
C_b: str
|
||||
cashu_id: str
|
||||
|
||||
|
||||
class Proof(BaseModel):
|
||||
amount: int
|
||||
secret: str
|
||||
C: str
|
||||
reserved: bool = False # whether this proof is reserved for sending
|
||||
send_id: str = "" # unique ID of send attempt
|
||||
time_created: str = ""
|
||||
time_reserved: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
return cls(
|
||||
amount=row[0],
|
||||
C=row[1],
|
||||
secret=row[2],
|
||||
reserved=row[3] or False,
|
||||
send_id=row[4] or "",
|
||||
time_created=row[5] or "",
|
||||
time_reserved=row[6] or "",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict):
|
||||
assert "secret" in d, "no secret in proof"
|
||||
assert "amount" in d, "no amount in proof"
|
||||
return cls(
|
||||
amount=d.get("amount"),
|
||||
C=d.get("C"),
|
||||
secret=d.get("secret"),
|
||||
reserved=d.get("reserved") or False,
|
||||
send_id=d.get("send_id") or "",
|
||||
time_created=d.get("time_created") or "",
|
||||
time_reserved=d.get("time_reserved") or "",
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return dict(amount=self.amount, secret=self.secret, C=self.C)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.__getattribute__(key)
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
self.__setattr__(key, val)
|
||||
|
||||
|
||||
class Proofs(BaseModel):
|
||||
"""TODO: Use this model"""
|
||||
|
||||
proofs: List[Proof]
|
||||
|
||||
|
||||
class Invoice(BaseModel):
|
||||
amount: int
|
||||
pr: str
|
||||
hash: str
|
||||
issued: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row):
|
||||
return cls(
|
||||
amount=int(row[0]),
|
||||
pr=str(row[1]),
|
||||
hash=str(row[2]),
|
||||
issued=bool(row[3]),
|
||||
)
|
||||
|
||||
|
||||
class BlindedMessage(BaseModel):
|
||||
amount: int
|
||||
B_: str
|
||||
|
||||
|
||||
class BlindedSignature(BaseModel):
|
||||
amount: int
|
||||
C_: str
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict):
|
||||
return cls(
|
||||
amount=d["amount"],
|
||||
C_=d["C_"],
|
||||
)
|
||||
|
||||
|
||||
class MintPayloads(BaseModel):
|
||||
blinded_messages: List[BlindedMessage] = []
|
||||
|
||||
|
||||
class SplitPayload(BaseModel):
|
||||
proofs: List[Proof]
|
||||
amount: int
|
||||
output_data: MintPayloads
|
||||
|
||||
|
||||
class CheckPayload(BaseModel):
|
||||
proofs: List[Proof]
|
||||
|
||||
|
||||
class MeltPayload(BaseModel):
|
||||
proofs: List[Proof]
|
||||
amount: int
|
||||
invoice: str
|
37
lnbits/extensions/cashu/static/js/base64.js
Normal file
37
lnbits/extensions/cashu/static/js/base64.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
function unescapeBase64Url(str) {
|
||||
return (str + '==='.slice((str.length + 3) % 4))
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/')
|
||||
}
|
||||
|
||||
function escapeBase64Url(str) {
|
||||
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
|
||||
}
|
||||
|
||||
const uint8ToBase64 = (function (exports) {
|
||||
'use strict'
|
||||
|
||||
var fromCharCode = String.fromCharCode
|
||||
var encode = function encode(uint8array) {
|
||||
var output = []
|
||||
|
||||
for (var i = 0, length = uint8array.length; i < length; i++) {
|
||||
output.push(fromCharCode(uint8array[i]))
|
||||
}
|
||||
|
||||
return btoa(output.join(''))
|
||||
}
|
||||
|
||||
var asCharCode = function asCharCode(c) {
|
||||
return c.charCodeAt(0)
|
||||
}
|
||||
|
||||
var decode = function decode(chars) {
|
||||
return Uint8Array.from(atob(chars), asCharCode)
|
||||
}
|
||||
|
||||
exports.decode = decode
|
||||
exports.encode = encode
|
||||
|
||||
return exports
|
||||
})({})
|
39
lnbits/extensions/cashu/static/js/dhke.js
Normal file
39
lnbits/extensions/cashu/static/js/dhke.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
async function hashToCurve(secretMessage) {
|
||||
console.log(
|
||||
'### secretMessage',
|
||||
nobleSecp256k1.utils.bytesToHex(secretMessage)
|
||||
)
|
||||
let point
|
||||
while (!point) {
|
||||
const hash = await nobleSecp256k1.utils.sha256(secretMessage)
|
||||
const hashHex = nobleSecp256k1.utils.bytesToHex(hash)
|
||||
const pointX = '02' + hashHex
|
||||
console.log('### pointX', pointX)
|
||||
try {
|
||||
point = nobleSecp256k1.Point.fromHex(pointX)
|
||||
console.log('### point', point.toHex())
|
||||
} catch (error) {
|
||||
secretMessage = await nobleSecp256k1.utils.sha256(secretMessage)
|
||||
}
|
||||
}
|
||||
return point
|
||||
}
|
||||
|
||||
async function step1Alice(secretMessage) {
|
||||
// todo: document & validate `secretMessage` format
|
||||
secretMessage = uint8ToBase64.encode(secretMessage)
|
||||
secretMessage = new TextEncoder().encode(secretMessage)
|
||||
const Y = await hashToCurve(secretMessage)
|
||||
const rpk = nobleSecp256k1.utils.randomPrivateKey()
|
||||
const r = bytesToNumber(rpk)
|
||||
const P = nobleSecp256k1.Point.fromPrivateKey(r)
|
||||
const B_ = Y.add(P)
|
||||
return {B_: B_.toHex(true), r: nobleSecp256k1.utils.bytesToHex(rpk)}
|
||||
}
|
||||
|
||||
function step3Alice(C_, r, A) {
|
||||
// const rInt = BigInt(r)
|
||||
const rInt = bytesToNumber(r)
|
||||
const C = C_.subtract(A.multiply(rInt))
|
||||
return C
|
||||
}
|
1178
lnbits/extensions/cashu/static/js/noble-secp256k1.js
Normal file
1178
lnbits/extensions/cashu/static/js/noble-secp256k1.js
Normal file
File diff suppressed because it is too large
Load diff
23
lnbits/extensions/cashu/static/js/utils.js
Normal file
23
lnbits/extensions/cashu/static/js/utils.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
function splitAmount(value) {
|
||||
const chunks = []
|
||||
for (let i = 0; i < 32; i++) {
|
||||
const mask = 1 << i
|
||||
if ((value & mask) !== 0) chunks.push(Math.pow(2, i))
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
function bytesToNumber(bytes) {
|
||||
return hexToNumber(nobleSecp256k1.utils.bytesToHex(bytes))
|
||||
}
|
||||
|
||||
function bigIntStringify(key, value) {
|
||||
return typeof value === 'bigint' ? value.toString() : value
|
||||
}
|
||||
|
||||
function hexToNumber(hex) {
|
||||
if (typeof hex !== 'string') {
|
||||
throw new TypeError('hexToNumber: expected string, got ' + typeof hex)
|
||||
}
|
||||
return BigInt(`0x${hex}`)
|
||||
}
|
33
lnbits/extensions/cashu/tasks.py
Normal file
33
lnbits/extensions/cashu/tasks.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
import asyncio
|
||||
import json
|
||||
|
||||
from cashu.core.migrations import migrate_databases
|
||||
from cashu.mint import migrations
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from . import db, ledger
|
||||
from .crud import get_cashu
|
||||
|
||||
|
||||
async def startup_cashu_mint():
|
||||
await migrate_databases(db, migrations)
|
||||
await ledger.load_used_proofs()
|
||||
await ledger.init_keysets(autosave=False)
|
||||
pass
|
||||
|
||||
|
||||
async def wait_for_paid_invoices():
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue)
|
||||
|
||||
while True:
|
||||
payment = await invoice_queue.get()
|
||||
await on_invoice_paid(payment)
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if payment.extra and not payment.extra.get("tag") == "cashu":
|
||||
return
|
||||
return
|
80
lnbits/extensions/cashu/templates/cashu/_api_docs.html
Normal file
80
lnbits/extensions/cashu/templates/cashu/_api_docs.html
Normal file
|
@ -0,0 +1,80 @@
|
|||
<q-expansion-item
|
||||
group="extras"
|
||||
icon="swap_vertical_circle"
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-btn flat label="Swagger API" type="a" href="../docs#/cashu"></q-btn>
|
||||
<!-- <q-expansion-item group="api" dense expand-separator label="List TPoS">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-blue">GET</span> /cashu/api/v1/mints</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 200 OK (application/json)
|
||||
</h5>
|
||||
<code>[<cashu_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.base_url }}cashu/api/v1/mints -H "X-Api-Key:
|
||||
<invoice_key>"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="Create a TPoS">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-green">POST</span> /cashu/api/v1/mints</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code
|
||||
>{"name": <string>, "currency": <string*ie USD*>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
</h5>
|
||||
<code
|
||||
>{"currency": <string>, "id": <string>, "name":
|
||||
<string>, "wallet": <string>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.base_url }}cashu/api/v1/mints -d '{"name":
|
||||
<string>, "currency": <string>}' -H "Content-type:
|
||||
application/json" -H "X-Api-Key: <admin_key>"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="Delete a TPoS"
|
||||
class="q-pb-md"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-pink">DELETE</span>
|
||||
/cashu/api/v1/mints/<cashu_id></code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <admin_key>}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
|
||||
<code></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.base_url
|
||||
}}cashu/api/v1/mints/<cashu_id> -H "X-Api-Key:
|
||||
<admin_key>"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item> -->
|
||||
</q-expansion-item>
|
13
lnbits/extensions/cashu/templates/cashu/_cashu.html
Normal file
13
lnbits/extensions/cashu/templates/cashu/_cashu.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<q-expansion-item group="extras" icon="info" label="About">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<p>Create Cashu ecash mints and wallets.</p>
|
||||
<small
|
||||
>Created by
|
||||
<a href="https://github.com/arcbtc" target="_blank">arcbtc</a>,
|
||||
<a href="https://github.com/motorina0" target="_blank">vlad</a>,
|
||||
<a href="https://github.com/calle" target="_blank">calle</a>.</small
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
367
lnbits/extensions/cashu/templates/cashu/index.html
Normal file
367
lnbits/extensions/cashu/templates/cashu/index.html
Normal file
|
@ -0,0 +1,367 @@
|
|||
{% 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-8 col-lg-7 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<b>Cashu mint and wallet</b>
|
||||
<p></p>
|
||||
<p>
|
||||
Here you can create multiple cashu mints that you can share. Each mint
|
||||
can service many users but all ecash tokens of a mint are only valid
|
||||
inside that mint and not across different mints. To exchange funds
|
||||
between mints, use Lightning payments.
|
||||
</p>
|
||||
<b>Important</b>
|
||||
<p></p>
|
||||
<p>
|
||||
If you are the operator of this LNbits instance, make sure to set
|
||||
CASHU_PRIVATE_KEY="randomkey" in your configuration file. Do not
|
||||
create mints before setting the key and do not change the key once
|
||||
set.
|
||||
</p>
|
||||
</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">Mints</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="cashus"
|
||||
row-key="id"
|
||||
:columns="cashusTable.columns"
|
||||
:pagination.sync="cashusTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="account_balance_wallet"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="'wallet/?' + 'mint_id=' + props.row.id"
|
||||
target="_blank"
|
||||
><q-tooltip>Shareable wallet</q-tooltip></q-btn
|
||||
>
|
||||
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="xs"
|
||||
icon="account_balance"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="'mint/' + props.row.id"
|
||||
target="_blank"
|
||||
><q-tooltip>Shareable mint page</q-tooltip></q-btn
|
||||
>
|
||||
</q-td>
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ (col.name == 'tip_options' && col.value ?
|
||||
JSON.parse(col.value).join(", ") : col.value) }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteMint(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
{% endraw %}
|
||||
</q-table>
|
||||
<q-btn
|
||||
class="q-pt-l"
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="formDialog.show = true"
|
||||
>New Mint</q-btn
|
||||
>
|
||||
</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}} Cashu extension</h6>
|
||||
</q-card-section>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list>
|
||||
{% include "cashu/_api_docs.html" %}
|
||||
<q-separator></q-separator>
|
||||
{% include "cashu/_cashu.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" style="width: 500px">
|
||||
<q-form @submit="createMint" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.name"
|
||||
label="Mint Name"
|
||||
placeholder="Cashu Mint"
|
||||
></q-input>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialog.data.wallet"
|
||||
:options="g.user.walletOptions"
|
||||
label="Cashu wallet *"
|
||||
></q-select>
|
||||
<!-- <q-toggle
|
||||
v-model="toggleAdvanced"
|
||||
label="Show advanced options"
|
||||
></q-toggle>
|
||||
<div v-show="toggleAdvanced">
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<q-checkbox
|
||||
v-model="formDialog.data.fraction"
|
||||
color="primary"
|
||||
label="sats/coins?"
|
||||
>
|
||||
<q-tooltip
|
||||
>Use with hedging extension to create a stablecoin!</q-tooltip
|
||||
>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<q-input
|
||||
v-if="!formDialog.data.fraction"
|
||||
filled
|
||||
dense
|
||||
type="number"
|
||||
v-model.trim="formDialog.data.cost"
|
||||
label="Sat coin cost (optional)"
|
||||
value="1"
|
||||
type="number"
|
||||
></q-input>
|
||||
<q-input
|
||||
v-if="!formDialog.data.fraction"
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialog.data.tickershort"
|
||||
label="Ticker shorthand"
|
||||
placeholder="sats"
|
||||
#
|
||||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<q-input
|
||||
class="q-mt-md"
|
||||
filled
|
||||
dense
|
||||
type="number"
|
||||
v-model.trim="formDialog.data.maxsats"
|
||||
label="Maximum mint liquidity (optional)"
|
||||
placeholder="∞"
|
||||
></q-input>
|
||||
<q-input
|
||||
class="q-mt-md"
|
||||
filled
|
||||
dense
|
||||
type="number"
|
||||
v-model.trim="formDialog.data.coins"
|
||||
label="Coins that 'exist' in mint (optional)"
|
||||
placeholder="∞"
|
||||
></q-input>
|
||||
</div> -->
|
||||
<div class="row q-mt-md">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="primary"
|
||||
:disable="formDialog.data.wallet == null || formDialog.data.name == null"
|
||||
type="submit"
|
||||
>Create Mint
|
||||
</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
var mapMint = function (obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
)
|
||||
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
|
||||
obj.cashu = ['/cashu/', obj.id].join('')
|
||||
return obj
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
cashus: [],
|
||||
hostname: location.protocol + '//' + location.host + '/cashu/mint/',
|
||||
toggleAdvanced: false,
|
||||
cashusTable: {
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'Mint ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Name', field: 'name'},
|
||||
// {
|
||||
// name: 'tickershort',
|
||||
// align: 'left',
|
||||
// label: 'Ticker',
|
||||
// field: 'tickershort'
|
||||
// },
|
||||
{
|
||||
name: 'wallet',
|
||||
align: 'left',
|
||||
label: 'Mint wallet',
|
||||
field: 'wallet'
|
||||
}
|
||||
// {
|
||||
// name: 'fraction',
|
||||
// align: 'left',
|
||||
// label: 'Using fraction',
|
||||
// field: 'fraction'
|
||||
// },
|
||||
// {
|
||||
// name: 'maxsats',
|
||||
// align: 'left',
|
||||
// label: 'Max Sats',
|
||||
// field: 'maxsats'
|
||||
// },
|
||||
// {
|
||||
// name: 'coins',
|
||||
// align: 'left',
|
||||
// label: 'No. of coins',
|
||||
// field: 'coins'
|
||||
// }
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {fraction: false}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeFormDialog: function () {
|
||||
this.formDialog.data = {}
|
||||
},
|
||||
getMints: function () {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/cashu/api/v1/mints?all_wallets=true',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.cashus = response.data.map(function (obj) {
|
||||
return mapMint(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
createMint: function () {
|
||||
if (this.formDialog.data.maxliquid == null) {
|
||||
this.formDialog.data.maxliquid = 0
|
||||
}
|
||||
var data = {
|
||||
name: this.formDialog.data.name,
|
||||
tickershort: this.formDialog.data.tickershort,
|
||||
maxliquid: this.formDialog.data.maxliquid
|
||||
}
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
'/cashu/api/v1/mints',
|
||||
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
|
||||
.inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
self.cashus.push(mapMint(response.data))
|
||||
self.formDialog.show = false
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteMint: function (cashuId) {
|
||||
var self = this
|
||||
var cashu = _.findWhere(this.cashus, {id: cashuId})
|
||||
console.log(cashu)
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
"Are you sure you want to delete this Mint? This mint's users will not be able to redeem their tokens!"
|
||||
)
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/cashu/api/v1/mints/' + cashuId,
|
||||
_.findWhere(self.g.user.wallets, {id: cashu.wallet}).adminkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.cashus = _.reject(self.cashus, function (obj) {
|
||||
return obj.id == cashuId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportCSV: function () {
|
||||
LNbits.utils.exportCSV(this.cashusTable.columns, this.cashus)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getMints()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
76
lnbits/extensions/cashu/templates/cashu/mint.html
Normal file
76
lnbits/extensions/cashu/templates/cashu/mint.html
Normal file
|
@ -0,0 +1,76 @@
|
|||
{% extends "public.html" %} {% 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-mb-xl">
|
||||
<q-card-section class="q-pa-none">
|
||||
<center>
|
||||
<q-icon
|
||||
name="account_balance"
|
||||
class="text-grey"
|
||||
style="font-size: 10rem"
|
||||
></q-icon>
|
||||
<h4 class="q-mt-none q-mb-md">{{ mint_name }}</h4>
|
||||
<a
|
||||
class="q-my-xl text-white"
|
||||
style="font-size: 1.5rem"
|
||||
href="../wallet?mint_id={{ mint_id }}"
|
||||
>Open wallet</a
|
||||
>
|
||||
</center>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-card class="q-pa-lg q-mb-xl">
|
||||
<q-card-section class="q-pa-none">
|
||||
<h5 class="q-my-md">Read the following carefully!</h5>
|
||||
<p>
|
||||
This is a
|
||||
<a href="https://cashu.space/" style="color: white" target="”_blank”"
|
||||
>Cashu</a
|
||||
>
|
||||
mint. Cashu is an ecash system for Bitcoin.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Open this page in your native browser</strong><br />
|
||||
Before you continue to the wallet, make sure to open this page in your
|
||||
device's native browser application (Safari for iOS, Chrome for
|
||||
Android). Do not use Cashu in an embedded browser that opens when you
|
||||
click a link in a messenger.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Add wallet to home screen</strong><br />
|
||||
You can add Cashu to your home screen as a progressive web app (PWA).
|
||||
After opening the wallet in your browser (click the link above), on
|
||||
Android (Chrome), click the menu at the upper right. On iOS (Safari),
|
||||
click the share button. Now press the Add to Home screen button.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Backup your wallet</strong><br />
|
||||
Ecash is a bearer asset. That means losing access to your wallet will
|
||||
make you lose your funds. The wallet stores ecash tokens on your
|
||||
device's database. If you lose the link or delete your your data
|
||||
without backing up, you will lose your tokens. Press the Backup button
|
||||
in the wallet to download a copy of your tokens.
|
||||
</p>
|
||||
<p>
|
||||
<strong>This service is in BETA</strong> <br />
|
||||
We hold no responsibility for people losing access to funds. Use at
|
||||
your own risk!
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
{% endblock %} {% block scripts %}
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
</div>
|
2337
lnbits/extensions/cashu/templates/cashu/wallet.html
Normal file
2337
lnbits/extensions/cashu/templates/cashu/wallet.html
Normal file
File diff suppressed because it is too large
Load diff
224
lnbits/extensions/cashu/views.py
Normal file
224
lnbits/extensions/cashu/views.py
Normal file
|
@ -0,0 +1,224 @@
|
|||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.params import Depends
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
|
||||
from . import cashu_ext, cashu_renderer
|
||||
from .crud import get_cashu
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@cashu_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(
|
||||
request: Request,
|
||||
user: User = Depends(check_user_exists), # type: ignore
|
||||
):
|
||||
return cashu_renderer().TemplateResponse(
|
||||
"cashu/index.html", {"request": request, "user": user.dict()}
|
||||
)
|
||||
|
||||
|
||||
@cashu_ext.get("/wallet")
|
||||
async def wallet(request: Request, mint_id: str):
|
||||
return cashu_renderer().TemplateResponse(
|
||||
"cashu/wallet.html",
|
||||
{
|
||||
"request": request,
|
||||
"web_manifest": f"/cashu/manifest/{mint_id}.webmanifest",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@cashu_ext.get("/mint/{mintID}")
|
||||
async def cashu(request: Request, mintID):
|
||||
cashu = await get_cashu(mintID)
|
||||
if not cashu:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist."
|
||||
)
|
||||
return cashu_renderer().TemplateResponse(
|
||||
"cashu/mint.html",
|
||||
{"request": request, "mint_name": cashu.name, "mint_id": mintID},
|
||||
)
|
||||
|
||||
|
||||
@cashu_ext.get("/manifest/{cashu_id}.webmanifest")
|
||||
async def manifest(cashu_id: str):
|
||||
cashu = await get_cashu(cashu_id)
|
||||
if not cashu:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist."
|
||||
)
|
||||
|
||||
return {
|
||||
"short_name": "Cashu",
|
||||
"name": "Cashu" + " - " + cashu.name,
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-512-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-96-96.png",
|
||||
"type": "image/png",
|
||||
"sizes": "96x96",
|
||||
},
|
||||
],
|
||||
"id": "/cashu/wallet?mint_id=" + cashu_id,
|
||||
"start_url": "/cashu/wallet?mint_id=" + cashu_id,
|
||||
"background_color": "#1F2234",
|
||||
"description": "Cashu ecash wallet",
|
||||
"display": "standalone",
|
||||
"scope": "/cashu/",
|
||||
"theme_color": "#1F2234",
|
||||
"protocol_handlers": [
|
||||
{"protocol": "cashu", "url": "&recv_token=%s"},
|
||||
{"protocol": "lightning", "url": "&lightning=%s"},
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Cashu" + " - " + cashu.name,
|
||||
"short_name": "Cashu",
|
||||
"description": "Cashu" + " - " + cashu.name,
|
||||
"url": "/cashu/wallet?mint_id=" + cashu_id,
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-512-512.png",
|
||||
"sizes": "512x512",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-192-192.png",
|
||||
"sizes": "192x192",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-144-144.png",
|
||||
"sizes": "144x144",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-96-96.png",
|
||||
"sizes": "96x96",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-72-72.png",
|
||||
"sizes": "72x72",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/android/android-launchericon-48-48.png",
|
||||
"sizes": "48x48",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/16.png",
|
||||
"sizes": "16x16",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/20.png",
|
||||
"sizes": "20x20",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/29.png",
|
||||
"sizes": "29x29",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/32.png",
|
||||
"sizes": "32x32",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/40.png",
|
||||
"sizes": "40x40",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/50.png",
|
||||
"sizes": "50x50",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/57.png",
|
||||
"sizes": "57x57",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/58.png",
|
||||
"sizes": "58x58",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/60.png",
|
||||
"sizes": "60x60",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/64.png",
|
||||
"sizes": "64x64",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/72.png",
|
||||
"sizes": "72x72",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/76.png",
|
||||
"sizes": "76x76",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/80.png",
|
||||
"sizes": "80x80",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/87.png",
|
||||
"sizes": "87x87",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/100.png",
|
||||
"sizes": "100x100",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/114.png",
|
||||
"sizes": "114x114",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/120.png",
|
||||
"sizes": "120x120",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/128.png",
|
||||
"sizes": "128x128",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/144.png",
|
||||
"sizes": "144x144",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/152.png",
|
||||
"sizes": "152x152",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/167.png",
|
||||
"sizes": "167x167",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/180.png",
|
||||
"sizes": "180x180",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/192.png",
|
||||
"sizes": "192x192",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/256.png",
|
||||
"sizes": "256x256",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/512.png",
|
||||
"sizes": "512x512",
|
||||
},
|
||||
{
|
||||
"src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/ios/1024.png",
|
||||
"sizes": "1024x1024",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
382
lnbits/extensions/cashu/views_api.py
Normal file
382
lnbits/extensions/cashu/views_api.py
Normal file
|
@ -0,0 +1,382 @@
|
|||
import json
|
||||
import math
|
||||
from http import HTTPStatus
|
||||
from typing import Dict, List, Union
|
||||
|
||||
import httpx
|
||||
|
||||
# -------- cashu imports
|
||||
from cashu.core.base import (
|
||||
BlindedSignature,
|
||||
CheckFeesRequest,
|
||||
CheckFeesResponse,
|
||||
CheckRequest,
|
||||
GetMeltResponse,
|
||||
GetMintResponse,
|
||||
Invoice,
|
||||
MeltRequest,
|
||||
MintRequest,
|
||||
PostSplitResponse,
|
||||
Proof,
|
||||
SplitRequest,
|
||||
)
|
||||
from fastapi import Query
|
||||
from fastapi.params import Depends
|
||||
from lnurl import decode as decode_lnurl
|
||||
from loguru import logger
|
||||
from secp256k1 import PublicKey
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.core.crud import check_internal, get_user
|
||||
from lnbits.core.services import (
|
||||
check_transaction_status,
|
||||
create_invoice,
|
||||
fee_reserve,
|
||||
pay_invoice,
|
||||
)
|
||||
from lnbits.core.views.api import api_payment
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
from lnbits.wallets.base import PaymentStatus
|
||||
|
||||
from . import cashu_ext, ledger
|
||||
from .crud import create_cashu, delete_cashu, get_cashu, get_cashus
|
||||
from .models import Cashu
|
||||
|
||||
# --------- extension imports
|
||||
|
||||
|
||||
LIGHTNING = True
|
||||
|
||||
########################################
|
||||
############### LNBITS MINTS ###########
|
||||
########################################
|
||||
|
||||
|
||||
@cashu_ext.get("/api/v1/mints", status_code=HTTPStatus.OK)
|
||||
async def api_cashus(
|
||||
all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore
|
||||
):
|
||||
"""
|
||||
Get all mints of this wallet.
|
||||
"""
|
||||
wallet_ids = [wallet.wallet.id]
|
||||
if all_wallets:
|
||||
user = await get_user(wallet.wallet.user)
|
||||
if user:
|
||||
wallet_ids = user.wallet_ids
|
||||
|
||||
return [cashu.dict() for cashu in await get_cashus(wallet_ids)]
|
||||
|
||||
|
||||
@cashu_ext.post("/api/v1/mints", status_code=HTTPStatus.CREATED)
|
||||
async def api_cashu_create(
|
||||
data: Cashu,
|
||||
wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore
|
||||
):
|
||||
"""
|
||||
Create a new mint for this wallet.
|
||||
"""
|
||||
cashu_id = urlsafe_short_hash()
|
||||
# generate a new keyset in cashu
|
||||
keyset = await ledger.load_keyset(cashu_id)
|
||||
|
||||
cashu = await create_cashu(
|
||||
cashu_id=cashu_id, keyset_id=keyset.id, wallet_id=wallet.wallet.id, data=data
|
||||
)
|
||||
logger.debug(cashu)
|
||||
return cashu.dict()
|
||||
|
||||
|
||||
@cashu_ext.delete("/api/v1/mints/{cashu_id}")
|
||||
async def api_cashu_delete(
|
||||
cashu_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) # type: ignore
|
||||
):
|
||||
"""
|
||||
Delete an existing cashu mint.
|
||||
"""
|
||||
cashu = await get_cashu(cashu_id)
|
||||
|
||||
if not cashu:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Cashu mint does not exist."
|
||||
)
|
||||
|
||||
if cashu.wallet != wallet.wallet.id:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="Not your Cashu mint."
|
||||
)
|
||||
|
||||
await delete_cashu(cashu_id)
|
||||
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)
|
||||
|
||||
|
||||
#######################################
|
||||
########### CASHU ENDPOINTS ###########
|
||||
#######################################
|
||||
|
||||
|
||||
@cashu_ext.get("/api/v1/{cashu_id}/keys", status_code=HTTPStatus.OK)
|
||||
async def keys(cashu_id: str = Query(None)) -> dict[int, str]:
|
||||
"""Get the public keys of the mint"""
|
||||
cashu: Union[Cashu, None] = await get_cashu(cashu_id)
|
||||
|
||||
if not cashu:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||
)
|
||||
|
||||
return ledger.get_keyset(keyset_id=cashu.keyset_id)
|
||||
|
||||
|
||||
@cashu_ext.get("/api/v1/{cashu_id}/keysets", status_code=HTTPStatus.OK)
|
||||
async def keysets(cashu_id: str = Query(None)) -> dict[str, list[str]]:
|
||||
"""Get the public keys of the mint"""
|
||||
cashu: Union[Cashu, None] = await get_cashu(cashu_id)
|
||||
|
||||
if not cashu:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||
)
|
||||
|
||||
return {"keysets": [cashu.keyset_id]}
|
||||
|
||||
|
||||
@cashu_ext.get("/api/v1/{cashu_id}/mint")
|
||||
async def request_mint(cashu_id: str = Query(None), amount: int = 0) -> GetMintResponse:
|
||||
"""
|
||||
Request minting of new tokens. The mint responds with a Lightning invoice.
|
||||
This endpoint can be used for a Lightning invoice UX flow.
|
||||
|
||||
Call `POST /mint` after paying the invoice.
|
||||
"""
|
||||
cashu: Union[Cashu, None] = await get_cashu(cashu_id)
|
||||
|
||||
if not cashu:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||
)
|
||||
|
||||
# create an invoice that the wallet needs to pay
|
||||
try:
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=cashu.wallet,
|
||||
amount=amount,
|
||||
memo=f"{cashu.name}",
|
||||
extra={"tag": "cashu"},
|
||||
)
|
||||
invoice = Invoice(
|
||||
amount=amount, pr=payment_request, hash=payment_hash, issued=False
|
||||
)
|
||||
# await store_lightning_invoice(cashu_id, invoice)
|
||||
await ledger.crud.store_lightning_invoice(invoice=invoice, db=ledger.db)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
|
||||
print(f"Lightning invoice: {payment_request}")
|
||||
resp = GetMintResponse(pr=payment_request, hash=payment_hash)
|
||||
# return {"pr": payment_request, "hash": payment_hash}
|
||||
return resp
|
||||
|
||||
|
||||
@cashu_ext.post("/api/v1/{cashu_id}/mint")
|
||||
async def mint_coins(
|
||||
data: MintRequest,
|
||||
cashu_id: str = Query(None),
|
||||
payment_hash: str = Query(None),
|
||||
) -> List[BlindedSignature]:
|
||||
"""
|
||||
Requests the minting of tokens belonging to a paid payment request.
|
||||
Call this endpoint after `GET /mint`.
|
||||
"""
|
||||
cashu: Union[Cashu, None] = await get_cashu(cashu_id)
|
||||
if cashu is None:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||
)
|
||||
|
||||
if LIGHTNING:
|
||||
invoice: Invoice = await ledger.crud.get_lightning_invoice(
|
||||
db=ledger.db, hash=payment_hash
|
||||
)
|
||||
if invoice is None:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Mint does not know this invoice.",
|
||||
)
|
||||
if invoice.issued == True:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
||||
detail="Tokens already issued for this invoice.",
|
||||
)
|
||||
|
||||
total_requested = sum([bm.amount for bm in data.blinded_messages])
|
||||
if total_requested > invoice.amount:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.PAYMENT_REQUIRED,
|
||||
detail=f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}",
|
||||
)
|
||||
|
||||
status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash)
|
||||
|
||||
if status.paid != True:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
|
||||
)
|
||||
try:
|
||||
keyset = ledger.keysets.keysets[cashu.keyset_id]
|
||||
|
||||
promises = await ledger._generate_promises(
|
||||
B_s=data.blinded_messages, keyset=keyset
|
||||
)
|
||||
assert len(promises), HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="No promises returned."
|
||||
)
|
||||
await ledger.crud.update_lightning_invoice(
|
||||
db=ledger.db, hash=payment_hash, issued=True
|
||||
)
|
||||
|
||||
return promises
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
|
||||
|
||||
@cashu_ext.post("/api/v1/{cashu_id}/melt")
|
||||
async def melt_coins(
|
||||
payload: MeltRequest, cashu_id: str = Query(None)
|
||||
) -> GetMeltResponse:
|
||||
"""Invalidates proofs and pays a Lightning invoice."""
|
||||
cashu: Union[None, Cashu] = await get_cashu(cashu_id)
|
||||
if cashu is None:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||
)
|
||||
proofs = payload.proofs
|
||||
invoice = payload.invoice
|
||||
|
||||
# !!!!!!! MAKE SURE THAT PROOFS ARE ONLY FROM THIS CASHU KEYSET ID
|
||||
# THIS IS NECESSARY BECAUSE THE CASHU BACKEND WILL ACCEPT ANY VALID
|
||||
# TOKENS
|
||||
assert all([p.id == cashu.keyset_id for p in proofs]), HTTPException(
|
||||
status_code=HTTPStatus.METHOD_NOT_ALLOWED,
|
||||
detail="Error: Tokens are from another mint.",
|
||||
)
|
||||
|
||||
assert all([ledger._verify_proof(p) for p in proofs]), HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="Could not verify proofs.",
|
||||
)
|
||||
|
||||
total_provided = sum([p["amount"] for p in proofs])
|
||||
invoice_obj = bolt11.decode(invoice)
|
||||
amount = math.ceil(invoice_obj.amount_msat / 1000)
|
||||
|
||||
internal_checking_id = await check_internal(invoice_obj.payment_hash)
|
||||
|
||||
if not internal_checking_id:
|
||||
fees_msat = fee_reserve(invoice_obj.amount_msat)
|
||||
else:
|
||||
fees_msat = 0
|
||||
assert total_provided >= amount + fees_msat / 1000, Exception(
|
||||
f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)."
|
||||
)
|
||||
|
||||
await pay_invoice(
|
||||
wallet_id=cashu.wallet,
|
||||
payment_request=invoice,
|
||||
description=f"pay cashu invoice",
|
||||
extra={"tag": "cashu", "cahsu_name": cashu.name},
|
||||
)
|
||||
|
||||
status: PaymentStatus = await check_transaction_status(
|
||||
cashu.wallet, invoice_obj.payment_hash
|
||||
)
|
||||
if status.paid == True:
|
||||
await ledger._invalidate_proofs(proofs)
|
||||
return GetMeltResponse(paid=status.paid, preimage=status.preimage)
|
||||
|
||||
|
||||
@cashu_ext.post("/api/v1/{cashu_id}/check")
|
||||
async def check_spendable(
|
||||
payload: CheckRequest, cashu_id: str = Query(None)
|
||||
) -> Dict[int, bool]:
|
||||
"""Check whether a secret has been spent already or not."""
|
||||
cashu: Union[None, Cashu] = await get_cashu(cashu_id)
|
||||
if cashu is None:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||
)
|
||||
return await ledger.check_spendable(payload.proofs)
|
||||
|
||||
|
||||
@cashu_ext.post("/api/v1/{cashu_id}/checkfees")
|
||||
async def check_fees(
|
||||
payload: CheckFeesRequest, cashu_id: str = Query(None)
|
||||
) -> CheckFeesResponse:
|
||||
"""
|
||||
Responds with the fees necessary to pay a Lightning invoice.
|
||||
Used by wallets for figuring out the fees they need to supply.
|
||||
This is can be useful for checking whether an invoice is internal (Cashu-to-Cashu).
|
||||
"""
|
||||
cashu: Union[None, Cashu] = await get_cashu(cashu_id)
|
||||
if cashu is None:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||
)
|
||||
invoice_obj = bolt11.decode(payload.pr)
|
||||
internal_checking_id = await check_internal(invoice_obj.payment_hash)
|
||||
|
||||
if not internal_checking_id:
|
||||
fees_msat = fee_reserve(invoice_obj.amount_msat)
|
||||
else:
|
||||
fees_msat = 0
|
||||
return CheckFeesResponse(fee=fees_msat / 1000)
|
||||
|
||||
|
||||
@cashu_ext.post("/api/v1/{cashu_id}/split")
|
||||
async def split(
|
||||
payload: SplitRequest, cashu_id: str = Query(None)
|
||||
) -> PostSplitResponse:
|
||||
"""
|
||||
Requetst a set of tokens with amount "total" to be split into two
|
||||
newly minted sets with amount "split" and "total-split".
|
||||
"""
|
||||
cashu: Union[None, Cashu] = await get_cashu(cashu_id)
|
||||
if cashu is None:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||
)
|
||||
proofs = payload.proofs
|
||||
|
||||
# !!!!!!! MAKE SURE THAT PROOFS ARE ONLY FROM THIS CASHU KEYSET ID
|
||||
# THIS IS NECESSARY BECAUSE THE CASHU BACKEND WILL ACCEPT ANY VALID
|
||||
# TOKENS
|
||||
if not all([p.id == cashu.keyset_id for p in proofs]):
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.METHOD_NOT_ALLOWED,
|
||||
detail="Error: Tokens are from another mint.",
|
||||
)
|
||||
|
||||
amount = payload.amount
|
||||
outputs = payload.outputs.blinded_messages
|
||||
assert outputs, Exception("no outputs provided.")
|
||||
split_return = None
|
||||
try:
|
||||
keyset = ledger.keysets.keysets[cashu.keyset_id]
|
||||
split_return = await ledger.split(proofs, amount, outputs, keyset)
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(exc),
|
||||
)
|
||||
if not split_return:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="there was an error with the split",
|
||||
)
|
||||
frst_promises, scnd_promises = split_return
|
||||
resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises)
|
||||
return resp
|
|
@ -487,6 +487,17 @@
|
|||
@click="copyText(lnurlValue, 'LNURL copied to clipboard!')"
|
||||
>Copy LNURL</q-btn
|
||||
>
|
||||
<q-chip
|
||||
v-if="websocketMessage == 'WebSocket NOT supported by your Browser!' || websocketMessage == 'Connection closed'"
|
||||
clickable
|
||||
color="red"
|
||||
text-color="white"
|
||||
icon="error"
|
||||
>{% raw %}{{ wsMessage }}{% endraw %}</q-chip
|
||||
>
|
||||
<q-chip v-else clickable color="green" text-color="white" icon="check"
|
||||
>{% raw %}{{ wsMessage }}{% endraw %}</q-chip
|
||||
>
|
||||
<br />
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn
|
||||
|
@ -534,6 +545,7 @@
|
|||
filter: '',
|
||||
currency: 'USD',
|
||||
lnurlValue: '',
|
||||
websocketMessage: '',
|
||||
switches: 0,
|
||||
lnurldeviceLinks: [],
|
||||
lnurldeviceLinksObj: [],
|
||||
|
@ -622,6 +634,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
wsMessage: function () {
|
||||
return this.websocketMessage
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openQrCodeDialog: function (lnurldevice_id) {
|
||||
var lnurldevice = _.findWhere(this.lnurldeviceLinks, {
|
||||
|
@ -631,11 +648,17 @@
|
|||
this.qrCodeDialog.data = _.clone(lnurldevice)
|
||||
this.qrCodeDialog.data.url =
|
||||
window.location.protocol + '//' + window.location.host
|
||||
this.lnurlValueFetch(this.qrCodeDialog.data.switches[0][3])
|
||||
this.lnurlValueFetch(
|
||||
this.qrCodeDialog.data.switches[0][3],
|
||||
this.qrCodeDialog.data.id
|
||||
)
|
||||
this.qrCodeDialog.show = true
|
||||
},
|
||||
lnurlValueFetch: function (lnurl) {
|
||||
lnurlValueFetch: function (lnurl, switchId) {
|
||||
this.lnurlValue = lnurl
|
||||
this.websocketConnector(
|
||||
'wss://' + window.location.host + '/lnurldevice/ws/' + switchId
|
||||
)
|
||||
},
|
||||
addSwitch: function () {
|
||||
var self = this
|
||||
|
@ -797,6 +820,25 @@
|
|||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
websocketConnector: function (websocketUrl) {
|
||||
if ('WebSocket' in window) {
|
||||
self = this
|
||||
var ws = new WebSocket(websocketUrl)
|
||||
self.updateWsMessage('Websocket connected')
|
||||
ws.onmessage = function (evt) {
|
||||
var received_msg = evt.data
|
||||
self.updateWsMessage('Message recieved: ' + received_msg)
|
||||
}
|
||||
ws.onclose = function () {
|
||||
self.updateWsMessage('Connection closed')
|
||||
}
|
||||
} else {
|
||||
self.updateWsMessage('WebSocket NOT supported by your Browser!')
|
||||
}
|
||||
},
|
||||
updateWsMessage: function (message) {
|
||||
this.websocketMessage = message
|
||||
},
|
||||
clearFormDialoglnurldevice() {
|
||||
this.formDialoglnurldevice.data = {
|
||||
lnurl_toggle: false,
|
||||
|
|
|
@ -8,7 +8,7 @@ async def m001_initial(db):
|
|||
id {db.serial_primary_key},
|
||||
wallet TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
amount {db.big_int} NOT NULL,
|
||||
served_meta INTEGER NOT NULL,
|
||||
served_pr INTEGER NOT NULL
|
||||
);
|
||||
|
|
|
@ -22,7 +22,7 @@ async def m001_initial(db):
|
|||
description TEXT NOT NULL,
|
||||
image TEXT, -- image/png;base64,...
|
||||
enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
price INTEGER NOT NULL,
|
||||
price {db.big_int} NOT NULL,
|
||||
unit TEXT NOT NULL DEFAULT 'sat'
|
||||
);
|
||||
"""
|
||||
|
|
|
@ -3,14 +3,14 @@ async def m001_initial(db):
|
|||
Initial paywalls table.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE paywall.paywalls (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
memo TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
amount {db.big_int} NOT NULL,
|
||||
time TIMESTAMP NOT NULL DEFAULT """
|
||||
+ db.timestamp_now
|
||||
+ """
|
||||
|
@ -25,14 +25,14 @@ async def m002_redux(db):
|
|||
"""
|
||||
await db.execute("ALTER TABLE paywall.paywalls RENAME TO paywalls_old")
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE paywall.paywalls (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
memo TEXT NOT NULL,
|
||||
description TEXT NULL,
|
||||
amount INTEGER DEFAULT 0,
|
||||
amount {db.big_int} DEFAULT 0,
|
||||
time TIMESTAMP NOT NULL DEFAULT """
|
||||
+ db.timestamp_now
|
||||
+ """,
|
||||
|
|
|
@ -3,14 +3,14 @@ async def m001_initial(db):
|
|||
Creates an improved satsdice table and migrates the existing data.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE satsdice.satsdice_pay (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT,
|
||||
title TEXT,
|
||||
min_bet INTEGER,
|
||||
max_bet INTEGER,
|
||||
amount INTEGER DEFAULT 0,
|
||||
amount {db.big_int} DEFAULT 0,
|
||||
served_meta INTEGER NOT NULL,
|
||||
served_pr INTEGER NOT NULL,
|
||||
multiplier FLOAT,
|
||||
|
@ -28,11 +28,11 @@ async def m002_initial(db):
|
|||
Creates an improved satsdice table and migrates the existing data.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE satsdice.satsdice_withdraw (
|
||||
id TEXT PRIMARY KEY,
|
||||
satsdice_pay TEXT,
|
||||
value INTEGER DEFAULT 1,
|
||||
value {db.big_int} DEFAULT 1,
|
||||
unique_hash TEXT UNIQUE,
|
||||
k1 TEXT,
|
||||
open_time INTEGER,
|
||||
|
@ -47,11 +47,11 @@ async def m003_initial(db):
|
|||
Creates an improved satsdice table and migrates the existing data.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE satsdice.satsdice_payment (
|
||||
payment_hash TEXT PRIMARY KEY,
|
||||
satsdice_pay TEXT,
|
||||
value INTEGER,
|
||||
value {db.big_int},
|
||||
paid BOOL DEFAULT FALSE,
|
||||
lost BOOL DEFAULT FALSE
|
||||
);
|
||||
|
|
|
@ -2,7 +2,5 @@
|
|||
"name": "SatsPay Server",
|
||||
"short_description": "Create onchain and LN charges",
|
||||
"icon": "payment",
|
||||
"contributors": [
|
||||
"arcbtc"
|
||||
]
|
||||
"contributors": ["arcbtc"]
|
||||
}
|
||||
|
|
|
@ -1,23 +1,28 @@
|
|||
import json
|
||||
from typing import List, Optional
|
||||
|
||||
import httpx
|
||||
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
|
||||
|
||||
# from lnbits.db import open_ext_db
|
||||
from . import db
|
||||
from .models import Charges, CreateCharge
|
||||
from .helpers import fetch_onchain_balance
|
||||
from .models import Charges, CreateCharge, SatsPayThemes
|
||||
|
||||
###############CHARGES##########################
|
||||
|
||||
|
||||
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)
|
||||
data.extra = json.dumps(
|
||||
{"mempool_endpoint": config.mempool_endpoint, "network": config.network}
|
||||
)
|
||||
onchain = await get_fresh_address(data.onchainwallet)
|
||||
onchainaddress = onchain.address
|
||||
else:
|
||||
|
@ -48,9 +53,11 @@ async def create_charge(user: str, data: CreateCharge) -> Charges:
|
|||
completelinktext,
|
||||
time,
|
||||
amount,
|
||||
balance
|
||||
balance,
|
||||
extra,
|
||||
custom_css
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
charge_id,
|
||||
|
@ -67,6 +74,8 @@ async def create_charge(user: str, data: CreateCharge) -> Charges:
|
|||
data.time,
|
||||
data.amount,
|
||||
0,
|
||||
data.extra,
|
||||
data.custom_css,
|
||||
),
|
||||
)
|
||||
return await get_charge(charge_id)
|
||||
|
@ -98,34 +107,118 @@ 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) -> List[Charges]:
|
||||
async def check_address_balance(charge_id: str) -> Optional[Charges]:
|
||||
charge = await get_charge(charge_id)
|
||||
|
||||
if not charge.paid:
|
||||
if charge.onchainaddress:
|
||||
config = await get_charge_config(charge_id)
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(
|
||||
config.mempool_endpoint
|
||||
+ "/api/address/"
|
||||
+ charge.onchainaddress
|
||||
)
|
||||
respAmount = r.json()["chain_stats"]["funded_txo_sum"]
|
||||
if respAmount > charge.balance:
|
||||
await update_charge(charge_id=charge_id, balance=respAmount)
|
||||
except Exception:
|
||||
pass
|
||||
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)
|
||||
row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
|
||||
return Charges.from_row(row) if row else None
|
||||
return await get_charge(charge_id)
|
||||
|
||||
|
||||
async def get_charge_config(charge_id: str):
|
||||
row = await db.fetchone(
|
||||
"""SELECT "user" FROM satspay.charges WHERE id = ?""", (charge_id,)
|
||||
################## SETTINGS ###################
|
||||
|
||||
|
||||
async def save_theme(data: SatsPayThemes, css_id: str = None):
|
||||
# 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) -> 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 "timestamp" DESC """,
|
||||
(user_id,),
|
||||
)
|
||||
return await get_config(row.user)
|
||||
|
||||
|
||||
################## SETTINGS ###################
|
||||
|
||||
|
||||
async def save_theme(data: SatsPayThemes, css_id: str = None):
|
||||
# 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) -> 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,8 +1,11 @@
|
|||
import httpx
|
||||
from loguru import logger
|
||||
|
||||
from .models import Charges
|
||||
|
||||
|
||||
def compact_charge(charge: Charges):
|
||||
return {
|
||||
def public_charge(charge: Charges):
|
||||
c = {
|
||||
"id": charge.id,
|
||||
"description": charge.description,
|
||||
"onchainaddress": charge.onchainaddress,
|
||||
|
@ -13,5 +16,40 @@ def compact_charge(charge: Charges):
|
|||
"balance": charge.balance,
|
||||
"paid": charge.paid,
|
||||
"timestamp": charge.timestamp,
|
||||
"completelink": charge.completelink, # should be secret?
|
||||
"time_elapsed": charge.time_elapsed,
|
||||
"time_left": charge.time_left,
|
||||
"paid": charge.paid,
|
||||
"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:
|
||||
r = await client.post(
|
||||
charge.webhook,
|
||||
json=public_charge(charge),
|
||||
timeout=40,
|
||||
)
|
||||
return {"webhook_success": r.is_success, "webhook_message": r.reason_phrase}
|
||||
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
|
||||
)
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.get(endpoint + "/api/address/" + charge.onchainaddress)
|
||||
return r.json()["chain_stats"]["funded_txo_sum"]
|
||||
|
|
|
@ -4,7 +4,7 @@ async def m001_initial(db):
|
|||
"""
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE satspay.charges (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
"user" TEXT,
|
||||
|
@ -18,11 +18,47 @@ async def m001_initial(db):
|
|||
completelink TEXT,
|
||||
completelinktext TEXT,
|
||||
time INTEGER,
|
||||
amount INTEGER,
|
||||
balance INTEGER DEFAULT 0,
|
||||
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,3 +1,4 @@
|
|||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from sqlite3 import Row
|
||||
from typing import Optional
|
||||
|
@ -5,6 +6,10 @@ 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)
|
||||
|
@ -13,8 +18,17 @@ class CreateCharge(BaseModel):
|
|||
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):
|
||||
|
@ -28,6 +42,13 @@ class Charges(BaseModel):
|
|||
webhook: Optional[str]
|
||||
completelink: Optional[str]
|
||||
completelinktext: Optional[str] = "Back to Merchant"
|
||||
<<<<<<< HEAD
|
||||
extra: str = "{}"
|
||||
custom_css: Optional[str]
|
||||
=======
|
||||
custom_css: Optional[str]
|
||||
extra: str = DEFAULT_MEMPOOL_CONFIG
|
||||
>>>>>>> main
|
||||
time: int
|
||||
amount: int
|
||||
balance: int
|
||||
|
@ -54,3 +75,22 @@ class Charges(BaseModel):
|
|||
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 == 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))
|
||||
|
|
|
@ -14,18 +14,22 @@ const retryWithDelay = async function (fn, retryCount = 0) {
|
|||
}
|
||||
|
||||
const mapCharge = (obj, oldObj = {}) => {
|
||||
const charge = _.clone(obj)
|
||||
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.expanded = false
|
||||
charge.displayUrl = ['/satspay/', obj.id].join('')
|
||||
charge.expanded = oldObj.expanded
|
||||
charge.expanded = oldObj.expanded || false
|
||||
charge.pendingBalance = oldObj.pendingBalance || 0
|
||||
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,4 +1,5 @@
|
|||
import asyncio
|
||||
import json
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
@ -7,7 +8,8 @@ from lnbits.extensions.satspay.crud import check_address_balance, get_charge
|
|||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
# from .crud import get_ticket, set_ticket_paid
|
||||
from .crud import update_charge
|
||||
from .helpers import call_webhook
|
||||
|
||||
|
||||
async def wait_for_paid_invoices():
|
||||
|
@ -30,4 +32,9 @@ async def on_invoice_paid(payment: Payment) -> None:
|
|||
return
|
||||
|
||||
await payment.set_pending(False)
|
||||
await check_address_balance(charge_id=charge.id)
|
||||
charge = await check_address_balance(charge_id=charge.id)
|
||||
|
||||
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))
|
||||
|
|
|
@ -5,7 +5,13 @@
|
|||
WatchOnly extension, we highly reccomend using a fresh extended public Key
|
||||
specifically for SatsPayServer!<br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a></small
|
||||
Created by, <a href="https://github.com/benarc">Ben Arc</a>,
|
||||
<a
|
||||
target="_blank"
|
||||
style="color: unset"
|
||||
href="https://github.com/motorina0"
|
||||
>motorina0</a
|
||||
></small
|
||||
>
|
||||
</p>
|
||||
<br />
|
||||
|
|
|
@ -109,7 +109,7 @@
|
|||
<q-btn
|
||||
flat
|
||||
disable
|
||||
v-if="!charge.lnbitswallet || charge.time_elapsed"
|
||||
v-if="!charge.payment_request || charge.time_elapsed"
|
||||
style="color: primary; width: 100%"
|
||||
label="lightning⚡"
|
||||
>
|
||||
|
@ -131,7 +131,7 @@
|
|||
<q-btn
|
||||
flat
|
||||
disable
|
||||
v-if="!charge.onchainwallet || charge.time_elapsed"
|
||||
v-if="!charge.onchainaddress || charge.time_elapsed"
|
||||
style="color: primary; width: 100%"
|
||||
label="onchain⛓️"
|
||||
>
|
||||
|
@ -170,13 +170,17 @@
|
|||
name="check"
|
||||
style="color: green; font-size: 21.4em"
|
||||
></q-icon>
|
||||
<q-btn
|
||||
outline
|
||||
v-if="charge.webhook"
|
||||
type="a"
|
||||
:href="charge.completelink"
|
||||
:label="charge.completelinktext"
|
||||
></q-btn>
|
||||
<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">
|
||||
|
@ -218,7 +222,7 @@
|
|||
<div class="col text-center">
|
||||
<a
|
||||
style="color: unset"
|
||||
:href="mempool_endpoint + '/address/' + charge.onchainaddress"
|
||||
:href="'https://' + mempoolHostname + '/address/' + charge.onchainaddress"
|
||||
target="_blank"
|
||||
><span
|
||||
class="text-subtitle1"
|
||||
|
@ -241,13 +245,17 @@
|
|||
name="check"
|
||||
style="color: green; font-size: 21.4em"
|
||||
></q-icon>
|
||||
<q-btn
|
||||
outline
|
||||
v-if="charge.webhook"
|
||||
type="a"
|
||||
:href="charge.completelink"
|
||||
:label="charge.completelinktext"
|
||||
></q-btn>
|
||||
<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">
|
||||
|
@ -289,7 +297,17 @@
|
|||
</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>
|
||||
|
@ -303,7 +321,8 @@
|
|||
data() {
|
||||
return {
|
||||
charge: JSON.parse('{{charge_data | tojson}}'),
|
||||
mempool_endpoint: '{{mempool_endpoint}}',
|
||||
mempoolEndpoint: '{{mempool_endpoint}}',
|
||||
network: '{{network}}',
|
||||
pendingFunds: 0,
|
||||
ws: null,
|
||||
newProgress: 0.4,
|
||||
|
@ -316,19 +335,19 @@
|
|||
cancelListener: () => {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
mempoolHostname: function () {
|
||||
let hostname = new URL(this.mempoolEndpoint).hostname
|
||||
if (this.network === 'Testnet') {
|
||||
hostname += '/testnet'
|
||||
}
|
||||
return hostname
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
startPaymentNotifier() {
|
||||
this.cancelListener()
|
||||
if (!this.lnbitswallet) return
|
||||
this.cancelListener = LNbits.events.onInvoicePaid(
|
||||
this.wallet,
|
||||
payment => {
|
||||
this.checkInvoiceBalance()
|
||||
}
|
||||
)
|
||||
},
|
||||
checkBalances: async function () {
|
||||
if (this.charge.hasStaleBalance) return
|
||||
if (!this.charge.payment_request && this.charge.hasOnchainStaleBalance)
|
||||
return
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
|
@ -345,7 +364,7 @@
|
|||
const {
|
||||
bitcoin: {addresses: addressesAPI}
|
||||
} = mempoolJS({
|
||||
hostname: new URL(this.mempool_endpoint).hostname
|
||||
hostname: new URL(this.mempoolEndpoint).hostname
|
||||
})
|
||||
|
||||
try {
|
||||
|
@ -353,7 +372,8 @@
|
|||
address: this.charge.onchainaddress
|
||||
})
|
||||
const newBalance = utxos.reduce((t, u) => t + u.value, 0)
|
||||
this.charge.hasStaleBalance = this.charge.balance === newBalance
|
||||
this.charge.hasOnchainStaleBalance =
|
||||
this.charge.balance === newBalance
|
||||
|
||||
this.pendingFunds = utxos
|
||||
.filter(u => !u.status.confirmed)
|
||||
|
@ -388,10 +408,10 @@
|
|||
const {
|
||||
bitcoin: {websocket}
|
||||
} = mempoolJS({
|
||||
hostname: new URL(this.mempool_endpoint).hostname
|
||||
hostname: new URL(this.mempoolEndpoint).hostname
|
||||
})
|
||||
|
||||
this.ws = new WebSocket('wss://mempool.space/api/v1/ws')
|
||||
this.ws = new WebSocket(`wss://${this.mempoolHostname}/api/v1/ws`)
|
||||
this.ws.addEventListener('open', x => {
|
||||
if (this.charge.onchainaddress) {
|
||||
this.trackAddress(this.charge.onchainaddress)
|
||||
|
@ -428,13 +448,14 @@
|
|||
}
|
||||
},
|
||||
created: async function () {
|
||||
if (this.charge.lnbitswallet) this.payInvoice()
|
||||
// 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()
|
||||
|
||||
// empty for onchain
|
||||
this.wallet.inkey = '{{ wallet_inkey }}'
|
||||
this.startPaymentNotifier()
|
||||
await this.checkBalances()
|
||||
|
||||
if (!this.charge.paid) {
|
||||
this.loopRefresh()
|
||||
|
|
|
@ -8,6 +8,26 @@
|
|||
<q-btn unelevated color="primary" @click="formDialogCharge.show = true"
|
||||
>New charge
|
||||
</q-btn>
|
||||
|
||||
<q-btn
|
||||
v-if="admin == 'True'"
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="getThemes();formDialogThemes.show = true"
|
||||
>New CSS Theme
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-else
|
||||
disable
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="getThemes();formDialogThemes.show = true"
|
||||
>New CSS Theme
|
||||
<q-tooltip
|
||||
>For security reason, custom css is only available to server
|
||||
admins.</q-tooltip
|
||||
></q-btn
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
|
@ -203,9 +223,14 @@
|
|||
:href="props.row.webhook"
|
||||
target="_blank"
|
||||
style="color: unset; text-decoration: none"
|
||||
>{{props.row.webhook || props.row.webhook}}</a
|
||||
>{{props.row.webhook}}</a
|
||||
>
|
||||
</div>
|
||||
<div class="col-4 q-pr-lg">
|
||||
<q-badge v-if="props.row.webhook_message" color="blue">
|
||||
{{props.row.webhook_message }}
|
||||
</q-badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row items-center q-mt-md q-mb-lg">
|
||||
<div class="col-2 q-pr-lg">ID:</div>
|
||||
|
@ -254,6 +279,63 @@
|
|||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card v-if="admin == 'True'">
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">Themes</h5>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:data="themeLinks"
|
||||
row-key="id"
|
||||
:columns="customCSSTable.columns"
|
||||
:pagination.sync="customCSSTable.pagination"
|
||||
>
|
||||
{% raw %}
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
<q-th auto-width></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.value }}
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="updateformDialog(props.row.css_id)"
|
||||
icon="edit"
|
||||
color="light-blue"
|
||||
></q-btn>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteTheme(props.row.css_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">
|
||||
|
@ -298,32 +380,6 @@
|
|||
>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.webhook"
|
||||
type="url"
|
||||
label="Webhook (URL to send transaction data to once paid)"
|
||||
>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.completelink"
|
||||
type="url"
|
||||
label="Completed button URL"
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.completelinktext"
|
||||
type="text"
|
||||
label="Completed button text (ie 'Back to merchant')"
|
||||
>
|
||||
</q-input>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div v-if="walletLinks.length > 0">
|
||||
|
@ -372,6 +428,52 @@
|
|||
label="Wallet *"
|
||||
>
|
||||
</q-select>
|
||||
<q-toggle
|
||||
v-model="showAdvanced"
|
||||
label="Show advanced options"
|
||||
></q-toggle>
|
||||
<div v-if="showAdvanced" class="row">
|
||||
<div class="col">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.webhook"
|
||||
type="url"
|
||||
label="Webhook (URL to send transaction data to once paid)"
|
||||
class="q-mt-lg"
|
||||
>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.completelink"
|
||||
type="url"
|
||||
label="Completed button URL"
|
||||
class="q-mt-lg"
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogCharge.data.completelinktext"
|
||||
type="text"
|
||||
label="Completed button text (ie 'Back to merchant')"
|
||||
class="q-mt-lg"
|
||||
>
|
||||
</q-input>
|
||||
<q-select
|
||||
filled
|
||||
dense
|
||||
emit-value
|
||||
v-model="formDialogCharge.data.custom_css"
|
||||
:options="themeOptions"
|
||||
label="Custom CSS theme (optional)"
|
||||
class="q-mt-lg"
|
||||
>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
|
@ -389,6 +491,43 @@
|
|||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="formDialogThemes.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form @submit="sendFormDataThemes" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogThemes.data.title"
|
||||
type="text"
|
||||
label="*Title"
|
||||
></q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="formDialogThemes.data.custom_css"
|
||||
type="textarea"
|
||||
label="Custom CSS"
|
||||
>
|
||||
</q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="formDialogThemes.data.css_id"
|
||||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
>Update CSS theme</q-btn
|
||||
>
|
||||
<q-btn v-else unelevated color="primary" type="submit"
|
||||
>Save CSS theme</q-btn
|
||||
>
|
||||
<q-btn @click="cancelThemes" flat color="grey" class="q-ml-auto"
|
||||
>Cancel</q-btn
|
||||
>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<!-- lnbits/static/vendor
|
||||
|
@ -405,15 +544,21 @@
|
|||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
settings: {},
|
||||
filter: '',
|
||||
admin: '{{ admin }}',
|
||||
balance: null,
|
||||
walletLinks: [],
|
||||
chargeLinks: [],
|
||||
themeLinks: [],
|
||||
themeOptions: [],
|
||||
onchainwallet: '',
|
||||
rescanning: false,
|
||||
mempool: {
|
||||
endpoint: ''
|
||||
endpoint: '',
|
||||
network: 'Mainnet'
|
||||
},
|
||||
showAdvanced: false,
|
||||
|
||||
chargesTable: {
|
||||
columns: [
|
||||
|
@ -488,7 +633,25 @@
|
|||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
|
||||
customCSSTable: {
|
||||
columns: [
|
||||
{
|
||||
name: 'css_id',
|
||||
align: 'left',
|
||||
label: 'ID',
|
||||
field: 'css_id'
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
align: 'left',
|
||||
label: 'Title',
|
||||
field: 'title'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
formDialogCharge: {
|
||||
show: false,
|
||||
data: {
|
||||
|
@ -496,20 +659,33 @@
|
|||
onchainwallet: '',
|
||||
lnbits: false,
|
||||
description: '',
|
||||
custom_css: '',
|
||||
time: null,
|
||||
amount: null
|
||||
}
|
||||
},
|
||||
formDialogThemes: {
|
||||
show: false,
|
||||
data: {
|
||||
custom_css: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancelThemes: function (data) {
|
||||
this.formDialogCharge.data.custom_css = ''
|
||||
this.formDialogThemes.show = false
|
||||
},
|
||||
cancelCharge: function (data) {
|
||||
this.formDialogCharge.data.description = ''
|
||||
this.formDialogCharge.data.onchain = false
|
||||
this.formDialogCharge.data.onchainwallet = ''
|
||||
this.formDialogCharge.data.lnbitswallet = ''
|
||||
this.formDialogCharge.data.time = null
|
||||
this.formDialogCharge.data.amount = null
|
||||
this.formDialogCharge.data.webhook = ''
|
||||
this.formDialogCharge.data.custom_css = ''
|
||||
this.formDialogCharge.data.completelink = ''
|
||||
this.formDialogCharge.show = false
|
||||
},
|
||||
|
@ -518,7 +694,7 @@
|
|||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/watchonly/api/v1/wallet',
|
||||
`/watchonly/api/v1/wallet?network=${this.mempool.network}`,
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
this.walletLinks = data.map(w => ({
|
||||
|
@ -538,6 +714,7 @@
|
|||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
this.mempool.endpoint = data.mempool_endpoint
|
||||
this.mempool.network = data.network || 'Mainnet'
|
||||
const url = new URL(this.mempool.endpoint)
|
||||
this.mempool.hostname = url.hostname
|
||||
} catch (error) {
|
||||
|
@ -572,12 +749,43 @@
|
|||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
sendFormDataCharge: function () {
|
||||
|
||||
getThemes: async function () {
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
'/satspay/api/v1/themes',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
console.log(data)
|
||||
this.themeLinks = data.map(c =>
|
||||
mapCSS(
|
||||
c,
|
||||
this.themeLinks.find(old => old.css_id === c.css_id)
|
||||
)
|
||||
)
|
||||
this.themeOptions = data.map(w => ({
|
||||
id: w.css_id,
|
||||
label: w.title + ' - ' + w.css_id
|
||||
}))
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
|
||||
sendFormDataThemes: function () {
|
||||
const wallet = this.g.user.wallets[0].inkey
|
||||
const data = this.formDialogThemes.data
|
||||
this.createTheme(wallet, data)
|
||||
},
|
||||
sendFormDataCharge: function () {
|
||||
this.formDialogCharge.data.custom_css = this.formDialogCharge.data.custom_css?.id
|
||||
const data = this.formDialogCharge.data
|
||||
const wallet = this.g.user.wallets[0].inkey
|
||||
data.amount = parseInt(data.amount)
|
||||
data.time = parseInt(data.time)
|
||||
data.onchainwallet = this.onchainwallet?.id
|
||||
data.lnbitswallet = data.lnbits ? data.lnbitswallet : null
|
||||
data.onchainwallet = data.onchain ? this.onchainwallet?.id : null
|
||||
this.createCharge(wallet, data)
|
||||
},
|
||||
refreshActiveChargesBalance: async function () {
|
||||
|
@ -642,6 +850,68 @@
|
|||
this.rescanning = false
|
||||
}
|
||||
},
|
||||
updateformDialog: function (themeId) {
|
||||
const theme = _.findWhere(this.themeLinks, {css_id: themeId})
|
||||
console.log(theme.css_id)
|
||||
this.formDialogThemes.data.css_id = theme.css_id
|
||||
this.formDialogThemes.data.title = theme.title
|
||||
this.formDialogThemes.data.custom_css = theme.custom_css
|
||||
this.formDialogThemes.show = true
|
||||
},
|
||||
createTheme: async function (wallet, data) {
|
||||
console.log(data.css_id)
|
||||
try {
|
||||
if (data.css_id) {
|
||||
const resp = await LNbits.api.request(
|
||||
'POST',
|
||||
'/satspay/api/v1/themes/' + data.css_id,
|
||||
wallet,
|
||||
data
|
||||
)
|
||||
this.themeLinks = _.reject(this.themeLinks, function (obj) {
|
||||
return obj.css_id === data.css_id
|
||||
})
|
||||
this.themeLinks.unshift(mapCSS(resp.data))
|
||||
} else {
|
||||
const resp = await LNbits.api.request(
|
||||
'POST',
|
||||
'/satspay/api/v1/themes',
|
||||
wallet,
|
||||
data
|
||||
)
|
||||
this.themeLinks.unshift(mapCSS(resp.data))
|
||||
}
|
||||
this.formDialogThemes.show = false
|
||||
this.formDialogThemes.data = {
|
||||
title: '',
|
||||
custom_css: ''
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('cun')
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
},
|
||||
|
||||
deleteTheme: function (themeId) {
|
||||
const theme = _.findWhere(this.themeLinks, {id: themeId})
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this theme?')
|
||||
.onOk(async () => {
|
||||
try {
|
||||
const response = await LNbits.api.request(
|
||||
'DELETE',
|
||||
'/satspay/api/v1/themes/' + themeId,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
|
||||
this.themeLinks = _.reject(this.themeLinks, function (obj) {
|
||||
return obj.css_id === themeId
|
||||
})
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
}
|
||||
})
|
||||
},
|
||||
createCharge: async function (wallet, data) {
|
||||
try {
|
||||
const resp = await LNbits.api.request(
|
||||
|
@ -694,9 +964,12 @@
|
|||
}
|
||||
},
|
||||
created: async function () {
|
||||
if (this.admin == 'True') {
|
||||
await this.getThemes()
|
||||
}
|
||||
await this.getCharges()
|
||||
await this.getWalletLinks()
|
||||
await this.getWalletConfig()
|
||||
await this.getWalletLinks()
|
||||
setInterval(() => this.refreshActiveChargesBalance(), 10 * 2000)
|
||||
await this.rescanOnchainAddresses()
|
||||
setInterval(() => this.rescanOnchainAddresses(), 10 * 1000)
|
||||
|
|
|
@ -1,25 +1,32 @@
|
|||
import json
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Response
|
||||
from fastapi.param_functions import Depends
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from loguru import logger
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits.core.crud import get_wallet
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
from lnbits.extensions.satspay.helpers import public_charge
|
||||
from lnbits.settings import settings
|
||||
|
||||
from . import satspay_ext, satspay_renderer
|
||||
from .crud import get_charge, get_charge_config
|
||||
from .crud import get_charge, get_theme
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@satspay_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
admin = False
|
||||
if settings.lnbits_admin_users and user.id in settings.lnbits_admin_users:
|
||||
admin = True
|
||||
return satspay_renderer().TemplateResponse(
|
||||
"satspay/index.html", {"request": request, "user": user.dict()}
|
||||
"satspay/index.html", {"request": request, "user": user.dict(), "admin": admin}
|
||||
)
|
||||
|
||||
|
||||
|
@ -30,18 +37,21 @@ async def display(request: Request, charge_id: str):
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
|
||||
)
|
||||
wallet = await get_wallet(charge.lnbitswallet)
|
||||
onchainwallet_config = await get_charge_config(charge_id)
|
||||
inkey = wallet.inkey if wallet else None
|
||||
mempool_endpoint = (
|
||||
onchainwallet_config.mempool_endpoint if onchainwallet_config else None
|
||||
)
|
||||
|
||||
return satspay_renderer().TemplateResponse(
|
||||
"satspay/display.html",
|
||||
{
|
||||
"request": request,
|
||||
"charge_data": charge.dict(),
|
||||
"wallet_inkey": inkey,
|
||||
"mempool_endpoint": mempool_endpoint,
|
||||
"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_id: str, response: Response):
|
||||
theme = await get_theme(css_id)
|
||||
if theme:
|
||||
return Response(content=theme.custom_css, media_type="text/css")
|
||||
return None
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import json
|
||||
from http import HTTPStatus
|
||||
|
||||
import httpx
|
||||
from fastapi.params import Depends
|
||||
from loguru import logger
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.crud import get_wallet
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
get_key_type,
|
||||
|
@ -11,17 +13,22 @@ from lnbits.decorators import (
|
|||
require_invoice_key,
|
||||
)
|
||||
from lnbits.extensions.satspay import satspay_ext
|
||||
from lnbits.settings import settings
|
||||
|
||||
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 compact_charge
|
||||
from .models import CreateCharge
|
||||
from .helpers import call_webhook, public_charge
|
||||
from .models import CreateCharge, SatsPayThemes
|
||||
|
||||
#############################CHARGES##########################
|
||||
|
||||
|
@ -58,6 +65,7 @@ async def api_charges_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
|
|||
**{"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)
|
||||
]
|
||||
|
@ -119,19 +127,55 @@ async def api_charge_balance(charge_id):
|
|||
status_code=HTTPStatus.NOT_FOUND, detail="Charge does not exist."
|
||||
)
|
||||
|
||||
if charge.paid and charge.webhook:
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
r = await client.post(
|
||||
charge.webhook,
|
||||
json=compact_charge(charge),
|
||||
timeout=40,
|
||||
)
|
||||
except AssertionError:
|
||||
charge.webhook = None
|
||||
return {
|
||||
**compact_charge(charge),
|
||||
**{"time_elapsed": charge.time_elapsed},
|
||||
**{"time_left": charge.time_left},
|
||||
**{"paid": charge.paid},
|
||||
}
|
||||
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")
|
||||
@satspay_ext.post("/api/v1/themes/{css_id}")
|
||||
async def api_themes_save(
|
||||
data: SatsPayThemes,
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
css_id: str = None,
|
||||
):
|
||||
if settings.lnbits_admin_users and wallet.wallet.user not in settings.lnbits_admin_users:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
detail="Only server admins can create themes.",
|
||||
)
|
||||
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)
|
||||
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}")
|
||||
async def api_charge_delete(theme_id, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
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
|
||||
|
|
|
@ -203,7 +203,7 @@ async def update_address(id: str, **kwargs) -> Optional[Address]:
|
|||
f"""UPDATE watchonly.addresses SET {q} WHERE id = ? """,
|
||||
(*kwargs.values(), id),
|
||||
)
|
||||
row = await db.fetchone("SELECT * FROM watchonly.addresses WHERE id = ?", (id))
|
||||
row = await db.fetchone("SELECT * FROM watchonly.addresses WHERE id = ?", (id,))
|
||||
return Address.from_row(row) if row else None
|
||||
|
||||
|
||||
|
|
|
@ -3,25 +3,25 @@ async def m001_initial(db):
|
|||
Initial wallet table.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE watchonly.wallets (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
"user" TEXT,
|
||||
masterpub TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
address_no INTEGER NOT NULL DEFAULT 0,
|
||||
balance INTEGER NOT NULL
|
||||
balance {db.big_int} NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE watchonly.addresses (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
address TEXT NOT NULL,
|
||||
wallet TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL
|
||||
amount {db.big_int} NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -3,13 +3,13 @@ async def m001_initial(db):
|
|||
Creates an improved withdraw table and migrates the existing data.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE withdraw.withdraw_links (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT,
|
||||
title TEXT,
|
||||
min_withdrawable INTEGER DEFAULT 1,
|
||||
max_withdrawable INTEGER DEFAULT 1,
|
||||
min_withdrawable {db.big_int} DEFAULT 1,
|
||||
max_withdrawable {db.big_int} DEFAULT 1,
|
||||
uses INTEGER DEFAULT 1,
|
||||
wait_time INTEGER,
|
||||
is_unique INTEGER DEFAULT 0,
|
||||
|
@ -28,13 +28,13 @@ async def m002_change_withdraw_table(db):
|
|||
Creates an improved withdraw table and migrates the existing data.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
f"""
|
||||
CREATE TABLE withdraw.withdraw_link (
|
||||
id TEXT PRIMARY KEY,
|
||||
wallet TEXT,
|
||||
title TEXT,
|
||||
min_withdrawable INTEGER DEFAULT 1,
|
||||
max_withdrawable INTEGER DEFAULT 1,
|
||||
min_withdrawable {db.big_int} DEFAULT 1,
|
||||
max_withdrawable {db.big_int} DEFAULT 1,
|
||||
uses INTEGER DEFAULT 1,
|
||||
wait_time INTEGER,
|
||||
is_unique INTEGER DEFAULT 0,
|
||||
|
|
|
@ -20,6 +20,8 @@ class Extension(NamedTuple):
|
|||
icon: Optional[str] = None
|
||||
contributors: Optional[List[str]] = None
|
||||
hidden: bool = False
|
||||
migration_module: Optional[str] = None
|
||||
db_name: Optional[str] = None
|
||||
|
||||
|
||||
class ExtensionManager:
|
||||
|
@ -64,6 +66,8 @@ class ExtensionManager:
|
|||
config.get("icon"),
|
||||
config.get("contributors"),
|
||||
config.get("hidden") or False,
|
||||
config.get("migration_module"),
|
||||
config.get("db_name"),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -126,7 +126,7 @@ async def check_pending_payments():
|
|||
|
||||
while True:
|
||||
async with db.connect() as conn:
|
||||
logger.debug(
|
||||
logger.info(
|
||||
f"Task: checking all pending payments (incoming={incoming}, outgoing={outgoing}) of last 15 days"
|
||||
)
|
||||
start_time: float = time.time()
|
||||
|
@ -142,15 +142,15 @@ async def check_pending_payments():
|
|||
for payment in pending_payments:
|
||||
await payment.check_status(conn=conn)
|
||||
|
||||
logger.debug(
|
||||
logger.info(
|
||||
f"Task: pending check finished for {len(pending_payments)} payments (took {time.time() - start_time:0.3f} s)"
|
||||
)
|
||||
# we delete expired invoices once upon the first pending check
|
||||
if incoming:
|
||||
logger.debug("Task: deleting all expired invoices")
|
||||
logger.info("Task: deleting all expired invoices")
|
||||
start_time: float = time.time()
|
||||
await delete_expired_invoices(conn=conn)
|
||||
logger.debug(
|
||||
logger.info(
|
||||
f"Task: expired invoice deletion finished (took {time.time() - start_time:0.3f} s)"
|
||||
)
|
||||
|
||||
|
|
711
poetry.lock
generated
711
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -12,57 +12,58 @@ script = "build.py"
|
|||
python = "^3.10 | ^3.9 | ^3.8 | ^3.7"
|
||||
aiofiles = "0.8.0"
|
||||
asgiref = "3.4.1"
|
||||
attrs = "21.2.0"
|
||||
attrs = "22.1.0"
|
||||
bech32 = "1.2.0"
|
||||
bitstring = "3.1.9"
|
||||
certifi = "2021.5.30"
|
||||
charset-normalizer = "2.0.6"
|
||||
click = "8.0.1"
|
||||
ecdsa = "0.17.0"
|
||||
certifi = "2022.9.24"
|
||||
charset-normalizer = "2.0.12"
|
||||
click = "8.0.4"
|
||||
ecdsa = "0.18.0"
|
||||
embit = "0.4.9"
|
||||
fastapi = "0.78.0"
|
||||
fastapi = "0.83.0"
|
||||
h11 = "0.12.0"
|
||||
httpcore = "0.15.0"
|
||||
httptools = "0.4.0"
|
||||
httpx = "0.23.0"
|
||||
idna = "3.2"
|
||||
importlib-metadata = "4.8.1"
|
||||
idna = "3.4"
|
||||
importlib-metadata = "5.0.0"
|
||||
jinja2 = "3.0.1"
|
||||
lnurl = "0.3.6"
|
||||
markupsafe = "2.0.1"
|
||||
marshmallow = "3.17.0"
|
||||
outcome = "1.1.0"
|
||||
marshmallow = "3.18.0"
|
||||
outcome = "1.2.0"
|
||||
psycopg2-binary = "2.9.1"
|
||||
pycryptodomex = "3.14.1"
|
||||
pydantic = "1.8.2"
|
||||
pydantic = "1.10.2"
|
||||
pypng = "0.0.21"
|
||||
pyqrcode = "1.2.1"
|
||||
pyScss = "1.4.0"
|
||||
python-dotenv = "0.19.0"
|
||||
pyyaml = "5.4.1"
|
||||
represent = "1.6.0.post0"
|
||||
rfc3986 = "1.5.0"
|
||||
secp256k1 = "0.14.0"
|
||||
shortuuid = "1.0.1"
|
||||
six = "1.16.0"
|
||||
sniffio = "1.2.0"
|
||||
sqlalchemy = "1.3.23"
|
||||
sniffio = "1.3.0"
|
||||
sqlalchemy = "1.3.24"
|
||||
sqlalchemy-aio = "0.17.0"
|
||||
sse-starlette = "0.6.2"
|
||||
typing-extensions = "3.10.0.2"
|
||||
uvicorn = "0.18.1"
|
||||
typing-extensions = "^4.4.0"
|
||||
uvicorn = "0.18.3"
|
||||
uvloop = "0.16.0"
|
||||
watchgod = "0.7"
|
||||
websockets = "10.0"
|
||||
zipp = "3.5.0"
|
||||
loguru = "0.5.3"
|
||||
cffi = "1.15.0"
|
||||
zipp = "3.9.0"
|
||||
loguru = "0.6.0"
|
||||
cffi = "1.15.1"
|
||||
websocket-client = "1.3.3"
|
||||
grpcio = "^1.49.1"
|
||||
protobuf = "^4.21.6"
|
||||
Cerberus = "^1.3.4"
|
||||
async-timeout = "^4.0.2"
|
||||
pyln-client = "0.11.1"
|
||||
cashu = "0.5.5"
|
||||
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
isort = "^5.10.1"
|
||||
|
|
137
requirements.txt
137
requirements.txt
|
@ -1,55 +1,82 @@
|
|||
aiofiles==0.8.0
|
||||
anyio==3.6.1
|
||||
asyncio==3.4.3
|
||||
attrs==21.4.0
|
||||
bech32==1.2.0
|
||||
bitstring==3.1.9
|
||||
cerberus==1.3.4
|
||||
certifi==2022.6.15
|
||||
cffi==1.15.0
|
||||
click==8.1.3
|
||||
ecdsa==0.18.0
|
||||
embit==0.5.0
|
||||
environs==9.5.0
|
||||
fastapi==0.79.0
|
||||
h11==0.12.0
|
||||
httpcore==0.15.0
|
||||
httptools==0.4.0
|
||||
httpx==0.23.0
|
||||
idna==3.3
|
||||
jinja2==3.0.1
|
||||
lnurl==0.3.6
|
||||
loguru==0.6.0
|
||||
markupsafe==2.1.1
|
||||
marshmallow==3.17.0
|
||||
outcome==1.2.0
|
||||
packaging==21.3
|
||||
psycopg2-binary==2.9.3
|
||||
pycparser==2.21
|
||||
pycryptodomex==3.15.0
|
||||
pydantic==1.9.1
|
||||
pyngrok==5.1.0
|
||||
pyparsing==3.0.9
|
||||
pypng==0.20220715.0
|
||||
pyqrcode==1.2.1
|
||||
pyscss==1.4.0
|
||||
python-dotenv==0.20.0
|
||||
pyyaml==6.0
|
||||
represent==1.6.0.post0
|
||||
rfc3986==1.5.0
|
||||
secp256k1==0.14.0
|
||||
shortuuid==1.0.9
|
||||
six==1.16.0
|
||||
sniffio==1.2.0
|
||||
sqlalchemy-aio==0.17.0
|
||||
sqlalchemy==1.3.23
|
||||
sse-starlette==0.10.3
|
||||
starlette==0.19.1
|
||||
typing-extensions==4.3.0
|
||||
uvicorn==0.18.2
|
||||
uvloop==0.16.0
|
||||
watchfiles==0.16.0
|
||||
websockets==10.3
|
||||
websocket-client==1.3.3
|
||||
async-timeout==4.0.2
|
||||
setuptools==65.4.0
|
||||
aiofiles==0.8.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
anyio==3.6.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
asgiref==3.4.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
asn1crypto==1.5.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
async-timeout==4.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
attrs==22.1.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
base58==2.1.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
bech32==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
bitstring==3.1.9 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cashu==0.5.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cerberus==1.3.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||
certifi==2022.9.24 ; python_version >= "3.7" and python_version < "4.0"
|
||||
cffi==1.15.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
charset-normalizer==2.0.12 ; python_version >= "3.7" and python_version < "4.0"
|
||||
click==8.0.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||
coincurve==17.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
colorama==0.4.5 ; python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" or python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
|
||||
cryptography==36.0.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
ecdsa==0.18.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
embit==0.4.9 ; python_version >= "3.7" and python_version < "4.0"
|
||||
enum34==1.1.10 ; python_version >= "3.7" and python_version < "4.0"
|
||||
environs==9.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
fastapi==0.83.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
grpcio==1.50.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
h11==0.12.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
httpcore==0.15.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
httptools==0.4.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
httpx==0.23.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
idna==3.4 ; python_version >= "3.7" and python_version < "4.0"
|
||||
importlib-metadata==5.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
iniconfig==1.1.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
jinja2==3.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
lnurl==0.3.6 ; python_version >= "3.7" and python_version < "4.0"
|
||||
loguru==0.6.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
markupsafe==2.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
marshmallow==3.18.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
outcome==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
packaging==21.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pathlib2==2.3.7.post1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pluggy==1.0.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
protobuf==4.21.9 ; python_version >= "3.7" and python_version < "4.0"
|
||||
psycopg2-binary==2.9.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
py==1.11.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pycryptodomex==3.14.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pydantic==1.10.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyln-bolt7==1.0.246 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyln-client==0.11.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyln-proto==0.11.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyparsing==3.0.9 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pypng==0.0.21 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyqrcode==1.2.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyscss==1.4.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pysocks==1.7.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pytest-asyncio==0.19.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pytest==7.1.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
python-bitcoinlib==0.11.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
python-dotenv==0.21.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
pyyaml==5.4.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
represent==1.6.0.post0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
requests==2.27.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
rfc3986==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
rfc3986[idna2008]==1.5.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
secp256k1==0.14.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
setuptools==65.6.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
shortuuid==1.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
six==1.16.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sniffio==1.3.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sqlalchemy-aio==0.17.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sqlalchemy==1.3.24 ; python_version >= "3.7" and python_version < "4.0"
|
||||
sse-starlette==0.6.2 ; python_version >= "3.7" and python_version < "4.0"
|
||||
starlette==0.19.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
tomli==2.0.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||
typing-extensions==4.4.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
urllib3==1.26.12 ; python_version >= "3.7" and python_version < "4"
|
||||
uvicorn==0.18.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
uvloop==0.16.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
watchgod==0.7 ; python_version >= "3.7" and python_version < "4.0"
|
||||
websocket-client==1.3.3 ; python_version >= "3.7" and python_version < "4.0"
|
||||
websockets==10.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
win32-setctime==1.1.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32"
|
||||
zipp==3.9.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||
|
|
Binary file not shown.
|
@ -128,6 +128,10 @@ def migrate_db(file: str, schema: str, exclude_tables: List[str] = []):
|
|||
|
||||
for table in tables:
|
||||
tableName = table[0]
|
||||
print(f"Migrating table {tableName}")
|
||||
# hard coded skip for dbversions (already produced during startup)
|
||||
if tableName == "dbversions":
|
||||
continue
|
||||
if tableName in exclude_tables:
|
||||
continue
|
||||
|
||||
|
@ -151,7 +155,7 @@ def build_insert_query(schema, tableName, columns):
|
|||
def to_column_type(columnType):
|
||||
if columnType == "TIMESTAMP":
|
||||
return "to_timestamp(%s)"
|
||||
if columnType == "BOOLEAN":
|
||||
if columnType in ["BOOLEAN", "BOOL"]:
|
||||
return "%s::boolean"
|
||||
return "%s"
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue