This commit is contained in:
benarc 2024-02-01 17:18:55 +00:00
parent 66d44f95fb
commit 2cac36be17
12 changed files with 272 additions and 311 deletions

View file

@ -8,9 +8,7 @@ from lnbits.tasks import catch_everything_and_restart
db = Database("ext_myextension") db = Database("ext_myextension")
myextension_ext: APIRouter = APIRouter( myextension_ext: APIRouter = APIRouter(prefix="/myextension", tags=["MyExtension"])
prefix="/myextension", tags=["MyExtension"]
)
temp_static_files = [ temp_static_files = [
{ {
@ -19,14 +17,17 @@ temp_static_files = [
} }
] ]
def myextension_renderer(): def myextension_renderer():
return template_renderer(["myextension/templates"]) return template_renderer(["myextension/templates"])
from .lnurl import * from .lnurl import *
from .tasks import wait_for_paid_invoices from .tasks import wait_for_paid_invoices
from .views import * from .views import *
from .views_api import * from .views_api import *
def myextension_start(): def myextension_start():
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))

63
crud.py
View file

@ -9,7 +9,10 @@ from fastapi import Request
from lnurl import encode as lnurl_encode from lnurl import encode as lnurl_encode
import shortuuid import shortuuid
async def create_myextension(wallet_id: str, data: CreateMyExtensionData, req: Request) -> MyExtension:
async def create_myextension(
wallet_id: str, data: CreateMyExtensionData, req: Request
) -> MyExtension:
myextension_id = urlsafe_short_hash() myextension_id = urlsafe_short_hash()
await db.execute( await db.execute(
""" """
@ -21,7 +24,7 @@ async def create_myextension(wallet_id: str, data: CreateMyExtensionData, req: R
wallet_id, wallet_id,
data.name, data.name,
data.lnurlpayamount, data.lnurlpayamount,
data.lnurlwithdrawamount data.lnurlwithdrawamount,
), ),
) )
myextension = await get_myextension(myextension_id, req) myextension = await get_myextension(myextension_id, req)
@ -29,25 +32,33 @@ async def create_myextension(wallet_id: str, data: CreateMyExtensionData, req: R
return myextension return myextension
async def get_myextension(myextension_id: str, req: Optional[Request] = None) -> Optional[MyExtension]: async def get_myextension(
myextension_id: str, req: Optional[Request] = None
) -> Optional[MyExtension]:
logger.debug(myextension_id) logger.debug(myextension_id)
row = await db.fetchone("SELECT * FROM myextension.maintable WHERE id = ?", (myextension_id,)) row = await db.fetchone(
"SELECT * FROM myextension.maintable WHERE id = ?", (myextension_id,)
)
if not row: if not row:
return None return None
rowAmended = MyExtension(**row) rowAmended = MyExtension(**row)
if req: if req:
rowAmended.lnurlpay = lnurl_encode( rowAmended.lnurlpay = lnurl_encode(
req.url_for("myextension.api_lnurl_pay", req.url_for("myextension.api_lnurl_pay", myextension_id=row.id)._url
myextension_id=row.id)._url )
)
rowAmended.lnurlwithdraw = lnurl_encode( rowAmended.lnurlwithdraw = lnurl_encode(
req.url_for("myextension.api_lnurl_withdraw", req.url_for(
myextension_id=row.id, "myextension.api_lnurl_withdraw",
tickerhash=shortuuid.uuid(name=rowAmended.id + str(rowAmended.ticker)))._url myextension_id=row.id,
) tickerhash=shortuuid.uuid(name=rowAmended.id + str(rowAmended.ticker)),
)._url
)
return rowAmended return rowAmended
async def get_myextensions(wallet_ids: Union[str, List[str]], req: Optional[Request] = None) -> List[MyExtension]:
async def get_myextensions(
wallet_ids: Union[str, List[str]], req: Optional[Request] = None
) -> List[MyExtension]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
@ -59,25 +70,33 @@ async def get_myextensions(wallet_ids: Union[str, List[str]], req: Optional[Requ
if req: if req:
for row in tempRows: for row in tempRows:
row.lnurlpay = lnurl_encode( row.lnurlpay = lnurl_encode(
req.url_for("myextension.api_lnurl_pay", req.url_for("myextension.api_lnurl_pay", myextension_id=row.id)._url
myextension_id=row.id)._url )
)
row.lnurlwithdraw = lnurl_encode( row.lnurlwithdraw = lnurl_encode(
req.url_for("myextension.api_lnurl_withdraw", req.url_for(
myextension_id=row.id, "myextension.api_lnurl_withdraw",
tickerhash=shortuuid.uuid(name=row.id + str(row.ticker)))._url myextension_id=row.id,
tickerhash=shortuuid.uuid(name=row.id + str(row.ticker)),
)._url
) )
return tempRows return tempRows
async def update_myextension(myextension_id: str, req: Optional[Request] = None, **kwargs) -> MyExtension:
async def update_myextension(
myextension_id: str, req: Optional[Request] = None, **kwargs
) -> MyExtension:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
logger.debug( kwargs.items()) logger.debug(kwargs.items())
await db.execute( await db.execute(
f"UPDATE myextension.maintable SET {q} WHERE id = ?", (*kwargs.values(), myextension_id) f"UPDATE myextension.maintable SET {q} WHERE id = ?",
(*kwargs.values(), myextension_id),
) )
myextension = await get_myextension(myextension_id, req) myextension = await get_myextension(myextension_id, req)
assert myextension, "Newly updated myextension couldn't be retrieved" assert myextension, "Newly updated myextension couldn't be retrieved"
return myextension return myextension
async def delete_myextension(myextension_id: str) -> None: async def delete_myextension(myextension_id: str) -> None:
await db.execute("DELETE FROM myextension.maintable WHERE id = ?", (myextension_id,)) await db.execute(
"DELETE FROM myextension.maintable WHERE id = ?", (myextension_id,)
)

View file

@ -19,8 +19,9 @@ import shortuuid
################################################# #################################################
################################################# #################################################
@myextension_ext.get( @myextension_ext.get(
"/api/v1/lnurl/pay/{myextension_id}", "/api/v1/lnurl/pay/{myextension_id}",
status_code=HTTPStatus.OK, status_code=HTTPStatus.OK,
name="myextension.api_lnurl_pay", name="myextension.api_lnurl_pay",
) )
@ -32,15 +33,20 @@ async def api_lnurl_pay(
if not myextension: if not myextension:
return {"status": "ERROR", "reason": "No myextension found"} return {"status": "ERROR", "reason": "No myextension found"}
return { return {
"callback": str(request.url_for("myextension.api_lnurl_pay_callback", myextension_id=myextension_id)), "callback": str(
"maxSendable": myextension.lnurlpayamount * 1000, request.url_for(
"minSendable": myextension.lnurlpayamount * 1000, "myextension.api_lnurl_pay_callback", myextension_id=myextension_id
"metadata":"[[\"text/plain\", \"" + myextension.name + "\"]]", )
"tag": "payRequest" ),
} "maxSendable": myextension.lnurlpayamount * 1000,
"minSendable": myextension.lnurlpayamount * 1000,
"metadata": '[["text/plain", "' + myextension.name + '"]]',
"tag": "payRequest",
}
@myextension_ext.get( @myextension_ext.get(
"/api/v1/lnurl/pay/cb/{myextension_id}", "/api/v1/lnurl/pay/cb/{myextension_id}",
status_code=HTTPStatus.OK, status_code=HTTPStatus.OK,
name="myextension.api_lnurl_pay_callback", name="myextension.api_lnurl_pay_callback",
) )
@ -53,27 +59,25 @@ async def api_lnurl_pay_cb(
logger.debug(myextension) logger.debug(myextension)
if not myextension: if not myextension:
return {"status": "ERROR", "reason": "No myextension found"} return {"status": "ERROR", "reason": "No myextension found"}
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=myextension.wallet, wallet_id=myextension.wallet,
amount=int(amount / 1000), amount=int(amount / 1000),
memo=myextension.name, memo=myextension.name,
unhashed_description=f"[[\"text/plain\", \"{myextension.name}\"]]".encode(), unhashed_description=f'[["text/plain", "{myextension.name}"]]'.encode(),
extra= { extra={
"tag": "MyExtension", "tag": "MyExtension",
"myextensionId": myextension_id, "myextensionId": myextension_id,
"extra": request.query_params.get("amount"), "extra": request.query_params.get("amount"),
}, },
) )
return { return {
"pr": payment_request, "pr": payment_request,
"routes": [], "routes": [],
"successAction": { "successAction": {"tag": "message", "message": f"Paid {myextension.name}"},
"tag": "message",
"message": f"Paid {myextension.name}"
}
} }
################################################# #################################################
######## A very simple LNURLwithdraw ############ ######## A very simple LNURLwithdraw ############
# https://github.com/lnurl/luds/blob/luds/03.md # # https://github.com/lnurl/luds/blob/luds/03.md #
@ -99,16 +103,21 @@ async def api_lnurl_withdraw(
return {"status": "ERROR", "reason": "LNURLw already used"} return {"status": "ERROR", "reason": "LNURLw already used"}
return { return {
"tag": "withdrawRequest", "tag": "withdrawRequest",
"callback": str(request.url_for("myextension.api_lnurl_withdraw_callback", myextension_id=myextension_id)), "callback": str(
"k1": k1, request.url_for(
"defaultDescription": myextension.name, "myextension.api_lnurl_withdraw_callback", myextension_id=myextension_id
"maxWithdrawable": myextension.lnurlwithdrawamount * 1000, )
"minWithdrawable": myextension.lnurlwithdrawamount * 1000 ),
} "k1": k1,
"defaultDescription": myextension.name,
"maxWithdrawable": myextension.lnurlwithdrawamount * 1000,
"minWithdrawable": myextension.lnurlwithdrawamount * 1000,
}
@myextension_ext.get( @myextension_ext.get(
"/api/v1/lnurl/withdraw/cb/{myextension_id}", "/api/v1/lnurl/withdraw/cb/{myextension_id}",
status_code=HTTPStatus.OK, status_code=HTTPStatus.OK,
name="myextension.api_lnurl_withdraw_callback", name="myextension.api_lnurl_withdraw_callback",
) )
@ -125,12 +134,14 @@ async def api_lnurl_withdraw_cb(
myextension = await get_myextension(myextension_id) myextension = await get_myextension(myextension_id)
if not myextension: if not myextension:
return {"status": "ERROR", "reason": "No myextension found"} return {"status": "ERROR", "reason": "No myextension found"}
k1Check = shortuuid.uuid(name=myextension.id + str(myextension.ticker)) k1Check = shortuuid.uuid(name=myextension.id + str(myextension.ticker))
if k1Check != k1: if k1Check != k1:
return {"status": "ERROR", "reason": "Wrong k1 check provided"} return {"status": "ERROR", "reason": "Wrong k1 check provided"}
await update_myextension(myextension_id=myextension_id, ticker=myextension.ticker + 1) await update_myextension(
myextension_id=myextension_id, ticker=myextension.ticker + 1
)
logger.debug(myextension.wallet) logger.debug(myextension.wallet)
logger.debug(pr) logger.debug(pr)
logger.debug(int(myextension.lnurlwithdrawamount * 1000)) logger.debug(int(myextension.lnurlwithdrawamount * 1000))
@ -138,6 +149,10 @@ async def api_lnurl_withdraw_cb(
wallet_id=myextension.wallet, wallet_id=myextension.wallet,
payment_request=pr, payment_request=pr,
max_sat=int(myextension.lnurlwithdrawamount * 1000), max_sat=int(myextension.lnurlwithdrawamount * 1000),
extra={"tag": "MyExtension", "myextensionId": myextension_id, "lnurlwithdraw": True} extra={
"tag": "MyExtension",
"myextensionId": myextension_id,
"lnurlwithdraw": True,
},
) )
return {"status": "OK"} return {"status": "OK"}

View file

@ -1,5 +1,6 @@
# The migration file is like a blockchain, never edit only add! # The migration file is like a blockchain, never edit only add!
async def m001_initial(db): async def m001_initial(db):
""" """
Initial templates table. Initial templates table.
@ -19,8 +20,10 @@ async def m001_initial(db):
""" """
) )
# Here we add another field to the database # Here we add another field to the database
async def m002_addtip_wallet(db): async def m002_addtip_wallet(db):
""" """
Add total to templates table Add total to templates table
@ -29,4 +32,4 @@ async def m002_addtip_wallet(db):
""" """
ALTER TABLE myextension.maintable ADD ticker INTEGER DEFAULT 1; ALTER TABLE myextension.maintable ADD ticker INTEGER DEFAULT 1;
""" """
) )

