Compare commits
30 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
138350dab4 |
||
|
|
766d317ce8 |
||
|
|
81184a0a53 |
||
|
|
d963af4042 |
||
|
|
b457fecc90 |
||
|
|
d1c14ac199 |
||
|
|
3f87a5e0ec | ||
|
|
4e2ebb7944 |
||
|
|
0bf1557ce7 |
||
|
|
c524bcee69 |
||
|
|
abe8250839 |
||
|
|
c36b5131c0 | ||
|
|
980b9eaaef |
||
|
|
5042d40af6 |
||
|
|
32bf4ae1d6 |
||
|
|
eb18fda87b |
||
|
|
d840f402da |
||
|
|
b4c8bc302d | ||
|
|
979d09d244 | ||
|
|
dbeb3d9050 |
||
|
|
82d4c65d1e |
||
|
|
4623b2e0a7 |
||
|
|
a2efae3c30 |
||
|
|
1e6e8e48c4 |
||
|
|
e69d236af9 |
||
|
|
0b621dae21 |
||
|
|
2e886fb38e |
||
|
|
6f3541be96 |
||
|
|
5bb234b797 |
||
|
|
a96897d4a2 |
30 changed files with 3124 additions and 454 deletions
10
.github/workflows/lint.yml
vendored
Normal file
10
.github/workflows/lint.yml
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
name: lint
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
uses: lnbits/lnbits/.github/workflows/lint.yml@dev
|
||||||
58
.github/workflows/release.yml
vendored
Normal file
58
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Create github release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
tag: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
gh release create "$tag" --generate-notes
|
||||||
|
|
||||||
|
pullrequest:
|
||||||
|
needs: [release]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.EXT_GITHUB }}
|
||||||
|
repository: lnbits/lnbits-extensions
|
||||||
|
path: './lnbits-extensions'
|
||||||
|
|
||||||
|
- name: setup git user
|
||||||
|
run: |
|
||||||
|
git config --global user.name "alan"
|
||||||
|
git config --global user.email "alan@lnbits.com"
|
||||||
|
|
||||||
|
- name: Create pull request in extensions repo
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.EXT_GITHUB }}
|
||||||
|
repo_name: '${{ github.event.repository.name }}'
|
||||||
|
tag: '${{ github.ref_name }}'
|
||||||
|
branch: 'update-${{ github.event.repository.name }}-${{ github.ref_name }}'
|
||||||
|
title: '[UPDATE] ${{ github.event.repository.name }} to ${{ github.ref_name }}'
|
||||||
|
body: 'https://github.com/lnbits/${{ github.event.repository.name }}/releases/${{ github.ref_name }}'
|
||||||
|
archive: 'https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip'
|
||||||
|
run: |
|
||||||
|
cd lnbits-extensions
|
||||||
|
git checkout -b $branch
|
||||||
|
|
||||||
|
# if there is another open PR
|
||||||
|
git pull origin $branch || echo "branch does not exist"
|
||||||
|
|
||||||
|
sh util.sh update_extension $repo_name $tag
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
git commit -am "$title"
|
||||||
|
git push origin $branch
|
||||||
|
|
||||||
|
# check if pr exists before creating it
|
||||||
|
gh config set pager cat
|
||||||
|
check=$(gh pr list -H $branch | wc -l)
|
||||||
|
test $check -ne 0 || gh pr create --title "$title" --body "$body" --repo lnbits/lnbits-extensions
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
__pycache__
|
||||||
|
node_modules
|
||||||
|
.mypy_cache
|
||||||
|
.venv
|
||||||
12
.prettierrc
Normal file
12
.prettierrc
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"insertPragma": false,
|
||||||
|
"printWidth": 80,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"useTabs": false,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"bracketSpacing": false
|
||||||
|
}
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 LNbits
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
47
Makefile
Normal file
47
Makefile
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
all: format check
|
||||||
|
|
||||||
|
format: prettier black ruff
|
||||||
|
|
||||||
|
check: mypy pyright checkblack checkruff checkprettier
|
||||||
|
|
||||||
|
prettier:
|
||||||
|
uv run ./node_modules/.bin/prettier --write .
|
||||||
|
pyright:
|
||||||
|
uv run ./node_modules/.bin/pyright
|
||||||
|
|
||||||
|
mypy:
|
||||||
|
uv run mypy .
|
||||||
|
|
||||||
|
black:
|
||||||
|
uv run black .
|
||||||
|
|
||||||
|
ruff:
|
||||||
|
uv run ruff check . --fix
|
||||||
|
|
||||||
|
checkruff:
|
||||||
|
uv run ruff check .
|
||||||
|
|
||||||
|
checkprettier:
|
||||||
|
uv run ./node_modules/.bin/prettier --check .
|
||||||
|
|
||||||
|
checkblack:
|
||||||
|
uv run black --check .
|
||||||
|
|
||||||
|
checkeditorconfig:
|
||||||
|
editorconfig-checker
|
||||||
|
|
||||||
|
test:
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
DEBUG=true \
|
||||||
|
uv run pytest
|
||||||
|
install-pre-commit-hook:
|
||||||
|
@echo "Installing pre-commit hook to git"
|
||||||
|
@echo "Uninstall the hook with uv run pre-commit uninstall"
|
||||||
|
uv run pre-commit install
|
||||||
|
|
||||||
|
pre-commit:
|
||||||
|
uv run pre-commit run --all-files
|
||||||
|
|
||||||
|
|
||||||
|
checkbundle:
|
||||||
|
@echo "skipping checkbundle"
|
||||||
23
README.md
23
README.md
|
|
@ -1,4 +1,6 @@
|
||||||
# Split Payments
|
# Split Payments - <small>[LNbits](https://github.com/lnbits/lnbits) extension</small>
|
||||||
|
|
||||||
|
<small>For more about LNBits extension check [this tutorial](https://github.com/lnbits/lnbits/wiki/LNbits-Documentation#use-cases-of-lnbits)</small>
|
||||||
|
|
||||||
## Have payments split between multiple wallets
|
## Have payments split between multiple wallets
|
||||||
|
|
||||||
|
|
@ -12,16 +14,16 @@ LNbits Split Payments extension allows for distributing payments across multiple
|
||||||
|
|
||||||
2. Add the wallet or wallets info to split payments to
|
2. Add the wallet or wallets info to split payments to
|
||||||
|
|
||||||
 - get the wallet id, or an invoice key from a different wallet. It can be a completely different user as long as it's under the same LNbits instance/domain. You can get the wallet information on the API Info section on every wallet page\
|
 - get the LNURLp, a LNaddress, wallet id, or an invoice key from a different wallet. It can be a completely different user on another instance/domain. You can get the wallet information on the API Info section on every wallet page\
|
||||||
 - set a wallet _Alias_ for your own identification\
|
 - set a wallet _Alias_ for your own identification\
|
||||||
|
|
||||||
- set how much, in percentage, this wallet will receive from every payment sent to the source wallets
|
- set how much, in percentage, this wallet will receive from every payment sent to the source wallet
|
||||||
|
|
||||||
3. When done, click "SAVE TARGETS" to make the splits effective
|
3. When done with adding or deleting a set of targets, click "SAVE TARGETS" to make the splits effective.
|
||||||
|
|
||||||
4. You can have several wallets to split to, as long as the sum of the percentages is under or equal to 100%
|
4. You can have several wallets to split to, as long as the sum of the percentages is under or equal to 100%. It can only reach 100% if the targets are all internal ones.
|
||||||
|
|
||||||
5. When the source wallet receives a payment, the extension will automatically split the corresponding values to every wallet\
|
5. When the source wallet receives a payment, the extension will automatically split the corresponding values to every wallet.
|
||||||
- on receiving a 20 sats payment\
|
- on receiving a 20 sats payment\
|
||||||

|

|
||||||
- source wallet gets 18 sats\
|
- source wallet gets 18 sats\
|
||||||
|
|
@ -29,6 +31,15 @@ LNbits Split Payments extension allows for distributing payments across multiple
|
||||||
- Ben's wallet (the wallet from the example) instantly, and feeless, gets the corresponding 10%, or 2 sats\
|
- Ben's wallet (the wallet from the example) instantly, and feeless, gets the corresponding 10%, or 2 sats\
|
||||||

|

|
||||||
|
|
||||||
|
IMPORTANT:
|
||||||
|
|
||||||
|
- If you split to a LNURLp or LNaddress through the LNURLp extension make sure your recipients allow comments ! Split&Scrub add a comment in your transaction - and if it is not allowed, the split/scrub will not take place.
|
||||||
|
- Make sure the LNURLp / LNaddress of the recipient has its min-sats set very low (e.g. 1 sat). If the wallet does not belong to you you can [check with a Decoder](https://lightningdecoder.com/), if that is the case already
|
||||||
|
- Yes, there is fees - internal and external! Updating your own wallets on your own instance will not cost any fees but sending to an external instance will. Please notice that you should therefore not split up to 100% if you send to a wallet that is external (leave 1-2% reserve for routing fees!). External fees are deducted from the individual payment percentage of the recipient
|
||||||
|
|
||||||
|
<img width="1148" alt="Bildschirmfoto 2023-05-01 um 22 14 36" src="https://user-images.githubusercontent.com/63317640/235534056-49296aeb-7295-4b4e-9f57-914a677f5ad4.png">
|
||||||
|
<img width="1402" alt="Bildschirmfoto 2023-05-01 um 22 17 52" src="https://user-images.githubusercontent.com/63317640/235534063-b2734654-7c1a-48a3-b48e-32798c232b49.png">
|
||||||
|
|
||||||
## Sponsored by
|
## Sponsored by
|
||||||
|
|
||||||
[](https://cryptograffiti.com/)
|
[](https://cryptograffiti.com/)
|
||||||
|
|
|
||||||
44
__init__.py
44
__init__.py
|
|
@ -1,35 +1,47 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi.staticfiles import StaticFiles
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.db import Database
|
from .crud import db
|
||||||
from lnbits.helpers import template_renderer
|
from .tasks import wait_for_paid_invoices
|
||||||
from lnbits.tasks import catch_everything_and_restart
|
from .views import splitpayments_generic_router
|
||||||
|
from .views_api import splitpayments_api_router
|
||||||
db = Database("ext_splitpayments")
|
|
||||||
|
|
||||||
splitpayments_static_files = [
|
splitpayments_static_files = [
|
||||||
{
|
{
|
||||||
"path": "/splitpayments/static",
|
"path": "/splitpayments/static",
|
||||||
"app": StaticFiles(packages=[("lnbits", "extensions/splitpayments/static")]),
|
|
||||||
"name": "splitpayments_static",
|
"name": "splitpayments_static",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
splitpayments_ext: APIRouter = APIRouter(
|
splitpayments_ext: APIRouter = APIRouter(
|
||||||
prefix="/splitpayments", tags=["splitpayments"]
|
prefix="/splitpayments", tags=["splitpayments"]
|
||||||
)
|
)
|
||||||
|
splitpayments_ext.include_router(splitpayments_generic_router)
|
||||||
|
splitpayments_ext.include_router(splitpayments_api_router)
|
||||||
|
|
||||||
|
scheduled_tasks: list[asyncio.Task] = []
|
||||||
|
|
||||||
|
|
||||||
def splitpayments_renderer():
|
def splitpayments_stop():
|
||||||
return template_renderer(["lnbits/extensions/splitpayments/templates"])
|
for task in scheduled_tasks:
|
||||||
|
try:
|
||||||
|
task.cancel()
|
||||||
from .tasks import wait_for_paid_invoices
|
except Exception as ex:
|
||||||
from .views import * # noqa: F401,F403
|
logger.warning(ex)
|
||||||
from .views_api import * # noqa: F401,F403
|
|
||||||
|
|
||||||
|
|
||||||
def splitpayments_start():
|
def splitpayments_start():
|
||||||
loop = asyncio.get_event_loop()
|
from lnbits.tasks import create_permanent_unique_task
|
||||||
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))
|
|
||||||
|
task = create_permanent_unique_task("ext_splitpayments", wait_for_paid_invoices)
|
||||||
|
scheduled_tasks.append(task)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"db",
|
||||||
|
"splitpayments_ext",
|
||||||
|
"splitpayments_start",
|
||||||
|
"splitpayments_static_files",
|
||||||
|
"splitpayments_stop",
|
||||||
|
]
|
||||||
|
|
|
||||||
54
config.json
54
config.json
|
|
@ -2,5 +2,57 @@
|
||||||
"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"]
|
"version": "1.1.0",
|
||||||
|
"min_lnbits_version": "1.3.0",
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "cryptograffiti",
|
||||||
|
"uri": "https://github.com/cryptograffiti",
|
||||||
|
"role": "Idea/Sponsor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fiatjaf",
|
||||||
|
"uri": "https://github.com/fiatjaf",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dni",
|
||||||
|
"uri": "https://github.com/dni",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "talvasconcelos",
|
||||||
|
"uri": "https://github.com/talvasconcelos",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "prusnak",
|
||||||
|
"uri": "https://github.com/prusnak",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "arbadacarbaYK",
|
||||||
|
"uri": "https://github.com/arbadacarbaYK",
|
||||||
|
"role": "Developer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "arcbtc",
|
||||||
|
"uri": "https://github.com/arcbtc",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"uri": "https://raw.githubusercontent.com/lnbits/splitpayments/main/static/image/1.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "https://raw.githubusercontent.com/lnbits/splitpayments/main/static/image/2.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "https://raw.githubusercontent.com/lnbits/splitpayments/main/static/image/3.png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description_md": "https://raw.githubusercontent.com/lnbits/splitpayments/main/description.md",
|
||||||
|
"terms_and_conditions_md": "https://raw.githubusercontent.com/lnbits/splitpayments/main/toc.md",
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
crud.py
37
crud.py
|
|
@ -1,36 +1,23 @@
|
||||||
from typing import List
|
from lnbits.db import Database
|
||||||
|
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
|
||||||
|
|
||||||
from . import db
|
|
||||||
from .models import Target
|
from .models import Target
|
||||||
|
|
||||||
|
db = Database("ext_splitpayments")
|
||||||
|
|
||||||
async def get_targets(source_wallet: str) -> List[Target]:
|
|
||||||
rows = await db.fetchall(
|
async def get_targets(source_wallet: str) -> list[Target]:
|
||||||
"SELECT * FROM splitpayments.targets WHERE source = ?", (source_wallet,)
|
return await db.fetchall(
|
||||||
|
"SELECT * FROM splitpayments.targets WHERE source = :source_wallet",
|
||||||
|
{"source_wallet": source_wallet},
|
||||||
|
Target,
|
||||||
)
|
)
|
||||||
return [Target(**row) for row in rows]
|
|
||||||
|
|
||||||
|
|
||||||
async def set_targets(source_wallet: str, targets: List[Target]):
|
async def set_targets(source_wallet: str, targets: list[Target]):
|
||||||
async with db.connect() as conn:
|
async with db.connect() as conn:
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
"DELETE FROM splitpayments.targets WHERE source = ?", (source_wallet,)
|
"DELETE FROM splitpayments.targets WHERE source = :source_wallet",
|
||||||
|
{"source_wallet": source_wallet},
|
||||||
)
|
)
|
||||||
for target in targets:
|
for target in targets:
|
||||||
await conn.execute(
|
await conn.insert("splitpayments.targets", target)
|
||||||
"""
|
|
||||||
INSERT INTO splitpayments.targets
|
|
||||||
(id, source, wallet, percent, tag, alias)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
urlsafe_short_hash(),
|
|
||||||
source_wallet,
|
|
||||||
target.wallet,
|
|
||||||
target.percent,
|
|
||||||
target.tag,
|
|
||||||
target.alias,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
|
||||||
16
description.md
Normal file
16
description.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
Split Payments across multiple wallets/lnaddresses/lnurlps seamlessly!
|
||||||
|
Once configured, it continuously splits your payments across different wallets.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
- Enable the Extension: Start by enabling the Split Payments extension.
|
||||||
|
- Select the Source Wallet: Identify and select the wallet that will receive and subsequently distribute the payments.
|
||||||
|
- Add Wallet Information for Payment Splitting: Enter the details of the wallets where the payments will be split. This could include LNURLp, LNaddress, wallet ID, or an invoice key from a different wallet. Wallet details can be found under the API Info section on each wallet's page. Optionally, assign an alias to each wallet for easier identification.
|
||||||
|
- Set Distribution Percentages: Specify the percentage of each payment that each wallet should receive. Ensure the total distribution does not exceed 100%.
|
||||||
|
- Save Your Settings: After adding or deleting wallet information, click “SAVE TARGETS” to activate the payment splits.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
You can distribute payments to multiple wallets as long as their combined percentage is at or below 100%. Distribution can only total exactly 100% if all target wallets are internal.
|
||||||
|
|
||||||
|
Automatic Payment Splitting:
|
||||||
|
When the source wallet receives a payment, the extension automatically allocates the specified percentages to each designated wallet.
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"repos": [
|
"repos": [
|
||||||
{
|
{
|
||||||
"id": "splitpayments",
|
"id": "splitpayments",
|
||||||
"organisation": "lnbits",
|
"organisation": "PatMulligan",
|
||||||
"repository": "splitpayments"
|
"repository": "splitpayments"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
|
from lnbits.db import Connection
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
|
|
||||||
async def m001_initial(db):
|
async def m001_initial(db: Connection):
|
||||||
"""
|
"""
|
||||||
Initial split payment table.
|
Initial split payment table.
|
||||||
"""
|
"""
|
||||||
|
|
@ -19,11 +20,12 @@ async def m001_initial(db):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def m002_float_percent(db):
|
async def m002_float_percent(db: Connection):
|
||||||
"""
|
"""
|
||||||
Add float percent and migrates the existing data.
|
alter percent to be float.
|
||||||
"""
|
"""
|
||||||
await db.execute("ALTER TABLE splitpayments.targets RENAME TO splitpayments_old")
|
await db.execute("ALTER TABLE splitpayments.targets RENAME TO splitpayments_m001")
|
||||||
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE splitpayments.targets (
|
CREATE TABLE splitpayments.targets (
|
||||||
|
|
@ -36,11 +38,9 @@ async def m002_float_percent(db):
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
result = await db.execute("SELECT * FROM splitpayments.splitpayments_m001")
|
||||||
for row in [
|
rows = result.mappings().all()
|
||||||
list(row)
|
for row in rows:
|
||||||
for row in await db.fetchall("SELECT * FROM splitpayments.splitpayments_old")
|
|
||||||
]:
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO splitpayments.targets (
|
INSERT INTO splitpayments.targets (
|
||||||
|
|
@ -49,19 +49,25 @@ async def m002_float_percent(db):
|
||||||
percent,
|
percent,
|
||||||
alias
|
alias
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (:wallet, :source, :percent, :alias)
|
||||||
""",
|
""",
|
||||||
(row[0], row[1], row[2], row[3]),
|
{
|
||||||
|
"wallet": row["wallet"],
|
||||||
|
"source": row["source"],
|
||||||
|
"percent": row["percent"],
|
||||||
|
"alias": row["alias"],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
await db.execute("DROP TABLE splitpayments.splitpayments_old")
|
await db.execute("DROP TABLE splitpayments.splitpayments_m001")
|
||||||
|
|
||||||
|
|
||||||
async def m003_add_id_and_tag(db):
|
async def m003_add_id_and_tag(db: Connection):
|
||||||
"""
|
"""
|
||||||
Add float percent and migrates the existing data.
|
Add id, tag and migrates the existing data.
|
||||||
"""
|
"""
|
||||||
await db.execute("ALTER TABLE splitpayments.targets RENAME TO splitpayments_old")
|
await db.execute("ALTER TABLE splitpayments.targets RENAME TO splitpayments_m002")
|
||||||
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE splitpayments.targets (
|
CREATE TABLE splitpayments.targets (
|
||||||
|
|
@ -76,11 +82,9 @@ async def m003_add_id_and_tag(db):
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
result = await db.execute("SELECT * FROM splitpayments.splitpayments_m002")
|
||||||
for row in [
|
rows = result.mappings().all()
|
||||||
list(row)
|
for row in rows:
|
||||||
for row in await db.fetchall("SELECT * FROM splitpayments.splitpayments_old")
|
|
||||||
]:
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO splitpayments.targets (
|
INSERT INTO splitpayments.targets (
|
||||||
|
|
@ -91,9 +95,42 @@ async def m003_add_id_and_tag(db):
|
||||||
tag,
|
tag,
|
||||||
alias
|
alias
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (:id, :wallet, :source, :percent, :tag, :alias)
|
||||||
""",
|
""",
|
||||||
(urlsafe_short_hash(), row[0], row[1], row[2], "", row[3]),
|
{
|
||||||
|
"id": urlsafe_short_hash(),
|
||||||
|
"wallet": row["wallet"],
|
||||||
|
"source": row["source"],
|
||||||
|
"percent": row["percent"],
|
||||||
|
"tag": row["tag"],
|
||||||
|
"alias": row["alias"],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
await db.execute("DROP TABLE splitpayments.splitpayments_old")
|
await db.execute("DROP TABLE splitpayments.splitpayments_m002")
|
||||||
|
|
||||||
|
|
||||||
|
async def m004_remove_tag(db: Connection):
|
||||||
|
"""
|
||||||
|
This removes tag
|
||||||
|
"""
|
||||||
|
keys = "id,wallet,source,percent,alias"
|
||||||
|
new_db = "splitpayments.targets"
|
||||||
|
old_db = "splitpayments.targets_m003"
|
||||||
|
|
||||||
|
await db.execute(f"ALTER TABLE {new_db} RENAME TO targets_m003")
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
|
||||||
26
models.py
26
models.py
|
|
@ -1,28 +1,20 @@
|
||||||
from sqlite3 import Row
|
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class Target(BaseModel):
|
class Target(BaseModel):
|
||||||
|
id: str
|
||||||
wallet: str
|
wallet: str
|
||||||
source: str
|
source: str
|
||||||
percent: float
|
percent: float
|
||||||
tag: str
|
alias: str | None = None
|
||||||
alias: Optional[str]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_row(cls, row: Row):
|
|
||||||
return cls(**dict(row))
|
|
||||||
|
|
||||||
|
|
||||||
class TargetPutList(BaseModel):
|
|
||||||
wallet: str = Query(...)
|
|
||||||
alias: str = Query("")
|
|
||||||
percent: float = Query(..., ge=0, lt=100)
|
|
||||||
tag: str
|
|
||||||
|
|
||||||
|
|
||||||
class TargetPut(BaseModel):
|
class TargetPut(BaseModel):
|
||||||
__root__: List[TargetPutList]
|
wallet: str = Query(...)
|
||||||
|
alias: str = Query("")
|
||||||
|
percent: float = Query(..., ge=0, le=100)
|
||||||
|
|
||||||
|
|
||||||
|
class TargetPutList(BaseModel):
|
||||||
|
targets: list[TargetPut]
|
||||||
|
|
|
||||||
59
package-lock.json
generated
Normal file
59
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"name": "splitpayments",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "splitpayments",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"pyright": "^1.1.358"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
||||||
|
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pyright": {
|
||||||
|
"version": "1.1.374",
|
||||||
|
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.374.tgz",
|
||||||
|
"integrity": "sha512-ISbC1YnYDYrEatoKKjfaA5uFIp0ddC/xw9aSlN/EkmwupXUMVn41Jl+G6wHEjRhC+n4abHZeGpEvxCUus/K9dA==",
|
||||||
|
"bin": {
|
||||||
|
"pyright": "index.js",
|
||||||
|
"pyright-langserver": "langserver.index.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
package.json
Normal file
15
package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "splitpayments",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"pyright": "^1.1.358"
|
||||||
|
}
|
||||||
|
}
|
||||||
92
pyproject.toml
Normal file
92
pyproject.toml
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
[project]
|
||||||
|
name = "splitpayments"
|
||||||
|
version = "1.1.1"
|
||||||
|
requires-python = ">=3.10,<3.13"
|
||||||
|
description = "Send incoming payments to different targets"
|
||||||
|
authors = [{name = "benarc"}, {name = "dni"}, {name = "alan"}]
|
||||||
|
urls = { Homepage = "https://lnbits.com", Repository = "https://github.com/lnbits/splitpayments" }
|
||||||
|
|
||||||
|
dependencies = [ "lnbits>1" ]
|
||||||
|
|
||||||
|
[tool.poetry]
|
||||||
|
package-mode = false
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
dev-dependencies = [
|
||||||
|
"black",
|
||||||
|
"pytest-asyncio",
|
||||||
|
"pytest",
|
||||||
|
"mypy==1.17.1",
|
||||||
|
"pre-commit",
|
||||||
|
"ruff",
|
||||||
|
"pytest-md",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
exclude = "(nostr/*)"
|
||||||
|
plugins = ["pydantic.mypy"]
|
||||||
|
|
||||||
|
[tool.pydantic-mypy]
|
||||||
|
init_forbid_extra = true
|
||||||
|
init_typed = true
|
||||||
|
warn_required_dynamic_aliases = true
|
||||||
|
warn_untyped_fields = true
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
log_cli = false
|
||||||
|
testpaths = [
|
||||||
|
"tests"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 88
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
# Same as Black. + 10% rule of black
|
||||||
|
line-length = 88
|
||||||
|
exclude = [
|
||||||
|
"nostr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
# Enable:
|
||||||
|
# F - pyflakes
|
||||||
|
# E - pycodestyle errors
|
||||||
|
# W - pycodestyle warnings
|
||||||
|
# I - isort
|
||||||
|
# A - flake8-builtins
|
||||||
|
# C - mccabe
|
||||||
|
# N - naming
|
||||||
|
# UP - pyupgrade
|
||||||
|
# RUF - ruff
|
||||||
|
# B - bugbear
|
||||||
|
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"]
|
||||||
|
ignore = ["C901"]
|
||||||
|
|
||||||
|
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||||
|
fixable = ["ALL"]
|
||||||
|
unfixable = []
|
||||||
|
|
||||||
|
# Allow unused variables when underscore-prefixed.
|
||||||
|
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||||
|
|
||||||
|
# needed for pydantic
|
||||||
|
[tool.ruff.lint.pep8-naming]
|
||||||
|
classmethod-decorators = [
|
||||||
|
"validator",
|
||||||
|
"root_validator",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Ignore unused imports in __init__.py files.
|
||||||
|
# [tool.ruff.lint.extend-per-file-ignores]
|
||||||
|
# "__init__.py" = ["F401", "F403"]
|
||||||
|
|
||||||
|
# [tool.ruff.lint.mccabe]
|
||||||
|
# max-complexity = 10
|
||||||
|
|
||||||
|
[tool.ruff.lint.flake8-bugbear]
|
||||||
|
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
|
||||||
|
extend-immutable-calls = [
|
||||||
|
"fastapi.Depends",
|
||||||
|
"fastapi.Query",
|
||||||
|
]
|
||||||
BIN
static/image/1.png
Normal file
BIN
static/image/1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
BIN
static/image/2.png
Normal file
BIN
static/image/2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
static/image/3.png
Normal file
BIN
static/image/3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
|
|
@ -1,7 +1,3 @@
|
||||||
/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */
|
|
||||||
|
|
||||||
Vue.component(VueQrcode.name, VueQrcode)
|
|
||||||
|
|
||||||
function hashTargets(targets) {
|
function hashTargets(targets) {
|
||||||
return targets
|
return targets
|
||||||
.filter(isTargetComplete)
|
.filter(isTargetComplete)
|
||||||
|
|
@ -17,18 +13,19 @@ function isTargetComplete(target) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
new Vue({
|
window.app = Vue.createApp({
|
||||||
el: '#vue',
|
el: '#vue',
|
||||||
mixins: [windowMixin],
|
mixins: [windowMixin],
|
||||||
|
watch: {
|
||||||
|
selectedWallet() {
|
||||||
|
this.getTargets()
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
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,18 +34,12 @@ 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) {
|
||||||
|
if (this.targets.length == 1) {
|
||||||
|
return this.deleteTargets()
|
||||||
|
}
|
||||||
this.targets.splice(index, 1)
|
this.targets.splice(index, 1)
|
||||||
console.log(this.targets)
|
Quasar.Notify.create({
|
||||||
this.$q.notify({
|
|
||||||
message: 'Removed item. You must click to save manually.',
|
message: 'Removed item. You must click to save manually.',
|
||||||
timeout: 500
|
timeout: 500
|
||||||
})
|
})
|
||||||
|
|
@ -60,136 +51,77 @@ new Vue({
|
||||||
'/splitpayments/api/v1/targets',
|
'/splitpayments/api/v1/targets',
|
||||||
this.selectedWallet.adminkey
|
this.selectedWallet.adminkey
|
||||||
)
|
)
|
||||||
|
.then(res => {
|
||||||
|
this.targets = res.data.map(t => ({
|
||||||
|
...t,
|
||||||
|
targetChoice: t.targetChoice || 'wallet'
|
||||||
|
}))
|
||||||
|
})
|
||||||
.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({
|
||||||
this.targets[index].tag = null
|
source: this.selectedWallet,
|
||||||
this.targets[index].method = 'split'
|
targetChoice: 'wallet'
|
||||||
} 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++) {
|
const payload = this.targets
|
||||||
if (this.targets[i].tag != '') {
|
.filter(t => t.wallet && String(t.wallet).trim() !== '')
|
||||||
this.targets[i].percent = 0
|
.map(({alias, percent, wallet}) => ({
|
||||||
} else {
|
alias,
|
||||||
this.targets[i].tag = ''
|
percent: Number(percent) || 0,
|
||||||
}
|
wallet
|
||||||
}
|
}))
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'PUT',
|
'PUT',
|
||||||
'/splitpayments/api/v1/targets',
|
'/splitpayments/api/v1/targets',
|
||||||
this.selectedWallet.adminkey,
|
this.selectedWallet.adminkey,
|
||||||
{
|
{
|
||||||
targets: this.targets
|
targets: payload
|
||||||
.filter(isTargetComplete)
|
|
||||||
.map(({wallet, percent, tag, alias}) => ({
|
|
||||||
wallet,
|
|
||||||
percent,
|
|
||||||
tag,
|
|
||||||
alias
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.$q.notify({
|
Quasar.Notify.create({
|
||||||
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 all targets?')
|
||||||
|
.onOk(() => {
|
||||||
|
this.targets = []
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'DELETE',
|
||||||
|
'/splitpayments/api/v1/targets',
|
||||||
|
this.selectedWallet.adminkey
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
message: 'Split payments targets deleted.',
|
||||||
|
timeout: 700
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.selectedWallet = this.g.user.wallets[0]
|
this.selectedWallet = this.g.user.wallets[0]
|
||||||
this.getTargets()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
100
tasks.py
100
tasks.py
|
|
@ -1,19 +1,25 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from math import floor
|
||||||
|
|
||||||
from loguru import logger
|
import bolt11
|
||||||
|
from lnbits.core.crud import get_standalone_payment
|
||||||
|
from lnbits.core.crud.wallets import get_wallet_for_key
|
||||||
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 (
|
||||||
from lnbits.helpers import get_current_extension_name
|
create_invoice,
|
||||||
|
fee_reserve,
|
||||||
|
get_pr_from_lnurl,
|
||||||
|
pay_invoice,
|
||||||
|
)
|
||||||
from lnbits.tasks import register_invoice_listener
|
from lnbits.tasks import register_invoice_listener
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from .crud import get_targets
|
from .crud import get_targets
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_paid_invoices():
|
async def wait_for_paid_invoices():
|
||||||
invoice_queue = asyncio.Queue()
|
invoice_queue = asyncio.Queue()
|
||||||
register_invoice_listener(invoice_queue, get_current_extension_name())
|
register_invoice_listener(invoice_queue, "ext_splitpayments_invoice_listener")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
payment = await invoice_queue.get()
|
payment = await invoice_queue.get()
|
||||||
await on_invoice_paid(payment)
|
await on_invoice_paid(payment)
|
||||||
|
|
@ -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,77 @@ 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 target.percent > 0:
|
||||||
|
amount_msat = int(payment.amount * target.percent / 100)
|
||||||
|
memo = (
|
||||||
|
f"Split payment: {target.percent}% "
|
||||||
|
f"for {target.alias or target.wallet}"
|
||||||
|
f";{payment.memo};{payment.payment_hash}"
|
||||||
|
)
|
||||||
|
|
||||||
if tagged or target.percent > 0:
|
if "@" in target.wallet or "LNURL" in target.wallet:
|
||||||
|
safe_amount_msat = amount_msat - fee_reserve(amount_msat)
|
||||||
if tagged:
|
payment_request = await get_lnurl_invoice(
|
||||||
memo = f"Pushed tagged payment to {target.alias}"
|
target.wallet, payment.wallet_id, safe_amount_msat, memo
|
||||||
amount_msat = int(amount_to_split)
|
)
|
||||||
else:
|
else:
|
||||||
amount_msat = int(amount_to_split * target.percent / 100)
|
wallet = await get_wallet_for_key(target.wallet)
|
||||||
memo = f"Split payment: {target.percent}% for {target.alias or target.wallet}"
|
if wallet is not None:
|
||||||
|
target.wallet = wallet.id
|
||||||
payment_hash, payment_request = await create_invoice(
|
new_payment = await create_invoice(
|
||||||
wallet_id=target.wallet,
|
wallet_id=target.wallet,
|
||||||
amount=int(amount_msat / 1000),
|
amount=int(amount_msat / 1000),
|
||||||
internal=True,
|
internal=True,
|
||||||
memo=memo,
|
memo=memo,
|
||||||
)
|
)
|
||||||
|
payment_request = new_payment.bolt11
|
||||||
|
|
||||||
extra = {**payment.extra, "tag": "splitpayments", "splitted": True}
|
extra = {**payment.extra, "splitted": True}
|
||||||
|
|
||||||
await pay_invoice(
|
if payment_request:
|
||||||
|
task = asyncio.create_task(
|
||||||
|
pay_invoice_in_background(
|
||||||
payment_request=payment_request,
|
payment_request=payment_request,
|
||||||
wallet_id=payment.wallet_id,
|
wallet_id=payment.wallet_id,
|
||||||
|
description=memo,
|
||||||
extra=extra,
|
extra=extra,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
task.add_done_callback(lambda fut: logger.success(fut.result()))
|
||||||
|
|
||||||
|
|
||||||
|
async def pay_invoice_in_background(payment_request, wallet_id, description, extra):
|
||||||
|
try:
|
||||||
|
await pay_invoice(
|
||||||
|
payment_request=payment_request,
|
||||||
|
wallet_id=wallet_id,
|
||||||
|
description=description,
|
||||||
|
extra=extra,
|
||||||
|
)
|
||||||
|
return f"Splitpayments: paid invoice for {description}"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to pay invoice: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_lnurl_invoice(
|
||||||
|
payoraddress: str, wallet_id: str, amount_msat: int, memo: str
|
||||||
|
) -> str | None:
|
||||||
|
|
||||||
|
rounded_amount = floor(amount_msat / 1000) * 1000
|
||||||
|
|
||||||
|
try:
|
||||||
|
payment_request = await get_pr_from_lnurl(payoraddress, rounded_amount, memo)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting LNURL invoice: {e!s}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
invoice = bolt11.decode(payment_request)
|
||||||
|
|
||||||
|
lnurlp_payment = await get_standalone_payment(invoice.payment_hash)
|
||||||
|
|
||||||
|
if lnurlp_payment and lnurlp_payment.wallet_id == wallet_id:
|
||||||
|
logger.error("split failed. cannot split payments to yourself via LNURL.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return payment_request
|
||||||
|
|
|
||||||
|
|
@ -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": <admin_key>}</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": <wallet id>, "alias": <chosen name for this
|
|
||||||
wallet>, "percent": <number between 1 and 100>}, ...]</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": <admin_key>}</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": <wallet id or invoice
|
|
||||||
key>, "alias": <name to identify this>, "percent": <number
|
|
||||||
between 1 and 100>}, ...]}'
|
|
||||||
</code>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-expansion-item>
|
|
||||||
</q-expansion-item>
|
|
||||||
|
|
@ -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">
|
||||||
|
|
@ -9,10 +9,9 @@
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
:options="g.user.wallets"
|
:options="g.user.wallets"
|
||||||
:value="selectedWallet"
|
v-model="selectedWallet"
|
||||||
label="Source Wallet:"
|
label="Source Wallet:"
|
||||||
option-label="name"
|
option-label="name"
|
||||||
@input="changedWallet"
|
|
||||||
>
|
>
|
||||||
</q-select>
|
</q-select>
|
||||||
</q-form>
|
</q-form>
|
||||||
|
|
@ -34,49 +33,57 @@
|
||||||
<q-input
|
<q-input
|
||||||
dense
|
dense
|
||||||
outlined
|
outlined
|
||||||
v-model="target.alias"
|
v-model.trim="target.alias"
|
||||||
label="Alias"
|
label="Alias"
|
||||||
:hint="t === targets.length - 1 ? 'A name to identify this target wallet locally.' : undefined"
|
:hint="t === targets.length - 1 ? 'A name to identify this target wallet locally.' : undefined"
|
||||||
style="width: 150px"
|
style="width: 150px"
|
||||||
></q-input>
|
></q-input>
|
||||||
|
<div class="column q-mt-none">
|
||||||
|
<div class="col">
|
||||||
|
<q-radio
|
||||||
|
class="float-left"
|
||||||
|
v-model="target.targetChoice"
|
||||||
|
val="wallet"
|
||||||
|
label="wallet"
|
||||||
|
></q-radio>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-radio
|
||||||
|
class="float-left"
|
||||||
|
v-model="target.targetChoice"
|
||||||
|
val="lnurl"
|
||||||
|
label="lnurl"
|
||||||
|
></q-radio>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<q-input
|
<q-input
|
||||||
|
v-if="target.targetChoice === 'lnurl'"
|
||||||
dense
|
dense
|
||||||
v-model="target.wallet"
|
v-model.trim="target.wallet"
|
||||||
label="Wallet"
|
label="Target"
|
||||||
:hint="t === targets.length - 1 ? 'A wallet ID or invoice key.' : undefined"
|
hint="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-select
|
||||||
<q-toggle
|
v-if="target.targetChoice === 'wallet'"
|
||||||
:false-value="'split'"
|
class="q-pr-md q-pt-sm"
|
||||||
:true-value="'tag'"
|
filled
|
||||||
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
|
|
||||||
v-if="target.method == 'tag'"
|
|
||||||
style="width: 150px"
|
|
||||||
dense
|
dense
|
||||||
outlined
|
style="width: 500px"
|
||||||
v-model="target.tag"
|
v-model="target.wallet"
|
||||||
label="Tag name"
|
:options="g.user.walletOptions"
|
||||||
suffix="#"
|
emit-value
|
||||||
></q-input>
|
map-options
|
||||||
|
label="Receive wallet *"
|
||||||
|
>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
<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 +93,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 +117,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,18 +128,44 @@
|
||||||
</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-list> {% include "splitpayments/_api_docs.html" %} </q-list>
|
<q-card>
|
||||||
|
<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-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
|
|
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
11
tests/test_init.py
Normal file
11
tests/test_init.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import pytest
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from .. import splitpayments_ext
|
||||||
|
|
||||||
|
|
||||||
|
# just import router and add it to a test router
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_router():
|
||||||
|
router = APIRouter()
|
||||||
|
router.include_router(splitpayments_ext)
|
||||||
29
toc.md
Normal file
29
toc.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Terms and Conditions for LNbits Extension
|
||||||
|
|
||||||
|
## 1. Acceptance of Terms
|
||||||
|
|
||||||
|
By installing and using the LNbits extension ("Extension"), you agree to be bound by these terms and conditions ("Terms"). If you do not agree to these Terms, do not use the Extension.
|
||||||
|
|
||||||
|
## 2. License
|
||||||
|
|
||||||
|
The Extension is free and open-source software, released under [specify the FOSS license here, e.g., GPL-3.0, MIT, etc.]. You are permitted to use, copy, modify, and distribute the Extension under the terms of that license.
|
||||||
|
|
||||||
|
## 3. No Warranty
|
||||||
|
|
||||||
|
The Extension is provided "as is" and with all faults, and the developer expressly disclaims all warranties of any kind, whether express, implied, statutory, or otherwise, including but not limited to warranties of merchantability, fitness for a particular purpose, non-infringement, and any warranties arising out of course of dealing or usage of trade. No advice or information, whether oral or written, obtained from the developer or elsewhere will create any warranty not expressly stated in this Terms.
|
||||||
|
|
||||||
|
## 4. Limitation of Liability
|
||||||
|
|
||||||
|
In no event will the developer be liable to you or any third party for any direct, indirect, incidental, special, consequential, or punitive damages, including lost profit, lost revenue, loss of data, or other damages arising out of or in connection with your use of the Extension, even if the developer has been advised of the possibility of such damages. The foregoing limitation of liability shall apply to the fullest extent permitted by law in the applicable jurisdiction.
|
||||||
|
|
||||||
|
## 5. Modification of Terms
|
||||||
|
|
||||||
|
The developer reserves the right to modify these Terms at any time. You are advised to review these Terms periodically for any changes. Changes to these Terms are effective when they are posted on the appropriate location within or associated with the Extension.
|
||||||
|
|
||||||
|
## 6. General Provisions
|
||||||
|
|
||||||
|
If any provision of these Terms is held to be invalid or unenforceable, that provision will be enforced to the maximum extent permissible, and the other provisions of these Terms will remain in full force and effect. These Terms constitute the entire agreement between you and the developer regarding the use of the Extension.
|
||||||
|
|
||||||
|
## 7. Contact Information
|
||||||
|
|
||||||
|
If you have any questions about these Terms, please contact the developer at [developer's contact information].
|
||||||
19
views.py
19
views.py
|
|
@ -1,17 +1,18 @@
|
||||||
from fastapi import Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.responses import HTMLResponse
|
||||||
from starlette.responses import HTMLResponse
|
|
||||||
|
|
||||||
from lnbits.core.models import User
|
from lnbits.core.models import User
|
||||||
from lnbits.decorators import check_user_exists
|
from lnbits.decorators import check_user_exists
|
||||||
|
from lnbits.helpers import template_renderer
|
||||||
|
|
||||||
from . import splitpayments_ext, splitpayments_renderer
|
splitpayments_generic_router = APIRouter()
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
|
||||||
|
|
||||||
|
|
||||||
@splitpayments_ext.get("/", response_class=HTMLResponse)
|
def splitpayments_renderer():
|
||||||
|
return template_renderer(["splitpayments/templates"])
|
||||||
|
|
||||||
|
|
||||||
|
@splitpayments_generic_router.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
return splitpayments_renderer().TemplateResponse(
|
return splitpayments_renderer().TemplateResponse(
|
||||||
"splitpayments/index.html", {"request": request, "user": user.dict()}
|
"splitpayments/index.html", {"request": request, "user": user.json()}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
74
views_api.py
74
views_api.py
|
|
@ -1,45 +1,52 @@
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from fastapi import Depends, Request
|
from fastapi import APIRouter, Depends, 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.core.models import WalletTypeInfo
|
||||||
|
from lnbits.decorators import require_admin_key
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from . import 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_api_router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@splitpayments_ext.get("/api/v1/targets")
|
@splitpayments_api_router.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_api_router.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:
|
||||||
|
|
||||||
|
if entry.wallet.find("@") < 0 and entry.wallet.find("LNURL") < 0:
|
||||||
wallet = await get_wallet(entry.wallet)
|
wallet = await get_wallet(entry.wallet)
|
||||||
if not wallet:
|
if not wallet:
|
||||||
wallet = await get_wallet_for_key(entry.wallet, "invoice")
|
wallet = await get_wallet_for_key(entry.wallet)
|
||||||
if not wallet:
|
if not wallet:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=f"Invalid wallet '{entry.wallet}'.",
|
detail=f"Invalid wallet '{entry.wallet}'.",
|
||||||
)
|
)
|
||||||
|
|
||||||
if wallet.id == wal.wallet.id:
|
if wallet.id == source_wallet.wallet.id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail="Can't split to itself."
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail="Can't split to itself.",
|
||||||
)
|
)
|
||||||
|
|
||||||
if entry.percent < 0:
|
if entry.percent <= 0:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
detail=f"Invalid percent '{entry.percent}'.",
|
detail=f"Invalid percent '{entry.percent}'.",
|
||||||
|
|
@ -47,17 +54,32 @@ async def api_targets_set(
|
||||||
|
|
||||||
targets.append(
|
targets.append(
|
||||||
Target(
|
Target(
|
||||||
wallet=wallet.id,
|
id=urlsafe_short_hash(),
|
||||||
source=wal.wallet.id,
|
wallet=entry.wallet,
|
||||||
tag=entry.tag,
|
source=source_wallet.wallet.id,
|
||||||
percent=entry.percent,
|
percent=entry.percent,
|
||||||
alias=entry.alias,
|
alias=entry.alias,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
percent_sum = sum([target.percent for target in targets])
|
percent_sum = sum([target.percent for target in targets])
|
||||||
if percent_sum > 100:
|
if percent_sum > 100:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.BAD_REQUEST, detail="Splitting over 100%."
|
status_code=HTTPStatus.BAD_REQUEST, detail="Splitting over 100%"
|
||||||
)
|
)
|
||||||
await set_targets(wal.wallet.id, targets)
|
|
||||||
return ""
|
await set_targets(source_wallet.wallet.id, targets)
|
||||||
|
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning(ex)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Cannot set targets.",
|
||||||
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
|
@splitpayments_api_router.delete("/api/v1/targets", status_code=HTTPStatus.OK)
|
||||||
|
async def api_targets_delete(
|
||||||
|
source_wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
|
) -> None:
|
||||||
|
await set_targets(source_wallet.wallet.id, [])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue