mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2024-11-19 18:11:30 +01:00
Merge branch 'main' into gerty
This commit is contained in:
commit
ec8202d008
@ -14,9 +14,6 @@ LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
|
||||
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available
|
||||
LNBITS_ADMIN_UI=false
|
||||
|
||||
# Restricts access, User IDs seperated by comma
|
||||
LNBITS_ALLOWED_USERS=""
|
||||
|
||||
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
||||
|
||||
# Ad space description
|
||||
|
1
.github/workflows/on-tag.yml
vendored
1
.github/workflows/on-tag.yml
vendored
@ -7,6 +7,7 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+-*"
|
||||
|
||||
jobs:
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM python:3.9-slim
|
||||
FROM python:3.10-slim
|
||||
|
||||
RUN apt-get clean
|
||||
RUN apt-get update
|
||||
|
@ -40,3 +40,33 @@ Allowed Users
|
||||
=============
|
||||
enviroment variable: LNBITS_ALLOWED_USERS, comma-seperated list of user ids
|
||||
By defining this users, LNbits will no longer be useable by the public, only defined users and admins can then access the LNbits frontend.
|
||||
|
||||
|
||||
How to activate
|
||||
=============
|
||||
```
|
||||
$ sudo systemctl stop lnbits.service
|
||||
$ cd ~/lnbits-legend
|
||||
$ sudo nano .env
|
||||
```
|
||||
-> set: `LNBITS_ADMIN_UI=true`
|
||||
|
||||
Now start LNbits once in the terminal window
|
||||
```
|
||||
$ poetry run lnbits
|
||||
```
|
||||
It will now show you the Super User Account:
|
||||
|
||||
`SUCCESS | ✔️ Access super user account at: https://127.0.0.1:5000/wallet?usr=5711d7..`
|
||||
|
||||
The `/wallet?usr=..` is your super user account. You just have to append it to your normal LNbits web domain.
|
||||
|
||||
After that you will find the __`Admin` / `Manage Server`__ between `Wallets` and `Extensions`
|
||||
|
||||
Here you can design the interface, it has TOPUP to fill wallets and you can restrict access rights to extensions only for admins or generally deactivated for everyone. You can make users admins or set up Allowed Users if you want to restrict access. And of course the classic settings of the .env file, e.g. to change the funding source wallet or set a charge fee.
|
||||
|
||||
Do not forget
|
||||
```
|
||||
sudo systemctl start lnbits.service
|
||||
```
|
||||
A little hint, if you set `RESET TO DEFAULTS`, then a new Super User Account will also be created. The old one is then no longer valid.
|
||||
|
@ -451,6 +451,34 @@ async def update_payment_details(
|
||||
return
|
||||
|
||||
|
||||
async def update_payment_extra(
|
||||
payment_hash: str,
|
||||
extra: dict,
|
||||
outgoing: bool = False,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Only update the `extra` field for the payment.
|
||||
Old values in the `extra` JSON object will be kept unless the new `extra` overwrites them.
|
||||
"""
|
||||
|
||||
amount_clause = "AND amount < 0" if outgoing else "AND amount > 0"
|
||||
|
||||
row = await (conn or db).fetchone(
|
||||
f"SELECT hash, extra from apipayments WHERE hash = ? {amount_clause}",
|
||||
(payment_hash,),
|
||||
)
|
||||
if not row:
|
||||
return
|
||||
db_extra = json.loads(row["extra"] if row["extra"] else "{}")
|
||||
db_extra.update(extra)
|
||||
|
||||
await (conn or db).execute(
|
||||
f"UPDATE apipayments SET extra = ? WHERE hash = ? {amount_clause} ",
|
||||
(json.dumps(db_extra), payment_hash),
|
||||
)
|
||||
|
||||
|
||||
async def delete_payment(checking_id: str, conn: Optional[Connection] = None) -> None:
|
||||
await (conn or db).execute(
|
||||
"DELETE FROM apipayments WHERE checking_id = ?", (checking_id,)
|
||||
|
@ -224,7 +224,7 @@ async def m007_set_invoice_expiries(db):
|
||||
)
|
||||
).fetchall()
|
||||
if len(rows):
|
||||
logger.info(f"Mirgraion: Checking expiry of {len(rows)} invoices")
|
||||
logger.info(f"Migration: Checking expiry of {len(rows)} invoices")
|
||||
for i, (
|
||||
payment_request,
|
||||
checking_id,
|
||||
@ -238,7 +238,7 @@ async def m007_set_invoice_expiries(db):
|
||||
invoice.date + invoice.expiry
|
||||
)
|
||||
logger.info(
|
||||
f"Mirgraion: {i+1}/{len(rows)} setting expiry of invoice {invoice.payment_hash} to {expiration_date}"
|
||||
f"Migration: {i+1}/{len(rows)} setting expiry of invoice {invoice.payment_hash} to {expiration_date}"
|
||||
)
|
||||
await db.execute(
|
||||
"""
|
||||
|
@ -4,13 +4,13 @@ import hmac
|
||||
import json
|
||||
import time
|
||||
from sqlite3 import Row
|
||||
from typing import Dict, List, NamedTuple, Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from ecdsa import SECP256k1, SigningKey # type: ignore
|
||||
from fastapi import Query
|
||||
from lnurl import encode as lnurl_encode # type: ignore
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, Extra, validator
|
||||
from pydantic import BaseModel
|
||||
|
||||
from lnbits.db import Connection
|
||||
from lnbits.helpers import url_for
|
||||
|
@ -13,12 +13,7 @@ from loguru import logger
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.db import Connection
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
get_key_type,
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.decorators import WalletTypeInfo, require_admin_key
|
||||
from lnbits.helpers import url_for, urlsafe_short_hash
|
||||
from lnbits.requestvars import g
|
||||
from lnbits.settings import (
|
||||
|
@ -4,7 +4,6 @@ from typing import Dict
|
||||
import httpx
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import SseListenersDict, register_invoice_listener
|
||||
|
||||
from . import db
|
||||
|
@ -38,7 +38,7 @@ from lnbits.decorators import (
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.helpers import url_for, urlsafe_short_hash
|
||||
from lnbits.helpers import url_for
|
||||
from lnbits.settings import get_wallet_class, settings
|
||||
from lnbits.utils.exchange_rates import (
|
||||
currencies,
|
||||
@ -48,14 +48,11 @@ from lnbits.utils.exchange_rates import (
|
||||
|
||||
from .. import core_app, db
|
||||
from ..crud import (
|
||||
create_payment,
|
||||
get_payments,
|
||||
get_standalone_payment,
|
||||
get_total_balance,
|
||||
get_wallet,
|
||||
get_wallet_for_key,
|
||||
save_balance_check,
|
||||
update_payment_status,
|
||||
update_wallet,
|
||||
)
|
||||
from ..services import (
|
||||
@ -71,6 +68,11 @@ from ..services import (
|
||||
from ..tasks import api_invoice_listeners
|
||||
|
||||
|
||||
@core_app.get("/api/v1/health", status_code=HTTPStatus.OK)
|
||||
async def health():
|
||||
return
|
||||
|
||||
|
||||
@core_app.get("/api/v1/wallet")
|
||||
async def api_wallet(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
if wallet.wallet_type == 0:
|
||||
@ -214,7 +216,8 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||
lnurl_response = resp["reason"]
|
||||
else:
|
||||
lnurl_response = True
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
except (httpx.ConnectError, httpx.RequestError) as ex:
|
||||
logger.error(ex)
|
||||
lnurl_response = False
|
||||
|
||||
return {
|
||||
@ -658,7 +661,7 @@ async def img(request: Request, data):
|
||||
)
|
||||
|
||||
|
||||
@core_app.get("/api/v1/audit/", dependencies=[Depends(check_admin)])
|
||||
@core_app.get("/api/v1/audit", dependencies=[Depends(check_admin)])
|
||||
async def api_auditor():
|
||||
WALLET = get_wallet_class()
|
||||
total_balance = await get_total_balance()
|
||||
|
@ -38,7 +38,7 @@ async def favicon():
|
||||
|
||||
|
||||
@core_html_routes.get("/", response_class=HTMLResponse)
|
||||
async def home(request: Request, lightning: str = None):
|
||||
async def home(request: Request, lightning: str = ""):
|
||||
return template_renderer().TemplateResponse(
|
||||
"core/index.html", {"request": request, "lnurl": lightning}
|
||||
)
|
||||
@ -124,12 +124,15 @@ async def wallet(
|
||||
if (
|
||||
len(settings.lnbits_allowed_users) > 0
|
||||
and user_id not in settings.lnbits_allowed_users
|
||||
and user_id not in settings.lnbits_admin_users
|
||||
and user_id != settings.super_user
|
||||
):
|
||||
return template_renderer().TemplateResponse(
|
||||
"error.html", {"request": request, "err": "User not authorized."}
|
||||
)
|
||||
if user_id == settings.super_user or user_id in settings.lnbits_admin_users:
|
||||
user.admin = True
|
||||
|
||||
if not wallet_id:
|
||||
if user.wallets and not wallet_name: # type: ignore
|
||||
wallet = user.wallets[0] # type: ignore
|
||||
|
@ -6,7 +6,6 @@ from urllib.parse import urlparse
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits import bolt11
|
||||
|
||||
|
@ -236,8 +236,8 @@ async def check_user_exists(usr: UUID4) -> User:
|
||||
if (
|
||||
len(settings.lnbits_allowed_users) > 0
|
||||
and g().user.id not in settings.lnbits_allowed_users
|
||||
and g().user.id != settings.super_user
|
||||
and g().user.id not in settings.lnbits_admin_users
|
||||
and g().user.id != settings.super_user
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
|
||||
|
@ -42,18 +42,23 @@ Updated for v0.1.3
|
||||
- Or you can Click the "KEYS / AUTH LINK" button to copy the auth URL to the clipboard. Then paste it into the Android app (Create Bolt Card -> PASTE AUTH URL).
|
||||
- Click WRITE CARD NOW and approach the NFC card to set it up. DO NOT REMOVE THE CARD PREMATURELY!
|
||||
|
||||
## Erasing the card - Boltcard NFC Card Creator
|
||||
Updated for v0.1.3
|
||||
## Rewriting / Erasing the card - Boltcard NFC Card Creator
|
||||
|
||||
Since v0.1.2 of Boltcard NFC Card Creator it is possible not only reset the keys but also disable the SUN function and do the complete erase so the card can be use again as a static tag (or set as a new Bolt Card, ofc).
|
||||
It is possible not only to reset the keys but also to disable the SUN function and completely erase the card so it can be used again as a static tag or set up as a new Bolt Card.
|
||||
|
||||
IMPORTANT:
|
||||
* It is immanent that you have access to your old keys so do not erase this card in LNbits before you copied those strings!
|
||||
* If you tried to write to them and failed you will need the same amount of positive writing sessions to unlock the card.
|
||||
|
||||
- in the BoltCard-Extension click the QR code button next to your old card and copy Key0
|
||||
- in the BoltApp click Advanced - Reset keys and paste the Key0 into the first field named Key0
|
||||
- repeat with Key1/Key2/Key3/Key0
|
||||
- when done pasting all 4 keys scan your card with the BoltApp
|
||||
- Thats it 🥳
|
||||
- If everything was successful the card can be safely deleted from LNbits (but keep the keys backed up anyway; batter safe than brick).
|
||||
|
||||
You can watch a video of this process here https://www.youtube.com/watch?time_continue=230&v=Pe0YXHawHvQ&feature=emb_logo
|
||||
|
||||
- Click the QR code button next to a card to view its details and select WIPE
|
||||
- OR click the red cross icon on the right side to reach the same
|
||||
- In the android app (Advanced -> Reset Keys)
|
||||
- Click SCAN QR CODE to scan the QR
|
||||
- Or click WIPE DATA in LNbits to copy and paste in to the app (PASTE KEY JSON)
|
||||
- Click RESET CARD NOW and approach the NFC card to erase it. DO NOT REMOVE THE CARD PREMATURELY!
|
||||
- Now if there is all success the card can be safely delete from LNbits (but keep the keys backuped anyway; batter safe than brick).
|
||||
|
||||
## Setting the card - computer (hard way)
|
||||
|
||||
|
@ -159,7 +159,7 @@ page_container %}
|
||||
size="lg"
|
||||
color="secondary"
|
||||
class="q-mr-md cursor-pointer"
|
||||
@click="recheckInvoice(props.row.hash)"
|
||||
@click="checkInvoice(props.row.hash)"
|
||||
>
|
||||
Check
|
||||
</q-badge>
|
||||
@ -1528,57 +1528,17 @@ page_container %}
|
||||
return proofs.reduce((s, t) => (s += t.amount), 0)
|
||||
},
|
||||
|
||||
deleteProofs: function (proofs) {
|
||||
// delete proofs from this.proofs
|
||||
const usedSecrets = proofs.map(p => p.secret)
|
||||
this.proofs = this.proofs.filter(p => !usedSecrets.includes(p.secret))
|
||||
this.storeProofs()
|
||||
return this.proofs
|
||||
},
|
||||
|
||||
//////////// API ///////////
|
||||
clearAllWorkers: function () {
|
||||
if (this.invoiceCheckListener) {
|
||||
clearInterval(this.invoiceCheckListener)
|
||||
}
|
||||
if (this.tokensCheckSpendableListener) {
|
||||
clearInterval(this.tokensCheckSpendableListener)
|
||||
}
|
||||
},
|
||||
invoiceCheckWorker: async function () {
|
||||
let nInterval = 0
|
||||
this.clearAllWorkers()
|
||||
this.invoiceCheckListener = setInterval(async () => {
|
||||
try {
|
||||
nInterval += 1
|
||||
|
||||
// exit loop after 2m
|
||||
if (nInterval > 40) {
|
||||
console.log('### stopping invoice check worker')
|
||||
this.clearAllWorkers()
|
||||
}
|
||||
console.log('### invoiceCheckWorker setInterval', nInterval)
|
||||
console.log(this.invoiceData)
|
||||
|
||||
// this will throw an error if the invoice is pending
|
||||
await this.recheckInvoice(this.invoiceData.hash, false)
|
||||
|
||||
// only without error (invoice paid) will we reach here
|
||||
console.log('### stopping invoice check worker')
|
||||
this.clearAllWorkers()
|
||||
this.invoiceData.bolt11 = ''
|
||||
this.showInvoiceDetails = false
|
||||
if (window.navigator.vibrate) navigator.vibrate(200)
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'positive',
|
||||
message: 'Payment received',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('not paid yet')
|
||||
}
|
||||
}, 3000)
|
||||
},
|
||||
// MINT
|
||||
|
||||
requestMintButton: async function () {
|
||||
await this.requestMint()
|
||||
@ -1586,8 +1546,12 @@ page_container %}
|
||||
await this.invoiceCheckWorker()
|
||||
},
|
||||
|
||||
// /mint
|
||||
|
||||
requestMint: async function () {
|
||||
// gets an invoice from the mint to get new tokens
|
||||
/*
|
||||
gets an invoice from the mint to get new tokens
|
||||
*/
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
@ -1611,7 +1575,14 @@ page_container %}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// /mint
|
||||
|
||||
mintApi: async function (amounts, payment_hash, verbose = true) {
|
||||
/*
|
||||
asks the mint to check whether the invoice with payment_hash has been paid
|
||||
and requests signing of the attached outputs (blindedMessages)
|
||||
*/
|
||||
console.log('### promises', payment_hash)
|
||||
try {
|
||||
let secrets = await this.generateSecrets(amounts)
|
||||
@ -1647,7 +1618,19 @@ page_container %}
|
||||
}
|
||||
this.proofs = this.proofs.concat(proofs)
|
||||
this.storeProofs()
|
||||
|
||||
// update UI
|
||||
await this.setInvoicePaid(payment_hash)
|
||||
tokensBase64 = btoa(JSON.stringify(proofs))
|
||||
|
||||
this.historyTokens.push({
|
||||
status: 'paid',
|
||||
amount: amount,
|
||||
date: currentDateStr(),
|
||||
token: tokensBase64
|
||||
})
|
||||
this.storehistoryTokens()
|
||||
|
||||
return proofs
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@ -1657,62 +1640,20 @@ page_container %}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
splitToSend: async function (proofs, amount, invlalidate = false) {
|
||||
// splits proofs so the user can keep firstProofs, send scndProofs
|
||||
try {
|
||||
const spendableProofs = proofs.filter(p => !p.reserved)
|
||||
if (this.sumProofs(spendableProofs) < amount) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'warning',
|
||||
message: 'Balance too low',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
throw Error('balance too low.')
|
||||
}
|
||||
let {fristProofs, scndProofs} = await this.split(
|
||||
spendableProofs,
|
||||
amount
|
||||
)
|
||||
|
||||
// set scndProofs in this.proofs as reserved
|
||||
const usedSecrets = proofs.map(p => p.secret)
|
||||
for (let i = 0; i < this.proofs.length; i++) {
|
||||
if (usedSecrets.includes(this.proofs[i].secret)) {
|
||||
this.proofs[i].reserved = true
|
||||
}
|
||||
}
|
||||
if (invlalidate) {
|
||||
// delete tokens from db
|
||||
this.proofs = fristProofs
|
||||
// add new fristProofs, scndProofs to this.proofs
|
||||
this.storeProofs()
|
||||
}
|
||||
|
||||
return {fristProofs, scndProofs}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
// SPLIT
|
||||
|
||||
split: async function (proofs, amount) {
|
||||
/*
|
||||
supplies proofs and requests a split from the mint of these
|
||||
proofs at a specific amount
|
||||
*/
|
||||
try {
|
||||
if (proofs.length == 0) {
|
||||
throw new Error('no proofs provided.')
|
||||
}
|
||||
let {fristProofs, scndProofs} = await this.splitApi(proofs, amount)
|
||||
// delete proofs from this.proofs
|
||||
const usedSecrets = proofs.map(p => p.secret)
|
||||
this.proofs = this.proofs.filter(p => !usedSecrets.includes(p.secret))
|
||||
this.deleteProofs(proofs)
|
||||
// add new fristProofs, scndProofs to this.proofs
|
||||
this.proofs = this.proofs.concat(fristProofs).concat(scndProofs)
|
||||
this.storeProofs()
|
||||
@ -1723,6 +1664,9 @@ page_container %}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// /split
|
||||
|
||||
splitApi: async function (proofs, amount) {
|
||||
try {
|
||||
const total = this.sumProofs(proofs)
|
||||
@ -1782,7 +1726,62 @@ page_container %}
|
||||
}
|
||||
},
|
||||
|
||||
splitToSend: async function (proofs, amount, invlalidate = false) {
|
||||
/*
|
||||
splits proofs so the user can keep firstProofs, send scndProofs.
|
||||
then sets scndProofs as reserved.
|
||||
|
||||
if invalidate, scndProofs (the one to send) are invalidated
|
||||
*/
|
||||
try {
|
||||
const spendableProofs = proofs.filter(p => !p.reserved)
|
||||
if (this.sumProofs(spendableProofs) < amount) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'warning',
|
||||
message: 'Balance too low',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
throw Error('balance too low.')
|
||||
}
|
||||
|
||||
// call /split
|
||||
|
||||
let {fristProofs, scndProofs} = await this.split(
|
||||
spendableProofs,
|
||||
amount
|
||||
)
|
||||
// set scndProofs in this.proofs as reserved
|
||||
const usedSecrets = proofs.map(p => p.secret)
|
||||
for (let i = 0; i < this.proofs.length; i++) {
|
||||
if (usedSecrets.includes(this.proofs[i].secret)) {
|
||||
this.proofs[i].reserved = true
|
||||
}
|
||||
}
|
||||
if (invlalidate) {
|
||||
// delete scndProofs from db
|
||||
this.deleteProofs(scndProofs)
|
||||
}
|
||||
|
||||
return {fristProofs, scndProofs}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
redeem: async function () {
|
||||
/*
|
||||
uses split to receive new tokens.
|
||||
*/
|
||||
this.showReceiveTokens = false
|
||||
console.log('### receive tokens', this.receiveData.tokensBase64)
|
||||
try {
|
||||
@ -1793,6 +1792,9 @@ page_container %}
|
||||
const proofs = JSON.parse(tokenJson)
|
||||
const amount = proofs.reduce((s, t) => (s += t.amount), 0)
|
||||
let {fristProofs, scndProofs} = await this.split(proofs, amount)
|
||||
|
||||
// update UI
|
||||
|
||||
// HACK: we need to do this so the balance updates
|
||||
this.proofs = this.proofs.concat([])
|
||||
|
||||
@ -1827,13 +1829,18 @@ page_container %}
|
||||
},
|
||||
|
||||
sendTokens: async function () {
|
||||
/*
|
||||
calls splitToSend, displays token and kicks off the spendableWorker
|
||||
*/
|
||||
try {
|
||||
// keep firstProofs, send scndProofs
|
||||
// keep firstProofs, send scndProofs and delete them (invalidate=true)
|
||||
let {fristProofs, scndProofs} = await this.splitToSend(
|
||||
this.proofs,
|
||||
this.sendData.amount,
|
||||
true
|
||||
)
|
||||
|
||||
// update UI
|
||||
this.sendData.tokens = scndProofs
|
||||
console.log('### this.sendData.tokens', this.sendData.tokens)
|
||||
this.sendData.tokensBase64 = btoa(
|
||||
@ -1846,33 +1853,19 @@ page_container %}
|
||||
date: currentDateStr(),
|
||||
token: this.sendData.tokensBase64
|
||||
})
|
||||
|
||||
// store "pending" outgoing tokens in history table
|
||||
this.storehistoryTokens()
|
||||
|
||||
this.checkTokenSpendableWorker()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
checkFees: async function (payment_request) {
|
||||
const payload = {
|
||||
pr: payment_request
|
||||
}
|
||||
console.log('#### payload', JSON.stringify(payload))
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
`/cashu/api/v1/${this.mintId}/checkfees`,
|
||||
'',
|
||||
payload
|
||||
)
|
||||
console.log('#### checkFees', payment_request, data.fee)
|
||||
return data.fee
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// /melt
|
||||
|
||||
melt: async function () {
|
||||
// todo: get fees from server and add to inputs
|
||||
this.payInvoiceData.blocking = true
|
||||
@ -1924,8 +1917,20 @@ page_container %}
|
||||
]
|
||||
})
|
||||
// delete spent tokens from db
|
||||
this.proofs = fristProofs
|
||||
this.storeProofs()
|
||||
this.deleteProofs(scndProofs)
|
||||
|
||||
// update UI
|
||||
|
||||
tokensBase64 = btoa(JSON.stringify(scndProofs))
|
||||
|
||||
this.historyTokens.push({
|
||||
status: 'paid',
|
||||
amount: -amount,
|
||||
date: currentDateStr(),
|
||||
token: tokensBase64
|
||||
})
|
||||
this.storehistoryTokens()
|
||||
|
||||
console.log({
|
||||
amount: -amount,
|
||||
bolt11: this.payInvoiceData.data.request,
|
||||
@ -1953,13 +1958,95 @@ page_container %}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// /check
|
||||
|
||||
checkProofsSpendable: async function (proofs, update_history = false) {
|
||||
/*
|
||||
checks with the mint whether an array of proofs is still
|
||||
spendable or already invalidated
|
||||
*/
|
||||
const payload = {
|
||||
proofs: proofs.flat()
|
||||
}
|
||||
console.log('#### payload', JSON.stringify(payload))
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
`/cashu/api/v1/${this.mintId}/check`,
|
||||
'',
|
||||
payload
|
||||
)
|
||||
|
||||
// delete proofs from database if it is spent
|
||||
let spentProofs = proofs.filter((p, pidx) => !data[pidx])
|
||||
if (spentProofs.length) {
|
||||
this.deleteProofs(spentProofs)
|
||||
|
||||
// update UI
|
||||
if (update_history) {
|
||||
tokensBase64 = btoa(JSON.stringify(spentProofs))
|
||||
|
||||
this.historyTokens.push({
|
||||
status: 'paid',
|
||||
amount: -this.sumProofs(spentProofs),
|
||||
date: currentDateStr(),
|
||||
token: tokensBase64
|
||||
})
|
||||
this.storehistoryTokens()
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// /checkfees
|
||||
checkFees: async function (payment_request) {
|
||||
const payload = {
|
||||
pr: payment_request
|
||||
}
|
||||
console.log('#### payload', JSON.stringify(payload))
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
`/cashu/api/v1/${this.mintId}/checkfees`,
|
||||
'',
|
||||
payload
|
||||
)
|
||||
console.log('#### checkFees', payment_request, data.fee)
|
||||
return data.fee
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// /keys
|
||||
|
||||
fetchMintKeys: async function () {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/cashu/api/v1/${this.mintId}/keys`
|
||||
)
|
||||
this.keys = data
|
||||
localStorage.setItem(
|
||||
this.mintKey(this.mintId, 'keys'),
|
||||
JSON.stringify(data)
|
||||
)
|
||||
},
|
||||
setInvoicePaid: async function (payment_hash) {
|
||||
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
||||
invoice.status = 'paid'
|
||||
this.storeinvoicesCashu()
|
||||
},
|
||||
recheckInvoice: async function (payment_hash, verbose = true) {
|
||||
console.log('### recheckInvoice.hash', payment_hash)
|
||||
checkInvoice: async function (payment_hash, verbose = true) {
|
||||
console.log('### checkInvoice.hash', payment_hash)
|
||||
const invoice = this.invoicesCashu.find(i => i.hash === payment_hash)
|
||||
try {
|
||||
proofs = await this.mint(invoice.amount, invoice.hash, verbose)
|
||||
@ -1969,15 +2056,15 @@ page_container %}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
recheckPendingInvoices: async function () {
|
||||
checkPendingInvoices: async function () {
|
||||
for (const invoice of this.invoicesCashu) {
|
||||
if (invoice.status === 'pending' && invoice.sat > 0) {
|
||||
this.recheckInvoice(invoice.hash, false)
|
||||
if (invoice.status === 'pending' && invoice.amount > 0) {
|
||||
this.checkInvoice(invoice.hash, false)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
recheckPendingTokens: async function () {
|
||||
checkPendingTokens: async function () {
|
||||
for (const token of this.historyTokens) {
|
||||
if (token.status === 'pending' && token.amount < 0) {
|
||||
this.checkTokenSpendable(token.token, false)
|
||||
@ -1990,6 +2077,113 @@ page_container %}
|
||||
this.storehistoryTokens()
|
||||
},
|
||||
|
||||
checkTokenSpendable: async function (token, verbose = true) {
|
||||
/*
|
||||
checks whether a base64-encoded token (from the history table) has been spent already.
|
||||
if it is spent, the appropraite entry in the history table is set to paid.
|
||||
*/
|
||||
const tokenJson = atob(token)
|
||||
const proofs = JSON.parse(tokenJson)
|
||||
let data = await this.checkProofsSpendable(proofs)
|
||||
|
||||
// iterate through response of form {0: true, 1: false, ...}
|
||||
let paid = false
|
||||
for (const [key, spendable] of Object.entries(data)) {
|
||||
if (!spendable) {
|
||||
this.setTokenPaid(token)
|
||||
paid = true
|
||||
}
|
||||
}
|
||||
if (paid) {
|
||||
console.log('### token paid')
|
||||
if (window.navigator.vibrate) navigator.vibrate(200)
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'positive',
|
||||
message: 'Token paid',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
} else {
|
||||
console.log('### token not paid yet')
|
||||
if (verbose) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
color: 'grey',
|
||||
message: 'Token still pending',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
this.sendData.tokens = token
|
||||
}
|
||||
return paid
|
||||
},
|
||||
|
||||
////////////// WORKERS //////////////
|
||||
|
||||
clearAllWorkers: function () {
|
||||
if (this.invoiceCheckListener) {
|
||||
clearInterval(this.invoiceCheckListener)
|
||||
}
|
||||
if (this.tokensCheckSpendableListener) {
|
||||
clearInterval(this.tokensCheckSpendableListener)
|
||||
}
|
||||
},
|
||||
invoiceCheckWorker: async function () {
|
||||
let nInterval = 0
|
||||
this.clearAllWorkers()
|
||||
this.invoiceCheckListener = setInterval(async () => {
|
||||
try {
|
||||
nInterval += 1
|
||||
|
||||
// exit loop after 2m
|
||||
if (nInterval > 40) {
|
||||
console.log('### stopping invoice check worker')
|
||||
this.clearAllWorkers()
|
||||
}
|
||||
console.log('### invoiceCheckWorker setInterval', nInterval)
|
||||
console.log(this.invoiceData)
|
||||
|
||||
// this will throw an error if the invoice is pending
|
||||
await this.checkInvoice(this.invoiceData.hash, false)
|
||||
|
||||
// only without error (invoice paid) will we reach here
|
||||
console.log('### stopping invoice check worker')
|
||||
this.clearAllWorkers()
|
||||
this.invoiceData.bolt11 = ''
|
||||
this.showInvoiceDetails = false
|
||||
if (window.navigator.vibrate) navigator.vibrate(200)
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'positive',
|
||||
message: 'Payment received',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('not paid yet')
|
||||
}
|
||||
}, 3000)
|
||||
},
|
||||
checkTokenSpendableWorker: async function () {
|
||||
let nInterval = 0
|
||||
this.clearAllWorkers()
|
||||
@ -2021,83 +2215,6 @@ page_container %}
|
||||
}, 3000)
|
||||
},
|
||||
|
||||
checkTokenSpendable: async function (token, verbose = true) {
|
||||
const tokenJson = atob(token)
|
||||
const proofs = JSON.parse(tokenJson)
|
||||
const payload = {
|
||||
proofs: proofs.flat()
|
||||
}
|
||||
console.log('#### payload', JSON.stringify(payload))
|
||||
try {
|
||||
const {data} = await LNbits.api.request(
|
||||
'POST',
|
||||
`/cashu/api/v1/${this.mintId}/check`,
|
||||
'',
|
||||
payload
|
||||
)
|
||||
// iterate through response of form {0: true, 1: false, ...}
|
||||
let paid = false
|
||||
for (const [key, spendable] of Object.entries(data)) {
|
||||
if (!spendable) {
|
||||
this.setTokenPaid(token)
|
||||
paid = true
|
||||
}
|
||||
}
|
||||
if (paid) {
|
||||
console.log('### token paid')
|
||||
if (window.navigator.vibrate) navigator.vibrate(200)
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'positive',
|
||||
message: 'Token paid',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
} else {
|
||||
console.log('### token not paid yet')
|
||||
if (verbose) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
color: 'grey',
|
||||
message: 'Token still pending',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
this.sendData.tokens = token
|
||||
}
|
||||
return paid
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
LNbits.utils.notifyApiError(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
fetchMintKeys: async function () {
|
||||
const {data} = await LNbits.api.request(
|
||||
'GET',
|
||||
`/cashu/api/v1/${this.mintId}/keys`
|
||||
)
|
||||
this.keys = data
|
||||
localStorage.setItem(
|
||||
this.mintKey(this.mintId, 'keys'),
|
||||
JSON.stringify(data)
|
||||
)
|
||||
},
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -2116,62 +2233,62 @@ page_container %}
|
||||
}
|
||||
},
|
||||
|
||||
checkInvoice: function () {
|
||||
console.log('#### checkInvoice')
|
||||
try {
|
||||
const invoice = decode(this.payInvoiceData.data.request)
|
||||
// checkInvoice: function () {
|
||||
// console.log('#### checkInvoice')
|
||||
// try {
|
||||
// const invoice = decode(this.payInvoiceData.data.request)
|
||||
|
||||
const cleanInvoice = {
|
||||
msat: invoice.human_readable_part.amount,
|
||||
sat: invoice.human_readable_part.amount / 1000,
|
||||
fsat: LNbits.utils.formatSat(
|
||||
invoice.human_readable_part.amount / 1000
|
||||
)
|
||||
}
|
||||
// const cleanInvoice = {
|
||||
// msat: invoice.human_readable_part.amount,
|
||||
// sat: invoice.human_readable_part.amount / 1000,
|
||||
// fsat: LNbits.utils.formatSat(
|
||||
// invoice.human_readable_part.amount / 1000
|
||||
// )
|
||||
// }
|
||||
|
||||
_.each(invoice.data.tags, tag => {
|
||||
if (_.isObject(tag) && _.has(tag, 'description')) {
|
||||
if (tag.description === 'payment_hash') {
|
||||
cleanInvoice.hash = tag.value
|
||||
} else if (tag.description === 'description') {
|
||||
cleanInvoice.description = tag.value
|
||||
} else if (tag.description === 'expiry') {
|
||||
var expireDate = new Date(
|
||||
(invoice.data.time_stamp + tag.value) * 1000
|
||||
)
|
||||
cleanInvoice.expireDate = Quasar.utils.date.formatDate(
|
||||
expireDate,
|
||||
'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||
)
|
||||
cleanInvoice.expired = false // TODO
|
||||
}
|
||||
}
|
||||
// _.each(invoice.data.tags, tag => {
|
||||
// if (_.isObject(tag) && _.has(tag, 'description')) {
|
||||
// if (tag.description === 'payment_hash') {
|
||||
// cleanInvoice.hash = tag.value
|
||||
// } else if (tag.description === 'description') {
|
||||
// cleanInvoice.description = tag.value
|
||||
// } else if (tag.description === 'expiry') {
|
||||
// var expireDate = new Date(
|
||||
// (invoice.data.time_stamp + tag.value) * 1000
|
||||
// )
|
||||
// cleanInvoice.expireDate = Quasar.utils.date.formatDate(
|
||||
// expireDate,
|
||||
// 'YYYY-MM-DDTHH:mm:ss.SSSZ'
|
||||
// )
|
||||
// cleanInvoice.expired = false // TODO
|
||||
// }
|
||||
// }
|
||||
|
||||
this.payInvoiceData.invoice = cleanInvoice
|
||||
})
|
||||
// this.payInvoiceData.invoice = cleanInvoice
|
||||
// })
|
||||
|
||||
console.log(
|
||||
'#### this.payInvoiceData.invoice',
|
||||
this.payInvoiceData.invoice
|
||||
)
|
||||
} catch (error) {
|
||||
this.$q.notify({
|
||||
timeout: 5000,
|
||||
type: 'warning',
|
||||
message: 'Could not decode invoice',
|
||||
caption: error + '',
|
||||
position: 'top',
|
||||
actions: [
|
||||
{
|
||||
icon: 'close',
|
||||
color: 'white',
|
||||
handler: () => {}
|
||||
}
|
||||
]
|
||||
})
|
||||
throw error
|
||||
}
|
||||
},
|
||||
// console.log(
|
||||
// '#### this.payInvoiceData.invoice',
|
||||
// this.payInvoiceData.invoice
|
||||
// )
|
||||
// } catch (error) {
|
||||
// this.$q.notify({
|
||||
// timeout: 5000,
|
||||
// type: 'warning',
|
||||
// message: 'Could not decode invoice',
|
||||
// caption: error + '',
|
||||
// position: 'top',
|
||||
// actions: [
|
||||
// {
|
||||
// icon: 'close',
|
||||
// color: 'white',
|
||||
// handler: () => {}
|
||||
// }
|
||||
// ]
|
||||
// })
|
||||
// throw error
|
||||
// }
|
||||
// },
|
||||
|
||||
////////////// STORAGE /////////////
|
||||
|
||||
@ -2335,8 +2452,9 @@ page_container %}
|
||||
console.log('#### this.mintId', this.mintId)
|
||||
console.log('#### this.mintName', this.mintName)
|
||||
|
||||
this.recheckPendingInvoices()
|
||||
this.recheckPendingTokens()
|
||||
this.checkProofsSpendable(this.proofs, true)
|
||||
this.checkPendingInvoices()
|
||||
this.checkPendingTokens()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
@ -46,9 +46,16 @@ from .models import Cashu
|
||||
|
||||
# --------- extension imports
|
||||
|
||||
# WARNING: Do not set this to False in production! This will create
|
||||
# tokens for free otherwise. This is for testing purposes only!
|
||||
|
||||
LIGHTNING = True
|
||||
|
||||
if not LIGHTNING:
|
||||
logger.warning(
|
||||
"Cashu: LIGHTNING is set False! That means that I will create ecash for free!"
|
||||
)
|
||||
|
||||
########################################
|
||||
############### LNBITS MINTS ###########
|
||||
########################################
|
||||
@ -130,6 +137,28 @@ async def keys(cashu_id: str = Query(None)) -> dict[int, str]:
|
||||
return ledger.get_keyset(keyset_id=cashu.keyset_id)
|
||||
|
||||
|
||||
@cashu_ext.get("/api/v1/{cashu_id}/keys/{idBase64Urlsafe}")
|
||||
async def keyset_keys(
|
||||
cashu_id: str = Query(None), idBase64Urlsafe: str = Query(None)
|
||||
) -> dict[int, str]:
|
||||
"""
|
||||
Get the public keys of the mint of a specificy keyset id.
|
||||
The id is encoded in base64_urlsafe and needs to be converted back to
|
||||
normal base64 before it can be processed.
|
||||
"""
|
||||
|
||||
cashu: Union[Cashu, None] = await get_cashu(cashu_id)
|
||||
|
||||
if not cashu:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||
)
|
||||
|
||||
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
|
||||
keyset = ledger.get_keyset(keyset_id=id)
|
||||
return keyset
|
||||
|
||||
|
||||
@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"""
|
||||
@ -182,7 +211,7 @@ async def request_mint(cashu_id: str = Query(None), amount: int = 0) -> GetMintR
|
||||
|
||||
|
||||
@cashu_ext.post("/api/v1/{cashu_id}/mint")
|
||||
async def mint_coins(
|
||||
async def mint(
|
||||
data: MintRequest,
|
||||
cashu_id: str = Query(None),
|
||||
payment_hash: str = Query(None),
|
||||
@ -197,6 +226,8 @@ async def mint_coins(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist."
|
||||
)
|
||||
|
||||
keyset = ledger.keysets.keysets[cashu.keyset_id]
|
||||
|
||||
if LIGHTNING:
|
||||
invoice: Invoice = await ledger.crud.get_lightning_invoice(
|
||||
db=ledger.db, hash=payment_hash
|
||||
@ -206,42 +237,55 @@ async def mint_coins(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Mint does not know this invoice.",
|
||||
)
|
||||
if invoice.issued == True:
|
||||
if invoice.issued:
|
||||
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 LIGHTNING and 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."
|
||||
)
|
||||
# set this invoice as issued
|
||||
await ledger.crud.update_lightning_invoice(
|
||||
db=ledger.db, hash=payment_hash, issued=True
|
||||
)
|
||||
|
||||
status: PaymentStatus = await check_transaction_status(
|
||||
cashu.wallet, payment_hash
|
||||
)
|
||||
|
||||
try:
|
||||
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}",
|
||||
)
|
||||
|
||||
if not status.paid:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid."
|
||||
)
|
||||
|
||||
promises = await ledger._generate_promises(
|
||||
B_s=data.blinded_messages, keyset=keyset
|
||||
)
|
||||
return promises
|
||||
except (Exception, HTTPException) as e:
|
||||
logger.debug(f"Cashu: /melt {str(e) or getattr(e, 'detail')}")
|
||||
# unset issued flag because something went wrong
|
||||
await ledger.crud.update_lightning_invoice(
|
||||
db=ledger.db, hash=payment_hash, issued=False
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=getattr(e, "status_code")
|
||||
or HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=str(e) or getattr(e, "detail"),
|
||||
)
|
||||
else:
|
||||
# only used for testing when LIGHTNING=false
|
||||
promises = await ledger._generate_promises(
|
||||
B_s=data.blinded_messages, keyset=keyset
|
||||
)
|
||||
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")
|
||||
@ -285,28 +329,38 @@ async def melt_coins(
|
||||
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)
|
||||
try:
|
||||
await pay_invoice(
|
||||
wallet_id=cashu.wallet,
|
||||
payment_request=invoice,
|
||||
description=f"Pay cashu invoice",
|
||||
extra={"tag": "cashu", "cashu_name": cashu.name},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"Cashu error paying invoice {invoice_obj.payment_hash}: {e}")
|
||||
raise e
|
||||
finally:
|
||||
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(
|
||||
f"Cashu: Payment successful, invalidating proofs for {invoice_obj.payment_hash}"
|
||||
)
|
||||
await ledger._invalidate_proofs(proofs)
|
||||
else:
|
||||
logger.debug(f"Cashu: Payment failed for {invoice_obj.payment_hash}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Cashu: Exception for {invoice_obj.payment_hash}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Cashu: {str(e)}",
|
||||
)
|
||||
finally:
|
||||
logger.debug(f"Cashu: Unset pending for {invoice_obj.payment_hash}")
|
||||
# delete proofs from pending list
|
||||
await ledger._unset_proofs_pending(proofs)
|
||||
|
||||
|
@ -11,7 +11,10 @@ from .models import createLnurldevice, lnurldevicepayment, lnurldevices
|
||||
async def create_lnurldevice(
|
||||
data: createLnurldevice,
|
||||
) -> lnurldevices:
|
||||
lnurldevice_id = urlsafe_short_hash()
|
||||
if data.device == "pos" or data.device == "atm":
|
||||
lnurldevice_id = str(await get_lnurldeviceposcount())
|
||||
else:
|
||||
lnurldevice_id = urlsafe_short_hash()
|
||||
lnurldevice_key = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
@ -79,6 +82,17 @@ async def update_lnurldevice(lnurldevice_id: str, **kwargs) -> Optional[lnurldev
|
||||
return lnurldevices(**row) if row else None
|
||||
|
||||
|
||||
async def get_lnurldeviceposcount() -> int:
|
||||
row = await db.fetchall(
|
||||
"SELECT * FROM lnurldevice.lnurldevices WHERE device = ? OR device = ?",
|
||||
(
|
||||
"pos",
|
||||
"atm",
|
||||
),
|
||||
)
|
||||
return len(row) + 1
|
||||
|
||||
|
||||
async def get_lnurldevice(lnurldevice_id: str) -> lnurldevices:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM lnurldevice.lnurldevices WHERE id = ?", (lnurldevice_id,)
|
||||
|
@ -17,8 +17,8 @@ class createLnurldevice(BaseModel):
|
||||
wallet: str
|
||||
currency: str
|
||||
device: str
|
||||
profit: float
|
||||
amount: int
|
||||
profit: float = 0
|
||||
amount: Optional[int] = 0
|
||||
pin: int = 0
|
||||
profit1: float = 0
|
||||
amount1: int = 0
|
||||
|
@ -2,8 +2,10 @@ import asyncio
|
||||
import json
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core import db as core_db
|
||||
from lnbits.core.crud import update_payment_extra
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
@ -48,19 +50,31 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||
if pay_link.webhook_headers:
|
||||
kwargs["headers"] = json.loads(pay_link.webhook_headers)
|
||||
|
||||
r = await client.post(pay_link.webhook_url, **kwargs)
|
||||
await mark_webhook_sent(payment, r.status_code)
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
await mark_webhook_sent(payment, -1)
|
||||
r: httpx.Response = await client.post(pay_link.webhook_url, **kwargs)
|
||||
await mark_webhook_sent(
|
||||
payment.payment_hash,
|
||||
r.status_code,
|
||||
r.is_success,
|
||||
r.reason_phrase,
|
||||
r.text,
|
||||
)
|
||||
except Exception as ex:
|
||||
logger.error(ex)
|
||||
await mark_webhook_sent(
|
||||
payment.payment_hash, -1, False, "Unexpected Error", str(ex)
|
||||
)
|
||||
|
||||
|
||||
async def mark_webhook_sent(payment: Payment, status: int) -> None:
|
||||
payment.extra["wh_status"] = status
|
||||
async def mark_webhook_sent(
|
||||
payment_hash: str, status: int, is_success: bool, reason_phrase="", text=""
|
||||
) -> None:
|
||||
|
||||
await core_db.execute(
|
||||
"""
|
||||
UPDATE apipayments SET extra = ?
|
||||
WHERE hash = ?
|
||||
""",
|
||||
(json.dumps(payment.extra), payment.payment_hash),
|
||||
await update_payment_extra(
|
||||
payment_hash,
|
||||
{
|
||||
"wh_status": status, # keep for backwards compability
|
||||
"wh_success": is_success,
|
||||
"wh_message": reason_phrase,
|
||||
"wh_response": text,
|
||||
},
|
||||
)
|
||||
|
@ -24,6 +24,8 @@ async def create_charge(user: str, data: CreateCharge) -> Charges:
|
||||
{"mempool_endpoint": config.mempool_endpoint, "network": config.network}
|
||||
)
|
||||
onchain = await get_fresh_address(data.onchainwallet)
|
||||
if not onchain:
|
||||
raise Exception(f"Wallet '{data.onchainwallet}' can no longer be accessed.")
|
||||
onchainaddress = onchain.address
|
||||
else:
|
||||
onchainaddress = None
|
||||
|
@ -37,13 +37,19 @@ from .models import CreateCharge, SatsPayThemes
|
||||
async def api_charge_create(
|
||||
data: CreateCharge, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
||||
):
|
||||
charge = await create_charge(user=wallet.wallet.user, data=data)
|
||||
return {
|
||||
**charge.dict(),
|
||||
**{"time_elapsed": charge.time_elapsed},
|
||||
**{"time_left": charge.time_left},
|
||||
**{"paid": charge.paid},
|
||||
}
|
||||
try:
|
||||
charge = await create_charge(user=wallet.wallet.user, data=data)
|
||||
return {
|
||||
**charge.dict(),
|
||||
**{"time_elapsed": charge.time_elapsed},
|
||||
**{"time_left": charge.time_left},
|
||||
**{"paid": charge.paid},
|
||||
}
|
||||
except Exception as ex:
|
||||
logger.debug(f"Satspay error: {str}")
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(ex)
|
||||
)
|
||||
|
||||
|
||||
@satspay_ext.put("/api/v1/charge/{charge_id}")
|
||||
@ -142,7 +148,7 @@ async def api_charge_balance(charge_id):
|
||||
@satspay_ext.post("/api/v1/themes/{css_id}", dependencies=[Depends(check_admin)])
|
||||
async def api_themes_save(
|
||||
data: SatsPayThemes,
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
css_id: str = Query(...),
|
||||
):
|
||||
|
||||
|
@ -10,12 +10,10 @@ from lnbits.core.crud import (
|
||||
from lnbits.core.models import Payment
|
||||
|
||||
from . import db
|
||||
from .models import CreateUserData, Users, Wallets
|
||||
|
||||
### Users
|
||||
from .models import CreateUserData, User, Wallet
|
||||
|
||||
|
||||
async def create_usermanager_user(data: CreateUserData) -> Users:
|
||||
async def create_usermanager_user(data: CreateUserData) -> User:
|
||||
account = await create_account()
|
||||
user = await get_user(account.id)
|
||||
assert user, "Newly created user couldn't be retrieved"
|
||||
@ -50,17 +48,17 @@ async def create_usermanager_user(data: CreateUserData) -> Users:
|
||||
return user_created
|
||||
|
||||
|
||||
async def get_usermanager_user(user_id: str) -> Optional[Users]:
|
||||
async def get_usermanager_user(user_id: str) -> Optional[User]:
|
||||
row = await db.fetchone("SELECT * FROM usermanager.users WHERE id = ?", (user_id,))
|
||||
return Users(**row) if row else None
|
||||
return User(**row) if row else None
|
||||
|
||||
|
||||
async def get_usermanager_users(user_id: str) -> List[Users]:
|
||||
async def get_usermanager_users(user_id: str) -> List[User]:
|
||||
rows = await db.fetchall(
|
||||
"SELECT * FROM usermanager.users WHERE admin = ?", (user_id,)
|
||||
)
|
||||
|
||||
return [Users(**row) for row in rows]
|
||||
return [User(**row) for row in rows]
|
||||
|
||||
|
||||
async def delete_usermanager_user(user_id: str, delete_core: bool = True) -> None:
|
||||
@ -73,12 +71,9 @@ async def delete_usermanager_user(user_id: str, delete_core: bool = True) -> Non
|
||||
await db.execute("""DELETE FROM usermanager.wallets WHERE "user" = ?""", (user_id,))
|
||||
|
||||
|
||||
### Wallets
|
||||
|
||||
|
||||
async def create_usermanager_wallet(
|
||||
user_id: str, wallet_name: str, admin_id: str
|
||||
) -> Wallets:
|
||||
) -> Wallet:
|
||||
wallet = await create_wallet(user_id=user_id, wallet_name=wallet_name)
|
||||
await db.execute(
|
||||
"""
|
||||
@ -92,28 +87,28 @@ async def create_usermanager_wallet(
|
||||
return wallet_created
|
||||
|
||||
|
||||
async def get_usermanager_wallet(wallet_id: str) -> Optional[Wallets]:
|
||||
async def get_usermanager_wallet(wallet_id: str) -> Optional[Wallet]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM usermanager.wallets WHERE id = ?", (wallet_id,)
|
||||
)
|
||||
return Wallets(**row) if row else None
|
||||
return Wallet(**row) if row else None
|
||||
|
||||
|
||||
async def get_usermanager_wallets(admin_id: str) -> Optional[Wallets]:
|
||||
async def get_usermanager_wallets(admin_id: str) -> List[Wallet]:
|
||||
rows = await db.fetchall(
|
||||
"SELECT * FROM usermanager.wallets WHERE admin = ?", (admin_id,)
|
||||
)
|
||||
return [Wallets(**row) for row in rows]
|
||||
return [Wallet(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_usermanager_users_wallets(user_id: str) -> Optional[Wallets]:
|
||||
async def get_usermanager_users_wallets(user_id: str) -> List[Wallet]:
|
||||
rows = await db.fetchall(
|
||||
"""SELECT * FROM usermanager.wallets WHERE "user" = ?""", (user_id,)
|
||||
)
|
||||
return [Wallets(**row) for row in rows]
|
||||
return [Wallet(**row) for row in rows]
|
||||
|
||||
|
||||
async def get_usermanager_wallet_transactions(wallet_id: str) -> Optional[Payment]:
|
||||
async def get_usermanager_wallet_transactions(wallet_id: str) -> List[Payment]:
|
||||
return await get_payments(
|
||||
wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True
|
||||
)
|
||||
|
@ -19,7 +19,7 @@ class CreateUserWallet(BaseModel):
|
||||
admin_id: str = Query(...)
|
||||
|
||||
|
||||
class Users(BaseModel):
|
||||
class User(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
admin: str
|
||||
@ -27,7 +27,7 @@ class Users(BaseModel):
|
||||
password: Optional[str] = None
|
||||
|
||||
|
||||
class Wallets(BaseModel):
|
||||
class Wallet(BaseModel):
|
||||
id: str
|
||||
admin: str
|
||||
name: str
|
||||
@ -36,5 +36,5 @@ class Wallets(BaseModel):
|
||||
inkey: str
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "Wallets":
|
||||
def from_row(cls, row: Row) -> "Wallet":
|
||||
return cls(**dict(row))
|
||||
|
@ -9,7 +9,9 @@ from . import usermanager_ext, usermanager_renderer
|
||||
|
||||
|
||||
@usermanager_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
async def index(
|
||||
request: Request, user: User = Depends(check_user_exists) # type: ignore
|
||||
):
|
||||
return usermanager_renderer().TemplateResponse(
|
||||
"usermanager/index.html", {"request": request, "user": user.dict()}
|
||||
)
|
||||
|
@ -23,25 +23,31 @@ from .crud import (
|
||||
)
|
||||
from .models import CreateUserData, CreateUserWallet
|
||||
|
||||
# Users
|
||||
|
||||
|
||||
@usermanager_ext.get("/api/v1/users", status_code=HTTPStatus.OK)
|
||||
async def api_usermanager_users(wallet: WalletTypeInfo = Depends(require_admin_key)):
|
||||
async def api_usermanager_users(
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||
):
|
||||
user_id = wallet.wallet.user
|
||||
return [user.dict() for user in await get_usermanager_users(user_id)]
|
||||
|
||||
|
||||
@usermanager_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK)
|
||||
async def api_usermanager_user(user_id, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
@usermanager_ext.get(
|
||||
"/api/v1/users/{user_id}",
|
||||
status_code=HTTPStatus.OK,
|
||||
dependencies=[Depends(get_key_type)],
|
||||
)
|
||||
async def api_usermanager_user(user_id):
|
||||
user = await get_usermanager_user(user_id)
|
||||
return user.dict()
|
||||
return user.dict() if user else None
|
||||
|
||||
|
||||
@usermanager_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED)
|
||||
async def api_usermanager_users_create(
|
||||
data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
@usermanager_ext.post(
|
||||
"/api/v1/users",
|
||||
status_code=HTTPStatus.CREATED,
|
||||
dependencies=[Depends(get_key_type)],
|
||||
)
|
||||
async def api_usermanager_users_create(data: CreateUserData):
|
||||
user = await create_usermanager_user(data)
|
||||
full = user.dict()
|
||||
full["wallets"] = [
|
||||
@ -50,11 +56,12 @@ async def api_usermanager_users_create(
|
||||
return full
|
||||
|
||||
|
||||
@usermanager_ext.delete("/api/v1/users/{user_id}")
|
||||
@usermanager_ext.delete(
|
||||
"/api/v1/users/{user_id}", dependencies=[Depends(require_admin_key)]
|
||||
)
|
||||
async def api_usermanager_users_delete(
|
||||
user_id,
|
||||
delete_core: bool = Query(True),
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
):
|
||||
user = await get_usermanager_user(user_id)
|
||||
if not user:
|
||||
@ -84,10 +91,8 @@ async def api_usermanager_activate_extension(
|
||||
# Wallets
|
||||
|
||||
|
||||
@usermanager_ext.post("/api/v1/wallets")
|
||||
async def api_usermanager_wallets_create(
|
||||
data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
@usermanager_ext.post("/api/v1/wallets", dependencies=[Depends(get_key_type)])
|
||||
async def api_usermanager_wallets_create(data: CreateUserWallet):
|
||||
user = await create_usermanager_wallet(
|
||||
user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id
|
||||
)
|
||||
@ -95,31 +100,33 @@ async def api_usermanager_wallets_create(
|
||||
|
||||
|
||||
@usermanager_ext.get("/api/v1/wallets")
|
||||
async def api_usermanager_wallets(wallet: WalletTypeInfo = Depends(require_admin_key)):
|
||||
async def api_usermanager_wallets(
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
|
||||
):
|
||||
admin_id = wallet.wallet.user
|
||||
return [wallet.dict() for wallet in await get_usermanager_wallets(admin_id)]
|
||||
|
||||
|
||||
@usermanager_ext.get("/api/v1/transactions/{wallet_id}")
|
||||
async def api_usermanager_wallet_transactions(
|
||||
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
@usermanager_ext.get(
|
||||
"/api/v1/transactions/{wallet_id}", dependencies=[Depends(get_key_type)]
|
||||
)
|
||||
async def api_usermanager_wallet_transactions(wallet_id):
|
||||
return await get_usermanager_wallet_transactions(wallet_id)
|
||||
|
||||
|
||||
@usermanager_ext.get("/api/v1/wallets/{user_id}")
|
||||
async def api_usermanager_users_wallets(
|
||||
user_id, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
@usermanager_ext.get(
|
||||
"/api/v1/wallets/{user_id}", dependencies=[Depends(require_admin_key)]
|
||||
)
|
||||
async def api_usermanager_users_wallets(user_id):
|
||||
return [
|
||||
s_wallet.dict() for s_wallet in await get_usermanager_users_wallets(user_id)
|
||||
]
|
||||
|
||||
|
||||
@usermanager_ext.delete("/api/v1/wallets/{wallet_id}")
|
||||
async def api_usermanager_wallets_delete(
|
||||
wallet_id, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
@usermanager_ext.delete(
|
||||
"/api/v1/wallets/{wallet_id}", dependencies=[Depends(require_admin_key)]
|
||||
)
|
||||
async def api_usermanager_wallets_delete(wallet_id):
|
||||
get_wallet = await get_usermanager_wallet(wallet_id)
|
||||
if not get_wallet:
|
||||
raise HTTPException(
|
||||
|
@ -27,9 +27,11 @@ async def create_withdraw_link(
|
||||
open_time,
|
||||
usescsv,
|
||||
webhook_url,
|
||||
webhook_headers,
|
||||
webhook_body,
|
||||
custom_url
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
link_id,
|
||||
@ -45,6 +47,8 @@ async def create_withdraw_link(
|
||||
int(datetime.now().timestamp()) + data.wait_time,
|
||||
usescsv,
|
||||
data.webhook_url,
|
||||
data.webhook_headers,
|
||||
data.webhook_body,
|
||||
data.custom_url,
|
||||
),
|
||||
)
|
||||
|
@ -11,6 +11,7 @@ from loguru import logger
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits.core.crud import update_payment_extra
|
||||
from lnbits.core.services import pay_invoice
|
||||
|
||||
from . import withdraw_ext
|
||||
@ -44,7 +45,11 @@ async def api_lnurl_response(request: Request, unique_hash):
|
||||
"minWithdrawable": link.min_withdrawable * 1000,
|
||||
"maxWithdrawable": link.max_withdrawable * 1000,
|
||||
"defaultDescription": link.title,
|
||||
"webhook_url": link.webhook_url,
|
||||
"webhook_headers": link.webhook_headers,
|
||||
"webhook_body": link.webhook_body,
|
||||
}
|
||||
|
||||
return json.dumps(withdrawResponse)
|
||||
|
||||
|
||||
@ -56,7 +61,7 @@ async def api_lnurl_response(request: Request, unique_hash):
|
||||
name="withdraw.api_lnurl_callback",
|
||||
summary="lnurl withdraw callback",
|
||||
description="""
|
||||
This enpoints allows you to put unique_hash, k1
|
||||
This endpoints allows you to put unique_hash, k1
|
||||
and a payment_request to get your payment_request paid.
|
||||
""",
|
||||
response_description="JSON with status",
|
||||
@ -143,18 +148,39 @@ async def api_lnurl_callback(
|
||||
if link.webhook_url:
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
r = await client.post(
|
||||
link.webhook_url,
|
||||
json={
|
||||
kwargs = {
|
||||
"json": {
|
||||
"payment_hash": payment_hash,
|
||||
"payment_request": payment_request,
|
||||
"lnurlw": link.id,
|
||||
},
|
||||
timeout=40,
|
||||
"timeout": 40,
|
||||
}
|
||||
if link.webhook_body:
|
||||
kwargs["json"]["body"] = json.loads(link.webhook_body)
|
||||
if link.webhook_headers:
|
||||
kwargs["headers"] = json.loads(link.webhook_headers)
|
||||
|
||||
r: httpx.Response = await client.post(link.webhook_url, **kwargs)
|
||||
await update_payment_extra(
|
||||
payment_hash=payment_hash,
|
||||
extra={
|
||||
"wh_success": r.is_success,
|
||||
"wh_message": r.reason_phrase,
|
||||
"wh_response": r.text,
|
||||
},
|
||||
outgoing=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
# webhook fails shouldn't cause the lnurlw to fail since invoice is already paid
|
||||
logger.error("Caught exception when dispatching webhook url:", exc)
|
||||
logger.error(
|
||||
"Caught exception when dispatching webhook url: " + str(exc)
|
||||
)
|
||||
await update_payment_extra(
|
||||
payment_hash=payment_hash,
|
||||
extra={"wh_success": False, "wh_message": str(exc)},
|
||||
outgoing=True,
|
||||
)
|
||||
|
||||
return {"status": "OK"}
|
||||
|
||||
|
@ -122,3 +122,13 @@ async def m005_add_custom_print_design(db):
|
||||
Adds custom print design
|
||||
"""
|
||||
await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN custom_url TEXT;")
|
||||
|
||||
|
||||
async def m006_webhook_headers_and_body(db):
|
||||
"""
|
||||
Add headers and body to webhooks
|
||||
"""
|
||||
await db.execute(
|
||||
"ALTER TABLE withdraw.withdraw_link ADD COLUMN webhook_headers TEXT;"
|
||||
)
|
||||
await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN webhook_body TEXT;")
|
||||
|
@ -16,6 +16,8 @@ class CreateWithdrawData(BaseModel):
|
||||
wait_time: int = Query(..., ge=1)
|
||||
is_unique: bool
|
||||
webhook_url: str = Query(None)
|
||||
webhook_headers: str = Query(None)
|
||||
webhook_body: str = Query(None)
|
||||
custom_url: str = Query(None)
|
||||
|
||||
|
||||
@ -35,6 +37,8 @@ class WithdrawLink(BaseModel):
|
||||
usescsv: str = Query(None)
|
||||
number: int = Query(0)
|
||||
webhook_url: str = Query(None)
|
||||
webhook_headers: str = Query(None)
|
||||
webhook_body: str = Query(None)
|
||||
custom_url: str = Query(None)
|
||||
|
||||
@property
|
||||
|
@ -63,7 +63,8 @@ new Vue({
|
||||
secondMultiplierOptions: ['seconds', 'minutes', 'hours'],
|
||||
data: {
|
||||
is_unique: false,
|
||||
use_custom: false
|
||||
use_custom: false,
|
||||
has_webhook: false
|
||||
}
|
||||
},
|
||||
simpleformDialog: {
|
||||
@ -188,23 +189,35 @@ new Vue({
|
||||
},
|
||||
updateWithdrawLink: function (wallet, data) {
|
||||
var self = this
|
||||
const body = _.pick(
|
||||
data,
|
||||
'title',
|
||||
'min_withdrawable',
|
||||
'max_withdrawable',
|
||||
'uses',
|
||||
'wait_time',
|
||||
'is_unique',
|
||||
'webhook_url',
|
||||
'webhook_headers',
|
||||
'webhook_body',
|
||||
'custom_url'
|
||||
)
|
||||
|
||||
if (data.has_webhook) {
|
||||
body = {
|
||||
...body,
|
||||
webhook_url: data.webhook_url,
|
||||
webhook_headers: data.webhook_headers,
|
||||
webhook_body: data.webhook_body
|
||||
}
|
||||
}
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/withdraw/api/v1/links/' + data.id,
|
||||
wallet.adminkey,
|
||||
_.pick(
|
||||
data,
|
||||
'title',
|
||||
'min_withdrawable',
|
||||
'max_withdrawable',
|
||||
'uses',
|
||||
'wait_time',
|
||||
'is_unique',
|
||||
'webhook_url',
|
||||
'custom_url'
|
||||
)
|
||||
body
|
||||
)
|
||||
.then(function (response) {
|
||||
self.withdrawLinks = _.reject(self.withdrawLinks, function (obj) {
|
||||
|
@ -209,7 +209,13 @@
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
<q-toggle
|
||||
label="Webhook"
|
||||
color="secodary"
|
||||
v-model="formDialog.data.has_webhook"
|
||||
></q-toggle>
|
||||
<q-input
|
||||
v-if="formDialog.data.has_webhook"
|
||||
filled
|
||||
dense
|
||||
v-model="formDialog.data.webhook_url"
|
||||
@ -217,6 +223,24 @@
|
||||
label="Webhook URL (optional)"
|
||||
hint="A URL to be called whenever this link gets used."
|
||||
></q-input>
|
||||
<q-input
|
||||
v-if="formDialog.data.has_webhook"
|
||||
filled
|
||||
dense
|
||||
v-model="formDialog.data.webhook_headers"
|
||||
type="text"
|
||||
label="Webhook Headers (optional)"
|
||||
hint="Custom data as JSON string, send headers along with the webhook."
|
||||
></q-input>
|
||||
<q-input
|
||||
v-if="formDialog.data.has_webhook"
|
||||
filled
|
||||
dense
|
||||
v-model="formDialog.data.webhook_body"
|
||||
type="text"
|
||||
label="Webhook custom data (optional)"
|
||||
hint="Custom data as JSON string, will get posted along with webhook 'body' field."
|
||||
></q-input>
|
||||
<q-list>
|
||||
<q-item tag="label" class="rounded-borders">
|
||||
<q-item-section avatar>
|
||||
|
@ -110,7 +110,6 @@ exclude = """(?x)(
|
||||
| ^lnbits/extensions/streamalerts.
|
||||
| ^lnbits/extensions/tipjar.
|
||||
| ^lnbits/extensions/tpos.
|
||||
| ^lnbits/extensions/usermanager.
|
||||
| ^lnbits/extensions/watchonly.
|
||||
| ^lnbits/extensions/withdraw.
|
||||
| ^lnbits/wallets/lnd_grpc_files.
|
||||
|
Loading…
Reference in New Issue
Block a user