View file

@ -9,6 +9,7 @@ from fastapi import Request
from lnbits.lnurl import encode as lnurl_encode from lnbits.lnurl import encode as lnurl_encode
from urllib.parse import urlparse from urllib.parse import urlparse
class CreateMyExtensionData(BaseModel): class CreateMyExtensionData(BaseModel):
wallet: Optional[str] wallet: Optional[str]
name: Optional[str] name: Optional[str]
@ -17,6 +18,7 @@ class CreateMyExtensionData(BaseModel):
lnurlwithdrawamount: Optional[int] lnurlwithdrawamount: Optional[int]
ticker: Optional[int] ticker: Optional[int]
class MyExtension(BaseModel): class MyExtension(BaseModel):
id: str id: str
wallet: Optional[str] wallet: Optional[str]
@ -30,4 +32,4 @@ class MyExtension(BaseModel):
@classmethod @classmethod
def from_row(cls, row: Row) -> "MyExtension": def from_row(cls, row: Row) -> "MyExtension":
return cls(**dict(row)) return cls(**dict(row))

View file

@ -29,6 +29,7 @@ async def wait_for_paid_invoices():
# do somethhing when an invoice related top this extension is paid # do somethhing when an invoice related top this extension is paid
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
logger.debug("payment received for myextension extension") logger.debug("payment received for myextension extension")
if payment.extra.get("tag") != "MyExtension": if payment.extra.get("tag") != "MyExtension":
@ -42,20 +43,18 @@ async def on_invoice_paid(payment: Payment) -> None:
total = myextension.total - payment.amount total = myextension.total - payment.amount
else: else:
total = myextension.total + payment.amount total = myextension.total + payment.amount
data_to_update = { data_to_update = {"total": total}
"total": total
}
await update_myextension(myextension_id=myextension_id, **data_to_update) await update_myextension(myextension_id=myextension_id, **data_to_update)
# here we could send some data to a websocket on wss://<your-lnbits>/api/v1/ws/<myextension_id> # 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() # and then listen to it on the frontend, which we do with index.html connectWebocket()
some_payment_data = { some_payment_data = {
"name": myextension.name, "name": myextension.name,
"amount": payment.amount, "amount": payment.amount,
"fee": payment.fee, "fee": payment.fee,
"checking_id": payment.checking_id "checking_id": payment.checking_id,
} }
await websocketUpdater(myextension_id, str(some_payment_data)) await websocketUpdater(myextension_id, str(some_payment_data))

