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_ADMIN_EXTENSIONS="ngrok"
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" 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 # Disable extensions for all users, use "all" to disable all extensions
LNBITS_DISABLED_EXTENSIONS="amilk" 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 # Choose from mint, flamingo, salvador, autumn, monochrome, classic
LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, mint, autumn, monochrome, salvador" 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 # LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet
LNBITS_BACKEND_WALLET_CLASS=VoidWallet LNBITS_BACKEND_WALLET_CLASS=VoidWallet
# VoidWallet is just a fallback that works without any actual Lightning capabilities, # 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_ENDPOINT=https://legend.lnbits.com
LNBITS_KEY=LNBITS_ADMIN_KEY 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 # LndRestWallet
LND_REST_ENDPOINT=https://127.0.0.1:8080/ 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" 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: actions/checkout@v1
- uses: jpetrucciani/mypy-check@master - uses: jpetrucciani/mypy-check@master
with: with:
mypy_flags: '--install-types --non-interactive'
path: lnbits 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 # Install build deps
RUN apt-get update 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 python -m pip install --upgrade pip
RUN pip install wheel
# Install runtime deps # Install runtime deps
COPY requirements.txt /tmp/requirements.txt COPY requirements.txt /tmp/requirements.txt
@@ -36,6 +37,9 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
WORKDIR /app WORKDIR /app
COPY --chown=1000:1000 lnbits /app/lnbits COPY --chown=1000:1000 lnbits /app/lnbits
ENV LNBITS_PORT="5000"
ENV LNBITS_HOST="0.0.0.0"
EXPOSE 5000 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)) (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! 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: 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 ```sh
git clone https://github.com/lnbits/lnbits-legend.git git clone https://github.com/lnbits/lnbits-legend.git
cd lnbits-legend/ cd lnbits-legend/
sudo apt-get install pipenv
pipenv shell pipenv shell
# pipenv --python 3.9 shell (if you wish to use a version of Python higher than 3.7) # pipenv --python 3.9 shell (if you wish to use a version of Python higher than 3.7)
pipenv install --dev 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 # If any of the modules fails to install, try checking and upgrading your setupTool module
# pip install -U setuptools # pip install -U setuptools
# install libffi/libpq in case "pipenv install" fails
# sudo apt-get install -y libffi-dev libpq-dev
``` ```
## Running the server ## 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: def check_funding_source(app: FastAPI) -> None:
@app.on_event("startup") @app.on_event("startup")
async def check_wallet_status(): async def check_wallet_status():
error_message, balance = await WALLET.status() while True:
if error_message: error_message, balance = await WALLET.status()
if not error_message:
break
warnings.warn( warnings.warn(
f" × The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'", f" × The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
RuntimeWarning, RuntimeWarning,
) )
print("Retrying connection to backend in 5 seconds...")
sys.exit(4) await asyncio.sleep(5)
else: print(
print( f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat."
f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat." )
)
def register_routes(app: FastAPI) -> None: def register_routes(app: FastAPI) -> None:
+19 -23
View File
@@ -85,18 +85,17 @@ async def pay_invoice(
description: str = "", description: str = "",
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> str: ) -> 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: async with (db.reuse_conn(conn) if conn else db.connect()) as conn:
temp_id = f"temp_{urlsafe_short_hash()}" temp_id = f"temp_{urlsafe_short_hash()}"
internal_id = f"internal_{urlsafe_short_hash()}" internal_id = f"internal_{urlsafe_short_hash()}"
invoice = bolt11.decode(payment_request)
if invoice.amount_msat == 0: if invoice.amount_msat == 0:
raise ValueError("Amountless invoices not supported.") raise ValueError("Amountless invoices not supported.")
if max_sat and invoice.amount_msat > max_sat * 1000: if max_sat and invoice.amount_msat > max_sat * 1000:
raise ValueError("Amount in invoice is too high.") raise ValueError("Amount in invoice is too high.")
wallet = await get_wallet(wallet_id, conn=conn)
# put all parameters that don't change here # put all parameters that don't change here
PaymentKwargs = TypedDict( PaymentKwargs = TypedDict(
"PaymentKwargs", "PaymentKwargs",
@@ -134,26 +133,20 @@ async def pay_invoice(
# the balance is enough in the next step # the balance is enough in the next step
await create_payment( await create_payment(
checking_id=temp_id, checking_id=temp_id,
fee=-fee_reserve(invoice.amount_msat), fee=-fee_reserve_msat,
conn=conn, conn=conn,
**payment_kwargs, **payment_kwargs,
) )
# do the balance check if internal payment # do the balance check
if internal_checking_id: wallet = await get_wallet(wallet_id, conn=conn)
wallet = await get_wallet(wallet_id, conn=conn) assert wallet
assert wallet if wallet.balance_msat < 0:
if wallet.balance_msat < 0: if not internal_checking_id and wallet.balance_msat > -fee_reserve_msat:
raise PermissionError("Insufficient balance.") raise PaymentFailure(
f"You must reserve at least 1% ({round(fee_reserve_msat/1000)} sat) to cover potential routing fees."
# 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."
) )
raise PermissionError("Insufficient balance.")
if internal_checking_id: if internal_checking_id:
# mark the invoice from the other side as not pending anymore # 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) await internal_invoice_queue.put(internal_checking_id)
else: else:
# actually pay the external invoice # 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: if payment.checking_id:
async with db.connect() as conn: async with db.connect() as conn:
await create_payment( await create_payment(
@@ -286,12 +281,12 @@ async def perform_lnurlauth(
sign_len = 6 + r_len + s_len sign_len = 6 + r_len + s_len
signature = BytesIO() 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((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_len.to_bytes(1, "big", signed=False))
signature.write(r) 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_len.to_bytes(1, "big", signed=False))
signature.write(s) signature.write(s)
@@ -340,5 +335,6 @@ async def check_invoice_status(
return 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: 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 () { decodeRequest: function () {
this.parse.show = true this.parse.show = true
let req = this.parse.data.request.toLowerCase()
if (this.parse.data.request.startsWith('lightning:')) { if (this.parse.data.request.startsWith('lightning:')) {
this.parse.data.request = this.parse.data.request.slice(10) this.parse.data.request = this.parse.data.request.slice(10)
} else if (this.parse.data.request.startsWith('lnurl:')) { } else if (this.parse.data.request.startsWith('lnurl:')) {
this.parse.data.request = this.parse.data.request.slice(6) 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 this.parse.data.request = this.parse.data.request
.split('lightning=')[1] .split('lightning=')[1]
.split('&')[0] .split('&')[0]
+439 -413
View File
@@ -273,444 +273,470 @@
</q-card-section> </q-card-section>
</q-card> </q-card>
</div> </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> {% if HIDE_API %}
{% include "core/_api_docs.html" %} <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> <q-separator></q-separator>
{% if wallet.lnurlwithdraw_full %} <q-list>
<q-expansion-item group="extras" icon="crop_free" label="Drain Funds"> {% include "core/_api_docs.html" %}
<q-card> <q-separator></q-separator>
<q-card-section class="text-center">
<p> {% if wallet.lnurlwithdraw_full %}
This is an LNURL-withdraw QR code for slurping everything from <q-expansion-item
this wallet. Do not share with anyone. group="extras"
</p> icon="crop_free"
<a href="lightning:{{wallet.lnurlwithdraw_full}}"> 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 <qrcode
value="{{wallet.lnurlwithdraw_full}}" :value="'{{request.base_url}}' +'wallet?usr={{user.id}}&wal={{wallet.id}}'"
:options="{width:240}" :options="{width:240}"
></qrcode> ></qrcode>
</a> </q-card-section>
<p> </q-card>
It is compatible with <code>balanceCheck</code> and </q-expansion-item>
<code>balanceNotify</code> so your wallet may keep pulling the <q-separator></q-separator>
funds continuously from here after the first withdraw. <q-expansion-item group="extras" icon="edit" label="Rename wallet">
</p> <q-card>
</q-card-section> <q-card-section>
</q-card> <div class="" style="max-width: 320px">
</q-expansion-item> <q-input
<q-separator></q-separator> filled
{% endif %} v-model.trim="newName"
label="Label"
<q-expansion-item dense="dense"
group="extras" @update:model-value="(e) => console.log(e)"
icon="settings_cell" />
label="Export to Phone with QR Code" </div>
> <q-btn
<q-card> :disable="!newName.length"
<q-card-section class="text-center"> unelevated
<p> class="q-mt-sm"
This QR code contains your wallet URL with full access. You color="primary"
can scan it from your phone to open your wallet from there. @click="updateWalletName()"
</p> >Update name</q-btn
<qrcode >
:value="'{{request.base_url}}' +'wallet?usr={{user.id}}&wal={{wallet.id}}'" </q-card-section>
:options="{width:240}" </q-card>
></qrcode> </q-expansion-item>
</q-card-section> <q-separator></q-separator>
</q-card> <q-expansion-item
</q-expansion-item> group="extras"
<q-separator></q-separator> icon="remove_circle"
<q-expansion-item group="extras" icon="edit" label="Rename wallet"> label="Delete wallet"
<q-card> >
<q-card-section> <q-card>
<div class="" style="max-width: 320px"> <q-card-section>
<q-input <p>
filled This whole wallet will be deleted, the funds will be
v-model.trim="newName" <strong>UNRECOVERABLE</strong>.
label="Label" </p>
dense="dense" <q-btn
@update:model-value="(e) => console.log(e)" unelevated
/> color="red-10"
</div> @click="deleteWallet('{{ wallet.id }}', '{{ user.id }}')"
<q-btn >Delete wallet</q-btn
:disable="!newName.length" >
unelevated </q-card-section>
class="q-mt-sm" </q-card>
color="primary" </q-expansion-item>
@click="updateWalletName()" </q-list>
>Update name</q-btn </q-card-section>
> </q-card>
</q-card-section> {% endif %} {% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD =
</q-card> ADS.split(';') %}
</q-expansion-item> <q-card>
<q-separator></q-separator> <a href="{{ AD[0] }}"
<q-expansion-item ><img width="100%" src="{{ AD[1] }}"
group="extras" /></a> </q-card
icon="remove_circle" >{% endfor %} {% endif %}
label="Delete wallet" </div>
>
<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>
</div> </div>
</div>
<q-dialog v-model="receive.show" @hide="closeReceiveDialog"> <q-dialog v-model="receive.show" @hide="closeReceiveDialog">
{% raw %} {% raw %}
<q-card <q-card
v-if="!receive.paymentReq" v-if="!receive.paymentReq"
class="q-pa-lg q-pt-xl lnbits__dialog-card" class="q-pa-lg q-pt-xl lnbits__dialog-card"
> >
<q-form @submit="createInvoice" class="q-gutter-md"> <q-form @submit="createInvoice" class="q-gutter-md">
<p v-if="receive.lnurl" class="text-h6 text-center q-my-none"> <p v-if="receive.lnurl" class="text-h6 text-center q-my-none">
<b>{{receive.lnurl.domain}}</b> is requesting an invoice: <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>?
</p> </p>
<q-separator class="q-my-sm"></q-separator> {% endraw %} {% if LNBITS_DENOMINATION != 'sats' %}
<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"
>
<q-input <q-input
filled filled
dense dense
v-model.trim="parse.data.request" v-model.number="receive.data.amount"
type="textarea" label="Amount ({{LNBITS_DENOMINATION}}) *"
label="Paste an invoice, payment request or lnurl code *" mask="#.##"
> fill-mask="0"
</q-input> reverse-fill-mask
<div class="row q-mt-lg"> :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 <q-btn
unelevated unelevated
color="primary" color="primary"
:disable="parse.data.request == ''" :disable="receive.data.amount == null || receive.data.amount <= 0"
type="submit" 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" <q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn >Cancel</q-btn
> >
</div> </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> <div v-else>
<q-responsive :ratio="1"> <q-form
<qrcode-stream v-if="!parse.camera.show"
@decode="decodeQR" @submit="decodeRequest"
class="rounded-borders" class="q-gutter-md"
></qrcode-stream> >
</q-responsive> <q-input
<div class="row q-mt-lg"> filled
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"> dense
Cancel v-model.trim="parse.data.request"
</q-btn> 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> </div>
</div> </q-card>
</q-card> </q-dialog>
</q-dialog>
<q-dialog v-model="parse.camera.show"> <q-dialog v-model="parse.camera.show">
<q-card class="q-pa-lg q-pt-xl"> <q-card class="q-pa-lg q-pt-xl">
<div class="text-center q-mb-lg"> <div class="text-center q-mb-lg">
<qrcode-stream @decode="decodeQR" class="rounded-borders"></qrcode-stream> <qrcode-stream
</div> @decode="decodeQR"
<div class="row q-mt-lg"> class="rounded-borders"
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto" ></qrcode-stream>
>Cancel</q-btn </div>
> <div class="row q-mt-lg">
</div> <q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
</q-card> >Cancel</q-btn
</q-dialog> >
</div>
</q-card>
</q-dialog>
<q-dialog v-model="paymentsChart.show"> <q-dialog v-model="paymentsChart.show">
<q-card class="q-pa-sm" style="width: 800px; max-width: unset"> <q-card class="q-pa-sm" style="width: 800px; max-width: unset">
<q-card-section> <q-card-section>
<canvas ref="canvas" width="600" height="400"></canvas> <canvas ref="canvas" width="600" height="400"></canvas>
</q-card-section> </q-card-section>
</q-card> </q-card>
</q-dialog> </q-dialog>
<q-tabs <q-tabs
class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-max" class="lt-md fixed-bottom left-0 right-0 bg-primary text-white shadow-2 z-max"
active-class="px-0" active-class="px-0"
indicator-color="transparent" indicator-color="transparent"
>
<q-tab
icon="account_balance_wallet"
label="Wallets"
@click="g.visibleDrawer = !g.visibleDrawer"
> >
</q-tab> <q-tab
<q-tab icon="content_paste" label="Paste" @click="showParseDialog"> </q-tab> icon="account_balance_wallet"
<q-tab icon="file_download" label="Receive" @click="showReceiveDialog"> label="Wallets"
</q-tab> @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-tab icon="photo_camera" label="Scan" @click="showCamera"> </q-tab>
</q-tabs> </q-tabs>
{% if service_fee > 0 %} {% if service_fee > 0 %}
<div ref="disclaimer"></div> <div ref="disclaimer"></div>
<q-dialog v-model="disclaimerDialog.show"> <q-dialog v-model="disclaimerDialog.show">
<q-card class="q-pa-lg"> <q-card class="q-pa-lg">
<h6 class="q-my-md text-deep-purple">Warning</h6> <h6 class="q-my-md text-deep-purple">Warning</h6>
<p> <p>
Login functionality to be released in v0.2, for now, Login functionality to be released in v0.2, for now,
<strong <strong
>make sure you bookmark this page for future access to your >make sure you bookmark this page for future access to your
wallet</strong wallet</strong
>! >!
</p> </p>
<p> <p>
This service is in BETA, and we hold no responsibility for people losing 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 access to funds. To encourage you to run your own LNbits installation,
balance on {% raw %}{{ disclaimerDialog.location.host }}{% endraw %} will any balance on {% raw %}{{ disclaimerDialog.location.host }}{% endraw %}
incur a charge of <strong>{{ service_fee }}% service fee</strong> per will incur a charge of
week. <strong>{{ service_fee }}% service fee</strong> per week.
</p> </p>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn <q-btn
outline outline
color="grey" color="grey"
@click="copyText(disclaimerDialog.location.href)" @click="copyText(disclaimerDialog.location.href)"
>Copy wallet URL</q-btn >Copy wallet URL</q-btn
> >
<q-btn v-close-popup flat color="grey" class="q-ml-auto" <q-btn v-close-popup flat color="grey" class="q-ml-auto"
>I understand</q-btn >I understand</q-btn
> >
</div> </div>
</q-card> </q-card>
</q-dialog> </q-dialog>
{% endif %} {% endblock %} {% 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 from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
import httpx import httpx
from fastapi import Query, Request from fastapi import Query, Request, Header
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from fastapi.param_functions import Depends from fastapi.param_functions import Depends
from fastapi.params import Body from fastapi.params import Body
@@ -23,25 +23,27 @@ from lnbits.decorators import (
WalletInvoiceKeyChecker, WalletInvoiceKeyChecker,
WalletTypeInfo, WalletTypeInfo,
get_key_type, 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.requestvars import g
from lnbits.settings import LNBITS_ADMIN_USERS, LNBITS_SITE_TITLE
from lnbits.utils.exchange_rates import ( from lnbits.utils.exchange_rates import (
currencies, currencies,
fiat_amount_as_satoshis, fiat_amount_as_satoshis,
satoshis_amount_as_fiat, satoshis_amount_as_fiat,
) )
from lnbits.settings import LNBITS_SITE_TITLE
from .. import core_app, db from .. import core_app, db
from ..crud import ( from ..crud import (
create_payment,
get_payments, get_payments,
get_standalone_payment, get_standalone_payment,
save_balance_check,
update_wallet,
create_payment,
get_wallet, get_wallet,
get_wallet_for_key,
save_balance_check,
update_payment_status, update_payment_status,
update_wallet,
) )
from ..services import ( from ..services import (
InvoiceFailure, InvoiceFailure,
@@ -52,8 +54,6 @@ from ..services import (
perform_lnurlauth, perform_lnurlauth,
) )
from ..tasks import api_invoice_listeners 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") @core_app.get("/api/v1/wallet")
@@ -125,7 +125,7 @@ async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
class CreateInvoiceData(BaseModel): class CreateInvoiceData(BaseModel):
out: Optional[bool] = True out: Optional[bool] = True
amount: float = Query(None, ge=0) amount: float = Query(None, ge=0)
memo: str = None memo: Optional[str] = None
unit: Optional[str] = "sat" unit: Optional[str] = "sat"
description_hash: Optional[str] = None description_hash: Optional[str] = None
lnurl_callback: 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}") @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) payment = await get_standalone_payment(payment_hash)
await check_invoice_status(payment.wallet_id, payment_hash) await check_invoice_status(payment.wallet_id, payment_hash)
payment = await get_standalone_payment(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." status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
) )
elif not payment.pending: 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} return {"paid": True, "preimage": payment.preimage}
try: try:
await payment.check_pending() await payment.check_pending()
except Exception: except Exception:
if wallet and wallet.id == payment.wallet_id:
return {"paid": False, "details": payment}
return {"paid": False} 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} return {"paid": not payment.pending, "preimage": payment.preimage}
@@ -501,14 +513,19 @@ async def api_lnurlscan(code: str):
return params return params
class DecodePayment(BaseModel):
data: str
@core_app.post("/api/v1/payments/decode") @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: try:
if data["data"][:5] == "LNURL": if payment_str[:5] == "LNURL":
url = lnurl.decode(data["data"]) url = lnurl.decode(payment_str)
return {"domain": url} return {"domain": url}
else: else:
invoice = bolt11.decode(data["data"]) invoice = bolt11.decode(payment_str)
return { return {
"payment_hash": invoice.payment_hash, "payment_hash": invoice.payment_hash,
"amount_msat": invoice.amount_msat, "amount_msat": invoice.amount_msat,
@@ -560,6 +577,6 @@ async def api_fiat_as_sats(data: ConversionData):
return output return output
else: else:
output[data.from_.upper()] = data.amount 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 output["BTC"] = output["sats"] / 100000000
return output return output
+9 -3
View File
@@ -130,9 +130,15 @@ class Database(Compat):
) )
) )
else: else:
self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3") if os.path.isdir(LNBITS_DATA_FOLDER):
database_uri = f"sqlite:///{self.path}" self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3")
self.type = SQLITE 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 self.schema = self.name
if self.name.startswith("ext_"): if self.name.startswith("ext_"):
+1 -1
View File
@@ -147,7 +147,7 @@ async def get_key_type(
try: try:
checker = WalletInvoiceKeyChecker(api_key=token) checker = WalletInvoiceKeyChecker(api_key=token)
await checker.__call__(r) 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): 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.") raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.")
return wallet 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]: async def get_copilots(user: str) -> List[Copilots]:
rows = await db.fetchall( 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] 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: 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 # EVENTS
+4 -3
View File
@@ -12,7 +12,7 @@ async def create_jukebox(
juke_id = urlsafe_short_hash() juke_id = urlsafe_short_hash()
result = await db.execute( 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
@@ -41,6 +41,7 @@ async def update_jukebox(
q = ", ".join([f"{field[0]} = ?" for field in data]) q = ", ".join([f"{field[0]} = ?" for field in data])
items = [f"{field[1]}" for field in data] items = [f"{field[1]}" for field in data]
items.append(juke_id) 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)) await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items))
row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,)) row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,))
return Jukebox(**row) if row else None 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]: 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: for row in rows:
if row.sp_playlists == None: if row.sp_playlists == None:
await delete_jukebox(row.id) 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] 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) juke_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key)
): ):
jukebox = await get_jukebox(juke_id) jukebox = await get_jukebox(juke_id)
return jukebox return jukebox
@@ -442,7 +441,7 @@ async def api_get_jukebox_currently(
token = await api_get_token(juke_id) token = await api_get_token(juke_id)
if token == False: if token == False:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail="INvoice not paid" status_code=HTTPStatus.FORBIDDEN, detail="Invoice not paid"
) )
elif retry: elif retry:
raise HTTPException( raise HTTPException(
@@ -456,5 +455,5 @@ async def api_get_jukebox_currently(
) )
except: except:
raise HTTPException( 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 %} {% raw %}
<template v-slot:header="props"> <template v-slot:header="props">
<q-tr :props="props"> <q-tr :props="props">
<q-th auto-width></q-th>
<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"> <q-th v-for="col in props.cols" :key="col.name" :props="props">
{{ col.label }} {{ col.label }}
</q-th> </q-th>
</q-tr> </q-tr>
</template> </template>
@@ -136,9 +137,19 @@
:href="'mailto:' + props.row.email" :href="'mailto:' + props.row.email"
></q-btn> ></q-btn>
</q-td> </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"> <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>
<q-td auto-width> <q-td auto-width>
@@ -249,6 +260,29 @@
</q-form> </q-form>
</q-card> </q-card>
</q-dialog> </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> </div>
{% endblock %} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block scripts %} {{ window_vars(user) }}
<script> <script>
@@ -318,6 +352,10 @@
formDialog: { formDialog: {
show: false, show: false,
data: {flatrate: 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 () { exportticketsCSV: function () {
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets) LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
}, },
@@ -421,12 +469,13 @@
}, },
updateformDialog: function (formId) { updateformDialog: function (formId) {
var link = _.findWhere(this.forms, {id: formId}) var link = _.findWhere(this.forms, {id: formId})
console.log("LINK", link)
this.formDialog.data.id = link.id this.formDialog.data.id = link.id
this.formDialog.data.wallet = link.wallet this.formDialog.data.wallet = link.wallet
this.formDialog.data.name = link.name this.formDialog.data.name = link.name
this.formDialog.data.description = link.description 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.data.amount = link.amount
this.formDialog.show = true this.formDialog.show = true
}, },
+4
View File
@@ -103,6 +103,10 @@ async def api_ticket_make_ticket(data: CreateTicketData, form_id):
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail=f"LNTicket does not exist." 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)) 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, link_id=None,
wallet: WalletTypeInfo = Depends(get_key_type), 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: if data.min > data.max:
raise HTTPException( raise HTTPException(
detail="Min is greater than max.", status_code=HTTPStatus.BAD_REQUEST detail="Min is greater than max.", status_code=HTTPStatus.BAD_REQUEST
@@ -64,7 +64,7 @@
label="lightning⚡" label="lightning⚡"
> >
<q-tooltip> <q-tooltip>
bitcoin onchain payment method not available bitcoin lightning payment method not available
</q-tooltip> </q-tooltip>
</q-btn> </q-btn>
<q-btn <q-btn
@@ -86,7 +86,7 @@
label="onchain⛓️" label="onchain⛓️"
> >
<q-tooltip> <q-tooltip>
bitcoin lightning payment method not available bitcoin onchain payment method not available
</q-tooltip> </q-tooltip>
</q-btn> </q-btn>
<q-btn <q-btn
@@ -243,7 +243,7 @@
} }
}, },
methods: { methods: {
startPaymentNotifier(){ startPaymentNotifier() {
this.cancelListener() this.cancelListener()
this.cancelListener = LNbits.event.onInvoicePaid( this.cancelListener = LNbits.event.onInvoicePaid(
+1 -1
View File
@@ -60,7 +60,7 @@ class Service(BaseModel):
onchain: Optional[str] onchain: Optional[str]
servicename: str # Currently, this will just always be "Streamlabs" servicename: str # Currently, this will just always be "Streamlabs"
authenticated: bool # Whether a token (see below) has been acquired yet 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 @classmethod
def from_row(cls, row: Row) -> "Service": def from_row(cls, row: Row) -> "Service":
@@ -62,7 +62,7 @@
donationDialog: { donationDialog: {
show: false, show: false,
data: { data: {
name: '', name: null,
sats: '', sats: '',
message: '' message: ''
} }
+5 -3
View File
@@ -7,6 +7,7 @@ from starlette.responses import RedirectResponse
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type from lnbits.decorators import WalletTypeInfo, get_key_type
from lnbits.extensions.satspay.models import CreateCharge
from lnbits.extensions.streamalerts.models import ( from lnbits.extensions.streamalerts.models import (
CreateDonation, CreateDonation,
CreateService, CreateService,
@@ -113,17 +114,18 @@ async def api_create_donation(data: CreateDonation, request: Request):
service_id = data.service service_id = data.service
service = await get_service(service_id) service = await get_service(service_id)
charge_details = await get_charge_details(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}" description = f"{sats} sats donation from {name} to {service.twitchuser}"
charge = await create_charge( create_charge_data = CreateCharge(
amount=sats, amount=sats,
completelink=f"https://twitch.tv/{service.twitchuser}", completelink=f"https://twitch.tv/{service.twitchuser}",
completelinktext="Back to Stream!", completelinktext="Back to Stream!",
webhook=webhook_base + "/streamalerts/api/v1/postdonation", webhook=webhook_base + "/streamalerts/api/v1/postdonation",
description=description, description=description,
**charge_details, **charge_details
) )
charge = await create_charge(user=charge_details["user"], data=create_charge_data)
await create_donation( await create_donation(
id=charge.id, id=charge.id,
wallet=service.wallet, wallet=service.wallet,
+3 -3
View File
@@ -1,5 +1,5 @@
from sqlite3 import Row from sqlite3 import Row
from typing import NamedTuple, Optional from typing import Optional
from fastapi.param_functions import Query from fastapi.param_functions import Query
from pydantic import BaseModel from pydantic import BaseModel
@@ -26,7 +26,7 @@ class createTip(BaseModel):
message: str = "" message: str = ""
class Tip(NamedTuple): class Tip(BaseModel):
"""A Tip represents a single donation""" """A Tip represents a single donation"""
id: str # This ID always corresponds to a satspay charge ID id: str # This ID always corresponds to a satspay charge ID
@@ -55,7 +55,7 @@ class createTips(BaseModel):
message: str message: str
class TipJar(NamedTuple): class TipJar(BaseModel):
"""A TipJar represents a user's tip jar""" """A TipJar represents a user's tip jar"""
id: int id: int
@@ -222,6 +222,7 @@
'INR', 'INR',
'IQD', 'IQD',
'IRR', 'IRR',
'IRT',
'ISK', 'ISK',
'JEP', 'JEP',
'JMD', '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( async def api_update_mempool(
endpoint: str = Query(...), w: WalletTypeInfo = Depends(require_admin_key) 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() return mempool.dict()
+5
View File
@@ -71,6 +71,11 @@ async def api_link_create_or_update(
link_id: str = None, link_id: str = None,
wallet: WalletTypeInfo = Depends(require_admin_key), 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: if data.max_withdrawable < data.min_withdrawable:
raise HTTPException( raise HTTPException(
detail="`max_withdrawable` needs to be at least `min_withdrawable`.", 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] ["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["SITE_TITLE"] = settings.LNBITS_SITE_TITLE
t.env.globals["LNBITS_DENOMINATION"] = settings.LNBITS_DENOMINATION t.env.globals["LNBITS_DENOMINATION"] = settings.LNBITS_DENOMINATION
t.env.globals["SITE_TAGLINE"] = settings.LNBITS_SITE_TAGLINE 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_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_SITE_TITLE = env.str("LNBITS_SITE_TITLE", default="LNbits")
LNBITS_DENOMINATION = env.str("LNBITS_DENOMINATION", default="sats") LNBITS_DENOMINATION = env.str("LNBITS_DENOMINATION", default="sats")
LNBITS_SITE_TAGLINE = env.str( 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", "IMP": "Isle of Man Pound",
"INR": "Indian Rupee", "INR": "Indian Rupee",
"IQD": "Iraqi Dinar", "IQD": "Iraqi Dinar",
"IRT": "Iranian Toman",
"ISK": "Icelandic Króna", "ISK": "Icelandic Króna",
"JEP": "Jersey Pound", "JEP": "Jersey Pound",
"JMD": "Jamaican Dollar", "JMD": "Jamaican Dollar",
@@ -179,6 +180,12 @@ class Provider(NamedTuple):
exchange_rate_providers = { 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": Provider(
"Bitfinex", "Bitfinex",
"bitfinex.com", "bitfinex.com",
-1
View File
@@ -2,7 +2,6 @@
from .void import VoidWallet from .void import VoidWallet
from .clightning import CLightningWallet from .clightning import CLightningWallet
from .lndgrpc import LndWallet
from .lntxbot import LntxbotWallet from .lntxbot import LntxbotWallet
from .opennode import OpenNodeWallet from .opennode import OpenNodeWallet
from .lnpay import LNPayWallet from .lnpay import LNPayWallet
+3 -1
View File
@@ -60,7 +60,9 @@ class Wallet(ABC):
pass pass
@abstractmethod @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 pass
@abstractmethod @abstractmethod
+13 -4
View File
@@ -18,6 +18,7 @@ from .base import (
Unsupported, Unsupported,
Wallet, Wallet,
) )
from lnbits import bolt11 as lnbits_bolt11
def async_wrap(func): def async_wrap(func):
@@ -31,8 +32,8 @@ def async_wrap(func):
return run return run
def _pay_invoice(ln, bolt11): def _pay_invoice(ln, payload):
return ln.pay(bolt11) return ln.call("pay", payload)
def _paid_invoices_stream(ln, last_pay_index): 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}'." error_message = f"lightningd '{exc.method}' failed with '{exc.error}'."
return InvoiceResponse(False, label, None, error_message) 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: try:
wrapped = async_wrap(_pay_invoice) wrapped = async_wrap(_pay_invoice)
r = await wrapped(self.ln, bolt11) r = await wrapped(self.ln, payload)
except RpcError as exc: except RpcError as exc:
return PaymentResponse(False, None, 0, None, str(exc)) return PaymentResponse(False, None, 0, None, str(exc))
+22 -9
View File
@@ -36,7 +36,13 @@ class FakeWallet(Wallet):
"out": False, "out": False,
"amount": amount, "amount": amount,
"currency": "bc", "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, "memo": None,
"description_hash": None, "description_hash": None,
"description": "", "description": "",
@@ -53,22 +59,29 @@ class FakeWallet(Wallet):
data["tags_set"] = ["d"] data["tags_set"] = ["d"]
data["memo"] = memo data["memo"] = memo
data["description"] = memo data["description"] = memo
randomHash = data["privkey"][:6] + hashlib.sha256( randomHash = (
str(random.getrandbits(256)).encode("utf-8") data["privkey"][:6]
).hexdigest()[6:] + hashlib.sha256(str(random.getrandbits(256)).encode("utf-8")).hexdigest()[
6:
]
)
data["paymenthash"] = randomHash data["paymenthash"] = randomHash
payment_request = encode(data) payment_request = encode(data)
checking_id = randomHash checking_id = randomHash
return InvoiceResponse(True, checking_id, payment_request) return InvoiceResponse(True, checking_id, payment_request)
async def pay_invoice(self, bolt11: str) -> PaymentResponse: async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse:
invoice = decode(bolt11) invoice = decode(bolt11)
if hasattr(invoice, 'checking_id') and invoice.checking_id[6:] == data["privkey"][:6]: if (
hasattr(invoice, "checking_id")
and invoice.checking_id[6:] == data["privkey"][:6]
):
return PaymentResponse(True, invoice.payment_hash, 0) return PaymentResponse(True, invoice.payment_hash, 0)
else: 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: async def get_invoice_status(self, checking_id: str) -> PaymentStatus:
return PaymentStatus(False) return PaymentStatus(False)
+1 -1
View File
@@ -80,7 +80,7 @@ class LNbitsWallet(Wallet):
return InvoiceResponse(ok, checking_id, payment_request, error_message) 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: async with httpx.AsyncClient() as client:
r = await client.post( r = await client.post(
url=f"{self.endpoint}/api/v1/payments", 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_GRPC_INVOICE_MACAROON")
or getenv("LND_INVOICE_MACAROON") or getenv("LND_INVOICE_MACAROON")
) )
encrypted_macaroon = getenv("LND_GRPC_MACAROON_ENCRYPTED") encrypted_macaroon = getenv("LND_GRPC_MACAROON_ENCRYPTED")
if encrypted_macaroon: 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.macaroon = load_macaroon(macaroon)
cert = open(self.cert_path, "rb").read() cert = open(self.cert_path, "rb").read()
@@ -143,10 +144,10 @@ class LndWallet(Wallet):
payment_request = str(resp.payment_request) payment_request = str(resp.payment_request)
return InvoiceResponse(True, checking_id, payment_request, None) 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:
resp = await self.rpc.SendPayment( fee_limit_fixed = ln.FeeLimit(fixed=fee_limit_msat // 1000)
lnrpc.SendPaymentRequest(payment_request=bolt11) req = ln.SendRequest(payment_request=bolt11, fee_limit=fee_limit_fixed)
) resp = await self.rpc.SendPaymentSync(req)
if resp.payment_error: if resp.payment_error:
return PaymentResponse(False, "", 0, None, 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") encrypted_macaroon = getenv("LND_REST_MACAROON_ENCRYPTED")
if encrypted_macaroon: 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.macaroon = load_macaroon(macaroon)
self.auth = {"Grpc-Metadata-macaroon": self.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: async def status(self) -> StatusResponse:
try: try:
@@ -97,15 +99,11 @@ class LndRestWallet(Wallet):
return InvoiceResponse(True, checking_id, payment_request, None) 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: async with httpx.AsyncClient(verify=self.cert) as client:
# set the fee limit for the payment # set the fee limit for the payment
invoice = lnbits_bolt11.decode(bolt11)
lnrpcFeeLimit = dict() lnrpcFeeLimit = dict()
if invoice.amount_msat > 1000_000: lnrpcFeeLimit["fixed_msat"] = "{}".format(fee_limit_msat)
lnrpcFeeLimit["percent"] = "1" # in percent
else:
lnrpcFeeLimit["fixed"] = "10" # in sat
r = await client.post( r = await client.post(
url=f"{self.endpoint}/v1/channels/transactions", 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 # for some reason our checking_ids are in base64 but the payment hashes
# returned here are in hex, lnd is weird # returned here are in hex, lnd is weird
checking_id = checking_id.replace("_", "/")
checking_id = base64.b64decode(checking_id).hex() checking_id = base64.b64decode(checking_id).hex()
for p in r.json()["payments"]: 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) 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: async with httpx.AsyncClient() as client:
r = await client.post( r = await client.post(
f"{self.endpoint}/wallet/{self.wallet_key}/withdraw", f"{self.endpoint}/wallet/{self.wallet_key}/withdraw",
+1 -1
View File
@@ -74,7 +74,7 @@ class LntxbotWallet(Wallet):
data = r.json() data = r.json()
return InvoiceResponse(True, data["payment_hash"], data["pay_req"], None) 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: async with httpx.AsyncClient() as client:
r = await client.post( r = await client.post(
f"{self.endpoint}/payinvoice", f"{self.endpoint}/payinvoice",
+1 -1
View File
@@ -77,7 +77,7 @@ class OpenNodeWallet(Wallet):
payment_request = data["lightning_invoice"]["payreq"] payment_request = data["lightning_invoice"]["payreq"]
return InvoiceResponse(True, checking_id, payment_request, None) 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: async with httpx.AsyncClient() as client:
r = await client.post( r = await client.post(
f"{self.endpoint}/v2/withdrawals", 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) 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: try:
r = await self.pay(bolt11) r = await self.pay(bolt11)
except (SparkError, UnknownError) as exc: except (SparkError, UnknownError) as exc:
+1 -1
View File
@@ -25,7 +25,7 @@ class VoidWallet(Wallet):
) )
return StatusResponse(None, 0) 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("") raise Unsupported("")
async def get_invoice_status(self, checking_id: str) -> PaymentStatus: 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.core.crud import get_wallet
from lnbits.settings import HOST, PORT from lnbits.settings import HOST, PORT
from lnbits.extensions.bleskomat.crud import get_bleskomat_lnurl 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.conftest import client
from tests.helpers import credit_wallet from tests.helpers import credit_wallet
from tests.extensions.bleskomat.conftest import bleskomat, lnurl 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) payload = query_to_signing_payload(query)
signature = generate_bleskomat_lnurl_signature( 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}") response = await client.get(f"/bleskomat/u?{payload}&signature={signature}")
assert response.status_code == 200 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}") response = await client.get(f"/bleskomat/u?k1={secret}&pr={pr}")
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["status"] == "ERROR" 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) wallet = await get_wallet(bleskomat.wallet)
assert wallet.balance_msat == 0 assert wallet.balance_msat == 0
bleskomat_lnurl = await get_bleskomat_lnurl(secret) 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 assert wallet.balance_msat == 50000
bleskomat_lnurl = await get_bleskomat_lnurl(secret) bleskomat_lnurl = await get_bleskomat_lnurl(secret)
assert bleskomat_lnurl.has_uses_remaining() == False 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 from lnbits.settings import WALLET
WALLET.status = AsyncMock(return_value=StatusResponse( WALLET.status = AsyncMock(
"",# no error return_value=StatusResponse(
1000000,# msats "", # no error
)) 1000000, # msats
WALLET.create_invoice = AsyncMock(return_value=InvoiceResponse( )
True,# ok )
"6621aafbdd7709ca6eea6203f362d64bd7cb2911baa91311a176b3ecaf2274bd",# checking_id (i.e. payment_hash) WALLET.create_invoice = AsyncMock(
"lntb1u1psezhgspp5vcs6477awuyu5mh2vgplxckkf0tuk2g3h253xydpw6e7etezwj7sdqqcqzpgxqyz5vqsp5dxpw8zs77hw5pla4wz4mfujllyxtlpu443auur2uxqdrs8q2h56q9qyyssq65zk30ylmygvv5y4tuwalnf3ttnqjn57ef6rmcqg0s53akem560jh8ptemjcmytn3lrlatw4hv9smg88exv3v4f4lqnp96s0psdrhxsp6pp75q",# payment_request return_value=InvoiceResponse(
"",# no error True, # ok
)) "6621aafbdd7709ca6eea6203f362d64bd7cb2911baa91311a176b3ecaf2274bd", # checking_id (i.e. payment_hash)
def pay_invoice_side_effect(payment_request: str): "lntb1u1psezhgspp5vcs6477awuyu5mh2vgplxckkf0tuk2g3h253xydpw6e7etezwj7sdqqcqzpgxqyz5vqsp5dxpw8zs77hw5pla4wz4mfujllyxtlpu443auur2uxqdrs8q2h56q9qyyssq65zk30ylmygvv5y4tuwalnf3ttnqjn57ef6rmcqg0s53akem560jh8ptemjcmytn3lrlatw4hv9smg88exv3v4f4lqnp96s0psdrhxsp6pp75q", # payment_request
"", # no error
)
)
def pay_invoice_side_effect(
payment_request: str, fee_limit_msat: int
) -> PaymentResponse:
invoice = bolt11.decode(payment_request) invoice = bolt11.decode(payment_request)
return PaymentResponse( return PaymentResponse(
True,# ok True, # ok
invoice.payment_hash,# checking_id (i.e. payment_hash) invoice.payment_hash, # checking_id (i.e. payment_hash)
0,# fee_msat 0, # fee_msat
"",# no error "", # no error
) )
WALLET.pay_invoice = AsyncMock(side_effect=pay_invoice_side_effect) WALLET.pay_invoice = AsyncMock(side_effect=pay_invoice_side_effect)
WALLET.get_invoice_status = AsyncMock(return_value=PaymentStatus( WALLET.get_invoice_status = AsyncMock(
True,# paid return_value=PaymentStatus(
)) True, # paid
WALLET.get_payment_status = AsyncMock(return_value=PaymentStatus( )
True,# paid )
)) WALLET.get_payment_status = AsyncMock(
return_value=PaymentStatus(
True, # paid
)
)