FIX: issues and improvements to frontend, add lnurlp/lnurladdress, remove split by tag feature (#4)

* deinitialize task
* rework of frontend
* add lnurl and lightningaddresses
* substract fee_reserve from external split, for potential routing fee, add a warning to ui
This commit is contained in:
dni ⚡ 2023-03-24 21:03:33 +01:00 committed by GitHub
commit 5bb234b797
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 269 additions and 344 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
__pycache__

View file

@ -1,4 +1,5 @@
import asyncio import asyncio
from typing import List
from fastapi import APIRouter from fastapi import APIRouter
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@ -9,6 +10,8 @@ from lnbits.tasks import catch_everything_and_restart
db = Database("ext_splitpayments") db = Database("ext_splitpayments")
scheduled_tasks: List[asyncio.Task] = []
splitpayments_static_files = [ splitpayments_static_files = [
{ {
"path": "/splitpayments/static", "path": "/splitpayments/static",
@ -32,4 +35,5 @@ from .views_api import * # noqa: F401,F403
def splitpayments_start(): def splitpayments_start():
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) task = loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
scheduled_tasks.append(task)

View file

@ -1,6 +1,6 @@
{ {
"name": "Split Payments", "name": "Split Payments",
"short_description": "Split incoming payments across wallets", "short_description": "Split incoming payments across wallets",
"tile": "/splitpayments/static/image/split-payments.png", "tile": "/splitpayments/static/image/split-payments.png",
"contributors": ["fiatjaf", "cryptograffiti"] "contributors": ["fiatjaf", "cryptograffiti"]
} }

View file

@ -22,15 +22,14 @@ async def set_targets(source_wallet: str, targets: List[Target]):
await conn.execute( await conn.execute(
""" """
INSERT INTO splitpayments.targets INSERT INTO splitpayments.targets
(id, source, wallet, percent, tag, alias) (id, source, wallet, percent, alias)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
""", """,
( (
urlsafe_short_hash(), urlsafe_short_hash(),
source_wallet, source_wallet,
target.wallet, target.wallet,
target.percent, target.percent,
target.tag,
target.alias, target.alias,
), ),
) )

View file

@ -1,9 +1,9 @@
{ {
"repos": [ "repos": [
{ {
"id": "splitpayments", "id": "splitpayments",
"organisation": "lnbits", "organisation": "lnbits",
"repository": "splitpayments" "repository": "splitpayments"
} }
] ]
} }

View file

@ -97,3 +97,28 @@ async def m003_add_id_and_tag(db):
) )
await db.execute("DROP TABLE splitpayments.splitpayments_old") await db.execute("DROP TABLE splitpayments.splitpayments_old")
async def m004_remove_tag(db):
"""
This removes tag
"""
keys = "id,wallet,source,percent,alias"
new_db = "splitpayments.targets"
old_db = "splitpayments.targets_old"
await db.execute(f"ALTER TABLE {new_db} RENAME TO targets_old")
await db.execute(
f"""
CREATE TABLE {new_db} (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
source TEXT NOT NULL,
percent REAL NOT NULL CHECK (percent >= 0 AND percent <= 100),
alias TEXT,
UNIQUE (source, wallet)
);
"""
)
await db.execute(f"INSERT INTO {new_db} ({keys}) SELECT {keys} FROM {old_db}")
await db.execute(f"DROP TABLE {old_db}")

View file

@ -9,7 +9,6 @@ class Target(BaseModel):
wallet: str wallet: str
source: str source: str
percent: float percent: float
tag: str
alias: Optional[str] alias: Optional[str]
@classmethod @classmethod
@ -17,12 +16,11 @@ class Target(BaseModel):
return cls(**dict(row)) return cls(**dict(row))
class TargetPutList(BaseModel): class TargetPut(BaseModel):
wallet: str = Query(...) wallet: str = Query(...)
alias: str = Query("") alias: str = Query("")
percent: float = Query(..., ge=0, lt=100) percent: float = Query(..., ge=0, le=100)
tag: str
class TargetPut(BaseModel): class TargetPutList(BaseModel):
__root__: List[TargetPutList] targets: List[TargetPut]

View file

@ -24,11 +24,7 @@ new Vue({
return { return {
selectedWallet: null, selectedWallet: null,
currentHash: '', // a string that must match if the edit data is unchanged currentHash: '', // a string that must match if the edit data is unchanged
targets: [ targets: []
{
method: 'split'
}
]
} }
}, },
computed: { computed: {
@ -37,14 +33,6 @@ new Vue({
} }
}, },
methods: { methods: {
clearTargets() {
this.targets = [{}]
this.$q.notify({
message:
'Cleared the form, but not saved. You must click to save manually.',
timeout: 500
})
},
clearTarget(index) { clearTarget(index) {
this.targets.splice(index, 1) this.targets.splice(index, 1)
console.log(this.targets) console.log(this.targets)
@ -60,106 +48,21 @@ new Vue({
'/splitpayments/api/v1/targets', '/splitpayments/api/v1/targets',
this.selectedWallet.adminkey this.selectedWallet.adminkey
) )
.then(response => {
this.targets = response.data
})
.catch(err => { .catch(err => {
LNbits.utils.notifyApiError(err) LNbits.utils.notifyApiError(err)
}) })
.then(response => {
this.currentHash = hashTargets(response.data)
this.targets = response.data.concat({})
for (let i = 0; i < this.targets.length; i++) {
if (this.targets[i].tag.length > 0) {
this.targets[i].method = 'tag'
} else if (this.targets[i].percent.length > 0) {
this.targets[i].method = 'split'
} else {
this.targets[i].method = ''
}
}
})
}, },
changedWallet(wallet) { changedWallet(wallet) {
this.selectedWallet = wallet this.selectedWallet = wallet
this.getTargets() this.getTargets()
}, },
clearChanged(index) { addTarget() {
if (this.targets[index].method == 'split') { this.targets.push({source: this.selectedWallet})
this.targets[index].tag = null
this.targets[index].method = 'split'
} else {
this.targets[index].percent = null
this.targets[index].method = 'tag'
}
},
targetChanged(index) {
// fix percent min and max range
if (this.targets[index].percent) {
if (this.targets[index].percent > 100) this.targets[index].percent = 100
if (this.targets[index].percent < 0) this.targets[index].percent = 0
this.targets[index].tag = ''
}
// not percentage
if (!this.targets[index].percent) {
this.targets[index].percent = 0
}
// remove empty lines (except last)
if (this.targets.length >= 2) {
for (let i = this.targets.length - 2; i >= 0; i--) {
let target = this.targets[i]
if (
(!target.wallet || target.wallet.trim() === '') &&
(!target.alias || target.alias.trim() === '') &&
(!target.tag || target.tag.trim() === '') &&
!target.percent
) {
this.targets.splice(i, 1)
}
}
}
// add a line at the end if the last one is filled
let last = this.targets[this.targets.length - 1]
if (last.wallet && last.wallet.trim() !== '') {
this.targets.push({})
}
// sum of all percents
let currentTotal = this.targets.reduce(
(acc, target) => acc + (target.percent || 0),
0
)
// remove last (unfilled) line if the percent is already 100
if (currentTotal >= 100) {
let last = this.targets[this.targets.length - 1]
if (
(!last.wallet || last.wallet.trim() === '') &&
(!last.alias || last.alias.trim() === '') &&
!last.percent
) {
this.targets = this.targets.slice(0, -1)
}
}
// adjust percents of other lines (not this one)
if (currentTotal > 100 && isPercent) {
let diff = (currentTotal - 100) / (100 - this.targets[index].percent)
this.targets.forEach((target, t) => {
if (t !== index) target.percent -= +(diff * target.percent).toFixed(2)
})
}
// overwrite so changes appear
this.targets = this.targets
}, },
saveTargets() { saveTargets() {
for (let i = 0; i < this.targets.length; i++) {
if (this.targets[i].tag != '') {
this.targets[i].percent = 0
} else {
this.targets[i].tag = ''
}
}
LNbits.api LNbits.api
.request( .request(
'PUT', 'PUT',
@ -167,13 +70,6 @@ new Vue({
this.selectedWallet.adminkey, this.selectedWallet.adminkey,
{ {
targets: this.targets targets: this.targets
.filter(isTargetComplete)
.map(({wallet, percent, tag, alias}) => ({
wallet,
percent,
tag,
alias
}))
} }
) )
.then(response => { .then(response => {
@ -181,11 +77,32 @@ new Vue({
message: 'Split payments targets set.', message: 'Split payments targets set.',
timeout: 700 timeout: 700
}) })
this.getTargets()
}) })
.catch(err => { .catch(err => {
LNbits.utils.notifyApiError(err) LNbits.utils.notifyApiError(err)
}) })
},
deleteTargets() {
LNbits.utils
.confirmDialog('Are you sure you want to delete the targets?')
.onOk(() => {
this.targets = []
LNbits.api
.request(
'DELETE',
'/splitpayments/api/v1/targets',
this.selectedWallet.adminkey
)
.then(response => {
this.$q.notify({
message: 'Split payments targets deleted.',
timeout: 700
})
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
})
} }
}, },
created() { created() {

109
tasks.py
View file

@ -1,9 +1,15 @@
import asyncio import asyncio
import json
from math import floor
from typing import Optional
import httpx
from loguru import logger from loguru import logger
from lnbits import bolt11
from lnbits.core.crud import get_standalone_payment
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.core.services import create_invoice, pay_invoice from lnbits.core.services import create_invoice, pay_invoice, fee_reserve
from lnbits.helpers import get_current_extension_name from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener from lnbits.tasks import register_invoice_listener
@ -30,7 +36,6 @@ async def on_invoice_paid(payment: Payment) -> None:
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:
@ -39,38 +44,84 @@ async def on_invoice_paid(payment: Payment) -> None:
logger.trace(f"splitpayments: performing split payments to {len(targets)} targets") 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
for target in targets: for target in targets:
tagged = target.tag in payment.extra
if tagged or target.percent > 0: if target.percent > 0:
if tagged: amount_msat = int(payment.amount * target.percent / 100)
memo = f"Pushed tagged payment to {target.alias}" memo = (
amount_msat = int(amount_to_split) f"Split payment: {target.percent}% for {target.alias or target.wallet}"
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(
wallet_id=target.wallet,
amount=int(amount_msat / 1000),
internal=True,
memo=memo,
) )
if target.wallet.find("@") >= 0 or target.wallet.find("LNURL") >= 0:
safe_amount_msat = amount_msat - fee_reserve(amount_msat)
payment_request = await get_lnurl_invoice(
target.wallet, payment.wallet_id, safe_amount_msat, memo
)
else:
_, payment_request = await create_invoice(
wallet_id=target.wallet,
amount=int(amount_msat / 1000),
internal=True,
memo=memo,
)
extra = {**payment.extra, "tag": "splitpayments", "splitted": True} extra = {**payment.extra, "tag": "splitpayments", "splitted": True}
await pay_invoice( if payment_request:
payment_request=payment_request, await pay_invoice(
wallet_id=payment.wallet_id, payment_request=payment_request,
extra=extra, wallet_id=payment.wallet_id,
description=memo,
extra=extra,
)
async def get_lnurl_invoice(
payoraddress, wallet_id, amount_msat, memo
) -> Optional[str]:
from lnbits.core.views.api import api_lnurlscan
data = await api_lnurlscan(payoraddress)
rounded_amount = floor(amount_msat / 1000) * 1000
async with httpx.AsyncClient() as client:
try:
r = await client.get(
data["callback"],
params={"amount": rounded_amount, "comment": memo},
timeout=40,
) )
if r.is_error:
raise httpx.ConnectError("issue with scrub callback")
r.raise_for_status()
except (httpx.ConnectError, httpx.RequestError):
logger.error(
f"splitting LNURL failed: Failed to connect to {data['callback']}."
)
return None
except Exception as exc:
logger.error(f"splitting LNURL failed: {str(exc)}.")
return None
params = json.loads(r.text)
if params.get("status") == "ERROR":
logger.error(f"{data['callback']} said: '{params.get('reason', '')}'")
return None
invoice = bolt11.decode(params["pr"])
lnurlp_payment = await get_standalone_payment(invoice.payment_hash)
if lnurlp_payment and lnurlp_payment.wallet_id == wallet_id:
logger.error(f"split failed. cannot split payments to yourself via LNURL.")
return None
if invoice.amount_msat != rounded_amount:
logger.error(
f"{data['callback']} returned an invalid invoice. Expected {amount_msat} msat, got {invoice.amount_msat}."
)
return None
return params["pr"]

View file

@ -1,97 +0,0 @@
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="How to use"
:content-inset-level="0.5"
>
<q-card>
<q-card-section>
<p>
Add some wallets to the list of "Target Wallets", each with an
associated <em>percent</em>. After saving, every time any payment
arrives at the "Source Wallet" that payment will be split with the
target wallets according to their percent.
</p>
<p>This is valid for every payment, doesn't matter how it was created.</p>
<p>Target wallets can be any wallet from this same LNbits instance.</p>
<p>
To remove a wallet from the targets list, just erase its fields and
save. To remove all, click "Clear" then save.
</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="extras"
icon="swap_vertical_circle"
label="API info"
:content-inset-level="0.5"
>
<q-btn
flat
label="Swagger API"
type="a"
href="../docs#/splitpayments"
></q-btn>
<q-expansion-item
group="api"
dense
expand-separator
label="List Target Wallets"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">GET</span>
/splitpayments/api/v1/targets</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>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code
>[{"wallet": &lt;wallet id&gt;, "alias": &lt;chosen name for this
wallet&gt;, "percent": &lt;number between 1 and 100&gt;}, ...]</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.base_url }}splitpayments/api/v1/targets -H
"X-Api-Key: {{ user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item
group="api"
dense
expand-separator
label="Set Target Wallets"
class="q-pb-md"
>
<q-card>
<q-card-section>
<code
><span class="text-blue">PUT</span>
/splitpayments/api/v1/targets</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>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X PUT {{ request.base_url }}splitpayments/api/v1/targets -H
"X-Api-Key: {{ user.wallets[0].adminkey }}" -H 'Content-Type:
application/json' -d '{"targets": [{"wallet": &lt;wallet id or invoice
key&gt;, "alias": &lt;name to identify this&gt;, "percent": &lt;number
between 1 and 100&gt;}, ...]}'
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>

View file

@ -1,7 +1,7 @@
{% 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 %}
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md"> <div class="col-12 col-md-8 q-gutter-y-md">
<q-card class="q-pa-sm col-5"> <q-card class="q-pa-sm col-5">
<q-card-section class="q-pa-none text-center"> <q-card-section class="q-pa-none text-center">
<q-form class="q-gutter-md"> <q-form class="q-gutter-md">
@ -43,40 +43,17 @@
<q-input <q-input
dense dense
v-model="target.wallet" v-model="target.wallet"
label="Wallet" label="Target"
:hint="t === targets.length - 1 ? 'A wallet ID or invoice key.' : undefined" hint="A wallet ID, invoice key, LNURLp or Lightning Address."
option-label="name" option-label="name"
style="width: 300px" style="width: 500px"
new-value-mode="add-unique" new-value-mode="add-unique"
use-input use-input
input-debounce="0" input-debounce="0"
emit-value emit-value
></q-input> ></q-input>
<q-toggle
:false-value="'split'"
:true-value="'tag'"
color="primary"
label=""
value="True"
style="width: 180px"
v-model="target.method"
:label="`${target.method}` === 'tag' ? 'Send funds by tag' : `${target.method}` === 'split' ? 'Split funds by %' : 'Split/tag?'"
@input="clearChanged(t)"
></q-toggle>
<q-input <q-input
v-if="target.method == 'tag'"
style="width: 150px"
dense
outlined
v-model="target.tag"
label="Tag name"
suffix="#"
></q-input>
<q-input
v-else-if="target.method == 'split' || target.percent >= 0"
style="width: 150px" style="width: 150px"
dense dense
outlined outlined
@ -86,29 +63,22 @@
></q-input> ></q-input>
<q-btn <q-btn
v-if="t == targets.length - 1 && (target.method == 'tag' || target.method == 'split')"
round
size="sm"
icon="add"
unelevated
color="primary"
@click="targetChanged(t)"
>
<q-tooltip>Add more</q-tooltip>
</q-btn>
<q-btn
v-if="t < targets.length - 1"
@click="clearTarget(t)" @click="clearTarget(t)"
round round
color="red" color="red"
size="5px" size="9px"
icon="close" icon="close"
></q-btn> ></q-btn>
</div> </div>
<div class="row justify-evenly q-pa-lg"> <div class="row justify-evenly q-pa-lg">
<div> <div>
<q-btn unelevated outline color="secondary" @click="clearTargets"> <q-btn icon="add" unelevated color="green" @click="addTarget()">
Clear Add Target
</q-btn>
</div>
<div>
<q-btn unelevated @click="deleteTargets()" color="red">
Delete all Targets
</q-btn> </q-btn>
</div> </div>
@ -117,7 +87,7 @@
unelevated unelevated
color="primary" color="primary"
type="submit" type="submit"
:disabled="targets.length < 2" :disabled="targets.length < 1"
> >
Save Targets Save Targets
</q-btn> </q-btn>
@ -128,17 +98,41 @@
</q-card> </q-card>
</div> </div>
<div class="col-12 col-md-5 q-gutter-y-md"> <div class="col-12 col-md-4 q-gutter-y-md">
<q-card> <q-card>
<q-card-section> <q-card-section>
<h6 class="text-subtitle1 q-my-none"> <h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} SplitPayments extension {{SITE_TITLE}} SplitPayments extension
</h6> </h6>
</q-card-section> </q-card-section>
<q-card-section class="q-pa-none"> <q-separator></q-separator>
<q-separator></q-separator> <q-card>
<q-list> {% include "splitpayments/_api_docs.html" %} </q-list> <q-card-section>
</q-card-section> <p>
Add some targets to the list of "Target Wallets", each with an
associated <em>percentage</em>. After saving, every time any payment
arrives at the "Source Wallet" that payment will be split with the
target wallets according to their percentage.
</p>
<p>
This is valid for every payment, doesn't matter how it was created.
</p>
<p>
Targets can be LNBits wallets from this LNBits instance or any valid LNURL or LN Address.
</p>
<p class="text-warning">
LNURLp and LN Addresses must allow comments > 100 chars and also have a flexible amount.
</p>
<p>
To remove a wallet from the targets list just press the X and save.
To remove all, click "Delete all Targets".
</p>
<p class="text-warning">
For each split via LNURLp or Lightning addresses a fee_reserve is
substracted, because of potential routing fees.
</p>
</q-card-section>
</q-card>
</q-card> </q-card>
</div> </div>
</div> </div>

View file

@ -1,63 +1,96 @@
from http import HTTPStatus from http import HTTPStatus
from typing import List
from fastapi import Depends, Request from fastapi import Depends
from loguru import logger
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from lnbits.core.crud import get_wallet, get_wallet_for_key from lnbits.core.crud import get_wallet, get_wallet_for_key
from lnbits.decorators import WalletTypeInfo, require_admin_key from lnbits.decorators import WalletTypeInfo, check_admin, require_admin_key
from . import splitpayments_ext from . import scheduled_tasks, splitpayments_ext
from .crud import get_targets, set_targets from .crud import get_targets, set_targets
from .models import Target, TargetPut from .models import Target, TargetPutList
@splitpayments_ext.get("/api/v1/targets") @splitpayments_ext.get("/api/v1/targets")
async def api_targets_get(wallet: WalletTypeInfo = Depends(require_admin_key)): async def api_targets_get(
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> List[Target]:
targets = await get_targets(wallet.wallet.id) targets = await get_targets(wallet.wallet.id)
return [target.dict() for target in targets] or [] return targets or []
@splitpayments_ext.put("/api/v1/targets") @splitpayments_ext.put("/api/v1/targets", status_code=HTTPStatus.OK)
async def api_targets_set( async def api_targets_set(
req: Request, wal: WalletTypeInfo = Depends(require_admin_key) target_put: TargetPutList,
): source_wallet: WalletTypeInfo = Depends(require_admin_key),
body = await req.json() ) -> None:
targets = [] try:
data = TargetPut.parse_obj(body["targets"]) targets: List[Target] = []
for entry in data.__root__: for entry in target_put.targets:
wallet = await get_wallet(entry.wallet)
if not wallet: if entry.wallet.find("@") < 0 and entry.wallet.find("LNURL") < 0:
wallet = await get_wallet_for_key(entry.wallet, "invoice") wallet = await get_wallet(entry.wallet)
if not wallet: if not wallet:
wallet = await get_wallet_for_key(entry.wallet, "invoice")
if not wallet:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Invalid wallet '{entry.wallet}'.",
)
if wallet.id == source_wallet.wallet.id:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Can't split to itself."
)
if entry.percent <= 0:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
detail=f"Invalid wallet '{entry.wallet}'.", detail=f"Invalid percent '{entry.percent}'.",
) )
if wallet.id == wal.wallet.id: targets.append(
raise HTTPException( Target(
status_code=HTTPStatus.BAD_REQUEST, detail="Can't split to itself." wallet=entry.wallet,
source=source_wallet.wallet.id,
percent=entry.percent,
alias=entry.alias,
)
) )
if entry.percent < 0: percent_sum = sum([target.percent for target in targets])
raise HTTPException( if percent_sum > 100:
status_code=HTTPStatus.BAD_REQUEST, raise HTTPException(
detail=f"Invalid percent '{entry.percent}'.", status_code=HTTPStatus.BAD_REQUEST, detail="Splitting over 100%"
) )
targets.append( await set_targets(source_wallet.wallet.id, targets)
Target(
wallet=wallet.id, except Exception as ex:
source=wal.wallet.id, logger.warning(ex)
tag=entry.tag, raise HTTPException(
percent=entry.percent, status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
alias=entry.alias, detail="Cannot set targets.",
)
) )
percent_sum = sum([target.percent for target in targets])
if percent_sum > 100:
raise HTTPException( @splitpayments_ext.delete("/api/v1/targets", status_code=HTTPStatus.OK)
status_code=HTTPStatus.BAD_REQUEST, detail="Splitting over 100%." async def api_targets_delete(
) source_wallet: WalletTypeInfo = Depends(require_admin_key),
await set_targets(wal.wallet.id, targets) ) -> None:
return "" await set_targets(source_wallet.wallet.id, [])
# deinit extension invoice listener
@splitpayments_ext.delete(
"/api/v1", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)]
)
async def api_stop():
for t in scheduled_tasks:
try:
t.cancel()
except Exception as ex:
logger.warning(ex)
return {"success": True}