View file

@ -1,8 +1,3 @@
<q-expansion-item <q-expansion-item group="extras" icon="swap_vertical_circle" label="API info" :content-inset-level="0.5">
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-btn flat label="Swagger API" type="a" href="../docs#/MyExtension"></q-btn> <q-btn flat label="Swagger API" type="a" href="../docs#/MyExtension"></q-btn>
</q-expansion-item> </q-expansion-item>

View file

@ -4,24 +4,10 @@
<p> <p>
Some more info about my excellent extension. Some more info about my excellent extension.
</p> </p>
<small <small>Created by
>Created by <a class="text-secondary" href="https://github.com/benarc" target="_blank">Ben Arc</a>.</small>
<a <small>Repo
class="text-secondary" <a class="text-secondary" href="https://github.com/lnbits/myextension" target="_blank">MyExtension</a>.</small>
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-section>
</q-card> </q-card>
</q-expansion-item> </q-expansion-item>

View file

@ -4,10 +4,8 @@
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md"> <div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card> <q-card>
<q-card-section> <q-card-section>
<q-btn unelevated color="primary" @click="formDialog.show = true" <q-btn unelevated color="primary" @click="formDialog.show = true">New MyExtension</q-btn>
>New MyExtension</q-btn </q-card-section>
>
</q-card-section>
</q-card> </q-card>
<q-card> <q-card>
@ -20,14 +18,8 @@
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn> <q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div> </div>
</div> </div>
<q-table <q-table dense flat :data="temps" row-key="id" :columns="tempsTable.columns"
dense :pagination.sync="tempsTable.pagination">
flat
:data="temps"
row-key="id"
:columns="tempsTable.columns"
:pagination.sync="tempsTable.pagination"
>
<myextension v-slot:header="props"> <myextension v-slot:header="props">
<q-tr :props="props"> <q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props"> <q-th v-for="col in props.cols" :key="col.name" :props="props">
@ -38,63 +30,30 @@
<template v-slot:body="props"> <template v-slot:body="props">
<q-tr :props="props"> <q-tr :props="props">
<q-td <q-td v-for="col in props.cols" :key="col.name" :props="props">
v-for="col in props.cols" <div v-if="col.field == 'total'">${ col.value / 1000} sats</div>
:key="col.name" <div v-else>${ col.value }</div>
:props="props" </q-td>
> <q-td auto-width>
<div v-if="col.field == 'total'">${ col.value / 1000} sats</div> <q-btn unelevated dense size="sm" icon="qr_code" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
<div v-else>${ col.value }</div> class="q-mr-sm" @click="openUrlDialog(props.row.id)"></q-btn></q-td>
</q-td> <q-td auto-width>
<q-td auto-width> <q-btn unelevated dense size="sm" icon="launch" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
<q-btn type="a" :href="props.row.myextension" target="_blank"><q-tooltip>Open public
unelevated page</q-tooltip></q-btn></q-td>
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.myextension"
target="_blank"
><q-tooltip>Open public page</q-tooltip></q-btn
></q-td>
<q-td> <q-td>
<q-btn <q-btn flat dense size="xs" @click="updateMyExtensionForm(props.row.id)" icon="edit" color="light-blue">
flat <q-tooltip> Edit copilot </q-tooltip>
dense </q-btn>
size="xs" </q-td>
@click="updateMyExtensionForm(props.row.id)"
icon="edit" <q-td>
color="light-blue" <q-btn flat dense size="xs" @click="deleteMyExtension(props.row.id)" icon="cancel" color="pink">
> <q-tooltip> Delete copilot </q-tooltip>
<q-tooltip> Edit copilot </q-tooltip> </q-btn>
</q-btn> </q-td>
</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> </q-tr>
</template> </template>
@ -107,7 +66,8 @@
<q-card> <q-card>
<q-card-section> <q-card-section>
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} MyExtension extension</h6> <h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} MyExtension extension</h6>
<p>Simple extension you can use as a base for your own extension. <br/> Includes very simple LNURL-pay and LNURL-withdraw example.</p> <p>Simple extension you can use as a base for your own extension. <br /> Includes very simple LNURL-pay and
LNURL-withdraw example.</p>
</q-card-section> </q-card-section>
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<q-separator></q-separator> <q-separator></q-separator>
@ -123,54 +83,20 @@
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog"> <q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px"> <q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendMyExtensionData" class="q-gutter-md"> <q-form @submit="sendMyExtensionData" class="q-gutter-md">
<q-input <q-input filled dense v-model.trim="formDialog.data.name" label="Name"
filled placeholder="Name for your record"></q-input>
dense <q-select filled dense emit-value v-model="formDialog.data.wallet" :options="g.user.walletOptions"
v-model.trim="formDialog.data.name" label="Wallet *"></q-select>
label="Name" <q-input filled dense type="number" v-model.trim="formDialog.data.lnurlwithdrawamount"
placeholder="Name for your record" label="LNURL-withdraw amount"></q-input>
></q-input> <q-input filled dense type="number" v-model.trim="formDialog.data.lnurlpayamount"
<q-select label="LNURL-pay amount"></q-input>
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
></q-select>
<q-input
filled
dense
type="number"
v-model.trim="formDialog.data.lnurlwithdrawamount"
label="LNURL-withdraw amount"
></q-input>
<q-input
filled
dense
type="number"
v-model.trim="formDialog.data.lnurlpayamount"
label="LNURL-pay amount"
></q-input>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn <q-btn v-if="formDialog.data.id" unelevated color="primary" type="submit">Update MyExtension</q-btn>
v-if="formDialog.data.id" <q-btn v-else unelevated color="primary"
unelevated
color="primary"
type="submit"
>Update MyExtension</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="formDialog.data.name == null || formDialog.data.wallet == null || formDialog.data.lnurlwithdrawamount == null || formDialog.data.lnurlpayamount == null" :disable="formDialog.data.name == null || formDialog.data.wallet == null || formDialog.data.lnurlwithdrawamount == null || formDialog.data.lnurlpayamount == null"
type="submit" type="submit">Create MyExtension</q-btn>
>Create MyExtension</q-btn <q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div> </div>
</q-form> </q-form>
</q-card> </q-card>
@ -178,31 +104,30 @@
<q-dialog v-model="urlDialog.show" position="top"> <q-dialog v-model="urlDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card"> <q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-responsive :ratio="1" class="q-mx-xl q-mb-md"> <q-responsive :ratio="1" class="q-mx-xl q-mb-md">
<lnbits-qrcode <lnbits-qrcode :value="qrValue"></lnbits-qrcode>
:value="qrValue" </q-responsive>
></lnbits-qrcode> <center><q-btn label="copy" @click="copyText(qrValue)"></q-btn>
</q-responsive> </center>
<center><q-btn label="copy" @click="copyText(qrValue)"></q-btn>
</center>
<q-separator></q-separator> <q-separator></q-separator>
<div class="row justify-start q-mt-lg"> <div class="row justify-start q-mt-lg">
<div class="col col-md-auto"> <div class="col col-md-auto">
<q-btn outline style="color: primmary;" @click="qrValue = urlDialog.data.lnurlpay">lnurlpay</q-btn> <q-btn outline style="color: primmary;" @click="qrValue = urlDialog.data.lnurlpay">lnurlpay</q-btn>
</div> </div>
<div class="col col-md-auto"> <div class="col col-md-auto">
<q-btn outline style="color: primmary;" @click="qrValue = urlDialog.data.lnurlwithdraw">lnurlwithdraw</q-btn> <q-btn outline style="color: primmary;" @click="qrValue = urlDialog.data.lnurlwithdraw">lnurlwithdraw</q-btn>
</div> </div>
<div class="col q-pl-md"> <div class="col q-pl-md">
<q-input filled bottom-slots dense v-model="invoiceAmount"> <q-input filled bottom-slots dense v-model="invoiceAmount">
<template v-slot:append> <template v-slot:append>
<q-btn round @click="createInvoice(urlDialog.data.wallet, urlDialog.data.id)" color="primary" flat icon="add_circle" /> <q-btn round @click="createInvoice(urlDialog.data.wallet, urlDialog.data.id)" color="primary" flat
icon="add_circle" />
</template> </template>
<template v-slot:hint> <template v-slot:hint>
Create an invoice Create an invoice
@ -213,7 +138,7 @@
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn> <q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div> </div>
</q-card> </q-card>
@ -242,15 +167,15 @@
temps: [], temps: [],
tempsTable: { tempsTable: {
columns: [ columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'}, { name: 'id', align: 'left', label: 'ID', field: 'id' },
{name: 'name', align: 'left', label: 'Name', field: 'name'}, { name: 'name', align: 'left', label: 'Name', field: 'name' },
{ {
name: 'wallet', name: 'wallet',
align: 'left', align: 'left',
label: 'Wallet', label: 'Wallet',
field: 'wallet' field: 'wallet'
}, },
{name: 'total', align: 'left', label: 'Total sent/received', field: 'total'}, { name: 'total', align: 'left', label: 'Total sent/received', field: 'total' },
], ],
pagination: { pagination: {
rowsPerPage: 10 rowsPerPage: 10
@ -317,7 +242,7 @@
} }
}, },
updateMyExtensionForm(tempId) { updateMyExtensionForm(tempId) {
const myextension = _.findWhere(this.temps, {id: tempId}) const myextension = _.findWhere(this.temps, { id: tempId })
this.formDialog.data = { this.formDialog.data = {
...myextension ...myextension
} }
@ -361,7 +286,7 @@
}, },
deleteMyExtension: function (tempId) { deleteMyExtension: function (tempId) {
var self = this var self = this
var myextension = _.findWhere(this.temps, {id: tempId}) var myextension = _.findWhere(this.temps, { id: tempId })
LNbits.utils LNbits.utils
.confirmDialog('Are you sure you want to delete this MyExtension?') .confirmDialog('Are you sure you want to delete this MyExtension?')
@ -370,7 +295,7 @@
.request( .request(
'DELETE', 'DELETE',
'/myextension/api/v1/temps/' + tempId, '/myextension/api/v1/temps/' + tempId,
_.findWhere(self.g.user.wallets, {id: myextension.wallet}).adminkey _.findWhere(self.g.user.wallets, { id: myextension.wallet }).adminkey
) )
.then(function (response) { .then(function (response) {
self.temps = _.reject(self.temps, function (obj) { self.temps = _.reject(self.temps, function (obj) {
@ -386,17 +311,17 @@
LNbits.utils.exportCSV(this.tempsTable.columns, this.temps) LNbits.utils.exportCSV(this.tempsTable.columns, this.temps)
}, },
itemsArray(tempId) { itemsArray(tempId) {
const myextension = _.findWhere(this.temps, {id: tempId}) const myextension = _.findWhere(this.temps, { id: tempId })
return [...myextension.itemsMap.values()] return [...myextension.itemsMap.values()]
}, },
itemFormatPrice(price, id) { itemFormatPrice(price, id) {
const myextension = id.split(':')[0] const myextension = id.split(':')[0]
const currency = _.findWhere(this.temps, {id: myextension}).currency const currency = _.findWhere(this.temps, { id: myextension }).currency
return LNbits.utils.formatCurrency(Number(price).toFixed(2), currency) return LNbits.utils.formatCurrency(Number(price).toFixed(2), currency)
}, },
openformDialog(id) { openformDialog(id) {
const [tempId, itemId] = id.split(':') const [tempId, itemId] = id.split(':')
const myextension = _.findWhere(this.temps, {id: tempId}) const myextension = _.findWhere(this.temps, { id: tempId })
if (itemId) { if (itemId) {
const item = myextension.itemsMap.get(id) const item = myextension.itemsMap.get(id)
this.formDialog.data = { this.formDialog.data = {
@ -419,16 +344,16 @@
} }
}, },
openUrlDialog(id) { openUrlDialog(id) {
this.urlDialog.data = _.findWhere(this.temps, {id}) this.urlDialog.data = _.findWhere(this.temps, { id })
this.qrValue = this.urlDialog.data.lnurlpay this.qrValue = this.urlDialog.data.lnurlpay
console.log(this.urlDialog.data.id) console.log(this.urlDialog.data.id)
this.connectWebocket(this.urlDialog.data.id) this.connectWebocket(this.urlDialog.data.id)
this.urlDialog.show = true this.urlDialog.show = true
}, },
createInvoice(walletId, myextensionId) { createInvoice(walletId, myextensionId) {
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
///Simple call to the api to create an invoice///// ///Simple call to the api to create an invoice/////
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
console.log(walletId) console.log(walletId)
const wallet = _.findWhere(this.g.user.wallets, { const wallet = _.findWhere(this.g.user.wallets, {
id: walletId id: walletId
@ -486,32 +411,32 @@
} }
frame() frame()
}, },
connectWebocket(id){ connectWebocket(id) {
////////////////////////////////////////////////// //////////////////////////////////////////////////
///wait for pay action to happen and do a thing//// ///wait for pay action to happen and do a thing////
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
self = this self = this
if (location.protocol !== 'http:') { if (location.protocol !== 'http:') {
localUrl = localUrl =
'wss://' + 'wss://' +
document.domain + document.domain +
':' + ':' +
location.port + location.port +
'/api/v1/ws/' + '/api/v1/ws/' +
id id
} else { } else {
localUrl = localUrl =
'ws://' + 'ws://' +
document.domain + document.domain +
':' + ':' +
location.port + location.port +
'/api/v1/ws/' + '/api/v1/ws/' +
id id
} }
this.connection = new WebSocket(localUrl) this.connection = new WebSocket(localUrl)
this.connection.onmessage = function (e) { this.connection.onmessage = function (e) {
self.makeItRain() self.makeItRain()
} }
} }
}, },
created: function () { created: function () {
@ -521,4 +446,4 @@
} }
}) })
</script> </script>
{% endblock %} {% endblock %}

View file

@ -6,29 +6,23 @@
<div class="text-center"> <div class="text-center">
<a class="text-secondary" href="lightning:{{ lnurl }}"> <a class="text-secondary" href="lightning:{{ lnurl }}">
<q-responsive :ratio="1" class="q-mx-md"> <q-responsive :ratio="1" class="q-mx-md">
<qrcode <qrcode :value="qrValue" :options="{width: 800}" class="rounded-borders"></qrcode>
:value="qrValue"
:options="{width: 800}"
class="rounded-borders"
></qrcode>
</q-responsive> </q-responsive>
</a> </a>
</div> </div>
<div class="row q-mt-lg q-gutter-sm"> <div class="row q-mt-lg q-gutter-sm">
<q-btn outline color="grey" @click="copyText(qrValue)" <q-btn outline color="grey" @click="copyText(qrValue)">Copy LNURL </q-btn>
>Copy LNURL </q-btn
>
</div> </div>
</q-card-section> </q-card-section>
<div class="row justify-start q-mt-lg"> <div class="row justify-start q-mt-lg">
<div class="col col-md-auto"> <div class="col col-md-auto">
<q-btn outline style="color: primmary;" @click="qrValue = '{{ lnurlpay }}'">lnurlpay</q-btn> <q-btn outline style="color: primmary;" @click="qrValue = '{{ lnurlpay }}'">lnurlpay</q-btn>
</div> </div>
<div class="col col-md-auto"> <div class="col col-md-auto">
<q-btn outline style="color: primmary;" @click="qrValue = '{{ lnurlwithdraw }}'">lnurlwithdraw</q-btn> <q-btn outline style="color: primmary;" @click="qrValue = '{{ lnurlwithdraw }}'">lnurlwithdraw</q-btn>
</div> </div>
</div> </div>
@ -40,15 +34,15 @@
<h6 class="text-subtitle1 q-mb-sm q-mt-none">Public page</h6> <h6 class="text-subtitle1 q-mb-sm q-mt-none">Public page</h6>
<p class="q-my-none"> <p class="q-my-none">
Moat extensions have a public page that can be shared Moat extensions have a public page that can be shared
(this page will still be accessible even if you have restricted (this page will still be accessible even if you have restricted
access to your LNbits install). 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. In this example when a user pays the LNURLpay it triggers an event via a websocket waiting for the payment.
</p>.</p> </p>.</p>
</q-card-section> </q-card-section>
<q-card-section class="q-pa-none"> <q-card-section class="q-pa-none">
<q-separator></q-separator> <q-separator></q-separator>
<q-list> </q-list> <q-list> </q-list>
</q-card-section> </q-card-section>
</q-card> </q-card>
</div> </div>
@ -102,34 +96,34 @@
} }
frame() frame()
}, },
connectWebocket(id){ connectWebocket(id) {
////////////////////////////////////////////////// //////////////////////////////////////////////////
///wait for pay action to happen and do a thing//// ///wait for pay action to happen and do a thing////
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
self = this self = this
if (location.protocol !== 'http:') { if (location.protocol !== 'http:') {
localUrl = localUrl =
'wss://' + 'wss://' +
document.domain + document.domain +
':' + ':' +
location.port + location.port +
'/api/v1/ws/' + '/api/v1/ws/' +
id id
} else { } else {
localUrl = localUrl =
'ws://' + 'ws://' +
document.domain + document.domain +
':' + ':' +
location.port + location.port +
'/api/v1/ws/' + '/api/v1/ws/' +
id id
} }
this.connection = new WebSocket(localUrl) this.connection = new WebSocket(localUrl)
this.connection.onmessage = function (e) { this.connection.onmessage = function (e) {
self.makeItRain() self.makeItRain()
} }
} }
}, },
}) })
</script> </script>
{% endblock %} {% endblock %}

