Merge pull request #1498 from lnbits/remove-splitpayments

Remove splitpayments
This commit is contained in:
Arc 2023-02-15 08:57:55 +00:00 committed by GitHub
commit 5bc2646103
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 0 additions and 833 deletions

View File

@ -1,34 +0,0 @@
# Split Payments
## Have payments split between multiple wallets
LNbits Split Payments extension allows for distributing payments across multiple wallets. Set it and forget it. It will keep splitting your payments across wallets forever.
## Usage
1. After enabling the extension, choose the source wallet that will receive and distribute the Payments
![choose wallet](https://i.imgur.com/nPQudqL.png)
2. Add the wallet or wallets info to split payments to
![split wallets](https://i.imgur.com/5hCNWpg.png) - 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\
![wallet info](https://i.imgur.com/betqflC.png) - 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
3. When done, 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%
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\
![get 20 sats payment](https://i.imgur.com/BKp0xvy.png)
- source wallet gets 18 sats\
![source wallet](https://i.imgur.com/GCxDZ5s.png)
- Ben's wallet (the wallet from the example) instantly, and feeless, gets the corresponding 10%, or 2 sats\
![ben wallet](https://i.imgur.com/MfsccNa.png)
## Sponsored by
[![](https://cdn.shopify.com/s/files/1/0826/9235/files/cryptograffiti_logo_clear_background.png?v=1504730421)](https://cryptograffiti.com/)

View File

@ -1,35 +0,0 @@
import asyncio
from fastapi import APIRouter
from fastapi.staticfiles import StaticFiles
from lnbits.db import Database
from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart
db = Database("ext_splitpayments")
splitpayments_static_files = [
{
"path": "/splitpayments/static",
"app": StaticFiles(packages=[("lnbits", "extensions/splitpayments/static")]),
"name": "splitpayments_static",
}
]
splitpayments_ext: APIRouter = APIRouter(
prefix="/splitpayments", tags=["splitpayments"]
)
def splitpayments_renderer():
return template_renderer(["lnbits/extensions/splitpayments/templates"])
from .tasks import wait_for_paid_invoices
from .views import * # noqa: F401,F403
from .views_api import * # noqa: F401,F403
def splitpayments_start():
loop = asyncio.get_event_loop()
loop.create_task(catch_everything_and_restart(wait_for_paid_invoices))

View File

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

View File

@ -1,36 +0,0 @@
from typing import List
from lnbits.helpers import urlsafe_short_hash
from . import db
from .models import Target
async def get_targets(source_wallet: str) -> List[Target]:
rows = await db.fetchall(
"SELECT * FROM splitpayments.targets WHERE source = ?", (source_wallet,)
)
return [Target(**row) for row in rows]
async def set_targets(source_wallet: str, targets: List[Target]):
async with db.connect() as conn:
await conn.execute(
"DELETE FROM splitpayments.targets WHERE source = ?", (source_wallet,)
)
for target in targets:
await conn.execute(
"""
INSERT INTO splitpayments.targets
(id, source, wallet, percent, tag, alias)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
urlsafe_short_hash(),
source_wallet,
target.wallet,
target.percent,
target.tag,
target.alias,
),
)

View File

@ -1,99 +0,0 @@
from lnbits.helpers import urlsafe_short_hash
async def m001_initial(db):
"""
Initial split payment table.
"""
await db.execute(
"""
CREATE TABLE splitpayments.targets (
wallet TEXT NOT NULL,
source TEXT NOT NULL,
percent INTEGER NOT NULL CHECK (percent >= 0 AND percent <= 100),
alias TEXT,
UNIQUE (source, wallet)
);
"""
)
async def m002_float_percent(db):
"""
Add float percent and migrates the existing data.
"""
await db.execute("ALTER TABLE splitpayments.targets RENAME TO splitpayments_old")
await db.execute(
"""
CREATE TABLE splitpayments.targets (
wallet TEXT NOT NULL,
source TEXT NOT NULL,
percent REAL NOT NULL CHECK (percent >= 0 AND percent <= 100),
alias TEXT,
UNIQUE (source, wallet)
);
"""
)
for row in [
list(row)
for row in await db.fetchall("SELECT * FROM splitpayments.splitpayments_old")
]:
await db.execute(
"""
INSERT INTO splitpayments.targets (
wallet,
source,
percent,
alias
)
VALUES (?, ?, ?, ?)
""",
(row[0], row[1], row[2], row[3]),
)
await db.execute("DROP TABLE splitpayments.splitpayments_old")
async def m003_add_id_and_tag(db):
"""
Add float percent and migrates the existing data.
"""
await db.execute("ALTER TABLE splitpayments.targets RENAME TO splitpayments_old")
await db.execute(
"""
CREATE TABLE splitpayments.targets (
id TEXT PRIMARY KEY,
wallet TEXT NOT NULL,
source TEXT NOT NULL,
percent REAL NOT NULL CHECK (percent >= 0 AND percent <= 100),
tag TEXT NOT NULL,
alias TEXT,
UNIQUE (source, wallet)
);
"""
)
for row in [
list(row)
for row in await db.fetchall("SELECT * FROM splitpayments.splitpayments_old")
]:
await db.execute(
"""
INSERT INTO splitpayments.targets (
id,
wallet,
source,
percent,
tag,
alias
)
VALUES (?, ?, ?, ?, ?, ?)
""",
(urlsafe_short_hash(), row[0], row[1], row[2], "", row[3]),
)
await db.execute("DROP TABLE splitpayments.splitpayments_old")

View File

@ -1,28 +0,0 @@
from sqlite3 import Row
from typing import List, Optional
from fastapi import Query
from pydantic import BaseModel
class Target(BaseModel):
wallet: str
source: str
percent: float
tag: str
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):
__root__: List[TargetPutList]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -1,195 +0,0 @@
/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */
Vue.component(VueQrcode.name, VueQrcode)
function hashTargets(targets) {
return targets
.filter(isTargetComplete)
.map(({wallet, percent, alias}) => `${wallet}${percent}${alias}`)
.join('')
}
function isTargetComplete(target) {
return (
target.wallet &&
target.wallet.trim() !== '' &&
(target.percent > 0 || target.tag != '')
)
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data() {
return {
selectedWallet: null,
currentHash: '', // a string that must match if the edit data is unchanged
targets: [
{
method: 'split'
}
]
}
},
computed: {
isDirty() {
return hashTargets(this.targets) !== this.currentHash
}
},
methods: {
clearTargets() {
this.targets = [{}]
this.$q.notify({
message:
'Cleared the form, but not saved. You must click to save manually.',
timeout: 500
})
},
clearTarget(index) {
this.targets.splice(index, 1)
console.log(this.targets)
this.$q.notify({
message: 'Removed item. You must click to save manually.',
timeout: 500
})
},
getTargets() {
LNbits.api
.request(
'GET',
'/splitpayments/api/v1/targets',
this.selectedWallet.adminkey
)
.catch(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) {
this.selectedWallet = wallet
this.getTargets()
},
clearChanged(index) {
if (this.targets[index].method == 'split') {
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() {
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
.request(
'PUT',
'/splitpayments/api/v1/targets',
this.selectedWallet.adminkey,
{
targets: this.targets
.filter(isTargetComplete)
.map(({wallet, percent, tag, alias}) => ({
wallet,
percent,
tag,
alias
}))
}
)
.then(response => {
this.$q.notify({
message: 'Split payments targets set.',
timeout: 700
})
this.getTargets()
})
.catch(err => {
LNbits.utils.notifyApiError(err)
})
}
},
created() {
this.selectedWallet = this.g.user.wallets[0]
this.getTargets()
}
})

View File

@ -1,76 +0,0 @@
import asyncio
from loguru import logger
from lnbits.core.models import Payment
from lnbits.core.services import create_invoice, pay_invoice
from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener
from .crud import get_targets
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") == "splitpayments" or payment.extra.get("splitted"):
# already a splitted payment, ignore
return
targets = await get_targets(payment.wallet_id)
if not targets:
return
# validate target percentages
total_percent = sum([target.percent for target in targets])
if total_percent > 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
for target in targets:
tagged = target.tag in payment.extra
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(
wallet_id=target.wallet,
amount=int(amount_msat / 1000),
internal=True,
memo=memo,
)
extra = {**payment.extra, "tag": "splitpayments", "splitted": True}
await pay_invoice(
payment_request=payment_request,
wallet_id=payment.wallet_id,
extra=extra,
)

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,147 +0,0 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card class="q-pa-sm col-5">
<q-card-section class="q-pa-none text-center">
<q-form class="q-gutter-md">
<q-select
filled
dense
:options="g.user.wallets"
:value="selectedWallet"
label="Source Wallet:"
option-label="name"
@input="changedWallet"
>
</q-select>
</q-form>
</q-card-section>
</q-card>
<q-card class="q-pa-sm col-5">
<q-card-section class="q-pa-none text-center">
<div class="col">
<h5 class="text-subtitle1 q-my-none">Target Wallets</h5>
</div>
<q-form class="q-gutter-md" @submit="saveTargets">
<div
class="q-gutter-md row items-start"
style="flex-wrap: nowrap"
v-for="(target, t) in targets"
>
<q-input
dense
outlined
v-model="target.alias"
label="Alias"
:hint="t === targets.length - 1 ? 'A name to identify this target wallet locally.' : undefined"
style="width: 150px"
></q-input>
<q-input
dense
v-model="target.wallet"
label="Wallet"
:hint="t === targets.length - 1 ? 'A wallet ID or invoice key.' : undefined"
option-label="name"
style="width: 300px"
new-value-mode="add-unique"
use-input
input-debounce="0"
emit-value
></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
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"
dense
outlined
v-model.number="target.percent"
label="split"
suffix="%"
></q-input>
<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)"
round
color="red"
size="5px"
icon="close"
></q-btn>
</div>
<div class="row justify-evenly q-pa-lg">
<div>
<q-btn unelevated outline color="secondary" @click="clearTargets">
Clear
</q-btn>
</div>
<div>
<q-btn
unelevated
color="primary"
type="submit"
:disabled="targets.length < 2"
>
Save Targets
</q-btn>
</div>
</div>
</q-form>
</q-card-section>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} SplitPayments extension
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list> {% include "splitpayments/_api_docs.html" %} </q-list>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script src="/splitpayments/static/js/index.js"></script>
{% endblock %}

View File

@ -1,17 +0,0 @@
from fastapi import Depends, Request
from fastapi.templating import Jinja2Templates
from starlette.responses import HTMLResponse
from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from . import splitpayments_ext, splitpayments_renderer
templates = Jinja2Templates(directory="templates")
@splitpayments_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return splitpayments_renderer().TemplateResponse(
"splitpayments/index.html", {"request": request, "user": user.dict()}
)

View File

@ -1,63 +0,0 @@
from http import HTTPStatus
from fastapi import Depends, Request
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_wallet, get_wallet_for_key
from lnbits.decorators import WalletTypeInfo, require_admin_key
from . import splitpayments_ext
from .crud import get_targets, set_targets
from .models import Target, TargetPut
@splitpayments_ext.get("/api/v1/targets")
async def api_targets_get(wallet: WalletTypeInfo = Depends(require_admin_key)):
targets = await get_targets(wallet.wallet.id)
return [target.dict() for target in targets] or []
@splitpayments_ext.put("/api/v1/targets")
async def api_targets_set(
req: Request, wal: WalletTypeInfo = Depends(require_admin_key)
):
body = await req.json()
targets = []
data = TargetPut.parse_obj(body["targets"])
for entry in data.__root__:
wallet = await get_wallet(entry.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 == wal.wallet.id:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Can't split to itself."
)
if entry.percent < 0:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Invalid percent '{entry.percent}'.",
)
targets.append(
Target(
wallet=wallet.id,
source=wal.wallet.id,
tag=entry.tag,
percent=entry.percent,
alias=entry.alias,
)
)
percent_sum = sum([target.percent for target in targets])
if percent_sum > 100:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Splitting over 100%."
)
await set_targets(wal.wallet.id, targets)
return ""