Merge branch 'main' into gerty
This commit is contained in:
commit
04bc6c8975
15 changed files with 439 additions and 167 deletions
|
|
@ -15,16 +15,6 @@ from .. import core_app
|
||||||
from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings
|
from ..crud import delete_admin_settings, get_admin_settings, update_admin_settings
|
||||||
|
|
||||||
|
|
||||||
@core_app.get(
|
|
||||||
"/admin/api/v1/restart/",
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
dependencies=[Depends(check_super_user)],
|
|
||||||
)
|
|
||||||
async def api_restart_server() -> dict[str, str]:
|
|
||||||
server_restart.set()
|
|
||||||
return {"status": "Success"}
|
|
||||||
|
|
||||||
|
|
||||||
@core_app.get("/admin/api/v1/settings/")
|
@core_app.get("/admin/api/v1/settings/")
|
||||||
async def api_get_settings(
|
async def api_get_settings(
|
||||||
user: User = Depends(check_admin), # type: ignore
|
user: User = Depends(check_admin), # type: ignore
|
||||||
|
|
@ -33,26 +23,6 @@ async def api_get_settings(
|
||||||
return admin_settings
|
return admin_settings
|
||||||
|
|
||||||
|
|
||||||
@core_app.put(
|
|
||||||
"/admin/api/v1/topup/",
|
|
||||||
status_code=HTTPStatus.OK,
|
|
||||||
dependencies=[Depends(check_admin)],
|
|
||||||
)
|
|
||||||
async def api_topup_balance(
|
|
||||||
id: str = Body(...), amount: int = Body(...)
|
|
||||||
) -> dict[str, str]:
|
|
||||||
try:
|
|
||||||
await get_wallet(id)
|
|
||||||
except:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.FORBIDDEN, detail="wallet does not exist."
|
|
||||||
)
|
|
||||||
|
|
||||||
await update_wallet_balance(wallet_id=id, amount=int(amount))
|
|
||||||
|
|
||||||
return {"status": "Success"}
|
|
||||||
|
|
||||||
|
|
||||||
@core_app.put(
|
@core_app.put(
|
||||||
"/admin/api/v1/settings/",
|
"/admin/api/v1/settings/",
|
||||||
status_code=HTTPStatus.OK,
|
status_code=HTTPStatus.OK,
|
||||||
|
|
@ -67,8 +37,38 @@ async def api_update_settings(data: EditableSetings):
|
||||||
@core_app.delete(
|
@core_app.delete(
|
||||||
"/admin/api/v1/settings/",
|
"/admin/api/v1/settings/",
|
||||||
status_code=HTTPStatus.OK,
|
status_code=HTTPStatus.OK,
|
||||||
dependencies=[Depends(check_admin)],
|
dependencies=[Depends(check_super_user)],
|
||||||
)
|
)
|
||||||
async def api_delete_settings() -> dict[str, str]:
|
async def api_delete_settings() -> None:
|
||||||
await delete_admin_settings()
|
await delete_admin_settings()
|
||||||
|
server_restart.set()
|
||||||
|
|
||||||
|
|
||||||
|
@core_app.get(
|
||||||
|
"/admin/api/v1/restart/",
|
||||||
|
status_code=HTTPStatus.OK,
|
||||||
|
dependencies=[Depends(check_super_user)],
|
||||||
|
)
|
||||||
|
async def api_restart_server() -> dict[str, str]:
|
||||||
|
server_restart.set()
|
||||||
|
return {"status": "Success"}
|
||||||
|
|
||||||
|
|
||||||
|
@core_app.put(
|
||||||
|
"/admin/api/v1/topup/",
|
||||||
|
status_code=HTTPStatus.OK,
|
||||||
|
dependencies=[Depends(check_super_user)],
|
||||||
|
)
|
||||||
|
async def api_topup_balance(
|
||||||
|
id: str = Body(...), amount: int = Body(...)
|
||||||
|
) -> dict[str, str]:
|
||||||
|
try:
|
||||||
|
await get_wallet(id)
|
||||||
|
except:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN, detail="wallet does not exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
await update_wallet_balance(wallet_id=id, amount=int(amount))
|
||||||
|
|
||||||
return {"status": "Success"}
|
return {"status": "Success"}
|
||||||
|
|
|
||||||
|
|
@ -6,26 +6,24 @@ This extension allows you to link your Bolt Card (or other compatible NXP NTAG d
|
||||||
|
|
||||||
**Disclaimer:** ***Use this only if you either know what you are doing or are a reckless lightning pioneer. Only you are responsible for all your sats, cards and other devices. Always backup all your card keys!***
|
**Disclaimer:** ***Use this only if you either know what you are doing or are a reckless lightning pioneer. Only you are responsible for all your sats, cards and other devices. Always backup all your card keys!***
|
||||||
|
|
||||||
***In order to use this extension you need to be able to setup your own card.*** That means writing a URL template pointing to your LNbits instance, configuring some SUN (SDM) settings and optionally changing the card's keys. There's a [guide](https://www.whitewolftech.com/articles/payment-card/) to set it up with a card reader connected to your computer. It can be done (without setting the keys) with [TagWriter app by NXP](https://play.google.com/store/apps/details?id=com.nxp.nfc.tagwriter) Android app. Last but not least, an OSS android app by name [bolt-nfc-android-app](https://github.com/boltcard/bolt-nfc-android-app) is being developed for these purposes. It's available from Google Play [here](https://play.google.com/store/apps/details?id=com.lightningnfcapp).
|
|
||||||
|
***In order to use this extension you need to be able to setup your own card.*** That means writing a URL template pointing to your LNbits instance, configuring some SUN (SDM) settings and optionally changing the card's keys. There's a [guide](https://www.whitewolftech.com/articles/payment-card/) to set it up with a card reader connected to your computer. It can be done (without setting the keys) with [TagWriter app by NXP](https://play.google.com/store/apps/details?id=com.nxp.nfc.tagwriter) Android app. Last but not least, an OSS android app by name [Boltcard NFC Card Creator](https://github.com/boltcard/bolt-nfc-android-app) is being developed for these purposes. It's available from Google Play [here](https://play.google.com/store/apps/details?id=com.lightningnfcapp).
|
||||||
|
|
||||||
## About the keys
|
## About the keys
|
||||||
|
|
||||||
Up to five 16-byte keys can be stored on the card, numbered from 00 to 04. In the empty state they all should be set to zeros (00000000000000000000000000000000). For this extension only two keys need to be set:
|
Up to five 16-byte keys can be stored on the card, numbered from 00 to 04. In the empty state they all should be set to zeros (00000000000000000000000000000000). For this extension only two keys need to be set, but for the security reasons all five keys should be changed from default (empty) state. The keys directly needed by this extension are:
|
||||||
|
|
||||||
One for encrypting the card UID and the counter (p parameter), let's called it meta key, key #01 or K1.
|
- One for encrypting the card UID and the counter (p parameter), let's called it meta key, key #01 or K1.
|
||||||
|
|
||||||
One for calculating CMAC (c parameter), let's called it file key, key #02 or K2.
|
- One for calculating CMAC (c parameter), let's called it file key, key #02 or K2.
|
||||||
|
|
||||||
The key #00, K0 (also know as auth key) is skipped to be use as authentification key. Is not needed by this extension, but can be filled in order to write the keys in cooperation with bolt-nfc-android-app.
|
The key #00, K0 (also know as auth key) is used as authentification key. It is not directly needed by this extension, but should be filled in order to write the keys in cooperation with Boltcard NFC Card Creator. In this case also K3 is set to same value as K1 and K4 as K2, so all keys are changed from default values. Keep that in your mind in case you ever need to reset the keys manually.
|
||||||
|
|
||||||
***Always backup all keys that you're trying to write on the card. Without them you may not be able to change them in the future!***
|
***Always backup all keys that you're trying to write on the card. Without them you may not be able to change them in the future!***
|
||||||
|
|
||||||
## Setting the card - bolt-nfc-android-app (easy way)
|
|
||||||
So far, regarding the keys, the app can only write a new key set on an empty card (with zero keys). **When you write non zero (and 'non debug') keys, they can't be rewrite with this app.** You have to do it on your computer.
|
|
||||||
|
|
||||||
- Read the card with the app. Note UID so you can fill it in the extension later.
|
## Setting the card - Boltcard NFC Card Creator (easy way)
|
||||||
- Write the link on the card. It shoud be like `YOUR_LNBITS_DOMAIN/boltcards/api/v1/scan/{external_id}`
|
Updated for v0.1.3
|
||||||
- `{external_id}` should be replaced with the External ID found in the LNbits dialog.
|
|
||||||
|
|
||||||
- Add new card in the extension.
|
- Add new card in the extension.
|
||||||
- Set a max sats per transaction. Any transaction greater than this amount will be rejected.
|
- Set a max sats per transaction. Any transaction greater than this amount will be rejected.
|
||||||
|
|
@ -33,14 +31,29 @@ So far, regarding the keys, the app can only write a new key set on an empty car
|
||||||
- Set a card name. This is just for your reference inside LNbits.
|
- Set a card name. This is just for your reference inside LNbits.
|
||||||
- Set the card UID. This is the unique identifier on your NFC card and is 7 bytes.
|
- Set the card UID. This is the unique identifier on your NFC card and is 7 bytes.
|
||||||
- If on an Android device with a newish version of Chrome, you can click the icon next to the input and tap your card to autofill this field.
|
- If on an Android device with a newish version of Chrome, you can click the icon next to the input and tap your card to autofill this field.
|
||||||
|
- Otherwise read it with the Android app (Advanced -> Read NFC) and paste it to the field.
|
||||||
- Advanced Options
|
- Advanced Options
|
||||||
- Card Keys (k0, k1, k2) will be automatically generated if not explicitly set.
|
- Card Keys (k0, k1, k2) will be automatically generated if not explicitly set.
|
||||||
- Set to 16 bytes of 0s (00000000000000000000000000000000) to leave the keys in debug mode.
|
- Set to 16 bytes of 0s (00000000000000000000000000000000) to leave the keys in default (empty) state (this is unsecure).
|
||||||
- GENERATE KEY button fill the keys randomly. If there is "debug" in the card name, a debug set of keys is filled instead.
|
- GENERATE KEY button fill the keys randomly.
|
||||||
- Click CREATE CARD button
|
- Click CREATE CARD button
|
||||||
- Click the QR code button next to a card to view its details. You can scan the QR code with the Android app to import the keys.
|
- Click the QR code button next to a card to view its details. Backup the keys now! They'll be comfortable in your password manager.
|
||||||
- Click the "KEYS / AUTH LINK" button to copy the auth URL to the clipboard. You can then paste this into the Android app to import the keys.
|
- Now you can scan the QR code with the Android app (Create Bolt Card -> SCAN QR CODE).
|
||||||
- Tap the NFC card to write the keys to the card.
|
- Or you can Click the "KEYS / AUTH LINK" button to copy the auth URL to the clipboard. Then paste it into the Android app (Create Bolt Card -> PASTE AUTH URL).
|
||||||
|
- Click WRITE CARD NOW and approach the NFC card to set it up. DO NOT REMOVE THE CARD PREMATURELY!
|
||||||
|
|
||||||
|
## Erasing the card - Boltcard NFC Card Creator
|
||||||
|
Updated for v0.1.3
|
||||||
|
|
||||||
|
Since v0.1.2 of Boltcard NFC Card Creator it is possible not only reset the keys but also disable the SUN function and do the complete erase so the card can be use again as a static tag (or set as a new Bolt Card, ofc).
|
||||||
|
|
||||||
|
- Click the QR code button next to a card to view its details and select WIPE
|
||||||
|
- OR click the red cross icon on the right side to reach the same
|
||||||
|
- In the android app (Advanced -> Reset Keys)
|
||||||
|
- Click SCAN QR CODE to scan the QR
|
||||||
|
- Or click WIPE DATA in LNbits to copy and paste in to the app (PASTE KEY JSON)
|
||||||
|
- Click RESET CARD NOW and approach the NFC card to erase it. DO NOT REMOVE THE CARD PREMATURELY!
|
||||||
|
- Now if there is all success the card can be safely delete from LNbits (but keep the keys backuped anyway; batter safe than brick).
|
||||||
|
|
||||||
## Setting the card - computer (hard way)
|
## Setting the card - computer (hard way)
|
||||||
|
|
||||||
|
|
@ -48,7 +61,7 @@ Follow the guide.
|
||||||
|
|
||||||
The URI should be `lnurlw://YOUR-DOMAIN.COM/boltcards/api/v1/scan/{YOUR_card_external_id}?p=00000000000000000000000000000000&c=0000000000000000`
|
The URI should be `lnurlw://YOUR-DOMAIN.COM/boltcards/api/v1/scan/{YOUR_card_external_id}?p=00000000000000000000000000000000&c=0000000000000000`
|
||||||
|
|
||||||
Then fill up the card parameters in the extension. Card Auth key (K0) can be omitted. Initical counter can be 0.
|
Then fill up the card parameters in the extension. Card Auth key (K0) can be filled in the extension just for the record. Initical counter can be 0.
|
||||||
|
|
||||||
## Setting the card - android NXP app (hard way)
|
## Setting the card - android NXP app (hard way)
|
||||||
- If you don't know the card ID, use NXP TagInfo app to find it out.
|
- If you don't know the card ID, use NXP TagInfo app to find it out.
|
||||||
|
|
@ -70,4 +83,4 @@ Then fill up the card parameters in the extension. Card Auth key (K0) can be omi
|
||||||
- Save & Write
|
- Save & Write
|
||||||
- Scan with compatible Wallet
|
- Scan with compatible Wallet
|
||||||
|
|
||||||
This app afaik cannot change the keys. If you cannot change them any other way, leave them empty in the extension dialog and remember you're not secure. Card Auth key (K0) can be omitted anyway. Initical counter can be 0.
|
This app afaik cannot change the keys. If you cannot change them any other way, leave them empty in the extension dialog and remember you're not secured. Card Auth key (K0) can be omitted anyway. Initical counter can be 0.
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,9 @@ async def get_hit(hit_id: str) -> Optional[Hit]:
|
||||||
|
|
||||||
|
|
||||||
async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]:
|
async def get_hits(cards_ids: Union[str, List[str]]) -> List[Hit]:
|
||||||
|
if len(cards_ids) == 0:
|
||||||
|
return []
|
||||||
|
|
||||||
q = ",".join(["?"] * len(cards_ids))
|
q = ",".join(["?"] * len(cards_ids))
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
f"SELECT * FROM boltcards.hits WHERE card_id IN ({q})", (*cards_ids,)
|
f"SELECT * FROM boltcards.hits WHERE card_id IN ({q})", (*cards_ids,)
|
||||||
|
|
@ -265,6 +268,9 @@ async def get_refund(refund_id: str) -> Optional[Refund]:
|
||||||
|
|
||||||
|
|
||||||
async def get_refunds(hits_ids: Union[str, List[str]]) -> List[Refund]:
|
async def get_refunds(hits_ids: Union[str, List[str]]) -> List[Refund]:
|
||||||
|
if len(hits_ids) == 0:
|
||||||
|
return []
|
||||||
|
|
||||||
q = ",".join(["?"] * len(hits_ids))
|
q = ",".join(["?"] * len(hits_ids))
|
||||||
rows = await db.fetchall(
|
rows = await db.fetchall(
|
||||||
f"SELECT * FROM boltcards.refunds WHERE hit_id IN ({q})", (*hits_ids,)
|
f"SELECT * FROM boltcards.refunds WHERE hit_id IN ({q})", (*hits_ids,)
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,13 @@
|
||||||
import base64
|
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
import json
|
import json
|
||||||
import secrets
|
import secrets
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from io import BytesIO
|
|
||||||
from typing import Optional
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from embit import bech32, compact
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from fastapi.param_functions import Query
|
from fastapi.param_functions import Query
|
||||||
from fastapi.params import Depends, Query
|
from fastapi.params import Depends, Query
|
||||||
from lnurl import Lnurl, LnurlWithdrawResponse
|
|
||||||
from lnurl import encode as lnurl_encode # type: ignore
|
from lnurl import encode as lnurl_encode # type: ignore
|
||||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||||
from loguru import logger
|
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import HTMLResponse
|
||||||
|
|
@ -33,7 +25,6 @@ from .crud import (
|
||||||
get_hit,
|
get_hit,
|
||||||
get_hits_today,
|
get_hits_today,
|
||||||
spend_hit,
|
spend_hit,
|
||||||
update_card,
|
|
||||||
update_card_counter,
|
update_card_counter,
|
||||||
update_card_otp,
|
update_card_otp,
|
||||||
)
|
)
|
||||||
|
|
@ -108,15 +99,27 @@ async def lnurl_callback(
|
||||||
pr: str = Query(None),
|
pr: str = Query(None),
|
||||||
k1: str = Query(None),
|
k1: str = Query(None),
|
||||||
):
|
):
|
||||||
|
if not k1:
|
||||||
|
return {"status": "ERROR", "reason": "Missing K1 token"}
|
||||||
|
|
||||||
hit = await get_hit(k1)
|
hit = await get_hit(k1)
|
||||||
card = await get_card(hit.card_id)
|
|
||||||
if not hit:
|
if not hit:
|
||||||
return {"status": "ERROR", "reason": f"LNURL-pay record not found."}
|
return {
|
||||||
if hit.id != k1:
|
"status": "ERROR",
|
||||||
return {"status": "ERROR", "reason": "Bad K1"}
|
"reason": "Record not found for this charge (bad k1)",
|
||||||
|
}
|
||||||
if hit.spent:
|
if hit.spent:
|
||||||
return {"status": "ERROR", "reason": f"Payment already claimed"}
|
return {"status": "ERROR", "reason": "Payment already claimed"}
|
||||||
|
if not pr:
|
||||||
|
return {"status": "ERROR", "reason": "Missing payment request"}
|
||||||
|
|
||||||
|
try:
|
||||||
invoice = bolt11.decode(pr)
|
invoice = bolt11.decode(pr)
|
||||||
|
except:
|
||||||
|
return {"status": "ERROR", "reason": "Failed to decode payment request"}
|
||||||
|
|
||||||
|
card = await get_card(hit.card_id)
|
||||||
hit = await spend_hit(id=hit.id, amount=int(invoice.amount_msat / 1000))
|
hit = await spend_hit(id=hit.id, amount=int(invoice.amount_msat / 1000))
|
||||||
try:
|
try:
|
||||||
await pay_invoice(
|
await pay_invoice(
|
||||||
|
|
@ -126,8 +129,8 @@ async def lnurl_callback(
|
||||||
extra={"tag": "boltcard", "tag": hit.id},
|
extra={"tag": "boltcard", "tag": hit.id},
|
||||||
)
|
)
|
||||||
return {"status": "OK"}
|
return {"status": "OK"}
|
||||||
except:
|
except Exception as exc:
|
||||||
return {"status": "ERROR", "reason": f"Payment failed"}
|
return {"status": "ERROR", "reason": f"Payment failed - {exc}"}
|
||||||
|
|
||||||
|
|
||||||
# /boltcards/api/v1/auth?a=00000000000000000000000000000000
|
# /boltcards/api/v1/auth?a=00000000000000000000000000000000
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@ new Vue({
|
||||||
},
|
},
|
||||||
qrCodeDialog: {
|
qrCodeDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
|
wipe: false,
|
||||||
data: null
|
data: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -259,9 +260,10 @@ new Vue({
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
openQrCodeDialog(cardId) {
|
openQrCodeDialog(cardId, wipe) {
|
||||||
var card = _.findWhere(this.cards, {id: cardId})
|
var card = _.findWhere(this.cards, {id: cardId})
|
||||||
this.qrCodeDialog.data = {
|
this.qrCodeDialog.data = {
|
||||||
|
id: card.id,
|
||||||
link: window.location.origin + '/boltcards/api/v1/auth?a=' + card.otp,
|
link: window.location.origin + '/boltcards/api/v1/auth?a=' + card.otp,
|
||||||
name: card.card_name,
|
name: card.card_name,
|
||||||
uid: card.uid,
|
uid: card.uid,
|
||||||
|
|
@ -272,6 +274,17 @@ new Vue({
|
||||||
k3: card.k1,
|
k3: card.k1,
|
||||||
k4: card.k2
|
k4: card.k2
|
||||||
}
|
}
|
||||||
|
this.qrCodeDialog.data_wipe = JSON.stringify({
|
||||||
|
action: 'wipe',
|
||||||
|
k0: card.k0,
|
||||||
|
k1: card.k1,
|
||||||
|
k2: card.k2,
|
||||||
|
k3: card.k1,
|
||||||
|
k4: card.k2,
|
||||||
|
uid: card.uid,
|
||||||
|
version: 1
|
||||||
|
})
|
||||||
|
this.qrCodeDialog.wipe = wipe
|
||||||
this.qrCodeDialog.show = true
|
this.qrCodeDialog.show = true
|
||||||
},
|
},
|
||||||
addCardOpen: function () {
|
addCardOpen: function () {
|
||||||
|
|
@ -397,8 +410,16 @@ new Vue({
|
||||||
let self = this
|
let self = this
|
||||||
let cards = _.findWhere(this.cards, {id: cardId})
|
let cards = _.findWhere(this.cards, {id: cardId})
|
||||||
|
|
||||||
|
Quasar.utils.exportFile(
|
||||||
|
cards.card_name + '.json',
|
||||||
|
this.qrCodeDialog.data_wipe,
|
||||||
|
'application/json'
|
||||||
|
)
|
||||||
|
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog('Are you sure you want to delete this card')
|
.confirmDialog(
|
||||||
|
"Are you sure you want to delete this card? Without access to the card keys you won't be able to reset them in the future!"
|
||||||
|
)
|
||||||
.onOk(function () {
|
.onOk(function () {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@
|
||||||
</q-th>
|
</q-th>
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
|
<q-th auto-width></q-th>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
|
|
@ -58,7 +59,7 @@
|
||||||
dense
|
dense
|
||||||
icon="qr_code"
|
icon="qr_code"
|
||||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||||
@click="openQrCodeDialog(props.row.id)"
|
@click="openQrCodeDialog(props.row.id, false)"
|
||||||
>
|
>
|
||||||
<q-tooltip>Card key credentials</q-tooltip>
|
<q-tooltip>Card key credentials</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|
@ -99,7 +100,7 @@
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
size="xs"
|
size="xs"
|
||||||
@click="deleteCard(props.row.id)"
|
@click="openQrCodeDialog(props.row.id, true)"
|
||||||
icon="cancel"
|
icon="cancel"
|
||||||
color="pink"
|
color="pink"
|
||||||
>
|
>
|
||||||
|
|
@ -215,6 +216,7 @@
|
||||||
emit-value
|
emit-value
|
||||||
v-model="cardDialog.data.wallet"
|
v-model="cardDialog.data.wallet"
|
||||||
:options="g.user.walletOptions"
|
:options="g.user.walletOptions"
|
||||||
|
:disable="cardDialog.data.id != null"
|
||||||
label="Wallet *"
|
label="Wallet *"
|
||||||
>
|
>
|
||||||
</q-select>
|
</q-select>
|
||||||
|
|
@ -283,7 +285,7 @@
|
||||||
v-model="toggleAdvanced"
|
v-model="toggleAdvanced"
|
||||||
label="Show advanced options"
|
label="Show advanced options"
|
||||||
></q-toggle>
|
></q-toggle>
|
||||||
<div v-show="toggleAdvanced">
|
<div v-show="toggleAdvanced" class="q-gutter-y-md">
|
||||||
<q-input
|
<q-input
|
||||||
filled
|
filled
|
||||||
dense
|
dense
|
||||||
|
|
@ -358,44 +360,105 @@
|
||||||
<q-dialog v-model="qrCodeDialog.show" position="top">
|
<q-dialog v-model="qrCodeDialog.show" position="top">
|
||||||
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
<q-card v-if="qrCodeDialog.data" class="q-pa-lg lnbits__dialog-card">
|
||||||
{% raw %}
|
{% raw %}
|
||||||
<q-responsive :ratio="1" class="q-mx-xl q-mb-md">
|
<div class="col q-mt-lg text-center">
|
||||||
|
<q-responsive
|
||||||
|
:ratio="1"
|
||||||
|
class="q-mx-xl q-mb-md"
|
||||||
|
v-show="!qrCodeDialog.wipe"
|
||||||
|
>
|
||||||
<qrcode
|
<qrcode
|
||||||
:value="qrCodeDialog.data.link"
|
:value="qrCodeDialog.data.link"
|
||||||
:options="{width: 800}"
|
:options="{width: 800}"
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
></qrcode>
|
></qrcode>
|
||||||
</q-responsive>
|
</q-responsive>
|
||||||
<p style="word-break: break-all" class="text-center">
|
<p class="text-center" v-show="!qrCodeDialog.wipe">
|
||||||
(Keys for
|
(QR for <strong>create</strong> the card in
|
||||||
<a
|
<a
|
||||||
href="https://play.google.com/store/apps/details?id=com.lightningnfcapp"
|
href="https://play.google.com/store/apps/details?id=com.lightningnfcapp"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>bolt-nfc-android-app</a
|
style="color: inherit"
|
||||||
|
>Boltcard NFC Card Creator</a
|
||||||
>)
|
>)
|
||||||
</p>
|
</p>
|
||||||
|
<q-responsive
|
||||||
|
:ratio="1"
|
||||||
|
class="q-mx-xl q-mb-md"
|
||||||
|
v-show="qrCodeDialog.wipe"
|
||||||
|
>
|
||||||
|
<qrcode
|
||||||
|
:value="qrCodeDialog.data_wipe"
|
||||||
|
:options="{width: 800}"
|
||||||
|
class="rounded-borders"
|
||||||
|
></qrcode>
|
||||||
|
</q-responsive>
|
||||||
|
<p class="text-center" v-show="qrCodeDialog.wipe">
|
||||||
|
(QR for <strong>wipe</strong> the card in
|
||||||
|
<a
|
||||||
|
href="https://play.google.com/store/apps/details?id=com.lightningnfcapp"
|
||||||
|
target="_blank"
|
||||||
|
style="color: inherit"
|
||||||
|
>Boltcard NFC Card Creator</a
|
||||||
|
>)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col q-mt-md q-mb-md text-center">
|
||||||
|
<q-btn-toggle
|
||||||
|
v-model="qrCodeDialog.wipe"
|
||||||
|
rounded
|
||||||
|
unelevated
|
||||||
|
toggle-color="primary"
|
||||||
|
color="white"
|
||||||
|
text-color="primary"
|
||||||
|
:options="[
|
||||||
|
{label: 'Create', value: false},
|
||||||
|
{label: 'Wipe', value: true}
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<p style="word-break: break-all">
|
<p style="word-break: break-all">
|
||||||
<strong>Name:</strong> {{ qrCodeDialog.data.name }}<br />
|
<strong>Name:</strong> {{ qrCodeDialog.data.name }}<br />
|
||||||
<strong>UID:</strong> {{ qrCodeDialog.data.uid }}<br />
|
<strong>UID:</strong> {{ qrCodeDialog.data.uid }}<br />
|
||||||
<strong>External ID:</strong> {{ qrCodeDialog.data.external_id }}<br />
|
<strong>External ID:</strong> {{ qrCodeDialog.data.external_id }}<br />
|
||||||
<strong>Lock key:</strong> {{ qrCodeDialog.data.k0 }}<br />
|
<strong>Lock key (K0):</strong> {{ qrCodeDialog.data.k0 }}<br />
|
||||||
<strong>Meta key:</strong> {{ qrCodeDialog.data.k1 }}<br />
|
<strong>Meta key (K1 & K3):</strong> {{ qrCodeDialog.data.k1 }}<br />
|
||||||
<strong>File key:</strong> {{ qrCodeDialog.data.k2 }}<br />
|
<strong>File key (K2 & K4):</strong> {{ qrCodeDialog.data.k2 }}<br />
|
||||||
<br />
|
</p>
|
||||||
Always backup all keys that you're trying to write on the card. Without
|
<p>
|
||||||
them you may not be able to change them in the future!<br />
|
Always backup all keys that you're trying to write on the card. Without
|
||||||
|
them you may not be able to change them in the future!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<br />
|
|
||||||
<q-btn
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
outline
|
outline
|
||||||
color="grey"
|
color="grey"
|
||||||
@click="copyText(qrCodeDialog.data.link)"
|
@click="copyText(qrCodeDialog.data.link)"
|
||||||
label="Keys/Auth link"
|
label="Create link"
|
||||||
|
v-show="!qrCodeDialog.wipe"
|
||||||
>
|
>
|
||||||
|
<q-tooltip>Click to copy, then paste to NFC Card Creator</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
outline
|
||||||
|
color="grey"
|
||||||
|
@click="copyText(qrCodeDialog.data_wipe)"
|
||||||
|
label="Wipe data"
|
||||||
|
v-show="qrCodeDialog.wipe"
|
||||||
|
>
|
||||||
|
<q-tooltip>Click to copy, then paste to NFC Card Creator</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
outline
|
||||||
|
color="red"
|
||||||
|
@click="deleteCard(qrCodeDialog.data.id)"
|
||||||
|
label="Delete card"
|
||||||
|
v-show="qrCodeDialog.wipe"
|
||||||
|
v-close-popup
|
||||||
|
>
|
||||||
|
<q-tooltip>Backup the keys, or wipe the card first!</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-tooltip>Click to copy, then add to NFC card</q-tooltip>
|
|
||||||
|
|
||||||
{% endraw %}
|
{% endraw %}
|
||||||
<div class="row q-mt-lg q-gutter-sm">
|
<div class="row q-mt-lg q-gutter-sm">
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||||
from . import boltcards_ext
|
from . import boltcards_ext
|
||||||
from .crud import (
|
from .crud import (
|
||||||
create_card,
|
create_card,
|
||||||
create_hit,
|
|
||||||
delete_card,
|
delete_card,
|
||||||
enable_disable_card,
|
enable_disable_card,
|
||||||
get_card,
|
get_card,
|
||||||
|
|
@ -22,11 +21,9 @@ from .crud import (
|
||||||
get_hits,
|
get_hits,
|
||||||
get_refunds,
|
get_refunds,
|
||||||
update_card,
|
update_card,
|
||||||
update_card_counter,
|
|
||||||
update_card_otp,
|
update_card_otp,
|
||||||
)
|
)
|
||||||
from .models import CreateCardData
|
from .models import CreateCardData
|
||||||
from .nxp424 import decryptSUN, getSunMAC
|
|
||||||
|
|
||||||
|
|
||||||
@boltcards_ext.get("/api/v1/cards")
|
@boltcards_ext.get("/api/v1/cards")
|
||||||
|
|
|
||||||
|
|
@ -18,19 +18,16 @@
|
||||||
label="Choose an amount *"
|
label="Choose an amount *"
|
||||||
:hint="'Minimum ' + paywallAmount + ' sat'"
|
:hint="'Minimum ' + paywallAmount + ' sat'"
|
||||||
>
|
>
|
||||||
<template v-slot:after>
|
|
||||||
<q-btn
|
|
||||||
round
|
|
||||||
dense
|
|
||||||
flat
|
|
||||||
icon="check"
|
|
||||||
color="primary"
|
|
||||||
type="submit"
|
|
||||||
@click="createInvoice"
|
|
||||||
:disabled="userAmount < paywallAmount || paymentReq"
|
|
||||||
></q-btn>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
</q-input>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disabled="userAmount < paywallAmount || paymentReq"
|
||||||
|
@click="createInvoice"
|
||||||
|
>Send</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
<div v-if="paymentReq" class="q-mt-lg">
|
<div v-if="paymentReq" class="q-mt-lg">
|
||||||
<a :href="'lightning:' + paymentReq">
|
<a :href="'lightning:' + paymentReq">
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 ""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue