Merge pull request #1195 from lnbits/switchpasstag

Adds tags to splitpayments
This commit is contained in:
Arc 2022-12-16 23:10:27 +00:00 committed by GitHub
commit 6140358fa2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 227 additions and 55 deletions

View file

@ -1,5 +1,7 @@
from typing import List from typing import List
from lnbits.helpers import urlsafe_short_hash
from . import db from . import db
from .models import Target from .models import Target
@ -20,8 +22,15 @@ async def set_targets(source_wallet: str, targets: List[Target]):
await conn.execute( await conn.execute(
""" """
INSERT INTO splitpayments.targets INSERT INTO splitpayments.targets
(source, wallet, percent, alias) (id, source, wallet, percent, tag, alias)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
""", """,
(source_wallet, target.wallet, target.percent, target.alias), (
urlsafe_short_hash(),
source_wallet,
target.wallet,
target.percent,
target.tag,
target.alias,
),
) )

View file

@ -1,3 +1,6 @@
from lnbits.helpers import urlsafe_short_hash
async def m001_initial(db): async def m001_initial(db):
""" """
Initial split payment table. Initial split payment table.
@ -52,3 +55,45 @@ async def m002_float_percent(db):
) )
await db.execute("DROP TABLE splitpayments.splitpayments_old") 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

@ -8,13 +8,15 @@ class Target(BaseModel):
wallet: str wallet: str
source: str source: str
percent: float percent: float
tag: str
alias: Optional[str] alias: Optional[str]
class TargetPutList(BaseModel): class TargetPutList(BaseModel):
wallet: str = Query(...) wallet: str = Query(...)
alias: str = Query("") alias: str = Query("")
percent: float = Query(..., ge=0.01, lt=100) percent: float = Query(..., ge=0, lt=100)
tag: str
class TargetPut(BaseModel): class TargetPut(BaseModel):

View file

