Compare commits

...
69 Commits
Author SHA1 Message Date
arcbtc 810b918619 quick fix 2022-04-02 00:36:50 +01:00
ArcandGitHub 81860bdc5d Merge pull request #579 from ashkanjalaliQ/main
add "IRT" currency to TPos extention
2022-04-01 23:57:19 +01:00
Ashkan JalaliandGitHub c5c76468cb add "IRT" currency to TPos extention 2022-04-01 20:05:53 +04:30
fusion44andGitHub 799065e982 Merge pull request #576 from lilcheti/IRT-api
add IRT api
2022-03-31 18:52:53 +02:00
rimthekid a94444ae2c add IRT 2022-03-30 05:22:48 -07:00
ArcandGitHub 54264315f0 Merge pull request #575 from axelhodler/exception_handling
fail early if LNBITS_DATA_FOLDER missing
2022-03-30 12:04:28 +01:00
Axel Hodler 946ebc350e fail early if LNBITS_DATA_FOLDER missing 2022-03-27 17:03:24 +02:00
calleandGitHub b1c75813a0 fix decodepay (#568) 2022-03-24 12:50:57 +01:00
ArcandGitHub a17108f108 Update installation.md 2022-03-22 19:25:30 +00:00
calleandGitHub 1209526147 remove get_payment_status (#562)
* remove get_payment_status

* remove comments
2022-03-18 18:51:28 +01:00
calleandGitHub 0f97f8f18b Fee reserve for lightning backends (#557)
* preparing fees

* fee_limit_msat

* await resp result

* clightning

* fix tests

* fix test

* add fee to test

* mypy

* invoice_status

* checking id fix

* fee reserve error message

* only for external payments
2022-03-16 07:20:15 +01:00
ArcandGitHub 911fe92e03 Merge pull request #559 from lnbits/withdrawpayzerocheck
Added check so number must be more than zero
2022-03-14 11:30:08 +00:00
Ben Arc eb8bbd3896 Added check so number must be more than zero 2022-03-14 11:22:56 +00:00
Ben Arc ff70efd1eb Removes LNDRPC, if someone wants to fix we can implement again 2022-03-11 14:13:28 +00:00
Ben Arc ce6620882f Satspay tooltip fix (quart commit 8e3230287dd191a743479ee9ceb2f497c43dd9bc) 2022-03-11 13:25:14 +00:00
Ben Arc ffba97850b Stopped 0 invoice creation in tickets 2022-03-11 13:12:27 +00:00
Ben Arc 435b4056fd fixed conversion api 2022-03-10 12:16:51 +00:00
Ben Arc 30a0d6336b removed print 2022-03-08 22:29:13 +00:00
Ben Arc 52ef2da76d added wrong key type 2022-03-08 22:22:51 +00:00
Ben Arc e4dd0972a5 Wrong key int being returned 2022-03-08 22:15:45 +00:00
Ben Arc c708cb1e2b Important fix to payment check 2022-03-07 18:27:03 +00:00
ArcandGitHub 43f9394014 Merge pull request #555 from lnbits/extraPayDetails
Adds more payment details if the correct key is providedd
2022-03-07 08:20:07 +00:00
benarc 9bbf78e026 Assigned wallet var 2022-03-07 08:19:14 +00:00
ArcandGitHub 6df951b873 Merge pull request #554 from lnbits/extraPayDetails
Adds more payment details if the correct key is provided
2022-03-07 08:11:11 +00:00
benarc d62ef0bb80 Added extra pay details 2022-03-07 08:08:08 +00:00
benarc 4a9e7f4264 Merge remote-tracking branch 'origin/main' into main 2022-03-07 05:56:10 +00:00
ArcandGitHub b886e5b0ea Merge pull request #528 from CandleHater/patch-2
added "missing" libffi/libpq package
2022-03-07 05:47:29 +00:00
benarc 72f2ec0be8 Merge remote-tracking branch 'origin/main' into main 2022-03-07 04:26:40 +00:00
ArcandGitHub ef6d12fe10 Merge pull request #545 from talvasconcelos/fix/paymentsDecode
fix payments decode
2022-03-07 04:24:32 +00:00
ArcandGitHub 1e759283ce Merge pull request #540 from talvasconcelos/fix/watchonly_mempool
fix update mempool
2022-03-07 04:19:23 +00:00
ArcandGitHub e92447092d Merge pull request #532 from lnbits/retry_connection_on_fail
Retry backend connection on startup
2022-03-07 04:18:57 +00:00
ArcandGitHub 7988e2c303 Merge pull request #541 from talvasconcelos/fix/streamlabsInv
fix streamalerts
2022-03-07 04:17:15 +00:00
ArcandGitHub a13bb9cfea Merge pull request #542 from talvasconcelos/fix/copilot_db
fix copilots not being fetched
2022-03-07 04:16:41 +00:00
ArcandGitHub 7088a3240f Merge pull request #543 from talvasconcelos/fix/tipjar
change namedtuple to basemodel
2022-03-07 04:16:16 +00:00
ArcandGitHub 00e52001b5 Merge pull request #544 from talvasconcelos/fix/events
fix typo in table name
2022-03-07 04:15:54 +00:00
ArcandGitHub cd35966a56 Merge pull request #546 from talvasconcelos/fix/jukebox
fix jukebox
2022-03-07 04:15:28 +00:00
ArcandGitHub c59a65cdb4 Merge pull request #550 from talvasconcelos/fix/lnticket
Tickets issue #548
2022-03-07 04:14:16 +00:00
benarc 5f52658626 Merge remote-tracking branch 'origin/main' into main 2022-03-07 04:05:43 +00:00
Tiago vasconcelos 2399b05855 open ticket in dialog for large text 2022-03-03 23:05:34 +00:00
Tiago vasconcelos 8fb70041d2 fix flatrate per word toggle 2022-03-03 21:35:58 +00:00
Joseph PentlandandGitHub baf55afc28 api: Fix conversion from fiat to satoshis (#516)
Conversion to satoshis incorrectly used data.to as fiat currency rather
than data._from.
2022-03-01 23:11:39 +03:00
Graham KrizekandGitHub 251e6a8233 Add a fallback to LND_REST_CERT to use the system chain if its not present (#539) 2022-03-01 22:58:54 +03:00
ArcandGitHub d73c3950b9 Update README.md 2022-03-01 19:07:21 +00:00
ArcandGitHub ac61cd5427 Update README.md 2022-03-01 19:07:07 +00:00
ArcandGitHub 0c14b8fb82 Merge pull request #533 from louneskmt/fix-docker-image-build
Fix Docker image autobuild GitHub Action and build for arm
2022-03-01 18:57:45 +00:00
Tiago vasconcelos 6c0aa8fa8d info/error if no music is playing yet on jukebox 2022-02-28 10:09:40 +00:00
Tiago vasconcelos 9b95a7743d fix user keyword to "user" 2022-02-28 10:08:50 +00:00
benarc e96701cb59 Added template banner 2022-02-24 16:03:10 +00:00
benarc c1fa5bbcbf Change HIDE to false 2022-02-24 12:32:37 +00:00
Tiago vasconcelos 241c81c790 fix update mempool 2022-02-23 22:09:38 +00:00
Tiago vasconcelos 42b444f54d make "anonymous" as default name 2022-02-23 21:40:11 +00:00
Tiago vasconcelos f7ff9d27d2 fix copilots not being fetched 2022-02-23 21:03:25 +00:00
benarc 4a588dda1b Added ad space and option to turn off api docs 2022-02-23 13:51:30 +00:00
Tiago vasconcelos e82ce89e9a change namedtuple to basemodel 2022-02-23 13:32:23 +00:00
Tiago vasconcelos 270cac37ac fix typo in table name 2022-02-23 10:37:15 +00:00
Tiago vasconcelos a374526faf make Anonymous default name 2022-02-23 10:13:52 +00:00
Tiago vasconcelos 8930152b0f fix models int to str 2022-02-22 21:22:07 +00:00
Tiago vasconcelos 71e11b7387 fix typo 2022-02-22 21:12:46 +00:00
Tiago vasconcelos 510a28f3d0 create_charge data 2022-02-22 20:38:35 +00:00
ArcandGitHub c58e2c5d74 Merge pull request #538 from talvasconcelos/fix/withdrawQR
Fix LNURLw QR code
2022-02-22 18:28:29 +00:00
Tiago vasconcelos eb7b369b6d fix payments decode 2022-02-22 16:45:35 +00:00
Tiago vasconcelos a5a3331106 revert to fallback scheme + make request case insensitive 2022-02-22 16:23:50 +00:00
Tiago vasconcelos 4f3e48e280 fix lnurl encoded in QR 2022-02-22 15:16:39 +00:00
Lounès Ksouri 31bcb32e2d feat: dynamic port and host using environment variables 2022-02-22 08:41:57 +01:00
Lounès Ksouri a132387fa8 fix: add build deps for arm 2022-02-22 08:41:19 +01:00
Lounès Ksouri 27694ee23d refactor: GitHub Action to build and push to Docker Hub on tag 2022-02-22 08:40:23 +01:00
callebtc 36952981c2 remove success var 2022-02-21 09:25:45 +01:00
callebtc fbafe2b8f1 Retry backend connection on startup 2022-02-21 09:17:36 +01:00
CandleandGitHub 4793ea0d18 added "missing" libffi/libpq package
When I tryed to run "pipenv install --dev" on a Pi 4 with the latest Raspberry OS it failed with two dependencies. These additional installations fixed it.
2022-02-18 16:35:32 +01:00
47 changed files with 804 additions and 619 deletions
+4 -9
View File
@@ -11,6 +11,9 @@ LNBITS_ADMIN_USERS=""
LNBITS_ADMIN_EXTENSIONS="ngrok"
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
LNBITS_AD_SPACE="" # csv ad image filepaths or urls, extensions can choose to honor
LNBITS_HIDE_API=false # Hides wallet api, extensions can choose to honor
# Disable extensions for all users, use "all" to disable all extensions
LNBITS_DISABLED_EXTENSIONS="amilk"
@@ -32,7 +35,7 @@ LNBITS_SITE_DESCRIPTION="Some description about your service, will display if ti
# Choose from mint, flamingo, salvador, autumn, monochrome, classic
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, mint, autumn, monochrome, salvador"
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC),
# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet,
# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet
LNBITS_BACKEND_WALLET_CLASS=VoidWallet
# VoidWallet is just a fallback that works without any actual Lightning capabilities,
@@ -50,14 +53,6 @@ CLIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc"
LNBITS_ENDPOINT=https://legend.lnbits.com
LNBITS_KEY=LNBITS_ADMIN_KEY
# LndWallet
LND_GRPC_ENDPOINT=127.0.0.1
LND_GRPC_PORT=11009
LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_GRPC_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING"
# To use an AES-encrypted macaroon, set
# LND_GRPC_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn"
# LndRestWallet
LND_REST_ENDPOINT=https://127.0.0.1:8080/
LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
+1
View File
@@ -9,4 +9,5 @@ jobs:
- uses: actions/checkout@v1
- uses: jpetrucciani/mypy-check@master
with:
mypy_flags: '--install-types --non-interactive'
path: lnbits
-58
View File
@@ -1,58 +0,0 @@
name: Docker build on push
env:
DOCKER_CLI_EXPERIMENTAL: enabled
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-20.04
name: Build and push lnbits image
steps:
- name: Login to Docker Hub
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Checkout project
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
id: qemu
- name: Setup Docker buildx action
uses: docker/setup-buildx-action@v1
id: buildx
- name: Show available Docker buildx platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Cache Docker layers
uses: actions/cache@v2
id: cache
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Run Docker buildx against commit hash
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
--tag ${{ secrets.DOCKER_USERNAME }}/lnbits:${GITHUB_SHA:0:7} \
--output "type=registry" ./
- name: Run Docker buildx against latest
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
--tag ${{ secrets.DOCKER_USERNAME }}/lnbits:latest \
--output "type=registry" ./
+68
View File
@@ -0,0 +1,68 @@
name: Build and push Docker image on tag
env:
DOCKER_CLI_EXPERIMENTAL: enabled
on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+-*"
jobs:
build:
runs-on: ubuntu-20.04
name: Build and push lnbits image
steps:
- name: Login to Docker Hub
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Checkout project
uses: actions/checkout@v2
- name: Import environment variables
id: import-env
shell: bash
run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
- name: Show set environment variables
run: |
printf " TAG: %s\n" "$TAG"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
id: qemu
- name: Setup Docker buildx action
uses: docker/setup-buildx-action@v1
id: buildx
- name: Show available Docker buildx platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Cache Docker layers
uses: actions/cache@v2
id: cache
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Run Docker buildx against tag
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform linux/amd64,linux/arm64 \
--tag ${{ secrets.DOCKER_USERNAME }}/lnbits-legend:${TAG} \
--output "type=registry" ./
- name: Run Docker buildx against latest
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform linux/amd64,linux/arm64 \
--tag ${{ secrets.DOCKER_USERNAME }}/lnbits-legend:latest \
--output "type=registry" ./
+6 -2
View File
@@ -8,8 +8,9 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# Install build deps
RUN apt-get update
RUN apt-get install -y --no-install-recommends build-essential
RUN apt-get install -y --no-install-recommends build-essential pkg-config
RUN python -m pip install --upgrade pip
RUN pip install wheel
# Install runtime deps
COPY requirements.txt /tmp/requirements.txt
@@ -36,6 +37,9 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
WORKDIR /app
COPY --chown=1000:1000 lnbits /app/lnbits
ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0"
EXPOSE 5000
CMD ["uvicorn", "lnbits.__main__:app", "--port", "5000", "--host", "0.0.0.0"]
CMD ["sh", "-c", "uvicorn lnbits.__main__:app --port $LNBITS_PORT --host $LNBITS_HOST"]
+2
View File
@@ -11,6 +11,8 @@ LNbits
(Join us on [https://t.me/lnbits](https://t.me/lnbits))
(LNbits is beta, for responsible disclosure of any concerns please contact lnbits@pm.me)
Use [lnbits.com](https://lnbits.com), or run your own LNbits server!
LNbits is a very simple Python server that sits on top of any funding source, and can be used as:
+5
View File
@@ -12,6 +12,8 @@ LNbits uses [Pipenv][pipenv] to manage Python packages.
```sh
git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/
sudo apt-get install pipenv
pipenv shell
# pipenv --python 3.9 shell (if you wish to use a version of Python higher than 3.7)
pipenv install --dev
@@ -19,6 +21,9 @@ pipenv install --dev
# If any of the modules fails to install, try checking and upgrading your setupTool module
# pip install -U setuptools
# install libffi/libpq in case "pipenv install" fails
# sudo apt-get install -y libffi-dev libpq-dev
```
## Running the server
+9 -8
View File
@@ -84,18 +84,19 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
def check_funding_source(app: FastAPI) -> None:
@app.on_event("startup")
async def check_wallet_status():
error_message, balance = await WALLET.status()
if error_message:
while True:
error_message, balance = await WALLET.status()
if not error_message:
break
warnings.warn(
f" × The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
RuntimeWarning,
)
sys.exit(4)
else:
print(
f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat."
)
print("Retrying connection to backend in 5 seconds...")
await asyncio.sleep(5)
print(
f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat."
)
def register_routes(app: FastAPI) -> None:
+19 -23
View File
@@ -85,18 +85,17 @@ async def pay_invoice(
description: str = "",
conn: Optional[Connection] = None,
) -> str:
invoice = bolt11.decode(payment_request)
fee_reserve_msat = fee_reserve(invoice.amount_msat)
async with (db.reuse_conn(conn) if conn else db.connect()) as conn:
temp_id = f"temp_{urlsafe_short_hash()}"
internal_id = f"internal_{urlsafe_short_hash()}"
invoice = bolt11.decode(payment_request)
if invoice.amount_msat == 0:
raise ValueError("Amountless invoices not supported.")
if max_sat and invoice.amount_msat > max_sat * 1000:
raise ValueError("Amount in invoice is too high.")
wallet = await get_wallet(wallet_id, conn=conn)
# put all parameters that don't change here
PaymentKwargs = TypedDict(
"PaymentKwargs",
@@ -134,26 +133,20 @@ async def pay_invoice(
# the balance is enough in the next step
await create_payment(
checking_id=temp_id,
fee=-fee_reserve(invoice.amount_msat),
fee=-fee_reserve_msat,
conn=conn,
**payment_kwargs,
)
# do the balance check if internal payment
if internal_checking_id:
wallet = await get_wallet(wallet_id, conn=conn)
assert wallet
if wallet.balance_msat < 0:
raise PermissionError("Insufficient balance.")
# do the balance check if external payment
else:
if invoice.amount_msat > wallet.balance_msat - (
wallet.balance_msat / 100 * 2
):
raise PermissionError(
"LNbits requires you keep at least 2% reserve to cover potential routing fees."
# do the balance check
wallet = await get_wallet(wallet_id, conn=conn)
assert wallet
if wallet.balance_msat < 0:
if not internal_checking_id and wallet.balance_msat > -fee_reserve_msat:
raise PaymentFailure(
f"You must reserve at least 1% ({round(fee_reserve_msat/1000)} sat) to cover potential routing fees."
)
raise PermissionError("Insufficient balance.")
if internal_checking_id:
# mark the invoice from the other side as not pending anymore
@@ -171,7 +164,9 @@ async def pay_invoice(
await internal_invoice_queue.put(internal_checking_id)
else:
# actually pay the external invoice
payment: PaymentResponse = await WALLET.pay_invoice(payment_request)
payment: PaymentResponse = await WALLET.pay_invoice(
payment_request, fee_reserve_msat
)
if payment.checking_id:
async with db.connect() as conn:
await create_payment(
@@ -286,12 +281,12 @@ async def perform_lnurlauth(
sign_len = 6 + r_len + s_len
signature = BytesIO()
signature.write(0x30 .to_bytes(1, "big", signed=False))
signature.write(0x30.to_bytes(1, "big", signed=False))
signature.write((sign_len - 2).to_bytes(1, "big", signed=False))
signature.write(0x02 .to_bytes(1, "big", signed=False))
signature.write(0x02.to_bytes(1, "big", signed=False))
signature.write(r_len.to_bytes(1, "big", signed=False))
signature.write(r)
signature.write(0x02 .to_bytes(1, "big", signed=False))
signature.write(0x02.to_bytes(1, "big", signed=False))
signature.write(s_len.to_bytes(1, "big", signed=False))
signature.write(s)
@@ -340,5 +335,6 @@ async def check_invoice_status(
return status
# WARN: this same value must be used for balance check and passed to WALLET.pay_invoice(), it may cause a vulnerability if the values differ
def fee_reserve(amount_msat: int) -> int:
return max(1000, int(amount_msat * 0.01))
return max(2000, int(amount_msat * 0.01))
+2 -2
View File
@@ -364,12 +364,12 @@ new Vue({
},
decodeRequest: function () {
this.parse.show = true
let req = this.parse.data.request.toLowerCase()
if (this.parse.data.request.startsWith('lightning:')) {
this.parse.data.request = this.parse.data.request.slice(10)
} else if (this.parse.data.request.startsWith('lnurl:')) {
this.parse.data.request = this.parse.data.request.slice(6)
} else if (this.parse.data.request.indexOf('lightning=lnurl1') !== -1) {
} else if (req.indexOf('lightning=lnurl1') !== -1) {
this.parse.data.request = this.parse.data.request
.split('lightning=')[1]
.split('&')[0]
+439 -413
View File
@@ -273,444 +273,470 @@
</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-mt-none q-mb-sm">
{{ SITE_TITLE }} Wallet: <strong><em>{{ wallet.name }}</em></strong>
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
{% include "core/_api_docs.html" %}
{% if HIDE_API %}
<div class="col-12 col-md-4 q-gutter-y-md">
{% else %}
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-mt-none q-mb-sm">
{{ SITE_TITLE }} Wallet: <strong><em>{{ wallet.name }}</em></strong>
</h6>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
{% if wallet.lnurlwithdraw_full %}
<q-expansion-item group="extras" icon="crop_free" label="Drain Funds">
<q-card>
<q-card-section class="text-center">
<p>
This is an LNURL-withdraw QR code for slurping everything from
this wallet. Do not share with anyone.
</p>
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
<q-list>
{% include "core/_api_docs.html" %}
<q-separator></q-separator>
{% if wallet.lnurlwithdraw_full %}
<q-expansion-item
group="extras"
icon="crop_free"
label="Drain Funds"
>
<q-card>
<q-card-section class="text-center">
<p>
This is an LNURL-withdraw QR code for slurping everything
from this wallet. Do not share with anyone.
</p>
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
<qrcode
value="{{wallet.lnurlwithdraw_full}}"
:options="{width:240}"
></qrcode>
</a>
<p>
It is compatible with <code>balanceCheck</code> and
<code>balanceNotify</code> so your wallet may keep pulling
the funds continuously from here after the first withdraw.
</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
{% endif %}
<q-expansion-item
group="extras"
icon="settings_cell"
label="Export to Phone with QR Code"
>
<q-card>
<q-card-section class="text-center">
<p>
This QR code contains your wallet URL with full access. You
can scan it from your phone to open your wallet from there.
</p>
<qrcode
value="{{wallet.lnurlwithdraw_full}}"
:value="'{{request.base_url}}' +'wallet?usr={{user.id}}&wal={{wallet.id}}'"
:options="{width:240}"
></qrcode>
</a>
<p>
It is compatible with <code>balanceCheck</code> and
<code>balanceNotify</code> so your wallet may keep pulling the
funds continuously from here after the first withdraw.
</p>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
{% endif %}
<q-expansion-item
group="extras"
icon="settings_cell"
label="Export to Phone with QR Code"
>
<q-card>
<q-card-section class="text-center">
<p>
This QR code contains your wallet URL with full access. You
can scan it from your phone to open your wallet from there.
</p>
<qrcode
:value="'{{request.base_url}}' +'wallet?usr={{user.id}}&wal={{wallet.id}}'"
:options="{width:240}"
></qrcode>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item group="extras" icon="edit" label="Rename wallet">
<q-card>
<q-card-section>
<div class="" style="max-width: 320px">
<q-input
filled
v-model.trim="newName"
label="Label"
dense="dense"
@update:model-value="(e) => console.log(e)"
/>
</div>
<q-btn
:disable="!newName.length"
unelevated
class="q-mt-sm"
color="primary"
@click="updateWalletName()"
>Update name</q-btn
>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item
group="extras"
icon="remove_circle"
label="Delete wallet"
>
<q-card>
<q-card-section>
<p>
This whole wallet will be deleted, the funds will be
<strong>UNRECOVERABLE</strong>.
</p>
<q-btn
unelevated
color="red-10"
@click="deleteWallet('{{ wallet.id }}', '{{ user.id }}')"
>Delete wallet</q-btn
>
</q-card-section>
</q-card>
</q-expansion-item>
</q-list>
</q-card-section>
</q-card>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item group="extras" icon="edit" label="Rename wallet">
<q-card>
<q-card-section>
<div class="" style="max-width: 320px">
<q-input
filled
v-model.trim="newName"
label="Label"
dense="dense"
@update:model-value="(e) => console.log(e)"
/>
</div>
<q-btn
:disable="!newName.length"
unelevated
class="q-mt-sm"
color="primary"
@click="updateWalletName()"
>Update name</q-btn
>
</q-card-section>
</q-card>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item
group="extras"
icon="remove_circle"
label="Delete wallet"
>
<q-card>
<q-card-section>
<p>
This whole wallet will be deleted, the funds will be
<strong>UNRECOVERABLE</strong>.
</p>
<q-btn
unelevated
color="red-10"
@click="deleteWallet('{{ wallet.id }}', '{{ user.id }}')"
>Delete wallet</q-btn
>
</q-card-section>
</q-card>
</q-expansion-item>
</q-list>
</q-card-section>
</q-card>
{% endif %} {% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD =
ADS.split(';') %}
<q-card>
<a href="{{ AD[0] }}"
><img width="100%" src="{{ AD[1] }}"
/></a> </q-card
>{% endfor %} {% endif %}
</div>
</div>
</div>
<q-dialog v-model="receive.show" @hide="closeReceiveDialog">
{% raw %}
<q-card
v-if="!receive.paymentReq"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<q-form @submit="createInvoice" class="q-gutter-md">
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
<b>{{receive.lnurl.domain}}</b> is requesting an invoice:
</p>
{% endraw %} {% if LNBITS_DENOMINATION != 'sats' %}
<q-input
filled
dense
v-model.number="receive.data.amount"
label="Amount ({{LNBITS_DENOMINATION}}) *"
mask="#.##"
fill-mask="0"
reverse-fill-mask
:min="receive.minMax[0]"
:max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed"
></q-input>
{% else %}
<q-select
filled
dense
v-model="receive.unit"
type="text"
label="Unit"
:options="receive.units"
></q-select>
<q-input
filled
dense
v-model.number="receive.data.amount"
:label="'Amount (' + receive.unit + ') *'"
:mask="receive.unit != 'sat' ? '#.##' : '#'"
fill-mask="0"
reverse-fill-mask
:step="receive.unit != 'sat' ? '0.01' : '1'"
:min="receive.minMax[0]"
:max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed"
></q-input>
{% endif %}
<q-input
filled
dense
v-model.trim="receive.data.memo"
label="Memo"
></q-input>
{% raw %}
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="receive.data.amount == null || receive.data.amount <= 0"
type="submit"
>
<span v-if="receive.lnurl">
Withdraw from {{receive.lnurl.domain}}
</span>
<span v-else> Create invoice </span>
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
<q-spinner
v-if="receive.status == 'loading'"
color="primary"
size="2.55em"
></q-spinner>
</q-form>
</q-card>
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="receive.paymentReq"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
{% endraw %}
</q-dialog>
<q-dialog v-model="parse.show" @hide="closeParseDialog">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div v-if="parse.invoice">
<h6 v-if="'{{LNBITS_DENOMINATION}}' != 'sats'" class="q-my-none">
{% raw %} {{ parseFloat(String(parse.invoice.fsat).replaceAll(",", ""))
/ 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
</h6>
<h6 v-else class="q-my-none">
{{ parse.invoice.fsat }}{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
</h6>
<q-separator class="q-my-sm"></q-separator>
<p class="text-wrap">
<strong>Description:</strong> {{ parse.invoice.description }}<br />
<strong>Expire date:</strong> {{ parse.invoice.expireDate }}<br />
<strong>Hash:</strong> {{ parse.invoice.hash }}
</p>
{% endraw %}
<div v-if="canPay" class="row q-mt-lg">
<q-btn unelevated color="primary" @click="payInvoice">Pay</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
<div v-else class="row q-mt-lg">
<q-btn unelevated disabled color="yellow" text-color="black"
>Not enough funds!</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</div>
<div v-else-if="parse.lnurlauth">
{% raw %}
<q-form @submit="authLnurl" class="q-gutter-md">
<p class="q-my-none text-h6">
Authenticate with <b>{{ parse.lnurlauth.domain }}</b>?
<q-dialog v-model="receive.show" @hide="closeReceiveDialog">
{% raw %}
<q-card
v-if="!receive.paymentReq"
class="q-pa-lg q-pt-xl lnbits__dialog-card"
>
<q-form @submit="createInvoice" class="q-gutter-md">
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
<b>{{receive.lnurl.domain}}</b> is requesting an invoice:
</p>
<q-separator class="q-my-sm"></q-separator>
<p>
For every website and for every LNbits wallet, a new keypair will be
deterministically generated so your identity can't be tied to your
LNbits wallet or linked across websites. No other data will be shared
with {{ parse.lnurlauth.domain }}.
</p>
<p>Your public key for <b>{{ parse.lnurlauth.domain }}</b> is:</p>
<p class="q-mx-xl">
<code class="text-wrap"> {{ parse.lnurlauth.pubkey }} </code>
</p>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit">Login</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
{% endraw %}
</div>
<div v-else-if="parse.lnurlpay">
{% raw %}
<q-form @submit="payLnurl" class="q-gutter-md">
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
<b>{{ parse.lnurlpay.domain }}</b> is requesting {{
parse.lnurlpay.maxSendable | msatoshiFormat }} {{LNBITS_DENOMINATION}}
<span v-if="parse.lnurlpay.commentAllowed > 0">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
</span>
</p>
<p v-else class="q-my-none text-h6 text-center">
<b>{{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }}</b> is
requesting <br />
between <b>{{ parse.lnurlpay.minSendable | msatoshiFormat }}</b> and
<b>{{ parse.lnurlpay.maxSendable | msatoshiFormat }}</b>
{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
<span v-if="parse.lnurlpay.commentAllowed > 0">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
</span>
</p>
<q-separator class="q-my-sm"></q-separator>
<div class="row">
<p class="col text-justify text-italic">
{{ parse.lnurlpay.description }}
</p>
<p class="col-4 q-pl-md" v-if="parse.lnurlpay.image">
<q-img :src="parse.lnurlpay.image" />
</p>
</div>
<div class="row">
<div class="col">
{% endraw %}
<q-input
filled
dense
v-model.number="parse.data.amount"
type="number"
label="Amount ({{LNBITS_DENOMINATION}}) *"
:min="parse.lnurlpay.minSendable / 1000"
:max="parse.lnurlpay.maxSendable / 1000"
:readonly="parse.lnurlpay.fixed"
></q-input>
{% raw %}
</div>
<div class="col-8 q-pl-md" v-if="parse.lnurlpay.commentAllowed > 0">
<q-input
filled
dense
v-model="parse.data.comment"
:type="parse.lnurlpay.commentAllowed > 64 ? 'textarea' : 'text'"
label="Comment (optional)"
:maxlength="parse.lnurlpay.commentAllowed"
></q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit"
>Send {{LNBITS_DENOMINATION}}</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
{% endraw %}
</div>
<div v-else>
<q-form
v-if="!parse.camera.show"
@submit="decodeRequest"
class="q-gutter-md"
>
{% endraw %} {% if LNBITS_DENOMINATION != 'sats' %}
<q-input
filled
dense
v-model.trim="parse.data.request"
type="textarea"
label="Paste an invoice, payment request or lnurl code *"
>
</q-input>
<div class="row q-mt-lg">
v-model.number="receive.data.amount"
label="Amount ({{LNBITS_DENOMINATION}}) *"
mask="#.##"
fill-mask="0"
reverse-fill-mask
:min="receive.minMax[0]"
:max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed"
></q-input>
{% else %}
<q-select
filled
dense
v-model="receive.unit"
type="text"
label="Unit"
:options="receive.units"
></q-select>
<q-input
filled
dense
v-model.number="receive.data.amount"
:label="'Amount (' + receive.unit + ') *'"
:mask="receive.unit != 'sat' ? '#.##' : '#'"
fill-mask="0"
reverse-fill-mask
:step="receive.unit != 'sat' ? '0.01' : '1'"
:min="receive.minMax[0]"
:max="receive.minMax[1]"
:readonly="receive.lnurl && receive.lnurl.fixed"
></q-input>
{% endif %}
<q-input
filled
dense
v-model.trim="receive.data.memo"
label="Memo"
></q-input>
{% raw %}
<div v-if="receive.status == 'pending'" class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="parse.data.request == ''"
:disable="receive.data.amount == null || receive.data.amount <= 0"
type="submit"
>Read</q-btn
>
<span v-if="receive.lnurl">
Withdraw from {{receive.lnurl.domain}}
</span>
<span v-else> Create invoice </span>
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
<q-spinner
v-if="receive.status == 'loading'"
color="primary"
size="2.55em"
></q-spinner>
</q-form>
</q-card>
<q-card v-else class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div class="text-center q-mb-lg">
<a :href="'lightning:' + receive.paymentReq">
<q-responsive :ratio="1" class="q-mx-xl">
<qrcode
:value="receive.paymentReq"
:options="{width: 340}"
class="rounded-borders"
></qrcode>
</q-responsive>
</a>
</div>
<div class="row q-mt-lg">
<q-btn outline color="grey" @click="copyText(receive.paymentReq)"
>Copy invoice</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
{% endraw %}
</q-dialog>
<q-dialog v-model="parse.show" @hide="closeParseDialog">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
<div v-if="parse.invoice">
<h6 v-if="'{{LNBITS_DENOMINATION}}' != 'sats'" class="q-my-none">
{% raw %} {{ parseFloat(String(parse.invoice.fsat).replaceAll(",",
"")) / 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
</h6>
<h6 v-else class="q-my-none">
{{ parse.invoice.fsat }}{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
</h6>
<q-separator class="q-my-sm"></q-separator>
<p class="text-wrap">
<strong>Description:</strong> {{ parse.invoice.description }}<br />
<strong>Expire date:</strong> {{ parse.invoice.expireDate }}<br />
<strong>Hash:</strong> {{ parse.invoice.hash }}
</p>
{% endraw %}
<div v-if="canPay" class="row q-mt-lg">
<q-btn unelevated color="primary" @click="payInvoice">Pay</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
<div v-else class="row q-mt-lg">
<q-btn unelevated disabled color="yellow" text-color="black"
>Not enough funds!</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</div>
<div v-else-if="parse.lnurlauth">
{% raw %}
<q-form @submit="authLnurl" class="q-gutter-md">
<p class="q-my-none text-h6">
Authenticate with <b>{{ parse.lnurlauth.domain }}</b>?
</p>
<q-separator class="q-my-sm"></q-separator>
<p>
For every website and for every LNbits wallet, a new keypair will be
deterministically generated so your identity can't be tied to your
LNbits wallet or linked across websites. No other data will be
shared with {{ parse.lnurlauth.domain }}.
</p>
<p>Your public key for <b>{{ parse.lnurlauth.domain }}</b> is:</p>
<p class="q-mx-xl">
<code class="text-wrap"> {{ parse.lnurlauth.pubkey }} </code>
</p>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit">Login</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
{% endraw %}
</div>
<div v-else-if="parse.lnurlpay">
{% raw %}
<q-form @submit="payLnurl" class="q-gutter-md">
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
<b>{{ parse.lnurlpay.domain }}</b> is requesting {{
parse.lnurlpay.maxSendable | msatoshiFormat }}
{{LNBITS_DENOMINATION}}
<span v-if="parse.lnurlpay.commentAllowed > 0">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
</span>
</p>
<p v-else class="q-my-none text-h6 text-center">
<b>{{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }}</b> is
requesting <br />
between <b>{{ parse.lnurlpay.minSendable | msatoshiFormat }}</b> and
<b>{{ parse.lnurlpay.maxSendable | msatoshiFormat }}</b>
{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
<span v-if="parse.lnurlpay.commentAllowed > 0">
<br />
and a {{parse.lnurlpay.commentAllowed}}-char comment
</span>
</p>
<q-separator class="q-my-sm"></q-separator>
<div class="row">
<p class="col text-justify text-italic">
{{ parse.lnurlpay.description }}
</p>
<p class="col-4 q-pl-md" v-if="parse.lnurlpay.image">
<q-img :src="parse.lnurlpay.image" />
</p>
</div>
<div class="row">
<div class="col">
{% endraw %}
<q-input
filled
dense
v-model.number="parse.data.amount"
type="number"
label="Amount ({{LNBITS_DENOMINATION}}) *"
:min="parse.lnurlpay.minSendable / 1000"
:max="parse.lnurlpay.maxSendable / 1000"
:readonly="parse.lnurlpay.fixed"
></q-input>
{% raw %}
</div>
<div class="col-8 q-pl-md" v-if="parse.lnurlpay.commentAllowed > 0">
<q-input
filled
dense
v-model="parse.data.comment"
:type="parse.lnurlpay.commentAllowed > 64 ? 'textarea' : 'text'"
label="Comment (optional)"
:maxlength="parse.lnurlpay.commentAllowed"
></q-input>
</div>
</div>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit"
>Send {{LNBITS_DENOMINATION}}</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
{% endraw %}
</div>
<div v-else>
<q-responsive :ratio="1">
<qrcode-stream
@decode="decodeQR"
class="rounded-borders"
></qrcode-stream>
</q-responsive>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto">
Cancel
</q-btn>
<q-form
v-if="!parse.camera.show"
@submit="decodeRequest"
class="q-gutter-md"
>
<q-input
filled
dense
v-model.trim="parse.data.request"
type="textarea"
label="Paste an invoice, payment request or lnurl code *"
>
</q-input>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
:disable="parse.data.request == ''"
type="submit"
>Read</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
<div v-else>
<q-responsive :ratio="1">
<qrcode-stream
@decode="decodeQR"
class="rounded-borders"
></qrcode-stream>
</q-responsive>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto">
Cancel
</q-btn>
</div>
</div>
</div>
</div>
</q-card>
</q-dialog>
</q-card>
</q-dialog>
<q-dialog v-model="parse.camera.show">
<q-card class="q-pa-lg q-pt-xl">
<div class="text-center q-mb-lg">
<qrcode-stream @decode="decodeQR" class="rounded-borders"></qrcode-stream>
</div>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="parse.camera.show">
<q-card class="q-pa-lg q-pt-xl">
<div class="text-center q-mb-lg">
<qrcode-stream
@decode="decodeQR"
class="rounded-borders"
></qrcode-stream>
</div>
<div class="row q-mt-lg">
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-card>
</q-dialog>
<q-dialog v-model="paymentsChart.show">
<q-card class="q-pa-sm" style="width: 800px; max-width: unset">
<q-card-section>
<canvas ref="canvas" width="600" height="400"></canvas>
</q-card-section>
</q-card>
</q-dialog>
<q-tabs
class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-max"
active-class="px-0"
indicator-color="transparent"
>
<q-tab
icon="account_balance_wallet"
label="Wallets"
@click="g.visibleDrawer = !g.visibleDrawer"
<q-dialog v-model="paymentsChart.show">
<q-card class="q-pa-sm" style="width: 800px; max-width: unset">
<q-card-section>
<canvas ref="canvas" width="600" height="400"></canvas>
</q-card-section>
</q-card>
</q-dialog>
<q-tabs
class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-max"
active-class="px-0"
indicator-color="transparent"
>
</q-tab>
<q-tab icon="content_paste" label="Paste" @click="showParseDialog"> </q-tab>
<q-tab icon="file_download" label="Receive" @click="showReceiveDialog">
</q-tab>
<q-tab
icon="account_balance_wallet"
label="Wallets"
@click="g.visibleDrawer = !g.visibleDrawer"
>
</q-tab>
<q-tab icon="content_paste" label="Paste" @click="showParseDialog"> </q-tab>
<q-tab icon="file_download" label="Receive" @click="showReceiveDialog">
</q-tab>
<q-tab icon="photo_camera" label="Scan" @click="showCamera"> </q-tab>
</q-tabs>
{% if service_fee > 0 %}
<div ref="disclaimer"></div>
<q-dialog v-model="disclaimerDialog.show">
<q-card class="q-pa-lg">
<h6 class="q-my-md text-deep-purple">Warning</h6>
<p>
Login functionality to be released in v0.2, for now,
<strong
>make sure you bookmark this page for future access to your
wallet</strong
>!
</p>
<p>
This service is in BETA, and we hold no responsibility for people losing
access to funds. To encourage you to run your own LNbits installation, any
balance on {% raw %}{{ disclaimerDialog.location.host }}{% endraw %} will
incur a charge of <strong>{{ service_fee }}% service fee</strong> per
week.
</p>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText(disclaimerDialog.location.href)"
>Copy wallet URL</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>I understand</q-btn
>
</div>
</q-card>
</q-dialog>
{% endif %} {% endblock %}
<q-tab icon="photo_camera" label="Scan" @click="showCamera"> </q-tab>
</q-tabs>
{% if service_fee > 0 %}
<div ref="disclaimer"></div>
<q-dialog v-model="disclaimerDialog.show">
<q-card class="q-pa-lg">
<h6 class="q-my-md text-deep-purple">Warning</h6>
<p>
Login functionality to be released in v0.2, for now,
<strong
>make sure you bookmark this page for future access to your
wallet</strong
>!
</p>
<p>
This service is in BETA, and we hold no responsibility for people losing
access to funds. To encourage you to run your own LNbits installation,
any balance on {% raw %}{{ disclaimerDialog.location.host }}{% endraw %}
will incur a charge of
<strong>{{ service_fee }}% service fee</strong> per week.
</p>
<div class="row q-mt-lg">
<q-btn
outline
color="grey"
@click="copyText(disclaimerDialog.location.href)"
>Copy wallet URL</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>I understand</q-btn
>
</div>
</q-card>
</q-dialog>
{% endif %} {% endblock %}
</div>
+32 -15
View File
@@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Union
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
import httpx
from fastapi import Query, Request
from fastapi import Query, Request, Header
from fastapi.exceptions import HTTPException
from fastapi.param_functions import Depends
from fastapi.params import Body
@@ -23,25 +23,27 @@ from lnbits.decorators import (
WalletInvoiceKeyChecker,
WalletTypeInfo,
get_key_type,
require_admin_key
)
from lnbits.helpers import url_for
from lnbits.helpers import url_for, urlsafe_short_hash
from lnbits.requestvars import g
from lnbits.settings import LNBITS_ADMIN_USERS, LNBITS_SITE_TITLE
from lnbits.utils.exchange_rates import (
currencies,
fiat_amount_as_satoshis,
satoshis_amount_as_fiat,
)
from lnbits.settings import LNBITS_SITE_TITLE
from .. import core_app, db
from ..crud import (
create_payment,
get_payments,
get_standalone_payment,
save_balance_check,
update_wallet,
create_payment,
get_wallet,
get_wallet_for_key,
save_balance_check,
update_payment_status,
update_wallet,
)
from ..services import (
InvoiceFailure,
@@ -52,8 +54,6 @@ from ..services import (
perform_lnurlauth,
)
from ..tasks import api_invoice_listeners
from lnbits.settings import LNBITS_ADMIN_USERS
from lnbits.helpers import urlsafe_short_hash
@core_app.get("/api/v1/wallet")
@@ -125,7 +125,7 @@ async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
class CreateInvoiceData(BaseModel):
out: Optional[bool] = True
amount: float = Query(None, ge=0)
memo: str = None
memo: Optional[str] = None
unit: Optional[str] = "sat"
description_hash: Optional[str] = None
lnurl_callback: Optional[str] = None
@@ -364,7 +364,13 @@ async def api_payments_sse(
@core_app.get("/api/v1/payments/{payment_hash}")
async def api_payment(payment_hash):
async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
wallet = None
try:
if X_Api_Key.extra:
print("No key")
except:
wallet = await get_wallet_for_key(X_Api_Key)
payment = await get_standalone_payment(payment_hash)
await check_invoice_status(payment.wallet_id, payment_hash)
payment = await get_standalone_payment(payment_hash)
@@ -373,13 +379,19 @@ async def api_payment(payment_hash):
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
)
elif not payment.pending:
if wallet and wallet.id == payment.wallet_id:
return {"paid": True, "preimage": payment.preimage, "details": payment}
return {"paid": True, "preimage": payment.preimage}
try:
await payment.check_pending()
except Exception:
if wallet and wallet.id == payment.wallet_id:
return {"paid": False, "details": payment}
return {"paid": False}
if wallet and wallet.id == payment.wallet_id:
return {"paid": not payment.pending, "preimage": payment.preimage, "details": payment}
return {"paid": not payment.pending, "preimage": payment.preimage}
@@ -501,14 +513,19 @@ async def api_lnurlscan(code: str):
return params
class DecodePayment(BaseModel):
data: str
@core_app.post("/api/v1/payments/decode")
async def api_payments_decode(data: str = Query(None)):
async def api_payments_decode(data: DecodePayment):
payment_str = data.data
try:
if data["data"][:5] == "LNURL":
url = lnurl.decode(data["data"])
if payment_str[:5] == "LNURL":
url = lnurl.decode(payment_str)
return {"domain": url}
else:
invoice = bolt11.decode(data["data"])
invoice = bolt11.decode(payment_str)
return {
"payment_hash": invoice.payment_hash,
"amount_msat": invoice.amount_msat,
@@ -560,6 +577,6 @@ async def api_fiat_as_sats(data: ConversionData):
return output
else:
output[data.from_.upper()] = data.amount
output["sats"] = await fiat_amount_as_satoshis(data.amount, data.to)
output["sats"] = await fiat_amount_as_satoshis(data.amount, data.from_)
output["BTC"] = output["sats"] / 100000000
return output
+9 -3
View File
@@ -130,9 +130,15 @@ class Database(Compat):
)
)
else:
self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3")
database_uri = f"sqlite:///{self.path}"
self.type = SQLITE
if os.path.isdir(LNBITS_DATA_FOLDER):
self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3")
database_uri = f"sqlite:///{self.path}"
self.type = SQLITE
else:
raise NotADirectoryError(
f"LNBITS_DATA_FOLDER named {LNBITS_DATA_FOLDER} was not created"
f" - please 'mkdir {LNBITS_DATA_FOLDER}' and try again"
)
self.schema = self.name
if self.name.startswith("ext_"):
+1 -1
View File
@@ -147,7 +147,7 @@ async def get_key_type(
try:
checker = WalletInvoiceKeyChecker(api_key=token)
await checker.__call__(r)
wallet = WalletTypeInfo(0, checker.wallet)
wallet = WalletTypeInfo(1, checker.wallet)
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS):
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.")
return wallet
+1 -1
View File
@@ -88,7 +88,7 @@ async def get_copilot(copilot_id: str) -> Copilots:
async def get_copilots(user: str) -> List[Copilots]:
rows = await db.fetchall(
"SELECT * FROM copilot.newer_copilots WHERE user = ?", (user,)
'SELECT * FROM copilot.newer_copilots WHERE "user" = ?', (user,)
)
return [Copilots(**row) for row in rows]
+1 -1
View File
@@ -76,7 +76,7 @@ async def delete_ticket(payment_hash: str) -> None:
async def delete_event_tickets(event_id: str) -> None:
await db.execute("DELETE FROM events.tickets WHERE event = ?", (event_id,))
await db.execute("DELETE FROM events.ticket WHERE event = ?", (event_id,))
# EVENTS
+4 -3
View File
@@ -12,7 +12,7 @@ async def create_jukebox(
juke_id = urlsafe_short_hash()
result = await db.execute(
"""
INSERT INTO jukebox.jukebox (id, user, title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit)
INSERT INTO jukebox.jukebox (id, "user", title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
@@ -41,6 +41,7 @@ async def update_jukebox(
q = ", ".join([f"{field[0]} = ?" for field in data])
items = [f"{field[1]}" for field in data]
items.append(juke_id)
q = q.replace("user", '"user"', 1) # hack to make user be "user"!
await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items))
row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,))
return Jukebox(**row) if row else None
@@ -57,11 +58,11 @@ async def get_jukebox_by_user(user: str) -> Optional[Jukebox]:
async def get_jukeboxs(user: str) -> List[Jukebox]:
rows = await db.fetchall("SELECT * FROM jukebox.jukebox WHERE user = ?", (user,))
rows = await db.fetchall('SELECT * FROM jukebox.jukebox WHERE "user" = ?', (user,))
for row in rows:
if row.sp_playlists == None:
await delete_jukebox(row.id)
rows = await db.fetchall("SELECT * FROM jukebox.jukebox WHERE user = ?", (user,))
rows = await db.fetchall('SELECT * FROM jukebox.jukebox WHERE "user" = ?', (user,))
return [Jukebox(**row) for row in rows]
+2 -3
View File
@@ -75,7 +75,6 @@ async def api_check_credentials_check(
juke_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)
):
jukebox = await get_jukebox(juke_id)
return jukebox
@@ -442,7 +441,7 @@ async def api_get_jukebox_currently(
token = await api_get_token(juke_id)
if token == False:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="INvoice not paid"
status_code=HTTPStatus.FORBIDDEN, detail="Invoice not paid"
)
elif retry:
raise HTTPException(
@@ -456,5 +455,5 @@ async def api_get_jukebox_currently(
)
except:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Something went wrong"
status_code=HTTPStatus.NOT_FOUND, detail="Something went wrong, or no song is playing yet"
)
@@ -117,9 +117,10 @@
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }}
{{ col.label }}
</q-th>
</q-tr>
</template>
@@ -136,9 +137,19 @@
:href="'mailto:' + props.row.email"
></q-btn>
</q-td>
<q-td auto-width>
<q-btn
unelevated
dense
size="xs"
icon="launch"
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
@click="ticketCard(props)"
><q-tooltip> Click to show ticket </q-tooltip></q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
{{ col.label == "Ticket" ? col.value.length > 20 ? `${col.value.substring(0, 20)}...` : col.value : col.value }}
</q-td>
<q-td auto-width>
@@ -249,6 +260,29 @@
</q-form>
</q-card>
</q-dialog>
<!-- Read Ticket Dialog -->
<q-dialog v-model="ticketDialog.show" position="top">
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
{% raw %}
<q-card-section>
<h4 class="text-subtitle1 q-my-none">
<i>{{this.ticketDialog.data.name}}</i> sent a ticket
</h4>
<div v-if="this.ticketDialog.data.email">
<small>{{this.ticketDialog.data.email}}</small>
</div>
<small>{{this.ticketDialog.data.date}}</small>
</q-card-section>
<q-separator></q-separator>
<q-card-section>
<p>{{this.ticketDialog.data.content}}</p>
</q-card-section>
{% endraw %}
<q-card-actions align="right">
<q-btn flat label="CLOSE" color="primary" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
@@ -318,6 +352,10 @@
formDialog: {
show: false,
data: {flatrate: false}
},
ticketDialog: {
show: false,
data: {}
}
}
},
@@ -372,6 +410,16 @@
})
})
},
ticketCard(ticket){
this.ticketDialog.show = true
let {date, email, ltext, name} = ticket.row
this.ticketDialog.data = {
date,
email,
content: ltext,
name
}
},
exportticketsCSV: function () {
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
},
@@ -421,12 +469,13 @@
},
updateformDialog: function (formId) {
var link = _.findWhere(this.forms, {id: formId})
console.log("LINK", link)
this.formDialog.data.id = link.id
this.formDialog.data.wallet = link.wallet
this.formDialog.data.name = link.name
this.formDialog.data.description = link.description
this.formDialog.data.flatrate = link.flatrate
this.formDialog.data.flatrate = Boolean(link.flatrate)
this.formDialog.data.amount = link.amount
this.formDialog.show = true
},
+4
View File
@@ -103,6 +103,10 @@ async def api_ticket_make_ticket(data: CreateTicketData, form_id):
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail=f"LNTicket does not exist."
)
if data.sats < 1:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail=f"0 invoices not allowed."
)
nwords = len(re.split(r"\s+", data.ltext))
+5
View File
@@ -76,6 +76,11 @@ async def api_link_create_or_update(
link_id=None,
wallet: WalletTypeInfo = Depends(get_key_type),
):
if data.min < 1:
raise HTTPException(
detail="Min must be more than 1.", status_code=HTTPStatus.BAD_REQUEST
)
if data.min > data.max:
raise HTTPException(
detail="Min is greater than max.", status_code=HTTPStatus.BAD_REQUEST
@@ -64,7 +64,7 @@
label="lightning⚡"
>
<q-tooltip>
bitcoin onchain payment method not available
bitcoin lightning payment method not available
</q-tooltip>
</q-btn>
<q-btn
@@ -86,7 +86,7 @@
label="onchain⛓️"
>
<q-tooltip>
bitcoin lightning payment method not available
bitcoin onchain payment method not available
</q-tooltip>
</q-btn>
<q-btn
@@ -243,7 +243,7 @@
}
},
methods: {
startPaymentNotifier(){
startPaymentNotifier() {
this.cancelListener()
this.cancelListener = LNbits.event.onInvoicePaid(
+1 -1
View File
@@ -60,7 +60,7 @@ class Service(BaseModel):
onchain: Optional[str]
servicename: str # Currently, this will just always be "Streamlabs"
authenticated: bool # Whether a token (see below) has been acquired yet
token: Optional[int] # The token with which to authenticate requests
token: Optional[str] # The token with which to authenticate requests
@classmethod
def from_row(cls, row: Row) -> "Service":
@@ -62,7 +62,7 @@
donationDialog: {
show: false,
data: {
name: '',
name: null,
sats: '',
message: ''
}
+5 -3
View File
@@ -7,6 +7,7 @@ from starlette.responses import RedirectResponse
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.satspay.models import CreateCharge
from lnbits.extensions.streamalerts.models import (
CreateDonation,
CreateService,
@@ -113,17 +114,18 @@ async def api_create_donation(data: CreateDonation, request: Request):
service_id = data.service
service = await get_service(service_id)
charge_details = await get_charge_details(service.id)
name = data.name
name = data.name if data.name else "Anonymous"
description = f"{sats} sats donation from {name} to {service.twitchuser}"
charge = await create_charge(
create_charge_data = CreateCharge(
amount=sats,
completelink=f"https://twitch.tv/{service.twitchuser}",
completelinktext="Back to Stream!",
webhook=webhook_base + "/streamalerts/api/v1/postdonation",
description=description,
**charge_details,
**charge_details
)
charge = await create_charge(user=charge_details["user"], data=create_charge_data)
await create_donation(
id=charge.id,
wallet=service.wallet,
+3 -3
View File
@@ -1,5 +1,5 @@
from sqlite3 import Row
from typing import NamedTuple, Optional
from typing import Optional
from fastapi.param_functions import Query
from pydantic import BaseModel
@@ -26,7 +26,7 @@ class createTip(BaseModel):
message: str = ""
class Tip(NamedTuple):
class Tip(BaseModel):
"""A Tip represents a single donation"""
id: str # This ID always corresponds to a satspay charge ID
@@ -55,7 +55,7 @@ class createTips(BaseModel):
message: str
class TipJar(NamedTuple):
class TipJar(BaseModel):
"""A TipJar represents a user's tip jar"""
id: int
@@ -222,6 +222,7 @@
'INR',
'IQD',
'IRR',
'IRT',
'ISK',
'JEP',
'JMD',
+1 -1
View File
@@ -112,7 +112,7 @@ async def api_get_addresses(wallet_id, w: WalletTypeInfo = Depends(get_key_type)
async def api_update_mempool(
endpoint: str = Query(...), w: WalletTypeInfo = Depends(require_admin_key)
):
mempool = await update_mempool(endpoint, user=w.wallet.user)
mempool = await update_mempool(**{"endpoint": endpoint}, user=w.wallet.user)
return mempool.dict()
+5
View File
@@ -71,6 +71,11 @@ async def api_link_create_or_update(
link_id: str = None,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
if data.min_withdrawable < 1:
raise HTTPException(
detail="Min must be more than 1.", status_code=HTTPStatus.BAD_REQUEST
)
if data.max_withdrawable < data.min_withdrawable:
raise HTTPException(
detail="`max_withdrawable` needs to be at least `min_withdrawable`.",
+3
View File
@@ -160,6 +160,9 @@ def template_renderer(additional_folders: List = []) -> Jinja2Templates:
["lnbits/templates", "lnbits/core/templates", *additional_folders]
)
)
if settings.LNBITS_AD_SPACE:
t.env.globals["AD_SPACE"] = settings.LNBITS_AD_SPACE
t.env.globals["HIDE_API"] = settings.LNBITS_HIDE_API
t.env.globals["SITE_TITLE"] = settings.LNBITS_SITE_TITLE
t.env.globals["LNBITS_DENOMINATION"] = settings.LNBITS_DENOMINATION
t.env.globals["SITE_TAGLINE"] = settings.LNBITS_SITE_TAGLINE
+2
View File
@@ -34,6 +34,8 @@ LNBITS_DISABLED_EXTENSIONS: List[str] = env.list(
"LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str
)
LNBITS_AD_SPACE = env.list("LNBITS_AD_SPACE", default=[])
LNBITS_HIDE_API = env.bool("LNBITS_HIDE_API", default=False)
LNBITS_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits")
LNBITS_DENOMINATION = env.str("LNBITS_DENOMINATION", default="sats")
LNBITS_SITE_TAGLINE = env.str(
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

+7
View File
@@ -71,6 +71,7 @@ currencies = {
"IMP": "Isle of Man Pound",
"INR": "Indian Rupee",
"IQD": "Iraqi Dinar",
"IRT": "Iranian Toman",
"ISK": "Icelandic Króna",
"JEP": "Jersey Pound",
"JMD": "Jamaican Dollar",
@@ -179,6 +180,12 @@ class Provider(NamedTuple):
exchange_rate_providers = {
"exir": Provider(
"Exir",
"exir.io",
"https://api.exir.io/v1/ticker?symbol={from}-{to}",
lambda data, replacements: data["last"],
),
"bitfinex": Provider(
"Bitfinex",
"bitfinex.com",
-1
View File
@@ -2,7 +2,6 @@
from .void import VoidWallet
from .clightning import CLightningWallet
from .lndgrpc import LndWallet
from .lntxbot import LntxbotWallet
from .opennode import OpenNodeWallet
from .lnpay import LNPayWallet
+3 -1
View File
@@ -60,7 +60,9 @@ class Wallet(ABC):
pass
@abstractmethod
def pay_invoice(self, bolt11: str) -> Coroutine[None, None, PaymentResponse]:
def pay_invoice(
self, bolt11: str, fee_limit_msat: int
) -> Coroutine[None, None, PaymentResponse]:
pass
@abstractmethod
+13 -4
View File
@@ -18,6 +18,7 @@ from .base import (
Unsupported,
Wallet,
)
from lnbits import bolt11 as lnbits_bolt11
def async_wrap(func):
@@ -31,8 +32,8 @@ def async_wrap(func):
return run
def _pay_invoice(ln, bolt11):
return ln.pay(bolt11)
def _pay_invoice(ln, payload):
return ln.call("pay", payload)
def _paid_invoices_stream(ln, last_pay_index):
@@ -102,10 +103,18 @@ class CLightningWallet(Wallet):
error_message = f"lightningd '{exc.method}' failed with '{exc.error}'."
return InvoiceResponse(False, label, None, error_message)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
invoice = lnbits_bolt11.decode(bolt11)
fee_limit_percent = fee_limit_msat / invoice.amount_msat * 100
payload = {
"bolt11": bolt11,
"maxfeepercent": "{:.11}".format(fee_limit_percent),
"exemptfee": 0, # so fee_limit_percent is applied even on payments with fee under 5000 millisatoshi (which is default value of exemptfee)
}
try:
wrapped = async_wrap(_pay_invoice)
r = await wrapped(self.ln, bolt11)
r = await wrapped(self.ln, payload)
except RpcError as exc:
return PaymentResponse(False, None, 0, None, str(exc))
+22 -9
View File
@@ -36,7 +36,13 @@ class FakeWallet(Wallet):
"out": False,
"amount": amount,
"currency": "bc",
"privkey": hashlib.pbkdf2_hmac('sha256', secret.encode("utf-8"), ("FakeWallet").encode("utf-8"), 2048, 32).hex(),
"privkey": hashlib.pbkdf2_hmac(
"sha256",
secret.encode("utf-8"),
("FakeWallet").encode("utf-8"),
2048,
32,
).hex(),
"memo": None,
"description_hash": None,
"description": "",
@@ -53,22 +59,29 @@ class FakeWallet(Wallet):
data["tags_set"] = ["d"]
data["memo"] = memo
data["description"] = memo
randomHash = data["privkey"][:6] + hashlib.sha256(
str(random.getrandbits(256)).encode("utf-8")
).hexdigest()[6:]
randomHash = (
data["privkey"][:6]
+ hashlib.sha256(str(random.getrandbits(256)).encode("utf-8")).hexdigest()[
6:
]
)
data["paymenthash"] = randomHash
payment_request = encode(data)
checking_id = randomHash
return InvoiceResponse(True, checking_id, payment_request)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
invoice = decode(bolt11)
if hasattr(invoice, 'checking_id') and invoice.checking_id[6:] == data["privkey"][:6]:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
invoice = decode(bolt11)
if (
hasattr(invoice, "checking_id")
and invoice.checking_id[6:] == data["privkey"][:6]
):
return PaymentResponse(True, invoice.payment_hash, 0)
else:
return PaymentResponse(ok = False, error_message="Only internal invoices can be used!")
return PaymentResponse(
ok=False, error_message="Only internal invoices can be used!"
)
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
return PaymentStatus(False)
+1 -1
View File
@@ -80,7 +80,7 @@ class LNbitsWallet(Wallet):
return InvoiceResponse(ok, checking_id, payment_request, error_message)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
async with httpx.AsyncClient() as client:
r = await client.post(
url=f"{self.endpoint}/api/v1/payments",
+8 -7
View File
@@ -92,11 +92,12 @@ class LndWallet(Wallet):
or getenv("LND_GRPC_INVOICE_MACAROON")
or getenv("LND_INVOICE_MACAROON")
)
encrypted_macaroon = getenv("LND_GRPC_MACAROON_ENCRYPTED")
if encrypted_macaroon:
macaroon = AESCipher(description="macaroon decryption").decrypt(encrypted_macaroon)
macaroon = AESCipher(description="macaroon decryption").decrypt(
encrypted_macaroon
)
self.macaroon = load_macaroon(macaroon)
cert = open(self.cert_path, "rb").read()
@@ -143,10 +144,10 @@ class LndWallet(Wallet):
payment_request = str(resp.payment_request)
return InvoiceResponse(True, checking_id, payment_request, None)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
resp = await self.rpc.SendPayment(
lnrpc.SendPaymentRequest(payment_request=bolt11)
)
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000)
req = ln.SendRequest(payment_request=bolt11, fee_limit=fee_limit_fixed)
resp = await self.rpc.SendPaymentSync(req)
if resp.payment_error:
return PaymentResponse(False, "", 0, None, resp.payment_error)
+8 -9
View File
@@ -39,11 +39,13 @@ class LndRestWallet(Wallet):
encrypted_macaroon = getenv("LND_REST_MACAROON_ENCRYPTED")
if encrypted_macaroon:
macaroon = AESCipher(description="macaroon decryption").decrypt(encrypted_macaroon)
macaroon = AESCipher(description="macaroon decryption").decrypt(
encrypted_macaroon
)
self.macaroon = load_macaroon(macaroon)
self.auth = {"Grpc-Metadata-macaroon": self.macaroon}
self.cert = getenv("LND_REST_CERT")
self.cert = getenv("LND_REST_CERT", True)
async def status(self) -> StatusResponse:
try:
@@ -97,15 +99,11 @@ class LndRestWallet(Wallet):
return InvoiceResponse(True, checking_id, payment_request, None)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
async with httpx.AsyncClient(verify=self.cert) as client:
# set the fee limit for the payment
invoice = lnbits_bolt11.decode(bolt11)
lnrpcFeeLimit = dict()
if invoice.amount_msat > 1000_000:
lnrpcFeeLimit["percent"] = "1" # in percent
else:
lnrpcFeeLimit["fixed"] = "10" # in sat
lnrpcFeeLimit["fixed_msat"] = "{}".format(fee_limit_msat)
r = await client.post(
url=f"{self.endpoint}/v1/channels/transactions",
@@ -162,6 +160,7 @@ class LndRestWallet(Wallet):
# for some reason our checking_ids are in base64 but the payment hashes
# returned here are in hex, lnd is weird
checking_id = checking_id.replace("_", "/")
checking_id = base64.b64decode(checking_id).hex()
for p in r.json()["payments"]:
+1 -1
View File
@@ -76,7 +76,7 @@ class LNPayWallet(Wallet):
return InvoiceResponse(ok, checking_id, payment_request, error_message)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
async with httpx.AsyncClient() as client:
r = await client.post(
f"{self.endpoint}/wallet/{self.wallet_key}/withdraw",
+1 -1
View File
@@ -74,7 +74,7 @@ class LntxbotWallet(Wallet):
data = r.json()
return InvoiceResponse(True, data["payment_hash"], data["pay_req"], None)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
async with httpx.AsyncClient() as client:
r = await client.post(
f"{self.endpoint}/payinvoice",
+1 -1
View File
@@ -77,7 +77,7 @@ class OpenNodeWallet(Wallet):
payment_request = data["lightning_invoice"]["payreq"]
return InvoiceResponse(True, checking_id, payment_request, None)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
async with httpx.AsyncClient() as client:
r = await client.post(
f"{self.endpoint}/v2/withdrawals",
+1 -1
View File
@@ -107,7 +107,7 @@ class SparkWallet(Wallet):
return InvoiceResponse(ok, checking_id, payment_request, error_message)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
try:
r = await self.pay(bolt11)
except (SparkError, UnknownError) as exc:
+1 -1
View File
@@ -25,7 +25,7 @@ class VoidWallet(Wallet):
)
return StatusResponse(None, 0)
async def pay_invoice(self, bolt11: str) -> PaymentResponse:
async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
raise Unsupported("")
async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
+11 -4
View File
@@ -3,7 +3,10 @@ import secrets
from lnbits.core.crud import get_wallet
from lnbits.settings import HOST, PORT
from lnbits.extensions.bleskomat.crud import get_bleskomat_lnurl
from lnbits.extensions.bleskomat.helpers import generate_bleskomat_lnurl_signature, query_to_signing_payload
from lnbits.extensions.bleskomat.helpers import (
generate_bleskomat_lnurl_signature,
query_to_signing_payload,
)
from tests.conftest import client
from tests.helpers import credit_wallet
from tests.extensions.bleskomat.conftest import bleskomat, lnurl
@@ -73,7 +76,9 @@ async def test_bleskomat_lnurl_api_valid_signature(client, bleskomat):
}
payload = query_to_signing_payload(query)
signature = generate_bleskomat_lnurl_signature(
payload=payload, api_key_secret=bleskomat.api_key_secret, api_key_encoding=bleskomat.api_key_encoding
payload=payload,
api_key_secret=bleskomat.api_key_secret,
api_key_encoding=bleskomat.api_key_encoding,
)
response = await client.get(f"/bleskomat/u?{payload}&signature={signature}")
assert response.status_code == 200
@@ -97,7 +102,9 @@ async def test_bleskomat_lnurl_api_action_insufficient_balance(client, lnurl):
response = await client.get(f"/bleskomat/u?k1={secret}&pr={pr}")
assert response.status_code == 200
assert response.json()["status"] == "ERROR"
assert ("Insufficient balance" in response.json()["reason"]) or ("fee" in response.json()["reason"])
assert ("Insufficient balance" in response.json()["reason"]) or (
"fee" in response.json()["reason"]
)
wallet = await get_wallet(bleskomat.wallet)
assert wallet.balance_msat == 0
bleskomat_lnurl = await get_bleskomat_lnurl(secret)
@@ -123,4 +130,4 @@ async def test_bleskomat_lnurl_api_action_success(client, lnurl):
assert wallet.balance_msat == 50000
bleskomat_lnurl = await get_bleskomat_lnurl(secret)
assert bleskomat_lnurl.has_uses_remaining() == False
WALLET.pay_invoice.assert_called_once_with(pr)
WALLET.pay_invoice.assert_called_once_with(pr, 2000)
+35 -21
View File
@@ -9,28 +9,42 @@ from lnbits.wallets.base import (
)
from lnbits.settings import WALLET
WALLET.status = AsyncMock(return_value=StatusResponse(
"",# no error
1000000,# msats
))
WALLET.create_invoice = AsyncMock(return_value=InvoiceResponse(
True,# ok
"6621aafbdd7709ca6eea6203f362d64bd7cb2911baa91311a176b3ecaf2274bd",# checking_id (i.e. payment_hash)
"lntb1u1psezhgspp5vcs6477awuyu5mh2vgplxckkf0tuk2g3h253xydpw6e7etezwj7sdqqcqzpgxqyz5vqsp5dxpw8zs77hw5pla4wz4mfujllyxtlpu443auur2uxqdrs8q2h56q9qyyssq65zk30ylmygvv5y4tuwalnf3ttnqjn57ef6rmcqg0s53akem560jh8ptemjcmytn3lrlatw4hv9smg88exv3v4f4lqnp96s0psdrhxsp6pp75q",# payment_request
"",# no error
))
def pay_invoice_side_effect(payment_request: str):
WALLET.status = AsyncMock(
return_value=StatusResponse(
"", # no error
1000000, # msats
)
)
WALLET.create_invoice = AsyncMock(
return_value=InvoiceResponse(
True, # ok
"6621aafbdd7709ca6eea6203f362d64bd7cb2911baa91311a176b3ecaf2274bd", # checking_id (i.e. payment_hash)
"lntb1u1psezhgspp5vcs6477awuyu5mh2vgplxckkf0tuk2g3h253xydpw6e7etezwj7sdqqcqzpgxqyz5vqsp5dxpw8zs77hw5pla4wz4mfujllyxtlpu443auur2uxqdrs8q2h56q9qyyssq65zk30ylmygvv5y4tuwalnf3ttnqjn57ef6rmcqg0s53akem560jh8ptemjcmytn3lrlatw4hv9smg88exv3v4f4lqnp96s0psdrhxsp6pp75q", # payment_request
"", # no error
)
)
def pay_invoice_side_effect(
payment_request: str, fee_limit_msat: int
) -> PaymentResponse:
invoice = bolt11.decode(payment_request)
return PaymentResponse(
True,# ok
invoice.payment_hash,# checking_id (i.e. payment_hash)
0,# fee_msat
"",# no error
True, # ok
invoice.payment_hash, # checking_id (i.e. payment_hash)
0, # fee_msat
"", # no error
)
WALLET.pay_invoice = AsyncMock(side_effect=pay_invoice_side_effect)
WALLET.get_invoice_status = AsyncMock(return_value=PaymentStatus(
True,# paid
))
WALLET.get_payment_status = AsyncMock(return_value=PaymentStatus(
True,# paid
))
WALLET.get_invoice_status = AsyncMock(
return_value=PaymentStatus(
True, # paid
)
)
WALLET.get_payment_status = AsyncMock(
return_value=PaymentStatus(
True, # paid
)
)