feat: update to v1.0.0

This commit is contained in:
dni ⚡ 2024-10-21 14:38:03 +02:00
parent 42b5edaf5d
commit 48b1069a9f
12 changed files with 1447 additions and 1370 deletions

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",

33
crud.py
View file

@ -1,7 +1,6 @@
from typing import Optional, Union
from lnbits.db import Database
from lnbits.helpers import insert_query, update_query
from .models import MyExtension
@ -10,10 +9,7 @@ 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(table_name, data)
return data
# this is how we used to do it
@ -38,34 +34,27 @@ async def create_myextension(data: MyExtension) -> MyExtension:
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(
f"SELECT * FROM {table_name} 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]:
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"'{wallet_id}'" for wallet_id in wallet_ids])
return await db.fetchall(
f"SELECT * FROM {table_name} WHERE wallet IN ({q})", 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(table_name, data)
return data
# this is how we used to do it
# 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 = ?",
@ -74,4 +63,4 @@ async def update_myextension(data: MyExtension) -> MyExtension:
async def delete_myextension(myextension_id: str) -> None:
await db.execute(f"DELETE FROM {table_name} WHERE id = ?", (myextension_id,))
await db.execute(f"DELETE FROM {table_name} WHERE id = :id", {"id": myextension_id})

View file

@ -21,3 +21,15 @@ async def m001_initial(db):
);
"""
)
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,5 +1,6 @@
# Data models for your extension
from datetime import datetime, timezone
from typing import Optional
from pydantic import BaseModel
@ -22,3 +23,4 @@ class MyExtension(BaseModel):
total: int
lnurlpay: Optional[str]
lnurlwithdraw: Optional[str]
created_at: datetime = datetime.now(timezone.utc)

2115
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

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

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

@ -0,0 +1,280 @@
///////////////////////////////////////////////////
//////////an object we can update with data////////
///////////////////////////////////////////////////
const mapMyExtension = obj => {
obj.date = Quasar.date.formatDate(
new Date(obj.created_at),
'YYYY-MM-DD HH:mm'
)
obj.myextension = ['/myextension/', obj.id].join('')
return obj
}
window.app = Vue.createApp({
el: '#vue',
mixins: [windowMixin],
delimiters: ['${', '}'],
data() {
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() {
LNbits.api
.request(
'GET',
'/myextension/api/v1/myex?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(response => {
this.myex = response.data.map(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(tempId) {
const myextension = _.findWhere(this.myex, {id: tempId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this MyExtension?')
.onOk(() => {
LNbits.api
.request(
'DELETE',
'/myextension/api/v1/myex/' + tempId,
_.findWhere(this.g.user.wallets, {id: myextension.wallet})
.adminkey
)
.then(response => {
this.myex = _.reject(this.myex, obj => {
return obj.id == tempId
})
})
.catch(LNbits.utils.notifyApiError)
})
},
exportCSV() {
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
const end = Date.now() + 2 * 1000
const colors = ['#FFD700', '#ffffff']
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
}
},
connectWebocket(wallet_id) {
//////////////////////////////////////////////////
///wait for pay action to happen and do a thing////
///////////////////////////////////////////////////
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 = e => {
this.makeItRain()
}
}
},
///////////////////////////////////////////////////
//////LIFECYCLE FUNCTIONS RUNNING ON PAGE LOAD/////
///////////////////////////////////////////////////
created() {
if (this.g.user.wallets.length) {
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_invoice_listener")
while True:
payment = await invoice_queue.get()
await on_invoice_paid(payment)

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

@ -1,12 +1,11 @@
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
@ -28,7 +27,7 @@ def myextension_renderer():
@myextension_generic_router.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return myextension_renderer().TemplateResponse(
"myextension/index.html", {"request": request, "user": user.dict()}
"myextension/index.html", {"request": request, "user": user.json()}
)

View file

@ -5,7 +5,6 @@ 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,
)
@ -32,16 +31,16 @@ myextension_api_router = APIRouter()
## 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),
):
wallet_ids = [wallet.wallet.id]
key_info: WalletTypeInfo = Depends(require_invoice_key),
) -> list[MyExtension]:
wallet_ids = [key_info.wallet.id]
if all_wallets:
user = await get_user(wallet.wallet.user)
user = await get_user(key_info.wallet.user)
wallet_ids = user.wallet_ids if user else []
return [myextension.dict() for myextension in await get_myextensions(wallet_ids)]
return await get_myextensions(wallet_ids)
## Get a single record
@ -49,16 +48,15 @@ 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):
async def api_myextension(myextension_id: str) -> MyExtension:
myextension = await get_myextension(myextension_id)
if not myextension:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
)
return myextension.dict()
return myextension
## update a record
@ -68,16 +66,15 @@ async def api_myextension(myextension_id: str):
async def api_myextension_update(
data: CreateMyExtensionData,
myextension_id: str,
wallet: WalletTypeInfo = Depends(get_key_type),
key_info: WalletTypeInfo = Depends(require_invoice_key),
) -> MyExtension:
if not myextension_id:
myextension = await get_myextension(myextension_id)
if not myextension:
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:
if key_info.wallet.id != myextension.wallet:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension."
)
@ -123,7 +120,7 @@ async def api_myextension_create(
@myextension_api_router.delete("/api/v1/myex/{myextension_id}")
async def api_myextension_delete(
myextension_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
myextension_id: str, key_info: WalletTypeInfo = Depends(require_admin_key)
):
myextension = await get_myextension(myextension_id)
@ -132,13 +129,12 @@ async def api_myextension_delete(
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
)
if myextension.wallet != wallet.wallet.id:
if myextension.wallet != key_info.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
@ -162,19 +158,14 @@ 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=amount,
memo=f"{memo} to {myextension.name}" if memo else f"{myextension.name}",
extra={
"tag": "myextension",
"amount": amount,
},
)
return {"payment_hash": payment_hash, "payment_request": payment_request}
return {"payment_hash": payment.payment_hash, "payment_request": payment.bolt11}

View file

@ -61,7 +61,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 +73,7 @@ async def api_lnurl_pay_cb(
},
)
return {
"pr": payment_request,
"pr": payment.bolt11,
"routes": [],
"successAction": {"tag": "message", "message": f"Paid {myextension.name}"},
}