Merge remote-tracking branch 'origin/main' into universalwebsocket
This commit is contained in:
commit
00123d6c16
|
@ -6,6 +6,7 @@ PORT=5000
|
||||||
|
|
||||||
DEBUG=false
|
DEBUG=false
|
||||||
|
|
||||||
|
# Allow users and admins by user IDs (comma separated list)
|
||||||
LNBITS_ALLOWED_USERS=""
|
LNBITS_ALLOWED_USERS=""
|
||||||
LNBITS_ADMIN_USERS=""
|
LNBITS_ADMIN_USERS=""
|
||||||
# Extensions only admin can access
|
# Extensions only admin can access
|
||||||
|
|
|
@ -157,30 +157,29 @@ class CreateInvoiceData(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||||
if data.description_hash:
|
if data.description_hash or data.unhashed_description:
|
||||||
try:
|
try:
|
||||||
description_hash = binascii.unhexlify(data.description_hash)
|
description_hash = (
|
||||||
|
binascii.unhexlify(data.description_hash)
|
||||||
|
if data.description_hash
|
||||||
|
else b""
|
||||||
|
)
|
||||||
|
unhashed_description = (
|
||||||
|
binascii.unhexlify(data.unhashed_description)
|
||||||
|
if data.unhashed_description
|
||||||
|
else b""
|
||||||
|
)
|
||||||
except binascii.Error:
|
except binascii.Error:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail="'description_hash' must be a valid hex string",
|
detail="'description_hash' and 'unhashed_description' must be a valid hex strings",
|
||||||
)
|
)
|
||||||
unhashed_description = b""
|
|
||||||
memo = ""
|
|
||||||
elif data.unhashed_description:
|
|
||||||
try:
|
|
||||||
unhashed_description = binascii.unhexlify(data.unhashed_description)
|
|
||||||
except binascii.Error:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail="'unhashed_description' must be a valid hex string",
|
|
||||||
)
|
|
||||||
description_hash = b""
|
|
||||||
memo = ""
|
memo = ""
|
||||||
else:
|
else:
|
||||||
description_hash = b""
|
description_hash = b""
|
||||||
unhashed_description = b""
|
unhashed_description = b""
|
||||||
memo = data.memo or LNBITS_SITE_TITLE
|
memo = data.memo or LNBITS_SITE_TITLE
|
||||||
|
|
||||||
if data.unit == "sat":
|
if data.unit == "sat":
|
||||||
amount = int(data.amount)
|
amount = int(data.amount)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -487,6 +487,17 @@
|
||||||
@click="copyText(lnurlValue, 'LNURL copied to clipboard!')"
|
@click="copyText(lnurlValue, 'LNURL copied to clipboard!')"
|
||||||
>Copy LNURL</q-btn
|
>Copy LNURL</q-btn
|
||||||
>
|
>
|
||||||
|
<q-chip
|
||||||
|
v-if="websocketMessage == 'WebSocket NOT supported by your Browser!' || websocketMessage == 'Connection closed'"
|
||||||
|
clickable
|
||||||
|
color="red"
|
||||||
|
text-color="white"
|
||||||
|
icon="error"
|
||||||
|
>{% raw %}{{ wsMessage }}{% endraw %}</q-chip
|
||||||
|
>
|
||||||
|
<q-chip v-else clickable color="green" text-color="white" icon="check"
|
||||||
|
>{% raw %}{{ wsMessage }}{% endraw %}</q-chip
|
||||||
|
>
|
||||||
<br />
|
<br />
|
||||||
<div class="row q-mt-lg q-gutter-sm">
|
<div class="row q-mt-lg q-gutter-sm">
|
||||||
<q-btn
|
<q-btn
|
||||||
|
@ -534,6 +545,7 @@
|
||||||
filter: '',
|
filter: '',
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
lnurlValue: '',
|
lnurlValue: '',
|
||||||
|
websocketMessage: '',
|
||||||
switches: 0,
|
switches: 0,
|
||||||
lnurldeviceLinks: [],
|
lnurldeviceLinks: [],
|
||||||
lnurldeviceLinksObj: [],
|
lnurldeviceLinksObj: [],
|
||||||
|
@ -622,6 +634,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
wsMessage: function () {
|
||||||
|
return this.websocketMessage
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openQrCodeDialog: function (lnurldevice_id) {
|
openQrCodeDialog: function (lnurldevice_id) {
|
||||||
var lnurldevice = _.findWhere(this.lnurldeviceLinks, {
|
var lnurldevice = _.findWhere(this.lnurldeviceLinks, {
|
||||||
|
@ -631,11 +648,17 @@
|
||||||
this.qrCodeDialog.data = _.clone(lnurldevice)
|
this.qrCodeDialog.data = _.clone(lnurldevice)
|
||||||
this.qrCodeDialog.data.url =
|
this.qrCodeDialog.data.url =
|
||||||
window.location.protocol + '//' + window.location.host
|
window.location.protocol + '//' + window.location.host
|
||||||
this.lnurlValueFetch(this.qrCodeDialog.data.switches[0][3])
|
this.lnurlValueFetch(
|
||||||
|
this.qrCodeDialog.data.switches[0][3],
|
||||||
|
this.qrCodeDialog.data.id
|
||||||
|
)
|
||||||
this.qrCodeDialog.show = true
|
this.qrCodeDialog.show = true
|
||||||
},
|
},
|
||||||
lnurlValueFetch: function (lnurl) {
|
lnurlValueFetch: function (lnurl, switchId) {
|
||||||
this.lnurlValue = lnurl
|
this.lnurlValue = lnurl
|
||||||
|
this.websocketConnector(
|
||||||
|
'wss://' + window.location.host + '/lnurldevice/ws/' + switchId
|
||||||
|
)
|
||||||
},
|
},
|
||||||
addSwitch: function () {
|
addSwitch: function () {
|
||||||
var self = this
|
var self = this
|
||||||
|
@ -797,6 +820,25 @@
|
||||||
LNbits.utils.notifyApiError(error)
|
LNbits.utils.notifyApiError(error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
websocketConnector: function (websocketUrl) {
|
||||||
|
if ('WebSocket' in window) {
|
||||||
|
self = this
|
||||||
|
var ws = new WebSocket(websocketUrl)
|
||||||
|
self.updateWsMessage('Websocket connected')
|
||||||
|
ws.onmessage = function (evt) {
|
||||||
|
var received_msg = evt.data
|
||||||
|
self.updateWsMessage('Message recieved: ' + received_msg)
|
||||||
|
}
|
||||||
|
ws.onclose = function () {
|
||||||
|
self.updateWsMessage('Connection closed')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.updateWsMessage('WebSocket NOT supported by your Browser!')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateWsMessage: function (message) {
|
||||||
|
this.websocketMessage = message
|
||||||
|
},
|
||||||
clearFormDialoglnurldevice() {
|
clearFormDialoglnurldevice() {
|
||||||
this.formDialoglnurldevice.data = {
|
this.formDialoglnurldevice.data = {
|
||||||
lnurl_toggle: false,
|
lnurl_toggle: false,
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
|
import json
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import httpx
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.services import create_invoice
|
from lnbits.core.services import create_invoice
|
||||||
from lnbits.core.views.api import api_payment
|
from lnbits.core.views.api import api_payment
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
from ..watchonly.crud import get_config, get_fresh_address
|
from ..watchonly.crud import get_config, get_fresh_address
|
||||||
|
|
||||||
# from lnbits.db import open_ext_db
|
|
||||||
from . import db
|
from . import db
|
||||||
|
from .helpers import fetch_onchain_balance
|
||||||
from .models import Charges, CreateCharge
|
from .models import Charges, CreateCharge
|
||||||
|
|
||||||
###############CHARGES##########################
|
###############CHARGES##########################
|
||||||
|
@ -18,6 +18,10 @@ from .models import Charges, CreateCharge
|
||||||
async def create_charge(user: str, data: CreateCharge) -> Charges:
|
async def create_charge(user: str, data: CreateCharge) -> Charges:
|
||||||
charge_id = urlsafe_short_hash()
|
charge_id = urlsafe_short_hash()
|
||||||
if data.onchainwallet:
|
if data.onchainwallet:
|
||||||
|
config = await get_config(user)
|
||||||
|
data.extra = json.dumps(
|
||||||
|
{"mempool_endpoint": config.mempool_endpoint, "network": config.network}
|
||||||
|
)
|
||||||
onchain = await get_fresh_address(data.onchainwallet)
|
onchain = await get_fresh_address(data.onchainwallet)
|
||||||
onchainaddress = onchain.address
|
onchainaddress = onchain.address
|
||||||
else:
|
else:
|
||||||
|
@ -48,9 +52,10 @@ async def create_charge(user: str, data: CreateCharge) -> Charges:
|
||||||
completelinktext,
|
completelinktext,
|
||||||
time,
|
time,
|
||||||
amount,
|
amount,
|
||||||
balance
|
balance,
|
||||||
|
extra
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
charge_id,
|
charge_id,
|
||||||
|
@ -67,6 +72,7 @@ async def create_charge(user: str, data: CreateCharge) -> Charges:
|
||||||
data.time,
|
data.time,
|
||||||
data.amount,
|
data.amount,
|
||||||
0,
|
0,
|
||||||
|
data.extra,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return await get_charge(charge_id)
|
return await get_charge(charge_id)
|
||||||
|
@ -98,34 +104,20 @@ async def delete_charge(charge_id: str) -> None:
|
||||||
await db.execute("DELETE FROM satspay.charges WHERE id = ?", (charge_id,))
|
await db.execute("DELETE FROM satspay.charges WHERE id = ?", (charge_id,))
|
||||||
|
|
||||||
|
|
||||||
async def check_address_balance(charge_id: str) -> List[Charges]:
|
async def check_address_balance(charge_id: str) -> Optional[Charges]:
|
||||||
charge = await get_charge(charge_id)
|
charge = await get_charge(charge_id)
|
||||||
|
|
||||||
if not charge.paid:
|
if not charge.paid:
|
||||||
if charge.onchainaddress:
|
if charge.onchainaddress:
|
||||||
config = await get_charge_config(charge_id)
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
respAmount = await fetch_onchain_balance(charge)
|
||||||
r = await client.get(
|
|
||||||
config.mempool_endpoint
|
|
||||||
+ "/api/address/"
|
|
||||||
+ charge.onchainaddress
|
|
||||||
)
|
|
||||||
respAmount = r.json()["chain_stats"]["funded_txo_sum"]
|
|
||||||
if respAmount > charge.balance:
|
if respAmount > charge.balance:
|
||||||
await update_charge(charge_id=charge_id, balance=respAmount)
|
await update_charge(charge_id=charge_id, balance=respAmount)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
logger.warning(e)
|
||||||
if charge.lnbitswallet:
|
if charge.lnbitswallet:
|
||||||
invoice_status = await api_payment(charge.payment_hash)
|
invoice_status = await api_payment(charge.payment_hash)
|
||||||
|
|
||||||
if invoice_status["paid"]:
|
if invoice_status["paid"]:
|
||||||
return await update_charge(charge_id=charge_id, balance=charge.amount)
|
return await update_charge(charge_id=charge_id, balance=charge.amount)
|
||||||
row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,))
|
return await get_charge(charge_id)
|
||||||
return Charges.from_row(row) if row else None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_charge_config(charge_id: str):
|
|
||||||
row = await db.fetchone(
|
|
||||||
"""SELECT "user" FROM satspay.charges WHERE id = ?""", (charge_id,)
|
|
||||||
)
|
|
||||||
return await get_config(row.user)
|
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
import httpx
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from .models import Charges
|
from .models import Charges
|
||||||
|
|
||||||
|
|
||||||
def compact_charge(charge: Charges):
|
def public_charge(charge: Charges):
|
||||||
return {
|
c = {
|
||||||
"id": charge.id,
|
"id": charge.id,
|
||||||
"description": charge.description,
|
"description": charge.description,
|
||||||
"onchainaddress": charge.onchainaddress,
|
"onchainaddress": charge.onchainaddress,
|
||||||
|
@ -13,5 +16,38 @@ def compact_charge(charge: Charges):
|
||||||
"balance": charge.balance,
|
"balance": charge.balance,
|
||||||
"paid": charge.paid,
|
"paid": charge.paid,
|
||||||
"timestamp": charge.timestamp,
|
"timestamp": charge.timestamp,
|
||||||
"completelink": charge.completelink, # should be secret?
|
"time_elapsed": charge.time_elapsed,
|
||||||
|
"time_left": charge.time_left,
|
||||||
|
"paid": charge.paid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if charge.paid:
|
||||||
|
c["completelink"] = charge.completelink
|
||||||
|
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
async def call_webhook(charge: Charges):
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
r = await client.post(
|
||||||
|
charge.webhook,
|
||||||
|
json=public_charge(charge),
|
||||||
|
timeout=40,
|
||||||
|
)
|
||||||
|
return {"webhook_success": r.is_success, "webhook_message": r.reason_phrase}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to call webhook for charge {charge.id}")
|
||||||
|
logger.warning(e)
|
||||||
|
return {"webhook_success": False, "webhook_message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_onchain_balance(charge: Charges):
|
||||||
|
endpoint = (
|
||||||
|
f"{charge.config.mempool_endpoint}/testnet"
|
||||||
|
if charge.config.network == "Testnet"
|
||||||
|
else charge.config.mempool_endpoint
|
||||||
|
)
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await client.get(endpoint + "/api/address/" + charge.onchainaddress)
|
||||||
|
return r.json()["chain_stats"]["funded_txo_sum"]
|
||||||
|
|
|
@ -26,3 +26,14 @@ async def m001_initial(db):
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def m002_add_charge_extra_data(db):
|
||||||
|
"""
|
||||||
|
Add 'extra' column for storing various config about the charge (JSON format)
|
||||||
|
"""
|
||||||
|
await db.execute(
|
||||||
|
"""ALTER TABLE satspay.charges
|
||||||
|
ADD COLUMN extra TEXT DEFAULT '{"mempool_endpoint": "https://mempool.space", "network": "Mainnet"}';
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from sqlite3 import Row
|
from sqlite3 import Row
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
@ -15,6 +16,14 @@ class CreateCharge(BaseModel):
|
||||||
completelinktext: str = Query(None)
|
completelinktext: str = Query(None)
|
||||||
time: int = Query(..., ge=1)
|
time: int = Query(..., ge=1)
|
||||||
amount: int = Query(..., ge=1)
|
amount: int = Query(..., ge=1)
|
||||||
|
extra: str = "{}"
|
||||||
|
|
||||||
|
|
||||||
|
class ChargeConfig(BaseModel):
|
||||||
|
mempool_endpoint: Optional[str]
|
||||||
|
network: Optional[str]
|
||||||
|
webhook_success: Optional[bool] = False
|
||||||
|
webhook_message: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class Charges(BaseModel):
|
class Charges(BaseModel):
|
||||||
|
@ -28,6 +37,7 @@ class Charges(BaseModel):
|
||||||
webhook: Optional[str]
|
webhook: Optional[str]
|
||||||
completelink: Optional[str]
|
completelink: Optional[str]
|
||||||
completelinktext: Optional[str] = "Back to Merchant"
|
completelinktext: Optional[str] = "Back to Merchant"
|
||||||
|
extra: str = "{}"
|
||||||
time: int
|
time: int
|
||||||
amount: int
|
amount: int
|
||||||
balance: int
|
balance: int
|
||||||
|
@ -54,3 +64,11 @@ class Charges(BaseModel):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self) -> ChargeConfig:
|
||||||
|
charge_config = json.loads(self.extra)
|
||||||
|
return ChargeConfig(**charge_config)
|
||||||
|
|
||||||
|
def must_call_webhook(self):
|
||||||
|
return self.webhook and self.paid and self.config.webhook_success == False
|
||||||
|
|
|
@ -14,15 +14,14 @@ const retryWithDelay = async function (fn, retryCount = 0) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapCharge = (obj, oldObj = {}) => {
|
const mapCharge = (obj, oldObj = {}) => {
|
||||||
const charge = _.clone(obj)
|
const charge = {...oldObj, ...obj}
|
||||||
|
|
||||||
charge.progress = obj.time_left < 0 ? 1 : 1 - obj.time_left / obj.time
|
charge.progress = obj.time_left < 0 ? 1 : 1 - obj.time_left / obj.time
|
||||||
charge.time = minutesToTime(obj.time)
|
charge.time = minutesToTime(obj.time)
|
||||||
charge.timeLeft = minutesToTime(obj.time_left)
|
charge.timeLeft = minutesToTime(obj.time_left)
|
||||||
|
|
||||||
charge.expanded = false
|
|
||||||
charge.displayUrl = ['/satspay/', obj.id].join('')
|
charge.displayUrl = ['/satspay/', obj.id].join('')
|
||||||
charge.expanded = oldObj.expanded
|
charge.expanded = oldObj.expanded || false
|
||||||
charge.pendingBalance = oldObj.pendingBalance || 0
|
charge.pendingBalance = oldObj.pendingBalance || 0
|
||||||
return charge
|
return charge
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
@ -7,7 +8,8 @@ from lnbits.extensions.satspay.crud import check_address_balance, get_charge
|
||||||
from lnbits.helpers import get_current_extension_name
|
from lnbits.helpers import get_current_extension_name
|
||||||
from lnbits.tasks import register_invoice_listener
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
|
||||||
# from .crud import get_ticket, set_ticket_paid
|
from .crud import update_charge
|
||||||
|
from .helpers import call_webhook
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_paid_invoices():
|
async def wait_for_paid_invoices():
|
||||||
|
@ -30,4 +32,9 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
await payment.set_pending(False)
|
await payment.set_pending(False)
|
||||||
await check_address_balance(charge_id=charge.id)
|
charge = await check_address_balance(charge_id=charge.id)
|
||||||
|
|
||||||
|
if charge.must_call_webhook():
|
||||||
|
resp = await call_webhook(charge)
|
||||||
|
extra = {**charge.config.dict(), **resp}
|
||||||
|
await update_charge(charge_id=charge.id, extra=json.dumps(extra))
|
||||||
|
|
|
@ -109,7 +109,7 @@
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
disable
|
disable
|
||||||
v-if="!charge.lnbitswallet || charge.time_elapsed"
|
v-if="!charge.payment_request || charge.time_elapsed"
|
||||||
style="color: primary; width: 100%"
|
style="color: primary; width: 100%"
|
||||||
label="lightning⚡"
|
label="lightning⚡"
|
||||||
>
|
>
|
||||||
|
@ -131,7 +131,7 @@
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
disable
|
disable
|
||||||
v-if="!charge.onchainwallet || charge.time_elapsed"
|
v-if="!charge.onchainaddress || charge.time_elapsed"
|
||||||
style="color: primary; width: 100%"
|
style="color: primary; width: 100%"
|
||||||
label="onchain⛓️"
|
label="onchain⛓️"
|
||||||
>
|
>
|
||||||
|
@ -170,6 +170,8 @@
|
||||||
name="check"
|
name="check"
|
||||||
style="color: green; font-size: 21.4em"
|
style="color: green; font-size: 21.4em"
|
||||||
></q-icon>
|
></q-icon>
|
||||||
|
<div class="row text-center q-mt-lg">
|
||||||
|
<div class="col text-center">
|
||||||
<q-btn
|
<q-btn
|
||||||
outline
|
outline
|
||||||
v-if="charge.webhook"
|
v-if="charge.webhook"
|
||||||
|
@ -178,6 +180,8 @@
|
||||||
:label="charge.completelinktext"
|
:label="charge.completelinktext"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="row text-center q-mb-sm">
|
<div class="row text-center q-mb-sm">
|
||||||
<div class="col text-center">
|
<div class="col text-center">
|
||||||
|
@ -218,7 +222,7 @@
|
||||||
<div class="col text-center">
|
<div class="col text-center">
|
||||||
<a
|
<a
|
||||||
style="color: unset"
|
style="color: unset"
|
||||||
:href="mempool_endpoint + '/address/' + charge.onchainaddress"
|
:href="'https://' + mempoolHostname + '/address/' + charge.onchainaddress"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
><span
|
><span
|
||||||
class="text-subtitle1"
|
class="text-subtitle1"
|
||||||
|
@ -241,6 +245,8 @@
|
||||||
name="check"
|
name="check"
|
||||||
style="color: green; font-size: 21.4em"
|
style="color: green; font-size: 21.4em"
|
||||||
></q-icon>
|
></q-icon>
|
||||||
|
<div class="row text-center q-mt-lg">
|
||||||
|
<div class="col text-center">
|
||||||
<q-btn
|
<q-btn
|
||||||
outline
|
outline
|
||||||
v-if="charge.webhook"
|
v-if="charge.webhook"
|
||||||
|
@ -249,6 +255,8 @@
|
||||||
:label="charge.completelinktext"
|
:label="charge.completelinktext"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="row items-center q-mb-sm">
|
<div class="row items-center q-mb-sm">
|
||||||
<div class="col text-center">
|
<div class="col text-center">
|
||||||
|
@ -303,7 +311,8 @@
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
charge: JSON.parse('{{charge_data | tojson}}'),
|
charge: JSON.parse('{{charge_data | tojson}}'),
|
||||||
mempool_endpoint: '{{mempool_endpoint}}',
|
mempoolEndpoint: '{{mempool_endpoint}}',
|
||||||
|
network: '{{network}}',
|
||||||
pendingFunds: 0,
|
pendingFunds: 0,
|
||||||
ws: null,
|
ws: null,
|
||||||
newProgress: 0.4,
|
newProgress: 0.4,
|
||||||
|
@ -316,19 +325,19 @@
|
||||||
cancelListener: () => {}
|
cancelListener: () => {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
computed: {
|
||||||
startPaymentNotifier() {
|
mempoolHostname: function () {
|
||||||
this.cancelListener()
|
let hostname = new URL(this.mempoolEndpoint).hostname
|
||||||
if (!this.lnbitswallet) return
|
if (this.network === 'Testnet') {
|
||||||
this.cancelListener = LNbits.events.onInvoicePaid(
|
hostname += '/testnet'
|
||||||
this.wallet,
|
}
|
||||||
payment => {
|
return hostname
|
||||||
this.checkInvoiceBalance()
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
checkBalances: async function () {
|
checkBalances: async function () {
|
||||||
if (this.charge.hasStaleBalance) return
|
if (!this.charge.payment_request && this.charge.hasOnchainStaleBalance)
|
||||||
|
return
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
|
@ -345,7 +354,7 @@
|
||||||
const {
|
const {
|
||||||
bitcoin: {addresses: addressesAPI}
|
bitcoin: {addresses: addressesAPI}
|
||||||
} = mempoolJS({
|
} = mempoolJS({
|
||||||
hostname: new URL(this.mempool_endpoint).hostname
|
hostname: new URL(this.mempoolEndpoint).hostname
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -353,7 +362,8 @@
|
||||||
address: this.charge.onchainaddress
|
address: this.charge.onchainaddress
|
||||||
})
|
})
|
||||||
const newBalance = utxos.reduce((t, u) => t + u.value, 0)
|
const newBalance = utxos.reduce((t, u) => t + u.value, 0)
|
||||||
this.charge.hasStaleBalance = this.charge.balance === newBalance
|
this.charge.hasOnchainStaleBalance =
|
||||||
|
this.charge.balance === newBalance
|
||||||
|
|
||||||
this.pendingFunds = utxos
|
this.pendingFunds = utxos
|
||||||
.filter(u => !u.status.confirmed)
|
.filter(u => !u.status.confirmed)
|
||||||
|
@ -388,10 +398,10 @@
|
||||||
const {
|
const {
|
||||||
bitcoin: {websocket}
|
bitcoin: {websocket}
|
||||||
} = mempoolJS({
|
} = mempoolJS({
|
||||||
hostname: new URL(this.mempool_endpoint).hostname
|
hostname: new URL(this.mempoolEndpoint).hostname
|
||||||
})
|
})
|
||||||
|
|
||||||
this.ws = new WebSocket('wss://mempool.space/api/v1/ws')
|
this.ws = new WebSocket(`wss://${this.mempoolHostname}/api/v1/ws`)
|
||||||
this.ws.addEventListener('open', x => {
|
this.ws.addEventListener('open', x => {
|
||||||
if (this.charge.onchainaddress) {
|
if (this.charge.onchainaddress) {
|
||||||
this.trackAddress(this.charge.onchainaddress)
|
this.trackAddress(this.charge.onchainaddress)
|
||||||
|
@ -428,13 +438,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: async function () {
|
created: async function () {
|
||||||
if (this.charge.lnbitswallet) this.payInvoice()
|
if (this.charge.payment_request) this.payInvoice()
|
||||||
else this.payOnchain()
|
else this.payOnchain()
|
||||||
await this.checkBalances()
|
|
||||||
|
|
||||||
// empty for onchain
|
await this.checkBalances()
|
||||||
this.wallet.inkey = '{{ wallet_inkey }}'
|
|
||||||
this.startPaymentNotifier()
|
|
||||||
|
|
||||||
if (!this.charge.paid) {
|
if (!this.charge.paid) {
|
||||||
this.loopRefresh()
|
this.loopRefresh()
|
||||||
|
|
|
@ -203,9 +203,14 @@
|
||||||
:href="props.row.webhook"
|
:href="props.row.webhook"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
style="color: unset; text-decoration: none"
|
style="color: unset; text-decoration: none"
|
||||||
>{{props.row.webhook || props.row.webhook}}</a
|
>{{props.row.webhook}}</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-4 q-pr-lg">
|
||||||
|
<q-badge v-if="props.row.webhook_message" color="blue">
|
||||||
|
{{props.row.webhook_message }}
|
||||||
|
</q-badge>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row items-center q-mt-md q-mb-lg">
|
<div class="row items-center q-mt-md q-mb-lg">
|
||||||
<div class="col-2 q-pr-lg">ID:</div>
|
<div class="col-2 q-pr-lg">ID:</div>
|
||||||
|
@ -409,10 +414,11 @@
|
||||||
balance: null,
|
balance: null,
|
||||||
walletLinks: [],
|
walletLinks: [],
|
||||||
chargeLinks: [],
|
chargeLinks: [],
|
||||||
onchainwallet: '',
|
onchainwallet: null,
|
||||||
rescanning: false,
|
rescanning: false,
|
||||||
mempool: {
|
mempool: {
|
||||||
endpoint: ''
|
endpoint: '',
|
||||||
|
network: 'Mainnet'
|
||||||
},
|
},
|
||||||
|
|
||||||
chargesTable: {
|
chargesTable: {
|
||||||
|
@ -505,6 +511,7 @@
|
||||||
methods: {
|
methods: {
|
||||||
cancelCharge: function (data) {
|
cancelCharge: function (data) {
|
||||||
this.formDialogCharge.data.description = ''
|
this.formDialogCharge.data.description = ''
|
||||||
|
this.formDialogCharge.data.onchain = false
|
||||||
this.formDialogCharge.data.onchainwallet = ''
|
this.formDialogCharge.data.onchainwallet = ''
|
||||||
this.formDialogCharge.data.lnbitswallet = ''
|
this.formDialogCharge.data.lnbitswallet = ''
|
||||||
this.formDialogCharge.data.time = null
|
this.formDialogCharge.data.time = null
|
||||||
|
@ -518,7 +525,7 @@
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request(
|
const {data} = await LNbits.api.request(
|
||||||
'GET',
|
'GET',
|
||||||
'/watchonly/api/v1/wallet',
|
`/watchonly/api/v1/wallet?network=${this.mempool.network}`,
|
||||||
this.g.user.wallets[0].inkey
|
this.g.user.wallets[0].inkey
|
||||||
)
|
)
|
||||||
this.walletLinks = data.map(w => ({
|
this.walletLinks = data.map(w => ({
|
||||||
|
@ -538,6 +545,7 @@
|
||||||
this.g.user.wallets[0].inkey
|
this.g.user.wallets[0].inkey
|
||||||
)
|
)
|
||||||
this.mempool.endpoint = data.mempool_endpoint
|
this.mempool.endpoint = data.mempool_endpoint
|
||||||
|
this.mempool.network = data.network || 'Mainnet'
|
||||||
const url = new URL(this.mempool.endpoint)
|
const url = new URL(this.mempool.endpoint)
|
||||||
this.mempool.hostname = url.hostname
|
this.mempool.hostname = url.hostname
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -577,7 +585,8 @@
|
||||||
const data = this.formDialogCharge.data
|
const data = this.formDialogCharge.data
|
||||||
data.amount = parseInt(data.amount)
|
data.amount = parseInt(data.amount)
|
||||||
data.time = parseInt(data.time)
|
data.time = parseInt(data.time)
|
||||||
data.onchainwallet = this.onchainwallet?.id
|
data.lnbitswallet = data.lnbits ? data.lnbitswallet : null
|
||||||
|
data.onchainwallet = data.onchain ? this.onchainwallet?.id : null
|
||||||
this.createCharge(wallet, data)
|
this.createCharge(wallet, data)
|
||||||
},
|
},
|
||||||
refreshActiveChargesBalance: async function () {
|
refreshActiveChargesBalance: async function () {
|
||||||
|
@ -695,8 +704,8 @@
|
||||||
},
|
},
|
||||||
created: async function () {
|
created: async function () {
|
||||||
await this.getCharges()
|
await this.getCharges()
|
||||||
await this.getWalletLinks()
|
|
||||||
await this.getWalletConfig()
|
await this.getWalletConfig()
|
||||||
|
await this.getWalletLinks()
|
||||||
setInterval(() => this.refreshActiveChargesBalance(), 10 * 2000)
|
setInterval(() => this.refreshActiveChargesBalance(), 10 * 2000)
|
||||||
await this.rescanOnchainAddresses()
|
await this.rescanOnchainAddresses()
|
||||||
setInterval(() => this.rescanOnchainAddresses(), 10 * 1000)
|
setInterval(() => this.rescanOnchainAddresses(), 10 * 1000)
|
||||||
|
|
|
@ -6,12 +6,12 @@ from starlette.exceptions import HTTPException
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
from lnbits.core.crud import get_wallet
|
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
from lnbits.decorators import check_user_exists
|
from lnbits.decorators import check_user_exists
|
||||||
|
from lnbits.extensions.satspay.helpers import public_charge
|
||||||
|
|
||||||
from . import satspay_ext, satspay_renderer
|
from . import satspay_ext, satspay_renderer
|
||||||
from .crud import get_charge, get_charge_config
|
from .crud import get_charge
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
@ -30,18 +30,13 @@ async def display(request: Request, charge_id: str):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Charge link does not exist."
|
||||||
)
|
)
|
||||||
wallet = await get_wallet(charge.lnbitswallet)
|
|
||||||
onchainwallet_config = await get_charge_config(charge_id)
|
|
||||||
inkey = wallet.inkey if wallet else None
|
|
||||||
mempool_endpoint = (
|
|
||||||
onchainwallet_config.mempool_endpoint if onchainwallet_config else None
|
|
||||||
)
|
|
||||||
return satspay_renderer().TemplateResponse(
|
return satspay_renderer().TemplateResponse(
|
||||||
"satspay/display.html",
|
"satspay/display.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"charge_data": charge.dict(),
|
"charge_data": public_charge(charge),
|
||||||
"wallet_inkey": inkey,
|
"mempool_endpoint": charge.config.mempool_endpoint,
|
||||||
"mempool_endpoint": mempool_endpoint,
|
"network": charge.config.network,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
import json
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
import httpx
|
|
||||||
from fastapi.params import Depends
|
from fastapi.params import Depends
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ from .crud import (
|
||||||
get_charges,
|
get_charges,
|
||||||
update_charge,
|
update_charge,
|
||||||
)
|
)
|
||||||
from .helpers import compact_charge
|
from .helpers import call_webhook, public_charge
|
||||||
from .models import CreateCharge
|
from .models import CreateCharge
|
||||||
|
|
||||||
#############################CHARGES##########################
|
#############################CHARGES##########################
|
||||||
|
@ -58,6 +58,7 @@ async def api_charges_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||||
**{"time_elapsed": charge.time_elapsed},
|
**{"time_elapsed": charge.time_elapsed},
|
||||||
**{"time_left": charge.time_left},
|
**{"time_left": charge.time_left},
|
||||||
**{"paid": charge.paid},
|
**{"paid": charge.paid},
|
||||||
|
**{"webhook_message": charge.config.webhook_message},
|
||||||
}
|
}
|
||||||
for charge in await get_charges(wallet.wallet.user)
|
for charge in await get_charges(wallet.wallet.user)
|
||||||
]
|
]
|
||||||
|
@ -119,19 +120,9 @@ async def api_charge_balance(charge_id):
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Charge does not exist."
|
status_code=HTTPStatus.NOT_FOUND, detail="Charge does not exist."
|
||||||
)
|
)
|
||||||
|
|
||||||
if charge.paid and charge.webhook:
|
if charge.must_call_webhook():
|
||||||
async with httpx.AsyncClient() as client:
|
resp = await call_webhook(charge)
|
||||||
try:
|
extra = {**charge.config.dict(), **resp}
|
||||||
r = await client.post(
|
await update_charge(charge_id=charge.id, extra=json.dumps(extra))
|
||||||
charge.webhook,
|
|
||||||
json=compact_charge(charge),
|
return {**public_charge(charge)}
|
||||||
timeout=40,
|
|
||||||
)
|
|
||||||
except AssertionError:
|
|
||||||
charge.webhook = None
|
|
||||||
return {
|
|
||||||
**compact_charge(charge),
|
|
||||||
**{"time_elapsed": charge.time_elapsed},
|
|
||||||
**{"time_left": charge.time_left},
|
|
||||||
**{"paid": charge.paid},
|
|
||||||
}
|
|
||||||
|
|
|
@ -124,7 +124,7 @@ async def check_pending_payments():
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
async with db.connect() as conn:
|
async with db.connect() as conn:
|
||||||
logger.debug(
|
logger.info(
|
||||||
f"Task: checking all pending payments (incoming={incoming}, outgoing={outgoing}) of last 15 days"
|
f"Task: checking all pending payments (incoming={incoming}, outgoing={outgoing}) of last 15 days"
|
||||||
)
|
)
|
||||||
start_time: float = time.time()
|
start_time: float = time.time()
|
||||||
|
@ -140,15 +140,15 @@ async def check_pending_payments():
|
||||||
for payment in pending_payments:
|
for payment in pending_payments:
|
||||||
await payment.check_status(conn=conn)
|
await payment.check_status(conn=conn)
|
||||||
|
|
||||||
logger.debug(
|
logger.info(
|
||||||
f"Task: pending check finished for {len(pending_payments)} payments (took {time.time() - start_time:0.3f} s)"
|
f"Task: pending check finished for {len(pending_payments)} payments (took {time.time() - start_time:0.3f} s)"
|
||||||
)
|
)
|
||||||
# we delete expired invoices once upon the first pending check
|
# we delete expired invoices once upon the first pending check
|
||||||
if incoming:
|
if incoming:
|
||||||
logger.debug("Task: deleting all expired invoices")
|
logger.info("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.debug(
|
logger.info(
|
||||||
f"Task: expired invoice deletion finished (took {time.time() - start_time:0.3f} s)"
|
f"Task: expired invoice deletion finished (took {time.time() - start_time:0.3f} s)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user