mirror of
https://github.com/lnbits/lnbits.git
synced 2025-06-01 18:49:45 +02:00
Merge branch 'master' into Jukebox
This commit is contained in:
commit
ff80f3d585
@ -8,7 +8,7 @@ PORT=5000
|
||||
LNBITS_SITE_TITLE=LNbits
|
||||
LNBITS_ALLOWED_USERS=""
|
||||
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
|
||||
LNBITS_DATA_FOLDER="."
|
||||
LNBITS_DATA_FOLDER="./data"
|
||||
LNBITS_DISABLED_EXTENSIONS="amilk"
|
||||
LNBITS_FORCE_HTTPS=true
|
||||
LNBITS_SERVICE_FEE="0.0"
|
||||
|
@ -5,15 +5,11 @@ title: Installation
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
|
||||
Installation
|
||||
============
|
||||
# Installation
|
||||
|
||||
Download the latest stable release https://github.com/lnbits/lnbits/releases
|
||||
|
||||
|
||||
Application dependencies
|
||||
------------------------
|
||||
## Application dependencies
|
||||
|
||||
The application uses [Pipenv][pipenv] to manage Python packages.
|
||||
While in development, you will need to install all dependencies:
|
||||
@ -24,7 +20,7 @@ $ pipenv install --dev
|
||||
```
|
||||
|
||||
If any of the modules fails to install, try checking and upgrading your setupTool module.
|
||||
`pip install -U setuptools`
|
||||
`pip install -U setuptools`
|
||||
|
||||
If you wish to use a version of Python higher than 3.7:
|
||||
|
||||
@ -41,9 +37,7 @@ E.g. when you want to use LND you have to `pipenv run pip install lndgrpc` and `
|
||||
|
||||
Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Network dev environment.
|
||||
|
||||
|
||||
Running the server
|
||||
------------------
|
||||
## Running the server
|
||||
|
||||
LNbits uses [Quart][quart] as an application server.
|
||||
|
||||
@ -51,12 +45,18 @@ LNbits uses [Quart][quart] as an application server.
|
||||
$ pipenv run python -m lnbits
|
||||
```
|
||||
|
||||
Frontend
|
||||
--------
|
||||
**Note**: You'll need to use _https_ for some endpoints and/or extensions. You can use [ngrok](https://ngrok.com/) for that. Follow the installation instructions on the website and when it's all set you can run:
|
||||
|
||||
```sh
|
||||
$ ./nrok http 5000
|
||||
```
|
||||
|
||||
this will give you an _https_ tunnel for the _localhost_, use that URL for navigating to LNBits.
|
||||
|
||||
## Frontend
|
||||
|
||||
The frontend uses [Vue.js and Quasar][quasar].
|
||||
|
||||
|
||||
[quart]: https://pgjones.gitlab.io/
|
||||
[pipenv]: https://pipenv.pypa.io/
|
||||
[polar]: https://lightningpolar.com/
|
||||
|
@ -17,6 +17,7 @@ cd lnbits/
|
||||
python3 -m venv venv
|
||||
./venv/bin/pip install -r requirements.txt
|
||||
cp .env.example .env
|
||||
mkdir data
|
||||
./venv/bin/quart assets
|
||||
./venv/bin/quart migrate
|
||||
./venv/bin/hypercorn -k trio --bind 0.0.0.0:5000 'lnbits.app:create_app()'
|
||||
@ -29,3 +30,31 @@ Now modify the `.env` file with any settings you prefer and add a proper [fundin
|
||||
Then you can run restart it and it will be using the new settings.
|
||||
|
||||
You might also need to install additional packages or perform additional setup steps, depending on the chosen backend. See [the short guide](./wallets.md) on each different funding source.
|
||||
|
||||
Docker installation
|
||||
===================
|
||||
|
||||
To install using docker you first need to build the docker image as:
|
||||
```
|
||||
git clone https://github.com/lnbits/lnbits.git
|
||||
cd lnbits/ # ${PWD} refered as <lnbits_repo>
|
||||
docker build -t lnbits .
|
||||
```
|
||||
|
||||
You can launch the docker in a different directory, but make sure to copy `.env.example` from lnbits there
|
||||
```
|
||||
cp <lnbits_repo>/.env.example .env
|
||||
```
|
||||
and change the configuration in `.env` as required.
|
||||
|
||||
Then create the data directory for the user ID 1000, which is the user that runs the lnbits within the docker container.
|
||||
```
|
||||
mkdir data
|
||||
sudo chown 1000:1000 ./data/
|
||||
```
|
||||
|
||||
Then the image can be run as:
|
||||
```
|
||||
docker run --detach --publish 5000:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbits
|
||||
``
|
||||
Finally you can access the lnbits on your machine port 5000.
|
||||
|
@ -54,7 +54,7 @@ Using this wallet requires the installation of the `lndgrpc` and `purerpc` Pytho
|
||||
|
||||
### LNPay
|
||||
|
||||
For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `<your LNbits host>/wallet/webhook` with the "Wallet Receive" event and no secret.
|
||||
For the invoice listener to work you have a publicly accessible URL in your LNbits and must set up [LNPay webhooks](https://dashboard.lnpay.co/webhook/) pointing to `<your LNbits host>/wallet/webhook` with the "Wallet Receive" event and no secret. For example, `https://mylnbits/wallet/webhook` will be the Endpoint Url that gets notified about the payment.
|
||||
|
||||
- `LNBITS_BACKEND_WALLET_CLASS`: **LNPayWallet**
|
||||
- `LNPAY_API_ENDPOINT`: https://api.lnpay.co/v1/
|
||||
|
@ -108,6 +108,7 @@ def register_assets(app: QuartTrio):
|
||||
def register_filters(app: QuartTrio):
|
||||
"""Jinja filters."""
|
||||
app.jinja_env.globals["SITE_TITLE"] = app.config["LNBITS_SITE_TITLE"]
|
||||
app.jinja_env.globals["LNBITS_VERSION"] = app.config["LNBITS_COMMIT"]
|
||||
app.jinja_env.globals["EXTENSIONS"] = get_valid_extensions()
|
||||
|
||||
|
||||
|
@ -104,4 +104,4 @@ async def api_lnurl_callback(link_id):
|
||||
if success_action:
|
||||
resp["success_action"] = success_action
|
||||
|
||||
return jsonify(), HTTPStatus.OK
|
||||
return jsonify(resp), HTTPStatus.OK
|
||||
|
@ -62,7 +62,9 @@ new Vue({
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
closeFormDialog() {},
|
||||
closeFormDialog() {
|
||||
this.resetFormData()
|
||||
},
|
||||
openQrCodeDialog(linkId) {
|
||||
var link = _.findWhere(this.payLinks, {id: linkId})
|
||||
if (link.currency) this.updateFiatRate(link.currency)
|
||||
@ -116,6 +118,13 @@ new Vue({
|
||||
this.createPayLink(wallet, data)
|
||||
}
|
||||
},
|
||||
resetFormData() {
|
||||
this.formDialog = {
|
||||
show: false,
|
||||
fixedAmount: true,
|
||||
data: {}
|
||||
}
|
||||
},
|
||||
updatePayLink(wallet, data) {
|
||||
let values = _.omit(
|
||||
_.pick(
|
||||
@ -147,6 +156,7 @@ new Vue({
|
||||
this.payLinks = _.reject(this.payLinks, obj => obj.id === data.id)
|
||||
this.payLinks.push(mapPayLink(response.data))
|
||||
this.formDialog.show = false
|
||||
this.resetFormData()
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
@ -158,6 +168,7 @@ new Vue({
|
||||
.then(response => {
|
||||
this.payLinks.push(mapPayLink(response.data))
|
||||
this.formDialog.show = false
|
||||
this.resetFormData()
|
||||
})
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
|
@ -291,7 +291,8 @@
|
||||
dense
|
||||
v-model.number="itemDialog.data.price"
|
||||
type="number"
|
||||
min="1"
|
||||
step="0.001"
|
||||
min="0.001"
|
||||
:label="`Item price (${itemDialog.data.unit})`"
|
||||
></q-input>
|
||||
<q-select
|
||||
|
@ -1,4 +1,27 @@
|
||||
# SatsPay Server
|
||||
|
||||
Create onchain and LN charges. Includes webhooks!
|
||||
## Create onchain and LN charges. Includes webhooks!
|
||||
|
||||
Easilly create invoices that support Lightning Network and on-chain BTC payment.
|
||||
|
||||
1. Create a "NEW CHARGE"\
|
||||

|
||||
2. Fill out the invoice fields
|
||||
- set a descprition for the payment
|
||||
- the amount in sats
|
||||
- the time, in minutes, the invoice is valid for, after this period the invoice can't be payed
|
||||
- set a webhook that will get the transaction details after a successful payment
|
||||
- set to where the user should redirect after payment
|
||||
- set the text for the button that will show after payment (not setting this, will display "NONE" in the button)
|
||||
- select if you want onchain payment, LN payment or both
|
||||
- depending on what you select you'll have to choose the respective wallets where to receive your payment\
|
||||

|
||||
3. The charge will appear on the _Charges_ section\
|
||||

|
||||
4. Your costumer/payee will get the payment page
|
||||
- they can choose to pay on LN\
|
||||

|
||||
- or pay on chain\
|
||||

|
||||
5. You can check the state of your charges in LNBits\
|
||||

|
||||
|
@ -22,7 +22,7 @@ async def create_charge(
|
||||
lnbitswallet: Optional[str] = None,
|
||||
webhook: Optional[str] = None,
|
||||
completelink: Optional[str] = None,
|
||||
completelinktext: Optional[str] = None,
|
||||
completelinktext: Optional[str] = 'Back to Merchant',
|
||||
time: Optional[int] = None,
|
||||
amount: Optional[int] = None,
|
||||
) -> Charges:
|
||||
|
@ -169,7 +169,7 @@
|
||||
></q-icon>
|
||||
<q-btn
|
||||
outline
|
||||
v-if="'{{ charge.webhook }}' != 'None'"
|
||||
v-if="'{{ charge.webhook }}' != None"
|
||||
type="a"
|
||||
href="{{ charge.completelink }}"
|
||||
label="{{ charge.completelinktext }}"
|
||||
|
7
lnbits/extensions/splitpayments/README.md
Normal file
7
lnbits/extensions/splitpayments/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Split Payments
|
||||
|
||||
Set this and forget. It will keep splitting your payments across wallets forever.
|
||||
|
||||
## Sponsored by
|
||||
|
||||
[](https://cryptograffiti.com/)
|
18
lnbits/extensions/splitpayments/__init__.py
Normal file
18
lnbits/extensions/splitpayments/__init__.py
Normal file
@ -0,0 +1,18 @@
|
||||
from quart import Blueprint
|
||||
|
||||
from lnbits.db import Database
|
||||
|
||||
db = Database("ext_splitpayments")
|
||||
|
||||
splitpayments_ext: Blueprint = Blueprint(
|
||||
"splitpayments", __name__, static_folder="static", template_folder="templates"
|
||||
)
|
||||
|
||||
|
||||
from .views_api import * # noqa
|
||||
from .views import * # noqa
|
||||
from .tasks import register_listeners
|
||||
|
||||
from lnbits.tasks import record_async
|
||||
|
||||
splitpayments_ext.record(record_async(register_listeners))
|
9
lnbits/extensions/splitpayments/config.json
Normal file
9
lnbits/extensions/splitpayments/config.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "SplitPayments",
|
||||
"short_description": "Split incoming payments to other wallets.",
|
||||
"icon": "call_split",
|
||||
"contributors": [
|
||||
"fiatjaf",
|
||||
"cryptograffiti"
|
||||
]
|
||||
}
|
23
lnbits/extensions/splitpayments/crud.py
Normal file
23
lnbits/extensions/splitpayments/crud.py
Normal file
@ -0,0 +1,23 @@
|
||||
from typing import List
|
||||
|
||||
from . import db
|
||||
from .models import Target
|
||||
|
||||
|
||||
async def get_targets(source_wallet: str) -> List[Target]:
|
||||
rows = await db.fetchall("SELECT * FROM targets WHERE source = ?", (source_wallet,))
|
||||
return [Target(**dict(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 targets WHERE source = ?", (source_wallet,))
|
||||
for target in targets:
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO targets
|
||||
(source, wallet, percent, alias)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(source_wallet, target.wallet, target.percent, target.alias),
|
||||
)
|
16
lnbits/extensions/splitpayments/migrations.py
Normal file
16
lnbits/extensions/splitpayments/migrations.py
Normal file
@ -0,0 +1,16 @@
|
||||
async def m001_initial(db):
|
||||
"""
|
||||
Initial split payment table.
|
||||
"""
|
||||
await db.execute(
|
||||
"""
|
||||
CREATE TABLE targets (
|
||||
wallet TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
percent INTEGER NOT NULL CHECK (percent >= 0 AND percent <= 100),
|
||||
alias TEXT,
|
||||
|
||||
UNIQUE (source, wallet)
|
||||
);
|
||||
"""
|
||||
)
|
8
lnbits/extensions/splitpayments/models.py
Normal file
8
lnbits/extensions/splitpayments/models.py
Normal file
@ -0,0 +1,8 @@
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class Target(NamedTuple):
|
||||
wallet: str
|
||||
source: str
|
||||
percent: int
|
||||
alias: str
|
143
lnbits/extensions/splitpayments/static/js/index.js
Normal file
143
lnbits/extensions/splitpayments/static/js/index.js
Normal file
@ -0,0 +1,143 @@
|
||||
/* 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
|
||||
}
|
||||
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data() {
|
||||
return {
|
||||
selectedWallet: null,
|
||||
currentHash: '', // a string that must match if the edit data is unchanged
|
||||
targets: []
|
||||
}
|
||||
},
|
||||
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
|
||||
})
|
||||
},
|
||||
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({})
|
||||
})
|
||||
},
|
||||
changedWallet(wallet) {
|
||||
this.selectedWallet = wallet
|
||||
this.getTargets()
|
||||
},
|
||||
targetChanged(isPercent, index) {
|
||||
// fix percent min and max range
|
||||
if (isPercent) {
|
||||
if (this.targets[index].percent > 100) this.targets[index].percent = 100
|
||||
if (this.targets[index].percent < 0) 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.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() !== '' && last.percent > 0) {
|
||||
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 -= Math.round(diff * target.percent)
|
||||
})
|
||||
}
|
||||
|
||||
// overwrite so changes appear
|
||||
this.targets = this.targets
|
||||
},
|
||||
saveTargets() {
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/splitpayments/api/v1/targets',
|
||||
this.selectedWallet.adminkey,
|
||||
{
|
||||
targets: this.targets
|
||||
.filter(isTargetComplete)
|
||||
.map(({wallet, percent, alias}) => ({wallet, percent, 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()
|
||||
}
|
||||
})
|
77
lnbits/extensions/splitpayments/tasks.py
Normal file
77
lnbits/extensions/splitpayments/tasks.py
Normal file
@ -0,0 +1,77 @@
|
||||
import json
|
||||
import trio # type: ignore
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.crud import create_payment
|
||||
from lnbits.core import db as core_db
|
||||
from lnbits.tasks import register_invoice_listener, internal_invoice_paid
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from .crud import get_targets
|
||||
|
||||
|
||||
async def register_listeners():
|
||||
invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2)
|
||||
register_invoice_listener(invoice_paid_chan_send)
|
||||
await wait_for_paid_invoices(invoice_paid_chan_recv)
|
||||
|
||||
|
||||
async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel):
|
||||
async for payment in invoice_paid_chan:
|
||||
await on_invoice_paid(payment)
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if "splitpayments" == payment.extra.get("tag") or payment.extra.get("splitted"):
|
||||
# already splitted, ignore
|
||||
return
|
||||
|
||||
# now we make some special internal transfers (from no one to the receiver)
|
||||
targets = await get_targets(payment.wallet_id)
|
||||
transfers = [
|
||||
(target.wallet, int(target.percent * payment.amount / 100))
|
||||
for target in targets
|
||||
]
|
||||
transfers = [(wallet, amount) for wallet, amount in transfers if amount > 0]
|
||||
amount_left = payment.amount - sum([amount for _, amount in transfers])
|
||||
|
||||
if amount_left < 0:
|
||||
print("splitpayments failure: amount_left is negative.", payment.payment_hash)
|
||||
return
|
||||
|
||||
if not targets:
|
||||
return
|
||||
|
||||
# mark the original payment with one extra key, "splitted"
|
||||
# (this prevents us from doing this process again and it's informative)
|
||||
# and reduce it by the amount we're going to send to the producer
|
||||
await core_db.execute(
|
||||
"""
|
||||
UPDATE apipayments
|
||||
SET extra = ?, amount = ?
|
||||
WHERE hash = ?
|
||||
AND checking_id NOT LIKE 'internal_%'
|
||||
""",
|
||||
(
|
||||
json.dumps(dict(**payment.extra, splitted=True)),
|
||||
amount_left,
|
||||
payment.payment_hash,
|
||||
),
|
||||
)
|
||||
|
||||
# perform the internal transfer using the same payment_hash
|
||||
for wallet, amount in transfers:
|
||||
internal_checking_id = f"internal_{urlsafe_short_hash()}"
|
||||
await create_payment(
|
||||
wallet_id=wallet,
|
||||
checking_id=internal_checking_id,
|
||||
payment_request="",
|
||||
payment_hash=payment.payment_hash,
|
||||
amount=amount,
|
||||
memo=payment.memo,
|
||||
pending=False,
|
||||
extra={"tag": "splitpayments"},
|
||||
)
|
||||
|
||||
# manually send this for now
|
||||
await internal_invoice_paid.send(internal_checking_id)
|
@ -0,0 +1,90 @@
|
||||
<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-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.url_root }}api/v1/livestream -H "X-Api-Key: {{
|
||||
g.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"
|
||||
>
|
||||
<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.url_root }}api/v1/splitpayments/targets -H
|
||||
"X-Api-Key: {{ g.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>
|
@ -0,0 +1,98 @@
|
||||
{% 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.wallet"
|
||||
label="Wallet"
|
||||
:hint="t === targets.length - 1 ? 'A wallet ID or invoice key.' : undefined"
|
||||
@input="targetChanged(false)"
|
||||
></q-input>
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
v-model="target.alias"
|
||||
label="Alias"
|
||||
:hint="t === targets.length - 1 ? 'A name to identify this target wallet locally.' : undefined"
|
||||
@input="targetChanged(false)"
|
||||
></q-input>
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
v-model.number="target.percent"
|
||||
label="Split Share"
|
||||
:hint="t === targets.length - 1 ? 'How much of the incoming payments will go to the target wallet.' : undefined"
|
||||
suffix="%"
|
||||
@input="targetChanged(true, t)"
|
||||
></q-input>
|
||||
</div>
|
||||
|
||||
<q-row class="row justify-evenly q-pa-lg">
|
||||
<q-col>
|
||||
<q-btn unelevated outline color="purple" @click="clearTargets">
|
||||
Clear
|
||||
</q-btn>
|
||||
</q-col>
|
||||
|
||||
<q-col>
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-purple"
|
||||
type="submit"
|
||||
:disabled="!isDirty"
|
||||
>
|
||||
Save Targets
|
||||
</q-btn>
|
||||
</q-col>
|
||||
</q-row>
|
||||
</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">LNbits 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 %}
|
12
lnbits/extensions/splitpayments/views.py
Normal file
12
lnbits/extensions/splitpayments/views.py
Normal file
@ -0,0 +1,12 @@
|
||||
from quart import g, render_template
|
||||
|
||||
from lnbits.decorators import check_user_exists, validate_uuids
|
||||
|
||||
from . import splitpayments_ext
|
||||
|
||||
|
||||
@splitpayments_ext.route("/")
|
||||
@validate_uuids(["usr"], required=True)
|
||||
@check_user_exists()
|
||||
async def index():
|
||||
return await render_template("splitpayments/index.html", user=g.user)
|
70
lnbits/extensions/splitpayments/views_api.py
Normal file
70
lnbits/extensions/splitpayments/views_api.py
Normal file
@ -0,0 +1,70 @@
|
||||
from quart import g, jsonify
|
||||
from http import HTTPStatus
|
||||
|
||||
from lnbits.decorators import api_check_wallet_key, api_validate_post_request
|
||||
from lnbits.core.crud import get_wallet, get_wallet_for_key
|
||||
|
||||
from . import splitpayments_ext
|
||||
from .crud import get_targets, set_targets
|
||||
from .models import Target
|
||||
|
||||
|
||||
@splitpayments_ext.route("/api/v1/targets", methods=["GET"])
|
||||
@api_check_wallet_key("admin")
|
||||
async def api_targets_get():
|
||||
targets = await get_targets(g.wallet.id)
|
||||
return jsonify([target._asdict() for target in targets] or [])
|
||||
|
||||
|
||||
@splitpayments_ext.route("/api/v1/targets", methods=["PUT"])
|
||||
@api_check_wallet_key("admin")
|
||||
@api_validate_post_request(
|
||||
schema={
|
||||
"targets": {
|
||||
"type": "list",
|
||||
"schema": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"wallet": {"type": "string"},
|
||||
"alias": {"type": "string"},
|
||||
"percent": {"type": "integer"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
async def api_targets_set():
|
||||
targets = []
|
||||
|
||||
for entry in g.data["targets"]:
|
||||
wallet = await get_wallet(entry["wallet"])
|
||||
if not wallet:
|
||||
wallet = await get_wallet_for_key(entry["wallet"], "invoice")
|
||||
if not wallet:
|
||||
return (
|
||||
jsonify({"message": f"Invalid wallet '{entry['wallet']}'."}),
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
|
||||
if wallet.id == g.wallet.id:
|
||||
return (
|
||||
jsonify({"message": "Can't split to itself."}),
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
|
||||
if entry["percent"] < 0:
|
||||
return (
|
||||
jsonify({"message": f"Invalid percent '{entry['percent']}'."}),
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
|
||||
targets.append(
|
||||
Target(wallet.id, g.wallet.id, entry["percent"], entry["alias"] or "")
|
||||
)
|
||||
|
||||
percent_sum = sum([target.percent for target in targets])
|
||||
if percent_sum > 100:
|
||||
return jsonify({"message": "Splitting over 100%."}), HTTPStatus.BAD_REQUEST
|
||||
|
||||
await set_targets(g.wallet.id, targets)
|
||||
return "", HTTPStatus.OK
|
@ -1,27 +1,29 @@
|
||||
<h1>Subdomains Extension</h1>
|
||||
|
||||
So the goal of the extension is to allow the owner of a domain to sell their subdomain to the anyone who is willing to pay some money for it.
|
||||
So the goal of the extension is to allow the owner of a domain to sell subdomains to anyone who is willing to pay some money for it.
|
||||
|
||||
[](https://youtu.be/O1X0fy3uNpw 'video tutorial subdomains')
|
||||
|
||||
## Requirements
|
||||
|
||||
- Free cloudflare account
|
||||
- Cloudflare as a dns server provider
|
||||
- Cloudflare TOKEN and Cloudflare zone-id where the domain is parked
|
||||
- Free Cloudflare account
|
||||
- Cloudflare as a DNS server provider
|
||||
- Cloudflare TOKEN and Cloudflare zone-ID where the domain is parked
|
||||
|
||||
## Usage
|
||||
|
||||
1. Register at cloudflare and setup your domain with them. (Just follow instructions they provide...)
|
||||
2. Change DNS server at your domain registrar to point to cloudflare's
|
||||
3. Get Cloudflare zoneID for your domain
|
||||
1. Register at Cloudflare and setup your domain with them. (Just follow instructions they provide...)
|
||||
2. Change DNS server at your domain registrar to point to Cloudflare's
|
||||
3. Get Cloudflare zone-ID for your domain
|
||||
<img src="https://i.imgur.com/xOgapHr.png">
|
||||
4. get Cloudflare API TOKEN
|
||||
4. Get Cloudflare API TOKEN
|
||||
<img src="https://i.imgur.com/BZbktTy.png">
|
||||
<img src="https://i.imgur.com/YDZpW7D.png">
|
||||
5. Open the lnbits subdomains extension and register your domain with lnbits
|
||||
5. Open the LNBits subdomains extension and register your domain
|
||||
6. Click on the button in the table to open the public form that was generated for your domain
|
||||
|
||||
- Extension also supports webhooks so you can get notified when someone buys a new domain
|
||||
<img src="https://i.imgur.com/hiauxeR.png">
|
||||
- Extension also supports webhooks so you can get notified when someone buys a new subdomain\
|
||||
<img src="https://i.imgur.com/hiauxeR.png">
|
||||
|
||||
## API Endpoints
|
||||
|
||||
@ -36,8 +38,6 @@ So the goal of the extension is to allow the owner of a domain to sell their sub
|
||||
- GET /api/v1/subdomains/<payment_hash>
|
||||
- DELETE /api/v1/subdomains/<subdomain_id>
|
||||
|
||||
## Useful
|
||||
|
||||
### Cloudflare
|
||||
|
||||
- Cloudflare offers programmatic subdomain registration... (create new A record)
|
||||
|
@ -1,3 +1,26 @@
|
||||
<h1>User Manager</h1>
|
||||
<h2>Make and manager users/wallets</h2>
|
||||
To help developers use LNbits to manage their users, the User Manager extension allows the creation and management of users and wallets. For example, a games developer may be developing a game that needs each user to have their own wallet, LNbits can be included in the develpoers stack as the user and wallet manager.
|
||||
# User Manager
|
||||
|
||||
## Make and manage users/wallets
|
||||
|
||||
To help developers use LNbits to manage their users, the User Manager extension allows the creation and management of users and wallets.
|
||||
|
||||
For example, a games developer may be developing a game that needs each user to have their own wallet, LNbits can be included in the developers stack as the user and wallet manager. Or someone wanting to manage their family's wallets (wife, children, parents, etc...) or you want to host a community Lightning Network node and want to manage wallets for the users.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Click the button "NEW USER" to create a new user\
|
||||

|
||||
2. Fill the user information\
|
||||
- username
|
||||
- the generated wallet name, user can create other wallets later on
|
||||
- email
|
||||
- set a password
|
||||

|
||||
3. After creating your user, it will appear in the **Users** section, and a user's wallet in the **Wallets** section.
|
||||
4. Next you can share the wallet with the corresponding user\
|
||||

|
||||
5. If you need to create more wallets for some user, click "NEW WALLET" at the top\
|
||||

|
||||
- select the existing user you wish to add the wallet
|
||||
- set a wallet name\
|
||||

|
||||
|
@ -214,7 +214,7 @@
|
||||
</div>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
var mapUserManager = function (obj) {
|
||||
var mapUserManager = function(obj) {
|
||||
obj.date = Quasar.utils.date.formatDate(
|
||||
new Date(obj.time * 1000),
|
||||
'YYYY-MM-DD HH:mm'
|
||||
@ -228,7 +228,7 @@
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
data: function() {
|
||||
return {
|
||||
wallets: [],
|
||||
users: [],
|
||||
@ -277,8 +277,8 @@
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userOptions: function () {
|
||||
return this.users.map(function (obj) {
|
||||
userOptions: function() {
|
||||
return this.users.map(function(obj) {
|
||||
console.log(obj.id)
|
||||
return {
|
||||
value: String(obj.id),
|
||||
@ -290,7 +290,7 @@
|
||||
methods: {
|
||||
///////////////Users////////////////////////////
|
||||
|
||||
getUsers: function () {
|
||||
getUsers: function() {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
@ -299,20 +299,20 @@
|
||||
'/usermanager/api/v1/users',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.users = response.data.map(function (obj) {
|
||||
.then(function(response) {
|
||||
self.users = response.data.map(function(obj) {
|
||||
return mapUserManager(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
openUserUpdateDialog: function (linkId) {
|
||||
openUserUpdateDialog: function(linkId) {
|
||||
var link = _.findWhere(this.users, {id: linkId})
|
||||
|
||||
this.userDialog.data = _.clone(link._data)
|
||||
this.userDialog.show = true
|
||||
},
|
||||
sendUserFormData: function () {
|
||||
sendUserFormData: function() {
|
||||
if (this.userDialog.data.id) {
|
||||
} else {
|
||||
var data = {
|
||||
@ -329,7 +329,7 @@
|
||||
}
|
||||
},
|
||||
|
||||
createUser: function (data) {
|
||||
createUser: function(data) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
@ -338,47 +338,48 @@
|
||||
this.g.user.wallets[0].inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
.then(function(response) {
|
||||
self.users.push(mapUserManager(response.data))
|
||||
self.userDialog.show = false
|
||||
self.userDialog.data = {}
|
||||
data = {}
|
||||
self.getWallets()
|
||||
})
|
||||
.catch(function (error) {
|
||||
.catch(function(error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteUser: function (userId) {
|
||||
deleteUser: function(userId) {
|
||||
var self = this
|
||||
|
||||
console.log(userId)
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this User link?')
|
||||
.onOk(function () {
|
||||
.onOk(function() {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/usermanager/api/v1/users/' + userId,
|
||||
self.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.users = _.reject(self.users, function (obj) {
|
||||
.then(function(response) {
|
||||
self.users = _.reject(self.users, function(obj) {
|
||||
return obj.id == userId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
.catch(function(error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
exportUsersCSV: function () {
|
||||
exportUsersCSV: function() {
|
||||
LNbits.utils.exportCSV(this.usersTable.columns, this.users)
|
||||
},
|
||||
|
||||
///////////////Wallets////////////////////////////
|
||||
|
||||
getWallets: function () {
|
||||
getWallets: function() {
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
@ -387,19 +388,19 @@
|
||||
'/usermanager/api/v1/wallets',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.wallets = response.data.map(function (obj) {
|
||||
.then(function(response) {
|
||||
self.wallets = response.data.map(function(obj) {
|
||||
return mapUserManager(obj)
|
||||
})
|
||||
})
|
||||
},
|
||||
openWalletUpdateDialog: function (linkId) {
|
||||
openWalletUpdateDialog: function(linkId) {
|
||||
var link = _.findWhere(this.users, {id: linkId})
|
||||
|
||||
this.walletDialog.data = _.clone(link._data)
|
||||
this.walletDialog.show = true
|
||||
},
|
||||
sendWalletFormData: function () {
|
||||
sendWalletFormData: function() {
|
||||
if (this.walletDialog.data.id) {
|
||||
} else {
|
||||
var data = {
|
||||
@ -414,7 +415,7 @@
|
||||
}
|
||||
},
|
||||
|
||||
createWallet: function (data) {
|
||||
createWallet: function(data) {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
@ -423,43 +424,43 @@
|
||||
this.g.user.wallets[0].inkey,
|
||||
data
|
||||
)
|
||||
.then(function (response) {
|
||||
.then(function(response) {
|
||||
self.wallets.push(mapUserManager(response.data))
|
||||
self.walletDialog.show = false
|
||||
self.walletDialog.data = {}
|
||||
data = {}
|
||||
})
|
||||
.catch(function (error) {
|
||||
.catch(function(error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
deleteWallet: function (userId) {
|
||||
deleteWallet: function(userId) {
|
||||
var self = this
|
||||
|
||||
LNbits.utils
|
||||
.confirmDialog('Are you sure you want to delete this wallet link?')
|
||||
.onOk(function () {
|
||||
.onOk(function() {
|
||||
LNbits.api
|
||||
.request(
|
||||
'DELETE',
|
||||
'/usermanager/api/v1/wallets/' + userId,
|
||||
self.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
self.wallets = _.reject(self.wallets, function (obj) {
|
||||
.then(function(response) {
|
||||
self.wallets = _.reject(self.wallets, function(obj) {
|
||||
return obj.id == userId
|
||||
})
|
||||
})
|
||||
.catch(function (error) {
|
||||
.catch(function(error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
exportWalletsCSV: function () {
|
||||
exportWalletsCSV: function() {
|
||||
LNbits.utils.exportCSV(this.walletsTable.columns, this.wallets)
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
created: function() {
|
||||
if (this.g.user.wallets.length) {
|
||||
this.getUsers()
|
||||
this.getWallets()
|
||||
|
@ -1,4 +1,19 @@
|
||||
# Watch Only wallet
|
||||
|
||||
## Monitor an onchain wallet and generate addresses for onchain payments
|
||||
|
||||
Monitor an extended public key and generate deterministic fresh public keys with this simple watch only wallet. Invoice payments can also be generated, both through a publically shareable page and API.
|
||||
|
||||
1. Start by clicking "NEW WALLET"\
|
||||

|
||||
2. Fill the requested fields:
|
||||
- give the wallet a name
|
||||
- paste an Extended Public Key (xpub, ypub, zpub)
|
||||
- click "CREATE WATCH-ONLY WALLET"\
|
||||

|
||||
3. You can then access your onchain addresses\
|
||||

|
||||
4. You can then generate bitcoin onchain adresses from LNbits\
|
||||

|
||||
|
||||
You can now use this wallet on the LNBits [SatsPayServer](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/satspay/README.md) extension
|
||||
|
@ -86,6 +86,8 @@
|
||||
<q-toolbar-title class="text-caption">
|
||||
<strong>LN</strong>bits, free and open-source lightning
|
||||
wallet/accounts system
|
||||
<br />
|
||||
<small>Commit version: {{LNBITS_VERSION}}</small>
|
||||
</q-toolbar-title>
|
||||
<q-space></q-space>
|
||||
<q-btn
|
||||
|
Loading…
x
Reference in New Issue
Block a user