Merge branch 'main' into Uthpalamain

This commit is contained in:
ben 2023-01-07 21:55:58 +00:00
commit 29f5f44e1b
37 changed files with 557 additions and 145 deletions

View file

@ -1,6 +1,6 @@
import secrets import secrets
from datetime import datetime from datetime import datetime
from typing import List, Optional, Union from typing import List, Optional
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
@ -66,9 +66,9 @@ async def update_card(card_id: str, **kwargs) -> Optional[Card]:
return Card(**row) if row else None return Card(**row) if row else None
async def get_cards(wallet_ids: Union[str, List[str]]) -> List[Card]: async def get_cards(wallet_ids: List[str]) -> List[Card]:
if isinstance(wallet_ids, str): if len(wallet_ids) == 0:
wallet_ids = [wallet_ids] return []
q = ",".join(["?"] * len(wallet_ids)) q = ",".join(["?"] * len(wallet_ids))
rows = await db.fetchall( rows = await db.fetchall(
@ -130,7 +130,7 @@ async def delete_card(card_id: str) -> None:
for hit in hits: for hit in hits:
await db.execute("DELETE FROM boltcards.hits WHERE id = ?", (hit.id,)) await db.execute("DELETE FROM boltcards.hits WHERE id = ?", (hit.id,))
# Delete refunds # Delete refunds
refunds = await get_refunds([hit]) refunds = await get_refunds([hit.id])
for refund in refunds: for refund in refunds:
await db.execute( await db.execute(
"DELETE FROM boltcards.refunds WHERE id = ?", (refund.hit_id,) "DELETE FROM boltcards.refunds WHERE id = ?", (refund.hit_id,)
@ -169,7 +169,7 @@ async def get_hit(hit_id: str) -> Optional[Hit]:
return Hit.parse_obj(hit) return Hit.parse_obj(hit)
async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]: async def get_hits(cards_ids: List[str]) -> List[Hit]:
if len(cards_ids) == 0: if len(cards_ids) == 0:
return [] return []
@ -266,7 +266,7 @@ async def get_refund(refund_id: str) -> Optional[Refund]:
return Refund.parse_obj(refund) return Refund.parse_obj(refund)
async def get_refunds(hits_ids: List[Hit]) -> List[Refund]: async def get_refunds(hits_ids: List[str]) -> List[Refund]:
if len(hits_ids) == 0: if len(hits_ids) == 0:
return [] return []

View file

@ -158,5 +158,8 @@ async def api_refunds(
for card in cards: for card in cards:
cards_ids.append(card.id) cards_ids.append(card.id)
hits = await get_hits(cards_ids) hits = await get_hits(cards_ids)
hits_ids = []
for hit in hits:
hits_ids.append(hit.id)
return [refund.dict() for refund in await get_refunds(hits)] return [refund.dict() for refund in await get_refunds(hits_ids)]

View file

@ -1,16 +1,34 @@
import asyncio
from fastapi import APIRouter from fastapi import APIRouter
from fastapi.staticfiles import StaticFiles
from lnbits.db import Database from lnbits.db import Database
from lnbits.helpers import template_renderer from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
db = Database("ext_example") db = Database("ext_example")
example_ext: APIRouter = APIRouter(prefix="/example", tags=["example"]) example_ext: APIRouter = APIRouter(prefix="/example", tags=["example"])
example_static_files = [
{
"path": "/example/static",
"app": StaticFiles(packages=[("lnbits", "extensions/example/static")]),
"name": "example_static",
}
]
def example_renderer(): def example_renderer():
return template_renderer(["lnbits/extensions/example/templates"]) return template_renderer(["lnbits/extensions/example/templates"])
from .views import * # noqa from .tasks import wait_for_paid_invoices
from .views_api import * # noqa from .views import *
from .views_api import *
def tpos_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))

View file

@ -0,0 +1,6 @@
{
"name": "Build your own!",
"short_description": "Extension building guide",
"tile": "/example/static/bitcoin-extension.png",
"contributors": ["github_username"]
}

View file

@ -0,0 +1,5 @@
# crud.py is for communication with your extensions database
# add your dependencies here
# add your fnctions here

View file

@ -1,6 +0,0 @@
{
"name": "Build your own!!",
"short_description": "Join us, make an extension",
"tile": "/cashu/static/image/tile.png",
"contributors": ["github_username"]
}

View file

@ -1,3 +1,5 @@
# migrations.py is for building your database
# async def m001_initial(db): # async def m001_initial(db):
# await db.execute( # await db.execute(
# f""" # f"""

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View file

@ -0,0 +1,29 @@
# tasks.py is for asynchronous when invoices get paid
# add your dependencies here
import asyncio
from loguru import logger
from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener
async def wait_for_paid_invoices():
invoice_queue = asyncio.Queue()
register_invoice_listener(invoice_queue, get_current_extension_name())
while True:
payment = await invoice_queue.get()
await on_invoice_paid(payment)
async def on_invoice_paid(payment: Payment) -> None:
if (
payment.extra.get("tag") != "example"
): # Will grab any payment with the tag "example"
logger.debug(payment)
# Do something
return

View file

@ -1,58 +1,338 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context {% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %} %} {% block page %}
<q-card>
<q-dialog v-model="thingDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="sendThingDialog" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="thingDialog.data.name"
type="text"
label="Name *"
></q-input>
<q-input
filled
dense
v-model.trim="thingDialog.data.email"
type="email"
label="Name *"
></q-input>
<q-select
filled
dense
emit-value
v-model="thingDialog.data.wallet"
:options="g.user.walletOptions"
label="Wallet *"
></q-select>
<div class="row q-mt-lg">
<q-btn
v-if="thingDialog.data.id"
unelevated
color="primary"
type="submit"
>Update thing</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="thingDialog.data.name == null"
type="submit"
>Create thing</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<q-card flat>
<q-card-section> <q-card-section>
<h5 class="text-subtitle1 q-mt-none q-mb-md"> <div class="text-h5 q-mb-md">
Frameworks used by {{SITE_TITLE}} {{SITE_TITLE}} Extension Development Guide
</h5> <small>(Collection of resources for extension developers)</small>
<q-list> </div>
<q-item
v-for="tool in tools" <q-card unelevated flat>
:key="tool.name" <q-tabs
tag="a" v-model="tab"
:href="tool.url" dense
target="_blank" class="text-grey"
active-color="primary"
indicator-color="primary"
align="justify"
narrow-indicator
> >
{% raw %} <q-tab name="frameworks" label="Frameworks"></q-tab>
<!-- with raw Flask won't try to interpret the Vue moustaches --> <q-tab name="tools" label="Useful Tools"></q-tab>
<q-item-section> <q-tab name="goodpractice" label="Good Practice"></q-tab>
<q-item-label>{{ tool.name }}</q-item-label> <q-tab name="enviroment" label="Dev Enviroment"></q-tab>
<q-item-label caption>{{ tool.language }}</q-item-label> <q-tab name="submission" label="Submisson to LNbits repo"></q-tab>
</q-item-section> </q-tabs>
{% endraw %}
</q-item> <q-separator></q-separator>
</q-list>
<q-separator class="q-my-lg"></q-separator> <q-tab-panels v-model="tab">
<p> <q-tab-panel name="frameworks">
A magical "g" is always available, with info about the user, wallets and <div class="text-h6">Frameworks</div>
extensions:
</p> <div>
<code class="text-caption">{% raw %}{{ g }}{% endraw %}</code> <template>
<q-tabs align="left" v-model="framworktab" inline-label>
<q-tab name="fastapi"
><img src="./static/fastapi-framework.png" />FASTAPI</q-tab
>
<q-tab name="quasar"
><img src="./static/quasar-framework.png" />QUASAR</q-tab
>
<q-tab name="vuejs"
><img src="./static/vuejs-framework.png" />VUE-JS</q-tab
>
</q-tabs>
</template>
<template>
<q-tab-panels v-model="framworktab">
<q-tab-panel name="fastapi" class="text-body1">
<a href="https://fastapi.tiangolo.com/"
><img src="./static/fastapilogo.png"
/></a>
<p>
LNbits API is built using
<a href="https://fastapi.tiangolo.com/" class="text-primary"
>FastAPI</a
>, a high-performance, easy to code API framework.<br /><br />
FastAPI auto-generates swagger UI docs for testing endpoints
<a class="text-primary" href="../docs">/docs</a>
</p>
<i>
<strong>TIP:</strong> Although it is possible for extensions
to use other extensions API endpoints (such as with the
Satspay and Onchain extension), ideally an extension should
only use LNbits
<a href="../docs#/default" class="text-primary">core</a>
endpoints. </i
><br /><br />
<code class="bg-grey-3 text-black">views.py</code> is used for
setting application routes:
<img src="./static/fastapi-example.png" /><br /><br />
<code class="bg-grey-3 text-black">views_api.py</code> is used
for setting application API endpoints:<br />
<img src="./static/fastapi-example2.png" />
</q-tab-panel>
<q-tab-panel name="quasar" class="text-body1">
<a href="https://quasar.dev/"
><img src="./static/quasarlogo.png"
/></a>
<p>
LNbits uses
<a class="text-primary" href="https://quasar.dev/"
>Quasar Framework</a
>
for frontend deisgn elements. Quasar Framework is an
open-source Vue.js based framework for building apps.
</p>
<i>
<strong>TIP:</strong> Look through
<code class="bg-grey-3 text-black">/template</code> files in
other extensions for examples of Quasar elements being used. </i
><br /><br />
<p>
In the below example we make a dialogue popup box (box can
be triggered
<q-btn
size="sm"
color="primary"
@click="thingDialog.show = true"
>here</q-btn
>): <q-tooltip>Exmple of a tooltip!</q-tooltip>
</p>
<img src="./static/quasar-example.png" /><br /><br />
<div class="text-h6">Useful links:</div>
<q-btn
color="primary"
type="a"
href="https://quasar.dev/style/"
>Style (typography, spacing, etc)</q-btn
>
<q-btn
color="primary"
type="a"
href="https://quasar.dev/vue-components/"
>Genral components (cards, buttons, popup dialogs,
etc)</q-btn
>
<q-btn
color="primary"
type="a"
href="https://quasar.dev/layout/grid"
>Layouts (rows/columns, flexbox)</q-btn
>
</q-tab-panel>
<q-tab-panel
v-if="someBool == true"
name="vuejs"
class="text-body1"
>
<a href="https://vuejs.org/">
<img src="./static/vuejslogo.png"
/></a>
<p>
LNbits uses
<a href="https://vuejs.org/" class="text-primary">Vue</a>
components for best-in-class high-performance and responsive
performance.
</p>
<p>Typical example of Vue components in a frontend script:</p>
<img
src="./static/script-example.png"
style="max-width: 800px"
/><br /><br />
<p>
In a page body, models can be called. <br />Content can be
conditionally rendered using Vue's
<code class="bg-grey-3 text-black">v-if</code>:
</p>
<img
src="./static/vif-example.png"
style="max-width: 800px"
/>
</q-tab-panel>
</q-tab-panels>
</template>
</div>
</q-tab-panel>
<q-tab-panel name="tools">
<div class="text-h6">Useful Tools</div>
<div>
<template>
<q-tabs v-model="usefultab" align="left">
<q-tab name="magicalg">MAGICAL G</q-tab>
<q-tab name="exchange">EXCHANGE RATES</q-tab>
</q-tabs>
</template>
<template>
<q-tab-panels v-model="usefultab">
<q-tab-panel name="magicalg" class="text-body1">
<div class="text-h5 q-mb-md">Magical G</div>
<p>
A magical "g" (ie
<code class="bg-grey-3 text-black"
>this.g.user.wallets[0].inkey</code
>) is always available, with info about the user, wallets
and extensions:
</p>
<code class="text-caption">{% raw %}{{ g }}{% endraw %}</code>
</q-tab-panel>
<q-tab-panel name="exchange">
<div class="text-h6">Exchange rates</div>
<p>
LNbits includes a handy
<a
href="../docs#/default/api_fiat_as_sats_api_v1_conversion_post"
class="text-primary"
>exchange rate function</a
>, that streams rates from 6 different sources.
</p>
Exchange rate API:<br />
<img src="./static/conversion-example.png" /><br /><br />
Exchange rate functions, included using
<code class="bg-grey-3 text-black"
>from lnbits.utils.exchange_rates import
fiat_amount_as_satoshis</code
>:<br />
<img src="./static/conversion-example2.png" />
</q-tab-panel>
</q-tab-panels>
</template>
</div>
</q-tab-panel>
<q-tab-panel name="goodpractice">
<div class="text-h6">Good Practice</div>
Coming soon...
</q-tab-panel>
<q-tab-panel name="enviroment">
<div class="text-h6">Dev Enviroment</div>
Coming soon...
</q-tab-panel>
<q-tab-panel name="submission">
<div class="text-h6">Submission</div>
Coming soon...
</q-tab-panel>
</q-tab-panels>
</q-card>
</q-card-section> </q-card-section>
</q-card> </q-card>
{% endblock %} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block scripts %} {{ window_vars(user) }}
<script> <script>
var someMapObject = obj => {
obj._data = _.clone(obj)
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
'YYYY-MM-DD HH:mm'
)
// here you can do something with the mapped data
return obj
}
new Vue({ new Vue({
el: '#vue', el: '#vue',
mixins: [windowMixin], mixins: [windowMixin],
data: function () { data: function () {
return { return {
tools: [] ///// Declare models/variables /////
thingDialog: {
show: false,
data: {}
},
someBool: true,
splitterModel: 20,
exampleData: [],
tab: 'frameworks',
framworktab: 'fastapi',
usefultab: 'magicalg'
} }
}, },
///// Where functions live /////
methods: {
exampleFunction(data) {
var theData = data
LNbits.api
.request(
'GET', // Type of request
'/example/api/v1/test/' + theData, // URL of the endpoint
this.g.user.wallets[0].inkey // Often endpoints require a key
)
.then(response => {
this.exampleData = response.data.map(someMapObject) // Often whats returned is mapped onto some model
})
.catch(error => {
LNbits.utils.notifyApiError(error) // Error will be passed to the frontend
})
},
sendThingDialog() {
console.log(this.thingDialog)
}
},
///// To run on startup /////
created: function () { created: function () {
var self = this self = this // Often used to run a real object, rather than the event (all a bit confusing really)
self.exampleFunction('lorum')
// axios is available for making requests
axios({
method: 'GET',
url: '/example/api/v1/tools',
headers: {
'X-example-header': 'not-used'
}
}).then(function (response) {
self.tools = response.data
})
} }
}) })
</script> </script>

