Browse Source

initial commit

atmext
benarc 4 years ago
parent
commit
2d2ba0bcf2
  1. 14
      lnbits/extensions/offlinelnurlw/README.md
  2. 8
      lnbits/extensions/offlinelnurlw/__init__.py
  3. 6
      lnbits/extensions/offlinelnurlw/config.json
  4. 77
      lnbits/extensions/offlinelnurlw/crud.py
  5. 15
      lnbits/extensions/offlinelnurlw/migrations.py
  6. 20
      lnbits/extensions/offlinelnurlw/models.py
  7. 217
      lnbits/extensions/offlinelnurlw/static/js/index.js
  8. 155
      lnbits/extensions/offlinelnurlw/templates/offlinelnurlw/_api_docs.html
  9. 29
      lnbits/extensions/offlinelnurlw/templates/offlinelnurlw/_lnurl.html
  10. 160
      lnbits/extensions/offlinelnurlw/templates/offlinelnurlw/index.html
  11. 13
      lnbits/extensions/offlinelnurlw/views.py
  12. 174
      lnbits/extensions/offlinelnurlw/views_api.py

14
lnbits/extensions/offlinelnurlw/README.md

@ -0,0 +1,14 @@
# ATM
## ATM link maker
LNURL withdraw is a very powerful tool and should not have his use limited to just faucet applications. With LNURL withdraw, you have the ability to give someone the right to spend a range, once or multiple times. This functionality has not existed in money before.
https://github.com/btcontract/lnurl-rfc/blob/master/spec.md#3-lnurl-withdraw
With this extension to can create/edit LNURL withdraws, set a min/max amount, set time (useful for subscription services)
![lnurl](https://i.imgur.com/qHi7ExL.png)
## API endpoint - /withdraw/api/v1/lnurlmaker
Easily fetch one-off LNURLw
curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/withdraw/api/v1/lnurlmaker -d '{"amount":"100","memo":"ATM"}' -H "X-Api-Key: YOUR-WALLET-ADMIN-KEY"

8
lnbits/extensions/offlinelnurlw/__init__.py

@ -0,0 +1,8 @@
from quart import Blueprint
offlinelnurlw_ext: Blueprint = Blueprint("offlinelnurlw", __name__, static_folder="static", template_folder="templates")
from .views_api import * # noqa
from .views import * # noqa

6
lnbits/extensions/offlinelnurlw/config.json

@ -0,0 +1,6 @@
{
"name": "OfflineLNURLw",
"short_description": "Offline LNURLw ATM, faucet links",
"icon": "crop_free",
"contributors": ["arcbtc"]
}

77
lnbits/extensions/offlinelnurlw/crud.py

@ -0,0 +1,77 @@
from datetime import datetime
from typing import List, Optional, Union
from lnbits.db import open_ext_db
from lnbits.helpers import urlsafe_short_hash
from .models import offlinelnurlwLink
import ecdsa
from hashlib import sha256
def create_offlinelnurlw_link(
title: str,
wallet_id: str,
) -> offlinelnurlwLink:
print("poo")
with open_ext_db("offlinelnurlw") as db:
link_id = urlsafe_short_hash()
private_key = urlsafe_short_hash()
db.execute(
"""
INSERT INTO offlinelnurlw_link (
id,
title,
wallet,
private_key,
amount,
used
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
link_id,
title,
wallet_id,
private_key,
0,
0,
),
)
return get_offlinelnurlw_link(link_id, 0)
def get_offlinelnurlw_link(link_id: str, hash_id=None, num=0) -> Optional[offlinelnurlwLink]:
with open_ext_db("offlinelnurlw") as db:
row = db.fetchone("SELECT * FROM offlinelnurlw_link WHERE id = ?", (link_id,))
if hash_id:
vk = ecdsa.VerifyingKey.from_string(bytes.fromhex(link_id), curve=ecdsa.SECP256k1, hashfunc=sha256)
if not vk.verify(bytes.fromhex(row[3]), hash_id):
return None
return offlinelnurlwLink(**row) if row else None
def get_offlinelnurlw_links(wallet_ids: Union[str, List[str]]) -> List[offlinelnurlwLink]:
if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids]
with open_ext_db("offlinelnurlw") as db:
q = ",".join(["?"] * len(wallet_ids))
rows = db.fetchall(f"SELECT * FROM offlinelnurlw_link WHERE wallet IN ({q})", (*wallet_ids,))
return [offlinelnurlwLink.from_row(row) for row in rows]
def update_offlinelnurlw_link(link_id: str, **kwargs) -> Optional[offlinelnurlwLink]:
q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()])
with open_ext_db("offlinelnurlw") as db:
db.execute(f"UPDATE offlinelnurlw_link SET {q} WHERE id = ?", (*kwargs.values(), link_id))
row = db.fetchone("SELECT * FROM offlinelnurlw_link WHERE id = ?", (link_id,))
return offlinelnurlwLink.from_row(row) if row else None
def delete_offlinelnurlw_link(link_id: str) -> None:
with open_ext_db("offlinelnurlw") as db:
db.execute("DELETE FROM offlinelnurlw_link WHERE id = ?", (link_id,))

15
lnbits/extensions/offlinelnurlw/migrations.py

@ -0,0 +1,15 @@
def m001_initial(db):
db.execute(
"""
CREATE TABLE IF NOT EXISTS offlinelnurlw_link (
id TEXT PRIMARY KEY,
wallet TEXT,
title TEXT,
private_key TEXT,
amount INT,
used INTEGER DEFAULT 0,
time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now'))
);
"""
)

20
lnbits/extensions/offlinelnurlw/models.py

@ -0,0 +1,20 @@
from quart import url_for
from lnurl import Lnurl, LnurlWithdrawResponse, encode as lnurl_encode
from sqlite3 import Row
from typing import NamedTuple
import shortuuid # type: ignore
class offlinelnurlwLink(NamedTuple):
id: str
wallet: str
title: str
private_key: str
amount: int
used: int
time: int
@classmethod
def from_row(cls, row: Row) -> "offlinelnurlwLink":
data = dict(row)
return cls(**data)

217
lnbits/extensions/offlinelnurlw/static/js/index.js

@ -0,0 +1,217 @@
/* global Vue, VueQrcode, _, Quasar, LOCALE, windowMixin, LNbits */
Vue.component(VueQrcode.name, VueQrcode)
var locationPath = [
window.location.protocol,
'//',
window.location.host,
window.location.pathname
].join('')
var mapWithdrawLink = function (obj) {
obj._data = _.clone(obj)
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm')
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
format: 'po',
checker: null,
withdrawLinks: [],
withdrawLinksTable: {
columns: [
{name: 'title', align: 'left', label: 'Title', field: 'title'},
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'private_key', align: 'left', label: 'Key', field: 'private_key'},
{
name: 'amount',
align: 'right',
label: 'Amount withdrawn',
field: 'amount'
},
],
pagination: {
rowsPerPage: 10
}
},
formDialog: {
show: false,
secondMultiplier: 'seconds',
secondMultiplierOptions: ['seconds', 'minutes', 'hours'],
data: {
is_unique: false
}
},
simpleformDialog: {
show: false,
data: {
is_unique: false,
title: 'ATM link',
min_withdrawable: 0,
wait_time: 1
}
},
qrCodeDialog: {
show: false,
data: null
}
}
},
computed: {
sortedWithdrawLinks: function () {
return this.withdrawLinks.sort(function (a, b) {
return b.uses_left - a.uses_left
})
}
},
methods: {
getWithdrawLinks: function () {
var self = this
LNbits.api
.request(
'GET',
'/offlinelnurlw/api/v1/links?all_wallets',
this.g.user.wallets[0].inkey
)
.then(function (response) {
self.withdrawLinks = response.data.map(function (obj) {
return mapWithdrawLink(obj)
})
})
.catch(function (error) {
clearInterval(self.checker)
LNbits.utils.notifyApiError(error)
})
},
closeFormDialog: function () {
this.formDialog.data = {
is_unique: false
}
},
simplecloseFormDialog: function () {
this.simpleformDialog.data = {
is_unique: false
}
},
openQrCodeDialog: function (linkId) {
var link = _.findWhere(this.withdrawLinks, {id: linkId})
this.qrCodeDialog.data = _.clone(link)
console.log(this.qrCodeDialog.data)
this.qrCodeDialog.data.url = window.location.host
this.qrCodeDialog.show = true
},
openUpdateDialog: function (linkId) {
var link = _.findWhere(this.withdrawLinks, {id: linkId})
this.formDialog.data = _.clone(link._data)
this.formDialog.show = true
},
sendFormData: function () {
var wallet = _.findWhere(this.g.user.wallets, {
id: this.formDialog.data.wallet
})
var data = _.omit(this.formDialog.data, 'wallet')
if (data.id) {
this.updateWithdrawLink(wallet, data)
} else {
this.createWithdrawLink(wallet, data)
}
},
simplesendFormData: function () {
var wallet = _.findWhere(this.g.user.wallets, {
id: this.simpleformDialog.data.wallet
})
var data = _.omit(this.simpleformDialog.data, 'wallet')
if (data.id) {
this.updateWithdrawLink(wallet, data)
} else {
this.createWithdrawLink(wallet, data)
}
},
updateWithdrawLink: function (wallet, data) {
var self = this
LNbits.api
.request(
'PUT',
'/offlinelnurlw/api/v1/links/' + data.id,
wallet.adminkey,
_.pick(
data,
'title'
)
)
.then(function (response) {
self.withdrawLinks = _.reject(self.withdrawLinks, function (obj) {
return obj.id === data.id
})
self.withdrawLinks.push(mapWithdrawLink(response.data))
self.formDialog.show = false
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
createWithdrawLink: function (wallet, data) {
var self = this
LNbits.api
.request('POST', '/offlinelnurlw/api/v1/links', wallet.adminkey, data)
.then(function (response) {
self.withdrawLinks.push(mapWithdrawLink(response.data))
self.formDialog.show = false
self.simpleformDialog.show = false
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
deleteWithdrawLink: function (linkId) {
var self = this
var link = _.findWhere(this.withdrawLinks, {id: linkId})
LNbits.utils
.confirmDialog('Are you sure you want to delete this offline withdraw link?')
.onOk(function () {
LNbits.api
.request(
'DELETE',
'/offlinelnurlw/api/v1/links/' + linkId,
_.findWhere(self.g.user.wallets, {id: link.wallet}).adminkey
)
.then(function (response) {
self.withdrawLinks = _.reject(self.withdrawLinks, function (obj) {
return obj.id === linkId
})
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
})
},
exportCSV: function () {
LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls)
}
},
created: function () {
var self = this
self.format = window.location.hostname
if (self.g.user.wallets.length) {
var getWithdrawLinks = self.getWithdrawLinks
getWithdrawLinks()
self.checker = setInterval(function () {
getWithdrawLinks()
}, 20000)
}
}
})

155
lnbits/extensions/offlinelnurlw/templates/offlinelnurlw/_api_docs.html

@ -0,0 +1,155 @@
<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 withdraw links"
>
<q-card>
<q-card-section>
<code><span class="text-blue">GET</span> /withdraw/api/v1/links</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;withdraw_link_object&gt;, ...]</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}withdraw/api/v1/links -H
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Get a withdraw link"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/withdraw/api/v1/links/&lt;withdraw_id&gt;</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 201 CREATED (application/json)
</h5>
<code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root
}}withdraw/api/v1/links/&lt;withdraw_id&gt; -H "X-Api-Key: {{
g.user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Create a withdraw link"
>
<q-card>
<q-card-section>
<code><span class="text-green">POST</span> /withdraw/api/v1/links</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">Body (application/json)</h5>
<code
>{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X POST {{ request.url_root }}withdraw/api/v1/links -d
'{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -H
"Content-type: application/json" -H "X-Api-Key: {{
g.user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Update a withdraw link"
>
<q-card>
<q-card-section>
<code
><span class="text-green">PUT</span>
/withdraw/api/v1/links/&lt;withdraw_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">Body (application/json)</h5>
<code
>{"title": &lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code>{"lnurl": &lt;string&gt;}</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X PUT {{ request.url_root
}}withdraw/api/v1/links/&lt;withdraw_id&gt; -d '{"title":
&lt;string&gt;, "min_withdrawable": &lt;integer&gt;,
"max_withdrawable": &lt;integer&gt;, "uses": &lt;integer&gt;,
"wait_time": &lt;integer&gt;, "is_unique": &lt;boolean&gt;}' -H
"Content-type: application/json" -H "X-Api-Key: {{
g.user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Delete a withdraw link"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-pink">DELETE</span>
/withdraw/api/v1/links/&lt;withdraw_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.url_root
}}withdraw/api/v1/links/&lt;withdraw_id&gt; -H "X-Api-Key: {{
g.user.wallets[0].adminkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>

29
lnbits/extensions/offlinelnurlw/templates/offlinelnurlw/_lnurl.html

@ -0,0 +1,29 @@
<q-expansion-item group="extras" icon="info" label="Powered by LNURL">
<q-card>
<q-card-section>
<p>
<b>WARNING: LNURL must be used over https or TOR</b><br />
LNURL is a range of lightning-network standards that allow us to use
lightning-network differently. An LNURL withdraw is the permission for
someone to pull a certain amount of funds from a lightning wallet. In
this extension time is also added - an amount can be withdraw over a
period of time. A typical use case for an LNURL withdraw is a faucet,
although it is a very powerful technology, with much further reaching
implications. For example, an LNURL withdraw could be minted to pay for
a subscription service.
</p>
<p>
Exploring LNURL and finding use cases, is really helping inform
lightning protocol development, rather than the protocol dictating how
lightning-network should be engaged with.
</p>
<small
>Check
<a href="https://github.com/fiatjaf/awesome-lnurl" target="_blank"
>Awesome LNURL</a
>
for further information.</small
>
</q-card-section>
</q-card>
</q-expansion-item>

160
lnbits/extensions/offlinelnurlw/templates/offlinelnurlw/index.html

@ -0,0 +1,160 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block scripts %} {{ window_vars(user) }}
<script type="text/javascript" src="/offlinelnurlw/static/js/index.js"></script>
{% endblock %} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-card-section>
<q-btn
unelevated
color="deep-purple"
@click="simpleformDialog.show = true"
>OfflineLNURLw link</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">OfflineLNURLw linkss <small v-model.trim="format"></small></h5>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportCSV">Export to CSVv</q-btn>
</div>
</div>
<q-table
dense
flat
:data="sortedWithdrawLinks"
row-key="id"
:columns="withdrawLinksTable.columns"
:pagination.sync="withdrawLinksTable.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.withdraw_url"
target="_blank"
></q-btn>
<q-btn
unelevated
dense
size="xs"
icon="visibility"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="openQrCodeDialog(props.row.id)"
></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="openUpdateDialog(props.row.id)"
icon="edit"
color="light-blue"
></q-btn>
<q-btn
flat
dense
size="xs"
@click="deleteWithdrawLink(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">
LNbits OfflineLNURLw extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "offlinelnurlw/_api_docs.html" %}
<q-separator></q-separator>
{% include "offlinelnurlw/_lnurl.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<q-dialog
v-model="simpleformDialog.show"
position="top"
@hide="simplecloseFormDialog"
>
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="simplesendFormData" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="simpleformDialog.data.title"
label="Name"
placeholder="Title"
></q-input>
<q-select
filled
dense
emit-value
v-model="simpleformDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
>
</q-select>
<div class="row q-mt-lg">
<q-btn
unelevated
color="deep-purple"
:disable="
simpleformDialog.data.wallet == null || simpleformDialog.data.title == null"
type="submit"
>Create link</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 %}

13
lnbits/extensions/offlinelnurlw/views.py

@ -0,0 +1,13 @@
from quart import g, abort, render_template
from http import HTTPStatus
from lnbits.decorators import check_user_exists, validate_uuids
from lnbits.extensions.offlinelnurlw import offlinelnurlw_ext
@offlinelnurlw_ext.route("/")
@validate_uuids(["usr"], required=True)
@check_user_exists()
async def index():
return await render_template("offlinelnurlw/index.html", user=g.user)

174
lnbits/extensions/offlinelnurlw/views_api.py

@ -0,0 +1,174 @@
from datetime import datetime
from quart import g, jsonify, request
from http import HTTPStatus
from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl
import shortuuid # type: ignore
from lnbits.core.crud import get_user
from lnbits.core.services import pay_invoice
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
from lnbits.extensions.offlinelnurlw import offlinelnurlw_ext
from .crud import (
create_offlinelnurlw_link,
get_offlinelnurlw_link,
get_offlinelnurlw_links,
update_offlinelnurlw_link,
delete_offlinelnurlw_link,
)
@offlinelnurlw_ext.route("/api/v1/links", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_links():
wallet_ids = [g.wallet.id]
if "all_wallets" in request.args:
wallet_ids = get_user(g.wallet.user).wallet_ids
try:
return (
jsonify([{**link._asdict()} for link in get_offlinelnurlw_links(wallet_ids)]),
HTTPStatus.OK,
)
except LnurlInvalidUrl:
return (
jsonify({"message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor."}),
HTTPStatus.UPGRADE_REQUIRED,
)
@offlinelnurlw_ext.route("/api/v1/links/<link_id>", methods=["GET"])
@api_check_wallet_key("invoice")
async def api_link_retrieve(link_id):
link = get_offlinelnurlw_link(link_id, 0)
if not link:
return jsonify({"message": "offlinelnurlw link does not exist."}), HTTPStatus.NOT_FOUND
if link.wallet != g.wallet.id:
return jsonify({"message": "Not your offlinelnurlw link."}), HTTPStatus.FORBIDDEN
return jsonify({**link._asdict()}), HTTPStatus.OK
@offlinelnurlw_ext.route("/api/v1/links", methods=["POST"])
@api_check_wallet_key("admin")
@api_validate_post_request(
schema={
"title": {"type": "string", "empty": False, "required": True}
}
)
async def api_link_create(link_id=None):
link = create_offlinelnurlw_link(wallet_id=g.wallet.id, title=g.data['title'])
return jsonify({**link._asdict()}), HTTPStatus.OK if link_id else HTTPStatus.CREATED
@offlinelnurlw_ext.route("/api/v1/links/<link_id>", methods=["DELETE"])
@api_check_wallet_key("admin")
async def api_link_delete(link_id):
link = get_offlinelnurlw_link(link_id)
if not link:
return jsonify({"message": "offlineLNURLw link does not exist."}), HTTPStatus.NOT_FOUND
if link.wallet != g.wallet.id:
return jsonify({"message": "Not your offlineLNURLw link."}), HTTPStatus.FORBIDDEN
delete_offlinelnurlw_link(link_id)
return "", HTTPStatus.NO_CONTENT
# FOR LNURLs WHICH ARE NOT UNIQUE
@offlinelnurlw_ext.route("/api/v1/lnurl/<unique_hash>", methods=["GET"])
async def api_lnurl_response(unique_hash):
link = get_offlinelnurlw_link_by_hash(unique_hash)
if not link:
return jsonify({"status": "ERROR", "reason": "LNURL-offlinelnurlw not found."}), HTTPStatus.OK
if link.is_unique == 1:
return jsonify({"status": "ERROR", "reason": "LNURL-offlinelnurlw not found."}), HTTPStatus.OK
usescsv = ""
for x in range(1, link.uses - link.used):
usescsv += "," + str(1)
usescsv = usescsv[1:]
link = update_offlinelnurlw_link(link.id, used=link.used + 1, usescsv=usescsv)
return jsonify(link.lnurl_response.dict()), HTTPStatus.OK
# FOR LNURLs WHICH ARE UNIQUE
@offlinelnurlw_ext.route("/api/v1/lnurl/<unique_hash>/<id_unique_hash>", methods=["GET"])
async def api_lnurl_multi_response(unique_hash, id_unique_hash):
link = get_offlinelnurlw_link_by_hash(unique_hash)
if not link:
return jsonify({"status": "ERROR", "reason": "LNURL-offlinelnurlw not found."}), HTTPStatus.OK
useslist = link.usescsv.split(",")
usescsv = ""
found = False
if link.is_unique == 0:
for x in range(link.uses - link.used):
usescsv += "," + str(1)
else:
for x in useslist:
tohash = link.id + link.unique_hash + str(x)
if id_unique_hash == shortuuid.uuid(name=tohash):
found = True
else:
usescsv += "," + x
if not found:
return jsonify({"status": "ERROR", "reason": "LNURL-offlinelnurlw not found."}), HTTPStatus.OK
usescsv = usescsv[1:]
link = update_offlinelnurlw_link(link.id, used=link.used + 1, usescsv=usescsv)
return jsonify(link.lnurl_response.dict()), HTTPStatus.OK
@offlinelnurlw_ext.route("/api/v1/lnurl/cb/<unique_hash>", methods=["GET"])
async def api_lnurl_callback(unique_hash):
link = get_offlinelnurlw_link_by_hash(unique_hash)
k1 = request.args.get("k1", type=str)
payment_request = request.args.get("pr", type=str)
now = int(datetime.now().timestamp())
if not link:
return jsonify({"status": "ERROR", "reason": "LNURL-offlinelnurlw not found."}), HTTPStatus.OK
if link.is_spent:
return jsonify({"status": "ERROR", "reason": "offlinelnurlw is spent."}), HTTPStatus.OK
if link.k1 != k1:
return jsonify({"status": "ERROR", "reason": "Bad request."}), HTTPStatus.OK
if now < link.open_time:
return jsonify({"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."}), HTTPStatus.OK
try:
pay_invoice(
wallet_id=link.wallet,
payment_request=payment_request,
max_sat=link.max_offlinelnurlwable,
extra={"tag": "offlinelnurlw"},
)
changes = {
"open_time": link.wait_time + now,
}
update_offlinelnurlw_link(link.id, **changes)
except ValueError as e:
return jsonify({"status": "ERROR", "reason": str(e)}), HTTPStatus.OK
except PermissionError:
return jsonify({"status": "ERROR", "reason": "offlinelnurlw link is empty."}), HTTPStatus.OK
except Exception as e:
return jsonify({"status": "ERROR", "reason": str(e)}), HTTPStatus.OK
return jsonify({"status": "OK"}), HTTPStatus.OK
Loading…
Cancel
Save