Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
810b918619 | ||
|
|
81860bdc5d | ||
|
|
c5c76468cb | ||
|
|
799065e982 | ||
|
|
a94444ae2c | ||
|
|
54264315f0 | ||
|
|
946ebc350e | ||
|
|
b1c75813a0 | ||
|
|
a17108f108 | ||
|
|
1209526147 | ||
|
|
0f97f8f18b | ||
|
|
911fe92e03 | ||
|
|
eb8bbd3896 | ||
|
|
ff70efd1eb | ||
|
|
ce6620882f | ||
|
|
ffba97850b | ||
|
|
435b4056fd | ||
|
|
30a0d6336b | ||
|
|
52ef2da76d | ||
|
|
e4dd0972a5 | ||
|
|
c708cb1e2b | ||
|
|
43f9394014 | ||
|
|
9bbf78e026 | ||
|
|
6df951b873 | ||
|
|
d62ef0bb80 | ||
|
|
4a9e7f4264 | ||
|
|
b886e5b0ea | ||
|
|
72f2ec0be8 | ||
|
|
ef6d12fe10 | ||
|
|
1e759283ce | ||
|
|
e92447092d | ||
|
|
7988e2c303 | ||
|
|
a13bb9cfea | ||
|
|
7088a3240f | ||
|
|
00e52001b5 | ||
|
|
cd35966a56 | ||
|
|
c59a65cdb4 | ||
|
|
5f52658626 | ||
|
|
2399b05855 | ||
|
|
8fb70041d2 | ||
|
|
baf55afc28 | ||
|
|
251e6a8233 | ||
|
|
d73c3950b9 | ||
|
|
ac61cd5427 | ||
|
|
0c14b8fb82 | ||
|
|
6c0aa8fa8d | ||
|
|
9b95a7743d | ||
|
|
e96701cb59 | ||
|
|
c1fa5bbcbf | ||
|
|
241c81c790 | ||
|
|
42b444f54d | ||
|
|
f7ff9d27d2 | ||
|
|
4a588dda1b | ||
|
|
e82ce89e9a | ||
|
|
270cac37ac | ||
|
|
a374526faf | ||
|
|
8930152b0f | ||
|
|
71e11b7387 | ||
|
|
510a28f3d0 | ||
|
|
c58e2c5d74 | ||
|
|
eb7b369b6d | ||
|
|
a5a3331106 | ||
|
|
4f3e48e280 | ||
|
|
31bcb32e2d | ||
|
|
a132387fa8 | ||
|
|
27694ee23d | ||
|
|
36952981c2 | ||
|
|
fbafe2b8f1 | ||
|
|
4793ea0d18 |
+4
-9
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" ./
|
|
||||||
@@ -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
@@ -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"]
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
+5
-4
@@ -84,15 +84,16 @@ 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():
|
||||||
|
while True:
|
||||||
error_message, balance = await WALLET.status()
|
error_message, balance = await WALLET.status()
|
||||||
if error_message:
|
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."
|
||||||
)
|
)
|
||||||
|
|||||||
+16
-20
@@ -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:
|
||||||
raise PermissionError("Insufficient balance.")
|
if not internal_checking_id and wallet.balance_msat > -fee_reserve_msat:
|
||||||
|
raise PaymentFailure(
|
||||||
# do the balance check if external payment
|
f"You must reserve at least 1% ({round(fee_reserve_msat/1000)} sat) to cover potential routing fees."
|
||||||
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))
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -273,6 +273,10 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% 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">
|
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
@@ -288,12 +292,16 @@
|
|||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
|
|
||||||
{% if wallet.lnurlwithdraw_full %}
|
{% if wallet.lnurlwithdraw_full %}
|
||||||
<q-expansion-item group="extras" icon="crop_free" label="Drain Funds">
|
<q-expansion-item
|
||||||
|
group="extras"
|
||||||
|
icon="crop_free"
|
||||||
|
label="Drain Funds"
|
||||||
|
>
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section class="text-center">
|
<q-card-section class="text-center">
|
||||||
<p>
|
<p>
|
||||||
This is an LNURL-withdraw QR code for slurping everything from
|
This is an LNURL-withdraw QR code for slurping everything
|
||||||
this wallet. Do not share with anyone.
|
from this wallet. Do not share with anyone.
|
||||||
</p>
|
</p>
|
||||||
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
|
<a href="lightning:{{wallet.lnurlwithdraw_full}}">
|
||||||
<qrcode
|
<qrcode
|
||||||
@@ -303,8 +311,8 @@
|
|||||||
</a>
|
</a>
|
||||||
<p>
|
<p>
|
||||||
It is compatible with <code>balanceCheck</code> and
|
It is compatible with <code>balanceCheck</code> and
|
||||||
<code>balanceNotify</code> so your wallet may keep pulling the
|
<code>balanceNotify</code> so your wallet may keep pulling
|
||||||
funds continuously from here after the first withdraw.
|
the funds continuously from here after the first withdraw.
|
||||||
</p>
|
</p>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -378,10 +386,17 @@
|
|||||||
</q-list>
|
</q-list>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</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>
|
||||||
</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"
|
||||||
@@ -447,7 +462,9 @@
|
|||||||
</span>
|
</span>
|
||||||
<span v-else> Create invoice </span>
|
<span v-else> Create invoice </span>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<q-spinner
|
<q-spinner
|
||||||
v-if="receive.status == 'loading'"
|
v-if="receive.status == 'loading'"
|
||||||
@@ -476,14 +493,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
{% endraw %}
|
{% endraw %}
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
<q-dialog v-model="parse.show" @hide="closeParseDialog">
|
<q-dialog v-model="parse.show" @hide="closeParseDialog">
|
||||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
<div v-if="parse.invoice">
|
<div v-if="parse.invoice">
|
||||||
<h6 v-if="'{{LNBITS_DENOMINATION}}' != 'sats'" class="q-my-none">
|
<h6 v-if="'{{LNBITS_DENOMINATION}}' != 'sats'" class="q-my-none">
|
||||||
{% raw %} {{ parseFloat(String(parse.invoice.fsat).replaceAll(",", ""))
|
{% raw %} {{ parseFloat(String(parse.invoice.fsat).replaceAll(",",
|
||||||
/ 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
|
"")) / 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
|
||||||
</h6>
|
</h6>
|
||||||
<h6 v-else class="q-my-none">
|
<h6 v-else class="q-my-none">
|
||||||
{{ parse.invoice.fsat }}{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
|
{{ parse.invoice.fsat }}{% endraw %} {{LNBITS_DENOMINATION}} {% raw %}
|
||||||
@@ -497,13 +514,17 @@
|
|||||||
{% endraw %}
|
{% endraw %}
|
||||||
<div v-if="canPay" class="row q-mt-lg">
|
<div v-if="canPay" class="row q-mt-lg">
|
||||||
<q-btn unelevated color="primary" @click="payInvoice">Pay</q-btn>
|
<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>
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="row q-mt-lg">
|
<div v-else class="row q-mt-lg">
|
||||||
<q-btn unelevated disabled color="yellow" text-color="black"
|
<q-btn unelevated disabled color="yellow" text-color="black"
|
||||||
>Not enough funds!</q-btn
|
>Not enough funds!</q-btn
|
||||||
>
|
>
|
||||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
|
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
|
||||||
|
>Cancel</q-btn
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="parse.lnurlauth">
|
<div v-else-if="parse.lnurlauth">
|
||||||
@@ -516,8 +537,8 @@
|
|||||||
<p>
|
<p>
|
||||||
For every website and for every LNbits wallet, a new keypair will be
|
For every website and for every LNbits wallet, a new keypair will be
|
||||||
deterministically generated so your identity can't be tied to your
|
deterministically generated so your identity can't be tied to your
|
||||||
LNbits wallet or linked across websites. No other data will be shared
|
LNbits wallet or linked across websites. No other data will be
|
||||||
with {{ parse.lnurlauth.domain }}.
|
shared with {{ parse.lnurlauth.domain }}.
|
||||||
</p>
|
</p>
|
||||||
<p>Your public key for <b>{{ parse.lnurlauth.domain }}</b> is:</p>
|
<p>Your public key for <b>{{ parse.lnurlauth.domain }}</b> is:</p>
|
||||||
<p class="q-mx-xl">
|
<p class="q-mx-xl">
|
||||||
@@ -537,7 +558,8 @@
|
|||||||
<q-form @submit="payLnurl" class="q-gutter-md">
|
<q-form @submit="payLnurl" class="q-gutter-md">
|
||||||
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
|
<p v-if="parse.lnurlpay.fixed" class="q-my-none text-h6">
|
||||||
<b>{{ parse.lnurlpay.domain }}</b> is requesting {{
|
<b>{{ parse.lnurlpay.domain }}</b> is requesting {{
|
||||||
parse.lnurlpay.maxSendable | msatoshiFormat }} {{LNBITS_DENOMINATION}}
|
parse.lnurlpay.maxSendable | msatoshiFormat }}
|
||||||
|
{{LNBITS_DENOMINATION}}
|
||||||
<span v-if="parse.lnurlpay.commentAllowed > 0">
|
<span v-if="parse.lnurlpay.commentAllowed > 0">
|
||||||
<br />
|
<br />
|
||||||
and a {{parse.lnurlpay.commentAllowed}}-char comment
|
and a {{parse.lnurlpay.commentAllowed}}-char comment
|
||||||
@@ -642,12 +664,15 @@
|
|||||||
</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
|
||||||
|
@decode="decodeQR"
|
||||||
|
class="rounded-borders"
|
||||||
|
></qrcode-stream>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-mt-lg">
|
<div class="row q-mt-lg">
|
||||||
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
|
<q-btn @click="closeCamera" flat color="grey" class="q-ml-auto"
|
||||||
@@ -655,20 +680,20 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</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
|
<q-tab
|
||||||
icon="account_balance_wallet"
|
icon="account_balance_wallet"
|
||||||
label="Wallets"
|
label="Wallets"
|
||||||
@@ -680,10 +705,10 @@
|
|||||||
</q-tab>
|
</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>
|
||||||
@@ -695,10 +720,10 @@
|
|||||||
</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
|
||||||
@@ -712,5 +737,6 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
{% endif %} {% endblock %}
|
{% endif %} {% endblock %}
|
||||||
|
</div>
|
||||||
|
|||||||
+32
-15
@@ -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
|
||||||
|
|||||||
@@ -130,9 +130,15 @@ class Database(Compat):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
if os.path.isdir(LNBITS_DATA_FOLDER):
|
||||||
self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3")
|
self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3")
|
||||||
database_uri = f"sqlite:///{self.path}"
|
database_uri = f"sqlite:///{self.path}"
|
||||||
self.type = SQLITE
|
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_"):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +117,7 @@
|
|||||||
{% 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 }}
|
||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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: ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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`.",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 |
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
+21
-8
@@ -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)
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -93,10 +93,11 @@ class LndWallet(Wallet):
|
|||||||
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)
|
||||||
|
|||||||
@@ -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"]:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user