View file

@ -2,34 +2,12 @@
# add your dependencies here # add your dependencies here
# import httpx
# (use httpx just like requests, except instead of response.ok there's only the
# response.is_error that is its inverse)
from . import example_ext from . import example_ext
# add your endpoints here # add your endpoints here
@example_ext.get("/api/v1/tools") @example_ext.get("/api/v1/test/{test_data}")
async def api_example(): async def api_example(test_data):
"""Try to add descriptions for others.""" # Do some python things and return the data
tools = [ return test_data
{
"name": "fastAPI",
"url": "https://fastapi.tiangolo.com/",
"language": "Python",
},
{
"name": "Vue.js",
"url": "https://vuejs.org/",
"language": "JavaScript",
},
{
"name": "Quasar Framework",
"url": "https://quasar.dev/",
"language": "JavaScript",
},
]
return tools

View file

@ -86,12 +86,19 @@ async def lnurl_callback(
ls = await get_livestream_by_track(track_id) ls = await get_livestream_by_track(track_id)
extra_amount = amount_received - int(amount_received * (100 - ls.fee_pct) / 100)
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=ls.wallet, wallet_id=ls.wallet,
amount=int(amount_received / 1000), amount=int(amount_received / 1000),
memo=await track.fullname(), memo=await track.fullname(),
unhashed_description=(await track.lnurlpay_metadata()).encode(), unhashed_description=(await track.lnurlpay_metadata()).encode(),
extra={"tag": "livestream", "track": track.id, "comment": comment}, extra={
"tag": "livestream",
"track": track.id,
"comment": comment,
"amount": int(extra_amount / 1000),
},
) )
if amount_received < track.price_msat: if amount_received < track.price_msat:

