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:
padreug 2025-06-20 21:51:38 +02:00
parent edca91579c
commit 75dd03b15a
13 changed files with 32 additions and 971 deletions

View file

@ -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 = [
{

View file

@ -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
View file

@ -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()

View file

@ -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))

View file

@ -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

View file

@ -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: {

View file

@ -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

View file

@ -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>

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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,
}
],
}

View file

@ -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 ################

View file

@ -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"}