Merge branch 'master' into Jukebox

This commit is contained in:
Ben Arc 2021-06-10 23:33:26 +01:00
commit ff80f3d585
28 changed files with 747 additions and 70 deletions

View File

@ -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"

View File

@ -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/

View File

@ -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.

View File

@ -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/

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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"\
![new charge](https://i.imgur.com/fUl6p74.png)
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\
![charge form](https://i.imgur.com/F10yRiW.png)
3. The charge will appear on the _Charges_ section\
![charges](https://i.imgur.com/zqHpVxc.png)
4. Your costumer/payee will get the payment page
- they can choose to pay on LN\
![offchain payment](https://i.imgur.com/4191SMV.png)
- or pay on chain\
![onchain payment](https://i.imgur.com/wzLRR5N.png)
5. You can check the state of your charges in LNBits\
![invoice state](https://i.imgur.com/JnBd22p.png)

View File

@ -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:

View File

@ -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 }}"

View File

@ -0,0 +1,7 @@
# Split Payments
Set this and forget. It will keep splitting your payments across wallets forever.
## Sponsored by
[![](https://cdn.shopify.com/s/files/1/0826/9235/files/cryptograffiti_logo_clear_background.png?v=1504730421)](https://cryptograffiti.com/)

View 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))

View File

@ -0,0 +1,9 @@
{
"name": "SplitPayments",
"short_description": "Split incoming payments to other wallets.",
"icon": "call_split",
"contributors": [
"fiatjaf",
"cryptograffiti"
]
}

View 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),
)

View 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)
);
"""
)

View File

@ -0,0 +1,8 @@
from typing import NamedTuple
class Target(NamedTuple):
wallet: str
source: str
percent: int
alias: str

View 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()
}
})

View 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)

View File

@ -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": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<code
>[{"wallet": &lt;wallet id&gt;, "alias": &lt;chosen name for this
wallet&gt;, "percent": &lt;number between 1 and 100&gt;}, ...]</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.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": &lt;admin_key&gt;}</code><br />
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 200 OK (application/json)
</h5>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X PUT {{ request.url_root }}api/v1/splitpayments/targets -H
"X-Api-Key: {{ g.user.wallets[0].adminkey }}" -H 'Content-Type:
application/json' -d '{"targets": [{"wallet": &lt;wallet id or invoice
key&gt;, "alias": &lt;name to identify this&gt;, "percent": &lt;number
between 1 and 100&gt;}, ...]}'
</code>
</q-card-section>
</q-card>
</q-expansion-item>
</q-expansion-item>

View File

@ -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 %}

View 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)

View 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

View File

@ -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.
[![video tutorial livestream](http://img.youtube.com/vi/O1X0fy3uNpw/0.jpg)](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)

View File

@ -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\
![new user](https://i.imgur.com/4yZyfJE.png)
2. Fill the user information\
- username
- the generated wallet name, user can create other wallets later on
- email
- set a password
![user information](https://i.imgur.com/40du7W5.png)
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\
![user wallet](https://i.imgur.com/gAyajbx.png)
5. If you need to create more wallets for some user, click "NEW WALLET" at the top\
![multiple wallets](https://i.imgur.com/wovVnim.png)
- select the existing user you wish to add the wallet
- set a wallet name\
![new wallet](https://i.imgur.com/sGwG8dC.png)

View File

@ -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()

View File

@ -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"\
![new wallet](https://i.imgur.com/vgbAB7c.png)
2. Fill the requested fields:
- give the wallet a name
- paste an Extended Public Key (xpub, ypub, zpub)
- click "CREATE WATCH-ONLY WALLET"\
![fill wallet form](https://i.imgur.com/UVoG7LD.png)
3. You can then access your onchain addresses\
![get address](https://i.imgur.com/zkxTQ6l.png)
4. You can then generate bitcoin onchain adresses from LNbits\
![onchain address](https://i.imgur.com/4KVSSJn.png)
You can now use this wallet on the LNBits [SatsPayServer](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/satspay/README.md) extension

View File

@ -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