View file

@ -22,6 +22,7 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
if payment.extra.get("tag") != "livestream": if payment.extra.get("tag") != "livestream":
# not a livestream invoice # not a livestream invoice
return return
@ -41,21 +42,26 @@ async def on_invoice_paid(payment: Payment) -> None:
ls = await get_livestream_by_track(track.id) ls = await get_livestream_by_track(track.id)
assert ls, f"track {track.id} is not associated with a livestream" assert ls, f"track {track.id} is not associated with a livestream"
# now we make a special kind of internal transfer
amount = int(payment.amount * (100 - ls.fee_pct) / 100) amount = int(payment.amount * (100 - ls.fee_pct) / 100)
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=tpos.tip_wallet, wallet_id=producer.wallet,
amount=amount, # sats amount=int(amount / 1000),
internal=True, internal=True,
memo=f"Revenue from '{track.name}'.", memo=f"Revenue from '{track.name}'.",
) )
logger.debug(f"livestream: producer invoice created: {payment_hash}") logger.debug(
f"livestream: producer invoice created: {payment_hash}, {amount} msats"
)
checking_id = await pay_invoice( checking_id = await pay_invoice(
payment_request=payment_request, payment_request=payment_request,
wallet_id=payment.wallet_id, wallet_id=payment.wallet_id,
extra={"tag": "livestream"}, extra={
**payment.extra,
"shared_with": f"Producer ID: {producer.id}",
"received": payment.amount,
},
) )
logger.debug(f"livestream: producer invoice paid: {checking_id}") logger.debug(f"livestream: producer invoice paid: {checking_id}")

