Refactor MyExtension to DCA Admin: Update extension name and description in config.json, remove legacy MyExtension CRUD operations and related API endpoints, and adjust router tags. Clean up unused files and methods to streamline the codebase for DCA administration functionality.
Refactor DCA Admin page endpoints: Update description, remove unused CRUD operations and API endpoints related to MyExtension, and streamline the code for improved clarity and functionality. Remove QR Code dialog from MyExtension index.html: streamline the UI by eliminating unused dialog components, enhancing code clarity and maintainability.
This commit is contained in:
parent
edca91579c
commit
75dd03b15a
13 changed files with 32 additions and 971 deletions
|
|
@ -8,7 +8,6 @@ from .crud import db
|
|||
from .tasks import wait_for_paid_invoices, hourly_transaction_polling
|
||||
from .views import myextension_generic_router
|
||||
from .views_api import myextension_api_router
|
||||
from .views_lnurl import myextension_lnurl_router
|
||||
|
||||
logger.debug(
|
||||
"This logged message is from myextension/__init__.py, you can debug in your "
|
||||
|
|
@ -16,10 +15,9 @@ logger.debug(
|
|||
)
|
||||
|
||||
|
||||
myextension_ext: APIRouter = APIRouter(prefix="/myextension", tags=["MyExtension"])
|
||||
myextension_ext: APIRouter = APIRouter(prefix="/myextension", tags=["DCA Admin"])
|
||||
myextension_ext.include_router(myextension_generic_router)
|
||||
myextension_ext.include_router(myextension_api_router)
|
||||
myextension_ext.include_router(myextension_lnurl_router)
|
||||
|
||||
myextension_static_files = [
|
||||
{
|
||||
|
|
|
|||
44
config.json
44
config.json
|
|
@ -1,38 +1,32 @@
|
|||
{
|
||||
"name": "MyExtension",
|
||||
"short_description": "Minimal extension to build on",
|
||||
"name": "DCA Admin",
|
||||
"short_description": "Dollar Cost Averaging administration for Lamassu ATM integration",
|
||||
"tile": "/myextension/static/image/myextension.png",
|
||||
"min_lnbits_version": "1.0.0",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Alan Bits",
|
||||
"uri": "https://github.com/alanbits",
|
||||
"role": "Lead dev"
|
||||
"name": "Atitlan Community",
|
||||
"uri": "https://atitlan.io",
|
||||
"role": "R&D Venue"
|
||||
},
|
||||
{
|
||||
"name": "Ben Arc",
|
||||
"uri": "https://github.com/arcbtc",
|
||||
"role": "Dev"
|
||||
"name": "AtitlanIO",
|
||||
"uri": "https://atitlan.io",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "LNbits community",
|
||||
"uri": "https://t.me/lnbits",
|
||||
"role": "Emotional support"
|
||||
"name": "LNbits",
|
||||
"uri": "https://github.com/lnbits",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Claude",
|
||||
"uri": "https://claude.ai",
|
||||
"role": "Code Writing Agent"
|
||||
}
|
||||
],
|
||||
"images": [
|
||||
{
|
||||
"uri": "https://raw.githubusercontent.com/lnbits/myextension/main/static/image/1.png",
|
||||
"link": "https://www.youtube.com/embed/SkkIwO_X4i4?si=9JJh1Fc6GfHDZK6b"
|
||||
},
|
||||
{
|
||||
"uri": "https://raw.githubusercontent.com/lnbits/myextension/main/static/image/2.png"
|
||||
},
|
||||
{
|
||||
"uri": "https://raw.githubusercontent.com/lnbits/myextension/main/static/image/3.png"
|
||||
}
|
||||
],
|
||||
"description_md": "https://raw.githubusercontent.com/lnbits/myextension/main/description.md",
|
||||
"terms_and_conditions_md": "https://raw.githubusercontent.com/lnbits/myextension/main/toc.md",
|
||||
"images": [],
|
||||
"description_md": "/myextension/description.md",
|
||||
"terms_and_conditions_md": "/myextension/toc.md",
|
||||
"license": "MIT"
|
||||
}
|
||||
|
|
|
|||
36
crud.py
36
crud.py
|
|
@ -7,7 +7,6 @@ from lnbits.db import Database
|
|||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from .models import (
|
||||
CreateMyExtensionData, MyExtension,
|
||||
CreateDcaClientData, DcaClient, UpdateDcaClientData,
|
||||
CreateDepositData, DcaDeposit, UpdateDepositStatusData,
|
||||
CreateDcaPaymentData, DcaPayment,
|
||||
|
|
@ -19,41 +18,6 @@ from .models import (
|
|||
db = Database("ext_myextension")
|
||||
|
||||
|
||||
async def create_myextension(data: CreateMyExtensionData) -> MyExtension:
|
||||
data.id = urlsafe_short_hash()
|
||||
await db.insert("myextension.maintable", data)
|
||||
return MyExtension(**data.dict())
|
||||
|
||||
|
||||
async def get_myextension(myextension_id: str) -> Optional[MyExtension]:
|
||||
return await db.fetchone(
|
||||
"SELECT * FROM myextension.maintable WHERE id = :id",
|
||||
{"id": myextension_id},
|
||||
MyExtension,
|
||||
)
|
||||
|
||||
|
||||
async def get_myextensions(wallet_ids: Union[str, List[str]]) -> List[MyExtension]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
q = ",".join([f"'{w}'" for w in wallet_ids])
|
||||
return await db.fetchall(
|
||||
f"SELECT * FROM myextension.maintable WHERE wallet IN ({q}) ORDER BY id",
|
||||
model=MyExtension,
|
||||
)
|
||||
|
||||
|
||||
async def update_myextension(data: CreateMyExtensionData) -> MyExtension:
|
||||
await db.update("myextension.maintable", data)
|
||||
return MyExtension(**data.dict())
|
||||
|
||||
|
||||
async def delete_myextension(myextension_id: str) -> None:
|
||||
await db.execute(
|
||||
"DELETE FROM myextension.maintable WHERE id = :id", {"id": myextension_id}
|
||||
)
|
||||
|
||||
|
||||
# DCA Client CRUD Operations
|
||||
async def create_dca_client(data: CreateDcaClientData) -> DcaClient:
|
||||
client_id = urlsafe_short_hash()
|
||||
|
|
|
|||
17
helpers.py
17
helpers.py
|
|
@ -1,17 +0,0 @@
|
|||
# Description: A place for helper functions.
|
||||
|
||||
from fastapi import Request
|
||||
from lnurl.core import encode as lnurl_encode
|
||||
|
||||
# The lnurler function is used to generate the lnurlpay and lnurlwithdraw links
|
||||
# from the lnurl api endpoints in views_lnurl.py.
|
||||
# It needs the Request object to know the url of the LNbits.
|
||||
# Lnurler is used in views_api.py
|
||||
|
||||
|
||||
def lnurler(myex_id: str, route_name: str, req: Request) -> str:
|
||||
url = req.url_for(route_name, myextension_id=myex_id)
|
||||
url_str = str(url)
|
||||
if url.netloc.endswith(".onion"):
|
||||
url_str = url_str.replace("https://", "http://")
|
||||
return str(lnurl_encode(url_str))
|
||||
25
models.py
25
models.py
|
|
@ -207,28 +207,3 @@ class UpdateLamassuConfigData(BaseModel):
|
|||
ssh_private_key: Optional[str] = None
|
||||
|
||||
|
||||
# Legacy models (keep for backward compatibility during transition)
|
||||
class CreateMyExtensionData(BaseModel):
|
||||
id: Optional[str] = ""
|
||||
name: str
|
||||
lnurlpayamount: int
|
||||
lnurlwithdrawamount: int
|
||||
wallet: str
|
||||
total: int = 0
|
||||
|
||||
|
||||
class MyExtension(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
lnurlpayamount: int
|
||||
lnurlwithdrawamount: int
|
||||
wallet: str
|
||||
total: int
|
||||
lnurlpay: Optional[str] = ""
|
||||
lnurlwithdraw: Optional[str] = ""
|
||||
|
||||
|
||||
class CreatePayment(BaseModel):
|
||||
myextension_id: str
|
||||
amount: int
|
||||
memo: str
|
||||
|
|
|
|||
|
|
@ -119,32 +119,7 @@ window.app = Vue.createApp({
|
|||
currencyOptions: [
|
||||
{ label: 'GTQ', value: 'GTQ' },
|
||||
{ label: 'USD', value: 'USD' }
|
||||
],
|
||||
|
||||
// Legacy data (keep for backward compatibility)
|
||||
invoiceAmount: 10,
|
||||
qrValue: 'lnurlpay',
|
||||
myex: [],
|
||||
myexTable: {
|
||||
columns: [
|
||||
{ name: 'id', align: 'left', label: 'ID', field: 'id' },
|
||||
{ name: 'name', align: 'left', label: 'Name', field: 'name' },
|
||||
{ name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet' },
|
||||
{ name: 'total', align: 'left', label: 'Total sent/received', field: 'total' }
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
formDialog: {
|
||||
show: false,
|
||||
data: {},
|
||||
advanced: {}
|
||||
},
|
||||
urlDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -666,194 +641,6 @@ window.app = Vue.createApp({
|
|||
}
|
||||
},
|
||||
|
||||
// Legacy Methods (keep for backward compatibility)
|
||||
async closeFormDialog() {
|
||||
this.formDialog.show = false
|
||||
this.formDialog.data = {}
|
||||
},
|
||||
async getMyExtensions() {
|
||||
await LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/myextension/api/v1/myex',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(response => {
|
||||
this.myex = response.data
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
async sendMyExtensionData() {
|
||||
const data = {
|
||||
name: this.formDialog.data.name,
|
||||
lnurlwithdrawamount: this.formDialog.data.lnurlwithdrawamount,
|
||||
lnurlpayamount: this.formDialog.data.lnurlpayamount
|
||||
}
|
||||
const wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: this.formDialog.data.wallet
|
||||
})
|
||||
if (this.formDialog.data.id) {
|
||||
data.id = this.formDialog.data.id
|
||||
data.total = this.formDialog.data.total
|
||||
await this.updateMyExtension(wallet, data)
|
||||
} else {
|
||||
await this.createMyExtension(wallet, data)
|
||||
}
|
||||
},
|
||||
|
||||
async updateMyExtensionForm(tempId) {
|
||||
const myextension = _.findWhere(this.myex, { id: tempId })
|
||||
this.formDialog.data = {
|
||||
...myextension
|
||||
}
|
||||
if (this.formDialog.data.tip_wallet != '') {
|
||||
this.formDialog.advanced.tips = true
|
||||
}
|
||||
if (this.formDialog.data.withdrawlimit >= 1) {
|
||||
this.formDialog.advanced.otc = true
|
||||
}
|
||||
this.formDialog.show = true
|
||||
},
|
||||
async createMyExtension(wallet, data) {
|
||||
data.wallet = wallet.id
|
||||
await LNbits.api
|
||||
.request('POST', '/myextension/api/v1/myex', wallet.adminkey, data)
|
||||
.then(response => {
|
||||
this.myex.push(response.data)
|
||||
this.closeFormDialog()
|
||||
})
|
||||
.catch(error => {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
|
||||
async updateMyExtension(wallet, data) {
|
||||
data.wallet = wallet.id
|
||||
await LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
`/myextension/api/v1/myex/${data.id}`,
|
||||
wallet.adminkey,
|
||||
data
|
||||
)
|
||||
.then(response => {
|
||||
this.myex = _.reject(this.myex, obj => obj.id == data.id)
|
||||
this.myex.push(response.data)
|
||||
this.closeFormDialog()
|
||||
})
|
||||
.catch(error => {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
async deleteMyExtension(tempId) {
|
||||
var myextension = _.findWhere(this.myex, { id: tempId })
|
||||
const wallet = _.findWhere(this.g.user.wallets, {
|
||||
id: myextension.wallet
|
||||
})
|
||||
await LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this MyExtension?')
|
||||
.onOk(function () {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/myextension/api/v1/myex/' + tempId,
|
||||
wallet.adminkey
|
||||
)
|
||||
.then(() => {
|
||||
this.myex = _.reject(this.myex, function (obj) {
|
||||
return obj.id === myextension.id
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
async exportCSV() {
|
||||
await LNbits.utils.exportCSV(this.myexTable.columns, this.myex)
|
||||
},
|
||||
async itemsArray(tempId) {
|
||||
const myextension = _.findWhere(this.myex, { id: tempId })
|
||||
return [...myextension.itemsMap.values()]
|
||||
},
|
||||
async openformDialog(id) {
|
||||
const [tempId, itemId] = id.split(':')
|
||||
const myextension = _.findWhere(this.myex, { id: tempId })
|
||||
if (itemId) {
|
||||
const item = myextension.itemsMap.get(id)
|
||||
this.formDialog.data = {
|
||||
...item,
|
||||
myextension: tempId
|
||||
}
|
||||
} else {
|
||||
this.formDialog.data.myextension = tempId
|
||||
}
|
||||
this.formDialog.data.currency = myextension.currency
|
||||
this.formDialog.show = true
|
||||
},
|
||||
async openUrlDialog(tempid) {
|
||||
this.urlDialog.data = _.findWhere(this.myex, { id: tempid })
|
||||
this.qrValue = this.urlDialog.data.lnurlpay
|
||||
|
||||
// Connecting to our websocket fired in tasks.py
|
||||
this.connectWebocket(this.urlDialog.data.id)
|
||||
|
||||
this.urlDialog.show = true
|
||||
},
|
||||
async closeformDialog() {
|
||||
this.formDialog.show = false
|
||||
this.formDialog.data = {}
|
||||
},
|
||||
async createInvoice(tempid) {
|
||||
///////////////////////////////////////////////////
|
||||
///Simple call to the api to create an invoice/////
|
||||
///////////////////////////////////////////////////
|
||||
myex = _.findWhere(this.myex, { id: tempid })
|
||||
const wallet = _.findWhere(this.g.user.wallets, { id: myex.wallet })
|
||||
const data = {
|
||||
myextension_id: tempid,
|
||||
amount: this.invoiceAmount,
|
||||
memo: 'MyExtension - ' + myex.name
|
||||
}
|
||||
await LNbits.api
|
||||
.request('POST', `/myextension/api/v1/myex/payment`, wallet.inkey, data)
|
||||
.then(response => {
|
||||
this.qrValue = response.data.payment_request
|
||||
this.connectWebocket(wallet.inkey)
|
||||
})
|
||||
.catch(error => {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
connectWebocket(myextension_id) {
|
||||
//////////////////////////////////////////////////
|
||||
///wait for pay action to happen and do a thing////
|
||||
///////////////////////////////////////////////////
|
||||
if (location.protocol !== 'http:') {
|
||||
localUrl =
|
||||
'wss://' +
|
||||
document.domain +
|
||||
':' +
|
||||
location.port +
|
||||
'/api/v1/ws/' +
|
||||
myextension_id
|
||||
} else {
|
||||
localUrl =
|
||||
'ws://' +
|
||||
document.domain +
|
||||
':' +
|
||||
location.port +
|
||||
'/api/v1/ws/' +
|
||||
myextension_id
|
||||
}
|
||||
this.connection = new WebSocket(localUrl)
|
||||
this.connection.onmessage = () => {
|
||||
this.urlDialog.show = false
|
||||
}
|
||||
}
|
||||
},
|
||||
///////////////////////////////////////////////////
|
||||
//////LIFECYCLE FUNCTIONS RUNNING ON PAGE LOAD/////
|
||||
|
|
@ -866,9 +653,6 @@ window.app = Vue.createApp({
|
|||
this.getDeposits(),
|
||||
this.getLamassuTransactions()
|
||||
])
|
||||
|
||||
// Legacy data loading
|
||||
await this.getMyExtensions()
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
|
|||
41
tasks.py
41
tasks.py
|
|
@ -6,8 +6,6 @@ from lnbits.core.services import websocket_updater
|
|||
from lnbits.tasks import register_invoice_listener
|
||||
from loguru import logger
|
||||
|
||||
from .crud import get_myextension, update_myextension
|
||||
from .models import CreateMyExtensionData
|
||||
from .transaction_processor import poll_lamassu_transactions
|
||||
|
||||
#######################################
|
||||
|
|
@ -18,6 +16,7 @@ from .transaction_processor import poll_lamassu_transactions
|
|||
|
||||
|
||||
async def wait_for_paid_invoices():
|
||||
"""Invoice listener for DCA-related payments"""
|
||||
invoice_queue = asyncio.Queue()
|
||||
register_invoice_listener(invoice_queue, "ext_myextension")
|
||||
while True:
|
||||
|
|
@ -44,35 +43,11 @@ async def hourly_transaction_polling():
|
|||
await asyncio.sleep(300)
|
||||
|
||||
|
||||
# Do somethhing when an invoice related top this extension is paid
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if payment.extra.get("tag") != "MyExtension":
|
||||
return
|
||||
|
||||
myextension_id = payment.extra.get("myextensionId")
|
||||
assert myextension_id, "myextensionId not set in invoice"
|
||||
myextension = await get_myextension(myextension_id)
|
||||
assert myextension, "MyExtension does not exist"
|
||||
|
||||
# update something in the db
|
||||
if payment.extra.get("lnurlwithdraw"):
|
||||
total = myextension.total - payment.amount
|
||||
else:
|
||||
total = myextension.total + payment.amount
|
||||
|
||||
myextension.total = total
|
||||
await update_myextension(CreateMyExtensionData(**myextension.dict()))
|
||||
|
||||
# here we could send some data to a websocket on
|
||||
# wss://<your-lnbits>/api/v1/ws/<myextension_id> and then listen to it on
|
||||
|
||||
some_payment_data = {
|
||||
"name": myextension.name,
|
||||
"amount": payment.amount,
|
||||
"fee": payment.fee,
|
||||
"checking_id": payment.checking_id,
|
||||
}
|
||||
|
||||
await websocket_updater(myextension_id, str(some_payment_data))
|
||||
"""Handle DCA-related invoice payments"""
|
||||
# DCA payments are handled internally by the transaction processor
|
||||
# This function can be extended if needed for additional payment processing
|
||||
if payment.extra.get("tag") in ["dca_distribution", "dca_commission"]:
|
||||
logger.info(f"DCA payment processed: {payment.checking_id} - {payment.amount} sats")
|
||||
# Could add websocket notifications here if needed
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
<q-expansion-item group="extras" icon="info" label="More info">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<p>Some more info about my excellent extension.</p>
|
||||
<small
|
||||
>Created by
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://github.com/benarc"
|
||||
target="_blank"
|
||||
>Ben Arc</a
|
||||
>.</small
|
||||
>
|
||||
<small
|
||||
>Repo
|
||||
<a
|
||||
class="text-secondary"
|
||||
href="https://github.com/lnbits/myextension"
|
||||
target="_blank"
|
||||
>MyExtension</a
|
||||
>.</small
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
|
@ -265,93 +265,6 @@
|
|||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
<div class="col">
|
||||
<h5 class="text-subtitle1 q-my-none">MyExtension</h5>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:rows="myex"
|
||||
row-key="id"
|
||||
:columns="myexTable.columns"
|
||||
v-model:pagination="myexTable.pagination"
|
||||
>
|
||||
<myextension v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
${ col.label }
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</myextension>
|
||||
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<div v-if="col.field == 'total'">${ col.value / 1000} sats</div>
|
||||
<div v-else>${ col.value }</div>
|
||||
</q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="sm"
|
||||
icon="qr_code"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
class="q-mr-sm"
|
||||
@click="openUrlDialog(props.row.id)"
|
||||
></q-btn
|
||||
></q-td>
|
||||
<q-td auto-width>
|
||||
<q-btn
|
||||
unelevated
|
||||
dense
|
||||
size="sm"
|
||||
icon="launch"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
type="a"
|
||||
:href="props.row.id"
|
||||
target="_blank"
|
||||
><q-tooltip>Open public page</q-tooltip></q-btn
|
||||
></q-td
|
||||
>
|
||||
|
||||
<q-td>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="updateMyExtensionForm(props.row.id)"
|
||||
icon="edit"
|
||||
color="light-blue"
|
||||
>
|
||||
<q-tooltip> Edit copilot </q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
|
||||
<q-td>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="xs"
|
||||
@click="deleteMyExtension(props.row.id)"
|
||||
icon="cancel"
|
||||
color="pink"
|
||||
>
|
||||
<q-tooltip> Delete copilot </q-tooltip>
|
||||
</q-btn>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||
|
|
@ -854,55 +767,5 @@
|
|||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!--/////////////////////////////////////////////////-->
|
||||
<!--//////////////QR Code DIALOG/////////////////////-->
|
||||
<!--/////////////////////////////////////////////////-->
|
||||
|
||||
<q-dialog v-model="urlDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
||||
<lnbits-qrcode :value="qrValue"></lnbits-qrcode>
|
||||
</q-responsive>
|
||||
<center><q-btn label="copy" @click="copyText(qrValue)"></q-btn></center>
|
||||
|
||||
<q-separator></q-separator>
|
||||
|
||||
<div class="row justify-start q-mt-lg">
|
||||
<div class="col col-md-auto">
|
||||
<q-btn
|
||||
outline
|
||||
style="color: primmary"
|
||||
@click="qrValue = urlDialog.data.lnurlpay"
|
||||
>lnurlpay</q-btn
|
||||
>
|
||||
</div>
|
||||
<div class="col col-md-auto">
|
||||
<q-btn
|
||||
outline
|
||||
style="color: primmary"
|
||||
@click="qrValue = urlDialog.data.lnurlwithdraw"
|
||||
>lnurlwithdraw</q-btn
|
||||
>
|
||||
</div>
|
||||
<div class="col q-pl-md">
|
||||
<q-input filled bottom-slots dense v-model="invoiceAmount">
|
||||
<template v-slot:append>
|
||||
<q-btn
|
||||
round
|
||||
@click="createInvoice(urlDialog.data.id)"
|
||||
color="primary"
|
||||
flat
|
||||
icon="add_circle"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:hint> Create an invoice </template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
<!--/////////////////////////////////////////////////-->
|
||||
<!--////////////////USER FACING PAGE/////////////////-->
|
||||
<!--/////////////////////////////////////////////////-->
|
||||
|
||||
{% extends "public.html" %} {% block page %}
|
||||
<div class="row q-col-gutter-md justify-center">
|
||||
<div class="col-12 col-sm-6 col-md-5 col-lg-4">
|
||||
<q-card class="q-pa-lg">
|
||||
<q-card-section class="q-pa-none">
|
||||
<div class="text-center">
|
||||
<a class="text-secondary" href="lightning:{{ lnurl }}">
|
||||
<q-responsive :ratio="1" class="q-mx-md">
|
||||
<lnbits-qrcode
|
||||
:value="qrValue"
|
||||
:options="{width: 800}"
|
||||
class="rounded-borders"
|
||||
></lnbits-qrcode>
|
||||
</q-responsive>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn outline color="grey" @click="copyText(qrValue)"
|
||||
>Copy LNURL
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-md-5 col-lg-4 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-mb-sm q-mt-none">Public page</h6>
|
||||
<p class="q-my-none">
|
||||
Most extensions have a public page that can be shared (this page will
|
||||
still be accessible even if you have restricted access to your LNbits
|
||||
install).
|
||||
<br /><br />
|
||||
In this example when a user pays the LNURLpay it triggers an event via
|
||||
a websocket waiting for the payment, which you can subscribe to
|
||||
somewhere using wss://{your-lnbits}/api/v1/ws/{the-id-of-this-record}
|
||||
</p></q-card-section
|
||||
>
|
||||
<q-card-section class="q-pa-none">
|
||||
<q-separator></q-separator>
|
||||
<q-list> </q-list>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %} {% block scripts %}
|
||||
<script>
|
||||
window.app = Vue.createApp({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
qrValue: '{{lnurlpay}}',
|
||||
myExtensionID: '{{myextension_id}}'
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
// Will trigger payment reaction when payment received, sent from tasks.py
|
||||
eventReactionWebocket(this.myExtensionID)
|
||||
},
|
||||
methods: {}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
82
views.py
82
views.py
|
|
@ -1,16 +1,10 @@
|
|||
# Description: Add your page endpoints here.
|
||||
# Description: DCA Admin page endpoints.
|
||||
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
from lnbits.helpers import template_renderer
|
||||
from lnbits.settings import settings
|
||||
|
||||
from .crud import get_myextension
|
||||
from .helpers import lnurler
|
||||
|
||||
myextension_generic_router = APIRouter()
|
||||
|
||||
|
|
@ -19,79 +13,9 @@ def myextension_renderer():
|
|||
return template_renderer(["myextension/templates"])
|
||||
|
||||
|
||||
#######################################
|
||||
##### ADD YOUR PAGE ENDPOINTS HERE ####
|
||||
#######################################
|
||||
|
||||
|
||||
# Backend admin page
|
||||
|
||||
|
||||
# DCA Admin page
|
||||
@myextension_generic_router.get("/", response_class=HTMLResponse)
|
||||
async def index(req: Request, user: User = Depends(check_user_exists)):
|
||||
return myextension_renderer().TemplateResponse(
|
||||
"myextension/index.html", {"request": req, "user": user.json()}
|
||||
)
|
||||
|
||||
|
||||
# Frontend shareable page
|
||||
|
||||
|
||||
@myextension_generic_router.get("/{myextension_id}")
|
||||
async def myextension(req: Request, myextension_id):
|
||||
myex = await get_myextension(myextension_id)
|
||||
if not myex:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
|
||||
)
|
||||
return myextension_renderer().TemplateResponse(
|
||||
"myextension/myextension.html",
|
||||
{
|
||||
"request": req,
|
||||
"myextension_id": myextension_id,
|
||||
"lnurlpay": lnurler(myex.id, "myextension.api_lnurl_pay", req),
|
||||
"web_manifest": f"/myextension/manifest/{myextension_id}.webmanifest",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# Manifest for public page, customise or remove manifest completely
|
||||
|
||||
|
||||
@myextension_generic_router.get("/manifest/{myextension_id}.webmanifest")
|
||||
async def manifest(myextension_id: str):
|
||||
myextension = await get_myextension(myextension_id)
|
||||
if not myextension:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
|
||||
)
|
||||
|
||||
return {
|
||||
"short_name": settings.lnbits_site_title,
|
||||
"name": myextension.name + " - " + settings.lnbits_site_title,
|
||||
"icons": [
|
||||
{
|
||||
"src": (
|
||||
settings.lnbits_custom_logo
|
||||
if settings.lnbits_custom_logo
|
||||
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png"
|
||||
),
|
||||
"type": "image/png",
|
||||
"sizes": "900x900",
|
||||
}
|
||||
],
|
||||
"start_url": "/myextension/" + myextension_id,
|
||||
"background_color": "#1F2234",
|
||||
"description": "Minimal extension to build on",
|
||||
"display": "standalone",
|
||||
"scope": "/myextension/" + myextension_id,
|
||||
"theme_color": "#1F2234",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": myextension.name + " - " + settings.lnbits_site_title,
|
||||
"short_name": myextension.name,
|
||||
"description": myextension.name + " - " + settings.lnbits_site_title,
|
||||
"url": "/myextension/" + myextension_id,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
|
|
|||
160
views_api.py
160
views_api.py
|
|
@ -11,11 +11,6 @@ from lnbits.decorators import require_admin_key, require_invoice_key
|
|||
from starlette.exceptions import HTTPException
|
||||
|
||||
from .crud import (
|
||||
create_myextension,
|
||||
delete_myextension,
|
||||
get_myextension,
|
||||
get_myextensions,
|
||||
update_myextension,
|
||||
# DCA CRUD operations
|
||||
create_dca_client,
|
||||
get_dca_clients,
|
||||
|
|
@ -39,9 +34,7 @@ from .crud import (
|
|||
get_all_lamassu_transactions,
|
||||
get_lamassu_transaction
|
||||
)
|
||||
from .helpers import lnurler
|
||||
from .models import (
|
||||
CreateMyExtensionData, CreatePayment, MyExtension,
|
||||
# DCA models
|
||||
CreateDcaClientData, DcaClient, UpdateDcaClientData,
|
||||
CreateDepositData, DcaDeposit, UpdateDepositStatusData,
|
||||
|
|
@ -52,159 +45,6 @@ from .models import (
|
|||
|
||||
myextension_api_router = APIRouter()
|
||||
|
||||
# Note: we add the lnurl params to returns so the links
|
||||
# are generated in the MyExtension model in models.py
|
||||
|
||||
## Get all the records belonging to the user
|
||||
|
||||
|
||||
@myextension_api_router.get("/api/v1/myex")
|
||||
async def api_myextensions(
|
||||
req: Request, # Withoutthe lnurl stuff this wouldnt be needed
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
) -> list[MyExtension]:
|
||||
wallet_ids = [wallet.wallet.id]
|
||||
user = await get_user(wallet.wallet.user)
|
||||
wallet_ids = user.wallet_ids if user else []
|
||||
myextensions = await get_myextensions(wallet_ids)
|
||||
|
||||
# Populate lnurlpay and lnurlwithdraw for each instance.
|
||||
# Without the lnurl stuff this wouldnt be needed.
|
||||
for myex in myextensions:
|
||||
myex.lnurlpay = lnurler(myex.id, "myextension.api_lnurl_pay", req)
|
||||
myex.lnurlwithdraw = lnurler(myex.id, "myextension.api_lnurl_withdraw", req)
|
||||
|
||||
return myextensions
|
||||
|
||||
|
||||
## Get a single record
|
||||
|
||||
|
||||
@myextension_api_router.get(
|
||||
"/api/v1/myex/{myextension_id}",
|
||||
dependencies=[Depends(require_invoice_key)],
|
||||
)
|
||||
async def api_myextension(myextension_id: str, req: Request) -> MyExtension:
|
||||
myex = await get_myextension(myextension_id)
|
||||
if not myex:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
|
||||
)
|
||||
# Populate lnurlpay and lnurlwithdraw.
|
||||
# Without the lnurl stuff this wouldnt be needed.
|
||||
myex.lnurlpay = lnurler(myex.id, "myextension.api_lnurl_pay", req)
|
||||
myex.lnurlwithdraw = lnurler(myex.id, "myextension.api_lnurl_withdraw", req)
|
||||
|
||||
return myex
|
||||
|
||||
|
||||
## Create a new record
|
||||
|
||||
|
||||
@myextension_api_router.post("/api/v1/myex", status_code=HTTPStatus.CREATED)
|
||||
async def api_myextension_create(
|
||||
req: Request, # Withoutthe lnurl stuff this wouldnt be needed
|
||||
data: CreateMyExtensionData,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> MyExtension:
|
||||
myex = await create_myextension(data)
|
||||
|
||||
# Populate lnurlpay and lnurlwithdraw.
|
||||
# Withoutthe lnurl stuff this wouldnt be needed.
|
||||
myex.lnurlpay = lnurler(myex.id, "myextension.api_lnurl_pay", req)
|
||||
myex.lnurlwithdraw = lnurler(myex.id, "myextension.api_lnurl_withdraw", req)
|
||||
|
||||
return myex
|
||||
|
||||
|
||||
## update a record
|
||||
|
||||
|
||||
@myextension_api_router.put("/api/v1/myex/{myextension_id}")
|
||||
async def api_myextension_update(
|
||||
req: Request, # Withoutthe lnurl stuff this wouldnt be needed
|
||||
data: CreateMyExtensionData,
|
||||
myextension_id: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> MyExtension:
|
||||
myex = await get_myextension(myextension_id)
|
||||
if not myex:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
|
||||
)
|
||||
|
||||
if wallet.wallet.id != myex.wallet:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension."
|
||||
)
|
||||
|
||||
for key, value in data.dict().items():
|
||||
setattr(myex, key, value)
|
||||
|
||||
myex = await update_myextension(data)
|
||||
|
||||
# Populate lnurlpay and lnurlwithdraw.
|
||||
# Without the lnurl stuff this wouldnt be needed.
|
||||
myex.lnurlpay = lnurler(myex.id, "myextension.api_lnurl_pay", req)
|
||||
myex.lnurlwithdraw = lnurler(myex.id, "myextension.api_lnurl_withdraw", req)
|
||||
|
||||
return myex
|
||||
|
||||
|
||||
## Delete a record
|
||||
|
||||
|
||||
@myextension_api_router.delete("/api/v1/myex/{myextension_id}")
|
||||
async def api_myextension_delete(
|
||||
myextension_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
myex = await get_myextension(myextension_id)
|
||||
|
||||
if not myex:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
|
||||
)
|
||||
|
||||
if myex.wallet != wallet.wallet.id:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension."
|
||||
)
|
||||
|
||||
await delete_myextension(myextension_id)
|
||||
return
|
||||
|
||||
|
||||
# ANY OTHER ENDPOINTS YOU NEED
|
||||
|
||||
## This endpoint creates a payment
|
||||
|
||||
|
||||
@myextension_api_router.post("/api/v1/myex/payment", status_code=HTTPStatus.CREATED)
|
||||
async def api_myextension_create_invoice(data: CreatePayment) -> dict:
|
||||
myextension = await get_myextension(data.myextension_id)
|
||||
|
||||
if not myextension:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
|
||||
)
|
||||
|
||||
# we create a payment and add some tags,
|
||||
# so tasks.py can grab the payment once its paid
|
||||
|
||||
payment = await create_invoice(
|
||||
wallet_id=myextension.wallet,
|
||||
amount=data.amount,
|
||||
memo=(
|
||||
f"{data.memo} to {myextension.name}" if data.memo else f"{myextension.name}"
|
||||
),
|
||||
extra={
|
||||
"tag": "myextension",
|
||||
"amount": data.amount,
|
||||
},
|
||||
)
|
||||
|
||||
return {"payment_hash": payment.payment_hash, "payment_request": payment.bolt11}
|
||||
|
||||
|
||||
###################################################
|
||||
################ DCA API ENDPOINTS ################
|
||||
|
|
|
|||
146
views_lnurl.py
146
views_lnurl.py
|
|
@ -1,146 +0,0 @@
|
|||
# Description: Extensions that use LNURL usually have a few endpoints in views_lnurl.py.
|
||||
|
||||
from http import HTTPStatus
|
||||
from typing import Optional
|
||||
|
||||
import shortuuid
|
||||
from fastapi import APIRouter, Query, Request
|
||||
from lnbits.core.services import create_invoice, pay_invoice
|
||||
from loguru import logger
|
||||
|
||||
from .crud import get_myextension
|
||||
|
||||
#################################################
|
||||
########### A very simple LNURLpay ##############
|
||||
# https://github.com/lnurl/luds/blob/luds/06.md #
|
||||
#################################################
|
||||
#################################################
|
||||
|
||||
myextension_lnurl_router = APIRouter()
|
||||
|
||||
|
||||
@myextension_lnurl_router.get(
|
||||
"/api/v1/lnurl/pay/{myextension_id}",
|
||||
status_code=HTTPStatus.OK,
|
||||
name="myextension.api_lnurl_pay",
|
||||
)
|
||||
async def api_lnurl_pay(
|
||||
request: Request,
|
||||
myextension_id: str,
|
||||
):
|
||||
myextension = await get_myextension(myextension_id)
|
||||
if not myextension:
|
||||
return {"status": "ERROR", "reason": "No myextension found"}
|
||||
return {
|
||||
"callback": str(
|
||||
request.url_for(
|
||||
"myextension.api_lnurl_pay_callback", myextension_id=myextension_id
|
||||
)
|
||||
),
|
||||
"maxSendable": myextension.lnurlpayamount * 1000,
|
||||
"minSendable": myextension.lnurlpayamount * 1000,
|
||||
"metadata": '[["text/plain", "' + myextension.name + '"]]',
|
||||
"tag": "payRequest",
|
||||
}
|
||||
|
||||
|
||||
@myextension_lnurl_router.get(
|
||||
"/api/v1/lnurl/paycb/{myextension_id}",
|
||||
status_code=HTTPStatus.OK,
|
||||
name="myextension.api_lnurl_pay_callback",
|
||||
)
|
||||
async def api_lnurl_pay_cb(
|
||||
request: Request,
|
||||
myextension_id: str,
|
||||
amount: int = Query(...),
|
||||
):
|
||||
myextension = await get_myextension(myextension_id)
|
||||
logger.debug(myextension)
|
||||
if not myextension:
|
||||
return {"status": "ERROR", "reason": "No myextension found"}
|
||||
|
||||
payment = await create_invoice(
|
||||
wallet_id=myextension.wallet,
|
||||
amount=int(amount / 1000),
|
||||
memo=myextension.name,
|
||||
unhashed_description=f'[["text/plain", "{myextension.name}"]]'.encode(),
|
||||
extra={
|
||||
"tag": "MyExtension",
|
||||
"myextensionId": myextension_id,
|
||||
"extra": request.query_params.get("amount"),
|
||||
},
|
||||
)
|
||||
return {
|
||||
"pr": payment.bolt11,
|
||||
"routes": [],
|
||||
"successAction": {"tag": "message", "message": f"Paid {myextension.name}"},
|
||||
}
|
||||
|
||||
|
||||
#################################################
|
||||
######## A very simple LNURLwithdraw ############
|
||||
# https://github.com/lnurl/luds/blob/luds/03.md #
|
||||
#################################################
|
||||
## withdraw is unlimited, look at withdraw ext ##
|
||||
## for more advanced withdraw options ##
|
||||
#################################################
|
||||
|
||||
|
||||
@myextension_lnurl_router.get(
|
||||
"/api/v1/lnurl/withdraw/{myextension_id}",
|
||||
status_code=HTTPStatus.OK,
|
||||
name="myextension.api_lnurl_withdraw",
|
||||
)
|
||||
async def api_lnurl_withdraw(
|
||||
request: Request,
|
||||
myextension_id: str,
|
||||
):
|
||||
myextension = await get_myextension(myextension_id)
|
||||
if not myextension:
|
||||
return {"status": "ERROR", "reason": "No myextension found"}
|
||||
k1 = shortuuid.uuid(name=myextension.id)
|
||||
return {
|
||||
"tag": "withdrawRequest",
|
||||
"callback": str(
|
||||
request.url_for(
|
||||
"myextension.api_lnurl_withdraw_callback", myextension_id=myextension_id
|
||||
)
|
||||
),
|
||||
"k1": k1,
|
||||
"defaultDescription": myextension.name,
|
||||
"maxWithdrawable": myextension.lnurlwithdrawamount * 1000,
|
||||
"minWithdrawable": myextension.lnurlwithdrawamount * 1000,
|
||||
}
|
||||
|
||||
|
||||
@myextension_lnurl_router.get(
|
||||
"/api/v1/lnurl/withdrawcb/{myextension_id}",
|
||||
status_code=HTTPStatus.OK,
|
||||
name="myextension.api_lnurl_withdraw_callback",
|
||||
)
|
||||
async def api_lnurl_withdraw_cb(
|
||||
myextension_id: str,
|
||||
pr: Optional[str] = None,
|
||||
k1: Optional[str] = None,
|
||||
):
|
||||
assert k1, "k1 is required"
|
||||
assert pr, "pr is required"
|
||||
myextension = await get_myextension(myextension_id)
|
||||
if not myextension:
|
||||
return {"status": "ERROR", "reason": "No myextension found"}
|
||||
|
||||
k1_check = shortuuid.uuid(name=myextension.id)
|
||||
if k1_check != k1:
|
||||
return {"status": "ERROR", "reason": "Wrong k1 check provided"}
|
||||
|
||||
await pay_invoice(
|
||||
wallet_id=myextension.wallet,
|
||||
payment_request=pr,
|
||||
max_sat=int(myextension.lnurlwithdrawamount * 1000),
|
||||
extra={
|
||||
"tag": "MyExtension",
|
||||
"myextensionId": myextension_id,
|
||||
"lnurlwithdraw": True,
|
||||
},
|
||||
)
|
||||
return {"status": "OK"}
|
||||
Loading…
Add table
Add a link
Reference in a new issue