mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-24 14:51:05 +01:00
Merge remote-tracking branch 'origin/main' into gerty
This commit is contained in:
commit
ee79bdcfc8
21 changed files with 294 additions and 134 deletions
|
@ -8,7 +8,7 @@ import warnings
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.exceptions import RequestValidationError
|
from fastapi.exceptions import HTTPException, RequestValidationError
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.middleware.gzip import GZipMiddleware
|
from fastapi.middleware.gzip import GZipMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
@ -68,28 +68,6 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
|
||||||
g().config = lnbits.settings
|
g().config = lnbits.settings
|
||||||
g().base_url = f"http://{lnbits.settings.HOST}:{lnbits.settings.PORT}"
|
g().base_url = f"http://{lnbits.settings.HOST}:{lnbits.settings.PORT}"
|
||||||
|
|
||||||
@app.exception_handler(RequestValidationError)
|
|
||||||
async def validation_exception_handler(
|
|
||||||
request: Request, exc: RequestValidationError
|
|
||||||
):
|
|
||||||
# Only the browser sends "text/html" request
|
|
||||||
# not fail proof, but everything else get's a JSON response
|
|
||||||
|
|
||||||
if (
|
|
||||||
request.headers
|
|
||||||
and "accept" in request.headers
|
|
||||||
and "text/html" in request.headers["accept"]
|
|
||||||
):
|
|
||||||
return template_renderer().TemplateResponse(
|
|
||||||
"error.html",
|
|
||||||
{"request": request, "err": f"{exc.errors()} is not a valid UUID."},
|
|
||||||
)
|
|
||||||
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=HTTPStatus.NO_CONTENT,
|
|
||||||
content={"detail": exc.errors()},
|
|
||||||
)
|
|
||||||
|
|
||||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||||
|
|
||||||
check_funding_source(app)
|
check_funding_source(app)
|
||||||
|
@ -192,12 +170,33 @@ def register_async_tasks(app):
|
||||||
|
|
||||||
def register_exception_handlers(app: FastAPI):
|
def register_exception_handlers(app: FastAPI):
|
||||||
@app.exception_handler(Exception)
|
@app.exception_handler(Exception)
|
||||||
async def basic_error(request: Request, err):
|
async def exception_handler(request: Request, exc: Exception):
|
||||||
logger.error("handled error", traceback.format_exc())
|
|
||||||
logger.error("ERROR:", err)
|
|
||||||
etype, _, tb = sys.exc_info()
|
etype, _, tb = sys.exc_info()
|
||||||
traceback.print_exception(etype, err, tb)
|
traceback.print_exception(etype, exc, tb)
|
||||||
exc = traceback.format_exc()
|
logger.error(f"Exception: {str(exc)}")
|
||||||
|
# Only the browser sends "text/html" request
|
||||||
|
# not fail proof, but everything else get's a JSON response
|
||||||
|
if (
|
||||||
|
request.headers
|
||||||
|
and "accept" in request.headers
|
||||||
|
and "text/html" in request.headers["accept"]
|
||||||
|
):
|
||||||
|
return template_renderer().TemplateResponse(
|
||||||
|
"error.html", {"request": request, "err": f"Error: {str(exc)}"}
|
||||||
|
)
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
content={"detail": str(exc)},
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(RequestValidationError)
|
||||||
|
async def validation_exception_handler(
|
||||||
|
request: Request, exc: RequestValidationError
|
||||||
|
):
|
||||||
|
logger.error(f"RequestValidationError: {str(exc)}")
|
||||||
|
# Only the browser sends "text/html" request
|
||||||
|
# not fail proof, but everything else get's a JSON response
|
||||||
|
|
||||||
if (
|
if (
|
||||||
request.headers
|
request.headers
|
||||||
|
@ -205,12 +204,37 @@ def register_exception_handlers(app: FastAPI):
|
||||||
and "text/html" in request.headers["accept"]
|
and "text/html" in request.headers["accept"]
|
||||||
):
|
):
|
||||||
return template_renderer().TemplateResponse(
|
return template_renderer().TemplateResponse(
|
||||||
"error.html", {"request": request, "err": err}
|
"error.html",
|
||||||
|
{"request": request, "err": f"Error: {str(exc)}"},
|
||||||
)
|
)
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=HTTPStatus.NO_CONTENT,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
content={"detail": err},
|
content={"detail": str(exc)},
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(HTTPException)
|
||||||
|
async def http_exception_handler(request: Request, exc: HTTPException):
|
||||||
|
logger.error(f"HTTPException {exc.status_code}: {exc.detail}")
|
||||||
|
# Only the browser sends "text/html" request
|
||||||
|
# not fail proof, but everything else get's a JSON response
|
||||||
|
|
||||||
|
if (
|
||||||
|
request.headers
|
||||||
|
and "accept" in request.headers
|
||||||
|
and "text/html" in request.headers["accept"]
|
||||||
|
):
|
||||||
|
return template_renderer().TemplateResponse(
|
||||||
|
"error.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"err": f"HTTP Error {exc.status_code}: {exc.detail}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content={"detail": exc.detail},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -339,37 +339,14 @@ async def delete_expired_invoices(
|
||||||
AND time < {db.timestamp_now} - {db.interval_seconds(2592000)}
|
AND time < {db.timestamp_now} - {db.interval_seconds(2592000)}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
# then we delete all invoices whose expiry date is in the past
|
||||||
# then we delete all expired invoices, checking one by one
|
await (conn or db).execute(
|
||||||
rows = await (conn or db).fetchall(
|
|
||||||
f"""
|
f"""
|
||||||
SELECT bolt11
|
DELETE FROM apipayments
|
||||||
FROM apipayments
|
WHERE pending = true AND amount > 0
|
||||||
WHERE pending = true
|
AND expiry < {db.timestamp_now}
|
||||||
AND bolt11 IS NOT NULL
|
|
||||||
AND amount > 0 AND time < {db.timestamp_now} - {db.interval_seconds(86400)}
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
logger.debug(f"Checking expiry of {len(rows)} invoices")
|
|
||||||
for i, (payment_request,) in enumerate(rows):
|
|
||||||
try:
|
|
||||||
invoice = bolt11.decode(payment_request)
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
expiration_date = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
|
|
||||||
if expiration_date > datetime.datetime.utcnow():
|
|
||||||
continue
|
|
||||||
logger.debug(
|
|
||||||
f"Deleting expired invoice {i}/{len(rows)}: {invoice.payment_hash} (expired: {expiration_date})"
|
|
||||||
)
|
|
||||||
await (conn or db).execute(
|
|
||||||
"""
|
|
||||||
DELETE FROM apipayments
|
|
||||||
WHERE pending = true AND hash = ?
|
|
||||||
""",
|
|
||||||
(invoice.payment_hash,),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# payments
|
# payments
|
||||||
|
@ -396,12 +373,19 @@ async def create_payment(
|
||||||
# previous_payment = await get_wallet_payment(wallet_id, payment_hash, conn=conn)
|
# previous_payment = await get_wallet_payment(wallet_id, payment_hash, conn=conn)
|
||||||
# assert previous_payment is None, "Payment already exists"
|
# assert previous_payment is None, "Payment already exists"
|
||||||
|
|
||||||
|
try:
|
||||||
|
invoice = bolt11.decode(payment_request)
|
||||||
|
expiration_date = datetime.datetime.fromtimestamp(invoice.date + invoice.expiry)
|
||||||
|
except:
|
||||||
|
# assume maximum bolt11 expiry of 31 days to be on the safe side
|
||||||
|
expiration_date = datetime.datetime.now() + datetime.timedelta(days=31)
|
||||||
|
|
||||||
await (conn or db).execute(
|
await (conn or db).execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO apipayments
|
INSERT INTO apipayments
|
||||||
(wallet, checking_id, bolt11, hash, preimage,
|
(wallet, checking_id, bolt11, hash, preimage,
|
||||||
amount, pending, memo, fee, extra, webhook)
|
amount, pending, memo, fee, extra, webhook, expiry)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
wallet_id,
|
wallet_id,
|
||||||
|
@ -417,6 +401,7 @@ async def create_payment(
|
||||||
if extra and extra != {} and type(extra) is dict
|
if extra and extra != {} and type(extra) is dict
|
||||||
else None,
|
else None,
|
||||||
webhook,
|
webhook,
|
||||||
|
db.datetime_to_timestamp(expiration_date),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
from sqlalchemy.exc import OperationalError # type: ignore
|
from sqlalchemy.exc import OperationalError # type: ignore
|
||||||
|
|
||||||
|
from lnbits import bolt11
|
||||||
|
|
||||||
|
|
||||||
async def m000_create_migrations_table(db):
|
async def m000_create_migrations_table(db):
|
||||||
await db.execute(
|
await db.execute(
|
||||||
|
@ -188,3 +193,68 @@ async def m005_balance_check_balance_notify(db):
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m006_add_invoice_expiry_to_apipayments(db):
|
||||||
|
"""
|
||||||
|
Adds invoice expiry column to apipayments.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await db.execute("ALTER TABLE apipayments ADD COLUMN expiry TIMESTAMP")
|
||||||
|
except OperationalError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def m007_set_invoice_expiries(db):
|
||||||
|
"""
|
||||||
|
Precomputes invoice expiry for existing pending incoming payments.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
rows = await (
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
SELECT bolt11, checking_id
|
||||||
|
FROM apipayments
|
||||||
|
WHERE pending = true
|
||||||
|
AND amount > 0
|
||||||
|
AND bolt11 IS NOT NULL
|
||||||
|
AND expiry IS NULL
|
||||||
|
AND time < {db.timestamp_now}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
).fetchall()
|
||||||
|
if len(rows):
|
||||||
|
logger.info(f"Mirgraion: Checking expiry of {len(rows)} invoices")
|
||||||
|
for i, (
|
||||||
|
payment_request,
|
||||||
|
checking_id,
|
||||||
|
) in enumerate(rows):
|
||||||
|
try:
|
||||||
|
invoice = bolt11.decode(payment_request)
|
||||||
|
if invoice.expiry is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
expiration_date = datetime.datetime.fromtimestamp(
|
||||||
|
invoice.date + invoice.expiry
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"Mirgraion: {i+1}/{len(rows)} setting expiry of invoice {invoice.payment_hash} to {expiration_date}"
|
||||||
|
)
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
UPDATE apipayments SET expiry = ?
|
||||||
|
WHERE checking_id = ? AND amount > 0
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
db.datetime_to_timestamp(expiration_date),
|
||||||
|
checking_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
except OperationalError:
|
||||||
|
# this is necessary now because it may be the case that this migration will
|
||||||
|
# run twice in some environments.
|
||||||
|
# catching errors like this won't be necessary in anymore now that we
|
||||||
|
# keep track of db versions so no migration ever runs twice.
|
||||||
|
pass
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
from typing import Dict, List, NamedTuple, Optional
|
from typing import Dict, List, NamedTuple, Optional
|
||||||
|
|
||||||
|
@ -83,6 +85,7 @@ class Payment(BaseModel):
|
||||||
bolt11: str
|
bolt11: str
|
||||||
preimage: str
|
preimage: str
|
||||||
payment_hash: str
|
payment_hash: str
|
||||||
|
expiry: Optional[float]
|
||||||
extra: Optional[Dict] = {}
|
extra: Optional[Dict] = {}
|
||||||
wallet_id: str
|
wallet_id: str
|
||||||
webhook: Optional[str]
|
webhook: Optional[str]
|
||||||
|
@ -101,6 +104,7 @@ class Payment(BaseModel):
|
||||||
fee=row["fee"],
|
fee=row["fee"],
|
||||||
memo=row["memo"],
|
memo=row["memo"],
|
||||||
time=row["time"],
|
time=row["time"],
|
||||||
|
expiry=row["expiry"],
|
||||||
wallet_id=row["wallet"],
|
wallet_id=row["wallet"],
|
||||||
webhook=row["webhook"],
|
webhook=row["webhook"],
|
||||||
webhook_status=row["webhook_status"],
|
webhook_status=row["webhook_status"],
|
||||||
|
@ -128,6 +132,10 @@ class Payment(BaseModel):
|
||||||
def is_out(self) -> bool:
|
def is_out(self) -> bool:
|
||||||
return self.amount < 0
|
return self.amount < 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_expired(self) -> bool:
|
||||||
|
return self.expiry < time.time() if self.expiry else False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_uncheckable(self) -> bool:
|
def is_uncheckable(self) -> bool:
|
||||||
return self.checking_id.startswith("internal_")
|
return self.checking_id.startswith("internal_")
|
||||||
|
@ -170,7 +178,13 @@ class Payment(BaseModel):
|
||||||
|
|
||||||
logger.debug(f"Status: {status}")
|
logger.debug(f"Status: {status}")
|
||||||
|
|
||||||
if self.is_out and status.failed:
|
if self.is_in and status.pending and self.is_expired and self.expiry:
|
||||||
|
expiration_date = datetime.datetime.fromtimestamp(self.expiry)
|
||||||
|
logger.debug(
|
||||||
|
f"Deleting expired incoming pending payment {self.checking_id}: expired {expiration_date}"
|
||||||
|
)
|
||||||
|
await self.delete(conn)
|
||||||
|
elif self.is_out and status.failed:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Deleting outgoing failed payment {self.checking_id}: {status}"
|
f"Deleting outgoing failed payment {self.checking_id}: {status}"
|
||||||
)
|
)
|
||||||
|
|
|
@ -702,9 +702,9 @@ async def api_auditor(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
node_balance, delta = None, None
|
node_balance, delta = None, None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"node_balance_msats": node_balance,
|
"node_balance_msats": int(node_balance),
|
||||||
"lnbits_balance_msats": total_balance,
|
"lnbits_balance_msats": int(total_balance),
|
||||||
"delta_msats": delta,
|
"delta_msats": int(delta),
|
||||||
"timestamp": int(time.time()),
|
"timestamp": int(time.time()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
18
lnbits/db.py
18
lnbits/db.py
|
@ -29,6 +29,13 @@ class Compat:
|
||||||
return f"{seconds}"
|
return f"{seconds}"
|
||||||
return "<nothing>"
|
return "<nothing>"
|
||||||
|
|
||||||
|
def datetime_to_timestamp(self, date: datetime.datetime):
|
||||||
|
if self.type in {POSTGRES, COCKROACH}:
|
||||||
|
return date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
elif self.type == SQLITE:
|
||||||
|
return time.mktime(date.timetuple())
|
||||||
|
return "<nothing>"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timestamp_now(self) -> str:
|
def timestamp_now(self) -> str:
|
||||||
if self.type in {POSTGRES, COCKROACH}:
|
if self.type in {POSTGRES, COCKROACH}:
|
||||||
|
@ -125,6 +132,8 @@ class Database(Compat):
|
||||||
import psycopg2 # type: ignore
|
import psycopg2 # type: ignore
|
||||||
|
|
||||||
def _parse_timestamp(value, _):
|
def _parse_timestamp(value, _):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
f = "%Y-%m-%d %H:%M:%S.%f"
|
f = "%Y-%m-%d %H:%M:%S.%f"
|
||||||
if not "." in value:
|
if not "." in value:
|
||||||
f = "%Y-%m-%d %H:%M:%S"
|
f = "%Y-%m-%d %H:%M:%S"
|
||||||
|
@ -149,14 +158,7 @@ class Database(Compat):
|
||||||
|
|
||||||
psycopg2.extensions.register_type(
|
psycopg2.extensions.register_type(
|
||||||
psycopg2.extensions.new_type(
|
psycopg2.extensions.new_type(
|
||||||
(1184, 1114),
|
(1184, 1114), "TIMESTAMP2INT", _parse_timestamp
|
||||||
"TIMESTAMP2INT",
|
|
||||||
_parse_timestamp
|
|
||||||
# lambda value, curs: time.mktime(
|
|
||||||
# datetime.datetime.strptime(
|
|
||||||
# value, "%Y-%m-%d %H:%M:%S.%f"
|
|
||||||
# ).timetuple()
|
|
||||||
# ),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "public.html" %} {% block toolbar_title %} {% raw %} {{name}} Cashu
|
{% extends "public.html" %} {% block toolbar_title %} {% raw %} Cashu {% endraw
|
||||||
{% endraw %} {% endblock %} {% block footer %}{% endblock %} {% block
|
%} - {{mint_name}} {% endblock %} {% block footer %}{% endblock %} {% block
|
||||||
page_container %}
|
page_container %}
|
||||||
<q-page-container>
|
<q-page-container>
|
||||||
<q-page>
|
<q-page>
|
||||||
|
@ -752,7 +752,13 @@ page_container %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn @click="redeem" color="primary">Receive Tokens</q-btn>
|
<q-btn @click="redeem" color="primary">Receive</q-btn>
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
icon="content_copy"
|
||||||
|
class="q-mx-0"
|
||||||
|
@click="copyText(receiveData.tokensBase64)"
|
||||||
|
></q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
icon="photo_camera"
|
icon="photo_camera"
|
||||||
|
|
|
@ -27,11 +27,17 @@ async def index(
|
||||||
|
|
||||||
@cashu_ext.get("/wallet")
|
@cashu_ext.get("/wallet")
|
||||||
async def wallet(request: Request, mint_id: str):
|
async def wallet(request: Request, mint_id: str):
|
||||||
|
cashu = await get_cashu(mint_id)
|
||||||
|
if not cashu:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
|
)
|
||||||
return cashu_renderer().TemplateResponse(
|
return cashu_renderer().TemplateResponse(
|
||||||
"cashu/wallet.html",
|
"cashu/wallet.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"web_manifest": f"/cashu/manifest/{mint_id}.webmanifest",
|
"web_manifest": f"/cashu/manifest/{mint_id}.webmanifest",
|
||||||
|
"mint_name": cashu.name,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,7 +47,7 @@ async def cashu(request: Request, mintID):
|
||||||
cashu = await get_cashu(mintID)
|
cashu = await get_cashu(mintID)
|
||||||
if not cashu:
|
if not cashu:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
)
|
)
|
||||||
return cashu_renderer().TemplateResponse(
|
return cashu_renderer().TemplateResponse(
|
||||||
"cashu/mint.html",
|
"cashu/mint.html",
|
||||||
|
@ -54,7 +60,7 @@ async def manifest(cashu_id: str):
|
||||||
cashu = await get_cashu(cashu_id)
|
cashu = await get_cashu(cashu_id)
|
||||||
if not cashu:
|
if not cashu:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="TPoS does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -221,7 +221,7 @@ async def mint_coins(
|
||||||
|
|
||||||
status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash)
|
status: PaymentStatus = await check_transaction_status(cashu.wallet, payment_hash)
|
||||||
|
|
||||||
if status.paid != True:
|
if LIGHTNING and status.paid != True:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
|
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
|
||||||
)
|
)
|
||||||
|
@ -265,37 +265,51 @@ async def melt_coins(
|
||||||
detail="Error: Tokens are from another mint.",
|
detail="Error: Tokens are from another mint.",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert all([ledger._verify_proof(p) for p in proofs]), HTTPException(
|
# set proofs as pending
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
await ledger._set_proofs_pending(proofs)
|
||||||
detail="Could not verify proofs.",
|
|
||||||
)
|
|
||||||
|
|
||||||
total_provided = sum([p["amount"] for p in proofs])
|
try:
|
||||||
invoice_obj = bolt11.decode(invoice)
|
ledger._verify_proofs(proofs)
|
||||||
amount = math.ceil(invoice_obj.amount_msat / 1000)
|
|
||||||
|
|
||||||
internal_checking_id = await check_internal(invoice_obj.payment_hash)
|
total_provided = sum([p["amount"] for p in proofs])
|
||||||
|
invoice_obj = bolt11.decode(invoice)
|
||||||
|
amount = math.ceil(invoice_obj.amount_msat / 1000)
|
||||||
|
|
||||||
if not internal_checking_id:
|
internal_checking_id = await check_internal(invoice_obj.payment_hash)
|
||||||
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(
|
if not internal_checking_id:
|
||||||
wallet_id=cashu.wallet,
|
fees_msat = fee_reserve(invoice_obj.amount_msat)
|
||||||
payment_request=invoice,
|
else:
|
||||||
description=f"pay cashu invoice",
|
fees_msat = 0
|
||||||
extra={"tag": "cashu", "cahsu_name": cashu.name},
|
assert total_provided >= amount + math.ceil(fees_msat / 1000), Exception(
|
||||||
)
|
f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)."
|
||||||
|
)
|
||||||
|
logger.debug(f"Cashu: Initiating payment of {total_provided} sats")
|
||||||
|
await pay_invoice(
|
||||||
|
wallet_id=cashu.wallet,
|
||||||
|
payment_request=invoice,
|
||||||
|
description=f"Pay cashu invoice",
|
||||||
|
extra={"tag": "cashu", "cashu_name": cashu.name},
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Cashu: Wallet {cashu.wallet} checking PaymentStatus of {invoice_obj.payment_hash}"
|
||||||
|
)
|
||||||
|
status: PaymentStatus = await check_transaction_status(
|
||||||
|
cashu.wallet, invoice_obj.payment_hash
|
||||||
|
)
|
||||||
|
if status.paid == True:
|
||||||
|
logger.debug("Cashu: Payment successful, invalidating proofs")
|
||||||
|
await ledger._invalidate_proofs(proofs)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail=f"Cashu: {str(e)}",
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
# delete proofs from pending list
|
||||||
|
await ledger._unset_proofs_pending(proofs)
|
||||||
|
|
||||||
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)
|
return GetMeltResponse(paid=status.paid, preimage=status.preimage)
|
||||||
|
|
||||||
|
|
||||||
|
@ -333,7 +347,7 @@ async def check_fees(
|
||||||
fees_msat = fee_reserve(invoice_obj.amount_msat)
|
fees_msat = fee_reserve(invoice_obj.amount_msat)
|
||||||
else:
|
else:
|
||||||
fees_msat = 0
|
fees_msat = 0
|
||||||
return CheckFeesResponse(fee=fees_msat / 1000)
|
return CheckFeesResponse(fee=math.ceil(fees_msat / 1000))
|
||||||
|
|
||||||
|
|
||||||
@cashu_ext.post("/api/v1/{cashu_id}/split")
|
@cashu_ext.post("/api/v1/{cashu_id}/split")
|
||||||
|
|
|
@ -37,7 +37,11 @@ async def call_webhook(charge: Charges):
|
||||||
json=public_charge(charge),
|
json=public_charge(charge),
|
||||||
timeout=40,
|
timeout=40,
|
||||||
)
|
)
|
||||||
return {"webhook_success": r.is_success, "webhook_message": r.reason_phrase}
|
return {
|
||||||
|
"webhook_success": r.is_success,
|
||||||
|
"webhook_message": r.reason_phrase,
|
||||||
|
"webhook_response": r.text,
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to call webhook for charge {charge.id}")
|
logger.warning(f"Failed to call webhook for charge {charge.id}")
|
||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
|
|
|
@ -23,6 +23,7 @@ const mapCharge = (obj, oldObj = {}) => {
|
||||||
charge.displayUrl = ['/satspay/', obj.id].join('')
|
charge.displayUrl = ['/satspay/', obj.id].join('')
|
||||||
charge.expanded = oldObj.expanded || false
|
charge.expanded = oldObj.expanded || false
|
||||||
charge.pendingBalance = oldObj.pendingBalance || 0
|
charge.pendingBalance = oldObj.pendingBalance || 0
|
||||||
|
charge.extra = charge.extra ? JSON.parse(charge.extra) : charge.extra
|
||||||
return charge
|
return charge
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -227,7 +227,12 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4 q-pr-lg">
|
<div class="col-4 q-pr-lg">
|
||||||
<q-badge v-if="props.row.webhook_message" color="blue">
|
<q-badge
|
||||||
|
v-if="props.row.webhook_message"
|
||||||
|
@click="showWebhookResponseDialog(props.row.extra.webhook_response)"
|
||||||
|
color="blue"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>
|
||||||
{{props.row.webhook_message }}
|
{{props.row.webhook_message }}
|
||||||
</q-badge>
|
</q-badge>
|
||||||
</div>
|
</div>
|
||||||
|
@ -528,6 +533,23 @@
|
||||||
</q-form>
|
</q-form>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
|
<q-dialog v-model="showWebhookResponse" position="top">
|
||||||
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
readonly
|
||||||
|
v-model.trim="webhookResponse"
|
||||||
|
type="textarea"
|
||||||
|
label="Response"
|
||||||
|
></q-input>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn flat v-close-popup color="grey" class="q-ml-auto">Close</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
<!-- lnbits/static/vendor
|
<!-- lnbits/static/vendor
|
||||||
|
@ -669,7 +691,9 @@
|
||||||
data: {
|
data: {
|
||||||
custom_css: ''
|
custom_css: ''
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
showWebhookResponse: false,
|
||||||
|
webhookResponse: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -757,7 +781,6 @@
|
||||||
'/satspay/api/v1/themes',
|
'/satspay/api/v1/themes',
|
||||||
this.g.user.wallets[0].inkey
|
this.g.user.wallets[0].inkey
|
||||||
)
|
)
|
||||||
console.log(data)
|
|
||||||
this.themeLinks = data.map(c =>
|
this.themeLinks = data.map(c =>
|
||||||
mapCSS(
|
mapCSS(
|
||||||
c,
|
c,
|
||||||
|
@ -852,14 +875,12 @@
|
||||||
},
|
},
|
||||||
updateformDialog: function (themeId) {
|
updateformDialog: function (themeId) {
|
||||||
const theme = _.findWhere(this.themeLinks, {css_id: 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.css_id = theme.css_id
|
||||||
this.formDialogThemes.data.title = theme.title
|
this.formDialogThemes.data.title = theme.title
|
||||||
this.formDialogThemes.data.custom_css = theme.custom_css
|
this.formDialogThemes.data.custom_css = theme.custom_css
|
||||||
this.formDialogThemes.show = true
|
this.formDialogThemes.show = true
|
||||||
},
|
},
|
||||||
createTheme: async function (wallet, data) {
|
createTheme: async function (wallet, data) {
|
||||||
console.log(data.css_id)
|
|
||||||
try {
|
try {
|
||||||
if (data.css_id) {
|
if (data.css_id) {
|
||||||
const resp = await LNbits.api.request(
|
const resp = await LNbits.api.request(
|
||||||
|
@ -887,7 +908,6 @@
|
||||||
custom_css: ''
|
custom_css: ''
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('cun')
|
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -955,6 +975,10 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
showWebhookResponseDialog(webhookResponse) {
|
||||||
|
this.webhookResponse = webhookResponse
|
||||||
|
this.showWebhookResponse = true
|
||||||
|
},
|
||||||
exportchargeCSV: function () {
|
exportchargeCSV: function () {
|
||||||
LNbits.utils.exportCSV(
|
LNbits.utils.exportCSV(
|
||||||
this.chargesTable.columns,
|
this.chargesTable.columns,
|
||||||
|
|
|
@ -184,6 +184,7 @@ window.LNbits = {
|
||||||
bolt11: data.bolt11,
|
bolt11: data.bolt11,
|
||||||
preimage: data.preimage,
|
preimage: data.preimage,
|
||||||
payment_hash: data.payment_hash,
|
payment_hash: data.payment_hash,
|
||||||
|
expiry: data.expiry,
|
||||||
extra: data.extra,
|
extra: data.extra,
|
||||||
wallet_id: data.wallet_id,
|
wallet_id: data.wallet_id,
|
||||||
webhook: data.webhook,
|
webhook: data.webhook,
|
||||||
|
@ -195,6 +196,11 @@ window.LNbits = {
|
||||||
'YYYY-MM-DD HH:mm'
|
'YYYY-MM-DD HH:mm'
|
||||||
)
|
)
|
||||||
obj.dateFrom = moment(obj.date).fromNow()
|
obj.dateFrom = moment(obj.date).fromNow()
|
||||||
|
obj.expirydate = Quasar.utils.date.formatDate(
|
||||||
|
new Date(obj.expiry * 1000),
|
||||||
|
'YYYY-MM-DD HH:mm'
|
||||||
|
)
|
||||||
|
obj.expirydateFrom = moment(obj.expirydate).fromNow()
|
||||||
obj.msat = obj.amount
|
obj.msat = obj.amount
|
||||||
obj.sat = obj.msat / 1000
|
obj.sat = obj.msat / 1000
|
||||||
obj.tag = obj.extra.tag
|
obj.tag = obj.extra.tag
|
||||||
|
|
|
@ -192,9 +192,13 @@ Vue.component('lnbits-payment-details', {
|
||||||
</q-badge>
|
</q-badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-3"><b>Date</b>:</div>
|
<div class="col-3"><b>Created</b>:</div>
|
||||||
<div class="col-9">{{ payment.date }} ({{ payment.dateFrom }})</div>
|
<div class="col-9">{{ payment.date }} ({{ payment.dateFrom }})</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-3"><b>Expiry</b>:</div>
|
||||||
|
<div class="col-9">{{ payment.expirydate }} ({{ payment.expirydateFrom }})</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-3"><b>Description</b>:</div>
|
<div class="col-3"><b>Description</b>:</div>
|
||||||
<div class="col-9">{{ payment.memo }}</div>
|
<div class="col-9">{{ payment.memo }}</div>
|
||||||
|
|
|
@ -145,7 +145,7 @@ async def check_pending_payments():
|
||||||
)
|
)
|
||||||
# we delete expired invoices once upon the first pending check
|
# we delete expired invoices once upon the first pending check
|
||||||
if incoming:
|
if incoming:
|
||||||
logger.info("Task: deleting all expired invoices")
|
logger.debug("Task: deleting all expired invoices")
|
||||||
start_time: float = time.time()
|
start_time: float = time.time()
|
||||||
await delete_expired_invoices(conn=conn)
|
await delete_expired_invoices(conn=conn)
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
8
poetry.lock
generated
8
poetry.lock
generated
|
@ -123,7 +123,7 @@ uvloop = ["uvloop (>=0.15.2)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cashu"
|
name = "cashu"
|
||||||
version = "0.5.5"
|
version = "0.6.0"
|
||||||
description = "Ecash wallet and mint with Bitcoin Lightning support"
|
description = "Ecash wallet and mint with Bitcoin Lightning support"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -1144,7 +1144,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.10 | ^3.9 | ^3.8 | ^3.7"
|
python-versions = "^3.10 | ^3.9 | ^3.8 | ^3.7"
|
||||||
content-hash = "53a18d7695f02e9ad24dc7d0863b5ae815c18f2f390ef20d7166a54b202642ff"
|
content-hash = "7f75ca0b067a11f19520dc2121f0789e16738b573a8da84ba3838ed8a466a6e1"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
aiofiles = [
|
aiofiles = [
|
||||||
|
@ -1208,8 +1208,8 @@ black = [
|
||||||
{file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"},
|
{file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"},
|
||||||
]
|
]
|
||||||
cashu = [
|
cashu = [
|
||||||
{file = "cashu-0.5.5-py3-none-any.whl", hash = "sha256:c1d707479b852e503acca5ed53aa341b1880cd6bd70369488ec002d647970c9b"},
|
{file = "cashu-0.6.0-py3-none-any.whl", hash = "sha256:54096af145643aab45943b235f95a3357b0ec697835c1411e66523049ffb81f6"},
|
||||||
{file = "cashu-0.5.5.tar.gz", hash = "sha256:cc0349d3b6d9a2428cb575fee6280b20074ca9c20a1e2e9c68729a73c01f5f9d"},
|
{file = "cashu-0.6.0.tar.gz", hash = "sha256:503a90c4ca8d25d0b2c3f78a11b163c32902a726ea5b58e5337dc00eca8e96ad"},
|
||||||
]
|
]
|
||||||
Cerberus = [
|
Cerberus = [
|
||||||
{file = "Cerberus-1.3.4.tar.gz", hash = "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c"},
|
{file = "Cerberus-1.3.4.tar.gz", hash = "sha256:d1b21b3954b2498d9a79edf16b3170a3ac1021df88d197dc2ce5928ba519237c"},
|
||||||
|
|
|
@ -64,7 +64,7 @@ protobuf = "^4.21.6"
|
||||||
Cerberus = "^1.3.4"
|
Cerberus = "^1.3.4"
|
||||||
async-timeout = "^4.0.2"
|
async-timeout = "^4.0.2"
|
||||||
pyln-client = "0.11.1"
|
pyln-client = "0.11.1"
|
||||||
cashu = "0.5.5"
|
cashu = "^0.6.0"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
|
|
|
@ -7,7 +7,7 @@ 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"
|
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"
|
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"
|
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"
|
cashu==0.6.0 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
cerberus==1.3.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"
|
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"
|
cffi==1.15.1 ; python_version >= "3.7" and python_version < "4.0"
|
||||||
|
|
|
@ -46,11 +46,11 @@ async def test_get_wallet_no_redirect(client):
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
|
|
||||||
# check GET /wallet: wrong user, expect 204
|
# check GET /wallet: wrong user, expect 400
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_wallet_with_nonexistent_user(client):
|
async def test_get_wallet_with_nonexistent_user(client):
|
||||||
response = await client.get("wallet", params={"usr": "1"})
|
response = await client.get("wallet", params={"usr": "1"})
|
||||||
assert response.status_code == 204, (
|
assert response.status_code == 400, (
|
||||||
str(response.url) + " " + str(response.status_code)
|
str(response.url) + " " + str(response.status_code)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -91,11 +91,11 @@ async def test_get_wallet_with_user_and_wallet(client, to_user, to_wallet):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# check GET /wallet: wrong wallet and user, expect 204
|
# check GET /wallet: wrong wallet and user, expect 400
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_wallet_with_user_and_wrong_wallet(client, to_user):
|
async def test_get_wallet_with_user_and_wrong_wallet(client, to_user):
|
||||||
response = await client.get("wallet", params={"usr": to_user.id, "wal": "1"})
|
response = await client.get("wallet", params={"usr": to_user.id, "wal": "1"})
|
||||||
assert response.status_code == 204, (
|
assert response.status_code == 400, (
|
||||||
str(response.url) + " " + str(response.status_code)
|
str(response.url) + " " + str(response.status_code)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -109,20 +109,20 @@ async def test_get_extensions(client, to_user):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# check GET /extensions: extensions list wrong user, expect 204
|
# check GET /extensions: extensions list wrong user, expect 400
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_extensions_wrong_user(client, to_user):
|
async def test_get_extensions_wrong_user(client, to_user):
|
||||||
response = await client.get("extensions", params={"usr": "1"})
|
response = await client.get("extensions", params={"usr": "1"})
|
||||||
assert response.status_code == 204, (
|
assert response.status_code == 400, (
|
||||||
str(response.url) + " " + str(response.status_code)
|
str(response.url) + " " + str(response.status_code)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# check GET /extensions: no user given, expect code 204 no content
|
# check GET /extensions: no user given, expect code 400 bad request
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_extensions_no_user(client):
|
async def test_get_extensions_no_user(client):
|
||||||
response = await client.get("extensions")
|
response = await client.get("extensions")
|
||||||
assert response.status_code == 204, ( # no content
|
assert response.status_code == 400, ( # bad request
|
||||||
str(response.url) + " " + str(response.status_code)
|
str(response.url) + " " + str(response.status_code)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -61,21 +61,21 @@ async def test_endpoints_inkey(client, inkey_headers_to):
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.skipif(is_fake, reason="this test is only passes with regtest")
|
@pytest.mark.skipif(is_fake, reason="this test is only passes with regtest")
|
||||||
async def test_endpoints_adminkey_nocontent(client, adminkey_headers_to):
|
async def test_endpoints_adminkey_badrequest(client, adminkey_headers_to):
|
||||||
response = await client.post("/boltz/api/v1/swap", headers=adminkey_headers_to)
|
response = await client.post("/boltz/api/v1/swap", headers=adminkey_headers_to)
|
||||||
assert response.status_code == 204
|
assert response.status_code == 400
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
"/boltz/api/v1/swap/reverse", headers=adminkey_headers_to
|
"/boltz/api/v1/swap/reverse", headers=adminkey_headers_to
|
||||||
)
|
)
|
||||||
assert response.status_code == 204
|
assert response.status_code == 400
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
"/boltz/api/v1/swap/refund", headers=adminkey_headers_to
|
"/boltz/api/v1/swap/refund", headers=adminkey_headers_to
|
||||||
)
|
)
|
||||||
assert response.status_code == 204
|
assert response.status_code == 400
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
"/boltz/api/v1/swap/status", headers=adminkey_headers_to
|
"/boltz/api/v1/swap/status", headers=adminkey_headers_to
|
||||||
)
|
)
|
||||||
assert response.status_code == 204
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
Loading…
Add table
Reference in a new issue