Merge pull request #3 from lnbits/feat_lnurl_v1

another v1 fixup + moved lnurl stuff to models
This commit is contained in:
Arc 2024-11-18 22:47:16 +00:00 committed by GitHub
commit 2b0281857a
16 changed files with 1532 additions and 1494 deletions

View file

@ -1,6 +1,7 @@
import asyncio
from fastapi import APIRouter
from lnbits.tasks import create_permanent_unique_task
from loguru import logger
from .crud import db
@ -39,8 +40,6 @@ def myextension_stop():
def myextension_start():
from lnbits.tasks import create_permanent_unique_task
task = create_permanent_unique_task("ext_myextension", wait_for_paid_invoices)
scheduled_tasks.append(task)

View file

@ -2,7 +2,7 @@
"name": "MyExtension",
"short_description": "Minimal extension to build on",
"tile": "/myextension/static/image/myextension.png",
"min_lnbits_version": "0.12.5",
"min_lnbits_version": "1.0.0",
"contributors": [
{
"name": "Alan Bits",

70
crud.py
View file

@ -1,77 +1,45 @@
from typing import Optional, Union
# Description: This file contains the CRUD operations for talking to the database.
from typing import List, Optional, Union
from lnbits.db import Database
from lnbits.helpers import insert_query, update_query
from loguru import logger
from .models import MyExtension
db = Database("ext_myextension")
table_name = "myextension.maintable"
async def create_myextension(data: MyExtension) -> MyExtension:
await db.execute(
insert_query(table_name, data),
(*data.dict().values(),),
)
await db.insert("myextension.maintable", data)
return data
# this is how we used to do it
# myextension_id = urlsafe_short_hash()
# await db.execute(
# """
# INSERT INTO myextension.maintable
# (id, wallet, name, lnurlpayamount, lnurlwithdrawamount)
# VALUES (?, ?, ?, ?, ?)
# """,
# (
# myextension_id,
# wallet_id,
# data.name,
# data.lnurlpayamount,
# data.lnurlwithdrawamount,
# ),
# )
# myextension = await get_myextension(myextension_id)
# assert myextension, "Newly created table couldn't be retrieved"
async def get_myextension(myextension_id: str) -> Optional[MyExtension]:
row = await db.fetchone(
f"SELECT * FROM {table_name} WHERE id = ?", (myextension_id,)
return await db.fetchone(
"SELECT * FROM myextension.maintable WHERE id = :id",
{"id": myextension_id},
MyExtension,
)
return MyExtension(**row) if row else None
async def get_myextensions(wallet_ids: Union[str, list[str]]) -> list[MyExtension]:
async def get_myextensions(wallet_ids: Union[str, List[str]]) -> List[MyExtension]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM {table_name} WHERE wallet IN ({q})", (*wallet_ids,)
q = ",".join([f"'{w}'" for w in wallet_ids])
logger.debug(q)
return await db.fetchall(
f"SELECT * FROM myextension.maintable WHERE wallet IN ({q}) ORDER BY id",
model=MyExtension,
)
return [MyExtension(**row) for row in rows]
async def update_myextension(data: MyExtension) -> MyExtension:
await db.execute(
update_query(table_name, data),
(
*data.dict().values(),
data.id,
),
)
await db.update("myextension.maintable", data)
return data
# this is how we used to do it
# q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
# await db.execute(
# f"UPDATE myextension.maintable SET {q} WHERE id = ?",
# (*kwargs.values(), myextension_id),
# )
async def delete_myextension(myextension_id: str) -> None:
await db.execute(f"DELETE FROM {table_name} WHERE id = ?", (myextension_id,))
await db.execute(
"DELETE FROM myextension.maintable WHERE id = :id", {"id": myextension_id}
)

17
helpers.py Normal file
View file

@ -0,0 +1,17 @@
# 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

@ -15,9 +15,19 @@ async def m001_initial(db):
name TEXT NOT NULL,
total INTEGER DEFAULT 0,
lnurlpayamount INTEGER DEFAULT 0,
lnurlwithdrawamount INTEGER DEFAULT 0,
lnurlwithdraw TEXT,
lnurlpay TEXT
lnurlwithdrawamount INTEGER DEFAULT 0
);
"""
)
async def m002_add_timestamp(db):
"""
Add timestamp to templates table.
"""
await db.execute(
f"""
ALTER TABLE myextension.maintable
ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now};
"""
)

View file

@ -1,6 +1,4 @@
# Data models for your extension
from typing import Optional
# Description: Pydantic data models dictate what is passed between frontend and backend.
from pydantic import BaseModel
@ -9,16 +7,21 @@ class CreateMyExtensionData(BaseModel):
name: str
lnurlpayamount: int
lnurlwithdrawamount: int
wallet: Optional[str] = None
total: int = 0
class MyExtension(BaseModel):
id: str
wallet: str
lnurlpayamount: int
name: str
lnurlpayamount: int
lnurlwithdrawamount: int
wallet: str
total: int
lnurlpay: Optional[str]
lnurlwithdraw: Optional[str]
lnurlpay: str = ""
lnurlwithdraw: str = ""
class CreatePayment(BaseModel):
myextension_id: str
amount: int
memo: str

2118
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,8 @@ authors = ["benarc", "dni <dni@lnbits.com>"]
[tool.poetry.dependencies]
python = "^3.10 | ^3.9"
lnbits = "*"
lnbits = {version = "*", allow-prereleases = true}
mypy = "^1.13.0"
[tool.poetry.group.dev.dependencies]
black = "^24.3.0"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Before After
Before After

242
static/js/index.js Normal file
View file

@ -0,0 +1,242 @@
window.app = Vue.createApp({
el: '#vue',
mixins: [windowMixin],
delimiters: ['${', '}'],
data: function () {
return {
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: {}
}
}
},
///////////////////////////////////////////////////
////////////////METHODS FUNCTIONS//////////////////
///////////////////////////////////////////////////
methods: {
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) {
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) {
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})
await LNbits.utils
.confirmDialog('Are you sure you want to delete this MyExtension?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/myextension/api/v1/myex/' + tempId,
_.findWhere(this.g.user.wallets, {id: myextension.wallet})
.adminkey
)
.then(() => {
this.myex = _.reject(this.myex, obj => obj.id == tempId)
})
.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)
// We can also use this Lnbits core websocket function for getting a payment reaction
const wallet = _.findWhere(this.g.user.wallets, {
id: this.urlDialog.data.wallet
})
eventReactionWebocket(wallet.inkey)
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/////
///////////////////////////////////////////////////
async created() {
await this.getMyExtensions()
}
})

View file

@ -2,7 +2,6 @@ import asyncio
from lnbits.core.models import Payment
from lnbits.core.services import websocket_updater
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener
from .crud import get_myextension, update_myextension
@ -16,7 +15,7 @@ from .crud import get_myextension, update_myextension
async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue, get_current_extension_name())
register_invoice_listener(invoice_queue, "ext_myextension")
while True:
payment = await invoice_queue.get()
await on_invoice_paid(payment)
@ -45,7 +44,6 @@ async def on_invoice_paid(payment: Payment) -> None:
# here we could send some data to a websocket on
# wss://<your-lnbits>/api/v1/ws/<myextension_id> and then listen to it on
# the frontend, which we do with index.html connectWebocket()
some_payment_data = {
"name": myextension.name,

View file

@ -3,7 +3,9 @@
<!--/////////////////////////////////////////////////-->
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
%} {% block scripts %} {{ window_vars(user) }}
<script src="{{ static_url_for('myextension/static', path='js/index.js') }}"></script>
{% endblock %} {% block page %}
<div class="row q-col-gutter-md" id="makeItRain">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
@ -27,10 +29,10 @@
<q-table
dense
flat
:data="myex"
:rows="myex"
row-key="id"
:columns="myexTable.columns"
:pagination.sync="myexTable.pagination"
v-model:pagination="myexTable.pagination"
>
<myextension v-slot:header="props">
<q-tr :props="props">
@ -65,7 +67,7 @@
icon="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.myextension"
:href="props.row.id"
target="_blank"
><q-tooltip>Open public page</q-tooltip></q-btn
></q-td
@ -220,7 +222,7 @@
<template v-slot:append>
<q-btn
round
@click="createInvoice(urlDialog.data.wallet, urlDialog.data.id)"
@click="createInvoice(urlDialog.data.id)"
color="primary"
flat
icon="add_circle"
@ -236,296 +238,4 @@
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.4.0/dist/confetti.browser.min.js"></script>
<script>
///////////////////////////////////////////////////
//////////an object we can update with data////////
///////////////////////////////////////////////////
const mapMyExtension = obj => {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.myextension = ['/myextension/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
delimiters: ['${', '}'],
data: function () {
return {
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: {}
}
}
},
///////////////////////////////////////////////////
////////////////METHODS FUNCTIONS//////////////////
///////////////////////////////////////////////////
methods: {
closeFormDialog() {
this.formDialog.show = false
this.formDialog.data = {}
},
getMyExtensions: function () {
var self = this
LNbits.api
.request(
'GET',
'/myextension/api/v1/myex?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.myex = response.data.map(function (obj) {
return mapMyExtension(obj)
})
})
},
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.wallet = wallet.id
data.total = this.formDialog.data.total
this.updateMyExtension(wallet, data)
} else {
this.createMyExtension(wallet, data)
}
},
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
},
createMyExtension(wallet, data) {
LNbits.api
.request('POST', '/myextension/api/v1/myex', wallet.adminkey, data)
.then(response => {
this.myex.push(mapMyExtension(response.data))
this.closeFormDialog()
})
.catch(error => {
LNbits.utils.notifyApiError(error)
})
},
updateMyExtension(wallet, data) {
LNbits.api
.request(
'PUT',
`/myextension/api/v1/myex/${data.id}`,
wallet.adminkey,
data
)
.then(response => {
this.myex = _.reject(this.myex, obj => {
return obj.id == data.id
})
this.myex.push(mapMyExtension(response.data))
this.closeFormDialog()
})
.catch(error => {
LNbits.utils.notifyApiError(error)
})
},
deleteMyExtension: function (tempId) {
var self = this
var myextension = _.findWhere(this.myex, {id: tempId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this MyExtension?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/myextension/api/v1/myex/' + tempId,
_.findWhere(self.g.user.wallets, {id: myextension.wallet})
.adminkey
)
.then(function (response) {
self.myex = _.reject(self.myex, function (obj) {
return obj.id == tempId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportCSV: function () {
LNbits.utils.exportCSV(this.myexTable.columns, this.myex)
},
itemsArray(tempId) {
const myextension = _.findWhere(this.myex, {id: tempId})
return [...myextension.itemsMap.values()]
},
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
},
closeformDialog() {
this.formDialog.show = false
this.formDialog.data = {}
},
openUrlDialog(id) {
this.urlDialog.data = _.findWhere(this.myex, {id})
this.qrValue = this.urlDialog.data.lnurlpay
console.log(this.urlDialog.data.id)
this.connectWebocket(this.urlDialog.data.id)
this.urlDialog.show = true
},
createInvoice(walletId, myextensionId) {
///////////////////////////////////////////////////
///Simple call to the api to create an invoice/////
///////////////////////////////////////////////////
console.log(walletId)
const wallet = _.findWhere(this.g.user.wallets, {
id: walletId
})
const dataToSend = {
out: false,
amount: this.invoiceAmount,
memo: 'Invoice created by MyExtension',
extra: {
tag: 'MyExtension',
myextensionId: myextensionId
}
}
LNbits.api
.request('POST', `/api/v1/payments`, wallet.inkey, dataToSend)
.then(response => {
this.qrValue = response.data.payment_request
})
.catch(error => {
LNbits.utils.notifyApiError(error)
})
},
makeItRain() {
document.getElementById('vue').disabled = true
var end = Date.now() + 2 * 1000
var colors = ['#FFD700', '#ffffff']
function frame() {
confetti({
particleCount: 2,
angle: 60,
spread: 55,
origin: {x: 0},
colors: colors,
zIndex: 999999
})
confetti({
particleCount: 2,
angle: 120,
spread: 55,
origin: {x: 1},
colors: colors,
zIndex: 999999
})
if (Date.now() < end) {
requestAnimationFrame(frame)
} else {
document.getElementById('vue').disabled = false
}
}
frame()
},
connectWebocket(wallet_id) {
//////////////////////////////////////////////////
///wait for pay action to happen and do a thing////
///////////////////////////////////////////////////
self = this
if (location.protocol !== 'http:') {
localUrl =
'wss://' +
document.domain +
':' +
location.port +
'/api/v1/ws/' +
wallet_id
} else {
localUrl =
'ws://' +
document.domain +
':' +
location.port +
'/api/v1/ws/' +
wallet_id
}
this.connection = new WebSocket(localUrl)
this.connection.onmessage = function (e) {
self.makeItRain()
}
}
},
///////////////////////////////////////////////////
//////LIFECYCLE FUNCTIONS RUNNING ON PAGE LOAD/////
///////////////////////////////////////////////////
created: function () {
if (this.g.user.wallets.length) {
this.getMyExtensions()
}
}
})
</script>
{% endblock %}

View file

@ -10,11 +10,11 @@
<div class="text-center">
<a class="text-secondary" href="lightning:{{ lnurl }}">
<q-responsive :ratio="1" class="q-mx-md">
<qrcode
<lnbits-qrcode
:value="qrValue"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
></lnbits-qrcode>
</q-responsive>
</a>
</div>
@ -49,9 +49,7 @@
</div>
{% endblock %} {% block scripts %}
<script>
Vue.component(VueQrcode.name, VueQrcode)
new Vue({
window.app = Vue.createApp({
el: '#vue',
mixins: [windowMixin],
data: function () {

View file

@ -1,14 +1,16 @@
# Description: Add your page endpoints here.
from http import HTTPStatus
from fastapi import APIRouter, Depends, Request
from fastapi import APIRouter, Depends, HTTPException, 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 starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from .crud import get_myextension
from .helpers import lnurler
myextension_generic_router = APIRouter()
@ -26,9 +28,9 @@ def myextension_renderer():
@myextension_generic_router.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
async def index(req: Request, user: User = Depends(check_user_exists)):
return myextension_renderer().TemplateResponse(
"myextension/index.html", {"request": request, "user": user.dict()}
"myextension/index.html", {"request": req, "user": user.json()}
)
@ -36,18 +38,18 @@ async def index(request: Request, user: User = Depends(check_user_exists)):
@myextension_generic_router.get("/{myextension_id}")
async def myextension(request: Request, myextension_id):
myextension = await get_myextension(myextension_id)
if not myextension:
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": request,
"request": req,
"myextension_id": myextension_id,
"lnurlpay": myextension.lnurlpay,
"lnurlpay": lnurler(myex.id, "myextension.api_lnurl_pay", req),
"web_manifest": f"/myextension/manifest/{myextension_id}.webmanifest",
},
)

View file

@ -1,16 +1,13 @@
# Description: This file contains the extensions API endpoints.
from http import HTTPStatus
from fastapi import APIRouter, Depends, Query, Request
from fastapi import APIRouter, Depends, Request
from lnbits.core.crud import get_user
from lnbits.core.models import WalletTypeInfo
from lnbits.core.services import create_invoice
from lnbits.decorators import (
get_key_type,
require_admin_key,
require_invoice_key,
)
from lnbits.decorators import require_admin_key, require_invoice_key
from lnbits.helpers import urlsafe_short_hash
from lnurl import encode as lnurl_encode
from starlette.exceptions import HTTPException
from .crud import (
@ -20,28 +17,34 @@ from .crud import (
get_myextensions,
update_myextension,
)
from .models import CreateMyExtensionData, MyExtension
from .helpers import lnurler
from .models import CreateMyExtensionData, CreatePayment, MyExtension
myextension_api_router = APIRouter()
#######################################
##### ADD YOUR API ENDPOINTS HERE #####
#######################################
# 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", status_code=HTTPStatus.OK)
@myextension_api_router.get("/api/v1/myex")
async def api_myextensions(
all_wallets: bool = Query(False),
wallet: WalletTypeInfo = Depends(get_key_type),
):
req: Request, # Withoutthe lnurl stuff this wouldnt be needed
wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> list[MyExtension]:
wallet_ids = [wallet.wallet.id]
if all_wallets:
user = await get_user(wallet.wallet.user)
wallet_ids = user.wallet_ids if user else []
return [myextension.dict() for myextension in await get_myextensions(wallet_ids)]
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
@ -49,43 +52,20 @@ async def api_myextensions(
@myextension_api_router.get(
"/api/v1/myex/{myextension_id}",
status_code=HTTPStatus.OK,
dependencies=[Depends(require_invoice_key)],
)
async def api_myextension(myextension_id: str):
myextension = await get_myextension(myextension_id)
if not myextension:
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."
)
return myextension.dict()
# 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)
## update a record
@myextension_api_router.put("/api/v1/myex/{myextension_id}")
async def api_myextension_update(
data: CreateMyExtensionData,
myextension_id: str,
wallet: WalletTypeInfo = Depends(get_key_type),
) -> MyExtension:
if not myextension_id:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
)
myextension = await get_myextension(myextension_id)
assert myextension, "MyExtension couldn't be retrieved"
if wallet.wallet.id != myextension.wallet:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension."
)
for key, value in data.dict().items():
setattr(myextension, key, value)
return await update_myextension(myextension)
return myex
## Create a new record
@ -93,29 +73,53 @@ async def api_myextension_update(
@myextension_api_router.post("/api/v1/myex", status_code=HTTPStatus.CREATED)
async def api_myextension_create(
request: Request,
req: Request, # Withoutthe lnurl stuff this wouldnt be needed
data: CreateMyExtensionData,
key_type: WalletTypeInfo = Depends(require_admin_key),
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> MyExtension:
myextension_id = urlsafe_short_hash()
lnurlpay = lnurl_encode(
str(request.url_for("myextension.api_lnurl_pay", myextension_id=myextension_id))
)
lnurlwithdraw = lnurl_encode(
str(
request.url_for(
"myextension.api_lnurl_withdraw", myextension_id=myextension_id
)
myex = MyExtension(**data.dict(), wallet=wallet.wallet.id, id=urlsafe_short_hash())
myex = await create_myextension(myex)
# 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: MyExtension,
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."
)
)
data.wallet = data.wallet or key_type.wallet.id
myext = MyExtension(
id=myextension_id,
lnurlpay=lnurlpay,
lnurlwithdraw=lnurlwithdraw,
**data.dict(),
)
return await create_myextension(myext)
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
@ -125,20 +129,19 @@ async def api_myextension_create(
async def api_myextension_delete(
myextension_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
):
myextension = await get_myextension(myextension_id)
myex = await get_myextension(myextension_id)
if not myextension:
if not myex:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
)
if myextension.wallet != wallet.wallet.id:
if myex.wallet != wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension."
)
await delete_myextension(myextension_id)
return "", HTTPStatus.NO_CONTENT
# ANY OTHER ENDPOINTS YOU NEED
@ -146,13 +149,9 @@ async def api_myextension_delete(
## This endpoint creates a payment
@myextension_api_router.post(
"/api/v1/myex/payment/{myextension_id}", status_code=HTTPStatus.CREATED
)
async def api_myextension_create_invoice(
myextension_id: str, amount: int = Query(..., ge=1), memo: str = ""
) -> dict:
myextension = await get_myextension(myextension_id)
@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(
@ -162,19 +161,16 @@ async def api_myextension_create_invoice(
# we create a payment and add some tags,
# so tasks.py can grab the payment once its paid
try:
payment_hash, payment_request = await create_invoice(
wallet_id=myextension.wallet,
amount=amount,
memo=f"{memo} to {myextension.name}" if memo else f"{myextension.name}",
extra={
"tag": "myextension",
"amount": amount,
},
)
except Exception as exc:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc)
) from exc
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_hash, "payment_request": payment_request}
return {"payment_hash": payment.payment_hash, "payment_request": payment.bolt11}

View file

@ -1,6 +1,4 @@
# Maybe your extension needs some LNURL stuff.
# Here is a very simple example of how to do it.
# Feel free to delete this file if you don't need it.
# Description: Extensions that use LNURL usually have a few endpoints in views_lnurl.py.
from http import HTTPStatus
from typing import Optional
@ -61,7 +59,7 @@ async def api_lnurl_pay_cb(
if not myextension:
return {"status": "ERROR", "reason": "No myextension found"}
_, payment_request = await create_invoice(
payment = await create_invoice(
wallet_id=myextension.wallet,
amount=int(amount / 1000),
memo=myextension.name,
@ -73,7 +71,7 @@ async def api_lnurl_pay_cb(
},
)
return {
"pr": payment_request,
"pr": payment.bolt11,
"routes": [],
"successAction": {"tag": "message", "message": f"Paid {myextension.name}"},
}