View file

@ -1,19 +1,18 @@
from typing import List, Optional, Union from typing import List, Optional, Union
from lnbits.db import SQLITE from lnbits.helpers import urlsafe_short_hash
from . import db from . import db
from .models import CreatePayLinkData, PayLink from .models import CreatePayLinkData, PayLink
async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
link_id = urlsafe_short_hash()[:6]
returning = "" if db.type == SQLITE else "RETURNING ID" result = await db.execute(
method = db.execute if db.type == SQLITE else db.fetchone
result = await (method)(
f""" f"""
INSERT INTO lnurlp.pay_links ( INSERT INTO lnurlp.pay_links (
id,
wallet, wallet,
description, description,
min, min,
@ -29,10 +28,10 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
currency, currency,
fiat_base_multiplier fiat_base_multiplier
) )
VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?)
{returning}
""", """,
( (
link_id,
wallet_id, wallet_id,
data.description, data.description,
data.min, data.min,
@ -47,17 +46,13 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
data.fiat_base_multiplier, data.fiat_base_multiplier,
), ),
) )
if db.type == SQLITE:
link_id = result._result_proxy.lastrowid
else:
link_id = result[0]
link = await get_pay_link(link_id) link = await get_pay_link(link_id)
assert link, "Newly created link couldn't be retrieved" assert link, "Newly created link couldn't be retrieved"
return link return link
async def get_pay_link(link_id: int) -> Optional[PayLink]: async def get_pay_link(link_id: str) -> Optional[PayLink]:
row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,)) row = await db.fetchone("SELECT * FROM lnurlp.pay_links WHERE id = ?", (link_id,))
return PayLink.from_row(row) if row else None return PayLink.from_row(row) if row else None