View file

@ -22,6 +22,7 @@ temps = Jinja2Templates(directory="temps")
# Backend admin page # Backend admin page
@myextension_ext.get("/", response_class=HTMLResponse) @myextension_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)): async def index(request: Request, user: User = Depends(check_user_exists)):
return myextension_renderer().TemplateResponse( return myextension_renderer().TemplateResponse(
@ -31,6 +32,7 @@ async def index(request: Request, user: User = Depends(check_user_exists)):
# Frontend shareable page # Frontend shareable page
@myextension_ext.get("/{myextension_id}") @myextension_ext.get("/{myextension_id}")
async def myextension(request: Request, myextension_id): async def myextension(request: Request, myextension_id):
myextension = await get_myextension(myextension_id, request) myextension = await get_myextension(myextension_id, request)
@ -52,9 +54,10 @@ async def myextension(request: Request, myextension_id):
# Manifest for public page, customise or remove manifest completely # Manifest for public page, customise or remove manifest completely
@myextension_ext.get("/manifest/{myextension_id}.webmanifest") @myextension_ext.get("/manifest/{myextension_id}.webmanifest")
async def manifest(myextension_id: str): async def manifest(myextension_id: str):
myextension= await get_myextension(myextension_id) myextension = await get_myextension(myextension_id)
if not myextension: if not myextension:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist." status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist."
@ -86,4 +89,4 @@ async def manifest(myextension_id: str):
"url": "/myextension/" + myextension_id, "url": "/myextension/" + myextension_id,
} }
], ],
} }

