Merge branch 'main' into Uthpalamain
|
|
@ -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 []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
6
lnbits/extensions/example/config.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "Build your own!",
|
||||||
|
"short_description": "Extension building guide",
|
||||||
|
"tile": "/example/static/bitcoin-extension.png",
|
||||||
|
"contributors": ["github_username"]
|
||||||
|
}
|
||||||
5
lnbits/extensions/example/crud.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# crud.py is for communication with your extensions database
|
||||||
|
|
||||||
|
# add your dependencies here
|
||||||
|
|
||||||
|
# add your fnctions here
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Build your own!!",
|
|
||||||
"short_description": "Join us, make an extension",
|
|
||||||
"tile": "/cashu/static/image/tile.png",
|
|
||||||
"contributors": ["github_username"]
|
|
||||||
}
|
|
||||||
|
|
@ -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"""
|
||||||
|
|
|
||||||
BIN
lnbits/extensions/example/static/bitcoin-extension.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
lnbits/extensions/example/static/conversion-example.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
lnbits/extensions/example/static/conversion-example2.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
lnbits/extensions/example/static/fastapi-example.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
lnbits/extensions/example/static/fastapi-example2.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
lnbits/extensions/example/static/fastapi-framework.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
lnbits/extensions/example/static/fastapilogo.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
lnbits/extensions/example/static/quasar-example.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
lnbits/extensions/example/static/quasar-framework.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
lnbits/extensions/example/static/quasarlogo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
lnbits/extensions/example/static/script-example.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
lnbits/extensions/example/static/vif-example.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
lnbits/extensions/example/static/vuejs-framework.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
lnbits/extensions/example/static/vuejslogo.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
29
lnbits/extensions/example/tasks.py
Normal 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
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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":
|
||||||
|
|
|
||||||