@ -10,7 +10,11 @@ function hashTargets(targets) {
} }
function isTargetComplete(target) { function isTargetComplete(target) {
return target.wallet && target.wallet.trim() !== '' && target.percent > 0 return (
target.wallet &&
target.wallet.trim() !== '' &&
(target.percent > 0 || target.tag != '')
)
} }
new Vue({ new Vue({
@ -20,7 +24,11 @@ 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,6 +45,14 @@ new Vue({
timeout: 500 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() { getTargets() {
LNbits.api LNbits.api
.request( .request(
@ -50,17 +66,41 @@ new Vue({
.then(response => { .then(response => {
this.currentHash = hashTargets(response.data) this.currentHash = hashTargets(response.data)
this.targets = response.data.concat({}) 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()
}, },
targetChanged(isPercent, index) { 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 // fix percent min and max range
if (isPercent) { if (this.targets[index].percent) {
if (this.targets[index].percent > 100) this.targets[index].percent = 100 if (this.targets[index].percent > 100) this.targets[index].percent = 100
if (this.targets[index].percent < 0) this.targets[index].percent = 0 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) // remove empty lines (except last)
@ -70,6 +110,7 @@ new Vue({
if ( if (
(!target.wallet || target.wallet.trim() === '') && (!target.wallet || target.wallet.trim() === '') &&
(!target.alias || target.alias.trim() === '') && (!target.alias || target.alias.trim() === '') &&
(!target.tag || target.tag.trim() === '') &&
!target.percent !target.percent
) { ) {
this.targets.splice(i, 1) this.targets.splice(i, 1)
@ -79,7 +120,7 @@ new Vue({
// add a line at the end if the last one is filled // add a line at the end if the last one is filled
let last = this.targets[this.targets.length - 1] let last = this.targets[this.targets.length - 1]
if (last.wallet && last.wallet.trim() !== '' && last.percent > 0) { if (last.wallet && last.wallet.trim() !== '') {
this.targets.push({}) this.targets.push({})
} }
@ -108,11 +149,17 @@ new Vue({
if (t !== index) target.percent -= +(diff * target.percent).toFixed(2) if (t !== index) target.percent -= +(diff * target.percent).toFixed(2)
}) })
} }
// overwrite so changes appear // overwrite so changes appear
this.targets = this.targets 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',
@ -121,7 +168,12 @@ new Vue({
{ {
targets: this.targets targets: this.targets
.filter(isTargetComplete) .filter(isTargetComplete)
.map(({wallet, percent, alias}) => ({wallet, percent, alias})) .map(({wallet, percent, tag, alias}) => ({
wallet,
percent,
tag,
alias
}))
} }
) )
.then(response => { .then(response => {

View file

@ -25,7 +25,7 @@ async def on_invoice_paid(payment: Payment) -> None:
return return
targets = await get_targets(payment.wallet_id) targets = await get_targets(payment.wallet_id)
logger.debug(targets)
if not targets: if not targets:
return return
@ -35,8 +35,32 @@ async def on_invoice_paid(payment: Payment) -> None:
logger.error("splitpayment failure: total percent adds up to more than 100%") logger.error("splitpayment failure: total percent adds up to more than 100%")
return return
logger.debug(f"performing split payments to {len(targets)} targets") logger.debug(f"checking if tagged for {len(targets)} targets")
tagged = False
for target in targets: for target in targets:
if target.tag in payment.extra:
tagged = True
payment_hash, payment_request = await create_invoice(
wallet_id=target.wallet,
amount=int(payment.amount / 1000), # sats
internal=True,
memo=f"Pushed tagged payment to {target.alias}",
extra={"tag": "splitpayments"},
)
logger.debug(f"created split invoice: {payment_hash}")
checking_id = await pay_invoice(
payment_request=payment_request,
wallet_id=payment.wallet_id,
extra={"tag": "splitpayments"},
)
logger.debug(f"paid split invoice: {checking_id}")
logger.debug(f"performing split to {len(targets)} targets")
if tagged == False:
for target in targets:
if target.percent > 0:
amount = int(payment.amount * target.percent / 100) # msats amount = int(payment.amount * target.percent / 100) # msats
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=target.wallet, wallet_id=target.wallet,

View file

@ -31,39 +31,80 @@
style="flex-wrap: nowrap" style="flex-wrap: nowrap"
v-for="(target, t) in targets" v-for="(target, t) in targets"
> >
<q-select
dense
:options="g.user.wallets.filter(w => w.id !== selectedWallet.id).map(o => ({name: o.name, value: o.id}))"
v-model="target.wallet"
label="Wallet"
:hint="t === targets.length - 1 ? 'A wallet ID or invoice key.' : undefined"
@input="targetChanged(false)"
option-label="name"
style="width: 1000px"
new-value-mode="add-unique"
use-input
input-debounce="0"
emit-value
></q-select>
<q-input <q-input
dense dense
outlined outlined
v-model="target.alias" v-model="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"
@input="targetChanged(false)" style="width: 150px"
></q-input> ></q-input>
<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 dense
outlined outlined
v-model.number="target.percent" v-model.number="target.percent"
label="Split Share" label="split"
:hint="t === targets.length - 1 ? 'How much of the incoming payments will go to the target wallet.' : undefined"
suffix="%" suffix="%"
@input="targetChanged(true, t)"
></q-input> ></q-input>
</div>
<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 class="row justify-evenly q-pa-lg">
<div> <div>
<q-btn unelevated outline color="secondary" @click="clearTargets"> <q-btn unelevated outline color="secondary" @click="clearTargets">
@ -76,7 +117,7 @@
unelevated unelevated
color="primary" color="primary"
type="submit" type="submit"
:disabled="!isDirty" :disabled="targets.length < 2"
> >
Save Targets Save Targets
</q-btn> </q-btn>

View file

@ -50,16 +50,15 @@ async def api_targets_set(
Target( Target(
wallet=wallet.id, wallet=wallet.id,
source=wal.wallet.id, source=wal.wallet.id,
tag=entry.tag,
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) await set_targets(wal.wallet.id, targets)
return "" return ""