View file

@ -25,7 +25,7 @@ from .crud import (
update_myextension, update_myextension,
delete_myextension, delete_myextension,
get_myextension, get_myextension,
get_myextensions get_myextensions,
) )
from .models import CreateMyExtensionData from .models import CreateMyExtensionData
@ -36,25 +36,29 @@ from .models import CreateMyExtensionData
## Get all the records belonging to the user ## Get all the records belonging to the user
@myextension_ext.get("/api/v1/temps", status_code=HTTPStatus.OK) @myextension_ext.get("/api/v1/temps", status_code=HTTPStatus.OK)
async def api_myextensions( async def api_myextensions(
req: Request, all_wallets: req: Request,
bool = Query(False), all_wallets: bool = Query(False),
wallet: WalletTypeInfo = Depends(get_key_type) wallet: WalletTypeInfo = Depends(get_key_type),
): ):
wallet_ids = [wallet.wallet.id] wallet_ids = [wallet.wallet.id]
if all_wallets: if all_wallets:
user = await get_user(wallet.wallet.user) user = await get_user(wallet.wallet.user)
wallet_ids = user.wallet_ids if user else [] wallet_ids = user.wallet_ids if user else []
return [myextension.dict() for myextension in await get_myextensions(wallet_ids, req)] return [
myextension.dict() for myextension in await get_myextensions(wallet_ids, req)
]
## Get a single record ## Get a single record
@myextension_ext.get("/api/v1/temps/{myextension_id}", status_code=HTTPStatus.OK) @myextension_ext.get("/api/v1/temps/{myextension_id}", status_code=HTTPStatus.OK)
async def api_myextension( async def api_myextension(
req: Request, req: Request, myextension_id: str, WalletTypeInfo=Depends(get_key_type)
myextension_id: str, ):
WalletTypeInfo = Depends(get_key_type)):
myextension = await get_myextension(myextension_id, req) myextension = await get_myextension(myextension_id, req)
if not myextension: if not myextension:
raise HTTPException( raise HTTPException(
@ -62,8 +66,10 @@ async def api_myextension(
) )
return myextension.dict() return myextension.dict()
## update a record ## update a record
@myextension_ext.put("/api/v1/temps/{myextension_id}") @myextension_ext.put("/api/v1/temps/{myextension_id}")
async def api_myextension_update( async def api_myextension_update(
req: Request, req: Request,
@ -79,25 +85,33 @@ async def api_myextension_update(
assert myextension, "MyExtension couldn't be retrieved" assert myextension, "MyExtension couldn't be retrieved"
if wallet.wallet.id != myextension.wallet: if wallet.wallet.id != myextension.wallet:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension.") raise HTTPException(
myextension = await update_myextension(myextension_id=myextension_id, **data.dict(), req=req) status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension."
)
myextension = await update_myextension(
myextension_id=myextension_id, **data.dict(), req=req
)
return myextension.dict() return myextension.dict()
## Create a new record ## Create a new record
@myextension_ext.post("/api/v1/temps", status_code=HTTPStatus.CREATED) @myextension_ext.post("/api/v1/temps", status_code=HTTPStatus.CREATED)
async def api_myextension_create( async def api_myextension_create(
req: Request, req: Request,
data: CreateMyExtensionData, data: CreateMyExtensionData,
wallet: WalletTypeInfo = Depends(get_key_type) wallet: WalletTypeInfo = Depends(get_key_type),
): ):
myextension = await create_myextension(wallet_id=wallet.wallet.id, data=data, req=req) myextension = await create_myextension(
wallet_id=wallet.wallet.id, data=data, req=req
)
return myextension.dict() return myextension.dict()
## Delete a record ## Delete a record
@myextension_ext.delete("/api/v1/temps/{myextension_id}") @myextension_ext.delete("/api/v1/temps/{myextension_id}")
async def api_myextension_delete( async def api_myextension_delete(
myextension_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) myextension_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
@ -110,7 +124,9 @@ async def api_myextension_delete(
) )
if myextension.wallet != wallet.wallet.id: if myextension.wallet != wallet.wallet.id:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension.") raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="Not your MyExtension."
)
await delete_myextension(myextension_id) await delete_myextension(myextension_id)
return "", HTTPStatus.NO_CONTENT return "", HTTPStatus.NO_CONTENT
@ -120,7 +136,10 @@ async def api_myextension_delete(
## This endpoint creates a payment ## This endpoint creates a payment
@myextension_ext.post("/api/v1/temps/payment/{myextension_id}", status_code=HTTPStatus.CREATED)
@myextension_ext.post(
"/api/v1/temps/payment/{myextension_id}", status_code=HTTPStatus.CREATED
)
async def api_tpos_create_invoice( async def api_tpos_create_invoice(
myextension_id: str, amount: int = Query(..., ge=1), memo: str = "" myextension_id: str, amount: int = Query(..., ge=1), memo: str = ""
) -> dict: ) -> dict:
@ -130,7 +149,7 @@ async def api_tpos_create_invoice(
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="MyExtension does not exist." 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 # we create a payment and add some tags, so tasks.py can grab the payment once its paid
try: try:
@ -148,4 +167,4 @@ async def api_tpos_create_invoice(
except Exception as e: except Exception as e:
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
return {"payment_hash": payment_hash, "payment_request": payment_request} return {"payment_hash": payment_hash, "payment_request": payment_request}