This commit is contained in:
benarc 2021-11-28 18:11:35 +00:00
parent e3c6485903
commit b7fcf461f1
10 changed files with 521 additions and 0 deletions

View File

@ -0,0 +1,3 @@
# LNURLPayOut
## Auto-dump a wallets funds to an LNURLpay

View File

@ -0,0 +1,16 @@
from fastapi import APIRouter
from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_lnurlpayout")
lnurlpayout_ext: APIRouter = APIRouter(prefix="/lnurlpayout", tags=["lnurlpayout"])
def lnurlpayout_renderer():
return template_renderer(["lnbits/extensions/lnurlpayout/templates"])
from .views_api import * # noqa
from .views import * # noqa

View File

@ -0,0 +1,6 @@
{
"name": "LNURLPayout",
"short_description": "Autodump wallet funds to LNURLpay",
"icon": "exit_to_app",
"contributors": ["arcbtc"]
}

View File

@ -0,0 +1,42 @@
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import lnurlpayout, CreateLnurlPayoutData
async def create_lnurlpayout(wallet_id: str, data: CreateLnurlPayoutData) -> lnurlpayout:
lnurlpayout_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO lnurlpayout.lnurlpayouts (id, wallet, lnurlpay, threshold)
VALUES (?, ?, ?, ?)
""",
(lnurlpayout_id, wallet_id, data.name, data.currency),
)
lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
assert lnurlpayout, "Newly created lnurlpayout couldn't be retrieved"
return lnurlpayout
async def get_lnurlpayout(lnurlpayout_id: str) -> Optional[lnurlpayout]:
row = await db.fetchone("SELECT * FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,))
return lnurlpayout.from_row(row) if row else None
async def get_lnurlpayouts(wallet_ids: Union[str, List[str]]) -> List[lnurlpayout]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall(
f"SELECT * FROM lnurlpayout.lnurlpayouts WHERE wallet IN ({q})", (*wallet_ids,)
)
return [lnurlpayout.from_row(row) for row in rows]
async def delete_lnurlpayout(lnurlpayout_id: str) -> None:
await db.execute("DELETE FROM lnurlpayout.lnurlpayouts WHERE id = ?", (lnurlpayout_id,))

View File

@ -0,0 +1,14 @@
async def m001_initial(db):
"""
Initial lnurlpayouts table.
"""
await db.execute(
"""
CREATE TABLE lnurlpayout.lnurlpayouts (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
lnurlpay TEXT NOT NULL,
threshold INT NOT NULL
);
"""
)

View File

@ -0,0 +1,14 @@
from sqlite3 import Row
from pydantic import BaseModel
class CreateLnurlPayoutData(BaseModel):
wallet: str
lnurlpay: str
threshold: int
class lnurlpayout(BaseModel):
id: str
wallet: str
lnurlpay: str
threshold: str

View File

@ -0,0 +1,90 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-expansion-item group="api" dense expand-separator label="List lnurlpayout">
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/lnurlpayout/api/v1/lnurlpayouts</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>[&lt;lnurlpayout_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}api/v1/lnurlpayouts -H "X-Api-Key:
&lt;invoice_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Create a lnurlpayout"
>
<q-card>
<q-card-section>
<code
><span class="text-green">POST</span>
/lnurlpayout/api/v1/lnurlpayouts</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;invoice_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<code
>{"name": &lt;string&gt;, "currency": &lt;string*ie USD*&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code
>{"currency": &lt;string&gt;, "id": &lt;string&gt;, "name":
&lt;string&gt;, "wallet": &lt;string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.base_url }}api/v1/lnurlpayouts -d '{"name":
&lt;string&gt;, "currency": &lt;string&gt;}' -H "Content-type:
application/json" -H "X-Api-Key: &lt;admin_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Delete a lnurlpayout"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-pink">DELETE</span>
/lnurlpayout/api/v1/lnurlpayouts/&lt;lnurlpayout_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
<code>{"X-Api-Key": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Returns 204 NO CONTENT</h5>
<code></code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X DELETE {{ request.base_url
}}api/v1/lnurlpayouts/&lt;lnurlpayout_id&gt; -H "X-Api-Key:
&lt;admin_key&gt;"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>

View File

@ -0,0 +1,262 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn unelevated color="primary" @click="formDialog.show = true"
>New LNURLPayout</q-btn
>
</q-card-section>
</q-card>
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">LNURLPayout</h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSV</q-btn>
</div>
</div>
<q-table
dense
flat
:data="lnurlpayouts"
row-key="id"
:columns="lnurlpayoutsTable.columns"
:pagination.sync="lnurlpayoutsTable.pagination"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
type="a"
:href="props.row.lnurlpayout"
target="_blank"
></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="deletelnurlpayout(props.row.id)"
icon="cancel"
color="pink"
></q-btn>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} LNURLPayout extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "lnurlpayout/_api_docs.html" %}
<q-separator></q-separator>
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog v-model="formDialog.show" position="top" @hide="closeFormDialog">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="createlnurlpayout" class="q-gutter-md">
<q-select
filled
dense
emit-value
v-model="formDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
></q-select>
<q-input
filled
dense
v-model.trim="formDialog.data.lnurlpay"
label="LNURLPay"
placeholder="LNURLPay"
type="text"
></q-input>
<q-input
filled
dense
v-model.trim="formDialog.data.threshold"
label="Threshold"
placeholder="Threshold"
type="number"
></q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="formDialog.data.threshold == null"
type="submit"
>Create LNURLPayout</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
var maplnurlpayout = function (obj) {
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
obj.fsat = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.lnurlpayout = ['/lnurlpayout/', obj.id].join('')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
lnurlpayouts: [],
lnurlpayoutsTable: {
columns: [
{name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'},
{
name: 'lnurlpay',
align: 'left',
label: 'LNURLPay',
field: 'lnurlpay'
},
{
name: 'threshold',
align: 'left',
label: 'Threshold',
field: 'threshold'
}
],
pagination: {
rowsPerPage: 10
}
},
formDialog: {
show: false,
data: {}
}
}
},
methods: {
closeFormDialog: function () {
this.formDialog.data = {}
},
getlnurlpayouts: function () {
var self = this
LNbits.api
.request(
'GET',
'/lnurlpayout/api/v1/lnurlpayouts?all_wallets=true',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.lnurlpayouts = response.data.map(function (obj) {
return maplnurlpayout(obj)
})
})
},
createlnurlpayout: function () {
var data = {
lnurlpay: this.formDialog.data.lnurlpay,
threshold: this.formDialog.data.threshold
}
var self = this
LNbits.api
.request(
'POST',
'/lnurlpayout/api/v1/lnurlpayouts',
_.findWhere(this.g.user.wallets, {id: this.formDialog.data.wallet})
.inkey,
data
)
.then(function (response) {
console.log(data)
self.lnurlpayouts.push(maplnurlpayout(response.data))
self.formDialog.show = false
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deletelnurlpayout: function (lnurlpayoutId) {
var self = this
var lnurlpayout = _.findWhere(this.lnurlpayouts, {id: lnurlpayoutId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this lnurlpayout?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/lnurlpayout/api/v1/lnurlpayouts/' + lnurlpayoutId,
_.findWhere(self.g.user.wallets, {id: lnurlpayout.wallet})
.adminkey
)
.then(function (response) {
self.lnurlpayouts = _.reject(self.lnurlpayouts, function (obj) {
return obj.id == lnurlpayoutId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportCSV: function () {
LNbits.utils.exportCSV(
this.lnurlpayoutsTable.columns,
this.lnurlpayouts
)
}
},
created: function () {
if (this.g.user.wallets.length) {
this.getlnurlpayouts()
}
}
})
</script>
{% endblock %}

View File

@ -0,0 +1,22 @@
from http import HTTPStatus
from fastapi import Request
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import lnurlpayout_ext, lnurlpayout_renderer
from .crud import get_lnurlpayout
templates = Jinja2Templates(directory="templates")
@lnurlpayout_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return lnurlpayout_renderer().TemplateResponse(
"lnurlpayout/index.html", {"request": request, "user": user.dict()}
)

View File

@ -0,0 +1,52 @@
from http import HTTPStatus
from fastapi import Query
from fastapi.params import Depends
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.core.services import create_invoice
from lnbits.core.views.api import api_payment
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from . import lnurlpayout_ext
from .crud import create_lnurlpayout, delete_lnurlpayout, get_lnurlpayout, get_lnurlpayouts
from .models import lnurlpayout, CreateLnurlPayoutData
@lnurlpayout_ext.get("/api/v1/lnurlpayouts", status_code=HTTPStatus.OK)
async def api_lnurlpayouts(
all_wallets: bool = Query(None), wallet: WalletTypeInfo = Depends(get_key_type)
):
wallet_ids = [wallet.wallet.id]
if all_wallets:
wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids
return [lnurlpayout.dict() for lnurlpayout in await get_lnurlpayouts(wallet_ids)]
@lnurlpayout_ext.post("/api/v1/lnurlpayouts", status_code=HTTPStatus.CREATED)
async def api_lnurlpayout_create(
data: CreateLnurlPayoutData, wallet: WalletTypeInfo = Depends(get_key_type)
):
print("data")
# lnurlpayout = await create_lnurlpayout(wallet_id=wallet.wallet.id, data=data)
return #lnurlpayout.dict()
@lnurlpayout_ext.delete("/api/v1/lnurlpayouts/{lnurlpayout_id}")
async def api_lnurlpayout_delete(
lnurlpayout_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
):
lnurlpayout = await get_lnurlpayout(lnurlpayout_id)
if not lnurlpayout:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="lnurlpayout does not exist."
)
if lnurlpayout.wallet != wallet.wallet.id:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your lnurlpayout.")
await delete_lnurlpayout(lnurlpayout_id)
raise HTTPException(status_code=HTTPStatus.NO_CONTENT)