View file

@ -18,7 +18,12 @@ from .crud import increment_pay_link
@lnurlp_ext.get( @lnurlp_ext.get(
"/api/v1/lnurl/{link_id}", "/api/v1/lnurl/{link_id}", # Backwards compatibility for old LNURLs / QR codes (with long URL)
status_code=HTTPStatus.OK,
name="lnurlp.api_lnurl_response.deprecated",
)
@lnurlp_ext.get(
"/{link_id}",
status_code=HTTPStatus.OK, status_code=HTTPStatus.OK,
name="lnurlp.api_lnurl_response", name="lnurlp.api_lnurl_response",
) )

View file

@ -68,3 +68,81 @@ async def m005_webhook_headers_and_body(db):
""" """
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_headers TEXT;") await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_headers TEXT;")
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_body TEXT;") await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_body TEXT;")
async def m006_redux(db):
"""
Migrate ID column type to string for UUIDs and migrate existing data
"""
# we can simply change the column type for postgres
if db.type != "SQLITE":
await db.execute("ALTER TABLE lnurlp.pay_links ALTER COLUMN id TYPE TEXT;")
else:
# but we have to do this for sqlite
await db.execute("ALTER TABLE lnurlp.pay_links RENAME TO pay_links_old")
await db.execute(
f"""
CREATE TABLE lnurlp.pay_links (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
description TEXT NOT NULL,
min {db.big_int} NOT NULL,
max {db.big_int},
currency TEXT,
fiat_base_multiplier INTEGER DEFAULT 1,
served_meta INTEGER NOT NULL,
served_pr INTEGER NOT NULL,
webhook_url TEXT,
success_text TEXT,
success_url TEXT,
comment_chars INTEGER DEFAULT 0,
webhook_headers TEXT,
webhook_body TEXT
);
"""
)
for row in [
list(row) for row in await db.fetchall("SELECT * FROM lnurlp.pay_links_old")
]:
await db.execute(
"""
INSERT INTO lnurlp.pay_links (
id,
wallet,
description,
min,
served_meta,
served_pr,
webhook_url,
success_text,
success_url,
currency,
comment_chars,
max,
fiat_base_multiplier,
webhook_headers,
webhook_body
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
row[0],
row[1],
row[2],
row[3],
row[4],
row[5],
row[6],
row[7],
row[8],
row[9],
row[10],
row[11],
row[12],
row[13],
row[14],
),
)
await db.execute("DROP TABLE lnurlp.pay_links_old")

View file

@ -26,7 +26,7 @@ class CreatePayLinkData(BaseModel):
class PayLink(BaseModel): class PayLink(BaseModel):
id: int id: str
wallet: str wallet: str
description: str description: str
min: float min: float

View file

@ -17,7 +17,7 @@ var mapPayLink = obj => {
) )
obj.amount = new Intl.NumberFormat(LOCALE).format(obj.amount) obj.amount = new Intl.NumberFormat(LOCALE).format(obj.amount)
obj.print_url = [locationPath, 'print/', obj.id].join('') obj.print_url = [locationPath, 'print/', obj.id].join('')
obj.pay_url = [locationPath, obj.id].join('') obj.pay_url = [locationPath, 'link/', obj.id].join('')
return obj return obj
} }

View file

@ -21,7 +21,7 @@ async def index(request: Request, user: User = Depends(check_user_exists)):
) )
@lnurlp_ext.get("/{link_id}", response_class=HTMLResponse) @lnurlp_ext.get("/link/{link_id}", response_class=HTMLResponse)
async def display(request: Request, link_id): async def display(request: Request, link_id):
link = await get_pay_link(link_id) link = await get_pay_link(link_id)
if not link: if not link:

View file

@ -20,60 +20,57 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
if payment.extra.get("tag") == "splitpayments":
if payment.extra.get("tag") == "splitpayments" or payment.extra.get("splitted"):
# already a splitted payment, ignore # already a splitted payment, ignore
return return
targets = await get_targets(payment.wallet_id) targets = await get_targets(payment.wallet_id)
logger.debug(targets)
if not targets: if not targets:
return return
# validate target percentages
total_percent = sum([target.percent for target in targets]) total_percent = sum([target.percent for target in targets])
if total_percent > 100: if total_percent > 100:
logger.error("splitpayment failure: total percent adds up to more than 100%") logger.error("splitpayment: total percent adds up to more than 100%")
return
logger.trace(f"splitpayments: performing split payments to {len(targets)} targets")
if payment.extra.get("amount"):
amount_to_split = (payment.extra.get("amount") or 0) * 1000
else:
amount_to_split = payment.amount
if not amount_to_split:
logger.error("splitpayments: no amount to split")
return return
logger.debug(f"checking if tagged for {len(targets)} targets")
tagged = False
for target in targets: for target in targets:
if target.tag in payment.extra: tagged = target.tag in payment.extra
tagged = True
if tagged or target.percent > 0:
if tagged:
memo = f"Pushed tagged payment to {target.alias}"
amount_msat = int(amount_to_split)
else:
amount_msat = int(amount_to_split * target.percent / 100)
memo = f"Split payment: {target.percent}% for {target.alias or target.wallet}"
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=target.wallet, wallet_id=target.wallet,
amount=int(payment.amount / 1000), # sats amount=int(amount_msat / 1000),
internal=True, internal=True,
memo=f"Pushed tagged payment to {target.alias}", memo=memo,
extra={"tag": "splitpayments"},
) )
logger.debug(f"created split invoice: {payment_hash}")
checking_id = await pay_invoice( extra = {**payment.extra, "tag": "splitpayments", "splitted": True}
await pay_invoice(
payment_request=payment_request, payment_request=payment_request,
wallet_id=payment.wallet_id, wallet_id=payment.wallet_id,
extra={"tag": "splitpayments"}, extra=extra,
) )
logger.debug(f"paid split invoice: {checking_id}")
logger.debug(f"performing split to {len(targets)} targets")
if tagged == False:
for target in targets:
if target.percent > 0:
amount = int(payment.amount * target.percent / 100) # msats
payment_hash, payment_request = await create_invoice(
wallet_id=target.wallet,
amount=int(amount / 1000), # sats
internal=True,
memo=f"split payment: {target.percent}% for {target.alias or target.wallet}",
extra={"tag": "splitpayments"},
)
logger.debug(f"created split invoice: {payment_hash}")
checking_id = await pay_invoice(
payment_request=payment_request,
wallet_id=payment.wallet_id,
extra={"tag": "splitpayments"},
)
logger.debug(f"paid split invoice: {checking_id}")

View file

@ -20,6 +20,9 @@ async def wait_for_paid_invoices():
async def on_invoice_paid(payment: Payment) -> None: async def on_invoice_paid(payment: Payment) -> None:
if not payment.extra:
return
if payment.extra.get("tag") != "tpos": if payment.extra.get("tag") != "tpos":
return return
@ -50,7 +53,7 @@ async def on_invoice_paid(payment: Payment) -> None:
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=wallet_id, wallet_id=wallet_id,
amount=int(tipAmount), # sats amount=int(tipAmount),
internal=True, internal=True,
memo=f"tpos tip", memo=f"tpos tip",
) )
@ -59,6 +62,6 @@ async def on_invoice_paid(payment: Payment) -> None:
checking_id = await pay_invoice( checking_id = await pay_invoice(
payment_request=payment_request, payment_request=payment_request,
wallet_id=payment.wallet_id, wallet_id=payment.wallet_id,
extra={"tag": "tpos"}, extra={**payment.extra, "tipSplitted": True},
) )
logger.debug(f"tpos: tip invoice paid: {checking_id}") logger.debug(f"tpos: tip invoice paid: {checking_id}")

View file

@ -76,7 +76,12 @@ async def api_tpos_create_invoice(
wallet_id=tpos.wallet, wallet_id=tpos.wallet,
amount=amount, amount=amount,
memo=f"{tpos.name}", memo=f"{tpos.name}",
extra={"tag": "tpos", "tipAmount": tipAmount, "tposId": tpos_id}, extra={
"tag": "tpos",
"tipAmount": tipAmount,
"tposId": tpos_id,
"amount": amount - tipAmount if tipAmount else False,
},
) )
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))

View file

@ -12,7 +12,7 @@ from .models import CreateWithdrawData, HashCheck, WithdrawLink
async def create_withdraw_link( async def create_withdraw_link(
data: CreateWithdrawData, wallet_id: str data: CreateWithdrawData, wallet_id: str
) -> WithdrawLink: ) -> WithdrawLink:
link_id = urlsafe_short_hash() link_id = urlsafe_short_hash()[:6]
available_links = ",".join([str(i) for i in range(data.uses)]) available_links = ",".join([str(i) for i in range(data.uses)])
await db.execute( await db.execute(
""" """

View file

@ -200,8 +200,9 @@ class SparkWallet(Wallet):
if r["pays"][0]["payment_hash"] == checking_id: if r["pays"][0]["payment_hash"] == checking_id:
status = r["pays"][0]["status"] status = r["pays"][0]["status"]
if status == "complete": if status == "complete":
fee_msat = -int( fee_msat = -(
r["pays"][0]["amount_sent_msat"] - r["pays"][0]["amount_msat"] int(r["pays"][0]["amount_sent_msat"][0:-4])
- int(r["pays"][0]["amount_msat"][0:-4])
) )
return PaymentStatus(True, fee_msat, r["pays"][0]["preimage"]) return PaymentStatus(True, fee_msat, r["pays"][0]["preimage"])
elif status == "failed": elif status == "failed":